@gscdump/analysis 0.39.0 → 0.40.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/README.md +9 -5
- package/dist/analyzer/index.d.mts +10 -9
- package/dist/analyzer/index.mjs +73 -63
- package/dist/default-registry.mjs +73 -63
- package/dist/index.d.mts +55 -13
- package/dist/index.mjs +79 -66
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -89,7 +89,9 @@ const registry = createAnalyzerRegistry({ rows: ROW_ANALYZERS, sql: SQL_ANALYZER
|
|
|
89
89
|
const result = await runAnalyzerFromSource(source, { type: 'striking-distance', minImpressions: 100 }, registry)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`attachParquetIndex` and `attachSnapshotIndex` from `@gscdump/engine
|
|
92
|
+
`attachParquetIndex` and `attachSnapshotIndex` from `@gscdump/engine/node`
|
|
93
|
+
wire parquet files (per-day, per-month, or pre-baked `.duckdb` snapshots) into
|
|
94
|
+
a Node DuckDB session.
|
|
93
95
|
|
|
94
96
|
## Browser (DuckDB-WASM)
|
|
95
97
|
|
|
@@ -98,7 +100,6 @@ import { analyzeInBrowser } from '@gscdump/analysis'
|
|
|
98
100
|
// Compose your own narrow registry instead of pulling the kitchen-sink default
|
|
99
101
|
// (which statically imports every SQL analyzer). For demo only:
|
|
100
102
|
import { defaultAnalyzerRegistry } from '@gscdump/analysis/registry'
|
|
101
|
-
import { createEngine } from '@gscdump/engine-duckdb-wasm'
|
|
102
103
|
|
|
103
104
|
const result = await analyzeInBrowser(
|
|
104
105
|
runner,
|
|
@@ -110,7 +111,10 @@ const result = await analyzeInBrowser(
|
|
|
110
111
|
|
|
111
112
|
`analyzeInBrowser` wraps any runner with `query(sql, params, signal?)` in an `AnalysisQuerySource` with the `attachedTables` capability and dispatches via `runAnalyzerFromSource`.
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
`@gscdump/engine-duckdb-wasm` exports `bootDuckDBWasm`,
|
|
115
|
+
`attachParquetUrlTables`, `createBrowserAnalysisRuntime`, and `resolveWindow`
|
|
116
|
+
(re-exported from `@gscdump/engine/period`). Browser analysis uses attached
|
|
117
|
+
tables rather than a canonical-schema `createEngine`; see ADR-0001.
|
|
114
118
|
|
|
115
119
|
## SQLite (D1 / Cloudflare Workers)
|
|
116
120
|
|
|
@@ -148,7 +152,7 @@ Available source factories:
|
|
|
148
152
|
- `createCompositeSource({ engine, gsc })` — `@gscdump/analysis/source`; engine first, GSC fallback
|
|
149
153
|
- `createInMemoryQuerySource({ queryRows })` — `@gscdump/analysis/source`
|
|
150
154
|
- `createEngineQuerySource({ engine, ctx })` — `@gscdump/engine/source`
|
|
151
|
-
- `createEngine({ ... })` — `@gscdump/engine-
|
|
155
|
+
- `createEngine({ ... })` — `@gscdump/engine-sqlite`
|
|
152
156
|
|
|
153
157
|
Portable analyzers currently cover the row-based tools:
|
|
154
158
|
`striking-distance`, `opportunity`, `brand`, `clustering`, `concentration`,
|
|
@@ -192,7 +196,7 @@ Presets: `last-7d`, `last-28d`, `last-30d`, `last-90d`, `last-180d`, `last-365d`
|
|
|
192
196
|
|
|
193
197
|
- [`gscdump`](../gscdump) — REST client + query builder (edge-safe).
|
|
194
198
|
- [`@gscdump/engine`](../engine) — Parquet/DuckDB storage engine + analyzer/source/period contracts.
|
|
195
|
-
- [`@gscdump/engine
|
|
199
|
+
- [`@gscdump/engine/node`](../engine) — Node DuckDB handle + parquet/snapshot attach helpers.
|
|
196
200
|
- [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser runtime + drizzle adapter.
|
|
197
201
|
- [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 dialect adapter.
|
|
198
202
|
- [`@gscdump/engine-gsc-api`](../engine-gsc-api) — GSC live-API engine adapter.
|
|
@@ -81,6 +81,13 @@ interface BrandSegmentationOptions {
|
|
|
81
81
|
/** Minimum impressions for a keyword to be included. Default: 10 */
|
|
82
82
|
minImpressions?: number;
|
|
83
83
|
}
|
|
84
|
+
interface BrandSegmentationRow {
|
|
85
|
+
query?: string;
|
|
86
|
+
keyword?: string;
|
|
87
|
+
variants?: readonly string[];
|
|
88
|
+
clicks?: number | null;
|
|
89
|
+
impressions?: number | null;
|
|
90
|
+
}
|
|
84
91
|
interface BrandSummary {
|
|
85
92
|
brandClicks: number;
|
|
86
93
|
nonBrandClicks: number;
|
|
@@ -88,9 +95,9 @@ interface BrandSummary {
|
|
|
88
95
|
brandImpressions: number;
|
|
89
96
|
nonBrandImpressions: number;
|
|
90
97
|
}
|
|
91
|
-
interface BrandSegmentationResult {
|
|
92
|
-
brand:
|
|
93
|
-
nonBrand:
|
|
98
|
+
interface BrandSegmentationResult<T extends BrandSegmentationRow = QueriesRow> {
|
|
99
|
+
brand: T[];
|
|
100
|
+
nonBrand: T[];
|
|
94
101
|
summary: BrandSummary;
|
|
95
102
|
}
|
|
96
103
|
interface BrandResultRow {
|
|
@@ -698,12 +705,6 @@ interface TrendsResult {
|
|
|
698
705
|
series: TrendSeriesPoint[];
|
|
699
706
|
}
|
|
700
707
|
declare const trendsAnalyzer: import("@gscdump/engine/analyzer").DefinedAnalyzer;
|
|
701
|
-
/**
|
|
702
|
-
* `zero-click` — high impressions, low CTR, good ranking. SQL groups by
|
|
703
|
-
* (query, url) and applies HAVING/WHERE pushdown; row reducer dedupes to the
|
|
704
|
-
* best-positioned page per query (different semantics, kept for parity with
|
|
705
|
-
* the existing live-GSC behavior).
|
|
706
|
-
*/
|
|
707
708
|
interface ZeroClickResult {
|
|
708
709
|
query: string;
|
|
709
710
|
page: string;
|
package/dist/analyzer/index.mjs
CHANGED
|
@@ -522,8 +522,7 @@ function analyzeBrandSegmentation(keywords, options) {
|
|
|
522
522
|
const impressions = num(row.impressions);
|
|
523
523
|
if (impressions < minImpressions) continue;
|
|
524
524
|
const clicks = num(row.clicks);
|
|
525
|
-
|
|
526
|
-
if (lowerBrandTerms.some((term) => query.includes(term))) {
|
|
525
|
+
if ((row.variants?.length ? row.variants : [row.query ?? row.keyword ?? ""]).some((query) => lowerBrandTerms.some((term) => query.toLowerCase().includes(term)))) {
|
|
527
526
|
brand.push(row);
|
|
528
527
|
brandClicks += clicks;
|
|
529
528
|
brandImpressions += impressions;
|
|
@@ -3502,6 +3501,52 @@ function calculateCtrGapScore(actualCtr, position) {
|
|
|
3502
3501
|
const gap = expectedCtr - actualCtr;
|
|
3503
3502
|
return Math.min(gap / expectedCtr, 1);
|
|
3504
3503
|
}
|
|
3504
|
+
const SORT_DIR = {
|
|
3505
|
+
opportunityScore: "desc",
|
|
3506
|
+
potentialClicks: "desc",
|
|
3507
|
+
impressions: "desc",
|
|
3508
|
+
position: "asc"
|
|
3509
|
+
};
|
|
3510
|
+
function analyzeOpportunity(keywords, options = {}) {
|
|
3511
|
+
const minImpressions = options.minImpressions ?? 100;
|
|
3512
|
+
const positionWeight = options.weights?.position ?? 1;
|
|
3513
|
+
const impressionsWeight = options.weights?.impressions ?? 1;
|
|
3514
|
+
const ctrGapWeight = options.weights?.ctrGap ?? 1;
|
|
3515
|
+
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
3516
|
+
const sortBy = options.sortBy ?? "opportunityScore";
|
|
3517
|
+
const results = [];
|
|
3518
|
+
for (const row of keywords) {
|
|
3519
|
+
const impressions = num(row.impressions);
|
|
3520
|
+
if (impressions < minImpressions) continue;
|
|
3521
|
+
const position = num(row.position);
|
|
3522
|
+
const ctr = num(row.ctr);
|
|
3523
|
+
const clicks = num(row.clicks);
|
|
3524
|
+
const positionScore = calculatePositionScore(position);
|
|
3525
|
+
const impressionScore = calculateImpressionScore(impressions);
|
|
3526
|
+
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3527
|
+
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
3528
|
+
const opportunityScore = Math.round(weightedProduct ** (1 / totalWeight) * 100);
|
|
3529
|
+
const potentialClicks = Math.round(impressions * getExpectedCtr(Math.min(3, position)));
|
|
3530
|
+
results.push({
|
|
3531
|
+
keyword: row.query,
|
|
3532
|
+
page: row.page ?? null,
|
|
3533
|
+
clicks,
|
|
3534
|
+
impressions,
|
|
3535
|
+
ctr,
|
|
3536
|
+
position,
|
|
3537
|
+
opportunityScore,
|
|
3538
|
+
potentialClicks,
|
|
3539
|
+
factors: {
|
|
3540
|
+
positionScore,
|
|
3541
|
+
impressionScore,
|
|
3542
|
+
ctrGapScore
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
const direction = SORT_DIR[sortBy];
|
|
3547
|
+
results.sort((left, right) => direction === "asc" ? left[sortBy] - right[sortBy] : right[sortBy] - left[sortBy]);
|
|
3548
|
+
return options.limit ? results.slice(0, options.limit) : results;
|
|
3549
|
+
}
|
|
3505
3550
|
const opportunityAnalyzer = defineAnalyzer({
|
|
3506
3551
|
id: "opportunity",
|
|
3507
3552
|
buildSql(params) {
|
|
@@ -3615,46 +3660,11 @@ const opportunityAnalyzer = defineAnalyzer({
|
|
|
3615
3660
|
return { queries: queriesQueryState(periodOf(params), params.limit) };
|
|
3616
3661
|
},
|
|
3617
3662
|
reduceRows(rows, params) {
|
|
3618
|
-
const
|
|
3619
|
-
const minImpressions = params.minImpressions ?? 100;
|
|
3620
|
-
const positionWeight = 1;
|
|
3621
|
-
const impressionsWeight = 1;
|
|
3622
|
-
const ctrGapWeight = 1;
|
|
3623
|
-
const sortBy = "opportunityScore";
|
|
3624
|
-
const results = [];
|
|
3625
|
-
for (const row of keywords) {
|
|
3626
|
-
const impressions = num(row.impressions);
|
|
3627
|
-
const position = num(row.position);
|
|
3628
|
-
const ctr = num(row.ctr);
|
|
3629
|
-
const clicks = num(row.clicks);
|
|
3630
|
-
if (impressions < minImpressions) continue;
|
|
3631
|
-
const positionScore = calculatePositionScore(position);
|
|
3632
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
3633
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3634
|
-
const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
|
|
3635
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
3636
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
3637
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
3638
|
-
results.push({
|
|
3639
|
-
keyword: row.query,
|
|
3640
|
-
page: row.page ?? null,
|
|
3641
|
-
clicks,
|
|
3642
|
-
impressions,
|
|
3643
|
-
ctr,
|
|
3644
|
-
position,
|
|
3645
|
-
opportunityScore,
|
|
3646
|
-
potentialClicks,
|
|
3647
|
-
factors: {
|
|
3648
|
-
positionScore,
|
|
3649
|
-
impressionScore,
|
|
3650
|
-
ctrGapScore
|
|
3651
|
-
}
|
|
3652
|
-
});
|
|
3653
|
-
}
|
|
3663
|
+
const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
|
|
3654
3664
|
const paged = paginateSortedInMemory(results, {
|
|
3655
3665
|
limit: params.limit,
|
|
3656
3666
|
offset: params.offset
|
|
3657
|
-
}, (left, right) => right
|
|
3667
|
+
}, (left, right) => right.opportunityScore - left.opportunityScore);
|
|
3658
3668
|
return {
|
|
3659
3669
|
results: paged,
|
|
3660
3670
|
meta: {
|
|
@@ -4330,6 +4340,12 @@ const stlDecomposeAnalyzer = defineAnalyzer({
|
|
|
4330
4340
|
}
|
|
4331
4341
|
});
|
|
4332
4342
|
const DEFAULT_ROW_LIMIT$1 = 25e3;
|
|
4343
|
+
function paginateStrikingDistance(results, options) {
|
|
4344
|
+
return paginateSortedInMemory(results, {
|
|
4345
|
+
limit: options.limit ?? 1e3,
|
|
4346
|
+
offset: options.offset
|
|
4347
|
+
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4348
|
+
}
|
|
4333
4349
|
function filterStrikingDistance(rows, options = {}) {
|
|
4334
4350
|
const minPosition = options.minPosition ?? 4;
|
|
4335
4351
|
const maxPosition = options.maxPosition ?? 20;
|
|
@@ -4360,10 +4376,7 @@ const strikingDistanceAnalyzer = defineAnalyzer({
|
|
|
4360
4376
|
id: "striking-distance",
|
|
4361
4377
|
reduce(rows, params) {
|
|
4362
4378
|
const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
|
|
4363
|
-
const paged =
|
|
4364
|
-
limit: params.limit ?? 1e3,
|
|
4365
|
-
offset: params.offset
|
|
4366
|
-
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4379
|
+
const paged = paginateStrikingDistance(results, params);
|
|
4367
4380
|
return {
|
|
4368
4381
|
results: paged,
|
|
4369
4382
|
meta: {
|
|
@@ -4739,6 +4752,18 @@ const trendsAnalyzer = defineAnalyzer({
|
|
|
4739
4752
|
}
|
|
4740
4753
|
});
|
|
4741
4754
|
const DEFAULT_ROW_LIMIT = 25e3;
|
|
4755
|
+
function analyzeZeroClick(rows, options = {}) {
|
|
4756
|
+
const minImpressions = options.minImpressions ?? 1e3;
|
|
4757
|
+
const maxCtr = options.maxCtr ?? .03;
|
|
4758
|
+
const maxPosition = options.maxPosition ?? 10;
|
|
4759
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
4760
|
+
for (const row of rows) {
|
|
4761
|
+
if (row.impressions < minImpressions || row.position > maxPosition || row.ctr > maxCtr) continue;
|
|
4762
|
+
const existing = queryMap.get(row.query);
|
|
4763
|
+
if (!existing || row.position < existing.position) queryMap.set(row.query, { ...row });
|
|
4764
|
+
}
|
|
4765
|
+
return [...queryMap.values()].sort((left, right) => right.impressions - left.impressions);
|
|
4766
|
+
}
|
|
4742
4767
|
const zeroClickAnalyzer = defineAnalyzer({
|
|
4743
4768
|
id: "zero-click",
|
|
4744
4769
|
buildSql(params) {
|
|
@@ -4822,26 +4847,11 @@ const zeroClickAnalyzer = defineAnalyzer({
|
|
|
4822
4847
|
return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
|
|
4823
4848
|
},
|
|
4824
4849
|
reduceRows(rows, params) {
|
|
4825
|
-
const
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
for (const row of arr) {
|
|
4831
|
-
if (row.impressions < minImpressions) continue;
|
|
4832
|
-
if (row.position > maxPosition) continue;
|
|
4833
|
-
if (row.ctr > maxCtr) continue;
|
|
4834
|
-
const existing = queryMap.get(row.query);
|
|
4835
|
-
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
4836
|
-
query: row.query,
|
|
4837
|
-
page: row.page,
|
|
4838
|
-
clicks: row.clicks,
|
|
4839
|
-
impressions: row.impressions,
|
|
4840
|
-
ctr: row.ctr,
|
|
4841
|
-
position: row.position
|
|
4842
|
-
});
|
|
4843
|
-
}
|
|
4844
|
-
const results = Array.from(queryMap.values());
|
|
4850
|
+
const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
|
|
4851
|
+
minImpressions: params.minImpressions ?? 1e3,
|
|
4852
|
+
maxCtr: params.maxCtr ?? .03,
|
|
4853
|
+
maxPosition: params.maxPosition ?? 10
|
|
4854
|
+
});
|
|
4845
4855
|
const paged = paginateSortedInMemory(results, {
|
|
4846
4856
|
limit: params.limit,
|
|
4847
4857
|
offset: params.offset
|
|
@@ -522,8 +522,7 @@ function analyzeBrandSegmentation(keywords, options) {
|
|
|
522
522
|
const impressions = num(row.impressions);
|
|
523
523
|
if (impressions < minImpressions) continue;
|
|
524
524
|
const clicks = num(row.clicks);
|
|
525
|
-
|
|
526
|
-
if (lowerBrandTerms.some((term) => query.includes(term))) {
|
|
525
|
+
if ((row.variants?.length ? row.variants : [row.query ?? row.keyword ?? ""]).some((query) => lowerBrandTerms.some((term) => query.toLowerCase().includes(term)))) {
|
|
527
526
|
brand.push(row);
|
|
528
527
|
brandClicks += clicks;
|
|
529
528
|
brandImpressions += impressions;
|
|
@@ -3496,6 +3495,52 @@ function calculateCtrGapScore(actualCtr, position) {
|
|
|
3496
3495
|
const gap = expectedCtr - actualCtr;
|
|
3497
3496
|
return Math.min(gap / expectedCtr, 1);
|
|
3498
3497
|
}
|
|
3498
|
+
const SORT_DIR = {
|
|
3499
|
+
opportunityScore: "desc",
|
|
3500
|
+
potentialClicks: "desc",
|
|
3501
|
+
impressions: "desc",
|
|
3502
|
+
position: "asc"
|
|
3503
|
+
};
|
|
3504
|
+
function analyzeOpportunity(keywords, options = {}) {
|
|
3505
|
+
const minImpressions = options.minImpressions ?? 100;
|
|
3506
|
+
const positionWeight = options.weights?.position ?? 1;
|
|
3507
|
+
const impressionsWeight = options.weights?.impressions ?? 1;
|
|
3508
|
+
const ctrGapWeight = options.weights?.ctrGap ?? 1;
|
|
3509
|
+
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
3510
|
+
const sortBy = options.sortBy ?? "opportunityScore";
|
|
3511
|
+
const results = [];
|
|
3512
|
+
for (const row of keywords) {
|
|
3513
|
+
const impressions = num(row.impressions);
|
|
3514
|
+
if (impressions < minImpressions) continue;
|
|
3515
|
+
const position = num(row.position);
|
|
3516
|
+
const ctr = num(row.ctr);
|
|
3517
|
+
const clicks = num(row.clicks);
|
|
3518
|
+
const positionScore = calculatePositionScore(position);
|
|
3519
|
+
const impressionScore = calculateImpressionScore(impressions);
|
|
3520
|
+
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3521
|
+
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
3522
|
+
const opportunityScore = Math.round(weightedProduct ** (1 / totalWeight) * 100);
|
|
3523
|
+
const potentialClicks = Math.round(impressions * getExpectedCtr(Math.min(3, position)));
|
|
3524
|
+
results.push({
|
|
3525
|
+
keyword: row.query,
|
|
3526
|
+
page: row.page ?? null,
|
|
3527
|
+
clicks,
|
|
3528
|
+
impressions,
|
|
3529
|
+
ctr,
|
|
3530
|
+
position,
|
|
3531
|
+
opportunityScore,
|
|
3532
|
+
potentialClicks,
|
|
3533
|
+
factors: {
|
|
3534
|
+
positionScore,
|
|
3535
|
+
impressionScore,
|
|
3536
|
+
ctrGapScore
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
const direction = SORT_DIR[sortBy];
|
|
3541
|
+
results.sort((left, right) => direction === "asc" ? left[sortBy] - right[sortBy] : right[sortBy] - left[sortBy]);
|
|
3542
|
+
return options.limit ? results.slice(0, options.limit) : results;
|
|
3543
|
+
}
|
|
3499
3544
|
const opportunityAnalyzer = defineAnalyzer({
|
|
3500
3545
|
id: "opportunity",
|
|
3501
3546
|
buildSql(params) {
|
|
@@ -3609,46 +3654,11 @@ const opportunityAnalyzer = defineAnalyzer({
|
|
|
3609
3654
|
return { queries: queriesQueryState(periodOf(params), params.limit) };
|
|
3610
3655
|
},
|
|
3611
3656
|
reduceRows(rows, params) {
|
|
3612
|
-
const
|
|
3613
|
-
const minImpressions = params.minImpressions ?? 100;
|
|
3614
|
-
const positionWeight = 1;
|
|
3615
|
-
const impressionsWeight = 1;
|
|
3616
|
-
const ctrGapWeight = 1;
|
|
3617
|
-
const sortBy = "opportunityScore";
|
|
3618
|
-
const results = [];
|
|
3619
|
-
for (const row of keywords) {
|
|
3620
|
-
const impressions = num(row.impressions);
|
|
3621
|
-
const position = num(row.position);
|
|
3622
|
-
const ctr = num(row.ctr);
|
|
3623
|
-
const clicks = num(row.clicks);
|
|
3624
|
-
if (impressions < minImpressions) continue;
|
|
3625
|
-
const positionScore = calculatePositionScore(position);
|
|
3626
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
3627
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3628
|
-
const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
|
|
3629
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
3630
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
3631
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
3632
|
-
results.push({
|
|
3633
|
-
keyword: row.query,
|
|
3634
|
-
page: row.page ?? null,
|
|
3635
|
-
clicks,
|
|
3636
|
-
impressions,
|
|
3637
|
-
ctr,
|
|
3638
|
-
position,
|
|
3639
|
-
opportunityScore,
|
|
3640
|
-
potentialClicks,
|
|
3641
|
-
factors: {
|
|
3642
|
-
positionScore,
|
|
3643
|
-
impressionScore,
|
|
3644
|
-
ctrGapScore
|
|
3645
|
-
}
|
|
3646
|
-
});
|
|
3647
|
-
}
|
|
3657
|
+
const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
|
|
3648
3658
|
const paged = paginateSortedInMemory(results, {
|
|
3649
3659
|
limit: params.limit,
|
|
3650
3660
|
offset: params.offset
|
|
3651
|
-
}, (left, right) => right
|
|
3661
|
+
}, (left, right) => right.opportunityScore - left.opportunityScore);
|
|
3652
3662
|
return {
|
|
3653
3663
|
results: paged,
|
|
3654
3664
|
meta: {
|
|
@@ -4324,6 +4334,12 @@ const stlDecomposeAnalyzer = defineAnalyzer({
|
|
|
4324
4334
|
}
|
|
4325
4335
|
});
|
|
4326
4336
|
const DEFAULT_ROW_LIMIT$1 = 25e3;
|
|
4337
|
+
function paginateStrikingDistance(results, options) {
|
|
4338
|
+
return paginateSortedInMemory(results, {
|
|
4339
|
+
limit: options.limit ?? 1e3,
|
|
4340
|
+
offset: options.offset
|
|
4341
|
+
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4342
|
+
}
|
|
4327
4343
|
function filterStrikingDistance(rows, options = {}) {
|
|
4328
4344
|
const minPosition = options.minPosition ?? 4;
|
|
4329
4345
|
const maxPosition = options.maxPosition ?? 20;
|
|
@@ -4354,10 +4370,7 @@ const strikingDistanceAnalyzer = defineAnalyzer({
|
|
|
4354
4370
|
id: "striking-distance",
|
|
4355
4371
|
reduce(rows, params) {
|
|
4356
4372
|
const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
|
|
4357
|
-
const paged =
|
|
4358
|
-
limit: params.limit ?? 1e3,
|
|
4359
|
-
offset: params.offset
|
|
4360
|
-
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4373
|
+
const paged = paginateStrikingDistance(results, params);
|
|
4361
4374
|
return {
|
|
4362
4375
|
results: paged,
|
|
4363
4376
|
meta: {
|
|
@@ -4733,6 +4746,18 @@ const trendsAnalyzer = defineAnalyzer({
|
|
|
4733
4746
|
}
|
|
4734
4747
|
});
|
|
4735
4748
|
const DEFAULT_ROW_LIMIT = 25e3;
|
|
4749
|
+
function analyzeZeroClick(rows, options = {}) {
|
|
4750
|
+
const minImpressions = options.minImpressions ?? 1e3;
|
|
4751
|
+
const maxCtr = options.maxCtr ?? .03;
|
|
4752
|
+
const maxPosition = options.maxPosition ?? 10;
|
|
4753
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
4754
|
+
for (const row of rows) {
|
|
4755
|
+
if (row.impressions < minImpressions || row.position > maxPosition || row.ctr > maxCtr) continue;
|
|
4756
|
+
const existing = queryMap.get(row.query);
|
|
4757
|
+
if (!existing || row.position < existing.position) queryMap.set(row.query, { ...row });
|
|
4758
|
+
}
|
|
4759
|
+
return [...queryMap.values()].sort((left, right) => right.impressions - left.impressions);
|
|
4760
|
+
}
|
|
4736
4761
|
const defaultAnalyzerRegistry = createAnalyzerRegistry({ defined: [
|
|
4737
4762
|
bayesianCtrAnalyzer,
|
|
4738
4763
|
bipartitePagerankAnalyzer,
|
|
@@ -4845,26 +4870,11 @@ const defaultAnalyzerRegistry = createAnalyzerRegistry({ defined: [
|
|
|
4845
4870
|
return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
|
|
4846
4871
|
},
|
|
4847
4872
|
reduceRows(rows, params) {
|
|
4848
|
-
const
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
for (const row of arr) {
|
|
4854
|
-
if (row.impressions < minImpressions) continue;
|
|
4855
|
-
if (row.position > maxPosition) continue;
|
|
4856
|
-
if (row.ctr > maxCtr) continue;
|
|
4857
|
-
const existing = queryMap.get(row.query);
|
|
4858
|
-
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
4859
|
-
query: row.query,
|
|
4860
|
-
page: row.page,
|
|
4861
|
-
clicks: row.clicks,
|
|
4862
|
-
impressions: row.impressions,
|
|
4863
|
-
ctr: row.ctr,
|
|
4864
|
-
position: row.position
|
|
4865
|
-
});
|
|
4866
|
-
}
|
|
4867
|
-
const results = Array.from(queryMap.values());
|
|
4873
|
+
const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
|
|
4874
|
+
minImpressions: params.minImpressions ?? 1e3,
|
|
4875
|
+
maxCtr: params.maxCtr ?? .03,
|
|
4876
|
+
maxPosition: params.maxPosition ?? 10
|
|
4877
|
+
});
|
|
4868
4878
|
const paged = paginateSortedInMemory(results, {
|
|
4869
4879
|
limit: params.limit,
|
|
4870
4880
|
offset: params.offset
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Analyzer, Analyzer as Analyzer$1, AnalyzerCapabilityError, AnalyzerRegistry, AnalyzerRegistry as AnalyzerRegistry$1, AnalyzerRegistryInit, AnalyzerVariants, DefineAnalyzerOptions, DefinedAnalyzer, Plan, ReduceContext, ReduceCtx, Reducer, RequiredCapability, RowQueriesPlan, SqlExtraQuery, SqlPlan, SqlPlanSpec, TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
|
|
2
|
-
import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow
|
|
2
|
+
import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
|
|
3
3
|
import { AnalysisParams, AnalysisParams as AnalysisParams$1, AnalysisResult, AnalysisResult as AnalysisResult$1, AnalysisTool, num } from "@gscdump/engine/analysis-types";
|
|
4
4
|
import { Result } from "gscdump/result";
|
|
5
5
|
import { BuilderState } from "gscdump/query";
|
|
6
|
-
import { AnalysisQuerySource, AnalysisQuerySource as AnalysisQuerySource$1, AnalysisSourceKind, AttachedTableRunner, AttachedTableRunner as AnalyzerRunner, AttachedTableSourceOptions, AttachedTableSourceOptions as BrowserAnalyzeOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, QueryRow as QueryRow$1, SourceCapabilities,
|
|
6
|
+
import { AnalysisQuerySource, AnalysisQuerySource as AnalysisQuerySource$1, AnalysisSourceKind, AttachedTableRunner, AttachedTableRunner as AnalyzerRunner, AttachedTableSourceOptions, AttachedTableSourceOptions as BrowserAnalyzeOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, QueryRow as QueryRow$1, SourceCapabilities, createEngineQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine } from "@gscdump/engine/source";
|
|
7
7
|
import { DefineReportOptions, DefinedReport, DefinedReport as DefinedReport$1, ReportAction, ReportContext, ReportContext as ReportContext$1, ReportFinding, ReportParams, ReportPlanStep, ReportResult, ReportResult as ReportResult$1, ReportSection } from "@gscdump/engine/report";
|
|
8
8
|
import { PlannerCapabilities } from "gscdump/query/plan";
|
|
9
9
|
type ActionSource = 'cannibalization' | 'striking-distance' | 'ctr-anomaly' | 'change-point' | 'opportunity';
|
|
@@ -78,6 +78,13 @@ interface BrandSegmentationOptions {
|
|
|
78
78
|
/** Minimum impressions for a keyword to be included. Default: 10 */
|
|
79
79
|
minImpressions?: number;
|
|
80
80
|
}
|
|
81
|
+
interface BrandSegmentationRow {
|
|
82
|
+
query?: string;
|
|
83
|
+
keyword?: string;
|
|
84
|
+
variants?: readonly string[];
|
|
85
|
+
clicks?: number | null;
|
|
86
|
+
impressions?: number | null;
|
|
87
|
+
}
|
|
81
88
|
interface BrandSummary {
|
|
82
89
|
brandClicks: number;
|
|
83
90
|
nonBrandClicks: number;
|
|
@@ -85,16 +92,17 @@ interface BrandSummary {
|
|
|
85
92
|
brandImpressions: number;
|
|
86
93
|
nonBrandImpressions: number;
|
|
87
94
|
}
|
|
88
|
-
interface BrandSegmentationResult {
|
|
89
|
-
brand:
|
|
90
|
-
nonBrand:
|
|
95
|
+
interface BrandSegmentationResult<T extends BrandSegmentationRow = QueriesRow> {
|
|
96
|
+
brand: T[];
|
|
97
|
+
nonBrand: T[];
|
|
91
98
|
summary: BrandSummary;
|
|
92
99
|
}
|
|
93
100
|
/**
|
|
94
101
|
* Pure helper: segment keywords into brand and non-brand based on provided
|
|
95
102
|
* brand terms. Re-exported from `@gscdump/analysis` for portable callers.
|
|
96
103
|
*/
|
|
97
|
-
declare function analyzeBrandSegmentation(keywords: QueriesRow[], options: BrandSegmentationOptions): BrandSegmentationResult
|
|
104
|
+
declare function analyzeBrandSegmentation(keywords: QueriesRow[], options: BrandSegmentationOptions): BrandSegmentationResult<QueriesRow>;
|
|
105
|
+
declare function analyzeBrandSegmentation<T extends BrandSegmentationRow>(keywords: T[], options: BrandSegmentationOptions): BrandSegmentationResult<T>;
|
|
98
106
|
type CannibalizationSortMetric = 'clicks' | 'impressions' | 'positionSpread' | 'pageCount';
|
|
99
107
|
interface CannibalizationOptions {
|
|
100
108
|
/** Minimum impressions for a query to be considered. Default: 10 */
|
|
@@ -295,6 +303,7 @@ interface MoversResult {
|
|
|
295
303
|
* recent changes between two periods.
|
|
296
304
|
*/
|
|
297
305
|
declare function analyzeMovers(input: MoversInput, options?: MoversOptions): MoversResult;
|
|
306
|
+
type OpportunitySortMetric = 'opportunityScore' | 'potentialClicks' | 'impressions' | 'position';
|
|
298
307
|
interface OpportunityFactors {
|
|
299
308
|
positionScore: number;
|
|
300
309
|
impressionScore: number;
|
|
@@ -311,6 +320,19 @@ interface OpportunityResult {
|
|
|
311
320
|
potentialClicks: number;
|
|
312
321
|
factors: OpportunityFactors;
|
|
313
322
|
}
|
|
323
|
+
interface OpportunityWeights {
|
|
324
|
+
position?: number;
|
|
325
|
+
impressions?: number;
|
|
326
|
+
ctrGap?: number;
|
|
327
|
+
}
|
|
328
|
+
interface OpportunityOptions {
|
|
329
|
+
minImpressions?: number;
|
|
330
|
+
weights?: OpportunityWeights;
|
|
331
|
+
sortBy?: OpportunitySortMetric;
|
|
332
|
+
limit?: number;
|
|
333
|
+
}
|
|
334
|
+
/** Pure opportunity scorer shared by hosted and package analyzer callers. */
|
|
335
|
+
declare function analyzeOpportunity(keywords: QueriesRow[], options?: OpportunityOptions): OpportunityResult[];
|
|
314
336
|
type SeasonalityMetric = 'clicks' | 'impressions';
|
|
315
337
|
interface SeasonalityOptions {
|
|
316
338
|
/** Metric to analyze for seasonality. Default: clicks */
|
|
@@ -371,12 +393,25 @@ interface StrikingDistanceResult {
|
|
|
371
393
|
/** Estimated clicks at ~15% CTR (the average for positions 1–3). */
|
|
372
394
|
potentialClicks: number;
|
|
373
395
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
396
|
+
interface StrikingDistanceFilterOptions {
|
|
397
|
+
minPosition?: number;
|
|
398
|
+
maxPosition?: number;
|
|
399
|
+
minImpressions?: number;
|
|
400
|
+
maxCtr?: number;
|
|
401
|
+
}
|
|
402
|
+
interface StrikingDistanceOptions extends StrikingDistanceFilterOptions {
|
|
403
|
+
limit?: number;
|
|
404
|
+
offset?: number;
|
|
405
|
+
}
|
|
406
|
+
/** Pure striking-distance analysis for consumers that already own the rows. */
|
|
407
|
+
declare function analyzeStrikingDistance(rows: readonly {
|
|
408
|
+
query: unknown;
|
|
409
|
+
page?: unknown;
|
|
410
|
+
clicks: unknown;
|
|
411
|
+
impressions: unknown;
|
|
412
|
+
ctr: unknown;
|
|
413
|
+
position: unknown;
|
|
414
|
+
}[], options?: StrikingDistanceOptions): StrikingDistanceResult[];
|
|
380
415
|
interface ZeroClickResult {
|
|
381
416
|
query: string;
|
|
382
417
|
page: string;
|
|
@@ -385,6 +420,13 @@ interface ZeroClickResult {
|
|
|
385
420
|
ctr: number;
|
|
386
421
|
position: number;
|
|
387
422
|
}
|
|
423
|
+
interface ZeroClickOptions {
|
|
424
|
+
minImpressions?: number;
|
|
425
|
+
maxCtr?: number;
|
|
426
|
+
maxPosition?: number;
|
|
427
|
+
}
|
|
428
|
+
/** Pure zero-click detector shared by hosted and package analyzer callers. */
|
|
429
|
+
declare function analyzeZeroClick(rows: QueryPageRow[], options?: ZeroClickOptions): ZeroClickResult[];
|
|
388
430
|
declare function analyzeInBrowser(runner: AttachedTableRunner, opts: AttachedTableSourceOptions, params: AnalysisParams$1, registry: AnalyzerRegistry$1): Promise<AnalysisResult$1>;
|
|
389
431
|
/**
|
|
390
432
|
* Default analyzer registry built from every in-tree analyzer. Convenience
|
|
@@ -604,4 +646,4 @@ interface InMemoryQuerySourceOptions {
|
|
|
604
646
|
}
|
|
605
647
|
declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource$1;
|
|
606
648
|
declare const SQL_ANALYZERS: readonly Analyzer$1[];
|
|
607
|
-
export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, DEFAULT_PRIORITY_SOURCES, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type Effort, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityResult, type PadTimeseriesOptions, type PageRow, type Plan, type PriorityAction, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportAction, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type
|
|
649
|
+
export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, DEFAULT_PRIORITY_SOURCES, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type Effort, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type PriorityAction, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportAction, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, decodeIntent, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnalyzerCapabilityError, createAnalyzerRegistry, createAnalyzerRegistry as createAnalyzerRegistry$1, defineAnalyzer, defineAnalyzer as defineAnalyzer$1, requireAdapter, runAnalyzerFromSource, runAnalyzerFromSource as runAnalyzerFromSource$1 } from "@gscdump/engine/analyzer";
|
|
2
|
-
import { comparisonOf, comparisonOf as comparisonOf$1, defaultEndDate, padTimeseries, padTimeseries as padTimeseries$1, periodOf, periodOf as periodOf$1, resolveWindow
|
|
2
|
+
import { comparisonOf, comparisonOf as comparisonOf$1, defaultEndDate, padTimeseries, padTimeseries as padTimeseries$1, periodOf, periodOf as periodOf$1, resolveWindow } from "@gscdump/engine/period";
|
|
3
3
|
import { enumeratePartitions } from "@gscdump/engine/planner";
|
|
4
4
|
import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
|
|
5
5
|
import { num, num as num$1 } from "@gscdump/engine/analysis-types";
|
|
@@ -8,7 +8,7 @@ import { between, date, extractDateRange, gsc, page, query } from "gscdump/query
|
|
|
8
8
|
import { MS_PER_DAY, daysAgoUtc, toIsoDate } from "gscdump/dates";
|
|
9
9
|
import { buildExtrasQueries, buildTotalsSql, mergeExtras, pgResolverAdapter, resolveComparisonSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
|
|
10
10
|
import pluralize from "pluralize";
|
|
11
|
-
import { ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource,
|
|
11
|
+
import { ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource, queryRows, rewriteForTableSource, runAnalyzerWithEngine } from "@gscdump/engine/source";
|
|
12
12
|
import { computeInputHash, createReportRegistry, defineReport } from "@gscdump/engine/report";
|
|
13
13
|
import { canProxyToGsc } from "@gscdump/engine-gsc-api";
|
|
14
14
|
import { isStateResolvable } from "gscdump/query/plan";
|
|
@@ -754,8 +754,7 @@ function analyzeBrandSegmentation(keywords, options) {
|
|
|
754
754
|
const impressions = num$1(row.impressions);
|
|
755
755
|
if (impressions < minImpressions) continue;
|
|
756
756
|
const clicks = num$1(row.clicks);
|
|
757
|
-
|
|
758
|
-
if (lowerBrandTerms.some((term) => query.includes(term))) {
|
|
757
|
+
if ((row.variants?.length ? row.variants : [row.query ?? row.keyword ?? ""]).some((query) => lowerBrandTerms.some((term) => query.toLowerCase().includes(term)))) {
|
|
759
758
|
brand.push(row);
|
|
760
759
|
brandClicks += clicks;
|
|
761
760
|
brandImpressions += impressions;
|
|
@@ -4076,6 +4075,52 @@ function calculateCtrGapScore(actualCtr, position) {
|
|
|
4076
4075
|
const gap = expectedCtr - actualCtr;
|
|
4077
4076
|
return Math.min(gap / expectedCtr, 1);
|
|
4078
4077
|
}
|
|
4078
|
+
const SORT_DIR = {
|
|
4079
|
+
opportunityScore: "desc",
|
|
4080
|
+
potentialClicks: "desc",
|
|
4081
|
+
impressions: "desc",
|
|
4082
|
+
position: "asc"
|
|
4083
|
+
};
|
|
4084
|
+
function analyzeOpportunity(keywords, options = {}) {
|
|
4085
|
+
const minImpressions = options.minImpressions ?? 100;
|
|
4086
|
+
const positionWeight = options.weights?.position ?? 1;
|
|
4087
|
+
const impressionsWeight = options.weights?.impressions ?? 1;
|
|
4088
|
+
const ctrGapWeight = options.weights?.ctrGap ?? 1;
|
|
4089
|
+
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
4090
|
+
const sortBy = options.sortBy ?? "opportunityScore";
|
|
4091
|
+
const results = [];
|
|
4092
|
+
for (const row of keywords) {
|
|
4093
|
+
const impressions = num$1(row.impressions);
|
|
4094
|
+
if (impressions < minImpressions) continue;
|
|
4095
|
+
const position = num$1(row.position);
|
|
4096
|
+
const ctr = num$1(row.ctr);
|
|
4097
|
+
const clicks = num$1(row.clicks);
|
|
4098
|
+
const positionScore = calculatePositionScore(position);
|
|
4099
|
+
const impressionScore = calculateImpressionScore(impressions);
|
|
4100
|
+
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
4101
|
+
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
4102
|
+
const opportunityScore = Math.round(weightedProduct ** (1 / totalWeight) * 100);
|
|
4103
|
+
const potentialClicks = Math.round(impressions * getExpectedCtr(Math.min(3, position)));
|
|
4104
|
+
results.push({
|
|
4105
|
+
keyword: row.query,
|
|
4106
|
+
page: row.page ?? null,
|
|
4107
|
+
clicks,
|
|
4108
|
+
impressions,
|
|
4109
|
+
ctr,
|
|
4110
|
+
position,
|
|
4111
|
+
opportunityScore,
|
|
4112
|
+
potentialClicks,
|
|
4113
|
+
factors: {
|
|
4114
|
+
positionScore,
|
|
4115
|
+
impressionScore,
|
|
4116
|
+
ctrGapScore
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
const direction = SORT_DIR[sortBy];
|
|
4121
|
+
results.sort((left, right) => direction === "asc" ? left[sortBy] - right[sortBy] : right[sortBy] - left[sortBy]);
|
|
4122
|
+
return options.limit ? results.slice(0, options.limit) : results;
|
|
4123
|
+
}
|
|
4079
4124
|
const opportunityAnalyzer = defineAnalyzer$1({
|
|
4080
4125
|
id: "opportunity",
|
|
4081
4126
|
buildSql(params) {
|
|
@@ -4189,46 +4234,11 @@ const opportunityAnalyzer = defineAnalyzer$1({
|
|
|
4189
4234
|
return { queries: queriesQueryState(periodOf$1(params), params.limit) };
|
|
4190
4235
|
},
|
|
4191
4236
|
reduceRows(rows, params) {
|
|
4192
|
-
const
|
|
4193
|
-
const minImpressions = params.minImpressions ?? 100;
|
|
4194
|
-
const positionWeight = 1;
|
|
4195
|
-
const impressionsWeight = 1;
|
|
4196
|
-
const ctrGapWeight = 1;
|
|
4197
|
-
const sortBy = "opportunityScore";
|
|
4198
|
-
const results = [];
|
|
4199
|
-
for (const row of keywords) {
|
|
4200
|
-
const impressions = num$1(row.impressions);
|
|
4201
|
-
const position = num$1(row.position);
|
|
4202
|
-
const ctr = num$1(row.ctr);
|
|
4203
|
-
const clicks = num$1(row.clicks);
|
|
4204
|
-
if (impressions < minImpressions) continue;
|
|
4205
|
-
const positionScore = calculatePositionScore(position);
|
|
4206
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
4207
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
4208
|
-
const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
|
|
4209
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
4210
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
4211
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
4212
|
-
results.push({
|
|
4213
|
-
keyword: row.query,
|
|
4214
|
-
page: row.page ?? null,
|
|
4215
|
-
clicks,
|
|
4216
|
-
impressions,
|
|
4217
|
-
ctr,
|
|
4218
|
-
position,
|
|
4219
|
-
opportunityScore,
|
|
4220
|
-
potentialClicks,
|
|
4221
|
-
factors: {
|
|
4222
|
-
positionScore,
|
|
4223
|
-
impressionScore,
|
|
4224
|
-
ctrGapScore
|
|
4225
|
-
}
|
|
4226
|
-
});
|
|
4227
|
-
}
|
|
4237
|
+
const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
|
|
4228
4238
|
const paged = paginateSortedInMemory(results, {
|
|
4229
4239
|
limit: params.limit,
|
|
4230
4240
|
offset: params.offset
|
|
4231
|
-
}, (left, right) => right
|
|
4241
|
+
}, (left, right) => right.opportunityScore - left.opportunityScore);
|
|
4232
4242
|
return {
|
|
4233
4243
|
results: paged,
|
|
4234
4244
|
meta: {
|
|
@@ -4904,6 +4914,12 @@ const stlDecomposeAnalyzer = defineAnalyzer$1({
|
|
|
4904
4914
|
}
|
|
4905
4915
|
});
|
|
4906
4916
|
const DEFAULT_ROW_LIMIT$1 = 25e3;
|
|
4917
|
+
function paginateStrikingDistance(results, options) {
|
|
4918
|
+
return paginateSortedInMemory(results, {
|
|
4919
|
+
limit: options.limit ?? 1e3,
|
|
4920
|
+
offset: options.offset
|
|
4921
|
+
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4922
|
+
}
|
|
4907
4923
|
function filterStrikingDistance(rows, options = {}) {
|
|
4908
4924
|
const minPosition = options.minPosition ?? 4;
|
|
4909
4925
|
const maxPosition = options.maxPosition ?? 20;
|
|
@@ -4930,14 +4946,14 @@ function filterStrikingDistance(rows, options = {}) {
|
|
|
4930
4946
|
}
|
|
4931
4947
|
return results;
|
|
4932
4948
|
}
|
|
4949
|
+
function analyzeStrikingDistance(rows, options = {}) {
|
|
4950
|
+
return paginateStrikingDistance(filterStrikingDistance(rows, options), options);
|
|
4951
|
+
}
|
|
4933
4952
|
const strikingDistanceAnalyzer = defineAnalyzer$1({
|
|
4934
4953
|
id: "striking-distance",
|
|
4935
4954
|
reduce(rows, params) {
|
|
4936
4955
|
const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
|
|
4937
|
-
const paged =
|
|
4938
|
-
limit: params.limit ?? 1e3,
|
|
4939
|
-
offset: params.offset
|
|
4940
|
-
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4956
|
+
const paged = paginateStrikingDistance(results, params);
|
|
4941
4957
|
return {
|
|
4942
4958
|
results: paged,
|
|
4943
4959
|
meta: {
|
|
@@ -5313,6 +5329,18 @@ const trendsAnalyzer = defineAnalyzer$1({
|
|
|
5313
5329
|
}
|
|
5314
5330
|
});
|
|
5315
5331
|
const DEFAULT_ROW_LIMIT = 25e3;
|
|
5332
|
+
function analyzeZeroClick(rows, options = {}) {
|
|
5333
|
+
const minImpressions = options.minImpressions ?? 1e3;
|
|
5334
|
+
const maxCtr = options.maxCtr ?? .03;
|
|
5335
|
+
const maxPosition = options.maxPosition ?? 10;
|
|
5336
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
5337
|
+
for (const row of rows) {
|
|
5338
|
+
if (row.impressions < minImpressions || row.position > maxPosition || row.ctr > maxCtr) continue;
|
|
5339
|
+
const existing = queryMap.get(row.query);
|
|
5340
|
+
if (!existing || row.position < existing.position) queryMap.set(row.query, { ...row });
|
|
5341
|
+
}
|
|
5342
|
+
return [...queryMap.values()].sort((left, right) => right.impressions - left.impressions);
|
|
5343
|
+
}
|
|
5316
5344
|
const ALL_ANALYZERS = [
|
|
5317
5345
|
bayesianCtrAnalyzer,
|
|
5318
5346
|
bipartitePagerankAnalyzer,
|
|
@@ -5425,26 +5453,11 @@ const ALL_ANALYZERS = [
|
|
|
5425
5453
|
return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
|
|
5426
5454
|
},
|
|
5427
5455
|
reduceRows(rows, params) {
|
|
5428
|
-
const
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
for (const row of arr) {
|
|
5434
|
-
if (row.impressions < minImpressions) continue;
|
|
5435
|
-
if (row.position > maxPosition) continue;
|
|
5436
|
-
if (row.ctr > maxCtr) continue;
|
|
5437
|
-
const existing = queryMap.get(row.query);
|
|
5438
|
-
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
5439
|
-
query: row.query,
|
|
5440
|
-
page: row.page,
|
|
5441
|
-
clicks: row.clicks,
|
|
5442
|
-
impressions: row.impressions,
|
|
5443
|
-
ctr: row.ctr,
|
|
5444
|
-
position: row.position
|
|
5445
|
-
});
|
|
5446
|
-
}
|
|
5447
|
-
const results = Array.from(queryMap.values());
|
|
5456
|
+
const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
|
|
5457
|
+
minImpressions: params.minImpressions ?? 1e3,
|
|
5458
|
+
maxCtr: params.maxCtr ?? .03,
|
|
5459
|
+
maxPosition: params.maxPosition ?? 10
|
|
5460
|
+
});
|
|
5448
5461
|
const paged = paginateSortedInMemory(results, {
|
|
5449
5462
|
limit: params.limit,
|
|
5450
5463
|
offset: params.offset
|
|
@@ -7066,4 +7079,4 @@ function createInMemoryQuerySource(options) {
|
|
|
7066
7079
|
};
|
|
7067
7080
|
}
|
|
7068
7081
|
const SQL_ANALYZERS = ALL_ANALYZERS.flatMap((d) => d.sql ? [d.sql] : []);
|
|
7069
|
-
export { AnalyzerCapabilityError, DEFAULT_PRIORITY_SOURCES, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, NORMALIZER_VERSION, REPORTS, ROW_ANALYZERS, SEARCH_INTENT_CODE, SQL_ANALYZERS, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, classifyQueryIntent, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, decodeIntent, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf,
|
|
7082
|
+
export { AnalyzerCapabilityError, DEFAULT_PRIORITY_SOURCES, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, NORMALIZER_VERSION, REPORTS, ROW_ANALYZERS, SEARCH_INTENT_CODE, SQL_ANALYZERS, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, decodeIntent, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.40.1",
|
|
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",
|
|
@@ -74,11 +74,10 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"drizzle-orm": "1.0.0-rc.3",
|
|
78
77
|
"pluralize": "^8.0.0",
|
|
79
|
-
"@gscdump/engine": "0.
|
|
80
|
-
"@gscdump/engine-gsc-api": "0.
|
|
81
|
-
"gscdump": "0.
|
|
78
|
+
"@gscdump/engine": "0.40.1",
|
|
79
|
+
"@gscdump/engine-gsc-api": "0.40.1",
|
|
80
|
+
"gscdump": "0.40.1"
|
|
82
81
|
},
|
|
83
82
|
"devDependencies": {
|
|
84
83
|
"vitest": "^4.1.10"
|