@gscdump/analysis 0.7.1 → 0.7.5
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/README.md +43 -108
- package/dist/analyzer/index.d.mts +38 -179
- package/dist/analyzer/index.mjs +269 -502
- package/dist/default-registry.d.mts +2 -86
- package/dist/default-registry.mjs +2986 -204
- package/dist/index.d.mts +75 -234
- package/dist/index.mjs +3275 -800
- package/dist/query/index.d.mts +1 -1
- package/dist/query/index.mjs +1 -33
- package/dist/rollups.d.mts +163 -0
- package/dist/rollups.mjs +346 -0
- package/dist/source/index.d.mts +31 -219
- package/dist/source/index.mjs +27 -388
- package/package.json +23 -17
- package/dist/period/index.d.mts +0 -57
- package/dist/period/index.mjs +0 -150
package/dist/source/index.mjs
CHANGED
|
@@ -1,220 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { AnalyzerCapabilityError, defineAnalyzer, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
|
|
2
|
+
import { canProxyToGsc } from "@gscdump/engine-gsc-api";
|
|
3
|
+
import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
|
|
4
|
+
import { queryRows, typedQuery } from "@gscdump/engine/source";
|
|
5
|
+
import { num } from "@gscdump/engine/analysis-types";
|
|
6
|
+
import { comparisonOf, periodOf } from "@gscdump/engine/period";
|
|
6
7
|
import { enumeratePartitions } from "@gscdump/engine/planner";
|
|
7
8
|
import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
|
|
8
|
-
import { createEngine as createSqliteQuerySource } from "@gscdump/engine-sqlite";
|
|
9
|
-
var AnalyzerCapabilityError = class extends Error {
|
|
10
|
-
constructor(tool, missing) {
|
|
11
|
-
super(`analyzer "${tool}" requires capabilities [${missing.join(", ")}] not provided by source`);
|
|
12
|
-
this.tool = tool;
|
|
13
|
-
this.missing = missing;
|
|
14
|
-
this.name = "AnalyzerCapabilityError";
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
function sourceCapabilities(source) {
|
|
18
|
-
const caps = /* @__PURE__ */ new Set();
|
|
19
|
-
if (source.executeSql) caps.add("executeSql");
|
|
20
|
-
if (source.capabilities.fileSets) caps.add("partitionedParquet");
|
|
21
|
-
if (source.capabilities.regex) caps.add("regex");
|
|
22
|
-
if (source.capabilities.windowTotals) caps.add("windowTotals");
|
|
23
|
-
if (source.capabilities.comparisonJoin) caps.add("comparisonJoin");
|
|
24
|
-
if (source.capabilities.attachedTables) caps.add("attachedTables");
|
|
25
|
-
return caps;
|
|
26
|
-
}
|
|
27
|
-
function assertSatisfies(analyzer, caps) {
|
|
28
|
-
const missing = analyzer.requires.filter((c) => !caps.has(c));
|
|
29
|
-
if (missing.length > 0) throw new AnalyzerCapabilityError(analyzer.id, missing);
|
|
30
|
-
}
|
|
31
|
-
async function runAnalyzerFromSource(source, params, registry) {
|
|
32
|
-
const caps = sourceCapabilities(source);
|
|
33
|
-
const analyzer = registry.resolveAnalyzer(params.type, caps.has("executeSql") || caps.has("attachedTables"));
|
|
34
|
-
if (!analyzer) throw new AnalyzerCapabilityError(params.type, ["executeSql"]);
|
|
35
|
-
assertSatisfies(analyzer, caps);
|
|
36
|
-
const plan = analyzer.build(params);
|
|
37
|
-
if (plan.kind === "rows") return runRowsPlanAgainstSource(source, analyzer, plan, params);
|
|
38
|
-
return runSqlPlanAgainstSource(source, analyzer, plan, params);
|
|
39
|
-
}
|
|
40
|
-
async function runRowsPlanAgainstSource(source, analyzer, plan, params) {
|
|
41
|
-
const entries = Object.entries(plan.queries);
|
|
42
|
-
const resolved = await Promise.all(entries.map(async ([k, q]) => [k, await source.queryRows(q.state)]));
|
|
43
|
-
const rowMap = Object.fromEntries(resolved);
|
|
44
|
-
const { results, meta } = analyzer.reduce(rowMap, { params });
|
|
45
|
-
return {
|
|
46
|
-
results,
|
|
47
|
-
meta: {
|
|
48
|
-
tool: params.type,
|
|
49
|
-
...meta
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function fileSetsFor(plan) {
|
|
54
|
-
const fileSets = { FILES: plan.current };
|
|
55
|
-
if (plan.previous) fileSets.FILES_PREV = plan.previous;
|
|
56
|
-
if (plan.extraFiles) for (const [key, fs] of Object.entries(plan.extraFiles)) fileSets[`FILES_${key}`] = fs;
|
|
57
|
-
return fileSets;
|
|
58
|
-
}
|
|
59
|
-
async function runSqlPlanAgainstSource(source, analyzer, plan, params) {
|
|
60
|
-
if (!source.executeSql) throw new AnalyzerCapabilityError(analyzer.id, ["executeSql"]);
|
|
61
|
-
if (plan.requiresAttachedTables && !source.capabilities.attachedTables) throw new AnalyzerCapabilityError(analyzer.id, ["attachedTables"]);
|
|
62
|
-
const fileSets = source.capabilities.fileSets ? fileSetsFor(plan) : void 0;
|
|
63
|
-
const rows = await source.executeSql(plan.sql, plan.params, fileSets ? { fileSets } : void 0);
|
|
64
|
-
const extras = {};
|
|
65
|
-
if (plan.extraQueries) for (const q of plan.extraQueries) {
|
|
66
|
-
const extraRows = await source.executeSql(q.sql, q.params, fileSets ? { fileSets } : void 0);
|
|
67
|
-
extras[q.name] = extraRows;
|
|
68
|
-
}
|
|
69
|
-
const { results, meta } = analyzer.reduce(rows, {
|
|
70
|
-
params,
|
|
71
|
-
extras
|
|
72
|
-
});
|
|
73
|
-
const sourceMeta = source.capabilities.localSource ? { source: "local" } : {};
|
|
74
|
-
return {
|
|
75
|
-
results,
|
|
76
|
-
meta: {
|
|
77
|
-
tool: params.type,
|
|
78
|
-
...sourceMeta,
|
|
79
|
-
...meta
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
9
|
async function analyzeFromSource(source, params, registry) {
|
|
84
10
|
return runAnalyzerFromSource(source, params, registry);
|
|
85
11
|
}
|
|
86
|
-
async function collectRows(gen) {
|
|
87
|
-
const out = [];
|
|
88
|
-
for await (const batch of gen) out.push(...batch);
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
async function fetchGscTopN(opts) {
|
|
92
|
-
const { client, siteUrl, dimension, range, orderByClicksDesc, limit, sliceTop } = opts;
|
|
93
|
-
let builder = gsc.select(dimension).where(between(date, range.start, range.end));
|
|
94
|
-
if (orderByClicksDesc) builder = builder.orderBy(clicks, "desc");
|
|
95
|
-
if (typeof limit === "number") builder = builder.limit(limit);
|
|
96
|
-
const mapped = (await collectRows(client.query(siteUrl, builder))).map((r) => {
|
|
97
|
-
const row = r;
|
|
98
|
-
const key = row[dimension.dimension];
|
|
99
|
-
if (typeof key !== "string" || !key) return null;
|
|
100
|
-
const impressions = Number(row.impressions ?? 0);
|
|
101
|
-
const position = Number(row.position ?? 0);
|
|
102
|
-
return {
|
|
103
|
-
key,
|
|
104
|
-
clicks: Number(row.clicks ?? 0),
|
|
105
|
-
impressions,
|
|
106
|
-
sum_position: position * impressions
|
|
107
|
-
};
|
|
108
|
-
}).filter((x) => x != null);
|
|
109
|
-
if (!orderByClicksDesc) mapped.sort((a, b) => b.clicks - a.clicks);
|
|
110
|
-
return typeof sliceTop === "number" ? mapped.slice(0, sliceTop) : mapped;
|
|
111
|
-
}
|
|
112
|
-
async function fetchGscDaily(opts) {
|
|
113
|
-
const { client, siteUrl, range } = opts;
|
|
114
|
-
const builder = gsc.select(date).where(between(date, range.start, range.end));
|
|
115
|
-
return (await collectRows(client.query(siteUrl, builder))).map((r) => {
|
|
116
|
-
const row = r;
|
|
117
|
-
if (!row.date) return null;
|
|
118
|
-
const impressions = row.impressions ?? 0;
|
|
119
|
-
return {
|
|
120
|
-
date: Date.parse(`${row.date}T00:00:00Z`),
|
|
121
|
-
clicks: row.clicks ?? 0,
|
|
122
|
-
impressions,
|
|
123
|
-
sum_position: (row.position ?? 0) * impressions,
|
|
124
|
-
anonymizedImpressionsPct: 0
|
|
125
|
-
};
|
|
126
|
-
}).filter((x) => x != null).sort((a, b) => a.date - b.date);
|
|
127
|
-
}
|
|
128
|
-
const METRIC_NAMES = [
|
|
129
|
-
"clicks",
|
|
130
|
-
"impressions",
|
|
131
|
-
"ctr",
|
|
132
|
-
"position"
|
|
133
|
-
];
|
|
134
|
-
function isMetricDimension$2(dim) {
|
|
135
|
-
return METRIC_NAMES.includes(dim);
|
|
136
|
-
}
|
|
137
|
-
function applyBuilderStatePostProcessing(rows, state) {
|
|
138
|
-
const dimensionFilters = getDimensionFilters(state.filter, isMetricDimension$2);
|
|
139
|
-
const metricFilters = extractMetricFilters(state.filter);
|
|
140
|
-
const specialFilters = extractSpecialOperatorFilters(state.filter);
|
|
141
|
-
const ordered = [...rows.filter((row) => {
|
|
142
|
-
if (!dimensionFilters.every((filter) => matchesDimensionFilter(row, filter))) return false;
|
|
143
|
-
if (!metricFilters.every((filter) => matchesMetricFilter(row, filter))) return false;
|
|
144
|
-
if (specialFilters.some((filter) => filter.operator === "topLevel") && !matchesTopLevelPage(row)) return false;
|
|
145
|
-
return true;
|
|
146
|
-
})].sort((a, b) => {
|
|
147
|
-
const column = state.orderBy?.column ?? "clicks";
|
|
148
|
-
const dir = state.orderBy?.dir ?? "desc";
|
|
149
|
-
const left = column === "date" ? String(a.date ?? "") : metricValue(a, column);
|
|
150
|
-
const right = column === "date" ? String(b.date ?? "") : metricValue(b, column);
|
|
151
|
-
if (left === right) return 0;
|
|
152
|
-
if (dir === "asc") return left < right ? -1 : 1;
|
|
153
|
-
return left > right ? -1 : 1;
|
|
154
|
-
});
|
|
155
|
-
const offset = Math.max(0, Number(state.startRow ?? 0));
|
|
156
|
-
const limit = Math.max(0, Number((state.rowLimit ?? ordered.length) || 0));
|
|
157
|
-
return ordered.slice(offset, offset + limit);
|
|
158
|
-
}
|
|
159
|
-
const GSC_API_CAPABILITIES = {
|
|
160
|
-
regex: true,
|
|
161
|
-
multiDataset: false,
|
|
162
|
-
comparisonJoin: false,
|
|
163
|
-
windowTotals: false
|
|
164
|
-
};
|
|
165
|
-
function isMetricDimension$1(dim) {
|
|
166
|
-
return [
|
|
167
|
-
"clicks",
|
|
168
|
-
"impressions",
|
|
169
|
-
"ctr",
|
|
170
|
-
"position"
|
|
171
|
-
].includes(dim);
|
|
172
|
-
}
|
|
173
|
-
function builderFromState(state) {
|
|
174
|
-
return { getState: () => state };
|
|
175
|
-
}
|
|
176
|
-
function createGscApiQuerySource(options) {
|
|
177
|
-
const { client, siteUrl } = options;
|
|
178
|
-
return {
|
|
179
|
-
name: "gsc-api",
|
|
180
|
-
capabilities: GSC_API_CAPABILITIES,
|
|
181
|
-
async queryRows(state) {
|
|
182
|
-
buildLogicalPlan(state, GSC_API_CAPABILITIES);
|
|
183
|
-
const filterDims = getFilterDimensions(state.filter, isMetricDimension$1);
|
|
184
|
-
assertDimensionsSupported([...state.dimensions, ...filterDims], "api", "gsc-api query source");
|
|
185
|
-
return applyBuilderStatePostProcessing(await collectRows(client.query(siteUrl, builderFromState(state))), state);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const PRO_ONLY_DIMENSIONS = new Set(["queryCanonical", "page_keywords"]);
|
|
190
|
-
function canProxyToGsc(state) {
|
|
191
|
-
if (state.dimensions.some((d) => PRO_ONLY_DIMENSIONS.has(d))) return false;
|
|
192
|
-
if (extractMetricFilters(state.filter).length > 0) return false;
|
|
193
|
-
if (extractSpecialOperatorFilters(state.filter).length > 0) return false;
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
function createLiveGscSource(opts) {
|
|
197
|
-
let clientPromise = null;
|
|
198
|
-
function getClient() {
|
|
199
|
-
if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => googleSearchConsole({ accessToken }));
|
|
200
|
-
return clientPromise;
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
name: "gsc-api",
|
|
204
|
-
capabilities: {
|
|
205
|
-
regex: true,
|
|
206
|
-
multiDataset: false,
|
|
207
|
-
comparisonJoin: false,
|
|
208
|
-
windowTotals: false
|
|
209
|
-
},
|
|
210
|
-
async queryRows(state) {
|
|
211
|
-
return createGscApiQuerySource({
|
|
212
|
-
client: await getClient(),
|
|
213
|
-
siteUrl: opts.siteUrl
|
|
214
|
-
}).queryRows(state);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
12
|
function createCompositeSource(opts) {
|
|
219
13
|
const { engine, live, site } = opts;
|
|
220
14
|
function rangeCovered(state) {
|
|
@@ -231,56 +25,6 @@ function createCompositeSource(opts) {
|
|
|
231
25
|
executeSql: engine.executeSql
|
|
232
26
|
};
|
|
233
27
|
}
|
|
234
|
-
function isMetricDimension(dim) {
|
|
235
|
-
return [
|
|
236
|
-
"clicks",
|
|
237
|
-
"impressions",
|
|
238
|
-
"ctr",
|
|
239
|
-
"position"
|
|
240
|
-
].includes(dim);
|
|
241
|
-
}
|
|
242
|
-
const ENGINE_QUERY_CAPABILITIES = {
|
|
243
|
-
regex: true,
|
|
244
|
-
multiDataset: false,
|
|
245
|
-
comparisonJoin: false,
|
|
246
|
-
windowTotals: false
|
|
247
|
-
};
|
|
248
|
-
const ENGINE_SOURCE_CAPABILITIES = {
|
|
249
|
-
...ENGINE_QUERY_CAPABILITIES,
|
|
250
|
-
fileSets: true,
|
|
251
|
-
localSource: true
|
|
252
|
-
};
|
|
253
|
-
function createEngineQuerySource(options) {
|
|
254
|
-
const { engine, ctx } = options;
|
|
255
|
-
return {
|
|
256
|
-
name: "engine",
|
|
257
|
-
capabilities: ENGINE_SOURCE_CAPABILITIES,
|
|
258
|
-
async queryRows(state) {
|
|
259
|
-
const filterDims = getFilterDimensions(state.filter, isMetricDimension);
|
|
260
|
-
assertDimensionsSupported([...state.dimensions, ...filterDims], "stored", "engine query source");
|
|
261
|
-
if (state.dimensions.includes("queryCanonical") || filterDims.includes("queryCanonical")) throw new Error("engine query source does not support queryCanonical; use browser/sqlite query sources for derived dimensions");
|
|
262
|
-
return (await engine.query(ctx, state)).rows;
|
|
263
|
-
},
|
|
264
|
-
async executeSql(sql, params, opts) {
|
|
265
|
-
const fileSets = opts?.fileSets;
|
|
266
|
-
if (!fileSets?.FILES) throw new Error("engine query source: executeSql requires opts.fileSets with a FILES entry");
|
|
267
|
-
const { rows } = await engine.runSQL({
|
|
268
|
-
ctx,
|
|
269
|
-
table: fileSets.FILES.table,
|
|
270
|
-
fileSets,
|
|
271
|
-
sql,
|
|
272
|
-
params: params ?? []
|
|
273
|
-
});
|
|
274
|
-
return rows;
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
async function runAnalyzerWithEngine(deps, ctx, params, registry) {
|
|
279
|
-
return runAnalyzerFromSource(createEngineQuerySource({
|
|
280
|
-
engine: deps.engine,
|
|
281
|
-
ctx
|
|
282
|
-
}), params, registry);
|
|
283
|
-
}
|
|
284
28
|
const IN_MEMORY_DEFAULT_CAPABILITIES = {
|
|
285
29
|
regex: true,
|
|
286
30
|
multiDataset: true,
|
|
@@ -306,114 +50,6 @@ function pagesQueryState(period, limit = DEFAULT_LIMIT$1) {
|
|
|
306
50
|
function datesQueryState(period, limit = DEFAULT_LIMIT$1) {
|
|
307
51
|
return gsc.select(date).where(between(date, period.startDate, period.endDate)).limit(limit).getState();
|
|
308
52
|
}
|
|
309
|
-
const DEFAULT_SQL_REQUIRES = ["executeSql", "partitionedParquet"];
|
|
310
|
-
function defineAnalyzer(opts) {
|
|
311
|
-
const { id, reduce, reduceSql, reduceRows, buildSql, buildRows, sqlRequires = DEFAULT_SQL_REQUIRES, rowsRequires = [] } = opts;
|
|
312
|
-
const sqlReducer = reduceSql ?? reduce;
|
|
313
|
-
const rowsReducer = reduceRows ?? reduce;
|
|
314
|
-
if (buildSql && !sqlReducer) throw new Error(`defineAnalyzer(${id}): buildSql requires reduce or reduceSql`);
|
|
315
|
-
if (buildRows && !rowsReducer) throw new Error(`defineAnalyzer(${id}): buildRows requires reduce or reduceRows`);
|
|
316
|
-
const wrap = (fn) => (rows, params, ctx) => {
|
|
317
|
-
return fn(Array.isArray(rows) ? rows : pickSingle(rows) ?? rows, params, ctx);
|
|
318
|
-
};
|
|
319
|
-
return {
|
|
320
|
-
id,
|
|
321
|
-
sql: buildSql && sqlReducer ? {
|
|
322
|
-
id,
|
|
323
|
-
requires: sqlRequires,
|
|
324
|
-
build(params) {
|
|
325
|
-
const spec = buildSql(params);
|
|
326
|
-
return {
|
|
327
|
-
kind: "sql",
|
|
328
|
-
sql: spec.sql,
|
|
329
|
-
params: spec.params,
|
|
330
|
-
current: spec.current,
|
|
331
|
-
previous: spec.previous,
|
|
332
|
-
extraFiles: spec.extraFiles,
|
|
333
|
-
extraQueries: spec.extraQueries,
|
|
334
|
-
requiresAttachedTables: spec.requiresAttachedTables
|
|
335
|
-
};
|
|
336
|
-
},
|
|
337
|
-
reduce(rows, ctx) {
|
|
338
|
-
const { results, meta } = wrap(sqlReducer)(rows, ctx.params, { extras: ctx.extras });
|
|
339
|
-
return {
|
|
340
|
-
results,
|
|
341
|
-
meta
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
} : void 0,
|
|
345
|
-
rows: buildRows && rowsReducer ? {
|
|
346
|
-
id,
|
|
347
|
-
requires: rowsRequires,
|
|
348
|
-
build(params) {
|
|
349
|
-
const queries = buildRows(params);
|
|
350
|
-
return {
|
|
351
|
-
kind: "rows",
|
|
352
|
-
queries: Object.fromEntries(Object.entries(queries).map(([k, state]) => [k, { state }]))
|
|
353
|
-
};
|
|
354
|
-
},
|
|
355
|
-
reduce(rows, ctx) {
|
|
356
|
-
const { results, meta } = wrap(rowsReducer)(rows, ctx.params, {});
|
|
357
|
-
return {
|
|
358
|
-
results,
|
|
359
|
-
meta
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
} : void 0
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function pickSingle(rows) {
|
|
366
|
-
const keys = Object.keys(rows);
|
|
367
|
-
return keys.length === 1 ? rows[keys[0]] : void 0;
|
|
368
|
-
}
|
|
369
|
-
function defaultEndDate() {
|
|
370
|
-
return daysAgo(3);
|
|
371
|
-
}
|
|
372
|
-
function defaultStartDate() {
|
|
373
|
-
return daysAgo(31);
|
|
374
|
-
}
|
|
375
|
-
function periodOf(params) {
|
|
376
|
-
return {
|
|
377
|
-
startDate: params.startDate || defaultStartDate(),
|
|
378
|
-
endDate: params.endDate || defaultEndDate()
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function comparisonOf(params) {
|
|
382
|
-
if (!params.prevStartDate || !params.prevEndDate) throw new Error(`${params.type} analysis requires prevStartDate and prevEndDate`);
|
|
383
|
-
return {
|
|
384
|
-
current: periodOf(params),
|
|
385
|
-
previous: {
|
|
386
|
-
startDate: params.prevStartDate,
|
|
387
|
-
endDate: params.prevEndDate
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
function num(v) {
|
|
392
|
-
if (typeof v === "number") return v;
|
|
393
|
-
if (typeof v === "bigint") return Number(v);
|
|
394
|
-
if (v == null) return 0;
|
|
395
|
-
return Number(v);
|
|
396
|
-
}
|
|
397
|
-
function buildPeriodMap(rows, key, value, filter) {
|
|
398
|
-
const out = /* @__PURE__ */ new Map();
|
|
399
|
-
for (const row of rows) {
|
|
400
|
-
if (filter && !filter(row)) continue;
|
|
401
|
-
out.set(key(row), value(row));
|
|
402
|
-
}
|
|
403
|
-
return out;
|
|
404
|
-
}
|
|
405
|
-
function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
|
|
406
|
-
return (items, sortBy = defaultMetric, sortOrder = defaultOrder) => {
|
|
407
|
-
const mult = sortOrder === "desc" ? -1 : 1;
|
|
408
|
-
return [...items].sort((a, b) => (getValue(a, sortBy) - getValue(b, sortBy)) * mult);
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
function createMetricSorter(defaultMetric, orderByMetric) {
|
|
412
|
-
return (items, sortBy = defaultMetric) => {
|
|
413
|
-
const mult = orderByMetric[sortBy] === "desc" ? -1 : 1;
|
|
414
|
-
return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
53
|
function escapeRegexAlt(s) {
|
|
418
54
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
419
55
|
}
|
|
@@ -939,6 +575,26 @@ defineAnalyzer({
|
|
|
939
575
|
};
|
|
940
576
|
}
|
|
941
577
|
});
|
|
578
|
+
function buildPeriodMap(rows, key, value, filter) {
|
|
579
|
+
const out = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const row of rows) {
|
|
581
|
+
if (filter && !filter(row)) continue;
|
|
582
|
+
out.set(key(row), value(row));
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
|
|
587
|
+
return (items, sortBy = defaultMetric, sortOrder = defaultOrder) => {
|
|
588
|
+
const mult = sortOrder === "desc" ? -1 : 1;
|
|
589
|
+
return [...items].sort((a, b) => (getValue(a, sortBy) - getValue(b, sortBy)) * mult);
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function createMetricSorter(defaultMetric, orderByMetric) {
|
|
593
|
+
return (items, sortBy = defaultMetric) => {
|
|
594
|
+
const mult = orderByMetric[sortBy] === "desc" ? -1 : 1;
|
|
595
|
+
return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
942
598
|
const sortResults$2 = createMetricSorter("lostClicks", {
|
|
943
599
|
lostClicks: "desc",
|
|
944
600
|
declinePercent: "desc",
|
|
@@ -1769,23 +1425,6 @@ function analyzeStrikingDistance(keywords, options = {}) {
|
|
|
1769
1425
|
}
|
|
1770
1426
|
return sortResults(results, sortBy, sortOrder);
|
|
1771
1427
|
}
|
|
1772
|
-
function typedQuery(state) {
|
|
1773
|
-
return { state };
|
|
1774
|
-
}
|
|
1775
|
-
function isTypedQuery(value) {
|
|
1776
|
-
return "state" in value;
|
|
1777
|
-
}
|
|
1778
|
-
async function queryRows(source, query) {
|
|
1779
|
-
const state = isTypedQuery(query) ? query.state : query;
|
|
1780
|
-
return await source.queryRows(state);
|
|
1781
|
-
}
|
|
1782
|
-
async function queryComparisonRows(source, current, previous) {
|
|
1783
|
-
const [currentRows, previousRows] = await Promise.all([queryRows(source, current), queryRows(source, previous)]);
|
|
1784
|
-
return {
|
|
1785
|
-
current: currentRows,
|
|
1786
|
-
previous: previousRows
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
1428
|
function keywordQuery(period, limit) {
|
|
1790
1429
|
return typedQuery(keywordsQueryState(period, limit));
|
|
1791
1430
|
}
|
|
@@ -1907,4 +1546,4 @@ async function analyzeDecayFromSource(source, periods, options) {
|
|
|
1907
1546
|
async function analyzeMoversFromSource(source, periods, options) {
|
|
1908
1547
|
return runPortableAnalyzer(source, PORTABLE_ANALYZERS.movers, periods, options);
|
|
1909
1548
|
}
|
|
1910
|
-
export { AnalyzerCapabilityError,
|
|
1549
|
+
export { AnalyzerCapabilityError, IN_MEMORY_DEFAULT_CAPABILITIES, analyzeBrandSegmentationFromSource, analyzeClusteringFromSource, analyzeDecayFromSource, analyzeFromSource, analyzeKeywordConcentrationFromSource, analyzeMoversFromSource, analyzeOpportunityFromSource, analyzePageConcentrationFromSource, analyzeSeasonalityFromSource, analyzeStrikingDistanceFromSource, createCompositeSource, createInMemoryQuerySource, queryAnalyticsFromSource, queryComparisonFromSource };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"description": "GSC analyzers — striking-distance, opportunity, movers, decay, brand, clustering, concentration, seasonality. Pure row-based + DuckDB-native.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -23,35 +23,43 @@
|
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
25
25
|
"types": "./dist/index.d.mts",
|
|
26
|
-
"import": "./dist/index.mjs"
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
27
28
|
},
|
|
28
29
|
"./analyzer": {
|
|
29
30
|
"types": "./dist/analyzer/index.d.mts",
|
|
30
|
-
"import": "./dist/analyzer/index.mjs"
|
|
31
|
+
"import": "./dist/analyzer/index.mjs",
|
|
32
|
+
"default": "./dist/analyzer/index.mjs"
|
|
31
33
|
},
|
|
32
34
|
"./registry": {
|
|
33
35
|
"types": "./dist/default-registry.d.mts",
|
|
34
|
-
"import": "./dist/default-registry.mjs"
|
|
36
|
+
"import": "./dist/default-registry.mjs",
|
|
37
|
+
"default": "./dist/default-registry.mjs"
|
|
35
38
|
},
|
|
36
39
|
"./query": {
|
|
37
40
|
"types": "./dist/query/index.d.mts",
|
|
38
|
-
"import": "./dist/query/index.mjs"
|
|
41
|
+
"import": "./dist/query/index.mjs",
|
|
42
|
+
"default": "./dist/query/index.mjs"
|
|
39
43
|
},
|
|
40
44
|
"./source": {
|
|
41
45
|
"types": "./dist/source/index.d.mts",
|
|
42
|
-
"import": "./dist/source/index.mjs"
|
|
46
|
+
"import": "./dist/source/index.mjs",
|
|
47
|
+
"default": "./dist/source/index.mjs"
|
|
43
48
|
},
|
|
44
49
|
"./semantic": {
|
|
45
50
|
"types": "./dist/semantic/index.d.mts",
|
|
46
|
-
"import": "./dist/semantic/index.mjs"
|
|
47
|
-
|
|
48
|
-
"./period": {
|
|
49
|
-
"types": "./dist/period/index.d.mts",
|
|
50
|
-
"import": "./dist/period/index.mjs"
|
|
51
|
+
"import": "./dist/semantic/index.mjs",
|
|
52
|
+
"default": "./dist/semantic/index.mjs"
|
|
51
53
|
},
|
|
52
54
|
"./routing": {
|
|
53
55
|
"types": "./dist/routing/index.d.mts",
|
|
54
|
-
"import": "./dist/routing/index.mjs"
|
|
56
|
+
"import": "./dist/routing/index.mjs",
|
|
57
|
+
"default": "./dist/routing/index.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./rollups": {
|
|
60
|
+
"types": "./dist/rollups.d.mts",
|
|
61
|
+
"import": "./dist/rollups.mjs",
|
|
62
|
+
"default": "./dist/rollups.mjs"
|
|
55
63
|
}
|
|
56
64
|
},
|
|
57
65
|
"main": "./dist/index.mjs",
|
|
@@ -72,11 +80,9 @@
|
|
|
72
80
|
},
|
|
73
81
|
"dependencies": {
|
|
74
82
|
"drizzle-orm": "^0.45.2",
|
|
75
|
-
"@gscdump/engine": "0.7.
|
|
76
|
-
"@gscdump/engine-
|
|
77
|
-
"
|
|
78
|
-
"gscdump": "0.7.1",
|
|
79
|
-
"@gscdump/engine-duckdb-node": "0.7.1"
|
|
83
|
+
"@gscdump/engine": "0.7.5",
|
|
84
|
+
"@gscdump/engine-gsc-api": "0.7.5",
|
|
85
|
+
"gscdump": "0.7.5"
|
|
80
86
|
},
|
|
81
87
|
"devDependencies": {
|
|
82
88
|
"vitest": "^4.1.5"
|
package/dist/period/index.d.mts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { AnalysisParams } from "gscdump/contracts";
|
|
2
|
-
type WindowPreset = 'last-7d' | 'last-28d' | 'last-30d' | 'last-90d' | 'last-180d' | 'last-365d' | 'mtd' | 'ytd' | 'custom';
|
|
3
|
-
type ComparisonMode = 'none' | 'prev-period' | 'yoy';
|
|
4
|
-
interface ResolveWindowOptions {
|
|
5
|
-
preset: WindowPreset;
|
|
6
|
-
comparison?: ComparisonMode;
|
|
7
|
-
anchor?: string;
|
|
8
|
-
start?: string;
|
|
9
|
-
end?: string;
|
|
10
|
-
}
|
|
11
|
-
interface ResolvedWindow {
|
|
12
|
-
start: string;
|
|
13
|
-
end: string;
|
|
14
|
-
days: number;
|
|
15
|
-
comparison?: {
|
|
16
|
-
start: string;
|
|
17
|
-
end: string;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
interface AnalysisPeriod {
|
|
21
|
-
startDate: string;
|
|
22
|
-
endDate: string;
|
|
23
|
-
}
|
|
24
|
-
interface ComparisonPeriod {
|
|
25
|
-
current: AnalysisPeriod;
|
|
26
|
-
previous: AnalysisPeriod;
|
|
27
|
-
}
|
|
28
|
-
declare function defaultEndDate(): string;
|
|
29
|
-
declare function defaultStartDate(): string;
|
|
30
|
-
declare function periodOf(params: AnalysisParams): AnalysisPeriod;
|
|
31
|
-
declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
|
|
32
|
-
declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
|
|
33
|
-
/** Convert a ResolvedWindow into the AnalysisPeriod / ComparisonPeriod shape. */
|
|
34
|
-
declare function windowToPeriod(w: ResolvedWindow): AnalysisPeriod;
|
|
35
|
-
declare function windowToComparisonPeriod(w: ResolvedWindow): ComparisonPeriod | undefined;
|
|
36
|
-
interface PadTimeseriesOptions<T> {
|
|
37
|
-
/** ISO date (YYYY-MM-DD), inclusive lower bound. */
|
|
38
|
-
startDate: string;
|
|
39
|
-
/** ISO date (YYYY-MM-DD), inclusive upper bound. */
|
|
40
|
-
endDate: string;
|
|
41
|
-
/**
|
|
42
|
-
* Row to insert for missing dates. Defaults to `{ clicks: 0, impressions: 0, ctr: 0, position: 0 }`.
|
|
43
|
-
* The `date` field is set automatically.
|
|
44
|
-
*/
|
|
45
|
-
fill?: Omit<T, 'date'>;
|
|
46
|
-
/** Row-field that carries the ISO date. Defaults to `date`. */
|
|
47
|
-
dateKey?: string;
|
|
48
|
-
}
|
|
49
|
-
type DateRowShape = Record<string, unknown> & {
|
|
50
|
-
date?: unknown;
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Pad rows so every calendar day in `[startDate, endDate]` appears at least
|
|
54
|
-
* once. Existing dates keep all their rows (grouped timeseries safe).
|
|
55
|
-
*/
|
|
56
|
-
declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
|
|
57
|
-
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow, windowToComparisonPeriod, windowToPeriod };
|