@gscdump/engine-gsc-api 4.2.3 → 4.3.0

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
@@ -2,5 +2,5 @@ import { CreateLiveGscSourceOptions, canProxyToGsc, createLiveGscSource } from "
2
2
  import { FetchTopNOptions, GscDailyRow, GscRange, GscTopNRow, fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
3
3
  import { GSC_API_CAPABILITIES, GscApiQuerySourceOptions, createGscApiQuerySource } from "./source.mjs";
4
4
  import { GscSyncEntryWork, GscSyncFanoutEntry, GscSyncLedgerRow, GscSyncWindow, GscSyncWorkPlan, PlanGscBackfillDatesOptions, PlanGscBackwardDatesOptions, PlanGscSyncWorkOptions, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, tablesCoveredByGscSync } from "./sync-plan.mjs";
5
- import { GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SyncSliceDimensionFilter, SyncSliceDomainFilter, runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
6
- export { type CreateLiveGscSourceOptions, type FetchTopNOptions, GSC_API_CAPABILITIES, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscSyncEntryWork, type GscSyncFanoutEntry, type GscSyncLedgerRow, type GscSyncWindow, type GscSyncWorkPlan, type GscTopNRow, type PlanGscBackfillDatesOptions, type PlanGscBackwardDatesOptions, type PlanGscSyncWorkOptions, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
5
+ import { GscAggregation, GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SyncSliceDimensionFilter, SyncSliceDomainFilter, hostPagePattern, requestAggregation, runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
6
+ export { type CreateLiveGscSourceOptions, type FetchTopNOptions, GSC_API_CAPABILITIES, type GscAggregation, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscSyncEntryWork, type GscSyncFanoutEntry, type GscSyncLedgerRow, type GscSyncWindow, type GscSyncWorkPlan, type GscTopNRow, type PlanGscBackfillDatesOptions, type PlanGscBackwardDatesOptions, type PlanGscSyncWorkOptions, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, hostPagePattern, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, requestAggregation, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
2
2
  import { GSC_API_CAPABILITIES, createGscApiQuerySource } from "./source.mjs";
3
+ import { hostPagePattern, requestAggregation, runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
3
4
  import { canProxyToGsc, createLiveGscSource } from "./live.mjs";
4
5
  import { gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, tablesCoveredByGscSync } from "./sync-plan.mjs";
5
- import { runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
6
- export { GSC_API_CAPABILITIES, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
6
+ export { GSC_API_CAPABILITIES, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, hostPagePattern, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, requestAggregation, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
package/dist/live.d.mts CHANGED
@@ -20,5 +20,13 @@ export interface CreateLiveGscSourceOptions {
20
20
  * slice only. An explicit search type already present on the state wins.
21
21
  */
22
22
  searchType?: SearchType$1;
23
+ /**
24
+ * Scope every read to one exact host with the same `page` filter the sync
25
+ * applies. Set it whenever the sync filters, so live and stored rows count
26
+ * impressions the same way.
27
+ */
28
+ pageScope?: {
29
+ host: string;
30
+ };
23
31
  }
24
32
  export declare function createLiveGscSource(opts: CreateLiveGscSourceOptions): AnalysisQuerySource;
package/dist/live.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createGscApiQuerySource } from "./source.mjs";
2
+ import { hostPagePattern } from "./sync-slice.mjs";
2
3
  import { googleSearchConsole } from "gscdump";
3
- import { normalizeBuilderStateResult } from "gscdump/query";
4
+ import { and, normalizeBuilderStateResult, page, queryErrorToException, queryErrors, regex } from "gscdump/query";
4
5
  const PRO_ONLY_DIMENSIONS = /* @__PURE__ */ new Set(["queryCanonical", "page_keywords"]);
5
6
  function hasMatchingFilter(filter, matches) {
6
7
  return !!filter && (filter._filters.some((leaf) => matches(leaf.dimension)) || (filter._nestedGroups ?? []).some((group) => hasMatchingFilter(group, matches)));
@@ -17,6 +18,18 @@ function withSearchType(state, searchType) {
17
18
  searchType
18
19
  };
19
20
  }
21
+ function withPageScope(state, host) {
22
+ const parsed = normalizeBuilderStateResult(state);
23
+ if (!parsed.ok) throw queryErrorToException(parsed.error);
24
+ const base = parsed.value;
25
+ if (base.aggregationType === "byProperty") throw queryErrorToException(queryErrors.byPropertyNotAllowedWithPage());
26
+ if (base.aggregationType === "byNewsShowcasePanel") throw queryErrorToException(queryErrors.byNewsShowcaseNotAllowedWithPage());
27
+ const scope = regex(page, hostPagePattern(host));
28
+ return {
29
+ ...base,
30
+ filter: base.filter ? and(base.filter, scope) : scope
31
+ };
32
+ }
20
33
  function createLiveGscSource(opts) {
21
34
  let clientPromise = null;
22
35
  function getClient() {
@@ -37,7 +50,8 @@ function createLiveGscSource(opts) {
37
50
  },
38
51
  async queryRows(state) {
39
52
  const client = await getClient();
40
- const scopedState = opts.searchType !== void 0 ? withSearchType(state, opts.searchType) : state;
53
+ const typed = opts.searchType !== void 0 ? withSearchType(state, opts.searchType) : state;
54
+ const scopedState = opts.pageScope ? withPageScope(typed, opts.pageScope.host) : typed;
41
55
  return createGscApiQuerySource({
42
56
  client,
43
57
  siteUrl: opts.siteUrl
@@ -68,10 +68,18 @@ export interface RunGscSyncSliceOptions {
68
68
  rowsThisPage: number;
69
69
  }) => void;
70
70
  }
71
+ /**
72
+ * How GSC counted impressions for a request. Grouping or filtering by `page`
73
+ * counts one impression per ranking URL (`byPage`). Anything else counts one
74
+ * per search result (`byProperty`), which is what the Search Console UI shows.
75
+ */
76
+ export type GscAggregation = 'byPage' | 'byProperty';
71
77
  export interface RunGscSyncSliceResult {
72
78
  totalRows: number;
73
79
  hasMore: boolean;
74
80
  nextStartRow: number;
81
+ /** How GSC counted impressions for every row this slice wrote. */
82
+ aggregation: GscAggregation;
75
83
  /**
76
84
  * Metadata from the LAST GSC API page seen during this slice run. When
77
85
  * `dataState='hourly_all'` and grouped by `hour`, this surfaces
@@ -123,6 +131,8 @@ export type SearchAppearanceContinuation = {
123
131
  export interface RunGscSearchAppearanceContextSliceResult {
124
132
  appearances: string[];
125
133
  totalRows: number;
134
+ /** How GSC counted impressions for each table this run wrote rows to. */
135
+ aggregation: Partial<Record<'search_appearance' | SearchAppearanceContextTable, GscAggregation>>;
126
136
  hasMore: boolean;
127
137
  continuation?: SearchAppearanceContinuation;
128
138
  /**
@@ -133,6 +143,18 @@ export interface RunGscSearchAppearanceContextSliceResult {
133
143
  */
134
144
  metadata?: GscSearchAnalyticsMetadata;
135
145
  }
146
+ /** The `page` regex that scopes a request to one exact host. */
147
+ export declare function hostPagePattern(host: string): string;
148
+ /**
149
+ * The counting GSC should apply to a request with these dimensions, filters,
150
+ * and search type. The sync prefers the `responseAggregationType` GSC reports;
151
+ * this is the fallback when a response omits it.
152
+ */
153
+ export declare function requestAggregation(dimensions: readonly string[], filterGroups: readonly {
154
+ filters: readonly {
155
+ dimension: string;
156
+ }[];
157
+ }[] | undefined, searchType?: SearchType): GscAggregation;
136
158
  export declare function runGscSyncSlice(opts: RunGscSyncSliceOptions): Promise<RunGscSyncSliceResult>;
137
159
  /**
138
160
  * Implements GSC's required two-step search-appearance flow:
@@ -22,20 +22,27 @@ function isTimeoutLike(err) {
22
22
  if (!(err instanceof Error)) return false;
23
23
  return err.name === "AbortError" || err.message?.includes("timeout") || err.message?.includes("aborted");
24
24
  }
25
+ function hostPagePattern(host) {
26
+ return `^https?://${host.replace(/\./g, "\\.")}/`;
27
+ }
28
+ function requestAggregation(dimensions, filterGroups, searchType = "web") {
29
+ if (searchType === "discover" || searchType === "googleNews") return "byPage";
30
+ return dimensions.includes("page") || (filterGroups ?? []).some((group) => group.filters.some((filter) => filter.dimension === "page")) ? "byPage" : "byProperty";
31
+ }
32
+ function reportedAggregation(value) {
33
+ return value === "byPage" || value === "byProperty" ? value : null;
34
+ }
25
35
  function buildDimensionFilterGroups(domainFilter, filters = []) {
26
36
  const out = filters.map((f) => ({
27
37
  dimension: f.dimension,
28
38
  operator: f.operator ?? "equals",
29
39
  expression: f.expression
30
40
  }));
31
- if (domainFilter?.domain) {
32
- const pattern = `^https?://${domainFilter.domain.replace(/\./g, "\\.")}/`;
33
- out.push({
34
- dimension: "page",
35
- operator: "includingRegex",
36
- expression: pattern
37
- });
38
- }
41
+ if (domainFilter?.domain) out.push({
42
+ dimension: "page",
43
+ operator: "includingRegex",
44
+ expression: hostPagePattern(domainFilter.domain)
45
+ });
39
46
  return out.length > 0 ? [{ filters: out }] : void 0;
40
47
  }
41
48
  async function runGscSyncSlice(opts) {
@@ -46,6 +53,7 @@ async function runGscSyncSlice(opts) {
46
53
  const dimensions = opts.dimensions ? [...opts.dimensions] : [...DIMENSIONS_BY_TABLE[opts.table]];
47
54
  const dataState = opts.dataState ?? (dimensions.includes("hour") ? "hourly_all" : "all");
48
55
  const dimensionFilterGroups = buildDimensionFilterGroups(opts.domainFilter, opts.dimensionFilters);
56
+ let aggregation = requestAggregation(dimensions, dimensionFilterGroups, searchType);
49
57
  const loopStart = Date.now();
50
58
  let startRow = opts.initialStartRow ?? 0;
51
59
  let totalRows = 0;
@@ -67,6 +75,7 @@ async function runGscSyncSlice(opts) {
67
75
  return {
68
76
  kind: "ok",
69
77
  startRow: row,
78
+ reported: reportedAggregation(response.responseAggregationType),
70
79
  rows: response.rows ?? [],
71
80
  metadata: response.metadata
72
81
  };
@@ -91,7 +100,8 @@ async function runGscSyncSlice(opts) {
91
100
  totalRows,
92
101
  hasMore: true,
93
102
  nextStartRow: startRow,
94
- metadata
103
+ metadata,
104
+ aggregation
95
105
  };
96
106
  while (pending !== null) {
97
107
  const page = await pending;
@@ -101,12 +111,14 @@ async function runGscSyncSlice(opts) {
101
111
  totalRows,
102
112
  hasMore: true,
103
113
  nextStartRow: page.startRow,
104
- metadata
114
+ metadata,
115
+ aggregation
105
116
  };
106
117
  const rows = page.rows;
107
118
  totalRows += rows.length;
108
119
  pageCount++;
109
120
  if (page.metadata) metadata = page.metadata;
121
+ aggregation = page.reported ?? aggregation;
110
122
  opts.onPage?.({
111
123
  searchType,
112
124
  rowsThisPage: rows.length
@@ -122,20 +134,23 @@ async function runGscSyncSlice(opts) {
122
134
  totalRows: totalRows - rows.length,
123
135
  hasMore: true,
124
136
  nextStartRow: page.startRow,
125
- metadata
137
+ metadata,
138
+ aggregation
126
139
  };
127
140
  }
128
141
  if (isLastPage) return {
129
142
  totalRows,
130
143
  hasMore: false,
131
144
  nextStartRow,
132
- metadata
145
+ metadata,
146
+ aggregation
133
147
  };
134
148
  if (prefetch === null) return {
135
149
  totalRows,
136
150
  hasMore: true,
137
151
  nextStartRow,
138
- metadata
152
+ metadata,
153
+ aggregation
139
154
  };
140
155
  startRow = nextStartRow;
141
156
  pending = prefetch;
@@ -144,7 +159,8 @@ async function runGscSyncSlice(opts) {
144
159
  totalRows,
145
160
  hasMore: false,
146
161
  nextStartRow: startRow,
147
- metadata
162
+ metadata,
163
+ aggregation
148
164
  };
149
165
  }
150
166
  function contextTableForGrain(grain) {
@@ -160,6 +176,7 @@ async function runGscSearchAppearanceContextSlice(opts) {
160
176
  let totalRows = 0;
161
177
  let hasMore = false;
162
178
  let metadata;
179
+ const aggregation = {};
163
180
  if (!opts.appearances && opts.continuation?.phase !== "context") {
164
181
  const discovered = new Set(appearances);
165
182
  const discovery = await runGscSyncSlice({
@@ -186,9 +203,11 @@ async function runGscSearchAppearanceContextSlice(opts) {
186
203
  });
187
204
  totalRows += discovery.totalRows;
188
205
  metadata = discovery.metadata;
206
+ aggregation.search_appearance = discovery.aggregation;
189
207
  if (discovery.hasMore) return {
190
208
  appearances: [...discovered],
191
209
  totalRows,
210
+ aggregation,
192
211
  hasMore: true,
193
212
  continuation: {
194
213
  phase: "discovery",
@@ -230,9 +249,11 @@ async function runGscSearchAppearanceContextSlice(opts) {
230
249
  });
231
250
  totalRows += context.totalRows;
232
251
  metadata = context.metadata;
252
+ aggregation[table] = context.aggregation;
233
253
  if (context.hasMore) return {
234
254
  appearances,
235
255
  totalRows,
256
+ aggregation,
236
257
  hasMore: true,
237
258
  continuation: {
238
259
  phase: "context",
@@ -248,7 +269,8 @@ async function runGscSearchAppearanceContextSlice(opts) {
248
269
  appearances,
249
270
  totalRows,
250
271
  hasMore,
251
- metadata
272
+ metadata,
273
+ aggregation
252
274
  };
253
275
  }
254
- export { runGscSearchAppearanceContextSlice, runGscSyncSlice };
276
+ export { hostPagePattern, requestAggregation, runGscSearchAppearanceContextSlice, runGscSyncSlice };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-gsc-api",
3
3
  "type": "module",
4
- "version": "4.2.3",
4
+ "version": "4.3.0",
5
5
  "description": "GSC live-API engine adapter — wraps the Search Analytics REST API as an AnalysisQuerySource for typed analyzer dispatch.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -36,8 +36,8 @@
36
36
  "node": ">=22.13.0"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "^4.2.3",
40
- "gscdump": "^4.2.3"
39
+ "@gscdump/engine": "^4.3.0",
40
+ "gscdump": "^4.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^5.0.1"