@gscdump/analysis 0.38.2 → 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 +154 -92
- package/dist/default-registry.mjs +154 -92
- package/dist/index.d.mts +55 -13
- package/dist/index.mjs +186 -101
- package/package.json +4 -5
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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";
|
|
6
6
|
import { err, ok, unwrapResult } from "gscdump/result";
|
|
7
7
|
import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
|
|
8
8
|
import { MS_PER_DAY, daysAgoUtc, toIsoDate } from "gscdump/dates";
|
|
9
|
-
import { buildExtrasQueries, buildTotalsSql, mergeExtras, pgResolverAdapter, resolveComparisonSQL,
|
|
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;
|
|
@@ -897,7 +896,7 @@ function createMetricSorter(defaultMetric, orderByMetric) {
|
|
|
897
896
|
return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
|
|
898
897
|
};
|
|
899
898
|
}
|
|
900
|
-
const sortRowResults
|
|
899
|
+
const sortRowResults = createSorter((item, metric) => {
|
|
901
900
|
switch (metric) {
|
|
902
901
|
case "clicks": return item.totalClicks;
|
|
903
902
|
case "impressions": return item.totalImpressions;
|
|
@@ -944,7 +943,7 @@ function analyzeCannibalization(rows, options = {}) {
|
|
|
944
943
|
positionSpread
|
|
945
944
|
});
|
|
946
945
|
}
|
|
947
|
-
return sortRowResults
|
|
946
|
+
return sortRowResults(results, sortBy, sortOrder);
|
|
948
947
|
}
|
|
949
948
|
const cannibalizationAnalyzer = defineAnalyzer$1({
|
|
950
949
|
id: "cannibalization",
|
|
@@ -2482,14 +2481,9 @@ function shapeDataQueryRows(rows, params, extras) {
|
|
|
2482
2481
|
function buildDataDetailPlan(params, options) {
|
|
2483
2482
|
const state = requireBuilderState(params.q, "data-detail");
|
|
2484
2483
|
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
2485
|
-
const main =
|
|
2486
|
-
const totals = buildTotalsSql(state, options);
|
|
2484
|
+
const main = resolveToSQLOptimized(state, options);
|
|
2487
2485
|
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
2488
|
-
const extraQueries = [
|
|
2489
|
-
name: "totals",
|
|
2490
|
-
sql: totals.sql,
|
|
2491
|
-
params: totals.params
|
|
2492
|
-
}];
|
|
2486
|
+
const extraQueries = [];
|
|
2493
2487
|
if (prev) {
|
|
2494
2488
|
const previousTotals = buildTotalsSql(prev, options);
|
|
2495
2489
|
extraQueries.push({
|
|
@@ -2507,18 +2501,22 @@ function buildDataDetailPlan(params, options) {
|
|
|
2507
2501
|
}
|
|
2508
2502
|
function shapeDataDetailRows(rows, params, extras) {
|
|
2509
2503
|
const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
2510
|
-
const
|
|
2504
|
+
const first = rows[0];
|
|
2505
|
+
const totals = {
|
|
2506
|
+
clicks: Number(first?.totalClicks ?? 0),
|
|
2507
|
+
impressions: Number(first?.totalImpressions ?? 0),
|
|
2508
|
+
ctr: Number(first?.totalCtr ?? 0),
|
|
2509
|
+
position: Number(first?.totalPosition ?? 0)
|
|
2510
|
+
};
|
|
2511
|
+
const coerced = rows.map((raw) => {
|
|
2512
|
+
const { totalCount: _tc, totalClicks: _tclk, totalImpressions: _timp, totalCtr: _tctr, totalPosition: _tpos, sum_position: _sp, ...rest } = raw;
|
|
2513
|
+
return coerceNumericCols(rest);
|
|
2514
|
+
});
|
|
2511
2515
|
const daily = rangeStart && rangeEnd ? padTimeseries$1(coerced, {
|
|
2512
2516
|
startDate: rangeStart,
|
|
2513
2517
|
endDate: rangeEnd
|
|
2514
2518
|
}) : coerced;
|
|
2515
|
-
const
|
|
2516
|
-
const meta = { totals: {
|
|
2517
|
-
clicks: Number(totalsRow.clicks ?? 0),
|
|
2518
|
-
impressions: Number(totalsRow.impressions ?? 0),
|
|
2519
|
-
ctr: Number(totalsRow.ctr ?? 0),
|
|
2520
|
-
position: Number(totalsRow.position ?? 0)
|
|
2521
|
-
} };
|
|
2519
|
+
const meta = { totals };
|
|
2522
2520
|
if (extras?.prevTotals) {
|
|
2523
2521
|
const previousTotalsRow = extras.prevTotals[0] ?? {};
|
|
2524
2522
|
meta.previousTotals = {
|
|
@@ -2647,10 +2645,12 @@ const SEPARATOR_RE$1 = /[-_/.@#:+]+/g;
|
|
|
2647
2645
|
const WHITESPACE_RE$1 = /\s+/g;
|
|
2648
2646
|
const DIACRITICS_RE$1 = /\p{Diacritic}/gu;
|
|
2649
2647
|
function tokenize(query) {
|
|
2650
|
-
|
|
2648
|
+
let index = 0;
|
|
2649
|
+
while (index < query.length && query.charCodeAt(index) <= 127) index++;
|
|
2650
|
+
const text = (index === query.length ? query : query.normalize("NFKD").replace(DIACRITICS_RE$1, "")).toLowerCase().replace(SEPARATOR_RE$1, " ").replace(WHITESPACE_RE$1, " ").trim();
|
|
2651
2651
|
return {
|
|
2652
2652
|
text,
|
|
2653
|
-
tokens: text.split(" ")
|
|
2653
|
+
tokens: text.length === 0 ? [] : text.split(" ")
|
|
2654
2654
|
};
|
|
2655
2655
|
}
|
|
2656
2656
|
function classifyQueryIntent(query) {
|
|
@@ -2816,15 +2816,27 @@ const NO_STRIP_S = /* @__PURE__ */ new Set([
|
|
|
2816
2816
|
"nervous",
|
|
2817
2817
|
"conscious"
|
|
2818
2818
|
]);
|
|
2819
|
+
const DEPLURALIZED_CACHE_MAX = 4096;
|
|
2820
|
+
const depluralizedCache = /* @__PURE__ */ new Map();
|
|
2819
2821
|
function depluralize(token) {
|
|
2820
2822
|
if (token.length <= 3 || NO_STRIP_S.has(token) || token.endsWith("sis")) return token;
|
|
2821
|
-
|
|
2823
|
+
const last = token.charCodeAt(token.length - 1);
|
|
2824
|
+
if (last < 97 || last > 122) return token;
|
|
2825
|
+
const cached = depluralizedCache.get(token);
|
|
2826
|
+
if (cached !== void 0) return cached;
|
|
2827
|
+
const singular = pluralize.singular(token);
|
|
2828
|
+
if (depluralizedCache.size >= DEPLURALIZED_CACHE_MAX) depluralizedCache.clear();
|
|
2829
|
+
depluralizedCache.set(token, singular);
|
|
2830
|
+
return singular;
|
|
2822
2831
|
}
|
|
2823
2832
|
const SEPARATOR_RE = /[-_/.@#:+]+/g;
|
|
2824
2833
|
const WHITESPACE_RE = /\s+/g;
|
|
2825
2834
|
const DIACRITICS_RE = /\p{Diacritic}/gu;
|
|
2826
2835
|
const NORMALIZER_VERSION = 2;
|
|
2827
2836
|
function foldUnicode(s) {
|
|
2837
|
+
let index = 0;
|
|
2838
|
+
while (index < s.length && s.charCodeAt(index) <= 127) index++;
|
|
2839
|
+
if (index === s.length) return s;
|
|
2828
2840
|
return s.normalize("NFKD").replace(DIACRITICS_RE, "");
|
|
2829
2841
|
}
|
|
2830
2842
|
const DIRECTIONAL_CONNECTORS = /* @__PURE__ */ new Set([
|
|
@@ -2860,9 +2872,15 @@ function isOrderSensitive(tokens) {
|
|
|
2860
2872
|
return false;
|
|
2861
2873
|
}
|
|
2862
2874
|
function normalizeQuery(query) {
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
const
|
|
2875
|
+
const normalized = foldUnicode(query).toLowerCase().replace(SEPARATOR_RE, " ").replace(WHITESPACE_RE, " ").trim();
|
|
2876
|
+
if (normalized.length === 0) return "";
|
|
2877
|
+
const cleaned = normalized.split(" ");
|
|
2878
|
+
const mapped = [];
|
|
2879
|
+
for (const token of cleaned) {
|
|
2880
|
+
const synonym = SYNONYMS[token] ?? token;
|
|
2881
|
+
if (synonym) mapped.push(depluralize(synonym));
|
|
2882
|
+
}
|
|
2883
|
+
const tokens = mapped.length > 0 ? mapped : cleaned.map(depluralize);
|
|
2866
2884
|
return (isOrderSensitive(tokens) ? tokens : tokens.sort()).join(" ");
|
|
2867
2885
|
}
|
|
2868
2886
|
const dataDetailAnalyzer = defineAnalyzer$1({
|
|
@@ -2967,7 +2985,69 @@ function paginateInMemory(rows, input) {
|
|
|
2967
2985
|
const o = clampOffset(input.offset);
|
|
2968
2986
|
return rows.slice(o, o + l);
|
|
2969
2987
|
}
|
|
2970
|
-
|
|
2988
|
+
function paginateSortedInMemory(rows, input, compare) {
|
|
2989
|
+
const limit = clampLimit(input.limit, rows.length);
|
|
2990
|
+
const offset = clampOffset(input.offset);
|
|
2991
|
+
const selectedCount = Math.min(rows.length, offset + limit);
|
|
2992
|
+
if (limit === 0 || offset >= rows.length || selectedCount === 0) return [];
|
|
2993
|
+
const fullSort = () => [...rows].sort(compare).slice(offset, offset + limit);
|
|
2994
|
+
if (selectedCount >= rows.length / 2) return fullSort();
|
|
2995
|
+
let invalidComparison = false;
|
|
2996
|
+
const compareStable = (left, right) => {
|
|
2997
|
+
const order = compare(left.value, right.value);
|
|
2998
|
+
if (Number.isNaN(order)) {
|
|
2999
|
+
invalidComparison = true;
|
|
3000
|
+
return left.index - right.index;
|
|
3001
|
+
}
|
|
3002
|
+
return order || left.index - right.index;
|
|
3003
|
+
};
|
|
3004
|
+
const isWorse = (left, right) => compareStable(left, right) > 0;
|
|
3005
|
+
const heap = [];
|
|
3006
|
+
const siftUp = (start) => {
|
|
3007
|
+
let index = start;
|
|
3008
|
+
while (index > 0) {
|
|
3009
|
+
const parent = index - 1 >>> 1;
|
|
3010
|
+
if (!isWorse(heap[index], heap[parent])) break;
|
|
3011
|
+
const swap = heap[parent];
|
|
3012
|
+
heap[parent] = heap[index];
|
|
3013
|
+
heap[index] = swap;
|
|
3014
|
+
index = parent;
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
const siftDown = () => {
|
|
3018
|
+
let index = 0;
|
|
3019
|
+
for (;;) {
|
|
3020
|
+
const left = index * 2 + 1;
|
|
3021
|
+
if (left >= heap.length) return;
|
|
3022
|
+
const right = left + 1;
|
|
3023
|
+
let worse = left;
|
|
3024
|
+
if (right < heap.length && isWorse(heap[right], heap[left])) worse = right;
|
|
3025
|
+
if (!isWorse(heap[worse], heap[index])) return;
|
|
3026
|
+
const swap = heap[index];
|
|
3027
|
+
heap[index] = heap[worse];
|
|
3028
|
+
heap[worse] = swap;
|
|
3029
|
+
index = worse;
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
for (let index = 0; index < rows.length; index++) {
|
|
3033
|
+
const candidate = {
|
|
3034
|
+
value: rows[index],
|
|
3035
|
+
index
|
|
3036
|
+
};
|
|
3037
|
+
if (heap.length < selectedCount) {
|
|
3038
|
+
heap.push(candidate);
|
|
3039
|
+
siftUp(heap.length - 1);
|
|
3040
|
+
} else if (compareStable(candidate, heap[0]) < 0) {
|
|
3041
|
+
heap[0] = candidate;
|
|
3042
|
+
siftDown();
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
if (invalidComparison) return fullSort();
|
|
3046
|
+
heap.sort(compareStable);
|
|
3047
|
+
if (invalidComparison) return fullSort();
|
|
3048
|
+
return heap.slice(offset, offset + limit).map((entry) => entry.value);
|
|
3049
|
+
}
|
|
3050
|
+
const sortResults = createMetricSorter("lostClicks", {
|
|
2971
3051
|
lostClicks: "desc",
|
|
2972
3052
|
declinePercent: "desc",
|
|
2973
3053
|
currentClicks: "asc"
|
|
@@ -3000,7 +3080,7 @@ function analyzeDecay(input, options = {}) {
|
|
|
3000
3080
|
positionDrop: currentPosition != null ? currentPosition - prev.position : null
|
|
3001
3081
|
});
|
|
3002
3082
|
}
|
|
3003
|
-
return sortResults
|
|
3083
|
+
return sortResults(results, sortBy);
|
|
3004
3084
|
}
|
|
3005
3085
|
const decayAnalyzer = defineAnalyzer$1({
|
|
3006
3086
|
id: "decay",
|
|
@@ -3995,12 +4075,52 @@ function calculateCtrGapScore(actualCtr, position) {
|
|
|
3995
4075
|
const gap = expectedCtr - actualCtr;
|
|
3996
4076
|
return Math.min(gap / expectedCtr, 1);
|
|
3997
4077
|
}
|
|
3998
|
-
const
|
|
4078
|
+
const SORT_DIR = {
|
|
3999
4079
|
opportunityScore: "desc",
|
|
4000
4080
|
potentialClicks: "desc",
|
|
4001
4081
|
impressions: "desc",
|
|
4002
4082
|
position: "asc"
|
|
4003
|
-
}
|
|
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
|
+
}
|
|
4004
4124
|
const opportunityAnalyzer = defineAnalyzer$1({
|
|
4005
4125
|
id: "opportunity",
|
|
4006
4126
|
buildSql(params) {
|
|
@@ -4114,51 +4234,15 @@ const opportunityAnalyzer = defineAnalyzer$1({
|
|
|
4114
4234
|
return { queries: queriesQueryState(periodOf$1(params), params.limit) };
|
|
4115
4235
|
},
|
|
4116
4236
|
reduceRows(rows, params) {
|
|
4117
|
-
const
|
|
4118
|
-
const
|
|
4119
|
-
const positionWeight = 1;
|
|
4120
|
-
const impressionsWeight = 1;
|
|
4121
|
-
const ctrGapWeight = 1;
|
|
4122
|
-
const sortBy = "opportunityScore";
|
|
4123
|
-
const results = [];
|
|
4124
|
-
for (const row of keywords) {
|
|
4125
|
-
const impressions = num$1(row.impressions);
|
|
4126
|
-
const position = num$1(row.position);
|
|
4127
|
-
const ctr = num$1(row.ctr);
|
|
4128
|
-
const clicks = num$1(row.clicks);
|
|
4129
|
-
if (impressions < minImpressions) continue;
|
|
4130
|
-
const positionScore = calculatePositionScore(position);
|
|
4131
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
4132
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
4133
|
-
const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
|
|
4134
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
4135
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
4136
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
4137
|
-
results.push({
|
|
4138
|
-
keyword: row.query,
|
|
4139
|
-
page: row.page ?? null,
|
|
4140
|
-
clicks,
|
|
4141
|
-
impressions,
|
|
4142
|
-
ctr,
|
|
4143
|
-
position,
|
|
4144
|
-
opportunityScore,
|
|
4145
|
-
potentialClicks,
|
|
4146
|
-
factors: {
|
|
4147
|
-
positionScore,
|
|
4148
|
-
impressionScore,
|
|
4149
|
-
ctrGapScore
|
|
4150
|
-
}
|
|
4151
|
-
});
|
|
4152
|
-
}
|
|
4153
|
-
const sorted = sortResults(results, sortBy);
|
|
4154
|
-
const paged = paginateInMemory(sorted, {
|
|
4237
|
+
const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
|
|
4238
|
+
const paged = paginateSortedInMemory(results, {
|
|
4155
4239
|
limit: params.limit,
|
|
4156
4240
|
offset: params.offset
|
|
4157
|
-
});
|
|
4241
|
+
}, (left, right) => right.opportunityScore - left.opportunityScore);
|
|
4158
4242
|
return {
|
|
4159
4243
|
results: paged,
|
|
4160
4244
|
meta: {
|
|
4161
|
-
total:
|
|
4245
|
+
total: results.length,
|
|
4162
4246
|
returned: paged.length
|
|
4163
4247
|
}
|
|
4164
4248
|
};
|
|
@@ -4830,6 +4914,12 @@ const stlDecomposeAnalyzer = defineAnalyzer$1({
|
|
|
4830
4914
|
}
|
|
4831
4915
|
});
|
|
4832
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
|
+
}
|
|
4833
4923
|
function filterStrikingDistance(rows, options = {}) {
|
|
4834
4924
|
const minPosition = options.minPosition ?? 4;
|
|
4835
4925
|
const maxPosition = options.maxPosition ?? 20;
|
|
@@ -4856,15 +4946,14 @@ function filterStrikingDistance(rows, options = {}) {
|
|
|
4856
4946
|
}
|
|
4857
4947
|
return results;
|
|
4858
4948
|
}
|
|
4949
|
+
function analyzeStrikingDistance(rows, options = {}) {
|
|
4950
|
+
return paginateStrikingDistance(filterStrikingDistance(rows, options), options);
|
|
4951
|
+
}
|
|
4859
4952
|
const strikingDistanceAnalyzer = defineAnalyzer$1({
|
|
4860
4953
|
id: "striking-distance",
|
|
4861
4954
|
reduce(rows, params) {
|
|
4862
4955
|
const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
|
|
4863
|
-
|
|
4864
|
-
const paged = paginateInMemory(results, {
|
|
4865
|
-
limit: params.limit ?? 1e3,
|
|
4866
|
-
offset: params.offset
|
|
4867
|
-
});
|
|
4956
|
+
const paged = paginateStrikingDistance(results, params);
|
|
4868
4957
|
return {
|
|
4869
4958
|
results: paged,
|
|
4870
4959
|
meta: {
|
|
@@ -5240,7 +5329,18 @@ const trendsAnalyzer = defineAnalyzer$1({
|
|
|
5240
5329
|
}
|
|
5241
5330
|
});
|
|
5242
5331
|
const DEFAULT_ROW_LIMIT = 25e3;
|
|
5243
|
-
|
|
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
|
+
}
|
|
5244
5344
|
const ALL_ANALYZERS = [
|
|
5245
5345
|
bayesianCtrAnalyzer,
|
|
5246
5346
|
bipartitePagerankAnalyzer,
|
|
@@ -5353,30 +5453,15 @@ const ALL_ANALYZERS = [
|
|
|
5353
5453
|
return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
|
|
5354
5454
|
},
|
|
5355
5455
|
reduceRows(rows, params) {
|
|
5356
|
-
const
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
if (row.impressions < minImpressions) continue;
|
|
5363
|
-
if (row.position > maxPosition) continue;
|
|
5364
|
-
if (row.ctr > maxCtr) continue;
|
|
5365
|
-
const existing = queryMap.get(row.query);
|
|
5366
|
-
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
5367
|
-
query: row.query,
|
|
5368
|
-
page: row.page,
|
|
5369
|
-
clicks: row.clicks,
|
|
5370
|
-
impressions: row.impressions,
|
|
5371
|
-
ctr: row.ctr,
|
|
5372
|
-
position: row.position
|
|
5373
|
-
});
|
|
5374
|
-
}
|
|
5375
|
-
const results = sortRowResults(Array.from(queryMap.values()), "impressions", "desc");
|
|
5376
|
-
const paged = paginateInMemory(results, {
|
|
5456
|
+
const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
|
|
5457
|
+
minImpressions: params.minImpressions ?? 1e3,
|
|
5458
|
+
maxCtr: params.maxCtr ?? .03,
|
|
5459
|
+
maxPosition: params.maxPosition ?? 10
|
|
5460
|
+
});
|
|
5461
|
+
const paged = paginateSortedInMemory(results, {
|
|
5377
5462
|
limit: params.limit,
|
|
5378
5463
|
offset: params.offset
|
|
5379
|
-
});
|
|
5464
|
+
}, (left, right) => right.impressions - left.impressions);
|
|
5380
5465
|
return {
|
|
5381
5466
|
results: paged,
|
|
5382
5467
|
meta: {
|
|
@@ -6994,4 +7079,4 @@ function createInMemoryQuerySource(options) {
|
|
|
6994
7079
|
};
|
|
6995
7080
|
}
|
|
6996
7081
|
const SQL_ANALYZERS = ALL_ANALYZERS.flatMap((d) => d.sql ? [d.sql] : []);
|
|
6997
|
-
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": "0.
|
|
81
|
-
"
|
|
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"
|