@gscdump/engine-gsc-api 1.4.0 → 1.4.1
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 +4 -204
- package/dist/index.mjs +4 -554
- package/dist/live.d.mts +25 -0
- package/dist/live.mjs +39 -0
- package/dist/post-process.mjs +146 -0
- package/dist/rollup-synth.d.mts +45 -0
- package/dist/rollup-synth.mjs +82 -0
- package/dist/source.d.mts +8 -0
- package/dist/source.mjs +50 -0
- package/dist/sync-slice.d.mts +136 -0
- package/dist/sync-slice.mjs +248 -0
- package/package.json +3 -3
|
@@ -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.
|
|
4
|
+
"version": "1.4.1",
|
|
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.
|
|
40
|
-
"gscdump": "^1.4.
|
|
39
|
+
"@gscdump/engine": "^1.4.1",
|
|
40
|
+
"gscdump": "^1.4.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^4.1.10"
|