@gscdump/engine-gsc-api 1.4.0 → 1.4.2

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.
@@ -0,0 +1,248 @@
1
+ const DIMENSIONS_BY_TABLE = {
2
+ pages: ["page", "date"],
3
+ queries: ["query", "date"],
4
+ countries: ["country", "date"],
5
+ dates: ["device", "date"],
6
+ page_queries: [
7
+ "page",
8
+ "query",
9
+ "date"
10
+ ],
11
+ search_appearance: ["searchAppearance"],
12
+ search_appearance_pages: ["page", "date"],
13
+ search_appearance_queries: ["query", "date"],
14
+ search_appearance_page_queries: [
15
+ "page",
16
+ "query",
17
+ "date"
18
+ ],
19
+ hourly_pages: ["hour", "page"]
20
+ };
21
+ function isTimeoutLike(err) {
22
+ if (!(err instanceof Error)) return false;
23
+ return err.name === "AbortError" || err.message?.includes("timeout") || err.message?.includes("aborted");
24
+ }
25
+ function buildDimensionFilterGroups(domainFilter, filters = []) {
26
+ const out = filters.map((f) => ({
27
+ dimension: f.dimension,
28
+ operator: f.operator ?? "equals",
29
+ expression: f.expression
30
+ }));
31
+ if (domainFilter?.domain) {
32
+ const pattern = `^https?://(www\\.)?${domainFilter.domain.replace(/^www\./, "").replace(/\./g, "\\.")}/`;
33
+ out.push({
34
+ dimension: "page",
35
+ operator: "includingRegex",
36
+ expression: pattern
37
+ });
38
+ }
39
+ return out.length > 0 ? [{ filters: out }] : void 0;
40
+ }
41
+ async function runGscSyncSlice(opts) {
42
+ const rowLimit = opts.rowLimit ?? 500;
43
+ const cpuBudgetMs = opts.cpuBudgetMs ?? 2e4;
44
+ const maxPages = opts.maxPages ?? Infinity;
45
+ const searchType = opts.searchType ?? "web";
46
+ const dimensions = opts.dimensions ? [...opts.dimensions] : [...DIMENSIONS_BY_TABLE[opts.table]];
47
+ const dataState = opts.dataState ?? (dimensions.includes("hour") ? "hourly_all" : "all");
48
+ const dimensionFilterGroups = buildDimensionFilterGroups(opts.domainFilter, opts.dimensionFilters);
49
+ const loopStart = Date.now();
50
+ let startRow = opts.initialStartRow ?? 0;
51
+ let totalRows = 0;
52
+ let pageCount = 0;
53
+ let metadata;
54
+ const fetchPage = async (row) => {
55
+ const query = {
56
+ startDate: opts.startDate,
57
+ endDate: opts.endDate,
58
+ dimensions,
59
+ rowLimit,
60
+ startRow: row,
61
+ dataState,
62
+ type: searchType,
63
+ ...dimensionFilterGroups ? { dimensionFilterGroups } : {}
64
+ };
65
+ try {
66
+ const response = await opts.client.searchAnalytics.query(opts.siteUrl, query);
67
+ return {
68
+ kind: "ok",
69
+ startRow: row,
70
+ rows: response.rows ?? [],
71
+ metadata: response.metadata
72
+ };
73
+ } catch (err) {
74
+ if (isTimeoutLike(err)) return {
75
+ kind: "timeout",
76
+ startRow: row
77
+ };
78
+ return {
79
+ kind: "error",
80
+ error: err
81
+ };
82
+ }
83
+ };
84
+ const startFetchIfAllowed = (row) => {
85
+ if (pageCount >= maxPages) return null;
86
+ if (Date.now() - loopStart >= cpuBudgetMs) return null;
87
+ return fetchPage(row);
88
+ };
89
+ let pending = startFetchIfAllowed(startRow);
90
+ if (pending === null) return {
91
+ totalRows,
92
+ hasMore: true,
93
+ nextStartRow: startRow,
94
+ metadata
95
+ };
96
+ while (pending !== null) {
97
+ const page = await pending;
98
+ pending = null;
99
+ if (page.kind === "error") throw page.error;
100
+ if (page.kind === "timeout") return {
101
+ totalRows,
102
+ hasMore: true,
103
+ nextStartRow: page.startRow,
104
+ metadata
105
+ };
106
+ const rows = page.rows;
107
+ totalRows += rows.length;
108
+ pageCount++;
109
+ if (page.metadata) metadata = page.metadata;
110
+ opts.onPage?.({
111
+ searchType,
112
+ rowsThisPage: rows.length
113
+ });
114
+ const isLastPage = rows.length === 0;
115
+ const nextStartRow = page.startRow + rows.length;
116
+ const prefetch = isLastPage ? null : startFetchIfAllowed(nextStartRow);
117
+ if (rows.length > 0) {
118
+ if (await opts.onBatch(rows).then(() => false).catch((err) => {
119
+ if (isTimeoutLike(err)) return true;
120
+ throw err;
121
+ })) return {
122
+ totalRows: totalRows - rows.length,
123
+ hasMore: true,
124
+ nextStartRow: page.startRow,
125
+ metadata
126
+ };
127
+ }
128
+ if (isLastPage) return {
129
+ totalRows,
130
+ hasMore: false,
131
+ nextStartRow,
132
+ metadata
133
+ };
134
+ if (prefetch === null) return {
135
+ totalRows,
136
+ hasMore: true,
137
+ nextStartRow,
138
+ metadata
139
+ };
140
+ startRow = nextStartRow;
141
+ pending = prefetch;
142
+ }
143
+ return {
144
+ totalRows,
145
+ hasMore: false,
146
+ nextStartRow: startRow,
147
+ metadata
148
+ };
149
+ }
150
+ function contextTableForGrain(grain) {
151
+ switch (grain) {
152
+ case "page": return "search_appearance_pages";
153
+ case "query": return "search_appearance_queries";
154
+ case "page_query": return "search_appearance_page_queries";
155
+ }
156
+ }
157
+ async function runGscSearchAppearanceContextSlice(opts) {
158
+ const table = opts.table ?? contextTableForGrain(opts.grain ?? "page_query");
159
+ const appearances = opts.continuation?.appearances?.slice() ?? opts.appearances?.slice() ?? [];
160
+ let totalRows = 0;
161
+ let hasMore = false;
162
+ if (!opts.appearances && opts.continuation?.phase !== "context") {
163
+ const discovered = new Set(appearances);
164
+ const discovery = await runGscSyncSlice({
165
+ client: opts.client,
166
+ siteUrl: opts.siteUrl,
167
+ table: "search_appearance",
168
+ startDate: opts.startDate,
169
+ endDate: opts.endDate,
170
+ domainFilter: opts.domainFilter,
171
+ dataState: opts.dataState,
172
+ rowLimit: opts.rowLimit,
173
+ maxPages: opts.maxPages,
174
+ cpuBudgetMs: opts.cpuBudgetMs,
175
+ searchType: opts.searchType,
176
+ initialStartRow: opts.continuation?.phase === "discovery" ? opts.continuation.nextStartRow : void 0,
177
+ onPage: opts.onPage,
178
+ onBatch: async (rows) => {
179
+ for (const row of rows) {
180
+ const value = String(row.keys?.[0] ?? "");
181
+ if (value) discovered.add(value);
182
+ }
183
+ await opts.onTotalBatch?.(rows);
184
+ }
185
+ });
186
+ totalRows += discovery.totalRows;
187
+ if (discovery.hasMore) return {
188
+ appearances: [...discovered],
189
+ totalRows,
190
+ hasMore: true,
191
+ continuation: {
192
+ phase: "discovery",
193
+ appearances: [...discovered],
194
+ nextStartRow: discovery.nextStartRow
195
+ }
196
+ };
197
+ hasMore ||= discovery.hasMore;
198
+ appearances.splice(0, appearances.length, ...discovered);
199
+ }
200
+ const startIndex = opts.continuation?.phase === "context" ? opts.continuation.appearanceIndex : 0;
201
+ const startRow = opts.continuation?.phase === "context" ? opts.continuation.nextStartRow : 0;
202
+ for (let i = startIndex; i < appearances.length; i++) {
203
+ const searchAppearance = appearances[i];
204
+ const context = await runGscSyncSlice({
205
+ client: opts.client,
206
+ siteUrl: opts.siteUrl,
207
+ table,
208
+ startDate: opts.startDate,
209
+ endDate: opts.endDate,
210
+ domainFilter: opts.domainFilter,
211
+ dimensionFilters: [{
212
+ dimension: "searchAppearance",
213
+ expression: searchAppearance
214
+ }],
215
+ dataState: opts.dataState,
216
+ rowLimit: opts.rowLimit,
217
+ maxPages: opts.maxPages,
218
+ cpuBudgetMs: opts.cpuBudgetMs,
219
+ searchType: opts.searchType,
220
+ initialStartRow: i === startIndex ? startRow : void 0,
221
+ onPage: opts.onPage,
222
+ onBatch: (rows) => opts.onContextBatch({
223
+ searchAppearance,
224
+ table,
225
+ rows
226
+ })
227
+ });
228
+ totalRows += context.totalRows;
229
+ if (context.hasMore) return {
230
+ appearances,
231
+ totalRows,
232
+ hasMore: true,
233
+ continuation: {
234
+ phase: "context",
235
+ appearances,
236
+ appearanceIndex: i,
237
+ nextStartRow: context.nextStartRow
238
+ }
239
+ };
240
+ hasMore ||= context.hasMore;
241
+ }
242
+ return {
243
+ appearances,
244
+ totalRows,
245
+ hasMore
246
+ };
247
+ }
248
+ export { 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": "1.4.0",
4
+ "version": "1.4.2",
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"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "^1.4.0",
40
- "gscdump": "^1.4.0"
39
+ "@gscdump/engine": "^1.4.2",
40
+ "gscdump": "^1.4.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^4.1.10"