@gscdump/analysis 3.8.0 → 4.0.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 +18 -4
- package/dist/analyzer/adapt-rows.mjs +6 -7
- package/dist/analyzer/paginate.mjs +12 -1
- package/dist/analyzers/brand.mjs +46 -29
- package/dist/analyzers/cannibalization.mjs +22 -9
- package/dist/analyzers/clustering.mjs +20 -6
- package/dist/analyzers/concentration.mjs +3 -3
- package/dist/analyzers/decay.mjs +27 -15
- package/dist/analyzers/movers.d.mts +19 -7
- package/dist/analyzers/movers.mjs +226 -149
- package/dist/analyzers/opportunity.mjs +9 -5
- package/dist/analyzers/seasonality.mjs +6 -3
- package/dist/analyzers/striking-distance.mjs +2 -3
- package/dist/analyzers/zero-click.mjs +8 -9
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/report/reports/movers.mjs +27 -10
- package/dist/report/reports/opportunities.mjs +11 -7
- package/dist/report/reports/pre-publish.mjs +3 -2
- package/dist/report/reports/risks.mjs +5 -5
- package/dist/report/runtime.mjs +5 -3
- package/dist/report/sections.mjs +6 -2
- package/package.json +6 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requireComparisonWindow } from "../require.mjs";
|
|
2
|
-
import { reportRows, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
2
|
+
import { reportRows, resultTotal, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
3
3
|
import { defineReport } from "@gscdump/engine/report";
|
|
4
4
|
const DEFAULT_MAX = 5;
|
|
5
5
|
const DEFAULT_MIN_CHANGE = 5;
|
|
@@ -32,15 +32,28 @@ const moversReport = defineReport({
|
|
|
32
32
|
};
|
|
33
33
|
return [
|
|
34
34
|
{
|
|
35
|
-
key: "movers",
|
|
35
|
+
key: "movers-rising",
|
|
36
36
|
type: "movers",
|
|
37
37
|
params: {
|
|
38
38
|
...cur,
|
|
39
39
|
...prev,
|
|
40
|
+
direction: "rising",
|
|
40
41
|
limit: 200
|
|
41
42
|
},
|
|
42
43
|
required: true,
|
|
43
|
-
feeds: ["rising"
|
|
44
|
+
feeds: ["rising"]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "movers-declining",
|
|
48
|
+
type: "movers",
|
|
49
|
+
params: {
|
|
50
|
+
...cur,
|
|
51
|
+
...prev,
|
|
52
|
+
direction: "declining",
|
|
53
|
+
limit: 200
|
|
54
|
+
},
|
|
55
|
+
required: true,
|
|
56
|
+
feeds: ["decliners"]
|
|
44
57
|
},
|
|
45
58
|
{
|
|
46
59
|
key: "decay",
|
|
@@ -67,18 +80,19 @@ const moversReport = defineReport({
|
|
|
67
80
|
const max = ctx.params.maxFindings ?? DEFAULT_MAX;
|
|
68
81
|
const minChange = ctx.params.minClicksChange ?? DEFAULT_MIN_CHANGE;
|
|
69
82
|
const sections = [];
|
|
70
|
-
const
|
|
83
|
+
const risingRes = results["movers-rising"];
|
|
84
|
+
const decliningRes = results["movers-declining"];
|
|
71
85
|
const decayRes = results.decay;
|
|
72
86
|
const strikingRes = results.striking;
|
|
73
|
-
sections.push(buildMoversSection(
|
|
74
|
-
sections.push(buildDeclinersSection(
|
|
87
|
+
sections.push(buildMoversSection(risingRes, "rising", max, minChange));
|
|
88
|
+
sections.push(buildDeclinersSection(decliningRes, decayRes, max, minChange));
|
|
75
89
|
sections.push(buildStrikingSection(strikingRes, max));
|
|
76
90
|
return { sections };
|
|
77
91
|
}
|
|
78
92
|
});
|
|
79
93
|
function buildMoversSection(res, direction, max, minChange) {
|
|
80
94
|
const rows = reportRows(res).filter((r) => r.direction === direction && Math.abs(r.clicksChange) >= minChange).sort((a, b) => Math.abs(b.clicksChange) - Math.abs(a.clicksChange));
|
|
81
|
-
const total = rows.length;
|
|
95
|
+
const total = resultTotal(res, rows.length);
|
|
82
96
|
const kept = rows.slice(0, max);
|
|
83
97
|
const findings = kept.map((r) => ({
|
|
84
98
|
entity: {
|
|
@@ -115,7 +129,8 @@ function buildMoversSection(res, direction, max, minChange) {
|
|
|
115
129
|
};
|
|
116
130
|
}
|
|
117
131
|
function buildDeclinersSection(moversRes, decayRes, max, minChange) {
|
|
118
|
-
const
|
|
132
|
+
const decliningRows = reportRows(moversRes).filter((r) => r.direction === "declining" && Math.abs(r.clicksChange) >= minChange).sort((a, b) => Math.abs(b.clicksChange) - Math.abs(a.clicksChange));
|
|
133
|
+
const decliningQueries = decliningRows.slice(0, max);
|
|
119
134
|
const lostPages = reportRows(decayRes).sort((a, b) => b.lostClicks - a.lostClicks).slice(0, max);
|
|
120
135
|
const findings = [...decliningQueries.map((r) => ({
|
|
121
136
|
entity: {
|
|
@@ -161,12 +176,14 @@ function buildDeclinersSection(moversRes, decayRes, max, minChange) {
|
|
|
161
176
|
magnitudeLabel: `${Math.round(totalLost)} clicks lost`
|
|
162
177
|
},
|
|
163
178
|
findings,
|
|
164
|
-
|
|
179
|
+
truncated: truncation(resultTotal(moversRes, decliningRows.length), decliningQueries.length),
|
|
180
|
+
coverage: sectionCoverage(moversRes) === "full" && sectionCoverage(decayRes) === "full" ? "full" : "partial"
|
|
165
181
|
};
|
|
166
182
|
}
|
|
167
183
|
function buildStrikingSection(res, max) {
|
|
168
184
|
const rows = reportRows(res).sort((a, b) => b.potentialClicks - a.potentialClicks);
|
|
169
185
|
const kept = rows.slice(0, max);
|
|
186
|
+
const total = resultTotal(res, rows.length);
|
|
170
187
|
const findings = kept.map((r) => ({
|
|
171
188
|
entity: {
|
|
172
189
|
kind: "query",
|
|
@@ -186,7 +203,7 @@ function buildStrikingSection(res, max) {
|
|
|
186
203
|
severity: "low",
|
|
187
204
|
summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.potentialClicks, 0)} potential clicks` },
|
|
188
205
|
findings,
|
|
189
|
-
truncated: truncation(
|
|
206
|
+
truncated: truncation(total, kept.length),
|
|
190
207
|
coverage: sectionCoverage(res),
|
|
191
208
|
artifact: sectionArtifact(res, "striking-distance")
|
|
192
209
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reportRows, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
1
|
+
import { reportRows, resultTotal, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
2
2
|
import { defineReport } from "@gscdump/engine/report";
|
|
3
3
|
const DEFAULT_MAX = 5;
|
|
4
4
|
const opportunitiesReport = defineReport({
|
|
@@ -87,7 +87,7 @@ function buildStrikingSection(res, max) {
|
|
|
87
87
|
severity: "low",
|
|
88
88
|
summary: { magnitudeLabel: `${Math.round(totalPotential)} potential clicks` },
|
|
89
89
|
findings,
|
|
90
|
-
truncated: truncation(rows.length, kept.length),
|
|
90
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
91
91
|
coverage: sectionCoverage(res),
|
|
92
92
|
artifact: sectionArtifact(res, "striking-distance")
|
|
93
93
|
};
|
|
@@ -114,11 +114,14 @@ function buildOpportunitySection(res, max) {
|
|
|
114
114
|
},
|
|
115
115
|
why: r.page ? `low CTR on ${r.page}` : "low CTR vs position"
|
|
116
116
|
})),
|
|
117
|
-
truncated: truncation(rows.length, kept.length),
|
|
117
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
118
118
|
coverage: sectionCoverage(res),
|
|
119
119
|
artifact: sectionArtifact(res, "opportunity")
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
+
function zeroClickWhy(r) {
|
|
123
|
+
return `${`${(r.ctr * 100).toFixed(1)}% CTR`} at position ${r.position.toFixed(1)} on ${r.page}`;
|
|
124
|
+
}
|
|
122
125
|
function buildZeroClickSection(res, max) {
|
|
123
126
|
const rows = reportRows(res).sort((a, b) => b.impressions - a.impressions);
|
|
124
127
|
const kept = rows.slice(0, max);
|
|
@@ -129,18 +132,19 @@ function buildZeroClickSection(res, max) {
|
|
|
129
132
|
},
|
|
130
133
|
metrics: {
|
|
131
134
|
impressions: r.impressions,
|
|
135
|
+
clicks: r.clicks,
|
|
132
136
|
position: r.position,
|
|
133
137
|
ctr: r.ctr
|
|
134
138
|
},
|
|
135
|
-
why:
|
|
139
|
+
why: zeroClickWhy(r)
|
|
136
140
|
}));
|
|
137
141
|
return {
|
|
138
142
|
id: "zero-click",
|
|
139
143
|
title: "Zero-click queries",
|
|
140
144
|
severity: "info",
|
|
141
|
-
summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.impressions, 0)} impressions
|
|
145
|
+
summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.impressions, 0)} impressions at low CTR` },
|
|
142
146
|
findings,
|
|
143
|
-
truncated: truncation(rows.length, kept.length),
|
|
147
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
144
148
|
coverage: sectionCoverage(res),
|
|
145
149
|
artifact: sectionArtifact(res, "zero-click")
|
|
146
150
|
};
|
|
@@ -164,7 +168,7 @@ function buildMigrationSection(res, max) {
|
|
|
164
168
|
},
|
|
165
169
|
why: `migrating from ${r.sourcePage}`
|
|
166
170
|
})),
|
|
167
|
-
truncated: truncation(rows.length, kept.length),
|
|
171
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
168
172
|
coverage: sectionCoverage(res),
|
|
169
173
|
artifact: sectionArtifact(res, "query-migration")
|
|
170
174
|
};
|
|
@@ -2,6 +2,7 @@ import { requireReportParam } from "../require.mjs";
|
|
|
2
2
|
import { reportRows, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
3
3
|
import { defineReport } from "@gscdump/engine/report";
|
|
4
4
|
const DEFAULT_MAX = 10;
|
|
5
|
+
const FULL_OUTPUT = 5e4;
|
|
5
6
|
const prePublishReport = defineReport({
|
|
6
7
|
id: "pre-publish",
|
|
7
8
|
description: "Pre-publish guard: cannibalization risk and striking-distance peers for a candidate topic or URL.",
|
|
@@ -30,7 +31,7 @@ const prePublishReport = defineReport({
|
|
|
30
31
|
type: "cannibalization",
|
|
31
32
|
params: {
|
|
32
33
|
...dates,
|
|
33
|
-
limit:
|
|
34
|
+
limit: FULL_OUTPUT
|
|
34
35
|
},
|
|
35
36
|
feeds: ["cannibalization-risk"]
|
|
36
37
|
}, {
|
|
@@ -38,7 +39,7 @@ const prePublishReport = defineReport({
|
|
|
38
39
|
type: "striking-distance",
|
|
39
40
|
params: {
|
|
40
41
|
...dates,
|
|
41
|
-
limit:
|
|
42
|
+
limit: FULL_OUTPUT
|
|
42
43
|
},
|
|
43
44
|
feeds: ["striking-peers"]
|
|
44
45
|
}];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requireComparisonWindow } from "../require.mjs";
|
|
2
|
-
import { reportRows, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
2
|
+
import { reportRows, resultTotal, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
|
|
3
3
|
import { defineReport } from "@gscdump/engine/report";
|
|
4
4
|
const DEFAULT_MAX = 5;
|
|
5
5
|
const risksReport = defineReport({
|
|
@@ -100,7 +100,7 @@ function buildDecaySection(res, max) {
|
|
|
100
100
|
magnitudeLabel: `${Math.round(totalLost)} clicks lost`
|
|
101
101
|
},
|
|
102
102
|
findings,
|
|
103
|
-
truncated: truncation(rows.length, kept.length),
|
|
103
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
104
104
|
coverage: sectionCoverage(res),
|
|
105
105
|
artifact: sectionArtifact(res, "decay")
|
|
106
106
|
};
|
|
@@ -129,7 +129,7 @@ function buildCannibalizationSection(res, max) {
|
|
|
129
129
|
severity: kept.length ? "medium" : "info",
|
|
130
130
|
summary: {},
|
|
131
131
|
findings,
|
|
132
|
-
truncated: truncation(rows.length, kept.length),
|
|
132
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
133
133
|
coverage: sectionCoverage(res),
|
|
134
134
|
artifact: sectionArtifact(res, "cannibalization")
|
|
135
135
|
};
|
|
@@ -155,7 +155,7 @@ function buildDarkTrafficSection(res, max) {
|
|
|
155
155
|
severity: kept.length ? "low" : "info",
|
|
156
156
|
summary: { magnitudeLabel: `${Math.round(totalDark)} unattributed clicks` },
|
|
157
157
|
findings,
|
|
158
|
-
truncated: truncation(rows.length, kept.length),
|
|
158
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
159
159
|
coverage: sectionCoverage(res),
|
|
160
160
|
artifact: sectionArtifact(res, "dark-traffic")
|
|
161
161
|
};
|
|
@@ -181,7 +181,7 @@ function buildDeviceGapSection(res, max) {
|
|
|
181
181
|
},
|
|
182
182
|
why: `desktop vs mobile delta`
|
|
183
183
|
})),
|
|
184
|
-
truncated: truncation(rows.length, kept.length),
|
|
184
|
+
truncated: truncation(resultTotal(res, rows.length), kept.length),
|
|
185
185
|
coverage: sectionCoverage(res),
|
|
186
186
|
artifact: sectionArtifact(res, "device-gap")
|
|
187
187
|
};
|
package/dist/report/runtime.mjs
CHANGED
|
@@ -2,16 +2,18 @@ import { analysisErrorToException, analysisErrors } from "../errors.mjs";
|
|
|
2
2
|
import { runAnalyzerFromSource } from "@gscdump/engine/analyzer";
|
|
3
3
|
import { err, ok, unwrapResult } from "gscdump/result";
|
|
4
4
|
import { computeInputHash } from "@gscdump/engine/report";
|
|
5
|
-
async function executeStep(source, analyzers, step) {
|
|
5
|
+
async function executeStep(source, analyzers, step, fetchBudget) {
|
|
6
6
|
const params = {
|
|
7
7
|
...step.params,
|
|
8
|
+
...fetchBudget != null ? { fetchBudget } : {},
|
|
8
9
|
type: step.type
|
|
9
10
|
};
|
|
10
11
|
return runAnalyzerFromSource(source, params, analyzers).then((result) => ({
|
|
11
12
|
state: {
|
|
12
13
|
key: step.key,
|
|
13
14
|
type: step.type,
|
|
14
|
-
status: "done"
|
|
15
|
+
status: "done",
|
|
16
|
+
...result.meta.coverage ? { coverage: result.meta.coverage } : {}
|
|
15
17
|
},
|
|
16
18
|
result
|
|
17
19
|
})).catch((thrown) => {
|
|
@@ -63,7 +65,7 @@ async function runReportResult(report, opts) {
|
|
|
63
65
|
registryVersion: opts.ctx.registryVersion
|
|
64
66
|
});
|
|
65
67
|
const steps = report.plan(opts.ctx.params, opts.ctx.window);
|
|
66
|
-
const outcomes = await Promise.all(steps.map((s) => executeStep(opts.source, opts.analyzers, s)));
|
|
68
|
+
const outcomes = await Promise.all(steps.map((s) => executeStep(opts.source, opts.analyzers, s, opts.ctx.fetchBudget)));
|
|
67
69
|
const required = new Map(steps.filter((s) => s.required).map((s) => [s.key, s]));
|
|
68
70
|
const errored = outcomes.filter((o) => o.state.status === "error");
|
|
69
71
|
for (const o of errored) if (required.has(o.state.key)) return err(analysisErrors.requiredStepFailed(report.id, o.state.key, o.state.error ?? "unknown error", o.cause));
|
package/dist/report/sections.mjs
CHANGED
|
@@ -2,7 +2,11 @@ function reportRows(result) {
|
|
|
2
2
|
return result?.results ?? [];
|
|
3
3
|
}
|
|
4
4
|
function sectionCoverage(result) {
|
|
5
|
-
return result ? "full" : "partial";
|
|
5
|
+
return result && result.meta.coverage?.kind !== "truncated" ? "full" : "partial";
|
|
6
|
+
}
|
|
7
|
+
function resultTotal(result, fallback) {
|
|
8
|
+
const total = result?.meta.total;
|
|
9
|
+
return typeof total === "number" ? Math.max(total, fallback) : fallback;
|
|
6
10
|
}
|
|
7
11
|
function sectionArtifact(result, analyzer, params = {}) {
|
|
8
12
|
return result ? {
|
|
@@ -19,4 +23,4 @@ function truncation(total, kept) {
|
|
|
19
23
|
total
|
|
20
24
|
} : void 0;
|
|
21
25
|
}
|
|
22
|
-
export { reportRows, sectionArtifact, sectionCoverage, truncation };
|
|
26
|
+
export { reportRows, resultTotal, sectionArtifact, sectionCoverage, truncation };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.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",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"dist"
|
|
59
59
|
],
|
|
60
60
|
"engines": {
|
|
61
|
-
"node": ">=22"
|
|
61
|
+
"node": ">=22.13.0"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@gscdump/engine": "^
|
|
65
|
-
"@gscdump/engine-gsc-api": "^
|
|
66
|
-
"gscdump": "^
|
|
64
|
+
"@gscdump/engine": "^4.0.1",
|
|
65
|
+
"@gscdump/engine-gsc-api": "^4.0.1",
|
|
66
|
+
"gscdump": "^4.0.1",
|
|
67
67
|
"pluralize": "^8.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"vitest": "^5.0.
|
|
70
|
+
"vitest": "^5.0.1"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "obuild",
|