@gscdump/analysis 0.25.5 → 0.25.7

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.
@@ -461,6 +461,13 @@ interface MoversSeriesPoint {
461
461
  }
462
462
  interface MoversResultRow extends MoverData {
463
463
  direction: 'rising' | 'declining' | 'stable';
464
+ clicks: number;
465
+ impressions: number;
466
+ ctr: number;
467
+ position: number;
468
+ prevClicks: number;
469
+ prevImpressions: number;
470
+ prevPosition: number;
464
471
  series?: MoversSeriesPoint[];
465
472
  }
466
473
  declare const moversAnalyzer: import("@gscdump/engine/analyzer").DefinedAnalyzer;
@@ -2388,6 +2388,34 @@ const dataQueryAnalyzer = defineAnalyzer({
2388
2388
  };
2389
2389
  }
2390
2390
  });
2391
+ const DEFAULT_LIMIT = 1e3;
2392
+ const MAX_LIMIT = 5e4;
2393
+ function clampLimit(limit, fallback = DEFAULT_LIMIT) {
2394
+ const n = Number(limit ?? fallback);
2395
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2396
+ return Math.min(n, MAX_LIMIT);
2397
+ }
2398
+ function clampOffset(offset) {
2399
+ const n = Number(offset ?? 0);
2400
+ if (!Number.isFinite(n) || n < 0) return 0;
2401
+ return Math.floor(n);
2402
+ }
2403
+ function paginateClause(input) {
2404
+ const l = clampLimit(input.limit);
2405
+ const o = clampOffset(input.offset);
2406
+ return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
2407
+ }
2408
+ function paginateInMemory(rows, input) {
2409
+ const l = clampLimit(input.limit, rows.length);
2410
+ const o = clampOffset(input.offset);
2411
+ return rows.slice(o, o + l);
2412
+ }
2413
+ function resolveSort(input, allowed, defaults) {
2414
+ return {
2415
+ sortBy: input.sortBy && allowed.includes(input.sortBy) ? input.sortBy : defaults.sortBy,
2416
+ sortDir: input.sortDir === "asc" || input.sortDir === "desc" ? input.sortDir : defaults.sortDir
2417
+ };
2418
+ }
2391
2419
  const sortResults$1 = createMetricSorter("lostClicks", {
2392
2420
  lostClicks: "desc",
2393
2421
  declinePercent: "desc",
@@ -2441,7 +2469,6 @@ const decayAnalyzer = defineAnalyzer({
2441
2469
  const { current: cur, previous: prev } = comparisonOf(params);
2442
2470
  const minPreviousClicks = params.minPreviousClicks ?? 50;
2443
2471
  const threshold = params.threshold ?? .2;
2444
- const limit = params.limit ?? 2e3;
2445
2472
  return {
2446
2473
  sql: `
2447
2474
  WITH cur AS (
@@ -2505,7 +2532,7 @@ const decayAnalyzer = defineAnalyzer({
2505
2532
  FROM joined
2506
2533
  WHERE declinePercent >= ? AND lostClicks > 0
2507
2534
  ORDER BY lostClicks DESC
2508
- LIMIT ${Number(limit)}
2535
+ LIMIT 2000
2509
2536
  `,
2510
2537
  params: [
2511
2538
  cur.startDate,
@@ -2529,25 +2556,28 @@ const decayAnalyzer = defineAnalyzer({
2529
2556
  }
2530
2557
  };
2531
2558
  },
2532
- reduceSql(rows) {
2533
- const arr = Array.isArray(rows) ? rows : [];
2559
+ reduceSql(rows, params) {
2560
+ const mapped = (Array.isArray(rows) ? rows : []).map((r) => ({
2561
+ page: str$12(r.page),
2562
+ currentClicks: num(r.currentClicks),
2563
+ previousClicks: num(r.previousClicks),
2564
+ lostClicks: num(r.lostClicks),
2565
+ declinePercent: num(r.declinePercent),
2566
+ currentPosition: num(r.currentPosition),
2567
+ previousPosition: num(r.previousPosition),
2568
+ positionDrop: num(r.positionDrop),
2569
+ series: parseJsonList$8(r.seriesJson).map((s) => ({
2570
+ week: str$12(s.week),
2571
+ clicks: num(s.clicks),
2572
+ impressions: num(s.impressions)
2573
+ }))
2574
+ }));
2534
2575
  return {
2535
- results: arr.map((r) => ({
2536
- page: str$12(r.page),
2537
- currentClicks: num(r.currentClicks),
2538
- previousClicks: num(r.previousClicks),
2539
- lostClicks: num(r.lostClicks),
2540
- declinePercent: num(r.declinePercent),
2541
- currentPosition: num(r.currentPosition),
2542
- previousPosition: num(r.previousPosition),
2543
- positionDrop: num(r.positionDrop),
2544
- series: parseJsonList$8(r.seriesJson).map((s) => ({
2545
- week: str$12(s.week),
2546
- clicks: num(s.clicks),
2547
- impressions: num(s.impressions)
2548
- }))
2549
- })),
2550
- meta: { total: arr.length }
2576
+ results: paginateInMemory(mapped, {
2577
+ limit: params.limit ?? 2e3,
2578
+ offset: params.offset
2579
+ }),
2580
+ meta: { total: mapped.length }
2551
2581
  };
2552
2582
  },
2553
2583
  buildRows(params) {
@@ -3151,6 +3181,45 @@ function parseJsonList$4(v) {
3151
3181
  }
3152
3182
  return [];
3153
3183
  }
3184
+ function withCanonicalFields(r) {
3185
+ return {
3186
+ ...r,
3187
+ clicks: r.recentClicks,
3188
+ impressions: r.recentImpressions,
3189
+ ctr: r.recentImpressions > 0 ? r.recentClicks / r.recentImpressions : 0,
3190
+ position: r.recentPosition,
3191
+ prevClicks: r.baselineClicks,
3192
+ prevImpressions: r.baselineImpressions,
3193
+ prevPosition: r.baselinePosition
3194
+ };
3195
+ }
3196
+ function selectMoversDirection(rising, declining, stable, params) {
3197
+ if (params.direction) {
3198
+ const selected = params.direction === "rising" ? rising : declining;
3199
+ const total = selected.length;
3200
+ const offset = params.offset ?? 0;
3201
+ const limit = params.limit ?? total;
3202
+ return {
3203
+ results: selected.slice(offset, offset + limit),
3204
+ meta: {
3205
+ total,
3206
+ rising: rising.length,
3207
+ declining: declining.length,
3208
+ stable: stable.length
3209
+ }
3210
+ };
3211
+ }
3212
+ const combined = [...rising, ...declining];
3213
+ return {
3214
+ results: combined,
3215
+ meta: {
3216
+ total: combined.length,
3217
+ rising: rising.length,
3218
+ declining: declining.length,
3219
+ stable: stable.length
3220
+ }
3221
+ };
3222
+ }
3154
3223
  function analyzeMovers(input, options = {}) {
3155
3224
  const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
3156
3225
  const normFactor = input.normalizationFactor ?? 1;
@@ -3223,7 +3292,7 @@ const moversAnalyzer = defineAnalyzer({
3223
3292
  const { current: cur, previous: prev } = comparisonOf(params);
3224
3293
  const minImpressions = params.minImpressions ?? 50;
3225
3294
  const changeThreshold = params.changeThreshold ?? .2;
3226
- const limit = params.limit ?? 2e3;
3295
+ const limit = params.direction ? 5e3 : params.limit ?? 2e3;
3227
3296
  return {
3228
3297
  sql: `
3229
3298
  WITH cur AS (
@@ -3328,40 +3397,36 @@ const moversAnalyzer = defineAnalyzer({
3328
3397
  }
3329
3398
  };
3330
3399
  },
3331
- reduceSql(rows) {
3332
- const normalized = (Array.isArray(rows) ? rows : []).map((r) => ({
3333
- keyword: str$7(r.keyword),
3334
- page: r.page == null ? null : str$7(r.page),
3335
- recentClicks: num(r.recentClicks),
3336
- recentImpressions: num(r.recentImpressions),
3337
- recentPosition: num(r.recentPosition),
3338
- baselineClicks: Math.round(num(r.baselineClicks)),
3339
- baselineImpressions: Math.round(num(r.baselineImpressions)),
3340
- baselinePosition: num(r.baselinePosition),
3341
- clicksChange: num(r.clicksChange),
3342
- clicksChangePercent: num(r.clicksChangePercent),
3343
- impressionsChangePercent: num(r.impressionsChangePercent),
3344
- positionChange: num(r.positionChange),
3345
- direction: str$7(r.direction),
3346
- series: parseJsonList$4(r.seriesJson).map((s) => ({
3347
- week: str$7(s.week),
3348
- clicks: num(s.clicks),
3349
- impressions: num(s.impressions)
3350
- }))
3351
- }));
3352
- const rising = normalized.filter((r) => r.direction === "rising");
3353
- const declining = normalized.filter((r) => r.direction === "declining");
3354
- const stable = normalized.filter((r) => r.direction === "stable");
3355
- const combined = [...rising, ...declining];
3356
- return {
3357
- results: combined,
3358
- meta: {
3359
- total: combined.length,
3360
- rising: rising.length,
3361
- declining: declining.length,
3362
- stable: stable.length
3363
- }
3364
- };
3400
+ reduceSql(rows, params) {
3401
+ const normalized = (Array.isArray(rows) ? rows : []).map((r) => {
3402
+ const recentClicks = num(r.recentClicks);
3403
+ const recentImpressions = num(r.recentImpressions);
3404
+ const recentPosition = num(r.recentPosition);
3405
+ const baselineClicks = Math.round(num(r.baselineClicks));
3406
+ const baselineImpressions = Math.round(num(r.baselineImpressions));
3407
+ const baselinePosition = num(r.baselinePosition);
3408
+ return {
3409
+ keyword: str$7(r.keyword),
3410
+ page: r.page == null ? null : str$7(r.page),
3411
+ recentClicks,
3412
+ recentImpressions,
3413
+ recentPosition,
3414
+ baselineClicks,
3415
+ baselineImpressions,
3416
+ baselinePosition,
3417
+ clicksChange: num(r.clicksChange),
3418
+ clicksChangePercent: num(r.clicksChangePercent),
3419
+ impressionsChangePercent: num(r.impressionsChangePercent),
3420
+ positionChange: num(r.positionChange),
3421
+ direction: str$7(r.direction),
3422
+ series: parseJsonList$4(r.seriesJson).map((s) => ({
3423
+ week: str$7(s.week),
3424
+ clicks: num(s.clicks),
3425
+ impressions: num(s.impressions)
3426
+ }))
3427
+ };
3428
+ }).map(withCanonicalFields);
3429
+ return selectMoversDirection(normalized.filter((r) => r.direction === "rising"), normalized.filter((r) => r.direction === "declining"), normalized.filter((r) => r.direction === "stable"), params);
3365
3430
  },
3366
3431
  buildRows(params) {
3367
3432
  const { current, previous } = comparisonOf(params);
@@ -3382,49 +3447,15 @@ const moversAnalyzer = defineAnalyzer({
3382
3447
  changeThreshold: params.changeThreshold,
3383
3448
  minImpressions: params.minImpressions
3384
3449
  });
3385
- return {
3386
- results: [...result.rising.map((r) => ({
3387
- ...r,
3388
- direction: "rising"
3389
- })), ...result.declining.map((r) => ({
3390
- ...r,
3391
- direction: "declining"
3392
- }))],
3393
- meta: {
3394
- rising: result.rising.length,
3395
- declining: result.declining.length
3396
- }
3397
- };
3450
+ return selectMoversDirection(result.rising.map((r) => withCanonicalFields({
3451
+ ...r,
3452
+ direction: "rising"
3453
+ })), result.declining.map((r) => withCanonicalFields({
3454
+ ...r,
3455
+ direction: "declining"
3456
+ })), [], params);
3398
3457
  }
3399
3458
  });
3400
- const DEFAULT_LIMIT = 1e3;
3401
- const MAX_LIMIT = 5e4;
3402
- function clampLimit(limit, fallback = DEFAULT_LIMIT) {
3403
- const n = Number(limit ?? fallback);
3404
- if (!Number.isFinite(n) || n <= 0) return fallback;
3405
- return Math.min(n, MAX_LIMIT);
3406
- }
3407
- function clampOffset(offset) {
3408
- const n = Number(offset ?? 0);
3409
- if (!Number.isFinite(n) || n < 0) return 0;
3410
- return Math.floor(n);
3411
- }
3412
- function paginateClause(input) {
3413
- const l = clampLimit(input.limit);
3414
- const o = clampOffset(input.offset);
3415
- return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
3416
- }
3417
- function paginateInMemory(rows, input) {
3418
- const l = clampLimit(input.limit, rows.length);
3419
- const o = clampOffset(input.offset);
3420
- return rows.slice(o, o + l);
3421
- }
3422
- function resolveSort(input, allowed, defaults) {
3423
- return {
3424
- sortBy: input.sortBy && allowed.includes(input.sortBy) ? input.sortBy : defaults.sortBy,
3425
- sortDir: input.sortDir === "asc" || input.sortDir === "desc" ? input.sortDir : defaults.sortDir
3426
- };
3427
- }
3428
3459
  const EXPECTED_CTR_BY_POSITION = {
3429
3460
  1: .3,
3430
3461
  2: .15,
@@ -2388,6 +2388,28 @@ const dataQueryAnalyzer = defineAnalyzer({
2388
2388
  };
2389
2389
  }
2390
2390
  });
2391
+ const DEFAULT_LIMIT = 1e3;
2392
+ const MAX_LIMIT = 5e4;
2393
+ function clampLimit(limit, fallback = DEFAULT_LIMIT) {
2394
+ const n = Number(limit ?? fallback);
2395
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2396
+ return Math.min(n, MAX_LIMIT);
2397
+ }
2398
+ function clampOffset(offset) {
2399
+ const n = Number(offset ?? 0);
2400
+ if (!Number.isFinite(n) || n < 0) return 0;
2401
+ return Math.floor(n);
2402
+ }
2403
+ function paginateClause(input) {
2404
+ const l = clampLimit(input.limit);
2405
+ const o = clampOffset(input.offset);
2406
+ return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
2407
+ }
2408
+ function paginateInMemory(rows, input) {
2409
+ const l = clampLimit(input.limit, rows.length);
2410
+ const o = clampOffset(input.offset);
2411
+ return rows.slice(o, o + l);
2412
+ }
2391
2413
  const sortResults$1 = createMetricSorter("lostClicks", {
2392
2414
  lostClicks: "desc",
2393
2415
  declinePercent: "desc",
@@ -2441,7 +2463,6 @@ const decayAnalyzer = defineAnalyzer({
2441
2463
  const { current: cur, previous: prev } = comparisonOf(params);
2442
2464
  const minPreviousClicks = params.minPreviousClicks ?? 50;
2443
2465
  const threshold = params.threshold ?? .2;
2444
- const limit = params.limit ?? 2e3;
2445
2466
  return {
2446
2467
  sql: `
2447
2468
  WITH cur AS (
@@ -2505,7 +2526,7 @@ const decayAnalyzer = defineAnalyzer({
2505
2526
  FROM joined
2506
2527
  WHERE declinePercent >= ? AND lostClicks > 0
2507
2528
  ORDER BY lostClicks DESC
2508
- LIMIT ${Number(limit)}
2529
+ LIMIT 2000
2509
2530
  `,
2510
2531
  params: [
2511
2532
  cur.startDate,
@@ -2529,25 +2550,28 @@ const decayAnalyzer = defineAnalyzer({
2529
2550
  }
2530
2551
  };
2531
2552
  },
2532
- reduceSql(rows) {
2533
- const arr = Array.isArray(rows) ? rows : [];
2553
+ reduceSql(rows, params) {
2554
+ const mapped = (Array.isArray(rows) ? rows : []).map((r) => ({
2555
+ page: str$12(r.page),
2556
+ currentClicks: num(r.currentClicks),
2557
+ previousClicks: num(r.previousClicks),
2558
+ lostClicks: num(r.lostClicks),
2559
+ declinePercent: num(r.declinePercent),
2560
+ currentPosition: num(r.currentPosition),
2561
+ previousPosition: num(r.previousPosition),
2562
+ positionDrop: num(r.positionDrop),
2563
+ series: parseJsonList$8(r.seriesJson).map((s) => ({
2564
+ week: str$12(s.week),
2565
+ clicks: num(s.clicks),
2566
+ impressions: num(s.impressions)
2567
+ }))
2568
+ }));
2534
2569
  return {
2535
- results: arr.map((r) => ({
2536
- page: str$12(r.page),
2537
- currentClicks: num(r.currentClicks),
2538
- previousClicks: num(r.previousClicks),
2539
- lostClicks: num(r.lostClicks),
2540
- declinePercent: num(r.declinePercent),
2541
- currentPosition: num(r.currentPosition),
2542
- previousPosition: num(r.previousPosition),
2543
- positionDrop: num(r.positionDrop),
2544
- series: parseJsonList$8(r.seriesJson).map((s) => ({
2545
- week: str$12(s.week),
2546
- clicks: num(s.clicks),
2547
- impressions: num(s.impressions)
2548
- }))
2549
- })),
2550
- meta: { total: arr.length }
2570
+ results: paginateInMemory(mapped, {
2571
+ limit: params.limit ?? 2e3,
2572
+ offset: params.offset
2573
+ }),
2574
+ meta: { total: mapped.length }
2551
2575
  };
2552
2576
  },
2553
2577
  buildRows(params) {
@@ -3151,6 +3175,45 @@ function parseJsonList$4(v) {
3151
3175
  }
3152
3176
  return [];
3153
3177
  }
3178
+ function withCanonicalFields(r) {
3179
+ return {
3180
+ ...r,
3181
+ clicks: r.recentClicks,
3182
+ impressions: r.recentImpressions,
3183
+ ctr: r.recentImpressions > 0 ? r.recentClicks / r.recentImpressions : 0,
3184
+ position: r.recentPosition,
3185
+ prevClicks: r.baselineClicks,
3186
+ prevImpressions: r.baselineImpressions,
3187
+ prevPosition: r.baselinePosition
3188
+ };
3189
+ }
3190
+ function selectMoversDirection(rising, declining, stable, params) {
3191
+ if (params.direction) {
3192
+ const selected = params.direction === "rising" ? rising : declining;
3193
+ const total = selected.length;
3194
+ const offset = params.offset ?? 0;
3195
+ const limit = params.limit ?? total;
3196
+ return {
3197
+ results: selected.slice(offset, offset + limit),
3198
+ meta: {
3199
+ total,
3200
+ rising: rising.length,
3201
+ declining: declining.length,
3202
+ stable: stable.length
3203
+ }
3204
+ };
3205
+ }
3206
+ const combined = [...rising, ...declining];
3207
+ return {
3208
+ results: combined,
3209
+ meta: {
3210
+ total: combined.length,
3211
+ rising: rising.length,
3212
+ declining: declining.length,
3213
+ stable: stable.length
3214
+ }
3215
+ };
3216
+ }
3154
3217
  function analyzeMovers(input, options = {}) {
3155
3218
  const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
3156
3219
  const normFactor = input.normalizationFactor ?? 1;
@@ -3223,7 +3286,7 @@ const moversAnalyzer = defineAnalyzer({
3223
3286
  const { current: cur, previous: prev } = comparisonOf(params);
3224
3287
  const minImpressions = params.minImpressions ?? 50;
3225
3288
  const changeThreshold = params.changeThreshold ?? .2;
3226
- const limit = params.limit ?? 2e3;
3289
+ const limit = params.direction ? 5e3 : params.limit ?? 2e3;
3227
3290
  return {
3228
3291
  sql: `
3229
3292
  WITH cur AS (
@@ -3328,40 +3391,36 @@ const moversAnalyzer = defineAnalyzer({
3328
3391
  }
3329
3392
  };
3330
3393
  },
3331
- reduceSql(rows) {
3332
- const normalized = (Array.isArray(rows) ? rows : []).map((r) => ({
3333
- keyword: str$7(r.keyword),
3334
- page: r.page == null ? null : str$7(r.page),
3335
- recentClicks: num(r.recentClicks),
3336
- recentImpressions: num(r.recentImpressions),
3337
- recentPosition: num(r.recentPosition),
3338
- baselineClicks: Math.round(num(r.baselineClicks)),
3339
- baselineImpressions: Math.round(num(r.baselineImpressions)),
3340
- baselinePosition: num(r.baselinePosition),
3341
- clicksChange: num(r.clicksChange),
3342
- clicksChangePercent: num(r.clicksChangePercent),
3343
- impressionsChangePercent: num(r.impressionsChangePercent),
3344
- positionChange: num(r.positionChange),
3345
- direction: str$7(r.direction),
3346
- series: parseJsonList$4(r.seriesJson).map((s) => ({
3347
- week: str$7(s.week),
3348
- clicks: num(s.clicks),
3349
- impressions: num(s.impressions)
3350
- }))
3351
- }));
3352
- const rising = normalized.filter((r) => r.direction === "rising");
3353
- const declining = normalized.filter((r) => r.direction === "declining");
3354
- const stable = normalized.filter((r) => r.direction === "stable");
3355
- const combined = [...rising, ...declining];
3356
- return {
3357
- results: combined,
3358
- meta: {
3359
- total: combined.length,
3360
- rising: rising.length,
3361
- declining: declining.length,
3362
- stable: stable.length
3363
- }
3364
- };
3394
+ reduceSql(rows, params) {
3395
+ const normalized = (Array.isArray(rows) ? rows : []).map((r) => {
3396
+ const recentClicks = num(r.recentClicks);
3397
+ const recentImpressions = num(r.recentImpressions);
3398
+ const recentPosition = num(r.recentPosition);
3399
+ const baselineClicks = Math.round(num(r.baselineClicks));
3400
+ const baselineImpressions = Math.round(num(r.baselineImpressions));
3401
+ const baselinePosition = num(r.baselinePosition);
3402
+ return {
3403
+ keyword: str$7(r.keyword),
3404
+ page: r.page == null ? null : str$7(r.page),
3405
+ recentClicks,
3406
+ recentImpressions,
3407
+ recentPosition,
3408
+ baselineClicks,
3409
+ baselineImpressions,
3410
+ baselinePosition,
3411
+ clicksChange: num(r.clicksChange),
3412
+ clicksChangePercent: num(r.clicksChangePercent),
3413
+ impressionsChangePercent: num(r.impressionsChangePercent),
3414
+ positionChange: num(r.positionChange),
3415
+ direction: str$7(r.direction),
3416
+ series: parseJsonList$4(r.seriesJson).map((s) => ({
3417
+ week: str$7(s.week),
3418
+ clicks: num(s.clicks),
3419
+ impressions: num(s.impressions)
3420
+ }))
3421
+ };
3422
+ }).map(withCanonicalFields);
3423
+ return selectMoversDirection(normalized.filter((r) => r.direction === "rising"), normalized.filter((r) => r.direction === "declining"), normalized.filter((r) => r.direction === "stable"), params);
3365
3424
  },
3366
3425
  buildRows(params) {
3367
3426
  const { current, previous } = comparisonOf(params);
@@ -3382,43 +3441,15 @@ const moversAnalyzer = defineAnalyzer({
3382
3441
  changeThreshold: params.changeThreshold,
3383
3442
  minImpressions: params.minImpressions
3384
3443
  });
3385
- return {
3386
- results: [...result.rising.map((r) => ({
3387
- ...r,
3388
- direction: "rising"
3389
- })), ...result.declining.map((r) => ({
3390
- ...r,
3391
- direction: "declining"
3392
- }))],
3393
- meta: {
3394
- rising: result.rising.length,
3395
- declining: result.declining.length
3396
- }
3397
- };
3444
+ return selectMoversDirection(result.rising.map((r) => withCanonicalFields({
3445
+ ...r,
3446
+ direction: "rising"
3447
+ })), result.declining.map((r) => withCanonicalFields({
3448
+ ...r,
3449
+ direction: "declining"
3450
+ })), [], params);
3398
3451
  }
3399
3452
  });
3400
- const DEFAULT_LIMIT = 1e3;
3401
- const MAX_LIMIT = 5e4;
3402
- function clampLimit(limit, fallback = DEFAULT_LIMIT) {
3403
- const n = Number(limit ?? fallback);
3404
- if (!Number.isFinite(n) || n <= 0) return fallback;
3405
- return Math.min(n, MAX_LIMIT);
3406
- }
3407
- function clampOffset(offset) {
3408
- const n = Number(offset ?? 0);
3409
- if (!Number.isFinite(n) || n < 0) return 0;
3410
- return Math.floor(n);
3411
- }
3412
- function paginateClause(input) {
3413
- const l = clampLimit(input.limit);
3414
- const o = clampOffset(input.offset);
3415
- return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
3416
- }
3417
- function paginateInMemory(rows, input) {
3418
- const l = clampLimit(input.limit, rows.length);
3419
- const o = clampOffset(input.offset);
3420
- return rows.slice(o, o + l);
3421
- }
3422
3453
  const EXPECTED_CTR_BY_POSITION = {
3423
3454
  1: .3,
3424
3455
  2: .15,
package/dist/index.mjs CHANGED
@@ -2754,6 +2754,28 @@ const dataQueryAnalyzer = defineAnalyzer$1({
2754
2754
  };
2755
2755
  }
2756
2756
  });
2757
+ const DEFAULT_LIMIT$1 = 1e3;
2758
+ const MAX_LIMIT = 5e4;
2759
+ function clampLimit(limit, fallback = DEFAULT_LIMIT$1) {
2760
+ const n = Number(limit ?? fallback);
2761
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2762
+ return Math.min(n, MAX_LIMIT);
2763
+ }
2764
+ function clampOffset(offset) {
2765
+ const n = Number(offset ?? 0);
2766
+ if (!Number.isFinite(n) || n < 0) return 0;
2767
+ return Math.floor(n);
2768
+ }
2769
+ function paginateClause(input) {
2770
+ const l = clampLimit(input.limit);
2771
+ const o = clampOffset(input.offset);
2772
+ return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
2773
+ }
2774
+ function paginateInMemory(rows, input) {
2775
+ const l = clampLimit(input.limit, rows.length);
2776
+ const o = clampOffset(input.offset);
2777
+ return rows.slice(o, o + l);
2778
+ }
2757
2779
  const sortResults$1 = createMetricSorter("lostClicks", {
2758
2780
  lostClicks: "desc",
2759
2781
  declinePercent: "desc",
@@ -2807,7 +2829,6 @@ const decayAnalyzer = defineAnalyzer$1({
2807
2829
  const { current: cur, previous: prev } = comparisonOf$1(params);
2808
2830
  const minPreviousClicks = params.minPreviousClicks ?? 50;
2809
2831
  const threshold = params.threshold ?? .2;
2810
- const limit = params.limit ?? 2e3;
2811
2832
  return {
2812
2833
  sql: `
2813
2834
  WITH cur AS (
@@ -2871,7 +2892,7 @@ const decayAnalyzer = defineAnalyzer$1({
2871
2892
  FROM joined
2872
2893
  WHERE declinePercent >= ? AND lostClicks > 0
2873
2894
  ORDER BY lostClicks DESC
2874
- LIMIT ${Number(limit)}
2895
+ LIMIT 2000
2875
2896
  `,
2876
2897
  params: [
2877
2898
  cur.startDate,
@@ -2895,25 +2916,28 @@ const decayAnalyzer = defineAnalyzer$1({
2895
2916
  }
2896
2917
  };
2897
2918
  },
2898
- reduceSql(rows) {
2899
- const arr = Array.isArray(rows) ? rows : [];
2919
+ reduceSql(rows, params) {
2920
+ const mapped = (Array.isArray(rows) ? rows : []).map((r) => ({
2921
+ page: str$12(r.page),
2922
+ currentClicks: num$1(r.currentClicks),
2923
+ previousClicks: num$1(r.previousClicks),
2924
+ lostClicks: num$1(r.lostClicks),
2925
+ declinePercent: num$1(r.declinePercent),
2926
+ currentPosition: num$1(r.currentPosition),
2927
+ previousPosition: num$1(r.previousPosition),
2928
+ positionDrop: num$1(r.positionDrop),
2929
+ series: parseJsonList$8(r.seriesJson).map((s) => ({
2930
+ week: str$12(s.week),
2931
+ clicks: num$1(s.clicks),
2932
+ impressions: num$1(s.impressions)
2933
+ }))
2934
+ }));
2900
2935
  return {
2901
- results: arr.map((r) => ({
2902
- page: str$12(r.page),
2903
- currentClicks: num$1(r.currentClicks),
2904
- previousClicks: num$1(r.previousClicks),
2905
- lostClicks: num$1(r.lostClicks),
2906
- declinePercent: num$1(r.declinePercent),
2907
- currentPosition: num$1(r.currentPosition),
2908
- previousPosition: num$1(r.previousPosition),
2909
- positionDrop: num$1(r.positionDrop),
2910
- series: parseJsonList$8(r.seriesJson).map((s) => ({
2911
- week: str$12(s.week),
2912
- clicks: num$1(s.clicks),
2913
- impressions: num$1(s.impressions)
2914
- }))
2915
- })),
2916
- meta: { total: arr.length }
2936
+ results: paginateInMemory(mapped, {
2937
+ limit: params.limit ?? 2e3,
2938
+ offset: params.offset
2939
+ }),
2940
+ meta: { total: mapped.length }
2917
2941
  };
2918
2942
  },
2919
2943
  buildRows(params) {
@@ -3513,6 +3537,45 @@ function parseJsonList$4(v) {
3513
3537
  }
3514
3538
  return [];
3515
3539
  }
3540
+ function withCanonicalFields(r) {
3541
+ return {
3542
+ ...r,
3543
+ clicks: r.recentClicks,
3544
+ impressions: r.recentImpressions,
3545
+ ctr: r.recentImpressions > 0 ? r.recentClicks / r.recentImpressions : 0,
3546
+ position: r.recentPosition,
3547
+ prevClicks: r.baselineClicks,
3548
+ prevImpressions: r.baselineImpressions,
3549
+ prevPosition: r.baselinePosition
3550
+ };
3551
+ }
3552
+ function selectMoversDirection(rising, declining, stable, params) {
3553
+ if (params.direction) {
3554
+ const selected = params.direction === "rising" ? rising : declining;
3555
+ const total = selected.length;
3556
+ const offset = params.offset ?? 0;
3557
+ const limit = params.limit ?? total;
3558
+ return {
3559
+ results: selected.slice(offset, offset + limit),
3560
+ meta: {
3561
+ total,
3562
+ rising: rising.length,
3563
+ declining: declining.length,
3564
+ stable: stable.length
3565
+ }
3566
+ };
3567
+ }
3568
+ const combined = [...rising, ...declining];
3569
+ return {
3570
+ results: combined,
3571
+ meta: {
3572
+ total: combined.length,
3573
+ rising: rising.length,
3574
+ declining: declining.length,
3575
+ stable: stable.length
3576
+ }
3577
+ };
3578
+ }
3516
3579
  function analyzeMovers(input, options = {}) {
3517
3580
  const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
3518
3581
  const normFactor = input.normalizationFactor ?? 1;
@@ -3585,7 +3648,7 @@ const moversAnalyzer = defineAnalyzer$1({
3585
3648
  const { current: cur, previous: prev } = comparisonOf$1(params);
3586
3649
  const minImpressions = params.minImpressions ?? 50;
3587
3650
  const changeThreshold = params.changeThreshold ?? .2;
3588
- const limit = params.limit ?? 2e3;
3651
+ const limit = params.direction ? 5e3 : params.limit ?? 2e3;
3589
3652
  return {
3590
3653
  sql: `
3591
3654
  WITH cur AS (
@@ -3690,40 +3753,36 @@ const moversAnalyzer = defineAnalyzer$1({
3690
3753
  }
3691
3754
  };
3692
3755
  },
3693
- reduceSql(rows) {
3694
- const normalized = (Array.isArray(rows) ? rows : []).map((r) => ({
3695
- keyword: str$7(r.keyword),
3696
- page: r.page == null ? null : str$7(r.page),
3697
- recentClicks: num$1(r.recentClicks),
3698
- recentImpressions: num$1(r.recentImpressions),
3699
- recentPosition: num$1(r.recentPosition),
3700
- baselineClicks: Math.round(num$1(r.baselineClicks)),
3701
- baselineImpressions: Math.round(num$1(r.baselineImpressions)),
3702
- baselinePosition: num$1(r.baselinePosition),
3703
- clicksChange: num$1(r.clicksChange),
3704
- clicksChangePercent: num$1(r.clicksChangePercent),
3705
- impressionsChangePercent: num$1(r.impressionsChangePercent),
3706
- positionChange: num$1(r.positionChange),
3707
- direction: str$7(r.direction),
3708
- series: parseJsonList$4(r.seriesJson).map((s) => ({
3709
- week: str$7(s.week),
3710
- clicks: num$1(s.clicks),
3711
- impressions: num$1(s.impressions)
3712
- }))
3713
- }));
3714
- const rising = normalized.filter((r) => r.direction === "rising");
3715
- const declining = normalized.filter((r) => r.direction === "declining");
3716
- const stable = normalized.filter((r) => r.direction === "stable");
3717
- const combined = [...rising, ...declining];
3718
- return {
3719
- results: combined,
3720
- meta: {
3721
- total: combined.length,
3722
- rising: rising.length,
3723
- declining: declining.length,
3724
- stable: stable.length
3725
- }
3726
- };
3756
+ reduceSql(rows, params) {
3757
+ const normalized = (Array.isArray(rows) ? rows : []).map((r) => {
3758
+ const recentClicks = num$1(r.recentClicks);
3759
+ const recentImpressions = num$1(r.recentImpressions);
3760
+ const recentPosition = num$1(r.recentPosition);
3761
+ const baselineClicks = Math.round(num$1(r.baselineClicks));
3762
+ const baselineImpressions = Math.round(num$1(r.baselineImpressions));
3763
+ const baselinePosition = num$1(r.baselinePosition);
3764
+ return {
3765
+ keyword: str$7(r.keyword),
3766
+ page: r.page == null ? null : str$7(r.page),
3767
+ recentClicks,
3768
+ recentImpressions,
3769
+ recentPosition,
3770
+ baselineClicks,
3771
+ baselineImpressions,
3772
+ baselinePosition,
3773
+ clicksChange: num$1(r.clicksChange),
3774
+ clicksChangePercent: num$1(r.clicksChangePercent),
3775
+ impressionsChangePercent: num$1(r.impressionsChangePercent),
3776
+ positionChange: num$1(r.positionChange),
3777
+ direction: str$7(r.direction),
3778
+ series: parseJsonList$4(r.seriesJson).map((s) => ({
3779
+ week: str$7(s.week),
3780
+ clicks: num$1(s.clicks),
3781
+ impressions: num$1(s.impressions)
3782
+ }))
3783
+ };
3784
+ }).map(withCanonicalFields);
3785
+ return selectMoversDirection(normalized.filter((r) => r.direction === "rising"), normalized.filter((r) => r.direction === "declining"), normalized.filter((r) => r.direction === "stable"), params);
3727
3786
  },
3728
3787
  buildRows(params) {
3729
3788
  const { current, previous } = comparisonOf$1(params);
@@ -3744,43 +3803,15 @@ const moversAnalyzer = defineAnalyzer$1({
3744
3803
  changeThreshold: params.changeThreshold,
3745
3804
  minImpressions: params.minImpressions
3746
3805
  });
3747
- return {
3748
- results: [...result.rising.map((r) => ({
3749
- ...r,
3750
- direction: "rising"
3751
- })), ...result.declining.map((r) => ({
3752
- ...r,
3753
- direction: "declining"
3754
- }))],
3755
- meta: {
3756
- rising: result.rising.length,
3757
- declining: result.declining.length
3758
- }
3759
- };
3806
+ return selectMoversDirection(result.rising.map((r) => withCanonicalFields({
3807
+ ...r,
3808
+ direction: "rising"
3809
+ })), result.declining.map((r) => withCanonicalFields({
3810
+ ...r,
3811
+ direction: "declining"
3812
+ })), [], params);
3760
3813
  }
3761
3814
  });
3762
- const DEFAULT_LIMIT$1 = 1e3;
3763
- const MAX_LIMIT = 5e4;
3764
- function clampLimit(limit, fallback = DEFAULT_LIMIT$1) {
3765
- const n = Number(limit ?? fallback);
3766
- if (!Number.isFinite(n) || n <= 0) return fallback;
3767
- return Math.min(n, MAX_LIMIT);
3768
- }
3769
- function clampOffset(offset) {
3770
- const n = Number(offset ?? 0);
3771
- if (!Number.isFinite(n) || n < 0) return 0;
3772
- return Math.floor(n);
3773
- }
3774
- function paginateClause(input) {
3775
- const l = clampLimit(input.limit);
3776
- const o = clampOffset(input.offset);
3777
- return o > 0 ? `LIMIT ${l} OFFSET ${o}` : `LIMIT ${l}`;
3778
- }
3779
- function paginateInMemory(rows, input) {
3780
- const l = clampLimit(input.limit, rows.length);
3781
- const o = clampOffset(input.offset);
3782
- return rows.slice(o, o + l);
3783
- }
3784
3815
  const EXPECTED_CTR_BY_POSITION = {
3785
3816
  1: .3,
3786
3817
  2: .15,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "0.25.5",
4
+ "version": "0.25.7",
5
5
  "description": "GSC analyzers — striking-distance, opportunity, movers, decay, brand, clustering, concentration, seasonality. Pure row-based + DuckDB-native.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -70,9 +70,9 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "drizzle-orm": "1.0.0-rc.3",
73
- "@gscdump/engine": "0.25.5",
74
- "@gscdump/engine-gsc-api": "0.25.5",
75
- "gscdump": "0.25.5"
73
+ "@gscdump/engine": "0.25.7",
74
+ "@gscdump/engine-gsc-api": "0.25.7",
75
+ "gscdump": "0.25.7"
76
76
  },
77
77
  "devDependencies": {
78
78
  "vitest": "^4.1.7"