@gscdump/analysis 3.8.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,26 @@
1
1
  import { queriesQueryState } from "../analyzer/adapt-rows.mjs";
2
+ import { paginateClause, paginateInMemory, resolveSort } from "../analyzer/paginate.mjs";
2
3
  import { parseJsonRows, rowString } from "../analyzer/row-values.mjs";
3
- import { buildPeriodMap } from "../types.mjs";
4
4
  import { percentDifference } from "../scoring.mjs";
5
- import { num } from "@gscdump/engine/analysis-types";
5
+ import { fetchBudgetOf, num } from "@gscdump/engine/analysis-types";
6
6
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
7
7
  import { comparisonOf } from "@gscdump/engine/period";
8
8
  import { enumeratePartitions } from "@gscdump/engine/planner";
9
9
  import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
10
+ const MOVERS_SORT_METRICS = [
11
+ "clicks",
12
+ "impressions",
13
+ "clicksDelta",
14
+ "clicksDeltaPercent",
15
+ "impressionsDelta",
16
+ "impressionsDeltaPercent",
17
+ "positionDelta"
18
+ ];
19
+ const DEFAULT_SORT = {
20
+ sortBy: "clicksDelta",
21
+ sortDir: "desc"
22
+ };
23
+ const DEFAULT_MOVERS_LIMIT = 2e3;
10
24
  function withCanonicalFields(r) {
11
25
  return {
12
26
  ...r,
@@ -19,90 +33,112 @@ function withCanonicalFields(r) {
19
33
  prevPosition: r.baselinePosition
20
34
  };
21
35
  }
22
- function selectMoversDirection(rising, declining, stable, params) {
23
- if (params.direction) {
24
- const selected = params.direction === "rising" ? rising : declining;
25
- const total = selected.length;
26
- const offset = params.offset ?? 0;
27
- const limit = params.limit ?? total;
28
- return {
29
- results: selected.slice(offset, offset + limit),
30
- meta: {
31
- total,
32
- rising: rising.length,
33
- declining: declining.length,
34
- stable: stable.length
35
- }
36
- };
36
+ function sortValue(row, sortBy) {
37
+ switch (sortBy) {
38
+ case "clicks": return row.recentClicks;
39
+ case "impressions": return row.recentImpressions;
40
+ case "clicksDelta": return Math.abs(row.clicksChange);
41
+ case "clicksDeltaPercent": return Math.abs(row.clicksChangePercent);
42
+ case "impressionsDelta": return Math.abs(row.impressionsChange);
43
+ case "impressionsDeltaPercent": return Math.abs(row.impressionsChangePercent);
44
+ case "positionDelta": return Math.abs(row.positionChange ?? 0);
37
45
  }
38
- const combined = [...rising, ...declining];
46
+ }
47
+ function moversComparator(sortBy, sortDir) {
48
+ const mult = sortDir === "asc" ? 1 : -1;
49
+ return (a, b) => (sortValue(a, sortBy) - sortValue(b, sortBy)) * mult || a.keyword.localeCompare(b.keyword) || (a.page ?? "").localeCompare(b.page ?? "");
50
+ }
51
+ const SQL_SORT_EXPR = {
52
+ clicks: "recentClicks",
53
+ impressions: "recentImpressions",
54
+ clicksDelta: "ABS(clicksChange)",
55
+ clicksDeltaPercent: "ABS(clicksChangePercent)",
56
+ impressionsDelta: "ABS(impressionsChange)",
57
+ impressionsDeltaPercent: "ABS(impressionsChangePercent)",
58
+ positionDelta: "ABS(COALESCE(positionChange, 0))"
59
+ };
60
+ function selectMoversDirection(result, params, compare) {
61
+ const tag = (direction) => (r) => withCanonicalFields({
62
+ ...r,
63
+ direction
64
+ });
65
+ const selected = params.direction === "rising" ? result.rising.map(tag("rising")) : params.direction === "declining" ? result.declining.map(tag("declining")) : [...result.rising.map(tag("rising")), ...result.declining.map(tag("declining"))].sort(compare);
66
+ const paged = paginateInMemory(selected, {
67
+ limit: params.limit ?? DEFAULT_MOVERS_LIMIT,
68
+ offset: params.offset
69
+ });
39
70
  return {
40
- results: combined,
71
+ results: paged,
41
72
  meta: {
42
- total: combined.length,
43
- rising: rising.length,
44
- declining: declining.length,
45
- stable: stable.length
73
+ total: selected.length,
74
+ returned: paged.length,
75
+ rising: result.rising.length,
76
+ declining: result.declining.length,
77
+ stable: result.stable.length
46
78
  }
47
79
  };
48
80
  }
81
+ function pairKey(query, page) {
82
+ return `${query}\u0000${page ?? ""}`;
83
+ }
84
+ function periodMap(rows, factor) {
85
+ const out = /* @__PURE__ */ new Map();
86
+ for (const row of rows) {
87
+ const page = row.page ?? null;
88
+ out.set(pairKey(row.query, page), {
89
+ query: row.query,
90
+ page,
91
+ clicks: num(row.clicks) / factor,
92
+ impressions: num(row.impressions) / factor,
93
+ position: num(row.position)
94
+ });
95
+ }
96
+ return out;
97
+ }
49
98
  function analyzeMovers(input, options = {}) {
50
- const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
51
- const normFactor = input.normalizationFactor ?? 1;
52
- const baselineMap = buildPeriodMap(input.previous, (r) => r.query, (r) => ({
53
- clicks: num(r.clicks) / normFactor,
54
- impressions: num(r.impressions) / normFactor,
55
- position: num(r.position),
56
- page: r.page ?? null
57
- }));
58
- const pageMap = /* @__PURE__ */ new Map();
59
- for (const row of input.current) if (!pageMap.has(row.query) && row.page) pageMap.set(row.query, row.page);
60
- for (const row of input.previous) if (!pageMap.has(row.query) && row.page) pageMap.set(row.query, row.page);
99
+ const { changeThreshold = .2, minImpressions = 50, sortBy = DEFAULT_SORT.sortBy, sortDir = DEFAULT_SORT.sortDir } = options;
100
+ const current = periodMap(input.current, 1);
101
+ const previous = periodMap(input.previous, input.normalizationFactor ?? 1);
61
102
  const rising = [];
62
103
  const declining = [];
63
104
  const stable = [];
64
- for (const row of input.current) {
65
- const impressions = num(row.impressions);
66
- const clicks = num(row.clicks);
67
- const position = num(row.position);
68
- if (impressions < minImpressions) continue;
69
- const baseline = baselineMap.get(row.query);
70
- const baselineClicksRaw = baseline?.clicks ?? 0;
71
- const baselineImpressionsRaw = baseline?.impressions ?? 0;
72
- const baselinePosition = baseline?.position ?? null;
105
+ for (const key of /* @__PURE__ */ new Set([...current.keys(), ...previous.keys()])) {
106
+ const cur = current.get(key);
107
+ const prev = previous.get(key);
108
+ const identity = cur ?? prev;
109
+ const clicks = cur?.clicks ?? 0;
110
+ const impressions = cur?.impressions ?? 0;
111
+ const baselineClicksRaw = prev?.clicks ?? 0;
112
+ const baselineImpressionsRaw = prev?.impressions ?? 0;
113
+ if (Math.max(impressions, baselineImpressionsRaw) < minImpressions) continue;
114
+ const recentPosition = cur?.position ?? null;
115
+ const baselinePosition = prev?.position ?? null;
116
+ const baselineClicks = Math.round(baselineClicksRaw);
117
+ const baselineImpressions = Math.round(baselineImpressionsRaw);
73
118
  const clicksChangePercent = percentDifference(clicks, baselineClicksRaw);
74
- const impressionsChangePercent = percentDifference(impressions, baselineImpressionsRaw);
75
119
  const data = {
76
- keyword: row.query,
77
- page: pageMap.get(row.query) ?? null,
120
+ keyword: identity.query,
121
+ page: identity.page,
78
122
  recentClicks: clicks,
79
123
  recentImpressions: impressions,
80
- recentPosition: position,
81
- baselineClicks: Math.round(baselineClicksRaw),
82
- baselineImpressions: Math.round(baselineImpressionsRaw),
124
+ recentPosition,
125
+ baselineClicks,
126
+ baselineImpressions,
83
127
  baselinePosition,
84
- clicksChange: clicks - Math.round(baselineClicksRaw),
128
+ clicksChange: clicks - baselineClicks,
85
129
  clicksChangePercent,
86
- impressionsChangePercent,
87
- positionChange: baselinePosition != null ? position - baselinePosition : null
130
+ impressionsChange: impressions - baselineImpressions,
131
+ impressionsChangePercent: percentDifference(impressions, baselineImpressionsRaw),
132
+ positionChange: recentPosition != null && baselinePosition != null ? recentPosition - baselinePosition : null
88
133
  };
89
134
  const absChange = Math.abs(clicksChangePercent / 100);
90
135
  if (clicksChangePercent > 0 && absChange >= changeThreshold) rising.push(data);
91
136
  else if (clicksChangePercent < 0 && absChange >= changeThreshold) declining.push(data);
92
137
  else stable.push(data);
93
138
  }
94
- const sortFn = (a, b) => {
95
- switch (sortBy) {
96
- case "clicks": return b.recentClicks - a.recentClicks;
97
- case "impressions": return b.recentImpressions - a.recentImpressions;
98
- case "clicksChange": return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
99
- case "impressionsChange": return Math.abs(b.impressionsChangePercent) - Math.abs(a.impressionsChangePercent);
100
- case "positionChange": return Math.abs(b.positionChange ?? 0) - Math.abs(a.positionChange ?? 0);
101
- default: return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
102
- }
103
- };
104
- rising.sort(sortFn);
105
- declining.sort(sortFn);
139
+ const compare = moversComparator(sortBy, sortDir);
140
+ rising.sort(compare);
141
+ declining.sort(compare);
106
142
  stable.sort((a, b) => b.recentClicks - a.recentClicks);
107
143
  return {
108
144
  rising,
@@ -110,13 +146,18 @@ function analyzeMovers(input, options = {}) {
110
146
  stable
111
147
  };
112
148
  }
149
+ function resolveMoversSort(params) {
150
+ return resolveSort(params, MOVERS_SORT_METRICS, DEFAULT_SORT);
151
+ }
113
152
  const moversAnalyzer = defineAnalyzer({
114
153
  id: "movers",
115
154
  buildSql(params) {
116
155
  const { current: cur, previous: prev } = comparisonOf(params);
117
156
  const minImpressions = params.minImpressions ?? 50;
118
157
  const changeThreshold = params.changeThreshold ?? .2;
119
- const limit = params.direction ? 5e3 : params.limit ?? 2e3;
158
+ const { sortBy, sortDir } = resolveMoversSort(params);
159
+ const orderBy = `${SQL_SORT_EXPR[sortBy]} ${sortDir === "asc" ? "ASC" : "DESC"}, keyword ASC, page ASC`;
160
+ const directions = params.direction === "rising" ? `('rising')` : params.direction === "declining" ? `('declining')` : `('rising', 'declining')`;
120
161
  return {
121
162
  sql: `
122
163
  WITH cur AS (
@@ -139,6 +180,67 @@ const moversAnalyzer = defineAnalyzer({
139
180
  WHERE date >= ? AND date <= ?
140
181
  GROUP BY query, url
141
182
  ),
183
+ joined AS (
184
+ SELECT
185
+ COALESCE(c.query, p.query) AS keyword,
186
+ COALESCE(c.url, p.url) AS page,
187
+ COALESCE(c.clicks, 0.0) AS recentClicks,
188
+ COALESCE(c.impressions, 0.0) AS recentImpressions,
189
+ c.position AS recentPosition,
190
+ COALESCE(p.clicks, 0.0) AS baselineClicks,
191
+ COALESCE(p.impressions, 0.0) AS baselineImpressions,
192
+ p.position AS baselinePosition
193
+ FROM cur c
194
+ FULL OUTER JOIN prev p ON c.query = p.query AND c.url = p.url
195
+ WHERE GREATEST(COALESCE(c.impressions, 0.0), COALESCE(p.impressions, 0.0)) >= ?
196
+ ),
197
+ derived AS (
198
+ SELECT
199
+ *,
200
+ (recentClicks - baselineClicks) AS clicksChange,
201
+ CASE
202
+ WHEN baselineClicks = 0 THEN CASE WHEN recentClicks > 0 THEN 100.0 ELSE 0.0 END
203
+ ELSE (recentClicks - baselineClicks) * 100.0 / baselineClicks
204
+ END AS clicksChangePercent,
205
+ (recentImpressions - baselineImpressions) AS impressionsChange,
206
+ CASE
207
+ WHEN baselineImpressions = 0 THEN CASE WHEN recentImpressions > 0 THEN 100.0 ELSE 0.0 END
208
+ ELSE (recentImpressions - baselineImpressions) * 100.0 / baselineImpressions
209
+ END AS impressionsChangePercent,
210
+ CASE
211
+ WHEN recentPosition IS NOT NULL AND baselinePosition IS NOT NULL THEN (recentPosition - baselinePosition)
212
+ ELSE NULL
213
+ END AS positionChange
214
+ FROM joined
215
+ ),
216
+ classified AS (
217
+ SELECT
218
+ *,
219
+ CASE
220
+ WHEN clicksChangePercent > 0 AND ABS(clicksChangePercent) / 100.0 >= ? THEN 'rising'
221
+ WHEN clicksChangePercent < 0 AND ABS(clicksChangePercent) / 100.0 >= ? THEN 'declining'
222
+ ELSE 'stable'
223
+ END AS direction
224
+ FROM derived
225
+ ),
226
+ counted AS (
227
+ SELECT
228
+ *,
229
+ CAST(SUM(CASE WHEN direction = 'rising' THEN 1 ELSE 0 END) OVER () AS DOUBLE) AS risingCount,
230
+ CAST(SUM(CASE WHEN direction = 'declining' THEN 1 ELSE 0 END) OVER () AS DOUBLE) AS decliningCount,
231
+ CAST(SUM(CASE WHEN direction = 'stable' THEN 1 ELSE 0 END) OVER () AS DOUBLE) AS stableCount
232
+ FROM classified
233
+ ),
234
+ selected AS (
235
+ SELECT *
236
+ FROM counted
237
+ WHERE direction IN ${directions}
238
+ ORDER BY ${orderBy}
239
+ ${paginateClause({
240
+ limit: params.limit ?? DEFAULT_MOVERS_LIMIT,
241
+ offset: params.offset
242
+ })}
243
+ ),
142
244
  weekly AS (
143
245
  SELECT query, url, date_trunc('week', CAST(date AS DATE)) AS week,
144
246
  ${METRIC_EXPR.clicks} AS clicks,
@@ -151,7 +253,8 @@ const moversAnalyzer = defineAnalyzer({
151
253
  SELECT query, url, date, clicks, impressions
152
254
  FROM read_parquet({{FILES_PREV}}, union_by_name = true)
153
255
  WHERE date >= ? AND date <= ?
154
- )
256
+ ) u
257
+ SEMI JOIN selected ON u.query = selected.keyword AND u.url = selected.page
155
258
  GROUP BY query, url, week
156
259
  ),
157
260
  series_by_entity AS (
@@ -161,55 +264,24 @@ const moversAnalyzer = defineAnalyzer({
161
264
  'impressions': impressions
162
265
  } ORDER BY week)) AS seriesJson
163
266
  FROM weekly GROUP BY query, url
164
- ),
165
- joined AS (
166
- SELECT
167
- c.query AS keyword,
168
- c.url AS page,
169
- c.clicks AS recentClicks,
170
- c.impressions AS recentImpressions,
171
- c.position AS recentPosition,
172
- COALESCE(p.clicks, 0.0) AS baselineClicks,
173
- COALESCE(p.impressions, 0.0) AS baselineImpressions,
174
- p.position AS baselinePosition,
175
- (c.clicks - COALESCE(p.clicks, 0.0)) AS clicksChange,
176
- CASE
177
- WHEN COALESCE(p.clicks, 0.0) = 0 THEN CASE WHEN c.clicks > 0 THEN 100.0 ELSE 0.0 END
178
- ELSE (c.clicks - p.clicks) * 100.0 / p.clicks
179
- END AS clicksChangePercent,
180
- CASE
181
- WHEN COALESCE(p.impressions, 0.0) = 0 THEN CASE WHEN c.impressions > 0 THEN 100.0 ELSE 0.0 END
182
- ELSE (c.impressions - p.impressions) * 100.0 / p.impressions
183
- END AS impressionsChangePercent,
184
- CASE WHEN p.position IS NOT NULL THEN (c.position - p.position) ELSE NULL END AS positionChange,
185
- s.seriesJson
186
- FROM cur c
187
- LEFT JOIN prev p ON c.query = p.query AND c.url = p.url
188
- LEFT JOIN series_by_entity s ON c.query = s.query AND c.url = s.url
189
- WHERE c.impressions >= ?
190
267
  )
191
- SELECT *,
192
- CASE
193
- WHEN clicksChangePercent > 0 AND ABS(clicksChangePercent) / 100.0 >= ? THEN 'rising'
194
- WHEN clicksChangePercent < 0 AND ABS(clicksChangePercent) / 100.0 >= ? THEN 'declining'
195
- ELSE 'stable'
196
- END AS direction
197
- FROM joined
198
- ORDER BY ABS(clicksChangePercent) DESC
199
- LIMIT ${Number(limit)}
268
+ SELECT selected.*, s.seriesJson
269
+ FROM selected
270
+ LEFT JOIN series_by_entity s ON selected.keyword = s.query AND selected.page = s.url
271
+ ORDER BY ${orderBy}
200
272
  `,
201
273
  params: [
202
274
  cur.startDate,
203
275
  cur.endDate,
204
276
  prev.startDate,
205
277
  prev.endDate,
278
+ minImpressions,
279
+ changeThreshold,
280
+ changeThreshold,
206
281
  cur.startDate,
207
282
  cur.endDate,
208
283
  prev.startDate,
209
- prev.endDate,
210
- minImpressions,
211
- changeThreshold,
212
- changeThreshold
284
+ prev.endDate
213
285
  ],
214
286
  current: {
215
287
  table: "page_queries",
@@ -222,41 +294,48 @@ const moversAnalyzer = defineAnalyzer({
222
294
  };
223
295
  },
224
296
  reduceSql(rows, params) {
225
- const normalized = (Array.isArray(rows) ? rows : []).map((r) => {
226
- const recentClicks = num(r.recentClicks);
227
- const recentImpressions = num(r.recentImpressions);
228
- const recentPosition = num(r.recentPosition);
229
- const baselineClicks = Math.round(num(r.baselineClicks));
230
- const baselineImpressions = Math.round(num(r.baselineImpressions));
231
- const baselinePosition = r.baselinePosition == null ? null : num(r.baselinePosition);
232
- return {
233
- keyword: rowString(r.keyword),
234
- page: r.page == null ? null : rowString(r.page),
235
- recentClicks,
236
- recentImpressions,
237
- recentPosition,
238
- baselineClicks,
239
- baselineImpressions,
240
- baselinePosition,
241
- clicksChange: num(r.clicksChange),
242
- clicksChangePercent: num(r.clicksChangePercent),
243
- impressionsChangePercent: num(r.impressionsChangePercent),
244
- positionChange: r.positionChange == null ? null : num(r.positionChange),
245
- direction: rowString(r.direction),
246
- series: parseJsonRows(r.seriesJson).map((s) => ({
247
- week: rowString(s.week),
248
- clicks: num(s.clicks),
249
- impressions: num(s.impressions)
250
- }))
251
- };
252
- }).map(withCanonicalFields);
253
- return selectMoversDirection(normalized.filter((r) => r.direction === "rising"), normalized.filter((r) => r.direction === "declining"), normalized.filter((r) => r.direction === "stable"), params);
297
+ const arr = Array.isArray(rows) ? rows : [];
298
+ const results = arr.map((r) => withCanonicalFields({
299
+ keyword: rowString(r.keyword),
300
+ page: r.page == null ? null : rowString(r.page),
301
+ recentClicks: num(r.recentClicks),
302
+ recentImpressions: num(r.recentImpressions),
303
+ recentPosition: r.recentPosition == null ? null : num(r.recentPosition),
304
+ baselineClicks: Math.round(num(r.baselineClicks)),
305
+ baselineImpressions: Math.round(num(r.baselineImpressions)),
306
+ baselinePosition: r.baselinePosition == null ? null : num(r.baselinePosition),
307
+ clicksChange: num(r.clicksChange),
308
+ clicksChangePercent: num(r.clicksChangePercent),
309
+ impressionsChange: num(r.impressionsChange),
310
+ impressionsChangePercent: num(r.impressionsChangePercent),
311
+ positionChange: r.positionChange == null ? null : num(r.positionChange),
312
+ direction: rowString(r.direction),
313
+ series: parseJsonRows(r.seriesJson).map((s) => ({
314
+ week: rowString(s.week),
315
+ clicks: num(s.clicks),
316
+ impressions: num(s.impressions)
317
+ }))
318
+ }));
319
+ const first = arr[0];
320
+ const rising = num(first?.risingCount);
321
+ const declining = num(first?.decliningCount);
322
+ const stable = num(first?.stableCount);
323
+ return {
324
+ results,
325
+ meta: {
326
+ total: params.direction === "rising" ? rising : params.direction === "declining" ? declining : rising + declining,
327
+ returned: results.length,
328
+ rising,
329
+ declining,
330
+ stable
331
+ }
332
+ };
254
333
  },
255
334
  buildRows(params) {
256
335
  const { current, previous } = comparisonOf(params);
257
336
  return {
258
- current: queriesQueryState(current, params.limit),
259
- previous: queriesQueryState(previous, params.limit)
337
+ current: queriesQueryState(current, fetchBudgetOf(params)),
338
+ previous: queriesQueryState(previous, fetchBudgetOf(params))
260
339
  };
261
340
  },
262
341
  reduceRows(rows, params) {
@@ -264,20 +343,18 @@ const moversAnalyzer = defineAnalyzer({
264
343
  current: [],
265
344
  previous: []
266
345
  };
267
- const result = analyzeMovers({
268
- current: map.current ?? [],
269
- previous: map.previous ?? []
346
+ const current = map.current ?? [];
347
+ const previous = map.previous ?? [];
348
+ const { sortBy, sortDir } = resolveMoversSort(params);
349
+ return selectMoversDirection(analyzeMovers({
350
+ current,
351
+ previous
270
352
  }, {
271
353
  changeThreshold: params.changeThreshold,
272
- minImpressions: params.minImpressions
273
- });
274
- return selectMoversDirection(result.rising.map((r) => withCanonicalFields({
275
- ...r,
276
- direction: "rising"
277
- })), result.declining.map((r) => withCanonicalFields({
278
- ...r,
279
- direction: "declining"
280
- })), [], params);
354
+ minImpressions: params.minImpressions,
355
+ sortBy,
356
+ sortDir
357
+ }), params, moversComparator(sortBy, sortDir));
281
358
  }
282
359
  });
283
- export { analyzeMovers, moversAnalyzer };
360
+ export { MOVERS_SORT_METRICS, analyzeMovers, moversAnalyzer };
@@ -1,6 +1,6 @@
1
1
  import { queriesQueryState } from "../analyzer/adapt-rows.mjs";
2
- import { paginateClause, paginateSortedInMemory } from "../analyzer/paginate.mjs";
3
- import { num } from "@gscdump/engine/analysis-types";
2
+ import { TOTAL_COUNT_SELECT, paginateClause, paginateSortedInMemory, totalCountOf } from "../analyzer/paginate.mjs";
3
+ import { fetchBudgetOf, num } from "@gscdump/engine/analysis-types";
4
4
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
5
5
  import { periodOf } from "@gscdump/engine/period";
6
6
  import { enumeratePartitions } from "@gscdump/engine/planner";
@@ -151,7 +151,8 @@ const opportunityAnalyzer = defineAnalyzer({
151
151
  ELSE 0.10
152
152
  END
153
153
  )) AS DOUBLE) AS potentialClicks,
154
- positionScore, impressionScore, ctrGapScore
154
+ positionScore, impressionScore, ctrGapScore,
155
+ ${TOTAL_COUNT_SELECT}
155
156
  FROM gapped
156
157
  ORDER BY opportunityScore DESC
157
158
  ${paginateClause({
@@ -188,11 +189,14 @@ const opportunityAnalyzer = defineAnalyzer({
188
189
  ctrGapScore: num(r.ctrGapScore)
189
190
  }
190
191
  })),
191
- meta: { total: arr.length }
192
+ meta: {
193
+ total: totalCountOf(arr),
194
+ returned: arr.length
195
+ }
192
196
  };
193
197
  },
194
198
  buildRows(params) {
195
- return { queries: queriesQueryState(periodOf(params), params.limit) };
199
+ return { queries: queriesQueryState(periodOf(params), fetchBudgetOf(params)) };
196
200
  },
197
201
  reduceRows(rows, params) {
198
202
  const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
@@ -1,6 +1,6 @@
1
1
  import { datesQueryState } from "../analyzer/adapt-rows.mjs";
2
2
  import { rowBoolean, rowString } from "../analyzer/row-values.mjs";
3
- import { num } from "@gscdump/engine/analysis-types";
3
+ import { fetchBudgetOf, num } from "@gscdump/engine/analysis-types";
4
4
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
5
5
  import { periodOf } from "@gscdump/engine/period";
6
6
  import { enumeratePartitions } from "@gscdump/engine/planner";
@@ -123,13 +123,16 @@ const seasonalityAnalyzer = defineAnalyzer({
123
123
  };
124
124
  },
125
125
  buildRows(params) {
126
- return { dates: datesQueryState(periodOf(params), params.limit) };
126
+ return { dates: datesQueryState(periodOf(params), fetchBudgetOf(params)) };
127
127
  },
128
128
  reduceRows(rows, params) {
129
129
  const result = analyzeSeasonality(Array.isArray(rows) ? rows : [], { metric: params.metric });
130
130
  return {
131
131
  results: result.monthlyBreakdown,
132
- meta: { strength: result.strength }
132
+ meta: {
133
+ total: result.monthlyBreakdown.length,
134
+ strength: result.strength
135
+ }
133
136
  };
134
137
  }
135
138
  });
@@ -1,10 +1,9 @@
1
1
  import { queriesQueryState } from "../analyzer/adapt-rows.mjs";
2
2
  import { paginateSortedInMemory } from "../analyzer/paginate.mjs";
3
- import { num } from "@gscdump/engine/analysis-types";
3
+ import { fetchBudgetOf, num } from "@gscdump/engine/analysis-types";
4
4
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
5
5
  import { periodOf } from "@gscdump/engine/period";
6
6
  import { enumeratePartitions } from "@gscdump/engine/planner";
7
- const DEFAULT_ROW_LIMIT = 25e3;
8
7
  function paginateStrikingDistance(results, options) {
9
8
  return paginateSortedInMemory(results, {
10
9
  limit: options.limit ?? 1e3,
@@ -76,7 +75,7 @@ const strikingDistanceAnalyzer = defineAnalyzer({
76
75
  };
77
76
  },
78
77
  buildRows(params) {
79
- return { queries: queriesQueryState(periodOf(params), params.limit ?? DEFAULT_ROW_LIMIT) };
78
+ return { queries: queriesQueryState(periodOf(params), fetchBudgetOf(params)) };
80
79
  }
81
80
  });
82
81
  export { analyzeStrikingDistance, filterStrikingDistance, strikingDistanceAnalyzer };
@@ -1,11 +1,10 @@
1
- import { paginateClause, paginateSortedInMemory } from "../analyzer/paginate.mjs";
2
- import { num } from "@gscdump/engine/analysis-types";
1
+ import { queriesQueryState } from "../analyzer/adapt-rows.mjs";
2
+ import { TOTAL_COUNT_SELECT, paginateClause, paginateSortedInMemory, totalCountOf } from "../analyzer/paginate.mjs";
3
+ import { fetchBudgetOf, num } from "@gscdump/engine/analysis-types";
3
4
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
4
5
  import { periodOf } from "@gscdump/engine/period";
5
6
  import { enumeratePartitions } from "@gscdump/engine/planner";
6
7
  import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
7
- import { between, date, gsc, page, query } from "gscdump/query";
8
- const DEFAULT_ROW_LIMIT = 25e3;
9
8
  function analyzeZeroClick(rows, options = {}) {
10
9
  const minImpressions = options.minImpressions ?? 1e3;
11
10
  const maxCtr = options.maxCtr ?? .03;
@@ -50,7 +49,8 @@ const zeroClickAnalyzer = defineAnalyzer({
50
49
  WHEN position <= 5 THEN 0.08
51
50
  ELSE 0.04
52
51
  END
53
- )) - clicks) AS DOUBLE) AS missedClicks
52
+ )) - clicks) AS DOUBLE) AS missedClicks,
53
+ ${TOTAL_COUNT_SELECT}
54
54
  FROM agg
55
55
  WHERE position <= ? AND ctr < ?
56
56
  ORDER BY impressions DESC
@@ -88,7 +88,8 @@ const zeroClickAnalyzer = defineAnalyzer({
88
88
  missedClicks: num(r.missedClicks)
89
89
  })),
90
90
  meta: {
91
- total: arr.length,
91
+ total: totalCountOf(arr),
92
+ returned: arr.length,
92
93
  minImpressions,
93
94
  maxCtr,
94
95
  maxPosition
@@ -96,9 +97,7 @@ const zeroClickAnalyzer = defineAnalyzer({
96
97
  };
97
98
  },
98
99
  buildRows(params) {
99
- const period = periodOf(params);
100
- const limit = params.limit ?? DEFAULT_ROW_LIMIT;
101
- return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
100
+ return { rows: queriesQueryState(periodOf(params), fetchBudgetOf(params)) };
102
101
  },
103
102
  reduceRows(rows, params) {
104
103
  const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
package/dist/index.d.mts CHANGED
@@ -4,7 +4,7 @@ import { CannibalizationCompetitor, CannibalizationEvent, CannibalizationOptions
4
4
  import { ClusterType, ClusteringOptions, ClusteringResult, KeywordCluster, analyzeClustering } from "./analyzers/clustering.mjs";
5
5
  import { ConcentrationInput, ConcentrationItem, ConcentrationOptions, ConcentrationResult, ConcentrationRiskLevel, analyzeConcentration, analyzeKeywordConcentration, analyzePageConcentration } from "./analyzers/concentration.mjs";
6
6
  import { DecayInput, DecayOptions, DecayResult, DecaySeriesPoint, DecaySortMetric, analyzeDecay } from "./analyzers/decay.mjs";
7
- import { MoverData, MoversInput, MoversOptions, MoversResult, MoversSortMetric, analyzeMovers } from "./analyzers/movers.mjs";
7
+ import { MOVERS_SORT_METRICS, MoverData, MoversInput, MoversOptions, MoversResult, MoversSortMetric, analyzeMovers } from "./analyzers/movers.mjs";
8
8
  import { OpportunityFactors, OpportunityOptions, OpportunityResult, OpportunitySortMetric, OpportunityWeights, analyzeOpportunity } from "./analyzers/opportunity.mjs";
9
9
  import { MonthlyData, SeasonalityMetric, SeasonalityOptions, SeasonalityResult, analyzeSeasonality } from "./analyzers/seasonality.mjs";
10
10
  import { StrikingDistanceInputRow, StrikingDistanceOptions, StrikingDistanceResult, analyzeStrikingDistance } from "./analyzers/striking-distance.mjs";
@@ -18,4 +18,4 @@ import { Analyzer, AnalyzerCapabilityError, AnalyzerRegistry, AnalyzerRegistryIn
18
18
  import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
19
19
  import { AnalysisQuerySource, AnalysisSourceKind, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, SourceCapabilities, createEngineQuerySource, queryRows, runAnalyzerWithEngine } from "@gscdump/engine/source";
20
20
  import { DefineReportOptions, DefinedReport, ReportContext, ReportFinding, ReportPlanStep, ReportResult, ReportSection } from "@gscdump/engine/report";
21
- export { type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, type CurrentSitemapScope, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, ENGINE_QUERY_CAPABILITIES, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, INTENT_CLASSIFIER_VERSION, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type QueriesRow, type QueryPageRow, type QueryRow, type ReduceContext, type ReduceCtx, type Reducer, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapCollapsePolicy, type SitemapCollapseState, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createEngineQuerySource, createSorter, defineAnalyzer, diffSitemapHealth, encodeIntent, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, sitemapHistoryHasCollapse };
21
+ export { type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, type CurrentSitemapScope, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, ENGINE_QUERY_CAPABILITIES, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, INTENT_CLASSIFIER_VERSION, type IntentClassification, type KeywordCluster, MOVERS_SORT_METRICS, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type QueriesRow, type QueryPageRow, type QueryRow, type ReduceContext, type ReduceCtx, type Reducer, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapCollapsePolicy, type SitemapCollapseState, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createEngineQuerySource, createSorter, defineAnalyzer, diffSitemapHealth, encodeIntent, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, sitemapHistoryHasCollapse };
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { analyzeCannibalization } from "./analyzers/cannibalization.mjs";
4
4
  import { analyzeClustering } from "./analyzers/clustering.mjs";
5
5
  import { analyzeConcentration, analyzeKeywordConcentration, analyzePageConcentration } from "./analyzers/concentration.mjs";
6
6
  import { analyzeDecay } from "./analyzers/decay.mjs";
7
- import { analyzeMovers } from "./analyzers/movers.mjs";
7
+ import { MOVERS_SORT_METRICS, analyzeMovers } from "./analyzers/movers.mjs";
8
8
  import { analyzeOpportunity } from "./analyzers/opportunity.mjs";
9
9
  import { analyzeSeasonality } from "./analyzers/seasonality.mjs";
10
10
  import { analyzeStrikingDistance } from "./analyzers/striking-distance.mjs";
@@ -17,4 +17,4 @@ import { num } from "@gscdump/engine/analysis-types";
17
17
  import { AnalyzerCapabilityError, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
18
18
  import { comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
19
19
  import { ENGINE_QUERY_CAPABILITIES, createEngineQuerySource, queryRows, runAnalyzerWithEngine } from "@gscdump/engine/source";
20
- export { AnalyzerCapabilityError, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, NORMALIZER_VERSION, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createEngineQuerySource, createSorter, defineAnalyzer, diffSitemapHealth, encodeIntent, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, sitemapHistoryHasCollapse };
20
+ export { AnalyzerCapabilityError, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, MOVERS_SORT_METRICS, NORMALIZER_VERSION, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createEngineQuerySource, createSorter, defineAnalyzer, diffSitemapHealth, encodeIntent, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, sitemapHistoryHasCollapse };