@gscdump/engine-gsc-api 1.3.2 → 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,146 @@
|
|
|
1
|
+
import { dimensionValue, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
|
|
2
|
+
import { extractMetricFilters, extractSpecialOperatorFilters } from "gscdump/query";
|
|
3
|
+
import { normalizeUrl } from "gscdump/normalize";
|
|
4
|
+
const METRIC_NAMES = [
|
|
5
|
+
"clicks",
|
|
6
|
+
"impressions",
|
|
7
|
+
"ctr",
|
|
8
|
+
"position"
|
|
9
|
+
];
|
|
10
|
+
function isMetricDimension(dim) {
|
|
11
|
+
return METRIC_NAMES.includes(dim);
|
|
12
|
+
}
|
|
13
|
+
function isLocalDimensionFilter(filter) {
|
|
14
|
+
return filter.dimension !== "date" && !isMetricDimension(filter.dimension) && filter.operator !== "topLevel" && !filter.operator.startsWith("metric");
|
|
15
|
+
}
|
|
16
|
+
function compileDimensionFilter(filter) {
|
|
17
|
+
const { dimension, expression } = filter;
|
|
18
|
+
switch (filter.operator) {
|
|
19
|
+
case "equals": return dimension === "page" ? (row) => normalizeUrl(dimensionValue(row, dimension)) === expression : (row) => dimensionValue(row, dimension) === expression;
|
|
20
|
+
case "notEquals": return dimension === "page" ? (row) => normalizeUrl(dimensionValue(row, dimension)) !== expression : (row) => dimensionValue(row, dimension) !== expression;
|
|
21
|
+
case "contains": return (row) => dimensionValue(row, dimension).includes(expression);
|
|
22
|
+
case "notContains": return (row) => !dimensionValue(row, dimension).includes(expression);
|
|
23
|
+
case "includingRegex": {
|
|
24
|
+
let regex;
|
|
25
|
+
return (row) => (regex ??= new RegExp(expression)).test(dimensionValue(row, dimension));
|
|
26
|
+
}
|
|
27
|
+
case "excludingRegex": {
|
|
28
|
+
let regex;
|
|
29
|
+
return (row) => !(regex ??= new RegExp(expression)).test(dimensionValue(row, dimension));
|
|
30
|
+
}
|
|
31
|
+
default: return () => true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function compileMetricFilter(filter) {
|
|
35
|
+
const { dimension } = filter;
|
|
36
|
+
const target = Number(filter.expression);
|
|
37
|
+
switch (filter.operator) {
|
|
38
|
+
case "metricGte": return (row) => metricValue(row, dimension) >= target;
|
|
39
|
+
case "metricGt": return (row) => metricValue(row, dimension) > target;
|
|
40
|
+
case "metricLte": return (row) => metricValue(row, dimension) <= target;
|
|
41
|
+
case "metricLt": return (row) => metricValue(row, dimension) < target;
|
|
42
|
+
case "metricBetween": {
|
|
43
|
+
const target2 = Number(filter.expression2);
|
|
44
|
+
return (row) => {
|
|
45
|
+
const value = metricValue(row, dimension);
|
|
46
|
+
return value >= target && value <= target2;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
default: return () => true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function compileDimensionFilterTree(filter) {
|
|
53
|
+
if (!filter || !("_filters" in filter)) return () => true;
|
|
54
|
+
const localFilters = filter._filters;
|
|
55
|
+
const matchers = [];
|
|
56
|
+
for (const localFilter of localFilters) if (isLocalDimensionFilter(localFilter)) matchers.push(compileDimensionFilter(localFilter));
|
|
57
|
+
for (const group of filter._nestedGroups ?? []) matchers.push(compileDimensionFilterTree(group));
|
|
58
|
+
if (matchers.length === 0) return () => true;
|
|
59
|
+
if (filter._groupType === "or") return (row) => {
|
|
60
|
+
for (const matches of matchers) if (matches(row)) return true;
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
return (row) => {
|
|
64
|
+
for (const matches of matchers) if (!matches(row)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function selectTopK(rows, k, compareRows) {
|
|
69
|
+
const heap = [];
|
|
70
|
+
const compare = (a, b) => compareRows(a.row, b.row) || a.index - b.index;
|
|
71
|
+
const siftUp = (start) => {
|
|
72
|
+
let child = start;
|
|
73
|
+
while (child > 0) {
|
|
74
|
+
const parent = child - 1 >>> 1;
|
|
75
|
+
if (compare(heap[parent], heap[child]) >= 0) break;
|
|
76
|
+
const swap = heap[parent];
|
|
77
|
+
heap[parent] = heap[child];
|
|
78
|
+
heap[child] = swap;
|
|
79
|
+
child = parent;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const siftDown = () => {
|
|
83
|
+
let parent = 0;
|
|
84
|
+
while (true) {
|
|
85
|
+
const left = parent * 2 + 1;
|
|
86
|
+
if (left >= heap.length) return;
|
|
87
|
+
const right = left + 1;
|
|
88
|
+
const worse = right < heap.length && compare(heap[right], heap[left]) > 0 ? right : left;
|
|
89
|
+
if (compare(heap[parent], heap[worse]) >= 0) return;
|
|
90
|
+
const swap = heap[parent];
|
|
91
|
+
heap[parent] = heap[worse];
|
|
92
|
+
heap[worse] = swap;
|
|
93
|
+
parent = worse;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (let index = 0; index < rows.length; index++) {
|
|
97
|
+
const candidate = {
|
|
98
|
+
row: rows[index],
|
|
99
|
+
index
|
|
100
|
+
};
|
|
101
|
+
if (heap.length < k) {
|
|
102
|
+
heap.push(candidate);
|
|
103
|
+
siftUp(heap.length - 1);
|
|
104
|
+
} else if (compare(candidate, heap[0]) < 0) {
|
|
105
|
+
heap[0] = candidate;
|
|
106
|
+
siftDown();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
heap.sort(compare);
|
|
110
|
+
return heap.map((entry) => entry.row);
|
|
111
|
+
}
|
|
112
|
+
function applyBuilderStatePostProcessing(rows, state) {
|
|
113
|
+
const matchesDimensions = compileDimensionFilterTree(state.filter);
|
|
114
|
+
const metricMatchers = extractMetricFilters(state.filter).map(compileMetricFilter);
|
|
115
|
+
const hasTopLevelFilter = extractSpecialOperatorFilters(state.filter).some((filter) => filter.operator === "topLevel");
|
|
116
|
+
const column = state.orderBy?.column ?? "clicks";
|
|
117
|
+
const dir = state.orderBy?.dir ?? "desc";
|
|
118
|
+
const checkFiniteOrderValues = column !== "date" && state.rowLimit !== void 0;
|
|
119
|
+
let hasNonFiniteOrderValue = false;
|
|
120
|
+
const filtered = [];
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
if (!matchesDimensions(row)) continue;
|
|
123
|
+
let matchesMetrics = true;
|
|
124
|
+
for (const matchesMetric of metricMatchers) if (!matchesMetric(row)) {
|
|
125
|
+
matchesMetrics = false;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (!matchesMetrics) continue;
|
|
129
|
+
if (hasTopLevelFilter && !matchesTopLevelPage(row)) continue;
|
|
130
|
+
filtered.push(row);
|
|
131
|
+
if (checkFiniteOrderValues && !Number.isFinite(metricValue(row, column))) hasNonFiniteOrderValue = true;
|
|
132
|
+
}
|
|
133
|
+
const compareRows = (a, b) => {
|
|
134
|
+
const left = column === "date" ? String(a.date ?? "") : metricValue(a, column);
|
|
135
|
+
const right = column === "date" ? String(b.date ?? "") : metricValue(b, column);
|
|
136
|
+
if (left === right) return 0;
|
|
137
|
+
if (dir === "asc") return left < right ? -1 : 1;
|
|
138
|
+
return left > right ? -1 : 1;
|
|
139
|
+
};
|
|
140
|
+
const offset = Math.max(0, Number(state.startRow ?? 0));
|
|
141
|
+
const limit = Math.max(0, Number((state.rowLimit ?? filtered.length) || 0));
|
|
142
|
+
if (limit === 0) return [];
|
|
143
|
+
const requested = offset + limit;
|
|
144
|
+
return (Number.isInteger(requested) && requested > 0 && requested < filtered.length / 2 && !hasNonFiniteOrderValue ? selectTopK(filtered, requested, compareRows) : filtered.sort(compareRows)).slice(offset, offset + limit);
|
|
145
|
+
}
|
|
146
|
+
export { applyBuilderStatePostProcessing };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GoogleSearchConsoleClient } from "gscdump";
|
|
2
|
+
import { Column, Dimension, SearchType } from "gscdump/query";
|
|
3
|
+
interface GscRange {
|
|
4
|
+
start: string;
|
|
5
|
+
end: string;
|
|
6
|
+
}
|
|
7
|
+
interface GscTopNRow {
|
|
8
|
+
key: string;
|
|
9
|
+
clicks: number;
|
|
10
|
+
impressions: number;
|
|
11
|
+
sum_position: number;
|
|
12
|
+
}
|
|
13
|
+
interface FetchTopNOptions<D extends Dimension> {
|
|
14
|
+
client: GoogleSearchConsoleClient;
|
|
15
|
+
siteUrl: string;
|
|
16
|
+
dimension: Column<D>;
|
|
17
|
+
range: GscRange;
|
|
18
|
+
/**
|
|
19
|
+
* Ask the GSC API to order by clicks desc. Skip for dimensions where GSC
|
|
20
|
+
* already returns sensibly ranked rows (e.g. country).
|
|
21
|
+
*/
|
|
22
|
+
orderByClicksDesc?: boolean;
|
|
23
|
+
/** Forwarded to the GSC builder. */
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** Trim after the fact (e.g. country has no server-side limit). */
|
|
26
|
+
sliceTop?: number;
|
|
27
|
+
/** GSC search corpus; callers default API-boundary omissions to web. */
|
|
28
|
+
searchType?: SearchType;
|
|
29
|
+
}
|
|
30
|
+
declare function fetchGscTopN<D extends Dimension>(opts: FetchTopNOptions<D>): Promise<GscTopNRow[]>;
|
|
31
|
+
interface GscDailyRow {
|
|
32
|
+
date: number;
|
|
33
|
+
clicks: number;
|
|
34
|
+
impressions: number;
|
|
35
|
+
sum_position: number;
|
|
36
|
+
anonymizedImpressionsPct: number;
|
|
37
|
+
}
|
|
38
|
+
declare function fetchGscDaily(opts: {
|
|
39
|
+
client: GoogleSearchConsoleClient;
|
|
40
|
+
siteUrl: string;
|
|
41
|
+
range: GscRange;
|
|
42
|
+
/** GSC search corpus; callers default API-boundary omissions to web. */
|
|
43
|
+
searchType?: SearchType;
|
|
44
|
+
}): Promise<GscDailyRow[]>;
|
|
45
|
+
export { FetchTopNOptions, GscDailyRow, GscRange, GscTopNRow, fetchGscDaily, fetchGscTopN };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { between, clicks, date, gsc } from "gscdump/query";
|
|
2
|
+
async function collectRows(gen) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for await (const batch of gen) out.push(...batch);
|
|
5
|
+
return out;
|
|
6
|
+
}
|
|
7
|
+
async function fetchGscTopN(opts) {
|
|
8
|
+
const { client, siteUrl, dimension, range, orderByClicksDesc, limit, sliceTop, searchType } = opts;
|
|
9
|
+
let builder = gsc.select(dimension).where(between(date, range.start, range.end));
|
|
10
|
+
if (searchType) builder = builder.type(searchType);
|
|
11
|
+
if (orderByClicksDesc) builder = builder.orderBy(clicks, "desc");
|
|
12
|
+
if (typeof limit === "number") builder = builder.limit(limit);
|
|
13
|
+
const rows = await collectRows(client.query(siteUrl, builder));
|
|
14
|
+
const mapRow = (raw) => {
|
|
15
|
+
const row = raw;
|
|
16
|
+
const key = row[dimension.dimension];
|
|
17
|
+
if (typeof key !== "string" || !key) return null;
|
|
18
|
+
const impressions = Number(row.impressions ?? 0);
|
|
19
|
+
const position = Math.max(1, Number(row.position ?? 0));
|
|
20
|
+
return {
|
|
21
|
+
key,
|
|
22
|
+
clicks: Number(row.clicks ?? 0),
|
|
23
|
+
impressions,
|
|
24
|
+
sum_position: (position - 1) * impressions
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const topLimit = typeof sliceTop === "number" && Number.isInteger(sliceTop) && sliceTop >= 0 ? sliceTop : void 0;
|
|
28
|
+
const mapped = [];
|
|
29
|
+
if (topLimit === 0) return mapped;
|
|
30
|
+
if (orderByClicksDesc && topLimit !== void 0) {
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const value = mapRow(row);
|
|
33
|
+
if (value) mapped.push(value);
|
|
34
|
+
if (mapped.length >= topLimit) break;
|
|
35
|
+
}
|
|
36
|
+
return mapped;
|
|
37
|
+
}
|
|
38
|
+
if (!orderByClicksDesc && topLimit !== void 0 && topLimit <= 128) {
|
|
39
|
+
let requiresFullSort = false;
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const value = mapRow(row);
|
|
42
|
+
if (!value) continue;
|
|
43
|
+
if (!Number.isFinite(value.clicks)) {
|
|
44
|
+
requiresFullSort = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
let insertAt = 0;
|
|
48
|
+
while (insertAt < mapped.length && mapped[insertAt].clicks >= value.clicks) insertAt++;
|
|
49
|
+
if (insertAt < topLimit) {
|
|
50
|
+
mapped.splice(insertAt, 0, value);
|
|
51
|
+
if (mapped.length > topLimit) mapped.pop();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!requiresFullSort) return mapped;
|
|
55
|
+
mapped.length = 0;
|
|
56
|
+
}
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const value = mapRow(row);
|
|
59
|
+
if (value) mapped.push(value);
|
|
60
|
+
}
|
|
61
|
+
if (!orderByClicksDesc) mapped.sort((a, b) => b.clicks - a.clicks);
|
|
62
|
+
return typeof sliceTop === "number" ? mapped.slice(0, sliceTop) : mapped;
|
|
63
|
+
}
|
|
64
|
+
async function fetchGscDaily(opts) {
|
|
65
|
+
const { client, siteUrl, range, searchType } = opts;
|
|
66
|
+
let builder = gsc.select(date).where(between(date, range.start, range.end));
|
|
67
|
+
if (searchType) builder = builder.type(searchType);
|
|
68
|
+
const query = builder;
|
|
69
|
+
return (await collectRows(client.query(siteUrl, query))).map((r) => {
|
|
70
|
+
const row = r;
|
|
71
|
+
if (!row.date) return null;
|
|
72
|
+
const impressions = row.impressions ?? 0;
|
|
73
|
+
return {
|
|
74
|
+
date: Date.parse(`${row.date}T00:00:00Z`),
|
|
75
|
+
clicks: row.clicks ?? 0,
|
|
76
|
+
impressions,
|
|
77
|
+
sum_position: (Math.max(1, row.position ?? 0) - 1) * impressions,
|
|
78
|
+
anonymizedImpressionsPct: 0
|
|
79
|
+
};
|
|
80
|
+
}).filter((x) => x != null).sort((a, b) => a.date - b.date);
|
|
81
|
+
}
|
|
82
|
+
export { collectRows, fetchGscDaily, fetchGscTopN };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GoogleSearchConsoleClient } from "gscdump";
|
|
2
|
+
import { AnalysisQuerySource } from "@gscdump/engine/source";
|
|
3
|
+
interface GscApiQuerySourceOptions {
|
|
4
|
+
client: GoogleSearchConsoleClient;
|
|
5
|
+
siteUrl: string;
|
|
6
|
+
}
|
|
7
|
+
declare function createGscApiQuerySource(options: GscApiQuerySourceOptions): AnalysisQuerySource;
|
|
8
|
+
export { GscApiQuerySourceOptions, createGscApiQuerySource };
|
package/dist/source.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { applyBuilderStatePostProcessing } from "./post-process.mjs";
|
|
2
|
+
import { collectRows } from "./rollup-synth.mjs";
|
|
3
|
+
import { assertDimensionsSupported, getFilterDimensions } from "@gscdump/engine/resolver";
|
|
4
|
+
import { extractMetricFilters, extractSpecialOperatorFilters } from "gscdump/query";
|
|
5
|
+
import { buildLogicalPlan } from "gscdump/query/plan";
|
|
6
|
+
const GSC_API_CAPABILITIES = {
|
|
7
|
+
regex: true,
|
|
8
|
+
multiDataset: false,
|
|
9
|
+
comparisonJoin: false,
|
|
10
|
+
windowTotals: false
|
|
11
|
+
};
|
|
12
|
+
function isMetricDimension(dim) {
|
|
13
|
+
return [
|
|
14
|
+
"clicks",
|
|
15
|
+
"impressions",
|
|
16
|
+
"ctr",
|
|
17
|
+
"position"
|
|
18
|
+
].includes(dim);
|
|
19
|
+
}
|
|
20
|
+
function builderFromState(state) {
|
|
21
|
+
return { getState: () => state };
|
|
22
|
+
}
|
|
23
|
+
function canPushDownPagination(state) {
|
|
24
|
+
return !state.orderBy && extractMetricFilters(state.filter).length === 0 && extractSpecialOperatorFilters(state.filter).length === 0;
|
|
25
|
+
}
|
|
26
|
+
function createGscApiQuerySource(options) {
|
|
27
|
+
const { client, siteUrl } = options;
|
|
28
|
+
return {
|
|
29
|
+
name: "gsc-api",
|
|
30
|
+
kind: "live",
|
|
31
|
+
capabilities: GSC_API_CAPABILITIES,
|
|
32
|
+
async queryRows(state) {
|
|
33
|
+
buildLogicalPlan(state, GSC_API_CAPABILITIES);
|
|
34
|
+
const filterDims = getFilterDimensions(state.filter, isMetricDimension);
|
|
35
|
+
assertDimensionsSupported([...state.dimensions, ...filterDims], "api", "gsc-api query source");
|
|
36
|
+
const pushDownPagination = canPushDownPagination(state);
|
|
37
|
+
const apiState = pushDownPagination ? state : {
|
|
38
|
+
...state,
|
|
39
|
+
rowLimit: void 0,
|
|
40
|
+
startRow: void 0
|
|
41
|
+
};
|
|
42
|
+
return applyBuilderStatePostProcessing(await collectRows(client.query(siteUrl, builderFromState(apiState))), pushDownPagination ? {
|
|
43
|
+
...state,
|
|
44
|
+
rowLimit: void 0,
|
|
45
|
+
startRow: void 0
|
|
46
|
+
} : state);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export { GSC_API_CAPABILITIES, createGscApiQuerySource };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { GoogleSearchConsoleClient } from "gscdump";
|
|
2
|
+
import { SearchType } from "@gscdump/engine";
|
|
3
|
+
import { GscDataState, GscSearchAnalyticsMetadata } from "gscdump/contracts";
|
|
4
|
+
interface GscApiRow {
|
|
5
|
+
keys: string[];
|
|
6
|
+
clicks: number;
|
|
7
|
+
impressions: number;
|
|
8
|
+
ctr: number;
|
|
9
|
+
position: number;
|
|
10
|
+
}
|
|
11
|
+
interface SyncSliceDomainFilter {
|
|
12
|
+
/**
|
|
13
|
+
* Domain (eTLD+1 + subdomain) to scope the slice to — matches both
|
|
14
|
+
* `www.` and bare variants. Strip the protocol; the regex is built here.
|
|
15
|
+
*/
|
|
16
|
+
domain?: string;
|
|
17
|
+
}
|
|
18
|
+
interface SyncSliceDimensionFilter {
|
|
19
|
+
dimension: 'page' | 'query' | 'country' | 'device' | 'searchAppearance';
|
|
20
|
+
operator?: 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
|
|
21
|
+
expression: string;
|
|
22
|
+
}
|
|
23
|
+
interface RunGscSyncSliceOptions {
|
|
24
|
+
client: GoogleSearchConsoleClient;
|
|
25
|
+
siteUrl: string;
|
|
26
|
+
/** One of the engine sync-fan tables. Drives the dimension list. */
|
|
27
|
+
table: 'pages' | 'queries' | 'countries' | 'dates' | 'page_queries' | 'search_appearance' | 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries' | 'hourly_pages';
|
|
28
|
+
startDate: string;
|
|
29
|
+
endDate: string;
|
|
30
|
+
domainFilter?: SyncSliceDomainFilter | null;
|
|
31
|
+
/** Additional AND filters, e.g. `searchAppearance = AMP_BLUE_LINK`. */
|
|
32
|
+
dimensionFilters?: SyncSliceDimensionFilter[];
|
|
33
|
+
/**
|
|
34
|
+
* Override the dimension list for this slice. Defaults to
|
|
35
|
+
* `DIMENSIONS_BY_TABLE[table]`. Hosts that need bespoke groupings (e.g.
|
|
36
|
+
* hourly Discover variants) supply this directly.
|
|
37
|
+
*/
|
|
38
|
+
dimensions?: string[];
|
|
39
|
+
/**
|
|
40
|
+
* GSC `dataState` for the query. Defaults to `'all'`. Use `'hourly_all'`
|
|
41
|
+
* for hourly Discover slices.
|
|
42
|
+
*/
|
|
43
|
+
dataState?: GscDataState;
|
|
44
|
+
/**
|
|
45
|
+
* Invoked per GSC API page with the rows fetched. Return a promise; the
|
|
46
|
+
* loop awaits it before paging further. Throw a durable error to abort the
|
|
47
|
+
* slice. A thrown AbortError / timeout stops the loop and returns retry
|
|
48
|
+
* state at the current cursor so the continuation re-processes this page.
|
|
49
|
+
*/
|
|
50
|
+
onBatch: (rows: GscApiRow[]) => Promise<void>;
|
|
51
|
+
initialStartRow?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Max GSC API pages per call. `Infinity` for unbounded; hosts cap this
|
|
54
|
+
* based on path (D1 vs R2) and table.
|
|
55
|
+
*/
|
|
56
|
+
maxPages?: number;
|
|
57
|
+
/** GSC page size. 500 is the D1-safe default; 10k is the R2 path. */
|
|
58
|
+
rowLimit?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Soft CPU budget for the loop itself. Returns `hasMore` when crossed so
|
|
61
|
+
* the continuation resumes from `nextStartRow`.
|
|
62
|
+
*/
|
|
63
|
+
cpuBudgetMs?: number;
|
|
64
|
+
searchType?: SearchType;
|
|
65
|
+
/** Invoked once per successful GSC API page. Hosts wire telemetry here. */
|
|
66
|
+
onPage?: (info: {
|
|
67
|
+
searchType: SearchType;
|
|
68
|
+
rowsThisPage: number;
|
|
69
|
+
}) => void;
|
|
70
|
+
}
|
|
71
|
+
interface RunGscSyncSliceResult {
|
|
72
|
+
totalRows: number;
|
|
73
|
+
hasMore: boolean;
|
|
74
|
+
nextStartRow: number;
|
|
75
|
+
/**
|
|
76
|
+
* Metadata from the LAST GSC API page seen during this slice run. When
|
|
77
|
+
* `dataState='hourly_all'` and grouped by `hour`, this surfaces
|
|
78
|
+
* `first_incomplete_hour` so hosts can watermark hourly progress.
|
|
79
|
+
*/
|
|
80
|
+
metadata?: GscSearchAnalyticsMetadata;
|
|
81
|
+
}
|
|
82
|
+
type SearchAppearanceContextGrain = 'page' | 'query' | 'page_query';
|
|
83
|
+
type SearchAppearanceContextTable = 'search_appearance_pages' | 'search_appearance_queries' | 'search_appearance_page_queries';
|
|
84
|
+
interface RunGscSearchAppearanceContextSliceOptions {
|
|
85
|
+
client: GoogleSearchConsoleClient;
|
|
86
|
+
siteUrl: string;
|
|
87
|
+
startDate: string;
|
|
88
|
+
endDate: string;
|
|
89
|
+
domainFilter?: SyncSliceDomainFilter | null;
|
|
90
|
+
/** Use a known appearance list to skip discovery. */
|
|
91
|
+
appearances?: string[];
|
|
92
|
+
/** Context grain to fetch for every discovered appearance. Defaults to page_query. */
|
|
93
|
+
grain?: SearchAppearanceContextGrain;
|
|
94
|
+
/** Context table to fetch. Overrides `grain` when provided. */
|
|
95
|
+
table?: SearchAppearanceContextTable;
|
|
96
|
+
dataState?: GscDataState;
|
|
97
|
+
rowLimit?: number;
|
|
98
|
+
maxPages?: number;
|
|
99
|
+
cpuBudgetMs?: number;
|
|
100
|
+
searchType?: SearchType;
|
|
101
|
+
onTotalBatch?: (rows: GscApiRow[]) => Promise<void>;
|
|
102
|
+
onContextBatch: (batch: {
|
|
103
|
+
searchAppearance: string;
|
|
104
|
+
table: SearchAppearanceContextTable;
|
|
105
|
+
rows: GscApiRow[];
|
|
106
|
+
}) => Promise<void>;
|
|
107
|
+
onPage?: (info: {
|
|
108
|
+
searchType: SearchType;
|
|
109
|
+
rowsThisPage: number;
|
|
110
|
+
}) => void;
|
|
111
|
+
continuation?: SearchAppearanceContinuation;
|
|
112
|
+
}
|
|
113
|
+
type SearchAppearanceContinuation = {
|
|
114
|
+
phase: 'discovery';
|
|
115
|
+
appearances: string[];
|
|
116
|
+
nextStartRow: number;
|
|
117
|
+
} | {
|
|
118
|
+
phase: 'context';
|
|
119
|
+
appearances: string[];
|
|
120
|
+
appearanceIndex: number;
|
|
121
|
+
nextStartRow: number;
|
|
122
|
+
};
|
|
123
|
+
interface RunGscSearchAppearanceContextSliceResult {
|
|
124
|
+
appearances: string[];
|
|
125
|
+
totalRows: number;
|
|
126
|
+
hasMore: boolean;
|
|
127
|
+
continuation?: SearchAppearanceContinuation;
|
|
128
|
+
}
|
|
129
|
+
declare function runGscSyncSlice(opts: RunGscSyncSliceOptions): Promise<RunGscSyncSliceResult>;
|
|
130
|
+
/**
|
|
131
|
+
* Implements GSC's required two-step search-appearance flow:
|
|
132
|
+
* 1. group by `searchAppearance` alone to discover available appearances;
|
|
133
|
+
* 2. for each appearance, filter by it and fetch page/query/date context.
|
|
134
|
+
*/
|
|
135
|
+
declare function runGscSearchAppearanceContextSlice(opts: RunGscSearchAppearanceContextSliceOptions): Promise<RunGscSearchAppearanceContextSliceResult>;
|
|
136
|
+
export { GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SearchAppearanceContinuation, SyncSliceDimensionFilter, SyncSliceDomainFilter, runGscSearchAppearanceContextSlice, runGscSyncSlice };
|