@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 +2 -2
- package/dist/index.mjs +2 -2
- package/dist/live.d.mts +8 -0
- package/dist/live.mjs +16 -2
- package/dist/sync-slice.d.mts +22 -0
- package/dist/sync-slice.mjs +38 -16
- package/package.json +3 -3
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
|
-
|
|
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
|
|
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
|
package/dist/sync-slice.d.mts
CHANGED
|
@@ -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:
|
package/dist/sync-slice.mjs
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
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.
|
|
40
|
-
"gscdump": "^4.
|
|
39
|
+
"@gscdump/engine": "^4.3.0",
|
|
40
|
+
"gscdump": "^4.3.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^5.0.1"
|