@gscdump/analysis 0.19.2 → 0.19.4
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/analyzer/index.mjs +156 -0
- package/dist/default-registry.mjs +156 -0
- package/dist/index.mjs +158 -0
- package/dist/query/index.d.mts +16 -1
- package/dist/query/index.mjs +137 -1
- package/dist/source/index.d.mts +1 -1
- package/dist/source/index.mjs +2 -0
- package/package.json +4 -4
package/dist/analyzer/index.mjs
CHANGED
|
@@ -2115,6 +2115,142 @@ function buildDataQueryPlan(params, options) {
|
|
|
2115
2115
|
extraQueries
|
|
2116
2116
|
};
|
|
2117
2117
|
}
|
|
2118
|
+
function totalsState(state) {
|
|
2119
|
+
return {
|
|
2120
|
+
...state,
|
|
2121
|
+
dimensions: [],
|
|
2122
|
+
orderBy: void 0,
|
|
2123
|
+
rowLimit: 1
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function readTotals(row) {
|
|
2127
|
+
return {
|
|
2128
|
+
clicks: Number(row?.clicks ?? 0),
|
|
2129
|
+
impressions: Number(row?.impressions ?? 0),
|
|
2130
|
+
ctr: Number(row?.ctr ?? 0),
|
|
2131
|
+
position: Number(row?.position ?? 0)
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function buildDataQueryRows(params) {
|
|
2135
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2136
|
+
if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
|
|
2137
|
+
const queries = {
|
|
2138
|
+
main: state,
|
|
2139
|
+
totals: totalsState(state)
|
|
2140
|
+
};
|
|
2141
|
+
const prev = optionalBuilderState(params.qc, "data-query", "qc");
|
|
2142
|
+
if (prev) {
|
|
2143
|
+
queries.prevMain = prev;
|
|
2144
|
+
queries.prevTotals = totalsState(prev);
|
|
2145
|
+
}
|
|
2146
|
+
return queries;
|
|
2147
|
+
}
|
|
2148
|
+
function shapeDataQueryRowResults(rowMap, params) {
|
|
2149
|
+
const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2150
|
+
const totals = readTotals(rowMap.totals?.[0]);
|
|
2151
|
+
if (params.qc == null) return {
|
|
2152
|
+
results: main,
|
|
2153
|
+
meta: {
|
|
2154
|
+
totalCount: main.length,
|
|
2155
|
+
totals
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2159
|
+
const dims = state.dimensions;
|
|
2160
|
+
const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
|
|
2161
|
+
const prevByKey = /* @__PURE__ */ new Map();
|
|
2162
|
+
for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
|
|
2163
|
+
const filter = params.comparisonFilter;
|
|
2164
|
+
const merged = [];
|
|
2165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2166
|
+
for (const cur of main) {
|
|
2167
|
+
const key = keyOf(cur);
|
|
2168
|
+
seen.add(key);
|
|
2169
|
+
const prev = prevByKey.get(key);
|
|
2170
|
+
const row = {
|
|
2171
|
+
...cur,
|
|
2172
|
+
prevClicks: Number(prev?.clicks ?? 0),
|
|
2173
|
+
prevImpressions: Number(prev?.impressions ?? 0),
|
|
2174
|
+
prevCtr: Number(prev?.ctr ?? 0),
|
|
2175
|
+
prevPosition: Number(prev?.position ?? 0)
|
|
2176
|
+
};
|
|
2177
|
+
if (passesComparisonFilter(filter, {
|
|
2178
|
+
isNew: !prev,
|
|
2179
|
+
isLost: false,
|
|
2180
|
+
clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
|
|
2181
|
+
})) merged.push(row);
|
|
2182
|
+
}
|
|
2183
|
+
for (const prev of rowMap.prevMain ?? []) {
|
|
2184
|
+
const p = prev;
|
|
2185
|
+
const key = keyOf(p);
|
|
2186
|
+
if (seen.has(key)) continue;
|
|
2187
|
+
const row = {
|
|
2188
|
+
...p,
|
|
2189
|
+
clicks: 0,
|
|
2190
|
+
impressions: 0,
|
|
2191
|
+
ctr: 0,
|
|
2192
|
+
position: 0,
|
|
2193
|
+
prevClicks: Number(p.clicks ?? 0),
|
|
2194
|
+
prevImpressions: Number(p.impressions ?? 0),
|
|
2195
|
+
prevCtr: Number(p.ctr ?? 0),
|
|
2196
|
+
prevPosition: Number(p.position ?? 0)
|
|
2197
|
+
};
|
|
2198
|
+
if (passesComparisonFilter(filter, {
|
|
2199
|
+
isNew: false,
|
|
2200
|
+
isLost: true,
|
|
2201
|
+
clicksChange: -Number(p.clicks ?? 0)
|
|
2202
|
+
})) merged.push(row);
|
|
2203
|
+
}
|
|
2204
|
+
if (state.orderBy) {
|
|
2205
|
+
const { column, dir } = state.orderBy;
|
|
2206
|
+
merged.sort((a, b) => {
|
|
2207
|
+
const av = Number(a[column]) || 0;
|
|
2208
|
+
const bv = Number(b[column]) || 0;
|
|
2209
|
+
return dir === "asc" ? av - bv : bv - av;
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
results: merged.map((r) => coerceNumericCols(r)),
|
|
2214
|
+
meta: {
|
|
2215
|
+
totalCount: merged.length,
|
|
2216
|
+
totals
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function passesComparisonFilter(filter, ctx) {
|
|
2221
|
+
switch (filter) {
|
|
2222
|
+
case "new": return ctx.isNew;
|
|
2223
|
+
case "lost": return ctx.isLost;
|
|
2224
|
+
case "improving": return ctx.clicksChange > 0;
|
|
2225
|
+
case "declining": return ctx.clicksChange < 0;
|
|
2226
|
+
default: return true;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function buildDataDetailRows(params) {
|
|
2230
|
+
const state = requireBuilderState(params.q, "data-detail");
|
|
2231
|
+
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
2232
|
+
const queries = {
|
|
2233
|
+
main: state,
|
|
2234
|
+
totals: totalsState(state)
|
|
2235
|
+
};
|
|
2236
|
+
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
2237
|
+
if (prev) queries.prevTotals = totalsState(prev);
|
|
2238
|
+
return queries;
|
|
2239
|
+
}
|
|
2240
|
+
function shapeDataDetailRowResults(rowMap, params) {
|
|
2241
|
+
const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
2242
|
+
const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2243
|
+
const daily = startDate && endDate ? padTimeseries(coerced, {
|
|
2244
|
+
startDate,
|
|
2245
|
+
endDate
|
|
2246
|
+
}) : coerced;
|
|
2247
|
+
const meta = { totals: readTotals(rowMap.totals?.[0]) };
|
|
2248
|
+
if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
|
|
2249
|
+
return {
|
|
2250
|
+
results: daily,
|
|
2251
|
+
meta
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2118
2254
|
function shapeDataQueryRows(rows, params, extras) {
|
|
2119
2255
|
return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
|
|
2120
2256
|
}
|
|
@@ -2200,6 +2336,16 @@ const dataDetailAnalyzer = defineAnalyzer({
|
|
|
2200
2336
|
results,
|
|
2201
2337
|
meta
|
|
2202
2338
|
};
|
|
2339
|
+
},
|
|
2340
|
+
buildRows(params) {
|
|
2341
|
+
return buildDataDetailRows(params);
|
|
2342
|
+
},
|
|
2343
|
+
reduceRows(rows, params) {
|
|
2344
|
+
const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2345
|
+
return {
|
|
2346
|
+
results,
|
|
2347
|
+
meta
|
|
2348
|
+
};
|
|
2203
2349
|
}
|
|
2204
2350
|
});
|
|
2205
2351
|
const dataQueryAnalyzer = defineAnalyzer({
|
|
@@ -2230,6 +2376,16 @@ const dataQueryAnalyzer = defineAnalyzer({
|
|
|
2230
2376
|
results,
|
|
2231
2377
|
meta
|
|
2232
2378
|
};
|
|
2379
|
+
},
|
|
2380
|
+
buildRows(params) {
|
|
2381
|
+
return buildDataQueryRows(params);
|
|
2382
|
+
},
|
|
2383
|
+
reduceRows(rows, params) {
|
|
2384
|
+
const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2385
|
+
return {
|
|
2386
|
+
results,
|
|
2387
|
+
meta
|
|
2388
|
+
};
|
|
2233
2389
|
}
|
|
2234
2390
|
});
|
|
2235
2391
|
const sortResults$1 = createMetricSorter("lostClicks", {
|
|
@@ -2115,6 +2115,142 @@ function buildDataQueryPlan(params, options) {
|
|
|
2115
2115
|
extraQueries
|
|
2116
2116
|
};
|
|
2117
2117
|
}
|
|
2118
|
+
function totalsState(state) {
|
|
2119
|
+
return {
|
|
2120
|
+
...state,
|
|
2121
|
+
dimensions: [],
|
|
2122
|
+
orderBy: void 0,
|
|
2123
|
+
rowLimit: 1
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function readTotals(row) {
|
|
2127
|
+
return {
|
|
2128
|
+
clicks: Number(row?.clicks ?? 0),
|
|
2129
|
+
impressions: Number(row?.impressions ?? 0),
|
|
2130
|
+
ctr: Number(row?.ctr ?? 0),
|
|
2131
|
+
position: Number(row?.position ?? 0)
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function buildDataQueryRows(params) {
|
|
2135
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2136
|
+
if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
|
|
2137
|
+
const queries = {
|
|
2138
|
+
main: state,
|
|
2139
|
+
totals: totalsState(state)
|
|
2140
|
+
};
|
|
2141
|
+
const prev = optionalBuilderState(params.qc, "data-query", "qc");
|
|
2142
|
+
if (prev) {
|
|
2143
|
+
queries.prevMain = prev;
|
|
2144
|
+
queries.prevTotals = totalsState(prev);
|
|
2145
|
+
}
|
|
2146
|
+
return queries;
|
|
2147
|
+
}
|
|
2148
|
+
function shapeDataQueryRowResults(rowMap, params) {
|
|
2149
|
+
const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2150
|
+
const totals = readTotals(rowMap.totals?.[0]);
|
|
2151
|
+
if (params.qc == null) return {
|
|
2152
|
+
results: main,
|
|
2153
|
+
meta: {
|
|
2154
|
+
totalCount: main.length,
|
|
2155
|
+
totals
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2159
|
+
const dims = state.dimensions;
|
|
2160
|
+
const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
|
|
2161
|
+
const prevByKey = /* @__PURE__ */ new Map();
|
|
2162
|
+
for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
|
|
2163
|
+
const filter = params.comparisonFilter;
|
|
2164
|
+
const merged = [];
|
|
2165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2166
|
+
for (const cur of main) {
|
|
2167
|
+
const key = keyOf(cur);
|
|
2168
|
+
seen.add(key);
|
|
2169
|
+
const prev = prevByKey.get(key);
|
|
2170
|
+
const row = {
|
|
2171
|
+
...cur,
|
|
2172
|
+
prevClicks: Number(prev?.clicks ?? 0),
|
|
2173
|
+
prevImpressions: Number(prev?.impressions ?? 0),
|
|
2174
|
+
prevCtr: Number(prev?.ctr ?? 0),
|
|
2175
|
+
prevPosition: Number(prev?.position ?? 0)
|
|
2176
|
+
};
|
|
2177
|
+
if (passesComparisonFilter(filter, {
|
|
2178
|
+
isNew: !prev,
|
|
2179
|
+
isLost: false,
|
|
2180
|
+
clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
|
|
2181
|
+
})) merged.push(row);
|
|
2182
|
+
}
|
|
2183
|
+
for (const prev of rowMap.prevMain ?? []) {
|
|
2184
|
+
const p = prev;
|
|
2185
|
+
const key = keyOf(p);
|
|
2186
|
+
if (seen.has(key)) continue;
|
|
2187
|
+
const row = {
|
|
2188
|
+
...p,
|
|
2189
|
+
clicks: 0,
|
|
2190
|
+
impressions: 0,
|
|
2191
|
+
ctr: 0,
|
|
2192
|
+
position: 0,
|
|
2193
|
+
prevClicks: Number(p.clicks ?? 0),
|
|
2194
|
+
prevImpressions: Number(p.impressions ?? 0),
|
|
2195
|
+
prevCtr: Number(p.ctr ?? 0),
|
|
2196
|
+
prevPosition: Number(p.position ?? 0)
|
|
2197
|
+
};
|
|
2198
|
+
if (passesComparisonFilter(filter, {
|
|
2199
|
+
isNew: false,
|
|
2200
|
+
isLost: true,
|
|
2201
|
+
clicksChange: -Number(p.clicks ?? 0)
|
|
2202
|
+
})) merged.push(row);
|
|
2203
|
+
}
|
|
2204
|
+
if (state.orderBy) {
|
|
2205
|
+
const { column, dir } = state.orderBy;
|
|
2206
|
+
merged.sort((a, b) => {
|
|
2207
|
+
const av = Number(a[column]) || 0;
|
|
2208
|
+
const bv = Number(b[column]) || 0;
|
|
2209
|
+
return dir === "asc" ? av - bv : bv - av;
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
results: merged.map((r) => coerceNumericCols(r)),
|
|
2214
|
+
meta: {
|
|
2215
|
+
totalCount: merged.length,
|
|
2216
|
+
totals
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function passesComparisonFilter(filter, ctx) {
|
|
2221
|
+
switch (filter) {
|
|
2222
|
+
case "new": return ctx.isNew;
|
|
2223
|
+
case "lost": return ctx.isLost;
|
|
2224
|
+
case "improving": return ctx.clicksChange > 0;
|
|
2225
|
+
case "declining": return ctx.clicksChange < 0;
|
|
2226
|
+
default: return true;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function buildDataDetailRows(params) {
|
|
2230
|
+
const state = requireBuilderState(params.q, "data-detail");
|
|
2231
|
+
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
2232
|
+
const queries = {
|
|
2233
|
+
main: state,
|
|
2234
|
+
totals: totalsState(state)
|
|
2235
|
+
};
|
|
2236
|
+
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
2237
|
+
if (prev) queries.prevTotals = totalsState(prev);
|
|
2238
|
+
return queries;
|
|
2239
|
+
}
|
|
2240
|
+
function shapeDataDetailRowResults(rowMap, params) {
|
|
2241
|
+
const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
2242
|
+
const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2243
|
+
const daily = startDate && endDate ? padTimeseries(coerced, {
|
|
2244
|
+
startDate,
|
|
2245
|
+
endDate
|
|
2246
|
+
}) : coerced;
|
|
2247
|
+
const meta = { totals: readTotals(rowMap.totals?.[0]) };
|
|
2248
|
+
if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
|
|
2249
|
+
return {
|
|
2250
|
+
results: daily,
|
|
2251
|
+
meta
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2118
2254
|
function shapeDataQueryRows(rows, params, extras) {
|
|
2119
2255
|
return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
|
|
2120
2256
|
}
|
|
@@ -2200,6 +2336,16 @@ const dataDetailAnalyzer = defineAnalyzer({
|
|
|
2200
2336
|
results,
|
|
2201
2337
|
meta
|
|
2202
2338
|
};
|
|
2339
|
+
},
|
|
2340
|
+
buildRows(params) {
|
|
2341
|
+
return buildDataDetailRows(params);
|
|
2342
|
+
},
|
|
2343
|
+
reduceRows(rows, params) {
|
|
2344
|
+
const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2345
|
+
return {
|
|
2346
|
+
results,
|
|
2347
|
+
meta
|
|
2348
|
+
};
|
|
2203
2349
|
}
|
|
2204
2350
|
});
|
|
2205
2351
|
const dataQueryAnalyzer = defineAnalyzer({
|
|
@@ -2230,6 +2376,16 @@ const dataQueryAnalyzer = defineAnalyzer({
|
|
|
2230
2376
|
results,
|
|
2231
2377
|
meta
|
|
2232
2378
|
};
|
|
2379
|
+
},
|
|
2380
|
+
buildRows(params) {
|
|
2381
|
+
return buildDataQueryRows(params);
|
|
2382
|
+
},
|
|
2383
|
+
reduceRows(rows, params) {
|
|
2384
|
+
const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2385
|
+
return {
|
|
2386
|
+
results,
|
|
2387
|
+
meta
|
|
2388
|
+
};
|
|
2233
2389
|
}
|
|
2234
2390
|
});
|
|
2235
2391
|
const sortResults$1 = createMetricSorter("lostClicks", {
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { buildExtrasQueries, buildTotalsSql, mergeExtras, pgResolverAdapter, res
|
|
|
9
9
|
import { ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery } from "@gscdump/engine/source";
|
|
10
10
|
import { computeInputHash, createReportRegistry, defineReport } from "@gscdump/engine/report";
|
|
11
11
|
import { canProxyToGsc } from "@gscdump/engine-gsc-api";
|
|
12
|
+
import { isStateResolvable } from "gscdump/query/plan";
|
|
12
13
|
function clamp01(value) {
|
|
13
14
|
if (value < 0) return 0;
|
|
14
15
|
if (value > 1) return 1;
|
|
@@ -2331,6 +2332,142 @@ function buildDataQueryPlan(params, options) {
|
|
|
2331
2332
|
extraQueries
|
|
2332
2333
|
};
|
|
2333
2334
|
}
|
|
2335
|
+
function totalsState(state) {
|
|
2336
|
+
return {
|
|
2337
|
+
...state,
|
|
2338
|
+
dimensions: [],
|
|
2339
|
+
orderBy: void 0,
|
|
2340
|
+
rowLimit: 1
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function readTotals(row) {
|
|
2344
|
+
return {
|
|
2345
|
+
clicks: Number(row?.clicks ?? 0),
|
|
2346
|
+
impressions: Number(row?.impressions ?? 0),
|
|
2347
|
+
ctr: Number(row?.ctr ?? 0),
|
|
2348
|
+
position: Number(row?.position ?? 0)
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function buildDataQueryRows(params) {
|
|
2352
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2353
|
+
if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
|
|
2354
|
+
const queries = {
|
|
2355
|
+
main: state,
|
|
2356
|
+
totals: totalsState(state)
|
|
2357
|
+
};
|
|
2358
|
+
const prev = optionalBuilderState(params.qc, "data-query", "qc");
|
|
2359
|
+
if (prev) {
|
|
2360
|
+
queries.prevMain = prev;
|
|
2361
|
+
queries.prevTotals = totalsState(prev);
|
|
2362
|
+
}
|
|
2363
|
+
return queries;
|
|
2364
|
+
}
|
|
2365
|
+
function shapeDataQueryRowResults(rowMap, params) {
|
|
2366
|
+
const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2367
|
+
const totals = readTotals(rowMap.totals?.[0]);
|
|
2368
|
+
if (params.qc == null) return {
|
|
2369
|
+
results: main,
|
|
2370
|
+
meta: {
|
|
2371
|
+
totalCount: main.length,
|
|
2372
|
+
totals
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
2376
|
+
const dims = state.dimensions;
|
|
2377
|
+
const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
|
|
2378
|
+
const prevByKey = /* @__PURE__ */ new Map();
|
|
2379
|
+
for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
|
|
2380
|
+
const filter = params.comparisonFilter;
|
|
2381
|
+
const merged = [];
|
|
2382
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2383
|
+
for (const cur of main) {
|
|
2384
|
+
const key = keyOf(cur);
|
|
2385
|
+
seen.add(key);
|
|
2386
|
+
const prev = prevByKey.get(key);
|
|
2387
|
+
const row = {
|
|
2388
|
+
...cur,
|
|
2389
|
+
prevClicks: Number(prev?.clicks ?? 0),
|
|
2390
|
+
prevImpressions: Number(prev?.impressions ?? 0),
|
|
2391
|
+
prevCtr: Number(prev?.ctr ?? 0),
|
|
2392
|
+
prevPosition: Number(prev?.position ?? 0)
|
|
2393
|
+
};
|
|
2394
|
+
if (passesComparisonFilter(filter, {
|
|
2395
|
+
isNew: !prev,
|
|
2396
|
+
isLost: false,
|
|
2397
|
+
clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
|
|
2398
|
+
})) merged.push(row);
|
|
2399
|
+
}
|
|
2400
|
+
for (const prev of rowMap.prevMain ?? []) {
|
|
2401
|
+
const p = prev;
|
|
2402
|
+
const key = keyOf(p);
|
|
2403
|
+
if (seen.has(key)) continue;
|
|
2404
|
+
const row = {
|
|
2405
|
+
...p,
|
|
2406
|
+
clicks: 0,
|
|
2407
|
+
impressions: 0,
|
|
2408
|
+
ctr: 0,
|
|
2409
|
+
position: 0,
|
|
2410
|
+
prevClicks: Number(p.clicks ?? 0),
|
|
2411
|
+
prevImpressions: Number(p.impressions ?? 0),
|
|
2412
|
+
prevCtr: Number(p.ctr ?? 0),
|
|
2413
|
+
prevPosition: Number(p.position ?? 0)
|
|
2414
|
+
};
|
|
2415
|
+
if (passesComparisonFilter(filter, {
|
|
2416
|
+
isNew: false,
|
|
2417
|
+
isLost: true,
|
|
2418
|
+
clicksChange: -Number(p.clicks ?? 0)
|
|
2419
|
+
})) merged.push(row);
|
|
2420
|
+
}
|
|
2421
|
+
if (state.orderBy) {
|
|
2422
|
+
const { column, dir } = state.orderBy;
|
|
2423
|
+
merged.sort((a, b) => {
|
|
2424
|
+
const av = Number(a[column]) || 0;
|
|
2425
|
+
const bv = Number(b[column]) || 0;
|
|
2426
|
+
return dir === "asc" ? av - bv : bv - av;
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
results: merged.map((r) => coerceNumericCols(r)),
|
|
2431
|
+
meta: {
|
|
2432
|
+
totalCount: merged.length,
|
|
2433
|
+
totals
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function passesComparisonFilter(filter, ctx) {
|
|
2438
|
+
switch (filter) {
|
|
2439
|
+
case "new": return ctx.isNew;
|
|
2440
|
+
case "lost": return ctx.isLost;
|
|
2441
|
+
case "improving": return ctx.clicksChange > 0;
|
|
2442
|
+
case "declining": return ctx.clicksChange < 0;
|
|
2443
|
+
default: return true;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function buildDataDetailRows(params) {
|
|
2447
|
+
const state = requireBuilderState(params.q, "data-detail");
|
|
2448
|
+
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
2449
|
+
const queries = {
|
|
2450
|
+
main: state,
|
|
2451
|
+
totals: totalsState(state)
|
|
2452
|
+
};
|
|
2453
|
+
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
2454
|
+
if (prev) queries.prevTotals = totalsState(prev);
|
|
2455
|
+
return queries;
|
|
2456
|
+
}
|
|
2457
|
+
function shapeDataDetailRowResults(rowMap, params) {
|
|
2458
|
+
const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
2459
|
+
const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
2460
|
+
const daily = startDate && endDate ? padTimeseries$1(coerced, {
|
|
2461
|
+
startDate,
|
|
2462
|
+
endDate
|
|
2463
|
+
}) : coerced;
|
|
2464
|
+
const meta = { totals: readTotals(rowMap.totals?.[0]) };
|
|
2465
|
+
if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
|
|
2466
|
+
return {
|
|
2467
|
+
results: daily,
|
|
2468
|
+
meta
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2334
2471
|
function shapeDataQueryRows(rows, params, extras) {
|
|
2335
2472
|
return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
|
|
2336
2473
|
}
|
|
@@ -2565,6 +2702,16 @@ const dataDetailAnalyzer = defineAnalyzer$1({
|
|
|
2565
2702
|
results,
|
|
2566
2703
|
meta
|
|
2567
2704
|
};
|
|
2705
|
+
},
|
|
2706
|
+
buildRows(params) {
|
|
2707
|
+
return buildDataDetailRows(params);
|
|
2708
|
+
},
|
|
2709
|
+
reduceRows(rows, params) {
|
|
2710
|
+
const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2711
|
+
return {
|
|
2712
|
+
results,
|
|
2713
|
+
meta
|
|
2714
|
+
};
|
|
2568
2715
|
}
|
|
2569
2716
|
});
|
|
2570
2717
|
const dataQueryAnalyzer = defineAnalyzer$1({
|
|
@@ -2595,6 +2742,16 @@ const dataQueryAnalyzer = defineAnalyzer$1({
|
|
|
2595
2742
|
results,
|
|
2596
2743
|
meta
|
|
2597
2744
|
};
|
|
2745
|
+
},
|
|
2746
|
+
buildRows(params) {
|
|
2747
|
+
return buildDataQueryRows(params);
|
|
2748
|
+
},
|
|
2749
|
+
reduceRows(rows, params) {
|
|
2750
|
+
const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
|
|
2751
|
+
return {
|
|
2752
|
+
results,
|
|
2753
|
+
meta
|
|
2754
|
+
};
|
|
2598
2755
|
}
|
|
2599
2756
|
});
|
|
2600
2757
|
const sortResults$1 = createMetricSorter("lostClicks", {
|
|
@@ -6767,6 +6924,7 @@ function nextDay(day) {
|
|
|
6767
6924
|
}
|
|
6768
6925
|
function shouldRouteToLive(state, site) {
|
|
6769
6926
|
if (!canProxyToGsc(state)) return false;
|
|
6927
|
+
if (!isStateResolvable(state)) return true;
|
|
6770
6928
|
const { startDate, endDate } = extractDateRange(state.filter);
|
|
6771
6929
|
if (!startDate || !endDate) return false;
|
|
6772
6930
|
if (!site.oldestDateSynced || !site.newestDateSynced) return true;
|
package/dist/query/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ResolverOptions } from "@gscdump/engine/resolver";
|
|
2
|
+
import { BuilderState } from "gscdump/query";
|
|
2
3
|
import { AnalysisParams } from "@gscdump/engine/analysis-types";
|
|
3
4
|
import { Row } from "@gscdump/engine/contracts";
|
|
4
5
|
interface QueryAnalyzerExtraQuery {
|
|
@@ -13,6 +14,20 @@ interface QueryAnalyzerPlan<TK extends string = string> {
|
|
|
13
14
|
extraQueries?: QueryAnalyzerExtraQuery[];
|
|
14
15
|
}
|
|
15
16
|
declare function buildDataQueryPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
|
|
17
|
+
/** Row-query plan for `data-query`. Comparison (`qc`) joins two periods. */
|
|
18
|
+
declare function buildDataQueryRows(params: AnalysisParams): Record<string, BuilderState>;
|
|
19
|
+
/** Post-processing for `data-query` row results. Mirrors `shapeDataQuery`. */
|
|
20
|
+
declare function shapeDataQueryRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
|
|
21
|
+
results: Row[];
|
|
22
|
+
meta: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
/** Row-query plan for `data-detail`. `qc` adds a previous-period totals query. */
|
|
25
|
+
declare function buildDataDetailRows(params: AnalysisParams): Record<string, BuilderState>;
|
|
26
|
+
/** Post-processing for `data-detail` row results. Mirrors `shapeDataDetailRows`. */
|
|
27
|
+
declare function shapeDataDetailRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
|
|
28
|
+
results: Row[];
|
|
29
|
+
meta: Record<string, unknown>;
|
|
30
|
+
};
|
|
16
31
|
/**
|
|
17
32
|
* Pure post-processing for `data-query` rows. Adapter-free so reducers can
|
|
18
33
|
* call it without re-running the SQL plan.
|
|
@@ -35,4 +50,4 @@ declare function shapeDataDetailRows(rows: Row[], params: AnalysisParams, extras
|
|
|
35
50
|
* Idempotent: `normalizeQuery(normalizeQuery(q)) === normalizeQuery(q)`.
|
|
36
51
|
*/
|
|
37
52
|
declare function normalizeQuery(query: string): string;
|
|
38
|
-
export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
|
|
53
|
+
export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };
|
package/dist/query/index.mjs
CHANGED
|
@@ -99,6 +99,142 @@ function buildDataQueryPlan(params, options) {
|
|
|
99
99
|
extraQueries
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
function totalsState(state) {
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
dimensions: [],
|
|
106
|
+
orderBy: void 0,
|
|
107
|
+
rowLimit: 1
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function readTotals(row) {
|
|
111
|
+
return {
|
|
112
|
+
clicks: Number(row?.clicks ?? 0),
|
|
113
|
+
impressions: Number(row?.impressions ?? 0),
|
|
114
|
+
ctr: Number(row?.ctr ?? 0),
|
|
115
|
+
position: Number(row?.position ?? 0)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildDataQueryRows(params) {
|
|
119
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
120
|
+
if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
|
|
121
|
+
const queries = {
|
|
122
|
+
main: state,
|
|
123
|
+
totals: totalsState(state)
|
|
124
|
+
};
|
|
125
|
+
const prev = optionalBuilderState(params.qc, "data-query", "qc");
|
|
126
|
+
if (prev) {
|
|
127
|
+
queries.prevMain = prev;
|
|
128
|
+
queries.prevTotals = totalsState(prev);
|
|
129
|
+
}
|
|
130
|
+
return queries;
|
|
131
|
+
}
|
|
132
|
+
function shapeDataQueryRowResults(rowMap, params) {
|
|
133
|
+
const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
134
|
+
const totals = readTotals(rowMap.totals?.[0]);
|
|
135
|
+
if (params.qc == null) return {
|
|
136
|
+
results: main,
|
|
137
|
+
meta: {
|
|
138
|
+
totalCount: main.length,
|
|
139
|
+
totals
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const state = requireBuilderState(params.q, "data-query");
|
|
143
|
+
const dims = state.dimensions;
|
|
144
|
+
const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
|
|
145
|
+
const prevByKey = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
|
|
147
|
+
const filter = params.comparisonFilter;
|
|
148
|
+
const merged = [];
|
|
149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const cur of main) {
|
|
151
|
+
const key = keyOf(cur);
|
|
152
|
+
seen.add(key);
|
|
153
|
+
const prev = prevByKey.get(key);
|
|
154
|
+
const row = {
|
|
155
|
+
...cur,
|
|
156
|
+
prevClicks: Number(prev?.clicks ?? 0),
|
|
157
|
+
prevImpressions: Number(prev?.impressions ?? 0),
|
|
158
|
+
prevCtr: Number(prev?.ctr ?? 0),
|
|
159
|
+
prevPosition: Number(prev?.position ?? 0)
|
|
160
|
+
};
|
|
161
|
+
if (passesComparisonFilter(filter, {
|
|
162
|
+
isNew: !prev,
|
|
163
|
+
isLost: false,
|
|
164
|
+
clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
|
|
165
|
+
})) merged.push(row);
|
|
166
|
+
}
|
|
167
|
+
for (const prev of rowMap.prevMain ?? []) {
|
|
168
|
+
const p = prev;
|
|
169
|
+
const key = keyOf(p);
|
|
170
|
+
if (seen.has(key)) continue;
|
|
171
|
+
const row = {
|
|
172
|
+
...p,
|
|
173
|
+
clicks: 0,
|
|
174
|
+
impressions: 0,
|
|
175
|
+
ctr: 0,
|
|
176
|
+
position: 0,
|
|
177
|
+
prevClicks: Number(p.clicks ?? 0),
|
|
178
|
+
prevImpressions: Number(p.impressions ?? 0),
|
|
179
|
+
prevCtr: Number(p.ctr ?? 0),
|
|
180
|
+
prevPosition: Number(p.position ?? 0)
|
|
181
|
+
};
|
|
182
|
+
if (passesComparisonFilter(filter, {
|
|
183
|
+
isNew: false,
|
|
184
|
+
isLost: true,
|
|
185
|
+
clicksChange: -Number(p.clicks ?? 0)
|
|
186
|
+
})) merged.push(row);
|
|
187
|
+
}
|
|
188
|
+
if (state.orderBy) {
|
|
189
|
+
const { column, dir } = state.orderBy;
|
|
190
|
+
merged.sort((a, b) => {
|
|
191
|
+
const av = Number(a[column]) || 0;
|
|
192
|
+
const bv = Number(b[column]) || 0;
|
|
193
|
+
return dir === "asc" ? av - bv : bv - av;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
results: merged.map((r) => coerceNumericCols(r)),
|
|
198
|
+
meta: {
|
|
199
|
+
totalCount: merged.length,
|
|
200
|
+
totals
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function passesComparisonFilter(filter, ctx) {
|
|
205
|
+
switch (filter) {
|
|
206
|
+
case "new": return ctx.isNew;
|
|
207
|
+
case "lost": return ctx.isLost;
|
|
208
|
+
case "improving": return ctx.clicksChange > 0;
|
|
209
|
+
case "declining": return ctx.clicksChange < 0;
|
|
210
|
+
default: return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function buildDataDetailRows(params) {
|
|
214
|
+
const state = requireBuilderState(params.q, "data-detail");
|
|
215
|
+
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
216
|
+
const queries = {
|
|
217
|
+
main: state,
|
|
218
|
+
totals: totalsState(state)
|
|
219
|
+
};
|
|
220
|
+
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
221
|
+
if (prev) queries.prevTotals = totalsState(prev);
|
|
222
|
+
return queries;
|
|
223
|
+
}
|
|
224
|
+
function shapeDataDetailRowResults(rowMap, params) {
|
|
225
|
+
const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
226
|
+
const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
|
|
227
|
+
const daily = startDate && endDate ? padTimeseries(coerced, {
|
|
228
|
+
startDate,
|
|
229
|
+
endDate
|
|
230
|
+
}) : coerced;
|
|
231
|
+
const meta = { totals: readTotals(rowMap.totals?.[0]) };
|
|
232
|
+
if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
|
|
233
|
+
return {
|
|
234
|
+
results: daily,
|
|
235
|
+
meta
|
|
236
|
+
};
|
|
237
|
+
}
|
|
102
238
|
function shapeDataQueryRows(rows, params, extras) {
|
|
103
239
|
return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
|
|
104
240
|
}
|
|
@@ -305,4 +441,4 @@ const WHITESPACE_RE = /\s+/g;
|
|
|
305
441
|
function normalizeQuery(query) {
|
|
306
442
|
return query.toLowerCase().replace(SEPARATOR_RE, " ").replace(WHITESPACE_RE, " ").trim().split(" ").filter(Boolean).map((token) => SYNONYMS[token] ?? token).filter(Boolean).map(depluralize).sort().join(" ");
|
|
307
443
|
}
|
|
308
|
-
export { buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
|
|
444
|
+
export { buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };
|
package/dist/source/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BuilderState } from "gscdump/query";
|
|
2
|
-
import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
|
|
3
2
|
import { PlannerCapabilities } from "gscdump/query/plan";
|
|
3
|
+
import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
|
|
4
4
|
interface SyncedRange {
|
|
5
5
|
oldestDateSynced: string | null;
|
|
6
6
|
newestDateSynced: string | null;
|
package/dist/source/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canProxyToGsc } from "@gscdump/engine-gsc-api";
|
|
2
2
|
import { extractDateRange } from "gscdump/query";
|
|
3
|
+
import { isStateResolvable } from "gscdump/query/plan";
|
|
3
4
|
function hasGapInCoveredSpans(start, end, coveredSpans) {
|
|
4
5
|
let cursor = start;
|
|
5
6
|
for (const span of coveredSpans) {
|
|
@@ -17,6 +18,7 @@ function nextDay(day) {
|
|
|
17
18
|
}
|
|
18
19
|
function shouldRouteToLive(state, site) {
|
|
19
20
|
if (!canProxyToGsc(state)) return false;
|
|
21
|
+
if (!isStateResolvable(state)) return true;
|
|
20
22
|
const { startDate, endDate } = extractDateRange(state.filter);
|
|
21
23
|
if (!startDate || !endDate) return false;
|
|
22
24
|
if (!site.oldestDateSynced || !site.newestDateSynced) return true;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.19.
|
|
4
|
+
"version": "0.19.4",
|
|
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",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"drizzle-orm": "^0.45.2",
|
|
78
|
-
"@gscdump/engine": "0.19.
|
|
79
|
-
"@gscdump/engine-gsc-api": "0.19.
|
|
80
|
-
"gscdump": "0.19.
|
|
78
|
+
"@gscdump/engine": "0.19.4",
|
|
79
|
+
"@gscdump/engine-gsc-api": "0.19.4",
|
|
80
|
+
"gscdump": "0.19.4"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"vitest": "^4.1.6"
|