gscdump 0.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/LICENSE +21 -0
- package/dist/index.d.mts +1195 -0
- package/dist/index.mjs +3823 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3823 @@
|
|
|
1
|
+
import { withBase, withHttps, withoutTrailingSlash } from "ufo";
|
|
2
|
+
import _dayjs from "dayjs";
|
|
3
|
+
import timezone from "dayjs/plugin/timezone.js";
|
|
4
|
+
import utc from "dayjs/plugin/utc.js";
|
|
5
|
+
import { ofetch } from "ofetch";
|
|
6
|
+
|
|
7
|
+
//#region src/analysis/types.ts
|
|
8
|
+
/** Coerce nullable number to number, defaulting to 0 */
|
|
9
|
+
function num(value) {
|
|
10
|
+
return value ?? 0;
|
|
11
|
+
}
|
|
12
|
+
/** Create a generic sorter for any metric type */
|
|
13
|
+
function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
|
|
14
|
+
return (items, sortBy = defaultMetric, sortOrder = defaultOrder) => {
|
|
15
|
+
const mult = sortOrder === "desc" ? -1 : 1;
|
|
16
|
+
return [...items].sort((a, b) => (getValue(a, sortBy) - getValue(b, sortBy)) * mult);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/analysis/striking-distance.ts
|
|
22
|
+
const sortResults$4 = createSorter((item, metric) => item[metric], "potentialClicks");
|
|
23
|
+
/**
|
|
24
|
+
* Finds striking distance keywords - high impressions, low CTR, position 4-20.
|
|
25
|
+
* These are "quick wins" that could gain significant traffic with small ranking improvements.
|
|
26
|
+
*/
|
|
27
|
+
function analyzeStrikingDistance(keywords, options = {}) {
|
|
28
|
+
const { minPosition = 4, maxPosition = 20, minImpressions = 100, maxCtr = .05, sortBy = "potentialClicks", sortOrder = "desc" } = options;
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const row of keywords) {
|
|
31
|
+
const position = num(row.position);
|
|
32
|
+
const impressions = num(row.impressions);
|
|
33
|
+
const ctr = num(row.ctr);
|
|
34
|
+
const clicks = num(row.clicks);
|
|
35
|
+
if (position < minPosition || position > maxPosition) continue;
|
|
36
|
+
if (impressions < minImpressions) continue;
|
|
37
|
+
if (ctr > maxCtr) continue;
|
|
38
|
+
const potentialClicks = Math.round(impressions * .15);
|
|
39
|
+
results.push({
|
|
40
|
+
keyword: row.keyword,
|
|
41
|
+
page: row.page ?? null,
|
|
42
|
+
clicks,
|
|
43
|
+
impressions,
|
|
44
|
+
ctr,
|
|
45
|
+
position,
|
|
46
|
+
potentialClicks
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return sortResults$4(results, sortBy, sortOrder);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/analysis/opportunity.ts
|
|
54
|
+
const EXPECTED_CTR_BY_POSITION = {
|
|
55
|
+
1: .3,
|
|
56
|
+
2: .15,
|
|
57
|
+
3: .1,
|
|
58
|
+
4: .07,
|
|
59
|
+
5: .05,
|
|
60
|
+
6: .04,
|
|
61
|
+
7: .03,
|
|
62
|
+
8: .025,
|
|
63
|
+
9: .02,
|
|
64
|
+
10: .015
|
|
65
|
+
};
|
|
66
|
+
function getExpectedCtr(position) {
|
|
67
|
+
return EXPECTED_CTR_BY_POSITION[Math.round(Math.max(1, Math.min(position, 10)))] || .01;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Position score: higher for positions 4-20 (improvable range).
|
|
71
|
+
* Peak opportunity at positions 8-15.
|
|
72
|
+
*/
|
|
73
|
+
function calculatePositionScore(position) {
|
|
74
|
+
if (position <= 3) return .2;
|
|
75
|
+
if (position > 50) return .1;
|
|
76
|
+
const distance = Math.abs(position - 11);
|
|
77
|
+
return Math.max(0, 1 - distance / 15);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Impression score using log scale.
|
|
81
|
+
* Higher impressions = more traffic potential.
|
|
82
|
+
*/
|
|
83
|
+
function calculateImpressionScore(impressions) {
|
|
84
|
+
if (impressions <= 0) return 0;
|
|
85
|
+
return Math.min(Math.log10(impressions) / 5, 1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* CTR gap score: difference between actual and expected CTR.
|
|
89
|
+
* Higher gap = more opportunity for improvement.
|
|
90
|
+
*/
|
|
91
|
+
function calculateCtrGapScore(actualCtr, position) {
|
|
92
|
+
const expectedCtr = getExpectedCtr(position);
|
|
93
|
+
if (actualCtr >= expectedCtr) return 0;
|
|
94
|
+
const gap = expectedCtr - actualCtr;
|
|
95
|
+
return Math.min(gap / expectedCtr, 1);
|
|
96
|
+
}
|
|
97
|
+
const SORT_ORDER$1 = {
|
|
98
|
+
opportunityScore: "desc",
|
|
99
|
+
potentialClicks: "desc",
|
|
100
|
+
impressions: "desc",
|
|
101
|
+
position: "asc"
|
|
102
|
+
};
|
|
103
|
+
const sortResults$3 = createSorter((item, metric) => item[metric], "opportunityScore");
|
|
104
|
+
/**
|
|
105
|
+
* Scores keywords by optimization opportunity.
|
|
106
|
+
* Composite score combining position, impressions, and CTR gap factors.
|
|
107
|
+
*/
|
|
108
|
+
function analyzeOpportunity(keywords, options = {}) {
|
|
109
|
+
const { minImpressions = 100, weights = {}, sortBy = "opportunityScore" } = options;
|
|
110
|
+
const positionWeight = weights.position ?? 1;
|
|
111
|
+
const impressionsWeight = weights.impressions ?? 1;
|
|
112
|
+
const ctrGapWeight = weights.ctrGap ?? 1;
|
|
113
|
+
const results = [];
|
|
114
|
+
for (const row of keywords) {
|
|
115
|
+
const impressions = num(row.impressions);
|
|
116
|
+
const position = num(row.position);
|
|
117
|
+
const ctr = num(row.ctr);
|
|
118
|
+
const clicks = num(row.clicks);
|
|
119
|
+
if (impressions < minImpressions) continue;
|
|
120
|
+
const positionScore = calculatePositionScore(position);
|
|
121
|
+
const impressionScore = calculateImpressionScore(impressions);
|
|
122
|
+
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
123
|
+
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
124
|
+
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
125
|
+
const geometricMean = Math.pow(weightedProduct, 1 / totalWeight);
|
|
126
|
+
const opportunityScore = Math.round(geometricMean * 100);
|
|
127
|
+
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
128
|
+
const potentialClicks = Math.round(impressions * targetCtr);
|
|
129
|
+
results.push({
|
|
130
|
+
keyword: row.keyword,
|
|
131
|
+
page: row.page ?? null,
|
|
132
|
+
clicks,
|
|
133
|
+
impressions,
|
|
134
|
+
ctr,
|
|
135
|
+
position,
|
|
136
|
+
opportunityScore,
|
|
137
|
+
potentialClicks,
|
|
138
|
+
factors: {
|
|
139
|
+
positionScore,
|
|
140
|
+
impressionScore,
|
|
141
|
+
ctrGapScore
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return sortResults$3(results, sortBy, SORT_ORDER$1[sortBy]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/analysis/brand.ts
|
|
150
|
+
/**
|
|
151
|
+
* Segments keywords into brand and non-brand based on provided brand terms.
|
|
152
|
+
* Simple string matching - keyword contains any brand term (case-insensitive).
|
|
153
|
+
*/
|
|
154
|
+
function analyzeBrandSegmentation(keywords, options) {
|
|
155
|
+
const { brandTerms, minImpressions = 10 } = options;
|
|
156
|
+
const lowerBrandTerms = brandTerms.map((t) => t.toLowerCase());
|
|
157
|
+
const brand = [];
|
|
158
|
+
const nonBrand = [];
|
|
159
|
+
for (const row of keywords) {
|
|
160
|
+
if (num(row.impressions) < minImpressions) continue;
|
|
161
|
+
if (lowerBrandTerms.some((term) => row.keyword.toLowerCase().includes(term))) brand.push(row);
|
|
162
|
+
else nonBrand.push(row);
|
|
163
|
+
}
|
|
164
|
+
const brandClicks = brand.reduce((sum, k) => sum + num(k.clicks), 0);
|
|
165
|
+
const nonBrandClicks = nonBrand.reduce((sum, k) => sum + num(k.clicks), 0);
|
|
166
|
+
const totalClicks = brandClicks + nonBrandClicks;
|
|
167
|
+
return {
|
|
168
|
+
brand,
|
|
169
|
+
nonBrand,
|
|
170
|
+
summary: {
|
|
171
|
+
brandClicks,
|
|
172
|
+
nonBrandClicks,
|
|
173
|
+
brandShare: totalClicks > 0 ? brandClicks / totalClicks : 0,
|
|
174
|
+
brandImpressions: brand.reduce((sum, k) => sum + num(k.impressions), 0),
|
|
175
|
+
nonBrandImpressions: nonBrand.reduce((sum, k) => sum + num(k.impressions), 0)
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/analysis/concentration.ts
|
|
182
|
+
/**
|
|
183
|
+
* Traffic concentration analysis - measures distribution across pages/keywords.
|
|
184
|
+
* Pure function operating on page or keyword data.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Calculates Gini coefficient for a list of values.
|
|
188
|
+
* Formula: sum((2*i - n - 1) * x_i) / (n * sum(x_i))
|
|
189
|
+
*/
|
|
190
|
+
function calculateGini(values) {
|
|
191
|
+
if (values.length === 0) return 0;
|
|
192
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
193
|
+
const n = sorted.length;
|
|
194
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
195
|
+
if (sum === 0) return 0;
|
|
196
|
+
let weightedSum = 0;
|
|
197
|
+
for (let i = 0; i < n; i++) weightedSum += (2 * (i + 1) - n - 1) * sorted[i];
|
|
198
|
+
return weightedSum / (n * sum);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Calculates Herfindahl-Hirschman Index (HHI).
|
|
202
|
+
* Formula: sum((share_i * 100)^2)
|
|
203
|
+
* Range: 0-10000 (0 = perfect competition, 10000 = monopoly)
|
|
204
|
+
*/
|
|
205
|
+
function calculateHHI(shares) {
|
|
206
|
+
return shares.reduce((sum, share) => sum + (share * 100) ** 2, 0);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Analyzes traffic concentration across items (pages or keywords).
|
|
210
|
+
* Returns Gini coefficient, HHI, and top-N concentration metrics.
|
|
211
|
+
*
|
|
212
|
+
* @param items Array of items with key and clicks
|
|
213
|
+
* @param options Configuration options
|
|
214
|
+
*/
|
|
215
|
+
function analyzeConcentration(items, options = {}) {
|
|
216
|
+
const { topN = 10 } = options;
|
|
217
|
+
if (items.length === 0) return {
|
|
218
|
+
giniCoefficient: 0,
|
|
219
|
+
hhi: 0,
|
|
220
|
+
topNConcentration: 0,
|
|
221
|
+
topNItems: [],
|
|
222
|
+
totalItems: 0,
|
|
223
|
+
totalClicks: 0,
|
|
224
|
+
riskLevel: "low"
|
|
225
|
+
};
|
|
226
|
+
const sorted = [...items].sort((a, b) => b.clicks - a.clicks);
|
|
227
|
+
const totalClicks = sorted.reduce((sum, item) => sum + item.clicks, 0);
|
|
228
|
+
const clickValues = sorted.map((i) => i.clicks);
|
|
229
|
+
const shares = totalClicks > 0 ? sorted.map((i) => i.clicks / totalClicks) : [];
|
|
230
|
+
const giniCoefficient = calculateGini(clickValues);
|
|
231
|
+
const hhi = calculateHHI(shares);
|
|
232
|
+
const topNItems = sorted.slice(0, topN).map((item) => ({
|
|
233
|
+
key: item.key,
|
|
234
|
+
clicks: item.clicks,
|
|
235
|
+
share: totalClicks > 0 ? item.clicks / totalClicks : 0
|
|
236
|
+
}));
|
|
237
|
+
const topNClicks = topNItems.reduce((sum, item) => sum + item.clicks, 0);
|
|
238
|
+
const topNConcentration = totalClicks > 0 ? topNClicks / totalClicks : 0;
|
|
239
|
+
let riskLevel = "low";
|
|
240
|
+
if (hhi > 2500) riskLevel = "high";
|
|
241
|
+
else if (hhi > 1500) riskLevel = "medium";
|
|
242
|
+
return {
|
|
243
|
+
giniCoefficient,
|
|
244
|
+
hhi,
|
|
245
|
+
topNConcentration,
|
|
246
|
+
topNItems,
|
|
247
|
+
totalItems: items.length,
|
|
248
|
+
totalClicks,
|
|
249
|
+
riskLevel
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convenience wrapper for page concentration analysis.
|
|
254
|
+
*/
|
|
255
|
+
function analyzePageConcentration(pages, options) {
|
|
256
|
+
return analyzeConcentration(pages.map((p) => ({
|
|
257
|
+
key: p.page,
|
|
258
|
+
clicks: num(p.clicks)
|
|
259
|
+
})), options);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Convenience wrapper for keyword concentration analysis.
|
|
263
|
+
*/
|
|
264
|
+
function analyzeKeywordConcentration(keywords, options) {
|
|
265
|
+
return analyzeConcentration(keywords.map((k) => ({
|
|
266
|
+
key: k.keyword,
|
|
267
|
+
clicks: num(k.clicks)
|
|
268
|
+
})), options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/analysis/decay.ts
|
|
273
|
+
const SORT_ORDER = {
|
|
274
|
+
lostClicks: "desc",
|
|
275
|
+
declinePercent: "desc",
|
|
276
|
+
currentClicks: "asc"
|
|
277
|
+
};
|
|
278
|
+
const sortResults$2 = createSorter((item, metric) => item[metric], "lostClicks");
|
|
279
|
+
/**
|
|
280
|
+
* Identifies "decaying" content - pages that have lost significant traffic.
|
|
281
|
+
* Useful for finding old content that needs updating.
|
|
282
|
+
*
|
|
283
|
+
* @param input Current and previous period page data
|
|
284
|
+
* @param options Filtering and sorting options
|
|
285
|
+
*/
|
|
286
|
+
function analyzeDecay(input, options = {}) {
|
|
287
|
+
const { minPreviousClicks = 50, threshold = .2, sortBy = "lostClicks" } = options;
|
|
288
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const row of input.current) currentMap.set(row.page, {
|
|
290
|
+
clicks: num(row.clicks),
|
|
291
|
+
position: num(row.position)
|
|
292
|
+
});
|
|
293
|
+
const previousMap = /* @__PURE__ */ new Map();
|
|
294
|
+
for (const row of input.previous) {
|
|
295
|
+
const clicks = num(row.clicks);
|
|
296
|
+
if (clicks >= minPreviousClicks) previousMap.set(row.page, {
|
|
297
|
+
clicks,
|
|
298
|
+
position: num(row.position)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const results = [];
|
|
302
|
+
for (const [page$1, prev] of previousMap) {
|
|
303
|
+
const curr = currentMap.get(page$1) || {
|
|
304
|
+
clicks: 0,
|
|
305
|
+
position: 0
|
|
306
|
+
};
|
|
307
|
+
const lostClicks = prev.clicks - curr.clicks;
|
|
308
|
+
const declinePercent = prev.clicks > 0 ? lostClicks / prev.clicks : 0;
|
|
309
|
+
if (declinePercent >= threshold && lostClicks > 0) results.push({
|
|
310
|
+
page: page$1,
|
|
311
|
+
currentClicks: curr.clicks,
|
|
312
|
+
previousClicks: prev.clicks,
|
|
313
|
+
lostClicks,
|
|
314
|
+
declinePercent,
|
|
315
|
+
currentPosition: curr.position,
|
|
316
|
+
previousPosition: prev.position,
|
|
317
|
+
positionDrop: curr.position - prev.position
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return sortResults$2(results, sortBy, SORT_ORDER[sortBy]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/analysis/movers.ts
|
|
325
|
+
function percentDifference$1(current, previous) {
|
|
326
|
+
if (previous === 0) return current > 0 ? 100 : 0;
|
|
327
|
+
return (current - previous) / previous * 100;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Identifies movers and shakers - keywords with significant recent changes.
|
|
331
|
+
* Compares recent period against baseline period.
|
|
332
|
+
*
|
|
333
|
+
* @param input Current and previous keyword data
|
|
334
|
+
* @param options Filtering and sorting options
|
|
335
|
+
*/
|
|
336
|
+
function analyzeMovers(input, options = {}) {
|
|
337
|
+
const { changeThreshold = .2, minImpressions = 50, sortBy = "clicksChange" } = options;
|
|
338
|
+
const normFactor = input.normalizationFactor ?? 1;
|
|
339
|
+
const baselineMap = /* @__PURE__ */ new Map();
|
|
340
|
+
for (const row of input.previous) baselineMap.set(row.keyword, {
|
|
341
|
+
clicks: num(row.clicks) / normFactor,
|
|
342
|
+
impressions: num(row.impressions) / normFactor,
|
|
343
|
+
position: num(row.position),
|
|
344
|
+
page: row.page ?? null
|
|
345
|
+
});
|
|
346
|
+
const pageMap = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const row of input.current) if (!pageMap.has(row.keyword) && row.page) pageMap.set(row.keyword, row.page);
|
|
348
|
+
for (const row of input.previous) if (!pageMap.has(row.keyword) && row.page) pageMap.set(row.keyword, row.page);
|
|
349
|
+
const rising = [];
|
|
350
|
+
const declining = [];
|
|
351
|
+
const stable = [];
|
|
352
|
+
for (const row of input.current) {
|
|
353
|
+
const impressions = num(row.impressions);
|
|
354
|
+
const clicks = num(row.clicks);
|
|
355
|
+
const position = num(row.position);
|
|
356
|
+
if (impressions < minImpressions) continue;
|
|
357
|
+
const baseline = baselineMap.get(row.keyword) || {
|
|
358
|
+
clicks: 0,
|
|
359
|
+
impressions: 0,
|
|
360
|
+
position: 0,
|
|
361
|
+
page: null
|
|
362
|
+
};
|
|
363
|
+
const clicksChangePercent = percentDifference$1(clicks, baseline.clicks);
|
|
364
|
+
const impressionsChangePercent = percentDifference$1(impressions, baseline.impressions);
|
|
365
|
+
const data = {
|
|
366
|
+
keyword: row.keyword,
|
|
367
|
+
page: pageMap.get(row.keyword) ?? null,
|
|
368
|
+
recentClicks: clicks,
|
|
369
|
+
recentImpressions: impressions,
|
|
370
|
+
recentPosition: position,
|
|
371
|
+
baselineClicks: Math.round(baseline.clicks),
|
|
372
|
+
baselineImpressions: Math.round(baseline.impressions),
|
|
373
|
+
baselinePosition: baseline.position,
|
|
374
|
+
clicksChange: clicks - Math.round(baseline.clicks),
|
|
375
|
+
clicksChangePercent,
|
|
376
|
+
impressionsChangePercent,
|
|
377
|
+
positionChange: position - baseline.position
|
|
378
|
+
};
|
|
379
|
+
const absChange = Math.abs(clicksChangePercent / 100);
|
|
380
|
+
if (clicksChangePercent > 0 && absChange >= changeThreshold) rising.push(data);
|
|
381
|
+
else if (clicksChangePercent < 0 && absChange >= changeThreshold) declining.push(data);
|
|
382
|
+
else stable.push(data);
|
|
383
|
+
}
|
|
384
|
+
const sortFn = (a, b) => {
|
|
385
|
+
switch (sortBy) {
|
|
386
|
+
case "clicks": return b.recentClicks - a.recentClicks;
|
|
387
|
+
case "impressions": return b.recentImpressions - a.recentImpressions;
|
|
388
|
+
case "clicksChange": return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
|
|
389
|
+
case "impressionsChange": return Math.abs(b.impressionsChangePercent) - Math.abs(a.impressionsChangePercent);
|
|
390
|
+
case "positionChange": return Math.abs(b.positionChange) - Math.abs(a.positionChange);
|
|
391
|
+
default: return Math.abs(b.clicksChangePercent) - Math.abs(a.clicksChangePercent);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
rising.sort(sortFn);
|
|
395
|
+
declining.sort(sortFn);
|
|
396
|
+
stable.sort((a, b) => b.recentClicks - a.recentClicks);
|
|
397
|
+
return {
|
|
398
|
+
rising,
|
|
399
|
+
declining,
|
|
400
|
+
stable
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/analysis/cannibalization.ts
|
|
406
|
+
const sortResults$1 = createSorter((item, metric) => {
|
|
407
|
+
switch (metric) {
|
|
408
|
+
case "clicks": return item.totalClicks;
|
|
409
|
+
case "impressions": return item.totalImpressions;
|
|
410
|
+
case "positionSpread": return item.positionSpread;
|
|
411
|
+
case "pageCount": return item.pages.length;
|
|
412
|
+
}
|
|
413
|
+
}, "clicks");
|
|
414
|
+
/**
|
|
415
|
+
* Detects keyword cannibalization - queries ranking for multiple pages.
|
|
416
|
+
* Returns queries where multiple pages compete for the same search term.
|
|
417
|
+
*
|
|
418
|
+
* @param rows Query+page data rows
|
|
419
|
+
* @param options Filtering and sorting options
|
|
420
|
+
*/
|
|
421
|
+
function analyzeCannibalization(rows, options = {}) {
|
|
422
|
+
const { minImpressions = 10, maxPositionSpread = 10, minPages = 2, sortBy = "clicks", sortOrder = "desc" } = options;
|
|
423
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const row of rows) {
|
|
425
|
+
if (row.impressions < minImpressions) continue;
|
|
426
|
+
const pages = queryMap.get(row.query) || [];
|
|
427
|
+
pages.push({
|
|
428
|
+
page: row.page,
|
|
429
|
+
clicks: row.clicks,
|
|
430
|
+
impressions: row.impressions,
|
|
431
|
+
ctr: row.ctr,
|
|
432
|
+
position: row.position
|
|
433
|
+
});
|
|
434
|
+
queryMap.set(row.query, pages);
|
|
435
|
+
}
|
|
436
|
+
const results = [];
|
|
437
|
+
for (const [query$1, pages] of queryMap) {
|
|
438
|
+
if (pages.length < minPages) continue;
|
|
439
|
+
pages.sort((a, b) => b.clicks - a.clicks);
|
|
440
|
+
const positions = pages.map((p) => p.position);
|
|
441
|
+
const positionSpread = Math.max(...positions) - Math.min(...positions);
|
|
442
|
+
if (positionSpread > maxPositionSpread) continue;
|
|
443
|
+
results.push({
|
|
444
|
+
query: query$1,
|
|
445
|
+
pages,
|
|
446
|
+
totalClicks: pages.reduce((sum, p) => sum + p.clicks, 0),
|
|
447
|
+
totalImpressions: pages.reduce((sum, p) => sum + p.impressions, 0),
|
|
448
|
+
positionSpread
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return sortResults$1(results, sortBy, sortOrder);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/analysis/zero-click.ts
|
|
456
|
+
const sortResults = createSorter((item) => item.impressions, "impressions");
|
|
457
|
+
/**
|
|
458
|
+
* Identifies potential "Zero-Click" queries.
|
|
459
|
+
* These are high-volume queries where you rank well but get few clicks,
|
|
460
|
+
* often due to SERP features (Answer Boxes, Knowledge Panels, AI Overviews).
|
|
461
|
+
*
|
|
462
|
+
* @param rows Query+page data rows
|
|
463
|
+
* @param options Filtering options
|
|
464
|
+
*/
|
|
465
|
+
function analyzeZeroClick(rows, options = {}) {
|
|
466
|
+
const { minImpressions = 1e3, maxCtr = .03, maxPosition = 10 } = options;
|
|
467
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
468
|
+
for (const row of rows) {
|
|
469
|
+
if (row.impressions < minImpressions) continue;
|
|
470
|
+
if (row.position > maxPosition) continue;
|
|
471
|
+
if (row.ctr > maxCtr) continue;
|
|
472
|
+
const existing = queryMap.get(row.query);
|
|
473
|
+
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
474
|
+
query: row.query,
|
|
475
|
+
page: row.page,
|
|
476
|
+
clicks: row.clicks,
|
|
477
|
+
impressions: row.impressions,
|
|
478
|
+
ctr: row.ctr,
|
|
479
|
+
position: row.position
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return sortResults(Array.from(queryMap.values()), "impressions", "desc");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/analysis/seasonality.ts
|
|
487
|
+
/**
|
|
488
|
+
* Calculates coefficient of variation (CV) as a seasonality strength measure.
|
|
489
|
+
* CV = std dev / mean, capped at 1 for the result.
|
|
490
|
+
*/
|
|
491
|
+
function calculateCV(values) {
|
|
492
|
+
if (values.length === 0) return 0;
|
|
493
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
494
|
+
if (mean === 0) return 0;
|
|
495
|
+
const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
|
|
496
|
+
const stdDev = Math.sqrt(variance);
|
|
497
|
+
return Math.min(stdDev / mean, 1);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Detects seasonality patterns by analyzing monthly traffic variation.
|
|
501
|
+
* Identifies peaks (>1.5x average) and troughs (<0.5x average).
|
|
502
|
+
*
|
|
503
|
+
* @param dates Array of date metrics (each row is one day)
|
|
504
|
+
* @param options Analysis options
|
|
505
|
+
*/
|
|
506
|
+
function analyzeSeasonality(dates, options = {}) {
|
|
507
|
+
const { metric = "clicks" } = options;
|
|
508
|
+
if (dates.length === 0) return {
|
|
509
|
+
hasSeasonality: false,
|
|
510
|
+
strength: 0,
|
|
511
|
+
peakMonths: [],
|
|
512
|
+
troughMonths: [],
|
|
513
|
+
monthlyBreakdown: [],
|
|
514
|
+
insufficientData: true
|
|
515
|
+
};
|
|
516
|
+
const monthlyMap = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const row of dates) {
|
|
518
|
+
const month = row.date.substring(0, 7);
|
|
519
|
+
const value = metric === "clicks" ? row.clicks : row.impressions;
|
|
520
|
+
monthlyMap.set(month, (monthlyMap.get(month) || 0) + value);
|
|
521
|
+
}
|
|
522
|
+
const months = Array.from(monthlyMap.keys()).sort();
|
|
523
|
+
const values = months.map((m) => monthlyMap.get(m) || 0);
|
|
524
|
+
const insufficientData = months.length < 12;
|
|
525
|
+
const totalValue = values.reduce((a, b) => a + b, 0);
|
|
526
|
+
const avgValue = values.length > 0 ? totalValue / values.length : 0;
|
|
527
|
+
const monthlyBreakdown = months.map((month, i) => {
|
|
528
|
+
const value = values[i];
|
|
529
|
+
const vsAverage = avgValue > 0 ? value / avgValue : 0;
|
|
530
|
+
return {
|
|
531
|
+
month,
|
|
532
|
+
value,
|
|
533
|
+
vsAverage,
|
|
534
|
+
isPeak: vsAverage > 1.5,
|
|
535
|
+
isTrough: vsAverage < .5
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
const peakMonths = [...new Set(monthlyBreakdown.filter((m) => m.isPeak).map((m) => m.month.substring(5, 7)))];
|
|
539
|
+
const troughMonths = [...new Set(monthlyBreakdown.filter((m) => m.isTrough).map((m) => m.month.substring(5, 7)))];
|
|
540
|
+
const strength = calculateCV(values);
|
|
541
|
+
return {
|
|
542
|
+
hasSeasonality: peakMonths.length > 0 || troughMonths.length > 0 || strength > .3,
|
|
543
|
+
strength,
|
|
544
|
+
peakMonths,
|
|
545
|
+
troughMonths,
|
|
546
|
+
monthlyBreakdown,
|
|
547
|
+
insufficientData
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/analysis/clustering.ts
|
|
553
|
+
const INTENT_PREFIXES = [
|
|
554
|
+
"how to",
|
|
555
|
+
"what is",
|
|
556
|
+
"what are",
|
|
557
|
+
"why is",
|
|
558
|
+
"why do",
|
|
559
|
+
"where to",
|
|
560
|
+
"when to",
|
|
561
|
+
"best",
|
|
562
|
+
"top",
|
|
563
|
+
"vs",
|
|
564
|
+
"versus",
|
|
565
|
+
"compare",
|
|
566
|
+
"review",
|
|
567
|
+
"buy",
|
|
568
|
+
"cheap",
|
|
569
|
+
"free",
|
|
570
|
+
"near me"
|
|
571
|
+
];
|
|
572
|
+
function extractIntentPrefix(keyword) {
|
|
573
|
+
const lower = keyword.toLowerCase();
|
|
574
|
+
for (const prefix of INTENT_PREFIXES) if (lower.startsWith(prefix + " ") || lower.startsWith(prefix)) return prefix;
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
function extractWordPrefix(keyword, wordCount = 2) {
|
|
578
|
+
const words = keyword.toLowerCase().split(/\s+/).filter(Boolean);
|
|
579
|
+
if (words.length < wordCount + 1) return null;
|
|
580
|
+
return words.slice(0, wordCount).join(" ");
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Clusters keywords by intent prefix or common word prefix.
|
|
584
|
+
* Simple regex/prefix approach - no external NLP dependencies.
|
|
585
|
+
*
|
|
586
|
+
* @param keywords Array of keyword data
|
|
587
|
+
* @param options Clustering options
|
|
588
|
+
*/
|
|
589
|
+
function analyzeClustering(keywords, options = {}) {
|
|
590
|
+
const { minClusterSize = 2, minImpressions = 10, clusterBy = "both" } = options;
|
|
591
|
+
const filtered = keywords.filter((k) => num(k.impressions) >= minImpressions);
|
|
592
|
+
const clusterMap = /* @__PURE__ */ new Map();
|
|
593
|
+
const clusteredKeywords = /* @__PURE__ */ new Set();
|
|
594
|
+
if (clusterBy === "intent" || clusterBy === "both") for (const kw of filtered) {
|
|
595
|
+
const intent = extractIntentPrefix(kw.keyword);
|
|
596
|
+
if (intent) {
|
|
597
|
+
const existing = clusterMap.get(intent);
|
|
598
|
+
if (existing) existing.keywords.push(kw);
|
|
599
|
+
else clusterMap.set(intent, {
|
|
600
|
+
type: "intent",
|
|
601
|
+
keywords: [kw]
|
|
602
|
+
});
|
|
603
|
+
clusteredKeywords.add(kw.keyword);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (clusterBy === "prefix" || clusterBy === "both") {
|
|
607
|
+
const unclustered = filtered.filter((kw) => !clusteredKeywords.has(kw.keyword));
|
|
608
|
+
const prefixMap = /* @__PURE__ */ new Map();
|
|
609
|
+
for (const kw of unclustered) {
|
|
610
|
+
const prefix = extractWordPrefix(kw.keyword);
|
|
611
|
+
if (prefix) {
|
|
612
|
+
const existing = prefixMap.get(prefix);
|
|
613
|
+
if (existing) existing.push(kw);
|
|
614
|
+
else prefixMap.set(prefix, [kw]);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const [prefix, kws] of prefixMap) if (kws.length >= minClusterSize) {
|
|
618
|
+
clusterMap.set(prefix, {
|
|
619
|
+
type: "prefix",
|
|
620
|
+
keywords: kws
|
|
621
|
+
});
|
|
622
|
+
kws.forEach((kw) => clusteredKeywords.add(kw.keyword));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const clusters = [];
|
|
626
|
+
for (const [name, data] of clusterMap) {
|
|
627
|
+
if (data.keywords.length < minClusterSize) continue;
|
|
628
|
+
const totalClicks = data.keywords.reduce((sum, k) => sum + num(k.clicks), 0);
|
|
629
|
+
const totalImpressions = data.keywords.reduce((sum, k) => sum + num(k.impressions), 0);
|
|
630
|
+
const avgPosition = data.keywords.reduce((sum, k) => sum + num(k.position), 0) / data.keywords.length;
|
|
631
|
+
clusters.push({
|
|
632
|
+
clusterName: name,
|
|
633
|
+
clusterType: data.type,
|
|
634
|
+
keywords: data.keywords,
|
|
635
|
+
totalClicks,
|
|
636
|
+
totalImpressions,
|
|
637
|
+
avgPosition,
|
|
638
|
+
keywordCount: data.keywords.length
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
clusters.sort((a, b) => b.totalClicks - a.totalClicks);
|
|
642
|
+
return {
|
|
643
|
+
clusters,
|
|
644
|
+
unclustered: filtered.filter((kw) => !clusteredKeywords.has(kw.keyword))
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/api/indexing.ts
|
|
650
|
+
/**
|
|
651
|
+
* Request Google to index or remove a URL via the Indexing API.
|
|
652
|
+
* Note: The Indexing API officially supports only job posting and livestream content,
|
|
653
|
+
* but can be used for any URL with varying success.
|
|
654
|
+
*/
|
|
655
|
+
async function requestIndexing(client, url, options = {}) {
|
|
656
|
+
const { type = "URL_UPDATED" } = options;
|
|
657
|
+
return client.indexing.publish(url, type).then((r) => ({
|
|
658
|
+
url,
|
|
659
|
+
type,
|
|
660
|
+
notifyTime: r.urlNotificationMetadata?.latestUpdate?.notifyTime || void 0
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get the indexing notification metadata for a URL.
|
|
665
|
+
* Returns when Google was last notified about updates/removals.
|
|
666
|
+
*/
|
|
667
|
+
async function getIndexingMetadata(client, url) {
|
|
668
|
+
return client.indexing.getMetadata(url).then((r) => ({
|
|
669
|
+
url,
|
|
670
|
+
latestUpdate: r.latestUpdate || void 0,
|
|
671
|
+
latestRemove: r.latestRemove || void 0
|
|
672
|
+
}));
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Batch request indexing for multiple URLs with rate limiting.
|
|
676
|
+
* Returns results for each URL.
|
|
677
|
+
*/
|
|
678
|
+
async function batchRequestIndexing(client, urls, options = {}) {
|
|
679
|
+
const { type = "URL_UPDATED", delayMs = 100, onProgress } = options;
|
|
680
|
+
const results = [];
|
|
681
|
+
for (let i = 0; i < urls.length; i++) {
|
|
682
|
+
const result = await requestIndexing(client, urls[i], { type });
|
|
683
|
+
results.push(result);
|
|
684
|
+
onProgress?.(result, i, urls.length);
|
|
685
|
+
if (i < urls.length - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
686
|
+
}
|
|
687
|
+
return results;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/api/inspection.ts
|
|
692
|
+
/**
|
|
693
|
+
* Inspects a URL in Google Search Console to check its indexing status.
|
|
694
|
+
*/
|
|
695
|
+
async function inspectUrl(client, siteUrl, inspectionUrl) {
|
|
696
|
+
const inspection = (await client.urlInspection.inspect(siteUrl, inspectionUrl)).inspectionResult;
|
|
697
|
+
return {
|
|
698
|
+
inspection,
|
|
699
|
+
isIndexed: inspection?.indexStatusResult?.verdict === "PASS"
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Batch inspect multiple URLs with rate limiting.
|
|
704
|
+
* Returns inspection results for each URL.
|
|
705
|
+
*/
|
|
706
|
+
async function batchInspectUrls(client, siteUrl, urls, options = {}) {
|
|
707
|
+
const { delayMs = 200, onProgress } = options;
|
|
708
|
+
const results = [];
|
|
709
|
+
for (let i = 0; i < urls.length; i++) {
|
|
710
|
+
const url = urls[i];
|
|
711
|
+
const { inspection, isIndexed } = await inspectUrl(client, siteUrl, url);
|
|
712
|
+
const result = {
|
|
713
|
+
url,
|
|
714
|
+
inspection,
|
|
715
|
+
isIndexed
|
|
716
|
+
};
|
|
717
|
+
results.push(result);
|
|
718
|
+
onProgress?.(result, i, urls.length);
|
|
719
|
+
if (i < urls.length - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
720
|
+
}
|
|
721
|
+
return results;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/api/sites.ts
|
|
726
|
+
/**
|
|
727
|
+
* Fetches all sites the authenticated user has access to in Google Search Console.
|
|
728
|
+
*/
|
|
729
|
+
async function fetchSites(client) {
|
|
730
|
+
return client.sites.list().then((res) => res?.siteEntry || []);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Fetches sitemaps for a site.
|
|
734
|
+
*/
|
|
735
|
+
async function fetchSitemaps(client, siteUrl) {
|
|
736
|
+
return client.sitemaps.list(siteUrl).then((res) => res.sitemap || []);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Gets details for a specific sitemap.
|
|
740
|
+
*/
|
|
741
|
+
async function fetchSitemap(client, siteUrl, feedpath) {
|
|
742
|
+
return client.sitemaps.get(siteUrl, feedpath);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Submits a sitemap to Google Search Console.
|
|
746
|
+
*/
|
|
747
|
+
async function submitSitemap(client, siteUrl, feedpath) {
|
|
748
|
+
return client.sitemaps.submit(siteUrl, feedpath);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Deletes a sitemap from Google Search Console.
|
|
752
|
+
*/
|
|
753
|
+
async function deleteSitemap(client, siteUrl, feedpath) {
|
|
754
|
+
return client.sitemaps.delete(siteUrl, feedpath);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Fetches all verified sites with their sitemaps from Google Search Console.
|
|
758
|
+
*/
|
|
759
|
+
async function fetchSitesWithSitemaps(client) {
|
|
760
|
+
const sites = (await client.sites.list().then((res) => res.siteEntry || [])).filter((s) => !!s.siteUrl && s.permissionLevel !== "siteUnverifiedUser");
|
|
761
|
+
return Promise.all(sites.map(async (site) => {
|
|
762
|
+
const sitemaps = site.permissionLevel === "siteOwner" ? await client.sitemaps.list(site.siteUrl).then((res) => res.sitemap || []) : [];
|
|
763
|
+
return {
|
|
764
|
+
...site,
|
|
765
|
+
sitemaps
|
|
766
|
+
};
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/utils/dayjs.ts
|
|
772
|
+
_dayjs.extend(utc);
|
|
773
|
+
_dayjs.extend(timezone);
|
|
774
|
+
function dayjs(date$1) {
|
|
775
|
+
return _dayjs(date$1);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/utils/format.ts
|
|
780
|
+
/**
|
|
781
|
+
* Formats a date for GSC API queries (YYYY-MM-DD format).
|
|
782
|
+
* @param d - Date object, date string, or null/undefined
|
|
783
|
+
* @returns Formatted date string or null/undefined if input is falsy
|
|
784
|
+
*/
|
|
785
|
+
function formatDateGsc(d) {
|
|
786
|
+
if (!d) return d;
|
|
787
|
+
if (typeof d === "string") return d;
|
|
788
|
+
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Calculates the percentage difference between two values.
|
|
792
|
+
* Returns 0 if either value is null, undefined, or 0 (no meaningful comparison).
|
|
793
|
+
* @param a - Current value
|
|
794
|
+
* @param b - Previous/comparison value
|
|
795
|
+
* @returns Percentage difference (positive = increase, negative = decrease), or 0 if either value is falsy
|
|
796
|
+
*/
|
|
797
|
+
function percentDifference(a, b) {
|
|
798
|
+
if (!b || !a) return 0;
|
|
799
|
+
return (a - b) / ((a + b) / 2) * 100;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Resolves a period string or object into start/end dates for current and previous periods.
|
|
803
|
+
* @param periodRange - Period string (e.g., '30d', '3mo', 'all') or Period object with start/end dates
|
|
804
|
+
* @returns Resolved period ranges with timestamps and formatted dates for both current and previous periods
|
|
805
|
+
*/
|
|
806
|
+
function userPeriodRange(periodRange = "30d") {
|
|
807
|
+
let startPeriod;
|
|
808
|
+
let endPeriod;
|
|
809
|
+
let startPrevPeriod;
|
|
810
|
+
let endPrevPeriod;
|
|
811
|
+
if (typeof periodRange === "string") {
|
|
812
|
+
endPeriod = dayjs();
|
|
813
|
+
if (periodRange === "all") {
|
|
814
|
+
startPeriod = dayjs().subtract(100, "year");
|
|
815
|
+
startPrevPeriod = dayjs().subtract(200, "year");
|
|
816
|
+
endPrevPeriod = dayjs().subtract(100, "year").subtract(1, "day");
|
|
817
|
+
} else if (periodRange === "max") {
|
|
818
|
+
const maxDays = 480;
|
|
819
|
+
startPeriod = endPeriod.clone().subtract(maxDays, "day");
|
|
820
|
+
startPrevPeriod = endPeriod.clone().subtract(maxDays * 2, "day");
|
|
821
|
+
endPrevPeriod = endPeriod.clone().subtract(maxDays + 1, "day");
|
|
822
|
+
} else {
|
|
823
|
+
const periodDays = periodRange.includes("d") ? Number.parseInt(periodRange.replace("d", "")) : Number.parseInt(periodRange.replace("mo", "")) * 30;
|
|
824
|
+
startPeriod = endPeriod.clone().subtract(periodDays, "day");
|
|
825
|
+
startPrevPeriod = endPeriod.clone().subtract(periodDays * 2, "day");
|
|
826
|
+
endPrevPeriod = endPeriod.clone().subtract(periodDays + 1, "day");
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
startPeriod = dayjs(periodRange.start);
|
|
830
|
+
endPeriod = dayjs(periodRange.end);
|
|
831
|
+
const dayDiff = endPeriod.diff(startPeriod, "day");
|
|
832
|
+
startPrevPeriod = dayjs(periodRange.start).subtract(dayDiff, "day");
|
|
833
|
+
endPrevPeriod = dayjs(periodRange.end).subtract(dayDiff, "day");
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
period: {
|
|
837
|
+
startTimestamp: startPeriod.valueOf(),
|
|
838
|
+
start: startPeriod.toDate(),
|
|
839
|
+
startDateTime: startPeriod.format("YYYY-MM-DD HH:mm:ss"),
|
|
840
|
+
startDate: startPeriod.format("YYYY-MM-DD"),
|
|
841
|
+
end: endPeriod.toDate(),
|
|
842
|
+
endDate: endPeriod.format("YYYY-MM-DD"),
|
|
843
|
+
endTimestamp: endPeriod.valueOf(),
|
|
844
|
+
endDateTime: endPeriod.format("YYYY-MM-DD HH:mm:ss")
|
|
845
|
+
},
|
|
846
|
+
prevPeriod: {
|
|
847
|
+
start: startPrevPeriod.toDate(),
|
|
848
|
+
startDate: startPrevPeriod.format("YYYY-MM-DD"),
|
|
849
|
+
end: endPrevPeriod.toDate(),
|
|
850
|
+
endDate: endPrevPeriod.format("YYYY-MM-DD")
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/api/search-analytics/query.ts
|
|
857
|
+
/**
|
|
858
|
+
* Recursively queries GSC search analytics, automatically handling pagination for large datasets.
|
|
859
|
+
*/
|
|
860
|
+
async function queryRecursive(client, siteUrl, query$1, options = {}) {
|
|
861
|
+
const rowLimit = query$1.rowLimit || 25e3;
|
|
862
|
+
const rows = [];
|
|
863
|
+
let page$1 = 1;
|
|
864
|
+
const initialRows = (await client.searchAnalytics.query(siteUrl, {
|
|
865
|
+
...query$1,
|
|
866
|
+
startRow: 0,
|
|
867
|
+
rowLimit
|
|
868
|
+
})).rows || [];
|
|
869
|
+
rows.push(...initialRows);
|
|
870
|
+
options.onProgress?.(rows.length);
|
|
871
|
+
if (initialRows.length === rowLimit) while (true) {
|
|
872
|
+
page$1++;
|
|
873
|
+
const nextRows = (await client.searchAnalytics.query(siteUrl, {
|
|
874
|
+
...query$1,
|
|
875
|
+
startRow: rows.length,
|
|
876
|
+
rowLimit
|
|
877
|
+
})).rows || [];
|
|
878
|
+
rows.push(...nextRows);
|
|
879
|
+
options.onProgress?.(rows.length);
|
|
880
|
+
if (nextRows.length < rowLimit) break;
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
rows,
|
|
884
|
+
pages: page$1
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Creates a GSC search analytics query request body with standard defaults.
|
|
889
|
+
*/
|
|
890
|
+
function createQueryBody(options = {}) {
|
|
891
|
+
const { domain, period = {
|
|
892
|
+
start: dayjs().subtract(30, "day").toDate(),
|
|
893
|
+
end: dayjs().toDate()
|
|
894
|
+
}, filters = [], type = "web", dataState = "all", aggregationType = "byPage", rowLimit = 25e3 } = options;
|
|
895
|
+
const allFilters = [...filters];
|
|
896
|
+
if (aggregationType === "byPage") {
|
|
897
|
+
allFilters.unshift({
|
|
898
|
+
dimension: "page",
|
|
899
|
+
operator: "excludingRegex",
|
|
900
|
+
expression: `#`
|
|
901
|
+
});
|
|
902
|
+
if (domain) allFilters.unshift({
|
|
903
|
+
dimension: "page",
|
|
904
|
+
operator: "includingRegex",
|
|
905
|
+
expression: `^${withoutTrailingSlash(domain).replace(/\./g, "\\.")}/.*`
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const result = {
|
|
909
|
+
type,
|
|
910
|
+
aggregationType,
|
|
911
|
+
dataState,
|
|
912
|
+
startDate: formatDateGsc(period.start),
|
|
913
|
+
endDate: formatDateGsc(period.end),
|
|
914
|
+
rowLimit
|
|
915
|
+
};
|
|
916
|
+
if (allFilters.length > 0) result.dimensionFilterGroups = [{ filters: allFilters }];
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Helper to add search appearance dimension filter.
|
|
921
|
+
* Use with spread: createQueryBody({ ...withSearchAppearance('AMP'), period })
|
|
922
|
+
*/
|
|
923
|
+
function withSearchAppearance(appearance) {
|
|
924
|
+
return { filters: [{
|
|
925
|
+
dimension: "searchAppearance",
|
|
926
|
+
operator: "equals",
|
|
927
|
+
expression: appearance
|
|
928
|
+
}] };
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Helper to set data type (web, image, video, news, discover, googleNews).
|
|
932
|
+
* Use with spread: createQueryBody({ ...withDataType('image'), period })
|
|
933
|
+
*/
|
|
934
|
+
function withDataType(type) {
|
|
935
|
+
return { type };
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Helper to include fresh/unfinalized data (last 3 days).
|
|
939
|
+
* Use with spread: createQueryBody({ ...withFreshData(), period })
|
|
940
|
+
*/
|
|
941
|
+
function withFreshData() {
|
|
942
|
+
return { dataState: "all" };
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Helper to use only finalized data (excludes last 3 days).
|
|
946
|
+
* Use with spread: createQueryBody({ ...withFinalData(), period })
|
|
947
|
+
*/
|
|
948
|
+
function withFinalData() {
|
|
949
|
+
return { dataState: "final" };
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Helper to use byProperty aggregation (domain-level rollup).
|
|
953
|
+
* Use for sc-domain: properties to get true totals.
|
|
954
|
+
* Use with spread: createQueryBody({ ...withPropertyAggregation(), period })
|
|
955
|
+
*/
|
|
956
|
+
function withPropertyAggregation() {
|
|
957
|
+
return { aggregationType: "byProperty" };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/api/search-analytics/analytics.ts
|
|
962
|
+
/**
|
|
963
|
+
* Fetches overall site analytics summary with period comparison and keyword data.
|
|
964
|
+
*/
|
|
965
|
+
async function fetchAnalyticsWithComparison(client, siteUrl, options = {}) {
|
|
966
|
+
const [currentSummary, previousSummary, currentKeywords, previousKeywords] = await Promise.all([
|
|
967
|
+
client.searchAnalytics.query(siteUrl, { ...createQueryBody(options) }).then((res) => (res.rows || [])[0] || {}),
|
|
968
|
+
options.prevPeriod ? client.searchAnalytics.query(siteUrl, { ...createQueryBody({
|
|
969
|
+
...options,
|
|
970
|
+
period: options.prevPeriod
|
|
971
|
+
}) }).then((res) => (res.rows || [])[0] || {}) : Promise.resolve({}),
|
|
972
|
+
client.searchAnalytics.query(siteUrl, {
|
|
973
|
+
...createQueryBody(options),
|
|
974
|
+
dimensions: ["date", "query"]
|
|
975
|
+
}).then((res) => res.rows || []),
|
|
976
|
+
options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
977
|
+
...createQueryBody({
|
|
978
|
+
...options,
|
|
979
|
+
period: options.prevPeriod
|
|
980
|
+
}),
|
|
981
|
+
dimensions: ["date", "query"]
|
|
982
|
+
}).then((res) => res.rows || []) : Promise.resolve([])
|
|
983
|
+
]);
|
|
984
|
+
return {
|
|
985
|
+
current: [{
|
|
986
|
+
...currentSummary,
|
|
987
|
+
keywords: currentKeywords
|
|
988
|
+
}],
|
|
989
|
+
previous: [{
|
|
990
|
+
...previousSummary,
|
|
991
|
+
keywords: previousKeywords
|
|
992
|
+
}],
|
|
993
|
+
metadata: {
|
|
994
|
+
currentCount: 1,
|
|
995
|
+
previousCount: options.prevPeriod ? 1 : 0,
|
|
996
|
+
currentKeywordCount: currentKeywords.length,
|
|
997
|
+
previousKeywordCount: previousKeywords.length
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/utils/countries.ts
|
|
1004
|
+
var countries_default = [
|
|
1005
|
+
{
|
|
1006
|
+
"name": "Afghanistan",
|
|
1007
|
+
"alpha-2": "AF",
|
|
1008
|
+
"alpha-3": "AFG",
|
|
1009
|
+
"country-code": "004"
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"name": "Åland Islands",
|
|
1013
|
+
"alpha-2": "AX",
|
|
1014
|
+
"alpha-3": "ALA",
|
|
1015
|
+
"country-code": "248"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"name": "Albania",
|
|
1019
|
+
"alpha-2": "AL",
|
|
1020
|
+
"alpha-3": "ALB",
|
|
1021
|
+
"country-code": "008"
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
"name": "Algeria",
|
|
1025
|
+
"alpha-2": "DZ",
|
|
1026
|
+
"alpha-3": "DZA",
|
|
1027
|
+
"country-code": "012"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
"name": "American Samoa",
|
|
1031
|
+
"alpha-2": "AS",
|
|
1032
|
+
"alpha-3": "ASM",
|
|
1033
|
+
"country-code": "016"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
"name": "Andorra",
|
|
1037
|
+
"alpha-2": "AD",
|
|
1038
|
+
"alpha-3": "AND",
|
|
1039
|
+
"country-code": "020"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
"name": "Angola",
|
|
1043
|
+
"alpha-2": "AO",
|
|
1044
|
+
"alpha-3": "AGO",
|
|
1045
|
+
"country-code": "024"
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
"name": "Anguilla",
|
|
1049
|
+
"alpha-2": "AI",
|
|
1050
|
+
"alpha-3": "AIA",
|
|
1051
|
+
"country-code": "660"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"name": "Antarctica",
|
|
1055
|
+
"alpha-2": "AQ",
|
|
1056
|
+
"alpha-3": "ATA",
|
|
1057
|
+
"country-code": "010"
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"name": "Antigua and Barbuda",
|
|
1061
|
+
"alpha-2": "AG",
|
|
1062
|
+
"alpha-3": "ATG",
|
|
1063
|
+
"country-code": "028"
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"name": "Argentina",
|
|
1067
|
+
"alpha-2": "AR",
|
|
1068
|
+
"alpha-3": "ARG",
|
|
1069
|
+
"country-code": "032"
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
"name": "Armenia",
|
|
1073
|
+
"alpha-2": "AM",
|
|
1074
|
+
"alpha-3": "ARM",
|
|
1075
|
+
"country-code": "051"
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
"name": "Aruba",
|
|
1079
|
+
"alpha-2": "AW",
|
|
1080
|
+
"alpha-3": "ABW",
|
|
1081
|
+
"country-code": "533"
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
"name": "Australia",
|
|
1085
|
+
"alpha-2": "AU",
|
|
1086
|
+
"alpha-3": "AUS",
|
|
1087
|
+
"country-code": "036"
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
"name": "Austria",
|
|
1091
|
+
"alpha-2": "AT",
|
|
1092
|
+
"alpha-3": "AUT",
|
|
1093
|
+
"country-code": "040"
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"name": "Azerbaijan",
|
|
1097
|
+
"alpha-2": "AZ",
|
|
1098
|
+
"alpha-3": "AZE",
|
|
1099
|
+
"country-code": "031"
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
"name": "Bahamas",
|
|
1103
|
+
"alpha-2": "BS",
|
|
1104
|
+
"alpha-3": "BHS",
|
|
1105
|
+
"country-code": "044"
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
"name": "Bahrain",
|
|
1109
|
+
"alpha-2": "BH",
|
|
1110
|
+
"alpha-3": "BHR",
|
|
1111
|
+
"country-code": "048"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"name": "Bangladesh",
|
|
1115
|
+
"alpha-2": "BD",
|
|
1116
|
+
"alpha-3": "BGD",
|
|
1117
|
+
"country-code": "050"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
"name": "Barbados",
|
|
1121
|
+
"alpha-2": "BB",
|
|
1122
|
+
"alpha-3": "BRB",
|
|
1123
|
+
"country-code": "052"
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
"name": "Belarus",
|
|
1127
|
+
"alpha-2": "BY",
|
|
1128
|
+
"alpha-3": "BLR",
|
|
1129
|
+
"country-code": "112"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
"name": "Belgium",
|
|
1133
|
+
"alpha-2": "BE",
|
|
1134
|
+
"alpha-3": "BEL",
|
|
1135
|
+
"country-code": "056"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"name": "Belize",
|
|
1139
|
+
"alpha-2": "BZ",
|
|
1140
|
+
"alpha-3": "BLZ",
|
|
1141
|
+
"country-code": "084"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"name": "Benin",
|
|
1145
|
+
"alpha-2": "BJ",
|
|
1146
|
+
"alpha-3": "BEN",
|
|
1147
|
+
"country-code": "204"
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
"name": "Bermuda",
|
|
1151
|
+
"alpha-2": "BM",
|
|
1152
|
+
"alpha-3": "BMU",
|
|
1153
|
+
"country-code": "060"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
"name": "Bhutan",
|
|
1157
|
+
"alpha-2": "BT",
|
|
1158
|
+
"alpha-3": "BTN",
|
|
1159
|
+
"country-code": "064"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"name": "Bolivia (Plurinational State of)",
|
|
1163
|
+
"alpha-2": "BO",
|
|
1164
|
+
"alpha-3": "BOL",
|
|
1165
|
+
"country-code": "068"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
"name": "Bonaire, Sint Eustatius and Saba",
|
|
1169
|
+
"alpha-2": "BQ",
|
|
1170
|
+
"alpha-3": "BES",
|
|
1171
|
+
"country-code": "535"
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
"name": "Bosnia and Herzegovina",
|
|
1175
|
+
"alpha-2": "BA",
|
|
1176
|
+
"alpha-3": "BIH",
|
|
1177
|
+
"country-code": "070"
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
"name": "Botswana",
|
|
1181
|
+
"alpha-2": "BW",
|
|
1182
|
+
"alpha-3": "BWA",
|
|
1183
|
+
"country-code": "072"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
"name": "Bouvet Island",
|
|
1187
|
+
"alpha-2": "BV",
|
|
1188
|
+
"alpha-3": "BVT",
|
|
1189
|
+
"country-code": "074"
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
"name": "Brazil",
|
|
1193
|
+
"alpha-2": "BR",
|
|
1194
|
+
"alpha-3": "BRA",
|
|
1195
|
+
"country-code": "076"
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
"name": "British Indian Ocean Territory",
|
|
1199
|
+
"alpha-2": "IO",
|
|
1200
|
+
"alpha-3": "IOT",
|
|
1201
|
+
"country-code": "086"
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
"name": "Brunei Darussalam",
|
|
1205
|
+
"alpha-2": "BN",
|
|
1206
|
+
"alpha-3": "BRN",
|
|
1207
|
+
"country-code": "096"
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
"name": "Bulgaria",
|
|
1211
|
+
"alpha-2": "BG",
|
|
1212
|
+
"alpha-3": "BGR",
|
|
1213
|
+
"country-code": "100"
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
"name": "Burkina Faso",
|
|
1217
|
+
"alpha-2": "BF",
|
|
1218
|
+
"alpha-3": "BFA",
|
|
1219
|
+
"country-code": "854"
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
"name": "Burundi",
|
|
1223
|
+
"alpha-2": "BI",
|
|
1224
|
+
"alpha-3": "BDI",
|
|
1225
|
+
"country-code": "108"
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
"name": "Cabo Verde",
|
|
1229
|
+
"alpha-2": "CV",
|
|
1230
|
+
"alpha-3": "CPV",
|
|
1231
|
+
"country-code": "132"
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
"name": "Cambodia",
|
|
1235
|
+
"alpha-2": "KH",
|
|
1236
|
+
"alpha-3": "KHM",
|
|
1237
|
+
"country-code": "116"
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
"name": "Cameroon",
|
|
1241
|
+
"alpha-2": "CM",
|
|
1242
|
+
"alpha-3": "CMR",
|
|
1243
|
+
"country-code": "120"
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
"name": "Canada",
|
|
1247
|
+
"alpha-2": "CA",
|
|
1248
|
+
"alpha-3": "CAN",
|
|
1249
|
+
"country-code": "124"
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
"name": "Cayman Islands",
|
|
1253
|
+
"alpha-2": "KY",
|
|
1254
|
+
"alpha-3": "CYM",
|
|
1255
|
+
"country-code": "136"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
"name": "Central African Republic",
|
|
1259
|
+
"alpha-2": "CF",
|
|
1260
|
+
"alpha-3": "CAF",
|
|
1261
|
+
"country-code": "140"
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
"name": "Chad",
|
|
1265
|
+
"alpha-2": "TD",
|
|
1266
|
+
"alpha-3": "TCD",
|
|
1267
|
+
"country-code": "148"
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
"name": "Chile",
|
|
1271
|
+
"alpha-2": "CL",
|
|
1272
|
+
"alpha-3": "CHL",
|
|
1273
|
+
"country-code": "152"
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
"name": "China",
|
|
1277
|
+
"alpha-2": "CN",
|
|
1278
|
+
"alpha-3": "CHN",
|
|
1279
|
+
"country-code": "156"
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
"name": "Christmas Island",
|
|
1283
|
+
"alpha-2": "CX",
|
|
1284
|
+
"alpha-3": "CXR",
|
|
1285
|
+
"country-code": "162"
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
"name": "Cocos (Keeling) Islands",
|
|
1289
|
+
"alpha-2": "CC",
|
|
1290
|
+
"alpha-3": "CCK",
|
|
1291
|
+
"country-code": "166"
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
"name": "Colombia",
|
|
1295
|
+
"alpha-2": "CO",
|
|
1296
|
+
"alpha-3": "COL",
|
|
1297
|
+
"country-code": "170"
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
"name": "Comoros",
|
|
1301
|
+
"alpha-2": "KM",
|
|
1302
|
+
"alpha-3": "COM",
|
|
1303
|
+
"country-code": "174"
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
"name": "Congo",
|
|
1307
|
+
"alpha-2": "CG",
|
|
1308
|
+
"alpha-3": "COG",
|
|
1309
|
+
"country-code": "178"
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
"name": "Congo, Democratic Republic of the",
|
|
1313
|
+
"alpha-2": "CD",
|
|
1314
|
+
"alpha-3": "COD",
|
|
1315
|
+
"country-code": "180"
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
"name": "Cook Islands",
|
|
1319
|
+
"alpha-2": "CK",
|
|
1320
|
+
"alpha-3": "COK",
|
|
1321
|
+
"country-code": "184"
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
"name": "Costa Rica",
|
|
1325
|
+
"alpha-2": "CR",
|
|
1326
|
+
"alpha-3": "CRI",
|
|
1327
|
+
"country-code": "188"
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
"name": "Côte d'Ivoire",
|
|
1331
|
+
"alpha-2": "CI",
|
|
1332
|
+
"alpha-3": "CIV",
|
|
1333
|
+
"country-code": "384"
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
"name": "Croatia",
|
|
1337
|
+
"alpha-2": "HR",
|
|
1338
|
+
"alpha-3": "HRV",
|
|
1339
|
+
"country-code": "191"
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
"name": "Cuba",
|
|
1343
|
+
"alpha-2": "CU",
|
|
1344
|
+
"alpha-3": "CUB",
|
|
1345
|
+
"country-code": "192"
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
"name": "Curaçao",
|
|
1349
|
+
"alpha-2": "CW",
|
|
1350
|
+
"alpha-3": "CUW",
|
|
1351
|
+
"country-code": "531"
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
"name": "Cyprus",
|
|
1355
|
+
"alpha-2": "CY",
|
|
1356
|
+
"alpha-3": "CYP",
|
|
1357
|
+
"country-code": "196"
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
"name": "Czechia",
|
|
1361
|
+
"alpha-2": "CZ",
|
|
1362
|
+
"alpha-3": "CZE",
|
|
1363
|
+
"country-code": "203"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
"name": "Denmark",
|
|
1367
|
+
"alpha-2": "DK",
|
|
1368
|
+
"alpha-3": "DNK",
|
|
1369
|
+
"country-code": "208"
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
"name": "Djibouti",
|
|
1373
|
+
"alpha-2": "DJ",
|
|
1374
|
+
"alpha-3": "DJI",
|
|
1375
|
+
"country-code": "262"
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"name": "Dominica",
|
|
1379
|
+
"alpha-2": "DM",
|
|
1380
|
+
"alpha-3": "DMA",
|
|
1381
|
+
"country-code": "212"
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
"name": "Dominican Republic",
|
|
1385
|
+
"alpha-2": "DO",
|
|
1386
|
+
"alpha-3": "DOM",
|
|
1387
|
+
"country-code": "214"
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
"name": "Ecuador",
|
|
1391
|
+
"alpha-2": "EC",
|
|
1392
|
+
"alpha-3": "ECU",
|
|
1393
|
+
"country-code": "218"
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
"name": "Egypt",
|
|
1397
|
+
"alpha-2": "EG",
|
|
1398
|
+
"alpha-3": "EGY",
|
|
1399
|
+
"country-code": "818"
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
"name": "El Salvador",
|
|
1403
|
+
"alpha-2": "SV",
|
|
1404
|
+
"alpha-3": "SLV",
|
|
1405
|
+
"country-code": "222"
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
"name": "Equatorial Guinea",
|
|
1409
|
+
"alpha-2": "GQ",
|
|
1410
|
+
"alpha-3": "GNQ",
|
|
1411
|
+
"country-code": "226"
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
"name": "Eritrea",
|
|
1415
|
+
"alpha-2": "ER",
|
|
1416
|
+
"alpha-3": "ERI",
|
|
1417
|
+
"country-code": "232"
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
"name": "Estonia",
|
|
1421
|
+
"alpha-2": "EE",
|
|
1422
|
+
"alpha-3": "EST",
|
|
1423
|
+
"country-code": "233"
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
"name": "Eswatini",
|
|
1427
|
+
"alpha-2": "SZ",
|
|
1428
|
+
"alpha-3": "SWZ",
|
|
1429
|
+
"country-code": "748"
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
"name": "Ethiopia",
|
|
1433
|
+
"alpha-2": "ET",
|
|
1434
|
+
"alpha-3": "ETH",
|
|
1435
|
+
"country-code": "231"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
"name": "Falkland Islands (Malvinas)",
|
|
1439
|
+
"alpha-2": "FK",
|
|
1440
|
+
"alpha-3": "FLK",
|
|
1441
|
+
"country-code": "238"
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
"name": "Faroe Islands",
|
|
1445
|
+
"alpha-2": "FO",
|
|
1446
|
+
"alpha-3": "FRO",
|
|
1447
|
+
"country-code": "234"
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
"name": "Fiji",
|
|
1451
|
+
"alpha-2": "FJ",
|
|
1452
|
+
"alpha-3": "FJI",
|
|
1453
|
+
"country-code": "242"
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
"name": "Finland",
|
|
1457
|
+
"alpha-2": "FI",
|
|
1458
|
+
"alpha-3": "FIN",
|
|
1459
|
+
"country-code": "246"
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
"name": "France",
|
|
1463
|
+
"alpha-2": "FR",
|
|
1464
|
+
"alpha-3": "FRA",
|
|
1465
|
+
"country-code": "250"
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
"name": "French Guiana",
|
|
1469
|
+
"alpha-2": "GF",
|
|
1470
|
+
"alpha-3": "GUF",
|
|
1471
|
+
"country-code": "254"
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
"name": "French Polynesia",
|
|
1475
|
+
"alpha-2": "PF",
|
|
1476
|
+
"alpha-3": "PYF",
|
|
1477
|
+
"country-code": "258"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
"name": "French Southern Territories",
|
|
1481
|
+
"alpha-2": "TF",
|
|
1482
|
+
"alpha-3": "ATF",
|
|
1483
|
+
"country-code": "260"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
"name": "Gabon",
|
|
1487
|
+
"alpha-2": "GA",
|
|
1488
|
+
"alpha-3": "GAB",
|
|
1489
|
+
"country-code": "266"
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
"name": "Gambia",
|
|
1493
|
+
"alpha-2": "GM",
|
|
1494
|
+
"alpha-3": "GMB",
|
|
1495
|
+
"country-code": "270"
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
"name": "Georgia",
|
|
1499
|
+
"alpha-2": "GE",
|
|
1500
|
+
"alpha-3": "GEO",
|
|
1501
|
+
"country-code": "268"
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
"name": "Germany",
|
|
1505
|
+
"alpha-2": "DE",
|
|
1506
|
+
"alpha-3": "DEU",
|
|
1507
|
+
"country-code": "276"
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
"name": "Ghana",
|
|
1511
|
+
"alpha-2": "GH",
|
|
1512
|
+
"alpha-3": "GHA",
|
|
1513
|
+
"country-code": "288"
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
"name": "Gibraltar",
|
|
1517
|
+
"alpha-2": "GI",
|
|
1518
|
+
"alpha-3": "GIB",
|
|
1519
|
+
"country-code": "292"
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
"name": "Greece",
|
|
1523
|
+
"alpha-2": "GR",
|
|
1524
|
+
"alpha-3": "GRC",
|
|
1525
|
+
"country-code": "300"
|
|
1526
|
+
},
|
|
1527
|
+
{
|
|
1528
|
+
"name": "Greenland",
|
|
1529
|
+
"alpha-2": "GL",
|
|
1530
|
+
"alpha-3": "GRL",
|
|
1531
|
+
"country-code": "304"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
"name": "Grenada",
|
|
1535
|
+
"alpha-2": "GD",
|
|
1536
|
+
"alpha-3": "GRD",
|
|
1537
|
+
"country-code": "308"
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
"name": "Guadeloupe",
|
|
1541
|
+
"alpha-2": "GP",
|
|
1542
|
+
"alpha-3": "GLP",
|
|
1543
|
+
"country-code": "312"
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
"name": "Guam",
|
|
1547
|
+
"alpha-2": "GU",
|
|
1548
|
+
"alpha-3": "GUM",
|
|
1549
|
+
"country-code": "316"
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
"name": "Guatemala",
|
|
1553
|
+
"alpha-2": "GT",
|
|
1554
|
+
"alpha-3": "GTM",
|
|
1555
|
+
"country-code": "320"
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
"name": "Guernsey",
|
|
1559
|
+
"alpha-2": "GG",
|
|
1560
|
+
"alpha-3": "GGY",
|
|
1561
|
+
"country-code": "831"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"name": "Guinea",
|
|
1565
|
+
"alpha-2": "GN",
|
|
1566
|
+
"alpha-3": "GIN",
|
|
1567
|
+
"country-code": "324"
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
"name": "Guinea-Bissau",
|
|
1571
|
+
"alpha-2": "GW",
|
|
1572
|
+
"alpha-3": "GNB",
|
|
1573
|
+
"country-code": "624"
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
"name": "Guyana",
|
|
1577
|
+
"alpha-2": "GY",
|
|
1578
|
+
"alpha-3": "GUY",
|
|
1579
|
+
"country-code": "328"
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
"name": "Haiti",
|
|
1583
|
+
"alpha-2": "HT",
|
|
1584
|
+
"alpha-3": "HTI",
|
|
1585
|
+
"country-code": "332"
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
"name": "Heard Island and McDonald Islands",
|
|
1589
|
+
"alpha-2": "HM",
|
|
1590
|
+
"alpha-3": "HMD",
|
|
1591
|
+
"country-code": "334"
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
"name": "Holy See",
|
|
1595
|
+
"alpha-2": "VA",
|
|
1596
|
+
"alpha-3": "VAT",
|
|
1597
|
+
"country-code": "336"
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
"name": "Honduras",
|
|
1601
|
+
"alpha-2": "HN",
|
|
1602
|
+
"alpha-3": "HND",
|
|
1603
|
+
"country-code": "340"
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
"name": "Hong Kong",
|
|
1607
|
+
"alpha-2": "HK",
|
|
1608
|
+
"alpha-3": "HKG",
|
|
1609
|
+
"country-code": "344"
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
"name": "Hungary",
|
|
1613
|
+
"alpha-2": "HU",
|
|
1614
|
+
"alpha-3": "HUN",
|
|
1615
|
+
"country-code": "348"
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
"name": "Iceland",
|
|
1619
|
+
"alpha-2": "IS",
|
|
1620
|
+
"alpha-3": "ISL",
|
|
1621
|
+
"country-code": "352"
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
"name": "India",
|
|
1625
|
+
"alpha-2": "IN",
|
|
1626
|
+
"alpha-3": "IND",
|
|
1627
|
+
"country-code": "356"
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
"name": "Indonesia",
|
|
1631
|
+
"alpha-2": "ID",
|
|
1632
|
+
"alpha-3": "IDN",
|
|
1633
|
+
"country-code": "360"
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
"name": "Iran (Islamic Republic of)",
|
|
1637
|
+
"alpha-2": "IR",
|
|
1638
|
+
"alpha-3": "IRN",
|
|
1639
|
+
"country-code": "364"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
"name": "Iraq",
|
|
1643
|
+
"alpha-2": "IQ",
|
|
1644
|
+
"alpha-3": "IRQ",
|
|
1645
|
+
"country-code": "368"
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
"name": "Ireland",
|
|
1649
|
+
"alpha-2": "IE",
|
|
1650
|
+
"alpha-3": "IRL",
|
|
1651
|
+
"country-code": "372"
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
"name": "Isle of Man",
|
|
1655
|
+
"alpha-2": "IM",
|
|
1656
|
+
"alpha-3": "IMN",
|
|
1657
|
+
"country-code": "833"
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
"name": "Israel",
|
|
1661
|
+
"alpha-2": "IL",
|
|
1662
|
+
"alpha-3": "ISR",
|
|
1663
|
+
"country-code": "376"
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
"name": "Italy",
|
|
1667
|
+
"alpha-2": "IT",
|
|
1668
|
+
"alpha-3": "ITA",
|
|
1669
|
+
"country-code": "380"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"name": "Jamaica",
|
|
1673
|
+
"alpha-2": "JM",
|
|
1674
|
+
"alpha-3": "JAM",
|
|
1675
|
+
"country-code": "388"
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
"name": "Japan",
|
|
1679
|
+
"alpha-2": "JP",
|
|
1680
|
+
"alpha-3": "JPN",
|
|
1681
|
+
"country-code": "392"
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
"name": "Jersey",
|
|
1685
|
+
"alpha-2": "JE",
|
|
1686
|
+
"alpha-3": "JEY",
|
|
1687
|
+
"country-code": "832"
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
"name": "Jordan",
|
|
1691
|
+
"alpha-2": "JO",
|
|
1692
|
+
"alpha-3": "JOR",
|
|
1693
|
+
"country-code": "400"
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
"name": "Kazakhstan",
|
|
1697
|
+
"alpha-2": "KZ",
|
|
1698
|
+
"alpha-3": "KAZ",
|
|
1699
|
+
"country-code": "398"
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
"name": "Kenya",
|
|
1703
|
+
"alpha-2": "KE",
|
|
1704
|
+
"alpha-3": "KEN",
|
|
1705
|
+
"country-code": "404"
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
"name": "Kiribati",
|
|
1709
|
+
"alpha-2": "KI",
|
|
1710
|
+
"alpha-3": "KIR",
|
|
1711
|
+
"country-code": "296"
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"name": "Korea (Democratic People's Republic of)",
|
|
1715
|
+
"alpha-2": "KP",
|
|
1716
|
+
"alpha-3": "PRK",
|
|
1717
|
+
"country-code": "408"
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
"name": "Korea, Republic of",
|
|
1721
|
+
"alpha-2": "KR",
|
|
1722
|
+
"alpha-3": "KOR",
|
|
1723
|
+
"country-code": "410"
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
"name": "Kuwait",
|
|
1727
|
+
"alpha-2": "KW",
|
|
1728
|
+
"alpha-3": "KWT",
|
|
1729
|
+
"country-code": "414"
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
"name": "Kyrgyzstan",
|
|
1733
|
+
"alpha-2": "KG",
|
|
1734
|
+
"alpha-3": "KGZ",
|
|
1735
|
+
"country-code": "417"
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
"name": "Lao People's Democratic Republic",
|
|
1739
|
+
"alpha-2": "LA",
|
|
1740
|
+
"alpha-3": "LAO",
|
|
1741
|
+
"country-code": "418"
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
"name": "Latvia",
|
|
1745
|
+
"alpha-2": "LV",
|
|
1746
|
+
"alpha-3": "LVA",
|
|
1747
|
+
"country-code": "428"
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
"name": "Lebanon",
|
|
1751
|
+
"alpha-2": "LB",
|
|
1752
|
+
"alpha-3": "LBN",
|
|
1753
|
+
"country-code": "422"
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
"name": "Lesotho",
|
|
1757
|
+
"alpha-2": "LS",
|
|
1758
|
+
"alpha-3": "LSO",
|
|
1759
|
+
"country-code": "426"
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
"name": "Liberia",
|
|
1763
|
+
"alpha-2": "LR",
|
|
1764
|
+
"alpha-3": "LBR",
|
|
1765
|
+
"country-code": "430"
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
"name": "Libya",
|
|
1769
|
+
"alpha-2": "LY",
|
|
1770
|
+
"alpha-3": "LBY",
|
|
1771
|
+
"country-code": "434"
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
"name": "Liechtenstein",
|
|
1775
|
+
"alpha-2": "LI",
|
|
1776
|
+
"alpha-3": "LIE",
|
|
1777
|
+
"country-code": "438"
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
"name": "Lithuania",
|
|
1781
|
+
"alpha-2": "LT",
|
|
1782
|
+
"alpha-3": "LTU",
|
|
1783
|
+
"country-code": "440"
|
|
1784
|
+
},
|
|
1785
|
+
{
|
|
1786
|
+
"name": "Luxembourg",
|
|
1787
|
+
"alpha-2": "LU",
|
|
1788
|
+
"alpha-3": "LUX",
|
|
1789
|
+
"country-code": "442"
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
"name": "Macao",
|
|
1793
|
+
"alpha-2": "MO",
|
|
1794
|
+
"alpha-3": "MAC",
|
|
1795
|
+
"country-code": "446"
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
"name": "Madagascar",
|
|
1799
|
+
"alpha-2": "MG",
|
|
1800
|
+
"alpha-3": "MDG",
|
|
1801
|
+
"country-code": "450"
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
"name": "Malawi",
|
|
1805
|
+
"alpha-2": "MW",
|
|
1806
|
+
"alpha-3": "MWI",
|
|
1807
|
+
"country-code": "454"
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
"name": "Malaysia",
|
|
1811
|
+
"alpha-2": "MY",
|
|
1812
|
+
"alpha-3": "MYS",
|
|
1813
|
+
"country-code": "458"
|
|
1814
|
+
},
|
|
1815
|
+
{
|
|
1816
|
+
"name": "Maldives",
|
|
1817
|
+
"alpha-2": "MV",
|
|
1818
|
+
"alpha-3": "MDV",
|
|
1819
|
+
"country-code": "462"
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
"name": "Mali",
|
|
1823
|
+
"alpha-2": "ML",
|
|
1824
|
+
"alpha-3": "MLI",
|
|
1825
|
+
"country-code": "466"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
"name": "Malta",
|
|
1829
|
+
"alpha-2": "MT",
|
|
1830
|
+
"alpha-3": "MLT",
|
|
1831
|
+
"country-code": "470"
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
"name": "Marshall Islands",
|
|
1835
|
+
"alpha-2": "MH",
|
|
1836
|
+
"alpha-3": "MHL",
|
|
1837
|
+
"country-code": "584"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
"name": "Martinique",
|
|
1841
|
+
"alpha-2": "MQ",
|
|
1842
|
+
"alpha-3": "MTQ",
|
|
1843
|
+
"country-code": "474"
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
"name": "Mauritania",
|
|
1847
|
+
"alpha-2": "MR",
|
|
1848
|
+
"alpha-3": "MRT",
|
|
1849
|
+
"country-code": "478"
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
"name": "Mauritius",
|
|
1853
|
+
"alpha-2": "MU",
|
|
1854
|
+
"alpha-3": "MUS",
|
|
1855
|
+
"country-code": "480"
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
"name": "Mayotte",
|
|
1859
|
+
"alpha-2": "YT",
|
|
1860
|
+
"alpha-3": "MYT",
|
|
1861
|
+
"country-code": "175"
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
"name": "Mexico",
|
|
1865
|
+
"alpha-2": "MX",
|
|
1866
|
+
"alpha-3": "MEX",
|
|
1867
|
+
"country-code": "484"
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
"name": "Micronesia (Federated States of)",
|
|
1871
|
+
"alpha-2": "FM",
|
|
1872
|
+
"alpha-3": "FSM",
|
|
1873
|
+
"country-code": "583"
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
"name": "Moldova, Republic of",
|
|
1877
|
+
"alpha-2": "MD",
|
|
1878
|
+
"alpha-3": "MDA",
|
|
1879
|
+
"country-code": "498"
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
"name": "Monaco",
|
|
1883
|
+
"alpha-2": "MC",
|
|
1884
|
+
"alpha-3": "MCO",
|
|
1885
|
+
"country-code": "492"
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
"name": "Mongolia",
|
|
1889
|
+
"alpha-2": "MN",
|
|
1890
|
+
"alpha-3": "MNG",
|
|
1891
|
+
"country-code": "496"
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
"name": "Montenegro",
|
|
1895
|
+
"alpha-2": "ME",
|
|
1896
|
+
"alpha-3": "MNE",
|
|
1897
|
+
"country-code": "499"
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
"name": "Montserrat",
|
|
1901
|
+
"alpha-2": "MS",
|
|
1902
|
+
"alpha-3": "MSR",
|
|
1903
|
+
"country-code": "500"
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
"name": "Morocco",
|
|
1907
|
+
"alpha-2": "MA",
|
|
1908
|
+
"alpha-3": "MAR",
|
|
1909
|
+
"country-code": "504"
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
"name": "Mozambique",
|
|
1913
|
+
"alpha-2": "MZ",
|
|
1914
|
+
"alpha-3": "MOZ",
|
|
1915
|
+
"country-code": "508"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
"name": "Myanmar",
|
|
1919
|
+
"alpha-2": "MM",
|
|
1920
|
+
"alpha-3": "MMR",
|
|
1921
|
+
"country-code": "104"
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
"name": "Namibia",
|
|
1925
|
+
"alpha-2": "NA",
|
|
1926
|
+
"alpha-3": "NAM",
|
|
1927
|
+
"country-code": "516"
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
"name": "Nauru",
|
|
1931
|
+
"alpha-2": "NR",
|
|
1932
|
+
"alpha-3": "NRU",
|
|
1933
|
+
"country-code": "520"
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
"name": "Nepal",
|
|
1937
|
+
"alpha-2": "NP",
|
|
1938
|
+
"alpha-3": "NPL",
|
|
1939
|
+
"country-code": "524"
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
"name": "Netherlands",
|
|
1943
|
+
"alpha-2": "NL",
|
|
1944
|
+
"alpha-3": "NLD",
|
|
1945
|
+
"country-code": "528"
|
|
1946
|
+
},
|
|
1947
|
+
{
|
|
1948
|
+
"name": "New Caledonia",
|
|
1949
|
+
"alpha-2": "NC",
|
|
1950
|
+
"alpha-3": "NCL",
|
|
1951
|
+
"country-code": "540"
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
"name": "New Zealand",
|
|
1955
|
+
"alpha-2": "NZ",
|
|
1956
|
+
"alpha-3": "NZL",
|
|
1957
|
+
"country-code": "554"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
"name": "Nicaragua",
|
|
1961
|
+
"alpha-2": "NI",
|
|
1962
|
+
"alpha-3": "NIC",
|
|
1963
|
+
"country-code": "558"
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
"name": "Niger",
|
|
1967
|
+
"alpha-2": "NE",
|
|
1968
|
+
"alpha-3": "NER",
|
|
1969
|
+
"country-code": "562"
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
"name": "Nigeria",
|
|
1973
|
+
"alpha-2": "NG",
|
|
1974
|
+
"alpha-3": "NGA",
|
|
1975
|
+
"country-code": "566"
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
"name": "Niue",
|
|
1979
|
+
"alpha-2": "NU",
|
|
1980
|
+
"alpha-3": "NIU",
|
|
1981
|
+
"country-code": "570"
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
"name": "Norfolk Island",
|
|
1985
|
+
"alpha-2": "NF",
|
|
1986
|
+
"alpha-3": "NFK",
|
|
1987
|
+
"country-code": "574"
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
"name": "North Macedonia",
|
|
1991
|
+
"alpha-2": "MK",
|
|
1992
|
+
"alpha-3": "MKD",
|
|
1993
|
+
"country-code": "807"
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
"name": "Northern Mariana Islands",
|
|
1997
|
+
"alpha-2": "MP",
|
|
1998
|
+
"alpha-3": "MNP",
|
|
1999
|
+
"country-code": "580"
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
"name": "Norway",
|
|
2003
|
+
"alpha-2": "NO",
|
|
2004
|
+
"alpha-3": "NOR",
|
|
2005
|
+
"country-code": "578"
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
"name": "Oman",
|
|
2009
|
+
"alpha-2": "OM",
|
|
2010
|
+
"alpha-3": "OMN",
|
|
2011
|
+
"country-code": "512"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"name": "Pakistan",
|
|
2015
|
+
"alpha-2": "PK",
|
|
2016
|
+
"alpha-3": "PAK",
|
|
2017
|
+
"country-code": "586"
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
"name": "Palau",
|
|
2021
|
+
"alpha-2": "PW",
|
|
2022
|
+
"alpha-3": "PLW",
|
|
2023
|
+
"country-code": "585"
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
"name": "Palestine, State of",
|
|
2027
|
+
"alpha-2": "PS",
|
|
2028
|
+
"alpha-3": "PSE",
|
|
2029
|
+
"country-code": "275"
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
"name": "Panama",
|
|
2033
|
+
"alpha-2": "PA",
|
|
2034
|
+
"alpha-3": "PAN",
|
|
2035
|
+
"country-code": "591"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
"name": "Papua New Guinea",
|
|
2039
|
+
"alpha-2": "PG",
|
|
2040
|
+
"alpha-3": "PNG",
|
|
2041
|
+
"country-code": "598"
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
"name": "Paraguay",
|
|
2045
|
+
"alpha-2": "PY",
|
|
2046
|
+
"alpha-3": "PRY",
|
|
2047
|
+
"country-code": "600"
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
"name": "Peru",
|
|
2051
|
+
"alpha-2": "PE",
|
|
2052
|
+
"alpha-3": "PER",
|
|
2053
|
+
"country-code": "604"
|
|
2054
|
+
},
|
|
2055
|
+
{
|
|
2056
|
+
"name": "Philippines",
|
|
2057
|
+
"alpha-2": "PH",
|
|
2058
|
+
"alpha-3": "PHL",
|
|
2059
|
+
"country-code": "608"
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
"name": "Pitcairn",
|
|
2063
|
+
"alpha-2": "PN",
|
|
2064
|
+
"alpha-3": "PCN",
|
|
2065
|
+
"country-code": "612"
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
"name": "Poland",
|
|
2069
|
+
"alpha-2": "PL",
|
|
2070
|
+
"alpha-3": "POL",
|
|
2071
|
+
"country-code": "616"
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
"name": "Portugal",
|
|
2075
|
+
"alpha-2": "PT",
|
|
2076
|
+
"alpha-3": "PRT",
|
|
2077
|
+
"country-code": "620"
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
"name": "Puerto Rico",
|
|
2081
|
+
"alpha-2": "PR",
|
|
2082
|
+
"alpha-3": "PRI",
|
|
2083
|
+
"country-code": "630"
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
"name": "Qatar",
|
|
2087
|
+
"alpha-2": "QA",
|
|
2088
|
+
"alpha-3": "QAT",
|
|
2089
|
+
"country-code": "634"
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
"name": "Réunion",
|
|
2093
|
+
"alpha-2": "RE",
|
|
2094
|
+
"alpha-3": "REU",
|
|
2095
|
+
"country-code": "638"
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
"name": "Romania",
|
|
2099
|
+
"alpha-2": "RO",
|
|
2100
|
+
"alpha-3": "ROU",
|
|
2101
|
+
"country-code": "642"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"name": "Russian Federation",
|
|
2105
|
+
"alpha-2": "RU",
|
|
2106
|
+
"alpha-3": "RUS",
|
|
2107
|
+
"country-code": "643"
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
"name": "Rwanda",
|
|
2111
|
+
"alpha-2": "RW",
|
|
2112
|
+
"alpha-3": "RWA",
|
|
2113
|
+
"country-code": "646"
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
"name": "Saint Barthélemy",
|
|
2117
|
+
"alpha-2": "BL",
|
|
2118
|
+
"alpha-3": "BLM",
|
|
2119
|
+
"country-code": "652"
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
"name": "Saint Helena, Ascension and Tristan da Cunha",
|
|
2123
|
+
"alpha-2": "SH",
|
|
2124
|
+
"alpha-3": "SHN",
|
|
2125
|
+
"country-code": "654"
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
"name": "Saint Kitts and Nevis",
|
|
2129
|
+
"alpha-2": "KN",
|
|
2130
|
+
"alpha-3": "KNA",
|
|
2131
|
+
"country-code": "659"
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
"name": "Saint Lucia",
|
|
2135
|
+
"alpha-2": "LC",
|
|
2136
|
+
"alpha-3": "LCA",
|
|
2137
|
+
"country-code": "662"
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
"name": "Saint Martin (French part)",
|
|
2141
|
+
"alpha-2": "MF",
|
|
2142
|
+
"alpha-3": "MAF",
|
|
2143
|
+
"country-code": "663"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
"name": "Saint Pierre and Miquelon",
|
|
2147
|
+
"alpha-2": "PM",
|
|
2148
|
+
"alpha-3": "SPM",
|
|
2149
|
+
"country-code": "666"
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
"name": "Saint Vincent and the Grenadines",
|
|
2153
|
+
"alpha-2": "VC",
|
|
2154
|
+
"alpha-3": "VCT",
|
|
2155
|
+
"country-code": "670"
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"name": "Samoa",
|
|
2159
|
+
"alpha-2": "WS",
|
|
2160
|
+
"alpha-3": "WSM",
|
|
2161
|
+
"country-code": "882"
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
"name": "San Marino",
|
|
2165
|
+
"alpha-2": "SM",
|
|
2166
|
+
"alpha-3": "SMR",
|
|
2167
|
+
"country-code": "674"
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
"name": "Sao Tome and Principe",
|
|
2171
|
+
"alpha-2": "ST",
|
|
2172
|
+
"alpha-3": "STP",
|
|
2173
|
+
"country-code": "678"
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
"name": "Saudi Arabia",
|
|
2177
|
+
"alpha-2": "SA",
|
|
2178
|
+
"alpha-3": "SAU",
|
|
2179
|
+
"country-code": "682"
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
"name": "Senegal",
|
|
2183
|
+
"alpha-2": "SN",
|
|
2184
|
+
"alpha-3": "SEN",
|
|
2185
|
+
"country-code": "686"
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
"name": "Serbia",
|
|
2189
|
+
"alpha-2": "RS",
|
|
2190
|
+
"alpha-3": "SRB",
|
|
2191
|
+
"country-code": "688"
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"name": "Seychelles",
|
|
2195
|
+
"alpha-2": "SC",
|
|
2196
|
+
"alpha-3": "SYC",
|
|
2197
|
+
"country-code": "690"
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
"name": "Sierra Leone",
|
|
2201
|
+
"alpha-2": "SL",
|
|
2202
|
+
"alpha-3": "SLE",
|
|
2203
|
+
"country-code": "694"
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
"name": "Singapore",
|
|
2207
|
+
"alpha-2": "SG",
|
|
2208
|
+
"alpha-3": "SGP",
|
|
2209
|
+
"country-code": "702"
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
"name": "Sint Maarten (Dutch part)",
|
|
2213
|
+
"alpha-2": "SX",
|
|
2214
|
+
"alpha-3": "SXM",
|
|
2215
|
+
"country-code": "534"
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
"name": "Slovakia",
|
|
2219
|
+
"alpha-2": "SK",
|
|
2220
|
+
"alpha-3": "SVK",
|
|
2221
|
+
"country-code": "703"
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
"name": "Slovenia",
|
|
2225
|
+
"alpha-2": "SI",
|
|
2226
|
+
"alpha-3": "SVN",
|
|
2227
|
+
"country-code": "705"
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
"name": "Solomon Islands",
|
|
2231
|
+
"alpha-2": "SB",
|
|
2232
|
+
"alpha-3": "SLB",
|
|
2233
|
+
"country-code": "090"
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
"name": "Somalia",
|
|
2237
|
+
"alpha-2": "SO",
|
|
2238
|
+
"alpha-3": "SOM",
|
|
2239
|
+
"country-code": "706"
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
"name": "South Africa",
|
|
2243
|
+
"alpha-2": "ZA",
|
|
2244
|
+
"alpha-3": "ZAF",
|
|
2245
|
+
"country-code": "710"
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
"name": "South Georgia and the South Sandwich Islands",
|
|
2249
|
+
"alpha-2": "GS",
|
|
2250
|
+
"alpha-3": "SGS",
|
|
2251
|
+
"country-code": "239"
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
"name": "South Sudan",
|
|
2255
|
+
"alpha-2": "SS",
|
|
2256
|
+
"alpha-3": "SSD",
|
|
2257
|
+
"country-code": "728"
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
"name": "Spain",
|
|
2261
|
+
"alpha-2": "ES",
|
|
2262
|
+
"alpha-3": "ESP",
|
|
2263
|
+
"country-code": "724"
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
"name": "Sri Lanka",
|
|
2267
|
+
"alpha-2": "LK",
|
|
2268
|
+
"alpha-3": "LKA",
|
|
2269
|
+
"country-code": "144"
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
"name": "Sudan",
|
|
2273
|
+
"alpha-2": "SD",
|
|
2274
|
+
"alpha-3": "SDN",
|
|
2275
|
+
"country-code": "729"
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
"name": "Suriname",
|
|
2279
|
+
"alpha-2": "SR",
|
|
2280
|
+
"alpha-3": "SUR",
|
|
2281
|
+
"country-code": "740"
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
"name": "Svalbard and Jan Mayen",
|
|
2285
|
+
"alpha-2": "SJ",
|
|
2286
|
+
"alpha-3": "SJM",
|
|
2287
|
+
"country-code": "744"
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
"name": "Sweden",
|
|
2291
|
+
"alpha-2": "SE",
|
|
2292
|
+
"alpha-3": "SWE",
|
|
2293
|
+
"country-code": "752"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
"name": "Switzerland",
|
|
2297
|
+
"alpha-2": "CH",
|
|
2298
|
+
"alpha-3": "CHE",
|
|
2299
|
+
"country-code": "756"
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
"name": "Syrian Arab Republic",
|
|
2303
|
+
"alpha-2": "SY",
|
|
2304
|
+
"alpha-3": "SYR",
|
|
2305
|
+
"country-code": "760"
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
"name": "Taiwan",
|
|
2309
|
+
"alpha-2": "TW",
|
|
2310
|
+
"alpha-3": "TWN",
|
|
2311
|
+
"country-code": "158"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
"name": "Tajikistan",
|
|
2315
|
+
"alpha-2": "TJ",
|
|
2316
|
+
"alpha-3": "TJK",
|
|
2317
|
+
"country-code": "762"
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
"name": "Tanzania, United Republic of",
|
|
2321
|
+
"alpha-2": "TZ",
|
|
2322
|
+
"alpha-3": "TZA",
|
|
2323
|
+
"country-code": "834"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
"name": "Thailand",
|
|
2327
|
+
"alpha-2": "TH",
|
|
2328
|
+
"alpha-3": "THA",
|
|
2329
|
+
"country-code": "764"
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
"name": "Timor-Leste",
|
|
2333
|
+
"alpha-2": "TL",
|
|
2334
|
+
"alpha-3": "TLS",
|
|
2335
|
+
"country-code": "626"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
"name": "Togo",
|
|
2339
|
+
"alpha-2": "TG",
|
|
2340
|
+
"alpha-3": "TGO",
|
|
2341
|
+
"country-code": "768"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
"name": "Tokelau",
|
|
2345
|
+
"alpha-2": "TK",
|
|
2346
|
+
"alpha-3": "TKL",
|
|
2347
|
+
"country-code": "772"
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
"name": "Tonga",
|
|
2351
|
+
"alpha-2": "TO",
|
|
2352
|
+
"alpha-3": "TON",
|
|
2353
|
+
"country-code": "776"
|
|
2354
|
+
},
|
|
2355
|
+
{
|
|
2356
|
+
"name": "Trinidad and Tobago",
|
|
2357
|
+
"alpha-2": "TT",
|
|
2358
|
+
"alpha-3": "TTO",
|
|
2359
|
+
"country-code": "780"
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
"name": "Tunisia",
|
|
2363
|
+
"alpha-2": "TN",
|
|
2364
|
+
"alpha-3": "TUN",
|
|
2365
|
+
"country-code": "788"
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
"name": "Turkey",
|
|
2369
|
+
"alpha-2": "TR",
|
|
2370
|
+
"alpha-3": "TUR",
|
|
2371
|
+
"country-code": "792"
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
"name": "Turkmenistan",
|
|
2375
|
+
"alpha-2": "TM",
|
|
2376
|
+
"alpha-3": "TKM",
|
|
2377
|
+
"country-code": "795"
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
"name": "Turks and Caicos Islands",
|
|
2381
|
+
"alpha-2": "TC",
|
|
2382
|
+
"alpha-3": "TCA",
|
|
2383
|
+
"country-code": "796"
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
"name": "Tuvalu",
|
|
2387
|
+
"alpha-2": "TV",
|
|
2388
|
+
"alpha-3": "TUV",
|
|
2389
|
+
"country-code": "798"
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
"name": "Uganda",
|
|
2393
|
+
"alpha-2": "UG",
|
|
2394
|
+
"alpha-3": "UGA",
|
|
2395
|
+
"country-code": "800"
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
"name": "Ukraine",
|
|
2399
|
+
"alpha-2": "UA",
|
|
2400
|
+
"alpha-3": "UKR",
|
|
2401
|
+
"country-code": "804"
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
"name": "United Arab Emirates",
|
|
2405
|
+
"alpha-2": "AE",
|
|
2406
|
+
"alpha-3": "ARE",
|
|
2407
|
+
"country-code": "784"
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
"name": "United Kingdom",
|
|
2411
|
+
"alpha-2": "GB",
|
|
2412
|
+
"alpha-3": "GBR",
|
|
2413
|
+
"country-code": "826"
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
"name": "America",
|
|
2417
|
+
"alpha-2": "US",
|
|
2418
|
+
"alpha-3": "USA",
|
|
2419
|
+
"country-code": "840"
|
|
2420
|
+
},
|
|
2421
|
+
{
|
|
2422
|
+
"name": "United States Minor Outlying Islands",
|
|
2423
|
+
"alpha-2": "UM",
|
|
2424
|
+
"alpha-3": "UMI",
|
|
2425
|
+
"country-code": "581"
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
"name": "Uruguay",
|
|
2429
|
+
"alpha-2": "UY",
|
|
2430
|
+
"alpha-3": "URY",
|
|
2431
|
+
"country-code": "858"
|
|
2432
|
+
},
|
|
2433
|
+
{
|
|
2434
|
+
"name": "Uzbekistan",
|
|
2435
|
+
"alpha-2": "UZ",
|
|
2436
|
+
"alpha-3": "UZB",
|
|
2437
|
+
"country-code": "860"
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
"name": "Vanuatu",
|
|
2441
|
+
"alpha-2": "VU",
|
|
2442
|
+
"alpha-3": "VUT",
|
|
2443
|
+
"country-code": "548"
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
"name": "Venezuela (Bolivarian Republic of)",
|
|
2447
|
+
"alpha-2": "VE",
|
|
2448
|
+
"alpha-3": "VEN",
|
|
2449
|
+
"country-code": "862"
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
"name": "Vietnam",
|
|
2453
|
+
"alpha-2": "VN",
|
|
2454
|
+
"alpha-3": "VNM",
|
|
2455
|
+
"country-code": "704"
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
"name": "Virgin Islands (British)",
|
|
2459
|
+
"alpha-2": "VG",
|
|
2460
|
+
"alpha-3": "VGB",
|
|
2461
|
+
"country-code": "092"
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
"name": "Virgin Islands (U.S.)",
|
|
2465
|
+
"alpha-2": "VI",
|
|
2466
|
+
"alpha-3": "VIR",
|
|
2467
|
+
"country-code": "850"
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
"name": "Wallis and Futuna",
|
|
2471
|
+
"alpha-2": "WF",
|
|
2472
|
+
"alpha-3": "WLF",
|
|
2473
|
+
"country-code": "876"
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
"name": "Western Sahara",
|
|
2477
|
+
"alpha-2": "EH",
|
|
2478
|
+
"alpha-3": "ESH",
|
|
2479
|
+
"country-code": "732"
|
|
2480
|
+
},
|
|
2481
|
+
{
|
|
2482
|
+
"name": "Yemen",
|
|
2483
|
+
"alpha-2": "YE",
|
|
2484
|
+
"alpha-3": "YEM",
|
|
2485
|
+
"country-code": "887"
|
|
2486
|
+
},
|
|
2487
|
+
{
|
|
2488
|
+
"name": "Zambia",
|
|
2489
|
+
"alpha-2": "ZM",
|
|
2490
|
+
"alpha-3": "ZMB",
|
|
2491
|
+
"country-code": "894"
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
"name": "Zimbabwe",
|
|
2495
|
+
"alpha-2": "ZW",
|
|
2496
|
+
"alpha-3": "ZWE",
|
|
2497
|
+
"country-code": "716"
|
|
2498
|
+
}
|
|
2499
|
+
];
|
|
2500
|
+
|
|
2501
|
+
//#endregion
|
|
2502
|
+
//#region src/api/search-analytics/breakdowns.ts
|
|
2503
|
+
/**
|
|
2504
|
+
* Fetches device breakdown (desktop, mobile, tablet) with period comparison.
|
|
2505
|
+
*/
|
|
2506
|
+
async function fetchDevicesWithComparison(client, siteUrl, options = {}) {
|
|
2507
|
+
const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2508
|
+
...createQueryBody(options),
|
|
2509
|
+
dimensions: ["device"]
|
|
2510
|
+
}).then((res) => {
|
|
2511
|
+
return (res.rows || []).map((row) => {
|
|
2512
|
+
return {
|
|
2513
|
+
...row,
|
|
2514
|
+
dimension: "device",
|
|
2515
|
+
device: row.keys?.[0] || "unknown",
|
|
2516
|
+
keys: null,
|
|
2517
|
+
clicks: Number(row.clicks) || 0,
|
|
2518
|
+
impressions: Number(row.impressions) || 0,
|
|
2519
|
+
ctr: Number(row.ctr) || 0,
|
|
2520
|
+
position: Number(row.position) || 0
|
|
2521
|
+
};
|
|
2522
|
+
});
|
|
2523
|
+
}), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
2524
|
+
...createQueryBody({
|
|
2525
|
+
...options,
|
|
2526
|
+
period: options.prevPeriod
|
|
2527
|
+
}),
|
|
2528
|
+
dimensions: ["device"]
|
|
2529
|
+
}).then((res) => {
|
|
2530
|
+
return (res.rows || []).map((row) => {
|
|
2531
|
+
return {
|
|
2532
|
+
...row,
|
|
2533
|
+
dimension: "device",
|
|
2534
|
+
device: row.keys?.[0] || "unknown",
|
|
2535
|
+
keys: null,
|
|
2536
|
+
clicks: Number(row.clicks) || 0,
|
|
2537
|
+
impressions: Number(row.impressions) || 0,
|
|
2538
|
+
ctr: Number(row.ctr) || 0,
|
|
2539
|
+
position: Number(row.position) || 0
|
|
2540
|
+
};
|
|
2541
|
+
});
|
|
2542
|
+
}) : Promise.resolve([])]);
|
|
2543
|
+
return {
|
|
2544
|
+
current,
|
|
2545
|
+
previous,
|
|
2546
|
+
metadata: {
|
|
2547
|
+
currentCount: current.length,
|
|
2548
|
+
previousCount: previous.length
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Fetches device breakdown (desktop, mobile, tablet).
|
|
2554
|
+
*/
|
|
2555
|
+
async function fetchDevices(client, siteUrl, options = {}) {
|
|
2556
|
+
return client.searchAnalytics.query(siteUrl, {
|
|
2557
|
+
...createQueryBody(options),
|
|
2558
|
+
dimensions: ["device"]
|
|
2559
|
+
}).then((res) => {
|
|
2560
|
+
return (res.rows || []).map((row) => {
|
|
2561
|
+
return {
|
|
2562
|
+
...row,
|
|
2563
|
+
dimension: "device",
|
|
2564
|
+
device: row.keys?.[0] || "unknown",
|
|
2565
|
+
keys: null,
|
|
2566
|
+
clicks: Number(row.clicks) || 0,
|
|
2567
|
+
impressions: Number(row.impressions) || 0,
|
|
2568
|
+
ctr: Number(row.ctr) || 0,
|
|
2569
|
+
position: Number(row.position) || 0
|
|
2570
|
+
};
|
|
2571
|
+
});
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
function fixCountryRows(res) {
|
|
2575
|
+
return (res.rows || []).map((row) => {
|
|
2576
|
+
const alpha3Code = row.keys?.[0] || "";
|
|
2577
|
+
const country$1 = countries_default.find((c) => c["alpha-3"].toLowerCase() === alpha3Code);
|
|
2578
|
+
return {
|
|
2579
|
+
...row,
|
|
2580
|
+
dimension: "country",
|
|
2581
|
+
countryCodeGsc: alpha3Code,
|
|
2582
|
+
country: country$1?.name || alpha3Code,
|
|
2583
|
+
countryCode: country$1?.["alpha-2"] || alpha3Code,
|
|
2584
|
+
keys: null
|
|
2585
|
+
};
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Fetches top countries by traffic with period comparison and keyword counts per country.
|
|
2590
|
+
*/
|
|
2591
|
+
async function fetchCountriesWithComparison(client, siteUrl, options = {}) {
|
|
2592
|
+
const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2593
|
+
...createQueryBody(options),
|
|
2594
|
+
dimensions: ["country"],
|
|
2595
|
+
rowLimit: 5
|
|
2596
|
+
}).then(fixCountryRows), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
2597
|
+
...createQueryBody({
|
|
2598
|
+
...options,
|
|
2599
|
+
period: options.prevPeriod
|
|
2600
|
+
}),
|
|
2601
|
+
dimensions: ["country"],
|
|
2602
|
+
rowLimit: 5
|
|
2603
|
+
}).then(fixCountryRows) : Promise.resolve([])]);
|
|
2604
|
+
const keywordCounts = await Promise.all(current.map((row) => {
|
|
2605
|
+
return client.searchAnalytics.query(siteUrl, {
|
|
2606
|
+
...createQueryBody({
|
|
2607
|
+
...options,
|
|
2608
|
+
filters: [{
|
|
2609
|
+
dimension: "country",
|
|
2610
|
+
operator: "equals",
|
|
2611
|
+
expression: row.countryCodeGsc
|
|
2612
|
+
}]
|
|
2613
|
+
}),
|
|
2614
|
+
dimensions: ["query"]
|
|
2615
|
+
}).then((res) => ({
|
|
2616
|
+
countryCodeGsc: row.countryCodeGsc,
|
|
2617
|
+
keywords: res.rows?.length || 0
|
|
2618
|
+
})).catch(() => ({
|
|
2619
|
+
countryCodeGsc: row.countryCodeGsc,
|
|
2620
|
+
keywords: 0
|
|
2621
|
+
}));
|
|
2622
|
+
}));
|
|
2623
|
+
for (const row of current) {
|
|
2624
|
+
const keywordCount = keywordCounts.find((k) => k.countryCodeGsc === row.countryCodeGsc);
|
|
2625
|
+
if (keywordCount) row.keywords = keywordCount.keywords;
|
|
2626
|
+
}
|
|
2627
|
+
return {
|
|
2628
|
+
current,
|
|
2629
|
+
previous,
|
|
2630
|
+
metadata: {
|
|
2631
|
+
currentCount: current.length,
|
|
2632
|
+
previousCount: previous.length,
|
|
2633
|
+
totalKeywords: keywordCounts.reduce((sum, k) => sum + k.keywords, 0)
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Fetches top countries by traffic.
|
|
2639
|
+
*/
|
|
2640
|
+
async function fetchCountries(client, siteUrl, options = {}) {
|
|
2641
|
+
return client.searchAnalytics.query(siteUrl, {
|
|
2642
|
+
...createQueryBody(options),
|
|
2643
|
+
dimensions: ["country"],
|
|
2644
|
+
rowLimit: 5
|
|
2645
|
+
}).then(fixCountryRows);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Fetches search appearance breakdown (AMP, rich results, etc.) with period comparison.
|
|
2649
|
+
*/
|
|
2650
|
+
async function fetchSearchAppearanceWithComparison(client, siteUrl, options = {}) {
|
|
2651
|
+
const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2652
|
+
...createQueryBody(options),
|
|
2653
|
+
dimensions: ["searchAppearance"]
|
|
2654
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2655
|
+
...row,
|
|
2656
|
+
dimension: "searchAppearance",
|
|
2657
|
+
searchAppearance: row.keys?.[0] || "unknown",
|
|
2658
|
+
keys: null,
|
|
2659
|
+
clicks: Number(row.clicks) || 0,
|
|
2660
|
+
impressions: Number(row.impressions) || 0,
|
|
2661
|
+
ctr: Number(row.ctr) || 0,
|
|
2662
|
+
position: Number(row.position) || 0
|
|
2663
|
+
}))), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
2664
|
+
...createQueryBody({
|
|
2665
|
+
...options,
|
|
2666
|
+
period: options.prevPeriod
|
|
2667
|
+
}),
|
|
2668
|
+
dimensions: ["searchAppearance"]
|
|
2669
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2670
|
+
...row,
|
|
2671
|
+
dimension: "searchAppearance",
|
|
2672
|
+
searchAppearance: row.keys?.[0] || "unknown",
|
|
2673
|
+
keys: null,
|
|
2674
|
+
clicks: Number(row.clicks) || 0,
|
|
2675
|
+
impressions: Number(row.impressions) || 0,
|
|
2676
|
+
ctr: Number(row.ctr) || 0,
|
|
2677
|
+
position: Number(row.position) || 0
|
|
2678
|
+
}))) : Promise.resolve([])]);
|
|
2679
|
+
return {
|
|
2680
|
+
current,
|
|
2681
|
+
previous,
|
|
2682
|
+
metadata: {
|
|
2683
|
+
currentCount: current.length,
|
|
2684
|
+
previousCount: previous.length
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Fetches search appearance breakdown (AMP, rich results, etc.).
|
|
2690
|
+
*/
|
|
2691
|
+
async function fetchSearchAppearance(client, siteUrl, options = {}) {
|
|
2692
|
+
return client.searchAnalytics.query(siteUrl, {
|
|
2693
|
+
...createQueryBody(options),
|
|
2694
|
+
dimensions: ["searchAppearance"]
|
|
2695
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2696
|
+
...row,
|
|
2697
|
+
dimension: "searchAppearance",
|
|
2698
|
+
searchAppearance: row.keys?.[0] || "unknown",
|
|
2699
|
+
keys: null,
|
|
2700
|
+
clicks: Number(row.clicks) || 0,
|
|
2701
|
+
impressions: Number(row.impressions) || 0,
|
|
2702
|
+
ctr: Number(row.ctr) || 0,
|
|
2703
|
+
position: Number(row.position) || 0
|
|
2704
|
+
})));
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
//#endregion
|
|
2708
|
+
//#region src/api/search-analytics/dates.ts
|
|
2709
|
+
function computeTotals(rows) {
|
|
2710
|
+
if (!rows.length) return {
|
|
2711
|
+
clicks: 0,
|
|
2712
|
+
impressions: 0,
|
|
2713
|
+
ctr: 0,
|
|
2714
|
+
position: 0
|
|
2715
|
+
};
|
|
2716
|
+
return {
|
|
2717
|
+
clicks: rows.reduce((sum, r) => sum + (r.clicks || 0), 0),
|
|
2718
|
+
impressions: rows.reduce((sum, r) => sum + (r.impressions || 0), 0),
|
|
2719
|
+
ctr: rows.reduce((sum, r) => sum + (r.ctr || 0), 0) / rows.length,
|
|
2720
|
+
position: rows.reduce((sum, r) => sum + (r.position || 0), 0) / rows.length
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* Fetches daily search analytics data with period-over-period comparison.
|
|
2725
|
+
*/
|
|
2726
|
+
async function fetchDatesWithComparison(client, siteUrl, options = {}) {
|
|
2727
|
+
const [current, previous] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2728
|
+
...createQueryBody({ ...options }),
|
|
2729
|
+
dimensions: ["date"]
|
|
2730
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2731
|
+
...row,
|
|
2732
|
+
dimension: "date",
|
|
2733
|
+
date: row.keys?.[0] || "",
|
|
2734
|
+
keys: null
|
|
2735
|
+
}))), options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
2736
|
+
...createQueryBody({
|
|
2737
|
+
...options,
|
|
2738
|
+
period: options.prevPeriod
|
|
2739
|
+
}),
|
|
2740
|
+
dimensions: ["date"]
|
|
2741
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2742
|
+
...row,
|
|
2743
|
+
dimension: "date",
|
|
2744
|
+
date: row.keys?.[0] || "",
|
|
2745
|
+
keys: null
|
|
2746
|
+
}))) : Promise.resolve([])]);
|
|
2747
|
+
const currentTotals = computeTotals(current);
|
|
2748
|
+
const previousTotals = computeTotals(previous);
|
|
2749
|
+
return {
|
|
2750
|
+
current,
|
|
2751
|
+
previous,
|
|
2752
|
+
metadata: {
|
|
2753
|
+
currentCount: current.length,
|
|
2754
|
+
previousCount: previous.length,
|
|
2755
|
+
totals: {
|
|
2756
|
+
current: currentTotals,
|
|
2757
|
+
previous: previousTotals,
|
|
2758
|
+
clicksPercent: percentDifference(currentTotals.clicks, previousTotals.clicks),
|
|
2759
|
+
impressionsPercent: percentDifference(currentTotals.impressions, previousTotals.impressions),
|
|
2760
|
+
ctrPercent: percentDifference(currentTotals.ctr, previousTotals.ctr),
|
|
2761
|
+
positionPercent: percentDifference(currentTotals.position, previousTotals.position)
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Fetches year-over-year comparison for site metrics.
|
|
2768
|
+
* GSC has ~16 months history, so works for periods up to ~4 months.
|
|
2769
|
+
*/
|
|
2770
|
+
async function fetchYoYComparison(client, siteUrl, options = {}) {
|
|
2771
|
+
const { period = {
|
|
2772
|
+
start: dayjs().subtract(28, "day").toDate(),
|
|
2773
|
+
end: dayjs().toDate()
|
|
2774
|
+
} } = options;
|
|
2775
|
+
const startDate = dayjs(period.start);
|
|
2776
|
+
const endDate = dayjs(period.end);
|
|
2777
|
+
const periodDays = endDate.diff(startDate, "day");
|
|
2778
|
+
const prevStart = startDate.subtract(1, "year");
|
|
2779
|
+
const prevEnd = endDate.subtract(1, "year");
|
|
2780
|
+
const withinLimit = dayjs().diff(prevStart, "day") <= 480;
|
|
2781
|
+
const [currentRes, previousRes] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2782
|
+
...createQueryBody({ period }),
|
|
2783
|
+
dimensions: ["date"]
|
|
2784
|
+
}), withinLimit ? client.searchAnalytics.query(siteUrl, {
|
|
2785
|
+
startDate: formatDateGsc(prevStart.toDate()) || "",
|
|
2786
|
+
endDate: formatDateGsc(prevEnd.toDate()) || "",
|
|
2787
|
+
dimensions: ["date"],
|
|
2788
|
+
rowLimit: 25e3
|
|
2789
|
+
}) : Promise.resolve({ rows: [] })]);
|
|
2790
|
+
const currentRows = currentRes.rows || [];
|
|
2791
|
+
const previousRows = previousRes.rows || [];
|
|
2792
|
+
const sumMetrics = (rows) => {
|
|
2793
|
+
if (!rows.length) return {
|
|
2794
|
+
clicks: 0,
|
|
2795
|
+
impressions: 0,
|
|
2796
|
+
ctr: 0,
|
|
2797
|
+
position: 0
|
|
2798
|
+
};
|
|
2799
|
+
return {
|
|
2800
|
+
clicks: rows.reduce((sum, r) => sum + (r.clicks || 0), 0),
|
|
2801
|
+
impressions: rows.reduce((sum, r) => sum + (r.impressions || 0), 0),
|
|
2802
|
+
ctr: rows.reduce((sum, r) => sum + (r.ctr || 0), 0) / rows.length,
|
|
2803
|
+
position: rows.reduce((sum, r) => sum + (r.position || 0), 0) / rows.length
|
|
2804
|
+
};
|
|
2805
|
+
};
|
|
2806
|
+
const current = sumMetrics(currentRows);
|
|
2807
|
+
const previous = sumMetrics(previousRows);
|
|
2808
|
+
return {
|
|
2809
|
+
current,
|
|
2810
|
+
previous,
|
|
2811
|
+
change: {
|
|
2812
|
+
clicks: current.clicks - previous.clicks,
|
|
2813
|
+
clicksPercent: percentDifference(current.clicks, previous.clicks),
|
|
2814
|
+
impressions: current.impressions - previous.impressions,
|
|
2815
|
+
impressionsPercent: percentDifference(current.impressions, previous.impressions),
|
|
2816
|
+
ctr: current.ctr - previous.ctr,
|
|
2817
|
+
ctrPercent: percentDifference(current.ctr, previous.ctr),
|
|
2818
|
+
position: current.position - previous.position,
|
|
2819
|
+
positionPercent: percentDifference(current.position, previous.position)
|
|
2820
|
+
},
|
|
2821
|
+
periodDays,
|
|
2822
|
+
withinLimit
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Fetches daily search analytics data for a site.
|
|
2827
|
+
*/
|
|
2828
|
+
async function fetchDates(client, siteUrl, options = {}) {
|
|
2829
|
+
return await client.searchAnalytics.query(siteUrl, {
|
|
2830
|
+
...createQueryBody(options),
|
|
2831
|
+
dimensions: ["date"]
|
|
2832
|
+
}).then((res) => {
|
|
2833
|
+
return (res.rows || []).map((row) => {
|
|
2834
|
+
return {
|
|
2835
|
+
...row,
|
|
2836
|
+
dimension: "date",
|
|
2837
|
+
date: row.keys?.[0] || "",
|
|
2838
|
+
keys: null
|
|
2839
|
+
};
|
|
2840
|
+
});
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
//#endregion
|
|
2845
|
+
//#region src/api/search-analytics/utils.ts
|
|
2846
|
+
function normalizePagePath(page$1, domain) {
|
|
2847
|
+
if (!page$1) return page$1;
|
|
2848
|
+
return page$1.replace("https://", "").replace(domain, "");
|
|
2849
|
+
}
|
|
2850
|
+
function extractDomain(siteUrl) {
|
|
2851
|
+
return siteUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
2852
|
+
}
|
|
2853
|
+
function formatPageForQuery(page$1, siteUrl) {
|
|
2854
|
+
let p = withBase(page$1, siteUrl);
|
|
2855
|
+
if (p === extractDomain(siteUrl)) p = `${p}/`;
|
|
2856
|
+
return withHttps(p);
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
//#endregion
|
|
2860
|
+
//#region src/api/search-analytics/keywords.ts
|
|
2861
|
+
/**
|
|
2862
|
+
* Fetches keyword/query performance data with period comparison and associated pages.
|
|
2863
|
+
*/
|
|
2864
|
+
async function fetchKeywordsWithComparison(client, siteUrl, options = {}) {
|
|
2865
|
+
const [currentKeywords, previousKeywords, pages] = await Promise.all([
|
|
2866
|
+
client.searchAnalytics.query(siteUrl, {
|
|
2867
|
+
...createQueryBody(options),
|
|
2868
|
+
dimensions: ["query"]
|
|
2869
|
+
}).then((res) => res.rows || []),
|
|
2870
|
+
options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
2871
|
+
...createQueryBody({
|
|
2872
|
+
...options,
|
|
2873
|
+
period: options.prevPeriod
|
|
2874
|
+
}),
|
|
2875
|
+
dimensions: ["query"]
|
|
2876
|
+
}).then((res) => res.rows || []) : Promise.resolve([]),
|
|
2877
|
+
client.searchAnalytics.query(siteUrl, {
|
|
2878
|
+
...createQueryBody(options),
|
|
2879
|
+
dimensions: ["page", "query"]
|
|
2880
|
+
}).then((res) => res.rows || [])
|
|
2881
|
+
]);
|
|
2882
|
+
const current = currentKeywords.map((row) => {
|
|
2883
|
+
const prevRow = previousKeywords.find((r) => r.keys?.[0] === row.keys?.[0]);
|
|
2884
|
+
const pageMatch = pages.find((r) => r.keys?.[1] === row.keys?.[0]);
|
|
2885
|
+
const position = row.position ?? 0;
|
|
2886
|
+
const ctr = row.ctr ?? 0;
|
|
2887
|
+
const prevPosition = prevRow?.position ?? 0;
|
|
2888
|
+
const prevCtr = prevRow?.ctr ?? 0;
|
|
2889
|
+
return {
|
|
2890
|
+
...row,
|
|
2891
|
+
dimension: "query",
|
|
2892
|
+
keyword: row.keys?.[0] || "",
|
|
2893
|
+
page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
|
|
2894
|
+
position,
|
|
2895
|
+
positionPercent: percentDifference(position, prevPosition),
|
|
2896
|
+
prevPosition,
|
|
2897
|
+
ctr,
|
|
2898
|
+
ctrPercent: percentDifference(ctr, prevCtr),
|
|
2899
|
+
prevCtr,
|
|
2900
|
+
clicks: row.clicks ?? 0,
|
|
2901
|
+
impressions: row.impressions ?? 0,
|
|
2902
|
+
keys: null
|
|
2903
|
+
};
|
|
2904
|
+
});
|
|
2905
|
+
const previous = previousKeywords.map((prevRow) => {
|
|
2906
|
+
const keywordKey = prevRow.keys?.[0] || "";
|
|
2907
|
+
const currentRow = currentKeywords.find((r) => r.keys?.[0] === keywordKey);
|
|
2908
|
+
const pageMatch = pages.find((r) => r.keys?.[1] === keywordKey);
|
|
2909
|
+
if (!currentRow) return {
|
|
2910
|
+
...prevRow,
|
|
2911
|
+
dimension: "query",
|
|
2912
|
+
keyword: keywordKey,
|
|
2913
|
+
page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
|
|
2914
|
+
lost: true,
|
|
2915
|
+
clicks: 0,
|
|
2916
|
+
position: 0,
|
|
2917
|
+
ctr: 0,
|
|
2918
|
+
prevCtr: prevRow.ctr ?? 0,
|
|
2919
|
+
prevPosition: prevRow.position ?? 0,
|
|
2920
|
+
prevClicks: prevRow.clicks ?? 0,
|
|
2921
|
+
prevImpressions: prevRow.impressions ?? 0,
|
|
2922
|
+
keys: null
|
|
2923
|
+
};
|
|
2924
|
+
return {
|
|
2925
|
+
...prevRow,
|
|
2926
|
+
dimension: "query",
|
|
2927
|
+
keyword: keywordKey,
|
|
2928
|
+
page: pageMatch?.keys?.[0] ? normalizePagePath(pageMatch.keys[0], siteUrl) : null,
|
|
2929
|
+
keys: null
|
|
2930
|
+
};
|
|
2931
|
+
});
|
|
2932
|
+
return {
|
|
2933
|
+
current,
|
|
2934
|
+
previous,
|
|
2935
|
+
metadata: {
|
|
2936
|
+
currentCount: current.length,
|
|
2937
|
+
previousCount: previous.length,
|
|
2938
|
+
pageMatches: pages.length
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Fetches all keywords with their performance data using recursive pagination.
|
|
2944
|
+
*/
|
|
2945
|
+
async function fetchKeywords(client, siteUrl, options = {}) {
|
|
2946
|
+
const { rows } = await queryRecursive(client, siteUrl, {
|
|
2947
|
+
...createQueryBody(options),
|
|
2948
|
+
dimensions: ["query"]
|
|
2949
|
+
});
|
|
2950
|
+
return rows.map((row) => ({
|
|
2951
|
+
...row,
|
|
2952
|
+
dimension: "query",
|
|
2953
|
+
keyword: row.keys?.[0] || "",
|
|
2954
|
+
keys: null
|
|
2955
|
+
}));
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Fetches detailed data for a specific keyword including daily trends and top pages.
|
|
2959
|
+
*/
|
|
2960
|
+
async function fetchKeyword(client, siteUrl, keyword, options = {}) {
|
|
2961
|
+
const { rowLimit = 5, ...queryOptions } = options;
|
|
2962
|
+
const [dates, pages] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
2963
|
+
...createQueryBody({
|
|
2964
|
+
...queryOptions,
|
|
2965
|
+
filters: [{
|
|
2966
|
+
dimension: "query",
|
|
2967
|
+
operator: "equals",
|
|
2968
|
+
expression: keyword
|
|
2969
|
+
}]
|
|
2970
|
+
}),
|
|
2971
|
+
dimensions: ["date"]
|
|
2972
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2973
|
+
...row,
|
|
2974
|
+
date: row.keys?.[0] ?? "",
|
|
2975
|
+
keys: null
|
|
2976
|
+
}))), client.searchAnalytics.query(siteUrl, {
|
|
2977
|
+
...createQueryBody({
|
|
2978
|
+
...queryOptions,
|
|
2979
|
+
filters: [{
|
|
2980
|
+
dimension: "query",
|
|
2981
|
+
operator: "equals",
|
|
2982
|
+
expression: keyword
|
|
2983
|
+
}]
|
|
2984
|
+
}),
|
|
2985
|
+
rowLimit,
|
|
2986
|
+
dimensions: ["page"]
|
|
2987
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
2988
|
+
...row,
|
|
2989
|
+
page: normalizePagePath(row.keys?.[0] || "", extractDomain(siteUrl)),
|
|
2990
|
+
keys: null
|
|
2991
|
+
})))]);
|
|
2992
|
+
return {
|
|
2993
|
+
dates,
|
|
2994
|
+
pages
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
//#endregion
|
|
2999
|
+
//#region src/api/search-analytics/pages.ts
|
|
3000
|
+
/**
|
|
3001
|
+
* Fetches all pages with their performance data using recursive pagination.
|
|
3002
|
+
*/
|
|
3003
|
+
async function fetchPages(client, siteUrl, options = {}) {
|
|
3004
|
+
return (await queryRecursive(client, siteUrl, {
|
|
3005
|
+
...createQueryBody(options),
|
|
3006
|
+
dimensions: ["page"]
|
|
3007
|
+
}).then((d) => d.rows)).map((row) => ({
|
|
3008
|
+
...row,
|
|
3009
|
+
page: row.keys?.[0] || ""
|
|
3010
|
+
}));
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Fetches page performance data with period comparison, including top keyword per page.
|
|
3014
|
+
*/
|
|
3015
|
+
async function fetchPagesWithComparison(client, siteUrl, options = {}) {
|
|
3016
|
+
const [currentPages, previousPages, keywords] = await Promise.all([
|
|
3017
|
+
client.searchAnalytics.query(siteUrl, {
|
|
3018
|
+
...createQueryBody(options),
|
|
3019
|
+
dimensions: ["page"]
|
|
3020
|
+
}).then((res) => res.rows || []),
|
|
3021
|
+
options.prevPeriod ? client.searchAnalytics.query(siteUrl, {
|
|
3022
|
+
...createQueryBody({
|
|
3023
|
+
...options,
|
|
3024
|
+
period: options.prevPeriod
|
|
3025
|
+
}),
|
|
3026
|
+
dimensions: ["page"]
|
|
3027
|
+
}).then((res) => res.rows || []) : Promise.resolve([]),
|
|
3028
|
+
client.searchAnalytics.query(siteUrl, {
|
|
3029
|
+
...createQueryBody(options),
|
|
3030
|
+
dimensions: ["query", "page"]
|
|
3031
|
+
}).then((res) => res.rows || [])
|
|
3032
|
+
]);
|
|
3033
|
+
const current = currentPages.map((row) => {
|
|
3034
|
+
const prevRow = previousPages.find((r) => r.keys?.[0] === row.keys?.[0]);
|
|
3035
|
+
const keyword = keywords.find((r) => r.keys?.[1] === row.keys?.[0]);
|
|
3036
|
+
const clicks = row.clicks ?? 0;
|
|
3037
|
+
const impressions = row.impressions ?? 0;
|
|
3038
|
+
const prevClicks = prevRow?.clicks ?? 0;
|
|
3039
|
+
const prevImpressions = prevRow?.impressions ?? 0;
|
|
3040
|
+
return {
|
|
3041
|
+
...row,
|
|
3042
|
+
dimension: "page",
|
|
3043
|
+
page: row.keys?.[0] || "",
|
|
3044
|
+
keyword: keyword?.keys?.[0] || void 0,
|
|
3045
|
+
keywordPosition: keyword?.position ?? 0,
|
|
3046
|
+
clicks,
|
|
3047
|
+
prevClicks,
|
|
3048
|
+
clicksPercent: percentDifference(clicks, prevClicks),
|
|
3049
|
+
impressions,
|
|
3050
|
+
impressionsPercent: percentDifference(impressions, prevImpressions),
|
|
3051
|
+
prevImpressions,
|
|
3052
|
+
keys: null
|
|
3053
|
+
};
|
|
3054
|
+
});
|
|
3055
|
+
const previous = previousPages.map((prevRow) => {
|
|
3056
|
+
const pageKey = prevRow.keys?.[0] || "";
|
|
3057
|
+
if (!currentPages.find((r) => r.keys?.[0] === pageKey)) return {
|
|
3058
|
+
...prevRow,
|
|
3059
|
+
dimension: "page",
|
|
3060
|
+
page: pageKey,
|
|
3061
|
+
lost: true,
|
|
3062
|
+
clicks: 0,
|
|
3063
|
+
impressions: 0,
|
|
3064
|
+
prevClicks: prevRow.clicks ?? 0,
|
|
3065
|
+
prevImpressions: prevRow.impressions ?? 0,
|
|
3066
|
+
keys: null
|
|
3067
|
+
};
|
|
3068
|
+
return {
|
|
3069
|
+
...prevRow,
|
|
3070
|
+
dimension: "page",
|
|
3071
|
+
page: pageKey,
|
|
3072
|
+
keys: null
|
|
3073
|
+
};
|
|
3074
|
+
});
|
|
3075
|
+
return {
|
|
3076
|
+
current,
|
|
3077
|
+
previous,
|
|
3078
|
+
metadata: {
|
|
3079
|
+
currentCount: current.length,
|
|
3080
|
+
previousCount: previous.length,
|
|
3081
|
+
keywordMatches: keywords.length
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Fetches detailed data for a specific page including daily trends and top keywords.
|
|
3087
|
+
*/
|
|
3088
|
+
async function fetchPage(client, siteUrl, url, options = {}) {
|
|
3089
|
+
const { rowLimit = 5, ...queryOptions } = options;
|
|
3090
|
+
const [dates, keywords] = await Promise.all([client.searchAnalytics.query(siteUrl, {
|
|
3091
|
+
...createQueryBody({
|
|
3092
|
+
...queryOptions,
|
|
3093
|
+
filters: [{
|
|
3094
|
+
dimension: "page",
|
|
3095
|
+
operator: "equals",
|
|
3096
|
+
expression: formatPageForQuery(url, extractDomain(siteUrl))
|
|
3097
|
+
}]
|
|
3098
|
+
}),
|
|
3099
|
+
dimensions: ["date"]
|
|
3100
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
3101
|
+
...row,
|
|
3102
|
+
date: row.keys?.[0] ?? "",
|
|
3103
|
+
keys: null
|
|
3104
|
+
}))), client.searchAnalytics.query(siteUrl, {
|
|
3105
|
+
...createQueryBody({
|
|
3106
|
+
...queryOptions,
|
|
3107
|
+
filters: [{
|
|
3108
|
+
dimension: "page",
|
|
3109
|
+
operator: "equals",
|
|
3110
|
+
expression: formatPageForQuery(url, extractDomain(siteUrl))
|
|
3111
|
+
}]
|
|
3112
|
+
}),
|
|
3113
|
+
rowLimit,
|
|
3114
|
+
dimensions: ["query"]
|
|
3115
|
+
}).then((res) => (res.rows || []).map((row) => ({
|
|
3116
|
+
...row,
|
|
3117
|
+
keyword: row.keys?.[0] || "",
|
|
3118
|
+
keys: null
|
|
3119
|
+
})))]);
|
|
3120
|
+
return {
|
|
3121
|
+
dates,
|
|
3122
|
+
keywords
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
//#endregion
|
|
3127
|
+
//#region src/api/search-analytics/stream.ts
|
|
3128
|
+
const DIMENSION_TO_FIELD = {
|
|
3129
|
+
query: "keyword",
|
|
3130
|
+
page: "page",
|
|
3131
|
+
date: "date",
|
|
3132
|
+
device: "device",
|
|
3133
|
+
country: "country"
|
|
3134
|
+
};
|
|
3135
|
+
function mapRowToDimensions(row, dimensions) {
|
|
3136
|
+
const { keys, ...metrics } = row;
|
|
3137
|
+
const fields = {};
|
|
3138
|
+
dimensions.forEach((dim, i) => {
|
|
3139
|
+
fields[DIMENSION_TO_FIELD[dim]] = keys?.[i] || "";
|
|
3140
|
+
});
|
|
3141
|
+
return {
|
|
3142
|
+
...metrics,
|
|
3143
|
+
...fields
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Async generator for memory-efficient pagination of GSC search analytics.
|
|
3148
|
+
* Yields batches of typed rows as they're fetched, avoiding accumulation in memory.
|
|
3149
|
+
*
|
|
3150
|
+
* **Design:** Single generic function with type inference replaces separate
|
|
3151
|
+
* `fetchPagesStream`, `fetchKeywordsStream` wrappers. Output type is inferred
|
|
3152
|
+
* from the dimensions tuple:
|
|
3153
|
+
* - `['query']` → `{ keyword: string, clicks, impressions, ctr, position }`
|
|
3154
|
+
* - `['page']` → `{ page: string, clicks, impressions, ctr, position }`
|
|
3155
|
+
* - `['query', 'page']` → `{ keyword: string, page: string, ... }`
|
|
3156
|
+
*
|
|
3157
|
+
* Each yield contains up to 25,000 rows (one API page). Use `collectStream()`
|
|
3158
|
+
* to gather all batches if full array is needed.
|
|
3159
|
+
*
|
|
3160
|
+
* @param client - GSC client instance
|
|
3161
|
+
* @param siteUrl - Site URL (e.g., 'https://example.com/' or 'sc-domain:example.com')
|
|
3162
|
+
* @param query - Query with dimensions array (requires `as const` for type inference)
|
|
3163
|
+
*
|
|
3164
|
+
* @example
|
|
3165
|
+
* ```ts
|
|
3166
|
+
* // Stream keyword+page combinations
|
|
3167
|
+
* for await (const batch of queryRecursiveStream(client, url, {
|
|
3168
|
+
* dimensions: ['query', 'page'] as const,
|
|
3169
|
+
* startDate: '2024-01-01',
|
|
3170
|
+
* endDate: '2024-01-31',
|
|
3171
|
+
* })) {
|
|
3172
|
+
* // batch: { keyword: string, page: string, clicks, impressions, ctr, position }[]
|
|
3173
|
+
* await db.insert(batch)
|
|
3174
|
+
* }
|
|
3175
|
+
*
|
|
3176
|
+
* // Or collect all at once
|
|
3177
|
+
* const allRows = await collectStream(queryRecursiveStream(client, url, {
|
|
3178
|
+
* dimensions: ['page'] as const,
|
|
3179
|
+
* startDate: '2024-01-01',
|
|
3180
|
+
* endDate: '2024-01-31',
|
|
3181
|
+
* }))
|
|
3182
|
+
* ```
|
|
3183
|
+
*
|
|
3184
|
+
* @remarks
|
|
3185
|
+
* - Requires `as const` on dimensions array for proper type inference
|
|
3186
|
+
* - Use for large datasets (>25k rows) where memory is a concern
|
|
3187
|
+
* - Non-paginating queries (devices, countries) don't benefit from streaming
|
|
3188
|
+
*/
|
|
3189
|
+
async function* queryRecursiveStream(client, siteUrl, query$1) {
|
|
3190
|
+
const rowLimit = query$1.rowLimit || 25e3;
|
|
3191
|
+
let startRow = 0;
|
|
3192
|
+
while (true) {
|
|
3193
|
+
const rows = (await client.searchAnalytics.query(siteUrl, {
|
|
3194
|
+
...query$1,
|
|
3195
|
+
dimensions: [...query$1.dimensions],
|
|
3196
|
+
startRow,
|
|
3197
|
+
rowLimit
|
|
3198
|
+
})).rows || [];
|
|
3199
|
+
if (rows.length === 0) break;
|
|
3200
|
+
yield rows.map((row) => mapRowToDimensions(row, query$1.dimensions));
|
|
3201
|
+
startRow += rows.length;
|
|
3202
|
+
if (rows.length < rowLimit) break;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
/** Collect all batches into single array (for testing / simple cases) */
|
|
3206
|
+
async function collectStream(gen) {
|
|
3207
|
+
const all = [];
|
|
3208
|
+
for await (const batch of gen) all.push(...batch);
|
|
3209
|
+
return all;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
//#endregion
|
|
3213
|
+
//#region src/core/client.ts
|
|
3214
|
+
const GSC_API = "https://searchconsole.googleapis.com";
|
|
3215
|
+
const INDEXING_API = "https://indexing.googleapis.com";
|
|
3216
|
+
function createAuth(options) {
|
|
3217
|
+
let credentials = { refresh_token: options.refreshToken };
|
|
3218
|
+
return {
|
|
3219
|
+
get credentials() {
|
|
3220
|
+
return credentials;
|
|
3221
|
+
},
|
|
3222
|
+
async getAccessToken() {
|
|
3223
|
+
if (credentials?.access_token && credentials.expiry_date && credentials.expiry_date > Date.now()) return { token: credentials.access_token };
|
|
3224
|
+
const response = await ofetch("https://oauth2.googleapis.com/token", {
|
|
3225
|
+
method: "POST",
|
|
3226
|
+
body: {
|
|
3227
|
+
client_id: options.clientId,
|
|
3228
|
+
client_secret: options.clientSecret,
|
|
3229
|
+
refresh_token: options.refreshToken,
|
|
3230
|
+
grant_type: "refresh_token"
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
credentials = {
|
|
3234
|
+
...credentials,
|
|
3235
|
+
access_token: response.access_token,
|
|
3236
|
+
expiry_date: Date.now() + response.expires_in * 1e3
|
|
3237
|
+
};
|
|
3238
|
+
return { token: response.access_token };
|
|
3239
|
+
}
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
async function resolveToken(auth) {
|
|
3243
|
+
if (typeof auth === "string") return auth;
|
|
3244
|
+
if ("accessToken" in auth && typeof auth.accessToken === "string") return auth.accessToken;
|
|
3245
|
+
if ("getAccessToken" in auth && typeof auth.getAccessToken === "function") {
|
|
3246
|
+
const { token } = await auth.getAccessToken();
|
|
3247
|
+
return token || "";
|
|
3248
|
+
}
|
|
3249
|
+
if ("credentials" in auth && auth.credentials) return auth.credentials.access_token || "";
|
|
3250
|
+
return "";
|
|
3251
|
+
}
|
|
3252
|
+
function createFetch(auth, options) {
|
|
3253
|
+
const authState = typeof auth === "object" && auth !== null && "clientId" in auth && "refreshToken" in auth && !("getAccessToken" in auth) ? createAuth(auth) : auth;
|
|
3254
|
+
return ofetch.create({
|
|
3255
|
+
...options,
|
|
3256
|
+
retry: 3,
|
|
3257
|
+
retryDelay: 1e3,
|
|
3258
|
+
retryStatusCodes: [
|
|
3259
|
+
408,
|
|
3260
|
+
409,
|
|
3261
|
+
425,
|
|
3262
|
+
429,
|
|
3263
|
+
500,
|
|
3264
|
+
502,
|
|
3265
|
+
503,
|
|
3266
|
+
504
|
|
3267
|
+
],
|
|
3268
|
+
headers: {
|
|
3269
|
+
...options?.headers,
|
|
3270
|
+
"Accept-Encoding": "gzip",
|
|
3271
|
+
"User-Agent": "gscdump (gzip)"
|
|
3272
|
+
},
|
|
3273
|
+
async onRequest({ options: options$1 }) {
|
|
3274
|
+
const token = await resolveToken(authState);
|
|
3275
|
+
if (token) {
|
|
3276
|
+
options$1.headers = new Headers(options$1.headers);
|
|
3277
|
+
options$1.headers.set("Authorization", `Bearer ${token}`);
|
|
3278
|
+
}
|
|
3279
|
+
},
|
|
3280
|
+
async onResponseError(ctx) {
|
|
3281
|
+
if (ctx.response.status === 403) console.error("[gscdump] Permission denied (403). check your service account permissions being added to the GSC property.");
|
|
3282
|
+
if (options?.onResponseError) if (Array.isArray(options.onResponseError)) for (const handler of options.onResponseError) await handler(ctx);
|
|
3283
|
+
else await options.onResponseError(ctx);
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
function googleSearchConsole(auth, options = {}) {
|
|
3288
|
+
let fetch;
|
|
3289
|
+
const authState = typeof auth === "object" && auth !== null && "clientId" in auth && "refreshToken" in auth && !("getAccessToken" in auth) ? createAuth(auth) : auth;
|
|
3290
|
+
if (options.fetch) fetch = options.fetch;
|
|
3291
|
+
else {
|
|
3292
|
+
const fetchOptions = options.fetchOptions || {};
|
|
3293
|
+
if (options.onRateLimited) {
|
|
3294
|
+
const originalOnError = fetchOptions.onResponseError;
|
|
3295
|
+
fetchOptions.onResponseError = async (ctx) => {
|
|
3296
|
+
if (ctx.response.status === 429) await options.onRateLimited({ response: ctx.response });
|
|
3297
|
+
if (originalOnError) if (Array.isArray(originalOnError)) for (const handler of originalOnError) await handler(ctx);
|
|
3298
|
+
else await originalOnError(ctx);
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
fetch = createFetch(authState, fetchOptions);
|
|
3302
|
+
}
|
|
3303
|
+
return {
|
|
3304
|
+
sites: { list: () => fetch(`${GSC_API}/webmasters/v3/sites`) },
|
|
3305
|
+
sitemaps: {
|
|
3306
|
+
list: (siteUrl) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps`),
|
|
3307
|
+
get: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`),
|
|
3308
|
+
submit: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "PUT" }),
|
|
3309
|
+
delete: (siteUrl, feedpath) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}`, { method: "DELETE" })
|
|
3310
|
+
},
|
|
3311
|
+
searchAnalytics: { query: (siteUrl, body) => fetch(`${GSC_API}/webmasters/v3/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`, {
|
|
3312
|
+
method: "POST",
|
|
3313
|
+
body
|
|
3314
|
+
}) },
|
|
3315
|
+
urlInspection: { inspect: (siteUrl, inspectionUrl) => fetch(`${GSC_API}/v1/urlInspection/index:inspect`, {
|
|
3316
|
+
method: "POST",
|
|
3317
|
+
body: {
|
|
3318
|
+
inspectionUrl,
|
|
3319
|
+
siteUrl
|
|
3320
|
+
}
|
|
3321
|
+
}) },
|
|
3322
|
+
indexing: {
|
|
3323
|
+
publish: (url, type) => fetch(`${INDEXING_API}/v3/urlNotifications:publish`, {
|
|
3324
|
+
method: "POST",
|
|
3325
|
+
body: {
|
|
3326
|
+
url,
|
|
3327
|
+
type
|
|
3328
|
+
}
|
|
3329
|
+
}),
|
|
3330
|
+
getMetadata: (url) => fetch(`${INDEXING_API}/v3/urlNotifications/metadata`, { query: { url } })
|
|
3331
|
+
}
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
//#endregion
|
|
3336
|
+
//#region src/core/errors.ts
|
|
3337
|
+
/** GSC API quota limits (approximate) */
|
|
3338
|
+
const GSC_QUOTAS = {
|
|
3339
|
+
searchAnalytics: 25e3,
|
|
3340
|
+
urlInspection: 2e3,
|
|
3341
|
+
indexing: 200
|
|
3342
|
+
};
|
|
3343
|
+
/**
|
|
3344
|
+
* Detects if an error is a quota exceeded error (403 quotaExceeded).
|
|
3345
|
+
*/
|
|
3346
|
+
function isQuotaError(error) {
|
|
3347
|
+
const msg = getErrorMessage(error).toLowerCase();
|
|
3348
|
+
return getErrorCode(error) === 403 && (msg.includes("quota") || msg.includes("limit exceeded") || msg.includes("rate limit") || msg.includes("quotaexceeded"));
|
|
3349
|
+
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Detects if an error is a rate limit error (429 Too Many Requests).
|
|
3352
|
+
*/
|
|
3353
|
+
function isRateLimitError(error) {
|
|
3354
|
+
return getErrorCode(error) === 429;
|
|
3355
|
+
}
|
|
3356
|
+
/**
|
|
3357
|
+
* Detects if an error is an authentication error (401/403 without quota).
|
|
3358
|
+
*/
|
|
3359
|
+
function isAuthError(error) {
|
|
3360
|
+
const code = getErrorCode(error);
|
|
3361
|
+
const msg = getErrorMessage(error).toLowerCase();
|
|
3362
|
+
if (code === 401) return true;
|
|
3363
|
+
if (code === 403 && !isQuotaError(error)) return msg.includes("access") || msg.includes("permission") || msg.includes("forbidden");
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Extracts HTTP status code from various error formats.
|
|
3368
|
+
*/
|
|
3369
|
+
function getErrorCode(error) {
|
|
3370
|
+
if (!error || typeof error !== "object") return void 0;
|
|
3371
|
+
const e = error;
|
|
3372
|
+
if ("statusCode" in e && typeof e.statusCode === "number") return e.statusCode;
|
|
3373
|
+
if ("status" in e && typeof e.status === "number") return e.status;
|
|
3374
|
+
if ("response" in e && e.response && typeof e.response === "object") {
|
|
3375
|
+
const resp = e.response;
|
|
3376
|
+
if ("status" in resp && typeof resp.status === "number") return resp.status;
|
|
3377
|
+
}
|
|
3378
|
+
if ("code" in e && typeof e.code === "number") return e.code;
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Extracts error message from various error formats.
|
|
3382
|
+
*/
|
|
3383
|
+
function getErrorMessage(error) {
|
|
3384
|
+
if (!error) return "Unknown error";
|
|
3385
|
+
if (typeof error === "string") return error;
|
|
3386
|
+
if (error instanceof Error) return error.message;
|
|
3387
|
+
if (typeof error === "object") {
|
|
3388
|
+
const e = error;
|
|
3389
|
+
if ("message" in e && typeof e.message === "string") return e.message;
|
|
3390
|
+
if ("statusMessage" in e && typeof e.statusMessage === "string") return e.statusMessage;
|
|
3391
|
+
if ("data" in e && e.data && typeof e.data === "object") {
|
|
3392
|
+
const data = e.data;
|
|
3393
|
+
if ("error" in data && data.error && typeof data.error === "object") {
|
|
3394
|
+
const err = data.error;
|
|
3395
|
+
if ("message" in err && typeof err.message === "string") return err.message;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
return String(error);
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Extracts retry-after value from error headers (in seconds).
|
|
3403
|
+
*/
|
|
3404
|
+
function getRetryAfter(error) {
|
|
3405
|
+
if (!error || typeof error !== "object") return void 0;
|
|
3406
|
+
const e = error;
|
|
3407
|
+
if ("headers" in e && e.headers && typeof e.headers === "object") {
|
|
3408
|
+
const headers = e.headers;
|
|
3409
|
+
const retryAfter = headers["retry-after"] || headers["Retry-After"];
|
|
3410
|
+
if (typeof retryAfter === "string") {
|
|
3411
|
+
const seconds = Number.parseInt(retryAfter, 10);
|
|
3412
|
+
return Number.isNaN(seconds) ? void 0 : seconds;
|
|
3413
|
+
}
|
|
3414
|
+
if (typeof retryAfter === "number") return retryAfter;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
function formatQuotaSuggestion(message, retryAfter) {
|
|
3418
|
+
if (message.includes("Search Console API")) return `You exceeded the Search Analytics quota (${GSC_QUOTAS.searchAnalytics}/day). Try again tomorrow.`;
|
|
3419
|
+
if (message.includes("Indexing API")) return `You exceeded the Indexing API quota (${GSC_QUOTAS.indexing}/day). Try again tomorrow.`;
|
|
3420
|
+
return `Quota exceeded. Try again in ${retryAfter ? `${retryAfter}s` : "24 hours"}.`;
|
|
3421
|
+
}
|
|
3422
|
+
function formatRateLimitSuggestion(retryAfter) {
|
|
3423
|
+
return `Rate limited. Slow down requests. Try again in ${retryAfter ? `${retryAfter}s` : "a few minutes"}.`;
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Analyzes an error and returns structured information with suggestions.
|
|
3427
|
+
*/
|
|
3428
|
+
function analyzeError(error) {
|
|
3429
|
+
const code = getErrorCode(error);
|
|
3430
|
+
const message = getErrorMessage(error);
|
|
3431
|
+
const retryAfter = getRetryAfter(error);
|
|
3432
|
+
if (isQuotaError(error)) return {
|
|
3433
|
+
isQuotaError: true,
|
|
3434
|
+
isRateLimitError: false,
|
|
3435
|
+
isAuthError: false,
|
|
3436
|
+
code,
|
|
3437
|
+
message,
|
|
3438
|
+
retryAfter,
|
|
3439
|
+
suggestion: formatQuotaSuggestion(message, retryAfter)
|
|
3440
|
+
};
|
|
3441
|
+
if (isRateLimitError(error)) return {
|
|
3442
|
+
isQuotaError: false,
|
|
3443
|
+
isRateLimitError: true,
|
|
3444
|
+
isAuthError: false,
|
|
3445
|
+
code,
|
|
3446
|
+
message,
|
|
3447
|
+
retryAfter: retryAfter || 60,
|
|
3448
|
+
suggestion: formatRateLimitSuggestion(retryAfter)
|
|
3449
|
+
};
|
|
3450
|
+
if (isAuthError(error)) return {
|
|
3451
|
+
isQuotaError: false,
|
|
3452
|
+
isRateLimitError: false,
|
|
3453
|
+
isAuthError: true,
|
|
3454
|
+
code,
|
|
3455
|
+
message,
|
|
3456
|
+
suggestion: "Run `gscdump auth` to re-authenticate."
|
|
3457
|
+
};
|
|
3458
|
+
return {
|
|
3459
|
+
isQuotaError: false,
|
|
3460
|
+
isRateLimitError: false,
|
|
3461
|
+
isAuthError: false,
|
|
3462
|
+
code,
|
|
3463
|
+
message,
|
|
3464
|
+
suggestion: ""
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Formats an error for CLI display with color codes.
|
|
3469
|
+
*/
|
|
3470
|
+
function formatErrorForCli(error) {
|
|
3471
|
+
const info = analyzeError(error);
|
|
3472
|
+
const lines = [];
|
|
3473
|
+
lines.push(`\x1B[31m${info.message}\x1B[0m`);
|
|
3474
|
+
if (info.suggestion) {
|
|
3475
|
+
lines.push("");
|
|
3476
|
+
lines.push(info.suggestion);
|
|
3477
|
+
}
|
|
3478
|
+
return lines.join("\n");
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
//#endregion
|
|
3482
|
+
//#region src/query/resolver.ts
|
|
3483
|
+
const DATE_OPERATORS = [
|
|
3484
|
+
"gte",
|
|
3485
|
+
"gt",
|
|
3486
|
+
"lte",
|
|
3487
|
+
"lt",
|
|
3488
|
+
"between"
|
|
3489
|
+
];
|
|
3490
|
+
function isDateOperator(op) {
|
|
3491
|
+
return DATE_OPERATORS.includes(op);
|
|
3492
|
+
}
|
|
3493
|
+
function addDays(dateStr, days) {
|
|
3494
|
+
const d = new Date(dateStr);
|
|
3495
|
+
d.setDate(d.getDate() + days);
|
|
3496
|
+
return d.toISOString().split("T")[0];
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Extract date range from filters. Used by analysis functions to get the period.
|
|
3500
|
+
*/
|
|
3501
|
+
function extractDateRange(filters) {
|
|
3502
|
+
const { startDate, endDate } = extractDateFilters(filters);
|
|
3503
|
+
return {
|
|
3504
|
+
startDate,
|
|
3505
|
+
endDate
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
function extractDateFilters(filters) {
|
|
3509
|
+
let startDate;
|
|
3510
|
+
let endDate;
|
|
3511
|
+
const nonDateFilters = [];
|
|
3512
|
+
for (const filter of filters) {
|
|
3513
|
+
const dateFilters = [];
|
|
3514
|
+
const otherFilters = [];
|
|
3515
|
+
for (const f of filter._filters) if (f.dimension === "date" && isDateOperator(f.operator)) dateFilters.push(f);
|
|
3516
|
+
else otherFilters.push(f);
|
|
3517
|
+
for (const df of dateFilters) switch (df.operator) {
|
|
3518
|
+
case "gte":
|
|
3519
|
+
startDate = df.expression;
|
|
3520
|
+
break;
|
|
3521
|
+
case "gt":
|
|
3522
|
+
startDate = addDays(df.expression, 1);
|
|
3523
|
+
break;
|
|
3524
|
+
case "lte":
|
|
3525
|
+
endDate = df.expression;
|
|
3526
|
+
break;
|
|
3527
|
+
case "lt":
|
|
3528
|
+
endDate = addDays(df.expression, -1);
|
|
3529
|
+
break;
|
|
3530
|
+
case "between":
|
|
3531
|
+
startDate = df.expression;
|
|
3532
|
+
endDate = df.expression2;
|
|
3533
|
+
break;
|
|
3534
|
+
}
|
|
3535
|
+
if (otherFilters.length > 0) nonDateFilters.push({
|
|
3536
|
+
...filter,
|
|
3537
|
+
_filters: otherFilters
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
return {
|
|
3541
|
+
startDate,
|
|
3542
|
+
endDate,
|
|
3543
|
+
nonDateFilters
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
function resolveToBody(state) {
|
|
3547
|
+
const { startDate, endDate, nonDateFilters } = extractDateFilters(state.filters);
|
|
3548
|
+
if (!startDate || !endDate) throw new Error("Date range required: use .where(between(date, start, end)) or .where(gte(date, start)).where(lte(date, end))");
|
|
3549
|
+
const body = {
|
|
3550
|
+
dimensions: state.dimensions,
|
|
3551
|
+
startDate,
|
|
3552
|
+
endDate
|
|
3553
|
+
};
|
|
3554
|
+
if (state.rowLimit) body.rowLimit = state.rowLimit;
|
|
3555
|
+
const filterGroups = resolveFilters(nonDateFilters);
|
|
3556
|
+
if (filterGroups.length > 0) body.dimensionFilterGroups = filterGroups;
|
|
3557
|
+
return body;
|
|
3558
|
+
}
|
|
3559
|
+
function resolveFilters(filters) {
|
|
3560
|
+
const groups = [];
|
|
3561
|
+
for (const filter of filters) if ((filter._groupType ?? "and") === "or" && filter._filters.length > 1) groups.push({
|
|
3562
|
+
groupType: "or",
|
|
3563
|
+
filters: filter._filters.map((f) => ({
|
|
3564
|
+
dimension: f.dimension,
|
|
3565
|
+
operator: f.operator,
|
|
3566
|
+
expression: f.expression
|
|
3567
|
+
}))
|
|
3568
|
+
});
|
|
3569
|
+
else if (filter._filters.length > 0) groups.push({ filters: filter._filters.map((f) => ({
|
|
3570
|
+
dimension: f.dimension,
|
|
3571
|
+
operator: f.operator,
|
|
3572
|
+
expression: f.expression
|
|
3573
|
+
})) });
|
|
3574
|
+
return groups;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
//#endregion
|
|
3578
|
+
//#region src/query/builder.ts
|
|
3579
|
+
function transformResponse(response, dimensions) {
|
|
3580
|
+
return { rows: (response.rows ?? []).map((row) => {
|
|
3581
|
+
const result = {
|
|
3582
|
+
clicks: row.clicks,
|
|
3583
|
+
impressions: row.impressions,
|
|
3584
|
+
ctr: row.ctr,
|
|
3585
|
+
position: row.position
|
|
3586
|
+
};
|
|
3587
|
+
dimensions.forEach((dim, i) => {
|
|
3588
|
+
result[dim] = row.keys?.[i];
|
|
3589
|
+
});
|
|
3590
|
+
return result;
|
|
3591
|
+
}) };
|
|
3592
|
+
}
|
|
3593
|
+
function createBuilder(state) {
|
|
3594
|
+
return {
|
|
3595
|
+
select(...dims) {
|
|
3596
|
+
return createBuilder({
|
|
3597
|
+
...state,
|
|
3598
|
+
dimensions: dims
|
|
3599
|
+
});
|
|
3600
|
+
},
|
|
3601
|
+
where(filter) {
|
|
3602
|
+
return createBuilder({
|
|
3603
|
+
...state,
|
|
3604
|
+
filters: [...state.filters, filter]
|
|
3605
|
+
});
|
|
3606
|
+
},
|
|
3607
|
+
siteUrl(url) {
|
|
3608
|
+
return createBuilder({
|
|
3609
|
+
...state,
|
|
3610
|
+
siteUrl: url
|
|
3611
|
+
});
|
|
3612
|
+
},
|
|
3613
|
+
limit(n) {
|
|
3614
|
+
return createBuilder({
|
|
3615
|
+
...state,
|
|
3616
|
+
rowLimit: n
|
|
3617
|
+
});
|
|
3618
|
+
},
|
|
3619
|
+
async execute(client) {
|
|
3620
|
+
const body = resolveToBody(state);
|
|
3621
|
+
return transformResponse(await client.searchAnalytics.query(state.siteUrl, body), state.dimensions);
|
|
3622
|
+
},
|
|
3623
|
+
toBody() {
|
|
3624
|
+
return resolveToBody(state);
|
|
3625
|
+
},
|
|
3626
|
+
getState() {
|
|
3627
|
+
return { ...state };
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
const gsc = createBuilder({
|
|
3632
|
+
dimensions: [],
|
|
3633
|
+
filters: []
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
//#endregion
|
|
3637
|
+
//#region src/query/columns.ts
|
|
3638
|
+
function createColumn(dimension) {
|
|
3639
|
+
return { dimension };
|
|
3640
|
+
}
|
|
3641
|
+
const page = createColumn("page");
|
|
3642
|
+
const query = createColumn("query");
|
|
3643
|
+
const device = createColumn("device");
|
|
3644
|
+
const country = createColumn("country");
|
|
3645
|
+
const searchAppearance = createColumn("searchAppearance");
|
|
3646
|
+
const date = createColumn("date");
|
|
3647
|
+
|
|
3648
|
+
//#endregion
|
|
3649
|
+
//#region src/query/operators.ts
|
|
3650
|
+
function eq(column, value) {
|
|
3651
|
+
return {
|
|
3652
|
+
_constraints: {},
|
|
3653
|
+
_filters: [{
|
|
3654
|
+
dimension: column.dimension,
|
|
3655
|
+
operator: "equals",
|
|
3656
|
+
expression: String(value)
|
|
3657
|
+
}]
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
function ne(column, value) {
|
|
3661
|
+
return {
|
|
3662
|
+
_constraints: {},
|
|
3663
|
+
_filters: [{
|
|
3664
|
+
dimension: column.dimension,
|
|
3665
|
+
operator: "notEquals",
|
|
3666
|
+
expression: String(value)
|
|
3667
|
+
}]
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
function inArray(column, values) {
|
|
3671
|
+
return {
|
|
3672
|
+
_constraints: {},
|
|
3673
|
+
_filters: values.map((v) => ({
|
|
3674
|
+
dimension: column.dimension,
|
|
3675
|
+
operator: "equals",
|
|
3676
|
+
expression: String(v)
|
|
3677
|
+
})),
|
|
3678
|
+
_groupType: "or"
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
function contains(column, pattern) {
|
|
3682
|
+
return {
|
|
3683
|
+
_constraints: {},
|
|
3684
|
+
_filters: [{
|
|
3685
|
+
dimension: column.dimension,
|
|
3686
|
+
operator: "contains",
|
|
3687
|
+
expression: pattern
|
|
3688
|
+
}]
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
function like(column, pattern) {
|
|
3692
|
+
return {
|
|
3693
|
+
_constraints: {},
|
|
3694
|
+
_filters: [{
|
|
3695
|
+
dimension: column.dimension,
|
|
3696
|
+
operator: "contains",
|
|
3697
|
+
expression: pattern.replace(/%/g, "")
|
|
3698
|
+
}]
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
function regex(column, pattern) {
|
|
3702
|
+
return {
|
|
3703
|
+
_constraints: {},
|
|
3704
|
+
_filters: [{
|
|
3705
|
+
dimension: column.dimension,
|
|
3706
|
+
operator: "includingRegex",
|
|
3707
|
+
expression: typeof pattern === "string" ? pattern : pattern.source
|
|
3708
|
+
}]
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
function notRegex(column, pattern) {
|
|
3712
|
+
return {
|
|
3713
|
+
_constraints: {},
|
|
3714
|
+
_filters: [{
|
|
3715
|
+
dimension: column.dimension,
|
|
3716
|
+
operator: "excludingRegex",
|
|
3717
|
+
expression: typeof pattern === "string" ? pattern : pattern.source
|
|
3718
|
+
}]
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
function and(...filters) {
|
|
3722
|
+
return {
|
|
3723
|
+
_constraints: {},
|
|
3724
|
+
_filters: filters.flatMap((f) => f._filters),
|
|
3725
|
+
_groupType: "and"
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
function or(...filters) {
|
|
3729
|
+
return {
|
|
3730
|
+
_constraints: {},
|
|
3731
|
+
_filters: filters.flatMap((f) => f._filters),
|
|
3732
|
+
_groupType: "or"
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
const DATE_OPS = [
|
|
3736
|
+
"gte",
|
|
3737
|
+
"gt",
|
|
3738
|
+
"lte",
|
|
3739
|
+
"lt",
|
|
3740
|
+
"between"
|
|
3741
|
+
];
|
|
3742
|
+
function not(filter) {
|
|
3743
|
+
return {
|
|
3744
|
+
_constraints: {},
|
|
3745
|
+
_filters: filter._filters.filter((f) => !DATE_OPS.includes(f.operator)).map((f) => ({
|
|
3746
|
+
...f,
|
|
3747
|
+
operator: invertOperator(f.operator)
|
|
3748
|
+
}))
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
function invertOperator(op) {
|
|
3752
|
+
return {
|
|
3753
|
+
equals: "notEquals",
|
|
3754
|
+
notEquals: "equals",
|
|
3755
|
+
contains: "notContains",
|
|
3756
|
+
notContains: "contains",
|
|
3757
|
+
includingRegex: "excludingRegex",
|
|
3758
|
+
excludingRegex: "includingRegex"
|
|
3759
|
+
}[op];
|
|
3760
|
+
}
|
|
3761
|
+
function gte(column, value) {
|
|
3762
|
+
return {
|
|
3763
|
+
_constraints: {},
|
|
3764
|
+
_filters: [{
|
|
3765
|
+
dimension: column.dimension,
|
|
3766
|
+
operator: "gte",
|
|
3767
|
+
expression: String(value)
|
|
3768
|
+
}]
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
function gt(column, value) {
|
|
3772
|
+
return {
|
|
3773
|
+
_constraints: {},
|
|
3774
|
+
_filters: [{
|
|
3775
|
+
dimension: column.dimension,
|
|
3776
|
+
operator: "gt",
|
|
3777
|
+
expression: String(value)
|
|
3778
|
+
}]
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
function lte(column, value) {
|
|
3782
|
+
return {
|
|
3783
|
+
_constraints: {},
|
|
3784
|
+
_filters: [{
|
|
3785
|
+
dimension: column.dimension,
|
|
3786
|
+
operator: "lte",
|
|
3787
|
+
expression: String(value)
|
|
3788
|
+
}]
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
function lt(column, value) {
|
|
3792
|
+
return {
|
|
3793
|
+
_constraints: {},
|
|
3794
|
+
_filters: [{
|
|
3795
|
+
dimension: column.dimension,
|
|
3796
|
+
operator: "lt",
|
|
3797
|
+
expression: String(value)
|
|
3798
|
+
}]
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
function between(column, start, end) {
|
|
3802
|
+
return {
|
|
3803
|
+
_constraints: {},
|
|
3804
|
+
_filters: [{
|
|
3805
|
+
dimension: column.dimension,
|
|
3806
|
+
operator: "between",
|
|
3807
|
+
expression: String(start),
|
|
3808
|
+
expression2: String(end)
|
|
3809
|
+
}]
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
//#endregion
|
|
3814
|
+
//#region src/query/constants.ts
|
|
3815
|
+
const Device = {
|
|
3816
|
+
MOBILE: "MOBILE",
|
|
3817
|
+
DESKTOP: "DESKTOP",
|
|
3818
|
+
TABLET: "TABLET"
|
|
3819
|
+
};
|
|
3820
|
+
const Country = Object.fromEntries(countries_default.map((c) => [c["alpha-3"], c["alpha-3"].toLowerCase()]));
|
|
3821
|
+
|
|
3822
|
+
//#endregion
|
|
3823
|
+
export { Country, Device, GSC_QUOTAS, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeError, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, and, batchInspectUrls, batchRequestIndexing, between, collectStream, contains, countries_default as countries, country, createAuth, createFetch, createQueryBody, createSorter, date, dayjs, deleteSitemap, device, eq, extractDateRange, fetchAnalyticsWithComparison, fetchCountries, fetchCountriesWithComparison, fetchDates, fetchDatesWithComparison, fetchDevices, fetchDevicesWithComparison, fetchKeyword, fetchKeywords, fetchKeywordsWithComparison, fetchPage, fetchPages, fetchPagesWithComparison, fetchSearchAppearance, fetchSearchAppearanceWithComparison, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, fetchYoYComparison, formatDateGsc, formatErrorForCli, getErrorCode, getErrorMessage, getIndexingMetadata, getRetryAfter, googleSearchConsole, gsc, gt, gte, inArray, inspectUrl, isAuthError, isQuotaError, isRateLimitError, like, lt, lte, ne, not, notRegex, num, or, page, percentDifference, query, queryRecursive, queryRecursiveStream, regex, requestIndexing, searchAppearance, submitSitemap, userPeriodRange, withDataType, withFinalData, withFreshData, withPropertyAggregation, withSearchAppearance };
|