@gscdump/engine-gsc-api 0.21.3 → 0.22.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
@@ -82,14 +82,21 @@ interface SyncSliceDomainFilter {
82
82
  */
83
83
  domain?: string;
84
84
  }
85
+ interface SyncSliceDimensionFilter {
86
+ dimension: 'page' | 'query' | 'country' | 'device' | 'searchAppearance';
87
+ operator?: 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
88
+ expression: string;
89
+ }
85
90
  interface RunGscSyncSliceOptions {
86
91
  client: GoogleSearchConsoleClient;
87
92
  siteUrl: string;
88
93
  /** One of the engine sync-fan tables. Drives the dimension list. */
89
- table: 'pages' | 'queries' | 'countries' | 'dates' | 'page_queries' | 'hourly_pages';
94
+ table: 'pages' | 'queries' | 'countries' | 'dates' | 'page_queries' | 'search_appearance' | 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries' | 'hourly_pages';
90
95
  startDate: string;
91
96
  endDate: string;
92
97
  domainFilter?: SyncSliceDomainFilter | null;
98
+ /** Additional AND filters, e.g. `searchAppearance = AMP_BLUE_LINK`. */
99
+ dimensionFilters?: SyncSliceDimensionFilter[];
93
100
  /**
94
101
  * Override the dimension list for this slice. Defaults to
95
102
  * `DIMENSIONS_BY_TABLE[table]`. Hosts that need bespoke groupings (e.g.
@@ -139,5 +146,46 @@ interface RunGscSyncSliceResult {
139
146
  */
140
147
  metadata?: GscSearchAnalyticsMetadata;
141
148
  }
149
+ type SearchAppearanceContextGrain = 'page' | 'query' | 'page_query';
150
+ type SearchAppearanceContextTable = 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries';
151
+ interface RunGscSearchAppearanceContextSliceOptions {
152
+ client: GoogleSearchConsoleClient;
153
+ siteUrl: string;
154
+ startDate: string;
155
+ endDate: string;
156
+ domainFilter?: SyncSliceDomainFilter | null;
157
+ /** Use a known appearance list to skip discovery. */
158
+ appearances?: string[];
159
+ /** Context grain to fetch for every discovered appearance. Defaults to page_query. */
160
+ grain?: SearchAppearanceContextGrain;
161
+ /** Context table to fetch. Overrides `grain` when provided. */
162
+ table?: SearchAppearanceContextTable;
163
+ dataState?: GscDataState;
164
+ rowLimit?: number;
165
+ maxPages?: number;
166
+ cpuBudgetMs?: number;
167
+ searchType?: SearchType$1;
168
+ onTotalBatch?: (rows: GscApiRow[]) => Promise<void>;
169
+ onContextBatch: (batch: {
170
+ searchAppearance: string;
171
+ table: SearchAppearanceContextTable;
172
+ rows: GscApiRow[];
173
+ }) => Promise<void>;
174
+ onPage?: (info: {
175
+ searchType: SearchType$1;
176
+ rowsThisPage: number;
177
+ }) => void;
178
+ }
179
+ interface RunGscSearchAppearanceContextSliceResult {
180
+ appearances: string[];
181
+ totalRows: number;
182
+ hasMore: boolean;
183
+ }
142
184
  declare function runGscSyncSlice(opts: RunGscSyncSliceOptions): Promise<RunGscSyncSliceResult>;
143
- export { type CreateLiveGscSourceOptions, type FetchTopNOptions, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscTopNRow, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSyncSlice };
185
+ /**
186
+ * Implements GSC's required two-step search-appearance flow:
187
+ * 1. group by `searchAppearance` alone to discover available appearances;
188
+ * 2. for each appearance, filter by it and fetch page/query/date context.
189
+ */
190
+ declare function runGscSearchAppearanceContextSlice(opts: RunGscSearchAppearanceContextSliceOptions): Promise<RunGscSearchAppearanceContextSliceResult>;
191
+ export { type CreateLiveGscSourceOptions, type FetchTopNOptions, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscTopNRow, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
package/dist/index.mjs CHANGED
@@ -172,19 +172,35 @@ const DIMENSIONS_BY_TABLE = {
172
172
  "query",
173
173
  "date"
174
174
  ],
175
+ search_appearance: ["searchAppearance"],
176
+ search_appearance_pages: ["page", "date"],
177
+ search_appearance_queries: ["query", "date"],
178
+ search_appearance_page_queries: [
179
+ "page",
180
+ "query",
181
+ "date"
182
+ ],
175
183
  hourly_pages: ["hour", "page"]
176
184
  };
177
185
  function isTimeoutLike(err) {
178
186
  if (!(err instanceof Error)) return false;
179
187
  return err.name === "AbortError" || err.message?.includes("timeout") || err.message?.includes("aborted");
180
188
  }
181
- function buildDomainFilterGroups(filter) {
182
- if (!filter?.domain) return void 0;
183
- return [{ filters: [{
184
- dimension: "page",
185
- operator: "includingRegex",
186
- expression: `^https?://(www\\.)?${filter.domain.replace(/^www\./, "").replace(/\./g, "\\.")}/`
187
- }] }];
189
+ function buildDimensionFilterGroups(domainFilter, filters = []) {
190
+ const out = filters.map((f) => ({
191
+ dimension: f.dimension,
192
+ operator: f.operator ?? "equals",
193
+ expression: f.expression
194
+ }));
195
+ if (domainFilter?.domain) {
196
+ const pattern = `^https?://(www\\.)?${domainFilter.domain.replace(/^www\./, "").replace(/\./g, "\\.")}/`;
197
+ out.push({
198
+ dimension: "page",
199
+ operator: "includingRegex",
200
+ expression: pattern
201
+ });
202
+ }
203
+ return out.length > 0 ? [{ filters: out }] : void 0;
188
204
  }
189
205
  async function runGscSyncSlice(opts) {
190
206
  const rowLimit = opts.rowLimit ?? 500;
@@ -193,7 +209,7 @@ async function runGscSyncSlice(opts) {
193
209
  const searchType = opts.searchType ?? "web";
194
210
  const dimensions = opts.dimensions ? [...opts.dimensions] : [...DIMENSIONS_BY_TABLE[opts.table]];
195
211
  const dataState = opts.dataState ?? "all";
196
- const dimensionFilterGroups = buildDomainFilterGroups(opts.domainFilter);
212
+ const dimensionFilterGroups = buildDimensionFilterGroups(opts.domainFilter, opts.dimensionFilters);
197
213
  const loopStart = Date.now();
198
214
  let startRow = opts.initialStartRow ?? 0;
199
215
  let totalRows = 0;
@@ -261,4 +277,76 @@ async function runGscSyncSlice(opts) {
261
277
  metadata
262
278
  };
263
279
  }
264
- export { canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSyncSlice };
280
+ function contextTableForGrain(grain) {
281
+ switch (grain) {
282
+ case "page": return "search_appearance_pages";
283
+ case "query": return "search_appearance_queries";
284
+ case "page_query": return "search_appearance_page_queries";
285
+ }
286
+ }
287
+ async function runGscSearchAppearanceContextSlice(opts) {
288
+ const table = opts.table ?? contextTableForGrain(opts.grain ?? "page_query");
289
+ const appearances = opts.appearances?.slice() ?? [];
290
+ let totalRows = 0;
291
+ let hasMore = false;
292
+ if (!opts.appearances) {
293
+ const discovered = /* @__PURE__ */ new Set();
294
+ const discovery = await runGscSyncSlice({
295
+ client: opts.client,
296
+ siteUrl: opts.siteUrl,
297
+ table: "search_appearance",
298
+ startDate: opts.startDate,
299
+ endDate: opts.endDate,
300
+ domainFilter: opts.domainFilter,
301
+ dataState: opts.dataState,
302
+ rowLimit: opts.rowLimit,
303
+ maxPages: opts.maxPages,
304
+ cpuBudgetMs: opts.cpuBudgetMs,
305
+ searchType: opts.searchType,
306
+ onPage: opts.onPage,
307
+ onBatch: async (rows) => {
308
+ for (const row of rows) {
309
+ const value = String(row.keys?.[0] ?? "");
310
+ if (value) discovered.add(value);
311
+ }
312
+ await opts.onTotalBatch?.(rows);
313
+ }
314
+ });
315
+ totalRows += discovery.totalRows;
316
+ hasMore ||= discovery.hasMore;
317
+ appearances.push(...discovered);
318
+ }
319
+ for (const searchAppearance of appearances) {
320
+ const context = await runGscSyncSlice({
321
+ client: opts.client,
322
+ siteUrl: opts.siteUrl,
323
+ table,
324
+ startDate: opts.startDate,
325
+ endDate: opts.endDate,
326
+ domainFilter: opts.domainFilter,
327
+ dimensionFilters: [{
328
+ dimension: "searchAppearance",
329
+ expression: searchAppearance
330
+ }],
331
+ dataState: opts.dataState,
332
+ rowLimit: opts.rowLimit,
333
+ maxPages: opts.maxPages,
334
+ cpuBudgetMs: opts.cpuBudgetMs,
335
+ searchType: opts.searchType,
336
+ onPage: opts.onPage,
337
+ onBatch: (rows) => opts.onContextBatch({
338
+ searchAppearance,
339
+ table,
340
+ rows
341
+ })
342
+ });
343
+ totalRows += context.totalRows;
344
+ hasMore ||= context.hasMore;
345
+ }
346
+ return {
347
+ appearances,
348
+ totalRows,
349
+ hasMore
350
+ };
351
+ }
352
+ export { canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, 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": "0.21.3",
4
+ "version": "0.22.4",
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,11 +36,11 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "0.21.3",
40
- "gscdump": "0.21.3"
39
+ "@gscdump/engine": "0.22.4",
40
+ "gscdump": "0.22.4"
41
41
  },
42
42
  "devDependencies": {
43
- "vitest": "^4.1.6"
43
+ "vitest": "^4.1.7"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "obuild",