@gscdump/sdk 0.33.0 → 0.33.4

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.
package/dist/index.d.mts CHANGED
@@ -317,7 +317,7 @@ interface GscDailySummary {
317
317
  impressions: number;
318
318
  }>;
319
319
  }
320
- /** Fill `sum_position` from `position * impressions` when the row only carries `position`. */
320
+ /** Fill `sum_position` from `(position - 1) * impressions` when the row only carries `position`. */
321
321
  declare function coerceRowMetrics<T extends {
322
322
  impressions: number;
323
323
  sum_position?: number;
package/dist/index.mjs CHANGED
@@ -8,6 +8,125 @@ import { resolveWindow } from "@gscdump/engine/period";
8
8
  import { endOfMonth, format, startOfMonth, startOfQuarter, startOfWeek, subDays, subMonths } from "date-fns";
9
9
  import { CANONICAL_WEBHOOK_EVENTS, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_CONTRACT_VERSION_HEADER as WEBHOOK_CONTRACT_VERSION_HEADER$1, WEBHOOK_DELIVERY_HEADER, WEBHOOK_DELIVERY_HEADER as WEBHOOK_DELIVERY_HEADER$1, WEBHOOK_EVENT_HEADER, WEBHOOK_EVENT_HEADER as WEBHOOK_EVENT_HEADER$1, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_HEADER as WEBHOOK_SIGNATURE_HEADER$1, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TIMESTAMP_HEADER as WEBHOOK_TIMESTAMP_HEADER$1, partnerWebhookEnvelopeSchema } from "@gscdump/contracts";
10
10
  export * from "@gscdump/contracts";
11
+ function isAnalysisSourcesOptions(value) {
12
+ return !!value && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function withDefaultSearchType(value, searchType) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
16
+ const scoped = value;
17
+ return {
18
+ ...scoped,
19
+ searchType: searchType ?? scoped.searchType ?? "web"
20
+ };
21
+ }
22
+ function searchTypeQuery(searchType) {
23
+ return { searchType: searchType ?? "web" };
24
+ }
25
+ function dateRangeOptionsQuery(options) {
26
+ const query = {};
27
+ const start = options?.start ?? options?.startDate;
28
+ const end = options?.end ?? options?.endDate;
29
+ if (start) query.start = start;
30
+ if (end) query.end = end;
31
+ return query;
32
+ }
33
+ function sourceInfoQuery(options) {
34
+ return {
35
+ ...searchTypeQuery(options?.searchType),
36
+ ...dateRangeOptionsQuery(options)
37
+ };
38
+ }
39
+ function tablesQuery(tablesOrOptions, options) {
40
+ const tables = isAnalysisSourcesOptions(tablesOrOptions) ? tablesOrOptions.tables : tablesOrOptions;
41
+ const source = isAnalysisSourcesOptions(tablesOrOptions) ? tablesOrOptions : options;
42
+ const query = {
43
+ ...searchTypeQuery(source?.searchType),
44
+ ...dateRangeOptionsQuery(source)
45
+ };
46
+ if (tables) query.tables = Array.isArray(tables) ? tables.join(",") : tables;
47
+ return query;
48
+ }
49
+ function dataQuery(state, options) {
50
+ const opts = options;
51
+ const scoped = withDefaultSearchType(state, opts?.searchType);
52
+ const query = {
53
+ q: JSON.stringify(scoped),
54
+ searchType: scoped.searchType ?? "web"
55
+ };
56
+ if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
57
+ if (opts?.filter) query.filter = opts.filter;
58
+ return query;
59
+ }
60
+ function dataDetailQuery(state, options) {
61
+ const opts = options;
62
+ const scoped = withDefaultSearchType(state, opts?.searchType);
63
+ const query = {
64
+ q: JSON.stringify(scoped),
65
+ searchType: scoped.searchType ?? "web"
66
+ };
67
+ if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
68
+ return query;
69
+ }
70
+ function analysisQuery(params) {
71
+ const query = {
72
+ preset: params.preset,
73
+ startDate: params.startDate,
74
+ endDate: params.endDate
75
+ };
76
+ if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
77
+ if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
78
+ if (params.brandTerms) query.brandTerms = params.brandTerms;
79
+ if (params.limit != null) query.limit = params.limit;
80
+ if (params.offset != null) query.offset = params.offset;
81
+ if (params.search) query.search = params.search;
82
+ if (params.minImpressions != null) query.minImpressions = params.minImpressions;
83
+ if (params.minPosition != null) query.minPosition = params.minPosition;
84
+ if (params.maxPosition != null) query.maxPosition = params.maxPosition;
85
+ if (params.maxCtr != null) query.maxCtr = params.maxCtr;
86
+ query.searchType = params.searchType ?? "web";
87
+ return query;
88
+ }
89
+ function indexingUrlsQuery(params = {}) {
90
+ const query = {};
91
+ if (params.limit != null) query.limit = params.limit;
92
+ if (params.offset != null) query.offset = params.offset;
93
+ if (params.status) query.status = params.status;
94
+ if (params.issue) query.issue = params.issue;
95
+ if (params.search) query.search = params.search;
96
+ return query;
97
+ }
98
+ function indexingDiagnosticsQuery(params = {}) {
99
+ const query = {};
100
+ if (params.sampleIssues) query.sampleIssues = Array.isArray(params.sampleIssues) ? params.sampleIssues.join(",") : params.sampleIssues;
101
+ if (params.sampleLimit != null) query.sampleLimit = params.sampleLimit;
102
+ return query;
103
+ }
104
+ function dateRangeQuery(params) {
105
+ return {
106
+ startDate: params.startDate,
107
+ endDate: params.endDate
108
+ };
109
+ }
110
+ function queryTrendQuery(params) {
111
+ const query = {
112
+ startDate: params.startDate,
113
+ endDate: params.endDate,
114
+ searchType: params.searchType ?? "web"
115
+ };
116
+ if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
117
+ if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
118
+ return query;
119
+ }
120
+ function pageTrendQuery(params) {
121
+ const query = {
122
+ startDate: params.startDate,
123
+ endDate: params.endDate,
124
+ searchType: params.searchType ?? "web"
125
+ };
126
+ if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
127
+ if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
128
+ return query;
129
+ }
11
130
  var PartnerApiError = class extends Error {
12
131
  kind;
13
132
  statusCode;
@@ -110,59 +229,6 @@ function createHostedRequester(options, defaults) {
110
229
  shouldValidate: (phase) => shouldValidate(options, phase)
111
230
  };
112
231
  }
113
- const DEFAULT_SEARCH_TYPE$1 = "web";
114
- function isAnalysisSourcesOptions$1(value) {
115
- return !!value && typeof value === "object" && !Array.isArray(value);
116
- }
117
- function searchTypeQuery(searchType) {
118
- return { searchType: searchType ?? DEFAULT_SEARCH_TYPE$1 };
119
- }
120
- function dateRangeOptionsQuery(options) {
121
- const query = {};
122
- const start = options?.start ?? options?.startDate;
123
- const end = options?.end ?? options?.endDate;
124
- if (start) query.start = start;
125
- if (end) query.end = end;
126
- return query;
127
- }
128
- function sourceInfoQuery(options) {
129
- return {
130
- ...searchTypeQuery(options?.searchType),
131
- ...dateRangeOptionsQuery(options)
132
- };
133
- }
134
- function tablesQuery$1(tablesOrOptions, options) {
135
- const tables = isAnalysisSourcesOptions$1(tablesOrOptions) ? tablesOrOptions.tables : tablesOrOptions;
136
- const source = isAnalysisSourcesOptions$1(tablesOrOptions) ? tablesOrOptions : options;
137
- const query = {
138
- ...searchTypeQuery(source?.searchType),
139
- ...dateRangeOptionsQuery(source)
140
- };
141
- if (tables) query.tables = Array.isArray(tables) ? tables.join(",") : tables;
142
- return query;
143
- }
144
- function withDefaultSearchType$1(value) {
145
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
146
- return {
147
- ...value,
148
- searchType: value.searchType ?? DEFAULT_SEARCH_TYPE$1
149
- };
150
- }
151
- function indexingUrlsQuery$1(params = {}) {
152
- const query = {};
153
- if (params.limit != null) query.limit = params.limit;
154
- if (params.offset != null) query.offset = params.offset;
155
- if (params.status) query.status = params.status;
156
- if (params.issue) query.issue = params.issue;
157
- if (params.search) query.search = params.search;
158
- return query;
159
- }
160
- function indexingDiagnosticsQuery$1(params = {}) {
161
- const query = {};
162
- if (params.sampleIssues) query.sampleIssues = Array.isArray(params.sampleIssues) ? params.sampleIssues.join(",") : params.sampleIssues;
163
- if (params.sampleLimit != null) query.sampleLimit = params.sampleLimit;
164
- return query;
165
- }
166
232
  function createAnalyticsClient(options = {}) {
167
233
  const { request, shouldValidate } = createHostedRequester(options, { apiBase: "" });
168
234
  return {
@@ -176,18 +242,18 @@ function createAnalyticsClient(options = {}) {
176
242
  return request(analyticsRoutes.site.sourceInfo(siteId), { query: sourceInfoQuery(options) }, analyticsEndpointSchemas.analyticsSourceInfo.response);
177
243
  },
178
244
  getAnalysisSources(siteId, tables, options) {
179
- return request(analyticsRoutes.site.analysisSources(siteId), { query: tablesQuery$1(tables, options) }, analyticsEndpointSchemas.analyticsAnalysisSources.response);
245
+ return request(analyticsRoutes.site.analysisSources(siteId), { query: tablesQuery(tables, options) }, analyticsEndpointSchemas.analyticsAnalysisSources.response);
180
246
  },
181
247
  analyze(siteId, params) {
182
248
  return request(analyticsRoutes.site.analyze(siteId), {
183
249
  method: "POST",
184
- body: withDefaultSearchType$1(params)
250
+ body: withDefaultSearchType(params)
185
251
  });
186
252
  },
187
253
  queryRows(siteId, state) {
188
254
  return request(analyticsRoutes.site.rows(siteId), {
189
255
  method: "POST",
190
- body: withDefaultSearchType$1(state)
256
+ body: withDefaultSearchType(state)
191
257
  }, analyticsEndpointSchemas.analyticsRows.response);
192
258
  },
193
259
  getRollup(siteId, rollupId, params) {
@@ -216,11 +282,11 @@ function createAnalyticsClient(options = {}) {
216
282
  return request(analyticsRoutes.site.inspectionHistory(siteId, hash), {}, analyticsEndpointSchemas.analyticsInspectionHistory.response);
217
283
  },
218
284
  getIndexingUrls(siteId, params = {}) {
219
- return request(analyticsRoutes.site.indexingUrls(siteId), { query: indexingUrlsQuery$1(params) }, analyticsEndpointSchemas.analyticsIndexingUrls.response);
285
+ return request(analyticsRoutes.site.indexingUrls(siteId), { query: indexingUrlsQuery(params) }, analyticsEndpointSchemas.analyticsIndexingUrls.response);
220
286
  },
221
287
  getIndexingDiagnostics(siteId, params = {}) {
222
288
  const parsed = shouldValidate("request") ? analyticsEndpointSchemas.analyticsIndexingDiagnostics.query.parse(params) : params;
223
- return request(analyticsRoutes.site.indexingDiagnostics(siteId), { query: indexingDiagnosticsQuery$1(parsed) }, analyticsEndpointSchemas.analyticsIndexingDiagnostics.response);
289
+ return request(analyticsRoutes.site.indexingDiagnostics(siteId), { query: indexingDiagnosticsQuery(parsed) }, analyticsEndpointSchemas.analyticsIndexingDiagnostics.response);
224
290
  },
225
291
  requestIndexingInspect(siteId, body) {
226
292
  const parsed = shouldValidate("request") ? analyticsEndpointSchemas.analyticsIndexingInspect.body.parse(body) : body;
@@ -337,35 +403,6 @@ function findLifecycleSite(lifecycle, siteIdOrPropertyUrl) {
337
403
  const propertyKey = normalizeGscPropertyKey(siteIdOrPropertyUrl);
338
404
  return lifecycle.sites.find((site) => site.siteId === siteIdOrPropertyUrl || site.externalSiteId === siteIdOrPropertyUrl || !!propertyKey && normalizeGscPropertyKey(site.gscPropertyUrl) === propertyKey || !site.gscPropertyUrl && normalizeLifecycleUrl(site.requestedUrl) === normalized || !propertyKey && normalizeLifecycleUrl(site.requestedUrl) === normalized) ?? null;
339
405
  }
340
- const DEFAULT_SEARCH_TYPE = "web";
341
- function withDefaultSearchType(state, searchType) {
342
- const scoped = state;
343
- return {
344
- ...state,
345
- searchType: searchType ?? scoped.searchType ?? DEFAULT_SEARCH_TYPE
346
- };
347
- }
348
- function dataQuery(state, options) {
349
- const opts = options;
350
- const scoped = withDefaultSearchType(state, opts?.searchType);
351
- const query = {
352
- q: JSON.stringify(scoped),
353
- searchType: scoped.searchType ?? DEFAULT_SEARCH_TYPE
354
- };
355
- if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
356
- if (opts?.filter) query.filter = opts.filter;
357
- return query;
358
- }
359
- function dataDetailQuery(state, options) {
360
- const opts = options;
361
- const scoped = withDefaultSearchType(state, opts?.searchType);
362
- const query = {
363
- q: JSON.stringify(scoped),
364
- searchType: scoped.searchType ?? DEFAULT_SEARCH_TYPE
365
- };
366
- if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
367
- return query;
368
- }
369
406
  function validateAnalysisParamsResult(params) {
370
407
  if ((params.preset === "non-brand" || params.preset === "brand-only") && !params.brandTerms?.trim()) return err(new PartnerApiError({
371
408
  kind: "validation",
@@ -377,80 +414,6 @@ function validateAnalysisParamsResult(params) {
377
414
  function assertAnalysisParams(params) {
378
415
  unwrapResult(validateAnalysisParamsResult(params), partnerErrorToException);
379
416
  }
380
- function analysisQuery(params) {
381
- const query = {
382
- preset: params.preset,
383
- startDate: params.startDate,
384
- endDate: params.endDate
385
- };
386
- if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
387
- if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
388
- if (params.brandTerms) query.brandTerms = params.brandTerms;
389
- if (params.limit != null) query.limit = params.limit;
390
- if (params.offset != null) query.offset = params.offset;
391
- if (params.search) query.search = params.search;
392
- if (params.minImpressions != null) query.minImpressions = params.minImpressions;
393
- if (params.minPosition != null) query.minPosition = params.minPosition;
394
- if (params.maxPosition != null) query.maxPosition = params.maxPosition;
395
- if (params.maxCtr != null) query.maxCtr = params.maxCtr;
396
- query.searchType = params.searchType ?? DEFAULT_SEARCH_TYPE;
397
- return query;
398
- }
399
- function indexingUrlsQuery(params = {}) {
400
- const query = {};
401
- if (params.limit != null) query.limit = params.limit;
402
- if (params.offset != null) query.offset = params.offset;
403
- if (params.status) query.status = params.status;
404
- if (params.issue) query.issue = params.issue;
405
- if (params.search) query.search = params.search;
406
- return query;
407
- }
408
- function indexingDiagnosticsQuery(params = {}) {
409
- const query = {};
410
- if (params.sampleIssues) query.sampleIssues = Array.isArray(params.sampleIssues) ? params.sampleIssues.join(",") : params.sampleIssues;
411
- if (params.sampleLimit != null) query.sampleLimit = params.sampleLimit;
412
- return query;
413
- }
414
- function isAnalysisSourcesOptions(value) {
415
- return !!value && typeof value === "object" && !Array.isArray(value);
416
- }
417
- function tablesQuery(tablesOrOptions, options) {
418
- const tables = isAnalysisSourcesOptions(tablesOrOptions) ? tablesOrOptions.tables : tablesOrOptions;
419
- const source = isAnalysisSourcesOptions(tablesOrOptions) ? tablesOrOptions : options;
420
- const query = { searchType: source?.searchType ?? DEFAULT_SEARCH_TYPE };
421
- const start = source?.start ?? source?.startDate;
422
- const end = source?.end ?? source?.endDate;
423
- if (start) query.start = start;
424
- if (end) query.end = end;
425
- if (tables) query.tables = Array.isArray(tables) ? tables.join(",") : tables;
426
- return query;
427
- }
428
- function dateRangeQuery(params) {
429
- return {
430
- startDate: params.startDate,
431
- endDate: params.endDate
432
- };
433
- }
434
- function queryTrendQuery(params) {
435
- const query = {
436
- startDate: params.startDate,
437
- endDate: params.endDate,
438
- searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
439
- };
440
- if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
441
- if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
442
- return query;
443
- }
444
- function pageTrendQuery(params) {
445
- const query = {
446
- startDate: params.startDate,
447
- endDate: params.endDate,
448
- searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
449
- };
450
- if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
451
- if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
452
- return query;
453
- }
454
417
  function sleep(ms) {
455
418
  return new Promise((resolve) => setTimeout(resolve, ms));
456
419
  }
@@ -663,7 +626,7 @@ function createPartnerClient(options = {}) {
663
626
  getKeywordSparklines(siteId, params) {
664
627
  const withSearchType = {
665
628
  ...params,
666
- searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
629
+ searchType: params.searchType ?? "web"
667
630
  };
668
631
  const body = shouldValidate("request") ? partnerEndpointSchemas.getKeywordSparklines.body.parse(withSearchType) : withSearchType;
669
632
  return request(partnerRoutes.sites.keywordSparklines(siteId), {
@@ -1023,7 +986,7 @@ const GSC_COLUMN_OPTIONS = [
1023
986
  function coerceRowMetrics(row) {
1024
987
  return {
1025
988
  ...row,
1026
- sum_position: row.sum_position ?? (row.position ?? 0) * row.impressions
989
+ sum_position: row.sum_position ?? (Math.max(1, row.position ?? 0) - 1) * row.impressions
1027
990
  };
1028
991
  }
1029
992
  function summarizeDailyRows(raw) {
@@ -1305,7 +1268,7 @@ const CALENDAR_TO_UPSTREAM = {
1305
1268
  "this-month": "mtd",
1306
1269
  "this-year": "ytd"
1307
1270
  };
1308
- function fmt$1(d) {
1271
+ function fmt(d) {
1309
1272
  return format(d, "yyyy-MM-dd");
1310
1273
  }
1311
1274
  function buildResultFromIso(start, end) {
@@ -1349,7 +1312,7 @@ function periodToDateRange(period, stableData = true) {
1349
1312
  }
1350
1313
  const today = todayInPST();
1351
1314
  const end = stableData ? subDays(today, 3) : subDays(today, 1);
1352
- const endIso = fmt$1(end);
1315
+ const endIso = fmt(end);
1353
1316
  const upstreamPreset = ROLLING_TO_UPSTREAM[period] ?? CALENDAR_TO_UPSTREAM[period];
1354
1317
  if (upstreamPreset) {
1355
1318
  const win = resolveWindow({
@@ -1365,14 +1328,14 @@ function periodToDateRange(period, stableData = true) {
1365
1328
  break;
1366
1329
  case "last-month": {
1367
1330
  const prevMonth = subMonths(end, 1);
1368
- return buildResultFromIso(fmt$1(startOfMonth(prevMonth)), fmt$1(endOfMonth(prevMonth)));
1331
+ return buildResultFromIso(fmt(startOfMonth(prevMonth)), fmt(endOfMonth(prevMonth)));
1369
1332
  }
1370
1333
  case "this-quarter":
1371
1334
  start = startOfQuarter(end);
1372
1335
  break;
1373
1336
  default: start = subDays(end, 27);
1374
1337
  }
1375
- return buildResultFromIso(fmt$1(start), endIso);
1338
+ return buildResultFromIso(fmt(start), endIso);
1376
1339
  }
1377
1340
  function periodToDays(period) {
1378
1341
  return periodToDateRange(period).days;
@@ -1576,6 +1539,14 @@ function createPartnerRealtimeClient(options) {
1576
1539
  };
1577
1540
  }
1578
1541
  const createGscdumpRealtimeClient = createPartnerRealtimeClient;
1542
+ function formatSearchConsoleCount(value) {
1543
+ return new Intl.NumberFormat("en").format(Math.max(0, Math.round(value)));
1544
+ }
1545
+ function countSearchConsoleIssues(issues, ...types) {
1546
+ if (!issues?.length) return 0;
1547
+ const wanted = new Set(types);
1548
+ return issues.reduce((sum, issue) => sum + (wanted.has(issue.type) ? issue.count : 0), 0);
1549
+ }
1579
1550
  const KNOWN_SITE_TYPES = /* @__PURE__ */ new Set([
1580
1551
  "saas",
1581
1552
  "ecommerce",
@@ -1687,19 +1658,14 @@ function deriveSiteBaseline(rawType, peer = {}) {
1687
1658
  goalKind: baseline.primaryGoal
1688
1659
  };
1689
1660
  }
1690
- function formatCount(value) {
1691
- return new Intl.NumberFormat("en").format(Math.max(0, Math.round(value)));
1692
- }
1693
- function issueCount$1(issues, ...types) {
1694
- if (!issues?.length) return 0;
1695
- const wanted = new Set(types);
1696
- return issues.reduce((sum, issue) => sum + (wanted.has(issue.type) ? issue.count : 0), 0);
1697
- }
1698
1661
  function totalSitemapErrors(sitemaps) {
1699
1662
  return (sitemaps ?? []).reduce((sum, sitemap) => {
1700
1663
  return sum + (sitemap.errors ?? 0) + (sitemap.lastError ? 1 : 0);
1701
1664
  }, 0);
1702
1665
  }
1666
+ function signedPct1(value) {
1667
+ return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`;
1668
+ }
1703
1669
  function stage(key, evidence) {
1704
1670
  return {
1705
1671
  key,
@@ -1841,11 +1807,11 @@ function classifySearchConsoleStage(input) {
1841
1807
  source: "indexing"
1842
1808
  }]);
1843
1809
  const sitemapErrors = totalSitemapErrors(sitemaps);
1844
- const unknown = issueCount$1(issues, "unknown_to_google");
1845
- const discovered = issueCount$1(issues, "discovered_not_indexed");
1846
- const crawled = issueCount$1(issues, "crawled_not_indexed");
1847
- const hardBlocks = issueCount$1(issues, "blocked_robots", "server_error", "access_denied", "forbidden") + (input.crawlAuditBlockerCount ?? 0);
1848
- const canonicalMismatches = Math.min(notIndexed, input.canonicalMismatchCount ?? issueCount$1(issues, "canonical_mismatch"));
1810
+ const unknown = countSearchConsoleIssues(issues, "unknown_to_google");
1811
+ const discovered = countSearchConsoleIssues(issues, "discovered_not_indexed");
1812
+ const crawled = countSearchConsoleIssues(issues, "crawled_not_indexed");
1813
+ const hardBlocks = countSearchConsoleIssues(issues, "blocked_robots", "server_error", "access_denied", "forbidden") + (input.crawlAuditBlockerCount ?? 0);
1814
+ const canonicalMismatches = Math.min(notIndexed, input.canonicalMismatchCount ?? countSearchConsoleIssues(issues, "canonical_mismatch"));
1849
1815
  const visibleNoClickPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && page.clicks === 0).length;
1850
1816
  const poorPositionPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && (page.position ?? 0) > 20).length;
1851
1817
  const ctrOutlierCount = input.ctrOutlierCount ?? 0;
@@ -1862,11 +1828,11 @@ function classifySearchConsoleStage(input) {
1862
1828
  const isEstablished = impressions28d != null && impressions28d >= ESTABLISHED_IMPRESSIONS_28D$1;
1863
1829
  if (hardBlocks > Math.max(10, totalUrls * .05)) return stage("crawl_blocked", [{
1864
1830
  label: "Crawl / on-page faults",
1865
- value: formatCount(hardBlocks),
1831
+ value: formatSearchConsoleCount(hardBlocks),
1866
1832
  source: "indexing"
1867
1833
  }, {
1868
1834
  label: "Indexed pages",
1869
- value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
1835
+ value: `${formatSearchConsoleCount(indexed)} of ${formatSearchConsoleCount(totalUrls)}`,
1870
1836
  source: "indexing"
1871
1837
  }]);
1872
1838
  if (isDeclining) return stage("declining_visibility", [{
@@ -1881,60 +1847,60 @@ function classifySearchConsoleStage(input) {
1881
1847
  if (isGrowing) return stage("healthy_growth_ready", [
1882
1848
  ...hardBlocks > 0 ? [{
1883
1849
  label: "Critical blockers",
1884
- value: formatCount(hardBlocks),
1850
+ value: formatSearchConsoleCount(hardBlocks),
1885
1851
  source: "indexing"
1886
1852
  }] : [],
1887
1853
  ...clicks90d != null ? [{
1888
1854
  label: "Clicks 90d",
1889
- value: `+${clicks90d.toFixed(1)}%`,
1855
+ value: signedPct1(clicks90d),
1890
1856
  source: "performance"
1891
1857
  }] : [],
1892
1858
  ...imp90d != null ? [{
1893
1859
  label: "Impressions 90d",
1894
- value: `+${imp90d.toFixed(1)}%`,
1860
+ value: signedPct1(imp90d),
1895
1861
  source: "performance"
1896
1862
  }] : [],
1897
1863
  ...(input.recoverableBacklinkCount ?? 0) > 0 ? [{
1898
1864
  label: "Recoverable backlinks",
1899
- value: formatCount(input.recoverableBacklinkCount),
1865
+ value: formatSearchConsoleCount(input.recoverableBacklinkCount),
1900
1866
  source: "performance"
1901
1867
  }] : [],
1902
1868
  ...(input.competitorGapCount ?? 0) > 0 ? [{
1903
1869
  label: "Competitor content gaps",
1904
- value: formatCount(input.competitorGapCount),
1870
+ value: formatSearchConsoleCount(input.competitorGapCount),
1905
1871
  source: "performance"
1906
1872
  }] : []
1907
1873
  ]);
1908
1874
  if (crawled > Math.max(10, totalUrls * .3) && totalUrls > 500) return stage("index_rejection", [{
1909
1875
  label: "Crawled, not indexed",
1910
- value: formatCount(crawled),
1876
+ value: formatSearchConsoleCount(crawled),
1911
1877
  source: "indexing"
1912
1878
  }, {
1913
1879
  label: "Not indexed",
1914
- value: formatCount(notIndexed),
1880
+ value: formatSearchConsoleCount(notIndexed),
1915
1881
  source: "indexing"
1916
1882
  }]);
1917
1883
  if (isNascent) return stage("waiting_for_data", [{
1918
1884
  label: "Impressions (28d)",
1919
- value: formatCount(impressions28d ?? 0),
1885
+ value: formatSearchConsoleCount(impressions28d ?? 0),
1920
1886
  source: "performance"
1921
1887
  }, {
1922
1888
  label: "Indexed pages",
1923
- value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
1889
+ value: `${formatSearchConsoleCount(indexed)} of ${formatSearchConsoleCount(totalUrls)}`,
1924
1890
  source: "indexing"
1925
1891
  }]);
1926
1892
  if (!isEstablished && (sitemaps.length === 0 || sitemapErrors > 0 || unknown > Math.max(5, totalUrls * .1))) return stage("weak_discovery", [{
1927
1893
  label: "Sitemaps",
1928
- value: sitemaps.length === 0 ? "None registered" : `${formatCount(sitemapErrors)} errors`,
1894
+ value: sitemaps.length === 0 ? "None registered" : `${formatSearchConsoleCount(sitemapErrors)} errors`,
1929
1895
  source: "sitemap"
1930
1896
  }, ...unknown > 0 ? [{
1931
1897
  label: "Unknown URLs",
1932
- value: formatCount(unknown),
1898
+ value: formatSearchConsoleCount(unknown),
1933
1899
  source: "indexing"
1934
1900
  }] : []]);
1935
1901
  if (discovered > Math.max(10, totalUrls * .15)) return stage("discovery_backlog", [{
1936
1902
  label: "Discovered, not crawled",
1937
- value: formatCount(discovered),
1903
+ value: formatSearchConsoleCount(discovered),
1938
1904
  source: "indexing"
1939
1905
  }, {
1940
1906
  label: "Indexed pages",
@@ -1943,7 +1909,7 @@ function classifySearchConsoleStage(input) {
1943
1909
  }]);
1944
1910
  if (canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [{
1945
1911
  label: "Canonical mismatches",
1946
- value: formatCount(canonicalMismatches),
1912
+ value: formatSearchConsoleCount(canonicalMismatches),
1947
1913
  source: "canonical"
1948
1914
  }]);
1949
1915
  const lowCtrType = siteTypeBaseline(input.siteType).ctrExpectation === "low";
@@ -1951,16 +1917,16 @@ function classifySearchConsoleStage(input) {
1951
1917
  const noClickTrigger = visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * noClickShare);
1952
1918
  if (ctrOutlierCount > 0 && !lowCtrType || noClickTrigger) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
1953
1919
  label: "CTR outliers",
1954
- value: formatCount(ctrOutlierCount),
1920
+ value: formatSearchConsoleCount(ctrOutlierCount),
1955
1921
  source: "performance"
1956
1922
  }] : [], ...visibleNoClickPages > 0 ? [{
1957
1923
  label: "Visible, no clicks",
1958
- value: formatCount(visibleNoClickPages),
1924
+ value: formatSearchConsoleCount(visibleNoClickPages),
1959
1925
  source: "performance"
1960
1926
  }] : []]);
1961
1927
  if (poorPositionPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("ranking_stalled", [{
1962
1928
  label: "Low-ranking visible pages",
1963
- value: formatCount(poorPositionPages),
1929
+ value: formatSearchConsoleCount(poorPositionPages),
1964
1930
  source: "performance"
1965
1931
  }]);
1966
1932
  return stage("healthy_growth_ready", [{
@@ -1969,7 +1935,7 @@ function classifySearchConsoleStage(input) {
1969
1935
  source: "indexing"
1970
1936
  }, {
1971
1937
  label: "Critical blockers",
1972
- value: formatCount(hardBlocks),
1938
+ value: formatSearchConsoleCount(hardBlocks),
1973
1939
  source: "indexing"
1974
1940
  }]);
1975
1941
  }
@@ -2001,14 +1967,6 @@ function reachLivenessRatio(daily) {
2001
1967
  for (let i = 6; i < series.length; i++) peakWeek = Math.max(peakWeek, rolling7(i));
2002
1968
  return peakWeek > 0 ? latestWeek / peakWeek : null;
2003
1969
  }
2004
- function issueCount(issues, ...types) {
2005
- if (!issues?.length) return 0;
2006
- const wanted = new Set(types);
2007
- return issues.reduce((sum, i) => sum + (wanted.has(i.type) ? i.count : 0), 0);
2008
- }
2009
- function fmt(n) {
2010
- return new Intl.NumberFormat("en").format(Math.max(0, Math.round(n)));
2011
- }
2012
1970
  function clamp01(n) {
2013
1971
  if (!Number.isFinite(n)) return 0;
2014
1972
  return Math.min(1, Math.max(0, n));
@@ -2083,11 +2041,11 @@ function isIntentionalRetirementSite(input, notFound, serverError) {
2083
2041
  function classifyHealthStage(input) {
2084
2042
  const issues = input.issues ?? [];
2085
2043
  const totalUrls = input.totalUrls ?? 0;
2086
- const noindex = issueCount(issues, "noindex");
2087
- const notFound = issueCount(issues, "not_found");
2088
- const softFound = issueCount(issues, "soft_404");
2089
- const serverError = issueCount(issues, "server_error", "blocked_robots", "access_denied", "forbidden");
2090
- const crawledNotIndexed = issueCount(issues, "crawled_not_indexed");
2044
+ const noindex = countSearchConsoleIssues(issues, "noindex");
2045
+ const notFound = countSearchConsoleIssues(issues, "not_found");
2046
+ const softFound = countSearchConsoleIssues(issues, "soft_404");
2047
+ const serverError = countSearchConsoleIssues(issues, "server_error", "blocked_robots", "access_denied", "forbidden");
2048
+ const crawledNotIndexed = countSearchConsoleIssues(issues, "crawled_not_indexed");
2091
2049
  const intentionalDead = isIntentionalRetirementSite(input, notFound, serverError) ? notFound : 0;
2092
2050
  const indexableUrls = Math.max(1, totalUrls - noindex - intentionalDead);
2093
2051
  const hardBlocks = serverError + (input.crawlAuditBlockerCount ?? 0);
@@ -2099,9 +2057,9 @@ function classifyHealthStage(input) {
2099
2057
  ...HEALTH_COPY.crawl_faults,
2100
2058
  evidence: [{
2101
2059
  label: "Access faults (5xx / broken)",
2102
- value: fmt(hardBlocks)
2060
+ value: formatSearchConsoleCount(hardBlocks)
2103
2061
  }],
2104
- progression: escapeReduce("healthy", "access faults", hardBlocks, faultTarget, `${fmt(hardBlocks)} faults — fix ~${fmt(toFix)} to clear the gate`)
2062
+ progression: escapeReduce("healthy", "access faults", hardBlocks, faultTarget, `${formatSearchConsoleCount(hardBlocks)} faults — fix ~${formatSearchConsoleCount(toFix)} to clear the gate`)
2105
2063
  };
2106
2064
  }
2107
2065
  const rejectPool = crawledNotIndexed + softFound;
@@ -2113,12 +2071,12 @@ function classifyHealthStage(input) {
2113
2071
  ...HEALTH_COPY.quality_rejection,
2114
2072
  evidence: [{
2115
2073
  label: "Crawled, then refused",
2116
- value: fmt(rejectPool)
2074
+ value: formatSearchConsoleCount(rejectPool)
2117
2075
  }, {
2118
2076
  label: "Share of known URLs",
2119
2077
  value: `${(share * 100).toFixed(0)}%`
2120
2078
  }],
2121
- progression: escapeReduce("healthy", "crawled-rejected share", share, REJECT_SHARE, `${(share * 100).toFixed(0)}% rejected — improve/consolidate ~${fmt(toClear)} pages to clear`)
2079
+ progression: escapeReduce("healthy", "crawled-rejected share", share, REJECT_SHARE, `${(share * 100).toFixed(0)}% rejected — improve/consolidate ~${formatSearchConsoleCount(toClear)} pages to clear`)
2122
2080
  };
2123
2081
  }
2124
2082
  return {
@@ -2178,8 +2136,8 @@ function classifyReachStage(input) {
2178
2136
  const liveness = input.livenessRatio ?? null;
2179
2137
  if (imp12m != null && imp12m < LIFETIME_FLOOR) return reach("waiting_for_data", [{
2180
2138
  label: "Impressions (12m)",
2181
- value: fmt(imp12m)
2182
- }], advance("emerging", "lifetime impressions", imp12m, LIFETIME_FLOOR, `${fmt(imp12m)} of ${fmt(LIFETIME_FLOOR)} lifetime impressions — collecting data, keep indexing`));
2139
+ value: formatSearchConsoleCount(imp12m)
2140
+ }], advance("emerging", "lifetime impressions", imp12m, LIFETIME_FLOOR, `${formatSearchConsoleCount(imp12m)} of ${formatSearchConsoleCount(LIFETIME_FLOOR)} lifetime impressions — collecting data, keep indexing`));
2183
2141
  const hadRealReach = (imp12m ?? imp28d) > LIFETIME_FLOOR;
2184
2142
  const isGrowing = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT || imp90dPct != null && imp90dPct > GROWTH_IMPRESSIONS_PCT && posDelta != null && posDelta < 0;
2185
2143
  if (hadRealReach && liveness != null && liveness < FADED_LIVENESS && !isGrowing) return reach("faded", [{
@@ -2202,38 +2160,40 @@ function classifyReachStage(input) {
2202
2160
  direction: "escape"
2203
2161
  });
2204
2162
  if (isGrowing) {
2205
- const growthPct = clicks90dPct ?? imp90dPct ?? 0;
2206
- const growthLabel = clicks90dPct != null ? `${pctStr(clicks90dPct)} 90d clicks — comfortably growing; defend & expand` : `${pctStr(growthPct)} 90d impressions comfortably growing; defend & expand`;
2163
+ const clickGrowth = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT;
2164
+ const growthPct = clickGrowth ? clicks90dPct : imp90dPct ?? clicks90dPct ?? 0;
2165
+ const growthMetric = clickGrowth ? "90d clicks growth" : "90d impressions growth";
2166
+ const growthLabel = clickGrowth ? `${pctStr(clicks90dPct)} 90d clicks — comfortably growing; defend & expand` : `${pctStr(growthPct)} 90d impressions — comfortably growing; defend & expand`;
2207
2167
  return reach("growing", [...clicks90dPct != null ? [{
2208
2168
  label: "Clicks 90d",
2209
- value: `+${clicks90dPct.toFixed(0)}%`
2169
+ value: pctStr(clicks90dPct)
2210
2170
  }] : [], ...imp90dPct != null ? [{
2211
2171
  label: "Impressions 90d",
2212
- value: `+${imp90dPct.toFixed(0)}%`
2213
- }] : []], sustain("90d clicks growth", growthPct, growthLabel));
2172
+ value: pctStr(imp90dPct)
2173
+ }] : []], sustain(growthMetric, growthPct, growthLabel));
2214
2174
  }
2215
2175
  const ctr = clicks28d != null && imp28d > 0 ? clicks28d / imp28d : null;
2216
2176
  if (hadRealReach && imp28d >= NASCENT_IMPRESSIONS_28D && ctr != null && ctr < DECAY_CTR) return reach("decayed", [{
2217
2177
  label: "Impressions (28d)",
2218
- value: fmt(imp28d)
2178
+ value: formatSearchConsoleCount(imp28d)
2219
2179
  }, {
2220
2180
  label: "Clicks (28d)",
2221
- value: fmt(clicks28d ?? 0)
2222
- }], escapeToward("growing", "CTR (clicks/impressions)", ctr, DECAY_CTR, `${(ctr * 100).toFixed(2)}% CTR on ${fmt(imp28d)} impressions — refresh content toward ~${(DECAY_CTR * 100).toFixed(1)}%`));
2181
+ value: formatSearchConsoleCount(clicks28d ?? 0)
2182
+ }], escapeToward("growing", "CTR (clicks/impressions)", ctr, DECAY_CTR, `${(ctr * 100).toFixed(2)}% CTR on ${formatSearchConsoleCount(imp28d)} impressions — refresh content toward ~${(DECAY_CTR * 100).toFixed(1)}%`));
2223
2183
  const growthVal = clicks90dPct ?? 0;
2224
2184
  const growthGap = Math.max(0, GROWTH_CLICKS_PCT - growthVal);
2225
2185
  const growthProgression = (stage) => advance("growing", "90d clicks growth", growthVal, GROWTH_CLICKS_PCT, clicks90dPct != null ? `${pctStr(growthVal)} 90d clicks — need ${pctStr(growthGap)} more to clear the +${GROWTH_CLICKS_PCT}% growth bar` : `${stage === "plateaued" ? "flat" : "rising"} — reach +${GROWTH_CLICKS_PCT}% 90d clicks growth to break into growing`);
2226
2186
  if (imp28d >= ESTABLISHED_IMPRESSIONS_28D) return reach("plateaued", [{
2227
2187
  label: "Impressions (28d)",
2228
- value: fmt(imp28d)
2188
+ value: formatSearchConsoleCount(imp28d)
2229
2189
  }], growthProgression("plateaued"));
2230
2190
  if (imp28d < NASCENT_IMPRESSIONS_28D) return reach("emerging", [{
2231
2191
  label: "Impressions (28d)",
2232
- value: fmt(imp28d)
2192
+ value: formatSearchConsoleCount(imp28d)
2233
2193
  }], growthProgression("emerging"));
2234
2194
  return reach("plateaued", [{
2235
2195
  label: "Impressions (28d)",
2236
- value: fmt(imp28d)
2196
+ value: formatSearchConsoleCount(imp28d)
2237
2197
  }], growthProgression("plateaued"));
2238
2198
  }
2239
2199
  function classifySiteTriage(input) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/sdk",
3
3
  "type": "module",
4
- "version": "0.33.0",
4
+ "version": "0.33.4",
5
5
  "description": "Consumer SDK for hosted gscdump.com integrations.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -44,10 +44,10 @@
44
44
  "date-fns": "^4.4.0",
45
45
  "ofetch": "^1.5.1",
46
46
  "zod": "^4.4.3",
47
- "@gscdump/contracts": "0.33.0",
48
- "@gscdump/engine": "0.33.0",
49
- "gscdump": "0.33.0",
50
- "@gscdump/analysis": "0.33.0"
47
+ "@gscdump/analysis": "0.33.4",
48
+ "@gscdump/contracts": "0.33.4",
49
+ "@gscdump/engine": "0.33.4",
50
+ "gscdump": "0.33.4"
51
51
  },
52
52
  "devDependencies": {
53
53
  "typescript": "^6.0.3",