seo 0.2.4 → 0.2.6
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/PRIVACY.md +42 -5
- package/README.md +29 -13
- package/dist/cli.js +94 -92
- package/dist/cli.js.map +1 -1
- package/dist/dist-PRmi6cXV.js +560 -0
- package/dist/dist-PRmi6cXV.js.map +1 -0
- package/dist/{dist-BhdgeyGe.js → dist-Xkhuyn70.js} +1 -1
- package/dist/index.d.ts +125 -34
- package/dist/index.js +560 -285
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -1
- package/dist/mcp.js +444 -102
- package/dist/mcp.js.map +1 -1
- package/dist/{package-D6bCOqrD.js → package-B9-D646K.js} +2 -2
- package/dist/{package-D6bCOqrD.js.map → package-B9-D646K.js.map} +1 -1
- package/evals/ai-referrals.json +7 -7
- package/evals/compare-crawls.json +2 -2
- package/evals/monthly-action-plan.json +1 -1
- package/evals/refresh-priorities.json +7 -7
- package/evals/seo-fix-queue.json +2 -2
- package/evals/seo-geo-readiness.json +1 -1
- package/evals/seo-to-ai-query.json +2 -2
- package/package.json +3 -3
- package/skills/seo/SKILL.md +7 -6
- package/skills/seo/agents/openai.yaml +1 -1
- package/dist/dist-DMSBrZYd.js +0 -713
- package/dist/dist-DMSBrZYd.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as load, t as version } from "./package-
|
|
2
|
+
import { n as load, t as version } from "./package-B9-D646K.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
5
|
+
import { arch, homedir, platform } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import lockfile from "proper-lockfile";
|
|
@@ -149,6 +149,7 @@ function resolveSeoCliPaths(input = {}) {
|
|
|
149
149
|
configFile: join(configDir, "config.json"),
|
|
150
150
|
tokensFile: join(configDir, "tokens.json"),
|
|
151
151
|
oauthClientFile: join(configDir, "oauth-client.json"),
|
|
152
|
+
telemetryStateFile: join(configDir, "telemetry.json"),
|
|
152
153
|
cacheDbFile: join(cacheDir, "cache.db")
|
|
153
154
|
};
|
|
154
155
|
}
|
|
@@ -218,6 +219,7 @@ const siteSchema = z.object({
|
|
|
218
219
|
isDefault: z.boolean().optional()
|
|
219
220
|
});
|
|
220
221
|
const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
|
|
222
|
+
const analyticsConnectionsSchema = z.object({ google: z.object({ propertyId: z.string() }).optional() }).default({});
|
|
221
223
|
const clientProfileSchema = z.object({
|
|
222
224
|
id: z.string(),
|
|
223
225
|
name: z.string(),
|
|
@@ -225,7 +227,7 @@ const clientProfileSchema = z.object({
|
|
|
225
227
|
startUrl: z.string().optional(),
|
|
226
228
|
watchUrls: z.array(z.string()).default([]),
|
|
227
229
|
brandTerms: z.array(z.string()).default([]),
|
|
228
|
-
|
|
230
|
+
analytics: analyticsConnectionsSchema,
|
|
229
231
|
reportDay: z.number().int().min(1).max(31).optional(),
|
|
230
232
|
technicalWeekday: z.number().int().min(0).max(7).optional(),
|
|
231
233
|
isDefault: z.boolean().optional(),
|
|
@@ -236,14 +238,14 @@ const configSchema = z.object({
|
|
|
236
238
|
defaultSite: z.string().optional(),
|
|
237
239
|
sites: z.array(siteSchema).default([]),
|
|
238
240
|
clients: z.array(clientProfileSchema).default([]),
|
|
239
|
-
google: z.object({
|
|
240
|
-
|
|
241
|
+
analytics: z.object({ google: z.object({
|
|
242
|
+
defaultPropertyId: z.string().optional(),
|
|
241
243
|
propertyMappings: z.array(z.object({
|
|
242
244
|
siteUrl: z.string(),
|
|
243
|
-
|
|
245
|
+
propertyId: z.string(),
|
|
244
246
|
addedAt: z.number().int().optional()
|
|
245
247
|
})).default([])
|
|
246
|
-
}).default({ propertyMappings: [] }),
|
|
248
|
+
}).default({ propertyMappings: [] }) }).default({ google: { propertyMappings: [] } }),
|
|
247
249
|
providers: z.object({
|
|
248
250
|
semrushApiKey: z.string().optional(),
|
|
249
251
|
dataForSeoLogin: z.string().optional(),
|
|
@@ -460,6 +462,14 @@ function missingOAuthClientMessage(source) {
|
|
|
460
462
|
//#endregion
|
|
461
463
|
//#region packages/core/dist/gsc/auth/types.js
|
|
462
464
|
const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
|
|
465
|
+
const GOOGLE_READONLY_SCOPE_LABELS = {
|
|
466
|
+
"https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
|
|
467
|
+
"https://www.googleapis.com/auth/analytics.readonly": "Google Analytics"
|
|
468
|
+
};
|
|
469
|
+
function missingGoogleReadonlyScopes(scope) {
|
|
470
|
+
const granted = new Set(scope.split(/\s+/u).filter(Boolean));
|
|
471
|
+
return GOOGLE_READONLY_SCOPES.filter((required) => !granted.has(required));
|
|
472
|
+
}
|
|
463
473
|
[
|
|
464
474
|
...GOOGLE_READONLY_SCOPES,
|
|
465
475
|
"openid",
|
|
@@ -657,6 +667,11 @@ function authRequired() {
|
|
|
657
667
|
function authExpired() {
|
|
658
668
|
return new SeoError("AUTH_EXPIRED", "Google refresh token is no longer valid. Run `seo auth login` again.");
|
|
659
669
|
}
|
|
670
|
+
function requireReadonlyScopes(tokens) {
|
|
671
|
+
const missing = missingGoogleReadonlyScopes(tokens.scope);
|
|
672
|
+
if (missing.length === 0) return;
|
|
673
|
+
throw new SeoError("ACCESS_DENIED", `Google access is missing ${missing.map((scope) => GOOGLE_READONLY_SCOPE_LABELS[scope]).join(" and ")}. Run \`seo auth login\` again, choose Select all, and approve both permission boxes.`);
|
|
674
|
+
}
|
|
660
675
|
function needsRefresh(tokens, now) {
|
|
661
676
|
return !tokens.access_token || tokens.expires_at - now < REFRESH_BUFFER_MS;
|
|
662
677
|
}
|
|
@@ -716,6 +731,7 @@ async function createAuthorizedClient() {
|
|
|
716
731
|
let tokens = await readTokens();
|
|
717
732
|
if (!tokens) throw authRequired();
|
|
718
733
|
requireClientConfig(tokens);
|
|
734
|
+
requireReadonlyScopes(tokens);
|
|
719
735
|
if (needsRefresh(tokens, Date.now())) tokens = await refreshStoredToken({ onlyIfExpiring: true });
|
|
720
736
|
return {
|
|
721
737
|
client: new LocalGoogleAccessTokenClient(tokens),
|
|
@@ -1068,8 +1084,8 @@ async function runGa4Report(propertyId, body, opts = {}) {
|
|
|
1068
1084
|
body: JSON.stringify(body)
|
|
1069
1085
|
});
|
|
1070
1086
|
if (!response.ok) {
|
|
1071
|
-
if (response.status === 403) throw new Error(`
|
|
1072
|
-
throw new Error(`
|
|
1087
|
+
if (response.status === 403) throw new Error(`Google Analytics report failed with 403. Check access to property ${propertyId}.`);
|
|
1088
|
+
throw new Error(`Google Analytics report failed with ${response.status}.`);
|
|
1073
1089
|
}
|
|
1074
1090
|
const result = await response.json();
|
|
1075
1091
|
if (cacheable) db.prepare(`INSERT OR REPLACE INTO ga4_cache
|
|
@@ -1091,7 +1107,7 @@ function ga4RowsToObjects(result) {
|
|
|
1091
1107
|
return output;
|
|
1092
1108
|
});
|
|
1093
1109
|
}
|
|
1094
|
-
function ga4ReportQualityWarnings(result, label = "
|
|
1110
|
+
function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
|
|
1095
1111
|
const warnings = [];
|
|
1096
1112
|
if (result.metadata?.dataLossFromOtherRow) warnings.push(`${label} grouped high-cardinality data into (other).`);
|
|
1097
1113
|
if (result.metadata?.subjectToThresholding) warnings.push(`${label} was subject to Google Analytics thresholding.`);
|
|
@@ -1197,11 +1213,11 @@ function compareText$16(left, right) {
|
|
|
1197
1213
|
function metric(row, name) {
|
|
1198
1214
|
const value = row[name];
|
|
1199
1215
|
const parsed = Number(value);
|
|
1200
|
-
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `
|
|
1216
|
+
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
|
|
1201
1217
|
return parsed;
|
|
1202
1218
|
}
|
|
1203
1219
|
function dateValue$1(value) {
|
|
1204
|
-
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1220
|
+
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics returned an invalid date value.");
|
|
1205
1221
|
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
1206
1222
|
}
|
|
1207
1223
|
function addMetrics(target, source) {
|
|
@@ -1240,7 +1256,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1240
1256
|
const detailTotal = emptyMetrics();
|
|
1241
1257
|
for (const row of input.detailRows) {
|
|
1242
1258
|
const definition = aiReferralSourceForValue(row.sessionSource ?? "");
|
|
1243
|
-
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1259
|
+
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
|
|
1244
1260
|
const metrics = {
|
|
1245
1261
|
sessions: metric(row, "sessions"),
|
|
1246
1262
|
eventCount: metric(row, "eventCount")
|
|
@@ -1273,11 +1289,11 @@ function analyzeAiReferralRows(input) {
|
|
|
1273
1289
|
...evidenceReasons("Source discovery", input.sourceDiscovery),
|
|
1274
1290
|
...evidenceReasons("Landing-page detail", input.detail),
|
|
1275
1291
|
...evidenceReasons("Total users", input.totalUsersEvidence),
|
|
1276
|
-
...detailMismatch ? ["
|
|
1277
|
-
...timeZones.size > 1 ? ["
|
|
1292
|
+
...detailMismatch ? ["Google Analytics detail totals differ from source-discovery totals; landing-page and daily breakdowns are incomplete."] : [],
|
|
1293
|
+
...timeZones.size > 1 ? ["Google Analytics queries returned inconsistent property time zones."] : []
|
|
1278
1294
|
];
|
|
1279
1295
|
const dataStatus = partialReasons.length ? "partial" : "complete";
|
|
1280
|
-
const generalCaveat = "
|
|
1296
|
+
const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
|
|
1281
1297
|
const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
|
|
1282
1298
|
const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
|
|
1283
1299
|
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$16(left[0], right[0]))[0]?.[0];
|
|
@@ -1309,13 +1325,13 @@ function analyzeAiReferralRows(input) {
|
|
|
1309
1325
|
dataStatus,
|
|
1310
1326
|
range: input.range,
|
|
1311
1327
|
methodology: {
|
|
1312
|
-
id: "
|
|
1328
|
+
id: "google-analytics-ai-referrals",
|
|
1313
1329
|
version: 2,
|
|
1314
1330
|
attributionDimension: "sessionSource",
|
|
1315
1331
|
sourceRulesVersion: AI_REFERRAL_SOURCES_VERSION
|
|
1316
1332
|
},
|
|
1317
1333
|
dataSource: {
|
|
1318
|
-
provider: "google-analytics
|
|
1334
|
+
provider: "google-analytics",
|
|
1319
1335
|
api: "analyticsdata.v1beta.runReport",
|
|
1320
1336
|
maxRows: input.maxRows,
|
|
1321
1337
|
calls: input.sourceDiscovery.calls + input.detail.calls + input.totalUsersEvidence.calls,
|
|
@@ -1332,7 +1348,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1332
1348
|
totalUsersStatus: input.totalUsers === null ? "not-reported" : "available",
|
|
1333
1349
|
sources: sourceTotals.size,
|
|
1334
1350
|
landingPages: pageTotals.size,
|
|
1335
|
-
verdict: total.sessions ? dataStatus === "complete" ? "Detected AI referral traffic in
|
|
1351
|
+
verdict: total.sessions ? dataStatus === "complete" ? "Detected AI referral traffic in Google Analytics." : "Detected AI referral traffic; partial counts and breakdowns are lower bounds." : dataStatus === "complete" ? "No sessions attributed to the known AI session-source rules were detected." : "No AI-attributed sessions were found in retained Google Analytics data; this partial report cannot establish absence.",
|
|
1336
1352
|
caveat: generalCaveat
|
|
1337
1353
|
},
|
|
1338
1354
|
sources: [...sourceTotals.values()].map((source) => ({
|
|
@@ -1354,7 +1370,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1354
1370
|
})).sort((left, right) => compareText$16(left.date, right.date)),
|
|
1355
1371
|
caveats: [
|
|
1356
1372
|
generalCaveat,
|
|
1357
|
-
"Attribution uses
|
|
1373
|
+
"Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
|
|
1358
1374
|
"Source totals come from a low-cardinality source query; landing-page and daily detail is queried separately for the exact observed source values.",
|
|
1359
1375
|
"Total users come from a separate filtered aggregate when AI sources are observed and are never summed across report rows; complete zero-session evidence implies zero users without another query.",
|
|
1360
1376
|
...selection.omittedRows > 0 ? [`Landing-page output returns the ${selection.returnedRows} highest-session pages from ${selection.retainedRows} retained detail rows; ${selection.omittedRows} lower-ranked pages are omitted. Increase resultLimit to inspect more.`] : [],
|
|
@@ -1456,7 +1472,7 @@ function reportRange(input) {
|
|
|
1456
1472
|
const endDate = input.endDate ?? "yesterday";
|
|
1457
1473
|
const absolute = validIsoDate$1(startDate) && validIsoDate$1(endDate);
|
|
1458
1474
|
const relative = validRelativeDate(startDate) && validRelativeDate(endDate);
|
|
1459
|
-
if (!absolute && !relative) throw new SeoError("INVALID_INPUT", "startDate and endDate must both be YYYY-MM-DD dates or
|
|
1475
|
+
if (!absolute && !relative) throw new SeoError("INVALID_INPUT", "startDate and endDate must both be YYYY-MM-DD dates or Google Analytics relative dates such as 28daysAgo and yesterday.");
|
|
1460
1476
|
if (absolute && startDate > endDate) throw new SeoError("INVALID_INPUT", "startDate must be on or before endDate.");
|
|
1461
1477
|
return {
|
|
1462
1478
|
startDate,
|
|
@@ -1515,7 +1531,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1515
1531
|
const property = input.property.trim();
|
|
1516
1532
|
if (!property) throw new SeoError("INVALID_INPUT", "property is required.");
|
|
1517
1533
|
const range = reportRange(input);
|
|
1518
|
-
const
|
|
1534
|
+
const googleAnalyticsDateRange = {
|
|
1519
1535
|
startDate: range.startDate,
|
|
1520
1536
|
endDate: range.endDate
|
|
1521
1537
|
};
|
|
@@ -1525,7 +1541,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1525
1541
|
const sourceResult = await fetchAiReferralRows({
|
|
1526
1542
|
property,
|
|
1527
1543
|
request: {
|
|
1528
|
-
dateRanges: [
|
|
1544
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1529
1545
|
dimensions: [{ name: "sessionSource" }],
|
|
1530
1546
|
metrics: [{ name: "sessions" }, { name: "eventCount" }],
|
|
1531
1547
|
orderBys: [{
|
|
@@ -1550,7 +1566,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1550
1566
|
detailResult = await fetchAiReferralRows({
|
|
1551
1567
|
property,
|
|
1552
1568
|
request: {
|
|
1553
|
-
dateRanges: [
|
|
1569
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1554
1570
|
dimensions: [
|
|
1555
1571
|
{ name: "date" },
|
|
1556
1572
|
{ name: "sessionSource" },
|
|
@@ -1586,7 +1602,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1586
1602
|
usersResult = await fetchAiReferralRows({
|
|
1587
1603
|
property,
|
|
1588
1604
|
request: {
|
|
1589
|
-
dateRanges: [
|
|
1605
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1590
1606
|
metrics: [{ name: "totalUsers" }],
|
|
1591
1607
|
dimensionFilter
|
|
1592
1608
|
},
|
|
@@ -9414,7 +9430,7 @@ function crawlDefinitionId(input) {
|
|
|
9414
9430
|
const normalized = {
|
|
9415
9431
|
config: normalizeCrawlConfig(input.config),
|
|
9416
9432
|
site: input.site ?? null,
|
|
9417
|
-
|
|
9433
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId ?? null
|
|
9418
9434
|
};
|
|
9419
9435
|
return `crawl_def_${createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 20)}`;
|
|
9420
9436
|
}
|
|
@@ -9617,7 +9633,7 @@ function createCrawlReport(input) {
|
|
|
9617
9633
|
const definitionId = crawlDefinitionId({
|
|
9618
9634
|
config,
|
|
9619
9635
|
site: input.site,
|
|
9620
|
-
|
|
9636
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId
|
|
9621
9637
|
});
|
|
9622
9638
|
const safePagesWithLinks = sortPages(sanitizePages(deriveInternalLinkAuthority(input.pages ?? [], input.linkGraph)));
|
|
9623
9639
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
@@ -9630,7 +9646,7 @@ function createCrawlReport(input) {
|
|
|
9630
9646
|
definitionId,
|
|
9631
9647
|
projectId: input.projectId,
|
|
9632
9648
|
site: input.site,
|
|
9633
|
-
|
|
9649
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
9634
9650
|
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9635
9651
|
status: input.status ?? "completed",
|
|
9636
9652
|
configHash: crawlConfigHash(config),
|
|
@@ -9668,7 +9684,7 @@ function normalizeLoadedCrawlReport(report) {
|
|
|
9668
9684
|
definitionId: report.definitionId ?? crawlDefinitionId({
|
|
9669
9685
|
config: report.config,
|
|
9670
9686
|
site: report.site,
|
|
9671
|
-
|
|
9687
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId
|
|
9672
9688
|
}),
|
|
9673
9689
|
summary: summarizeCrawlReport({
|
|
9674
9690
|
pages,
|
|
@@ -10074,7 +10090,7 @@ async function joinAnalytics(input) {
|
|
|
10074
10090
|
limit: input.limit
|
|
10075
10091
|
});
|
|
10076
10092
|
} catch (error) {
|
|
10077
|
-
const warning = `
|
|
10093
|
+
const warning = `Google Analytics metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
10078
10094
|
input.warnings.push(warning);
|
|
10079
10095
|
return {
|
|
10080
10096
|
status: "unavailable",
|
|
@@ -10087,7 +10103,7 @@ async function joinAnalytics(input) {
|
|
|
10087
10103
|
};
|
|
10088
10104
|
}
|
|
10089
10105
|
if (analytics.warning) {
|
|
10090
|
-
const warning = `
|
|
10106
|
+
const warning = `Google Analytics metrics unavailable: ${analytics.warning}`;
|
|
10091
10107
|
input.warnings.push(warning);
|
|
10092
10108
|
return {
|
|
10093
10109
|
status: "unavailable",
|
|
@@ -10112,7 +10128,7 @@ async function joinAnalytics(input) {
|
|
|
10112
10128
|
const sourceQualityKnown = analytics.source?.dataStatus !== void 0;
|
|
10113
10129
|
const retainedRowLimitReached = analytics.source?.retainedRowLimitReached;
|
|
10114
10130
|
const qualityWarnings = analytics.source?.qualityWarnings ?? [];
|
|
10115
|
-
const warning = !sourceQualityKnown ? `
|
|
10131
|
+
const warning = !sourceQualityKnown ? `Google Analytics metrics joined for ${joinedPages} of ${input.pages.length} crawled pages, but the provider did not expose row completeness; missing page metrics are not reliable zero-traffic evidence.` : retainedRowLimitReached ? `Google Analytics retained-row limit reached; missing page metrics are not reliable zero-traffic evidence.` : qualityWarnings.length ? `${qualityWarnings.join(" ")} Missing page metrics and relative analytics value are partial evidence.` : input.pages.length && joinedPages === 0 ? "Google Analytics metrics joined for 0 crawled pages." : void 0;
|
|
10116
10132
|
if (warning) input.warnings.push(warning);
|
|
10117
10133
|
return {
|
|
10118
10134
|
status: !sourceQualityKnown || analytics.source?.dataStatus === "partial" ? "partial" : joinedPages ? "joined" : "none",
|
|
@@ -10272,7 +10288,7 @@ async function crawlDataSources(input) {
|
|
|
10272
10288
|
joinedPages: 0,
|
|
10273
10289
|
retainedRowLimit: input.analyticsLimit,
|
|
10274
10290
|
retainedRowLimitReached: false,
|
|
10275
|
-
warning: input.cancelled ? "
|
|
10291
|
+
warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
10276
10292
|
}
|
|
10277
10293
|
};
|
|
10278
10294
|
if (!input.cancelled && input.site) dataSources.searchConsole = await joinSearchMetrics({
|
|
@@ -10288,8 +10304,8 @@ async function crawlDataSources(input) {
|
|
|
10288
10304
|
queryPagesMetricsBatch: input.queryPagesMetricsBatch,
|
|
10289
10305
|
queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
|
|
10290
10306
|
});
|
|
10291
|
-
if (!input.cancelled && input.
|
|
10292
|
-
propertyId: input.
|
|
10307
|
+
if (!input.cancelled && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
|
|
10308
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
10293
10309
|
pages: input.pages,
|
|
10294
10310
|
warnings: input.warnings,
|
|
10295
10311
|
limit: input.analyticsLimit,
|
|
@@ -11111,11 +11127,11 @@ function landingPageRankingPolicy(input) {
|
|
|
11111
11127
|
};
|
|
11112
11128
|
const warningSuffix = "Observed landing-page values remain visible but do not affect priority scores.";
|
|
11113
11129
|
const warnings = [
|
|
11114
|
-
...input.warning ? [`
|
|
11115
|
-
...input.source?.retainedRowLimitReached ? [`
|
|
11130
|
+
...input.warning ? [`Google Analytics: ${input.warning}`] : [],
|
|
11131
|
+
...input.source?.retainedRowLimitReached ? [`Google Analytics: the retained-row limit was reached. ${warningSuffix}`] : [],
|
|
11116
11132
|
...input.source?.qualityWarnings?.map((warning) => `${warning} ${warningSuffix}`) ?? []
|
|
11117
11133
|
];
|
|
11118
|
-
if (!input.source?.dataStatus && !input.warning) warnings.push(`
|
|
11134
|
+
if (!input.source?.dataStatus && !input.warning) warnings.push(`Google Analytics: landing-page completeness was not reported. ${warningSuffix}`);
|
|
11119
11135
|
return {
|
|
11120
11136
|
canRank: !input.warning && warnings.length === 0 && landingPageValuesCanRank(input.source),
|
|
11121
11137
|
warnings
|
|
@@ -11155,7 +11171,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
|
11155
11171
|
const values = landingPageValuesFromRows(rows);
|
|
11156
11172
|
const availableRows = result.rowCount;
|
|
11157
11173
|
const retainedRowLimitReached = availableRows !== void 0 && availableRows > rows.length || rows.length >= retainedRowLimit;
|
|
11158
|
-
const qualityWarnings = ga4ReportQualityWarnings(result, "
|
|
11174
|
+
const qualityWarnings = ga4ReportQualityWarnings(result, "Google Analytics landing-page report");
|
|
11159
11175
|
return {
|
|
11160
11176
|
values,
|
|
11161
11177
|
source: {
|
|
@@ -11909,7 +11925,7 @@ async function crawlSite(input, dependencies) {
|
|
|
11909
11925
|
const dataSources = await crawlDataSources({
|
|
11910
11926
|
cancelled,
|
|
11911
11927
|
site,
|
|
11912
|
-
|
|
11928
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
11913
11929
|
pages,
|
|
11914
11930
|
warnings,
|
|
11915
11931
|
searchMetricsLimit,
|
|
@@ -11964,7 +11980,7 @@ async function crawlSite(input, dependencies) {
|
|
|
11964
11980
|
config,
|
|
11965
11981
|
projectId: input.projectId,
|
|
11966
11982
|
site,
|
|
11967
|
-
|
|
11983
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
11968
11984
|
dataSources,
|
|
11969
11985
|
pages,
|
|
11970
11986
|
requests,
|
|
@@ -12107,7 +12123,7 @@ function searchValueForGroup(report, urls) {
|
|
|
12107
12123
|
};
|
|
12108
12124
|
}
|
|
12109
12125
|
function whyThisRanks(input, recommendation, searchStatus) {
|
|
12110
|
-
return `${input.searchVisibleUrls ? `${input.searchVisibleUrls} affected URLs have GSC visibility (${input.clicks} clicks, ${input.impressions} impressions).` : searchStatus && !["joined", "none"].includes(searchStatus) ? `No joined GSC visibility is available for these affected URLs; Search Console evidence is ${searchStatus}.` : "No affected URL has joined GSC visibility yet."}${input.sessions ? `
|
|
12126
|
+
return `${input.searchVisibleUrls ? `${input.searchVisibleUrls} affected URLs have GSC visibility (${input.clicks} clicks, ${input.impressions} impressions).` : searchStatus && !["joined", "none"].includes(searchStatus) ? `No joined GSC visibility is available for these affected URLs; Search Console evidence is ${searchStatus}.` : "No affected URL has joined GSC visibility yet."}${input.sessions ? ` Google Analytics adds ${input.sessions} sessions and ${input.conversions} conversions from affected landing pages.` : ""} ${recommendation === "review" ? "This is a review observation. Confirm it before scheduling implementation work." : `This ranks as a fix. Severity is ${input.severity}; ${input.affectedUrls} URL${input.affectedUrls === 1 ? " is" : "s are"} affected; estimated effort is ${input.effort}.`}`;
|
|
12111
12127
|
}
|
|
12112
12128
|
function verificationCommand(report, severity) {
|
|
12113
12129
|
return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
|
|
@@ -12223,7 +12239,7 @@ function snapshotInput(report) {
|
|
|
12223
12239
|
generatedAt: report.generatedAt,
|
|
12224
12240
|
projectId: report.projectId,
|
|
12225
12241
|
site: report.site,
|
|
12226
|
-
|
|
12242
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId,
|
|
12227
12243
|
url: report.config.url,
|
|
12228
12244
|
status: report.status,
|
|
12229
12245
|
configHash: report.configHash,
|
|
@@ -13861,7 +13877,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
13861
13877
|
definitionId: z.string(),
|
|
13862
13878
|
projectId: z.string().optional(),
|
|
13863
13879
|
site: z.string().optional(),
|
|
13864
|
-
|
|
13880
|
+
googleAnalyticsPropertyId: z.string().optional(),
|
|
13865
13881
|
generatedAt: z.string().datetime(),
|
|
13866
13882
|
status: z.enum([
|
|
13867
13883
|
"completed",
|
|
@@ -17727,7 +17743,7 @@ function stringMatchType(matchType) {
|
|
|
17727
17743
|
if (matchType === "equals") return "EXACT";
|
|
17728
17744
|
return "CONTAINS";
|
|
17729
17745
|
}
|
|
17730
|
-
function
|
|
17746
|
+
function googleAnalyticsLandingPageFilterForChange(change, group) {
|
|
17731
17747
|
if (change.scope === "site") return void 0;
|
|
17732
17748
|
if (change.scope === "query") return void 0;
|
|
17733
17749
|
if (change.scope === "group") {
|
|
@@ -17750,7 +17766,7 @@ function ga4LandingPageFilterForChange(change, group) {
|
|
|
17750
17766
|
}
|
|
17751
17767
|
} };
|
|
17752
17768
|
}
|
|
17753
|
-
function
|
|
17769
|
+
function summarizeGoogleAnalyticsRows(rows) {
|
|
17754
17770
|
return rows.reduce((sum, row) => ({
|
|
17755
17771
|
sessions: fixed(sum.sessions + (Number(row.sessions) || 0)),
|
|
17756
17772
|
engagedSessions: fixed(sum.engagedSessions + (Number(row.engagedSessions) || 0)),
|
|
@@ -17817,8 +17833,8 @@ function classify(input) {
|
|
|
17817
17833
|
}
|
|
17818
17834
|
//#endregion
|
|
17819
17835
|
//#region packages/core/dist/analyze/experiments/measurement/ga4-change-provider.js
|
|
17820
|
-
async function
|
|
17821
|
-
const result = await input.
|
|
17836
|
+
async function queryGoogleAnalyticsMetrics(input) {
|
|
17837
|
+
const result = await input.googleAnalyticsReport(input.propertyId, {
|
|
17822
17838
|
dateRanges: [input.window],
|
|
17823
17839
|
dimensions: [{ name: "date" }],
|
|
17824
17840
|
metrics: [
|
|
@@ -17832,7 +17848,7 @@ async function queryGa4Metrics(input) {
|
|
|
17832
17848
|
}, { refresh: input.refresh });
|
|
17833
17849
|
const rows = ga4RowsToObjects(result);
|
|
17834
17850
|
return {
|
|
17835
|
-
metrics:
|
|
17851
|
+
metrics: summarizeGoogleAnalyticsRows(rows),
|
|
17836
17852
|
source: {
|
|
17837
17853
|
rows: rows.length,
|
|
17838
17854
|
rowCount: result.rowCount ?? rows.length,
|
|
@@ -17842,19 +17858,19 @@ async function queryGa4Metrics(input) {
|
|
|
17842
17858
|
warnings: ga4ReportQualityWarnings(result, input.label)
|
|
17843
17859
|
};
|
|
17844
17860
|
}
|
|
17845
|
-
async function
|
|
17846
|
-
const [before, after] = await Promise.all([
|
|
17861
|
+
async function queryGoogleAnalyticsChangeWindows(input) {
|
|
17862
|
+
const [before, after] = await Promise.all([queryGoogleAnalyticsMetrics({
|
|
17847
17863
|
...input,
|
|
17848
17864
|
window: input.before,
|
|
17849
|
-
label: "
|
|
17850
|
-
}),
|
|
17865
|
+
label: "Google Analytics before window"
|
|
17866
|
+
}), queryGoogleAnalyticsMetrics({
|
|
17851
17867
|
...input,
|
|
17852
17868
|
window: input.after,
|
|
17853
|
-
label: "
|
|
17869
|
+
label: "Google Analytics after window"
|
|
17854
17870
|
})]);
|
|
17855
17871
|
const metadataWarnings = [];
|
|
17856
|
-
if (before.source.timeZone && after.source.timeZone && before.source.timeZone !== after.source.timeZone) metadataWarnings.push("
|
|
17857
|
-
if (before.source.currencyCode && after.source.currencyCode && before.source.currencyCode !== after.source.currencyCode) metadataWarnings.push("
|
|
17872
|
+
if (before.source.timeZone && after.source.timeZone && before.source.timeZone !== after.source.timeZone) metadataWarnings.push("Google Analytics returned different property timezones across the two windows.");
|
|
17873
|
+
if (before.source.currencyCode && after.source.currencyCode && before.source.currencyCode !== after.source.currencyCode) metadataWarnings.push("Google Analytics returned different currency codes across the two windows.");
|
|
17858
17874
|
return {
|
|
17859
17875
|
report: {
|
|
17860
17876
|
propertyId: input.propertyId,
|
|
@@ -17870,7 +17886,7 @@ async function queryGa4ChangeWindows(input) {
|
|
|
17870
17886
|
before: before.metrics,
|
|
17871
17887
|
after: after.metrics
|
|
17872
17888
|
}),
|
|
17873
|
-
note: "
|
|
17889
|
+
note: "Google Analytics attribution is landing-page based. Query-level tests use GSC only."
|
|
17874
17890
|
},
|
|
17875
17891
|
source: {
|
|
17876
17892
|
before: before.source,
|
|
@@ -18108,7 +18124,7 @@ function measurementWindow(input) {
|
|
|
18108
18124
|
//#region packages/core/dist/analyze/experiments/measure-change.js
|
|
18109
18125
|
const defaultDependencies$5 = {
|
|
18110
18126
|
searchAnalytics: querySearchAnalytics,
|
|
18111
|
-
|
|
18127
|
+
googleAnalyticsReport: runGa4Report,
|
|
18112
18128
|
contentGroup: getContentGroup,
|
|
18113
18129
|
now: () => /* @__PURE__ */ new Date()
|
|
18114
18130
|
};
|
|
@@ -18194,15 +18210,15 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
18194
18210
|
confidence: "medium",
|
|
18195
18211
|
note: `${classified.note} Confidence is capped because Search Console evidence is partial.`
|
|
18196
18212
|
} : classified;
|
|
18197
|
-
const
|
|
18213
|
+
const googleAnalyticsFilter = googleAnalyticsLandingPageFilterForChange(change, group);
|
|
18198
18214
|
const queryDimension = change.scope === "query" || group?.dimension === "query";
|
|
18199
|
-
const analytics = input.
|
|
18200
|
-
propertyId: input.
|
|
18215
|
+
const analytics = input.googleAnalyticsPropertyId && !queryDimension ? await queryGoogleAnalyticsChangeWindows({
|
|
18216
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
18201
18217
|
before,
|
|
18202
18218
|
after,
|
|
18203
|
-
filter:
|
|
18219
|
+
filter: googleAnalyticsFilter,
|
|
18204
18220
|
refresh: input.refresh,
|
|
18205
|
-
|
|
18221
|
+
googleAnalyticsReport: dependencies.googleAnalyticsReport
|
|
18206
18222
|
}) : void 0;
|
|
18207
18223
|
const warnings = [
|
|
18208
18224
|
...measured.warnings,
|
|
@@ -18281,7 +18297,7 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
18281
18297
|
...window.afterWindowTruncated ? [`The requested ${window.requestedDays}-day after window is incomplete, so this run is partial and has no directional verdict.`] : [],
|
|
18282
18298
|
...shortWindow ? ["Fewer than 7 finalized days per window is too short for a directional verdict."] : [],
|
|
18283
18299
|
...measured.completeness === "retained-query-date-aggregates" ? ["Query-scoped Search Console data can omit anonymized queries; an absent retained row is not zero traffic."] : [],
|
|
18284
|
-
...analytics ? [`
|
|
18300
|
+
...analytics ? [`Google Analytics attribution uses landing pages and the Google Analytics property timezone${analytics.source.before.timeZone ? ` (${analytics.source.before.timeZone})` : ""}; its day boundaries may differ from Search Console Pacific dates.`] : []
|
|
18285
18301
|
]
|
|
18286
18302
|
};
|
|
18287
18303
|
}
|
|
@@ -23866,7 +23882,7 @@ function reportCaveats(input) {
|
|
|
23866
23882
|
`Brand queries: ${input.includeBrand ? "included" : input.brandTerms?.length ? "excluded where saved brand terms matched" : "no saved brand terms, so no brand filter was applied"}.`,
|
|
23867
23883
|
`Data freshness: ${input.refresh ? "fresh fetch requested; local cache bypassed where supported" : "local cache allowed; rerun with --refresh to bypass cached GSC/HTTP data"}.`,
|
|
23868
23884
|
input.skipSearchData ? "Search Console: skipped because no property was selected. Run `seo start` to add performance evidence." : "Search Console: included where the selected property returned usable data.",
|
|
23869
|
-
"
|
|
23885
|
+
"Google Analytics: not included in this narrative; use refresh-priorities when Google Analytics value should influence prioritisation.",
|
|
23870
23886
|
input.skipSearchData ? "Content verification: not run because Search Console did not provide page candidates for this report." : input.verifyContent ? `Content verification: checked ${input.verified} of ${countLabel(input.quickWinCount, "quick-win candidate")}, limit ${input.verifyLimit ?? 3}.` : "Content verification: not run; recommendations are based on GSC/query data unless stated otherwise."
|
|
23871
23887
|
];
|
|
23872
23888
|
}
|
|
@@ -24988,12 +25004,12 @@ async function refreshPrioritiesWorkflow(input) {
|
|
|
24988
25004
|
});
|
|
24989
25005
|
const range = defaultDateRange$1(input.days ?? 28);
|
|
24990
25006
|
const analytics = await fetchLandingPageValues({
|
|
24991
|
-
propertyId: input.
|
|
25007
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
24992
25008
|
startDate: range.startDate,
|
|
24993
25009
|
endDate: range.endDate
|
|
24994
25010
|
});
|
|
24995
25011
|
const analyticsPolicy = landingPageRankingPolicy({
|
|
24996
|
-
propertyId: input.
|
|
25012
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
24997
25013
|
source: analytics.source,
|
|
24998
25014
|
warning: analytics.warning
|
|
24999
25015
|
});
|
|
@@ -25590,7 +25606,7 @@ async function runDoctor() {
|
|
|
25590
25606
|
const grantedScopes = new Set(tokens?.scope.split(/\s+/).filter(Boolean) ?? []);
|
|
25591
25607
|
for (const scope of REQUIRED_SCOPES) checks.push({
|
|
25592
25608
|
id: `scope:${scope}`,
|
|
25593
|
-
label: scope.endsWith("webmasters.readonly") ? "
|
|
25609
|
+
label: scope.endsWith("webmasters.readonly") ? "Search Console scope" : "Google Analytics scope",
|
|
25594
25610
|
status: serviceAccount.configured || tokens && grantedScopes.has(scope) ? "pass" : tokens ? "fail" : "warn",
|
|
25595
25611
|
detail: serviceAccount.configured ? `${scope} requested by the service account.` : tokens ? grantedScopes.has(scope) ? scope : `Missing ${scope}.` : "Cannot inspect scopes until login is complete.",
|
|
25596
25612
|
fix: !serviceAccount.configured && tokens && !grantedScopes.has(scope) ? "Run `seo auth logout` then `seo auth login`." : void 0
|
|
@@ -25602,10 +25618,10 @@ async function runDoctor() {
|
|
|
25602
25618
|
detail: config.defaultSite ?? "No saved default. Human CLI commands will prompt; agents can pass --site."
|
|
25603
25619
|
});
|
|
25604
25620
|
checks.push({
|
|
25605
|
-
id: "default-
|
|
25606
|
-
label: "Saved
|
|
25621
|
+
id: "default-google-analytics",
|
|
25622
|
+
label: "Saved Google Analytics property",
|
|
25607
25623
|
status: "pass",
|
|
25608
|
-
detail: config.google.
|
|
25624
|
+
detail: config.analytics.google.defaultPropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
|
|
25609
25625
|
});
|
|
25610
25626
|
return {
|
|
25611
25627
|
ok: checks.every((check) => check.status !== "fail"),
|
|
@@ -25867,6 +25883,317 @@ new LocalCredentialsProvider();
|
|
|
25867
25883
|
//#endregion
|
|
25868
25884
|
//#region packages/core/dist/version.js
|
|
25869
25885
|
const SEO_VERSION = version;
|
|
25886
|
+
const TELEMETRY_TIMEOUT_MS = 2e3;
|
|
25887
|
+
const TELEMETRY_REPORTS = [
|
|
25888
|
+
"affected-urls",
|
|
25889
|
+
"agent-readiness",
|
|
25890
|
+
"ai-readiness",
|
|
25891
|
+
"ai-referrals",
|
|
25892
|
+
"ai-search-scorecard",
|
|
25893
|
+
"audit-page",
|
|
25894
|
+
"audit-urls",
|
|
25895
|
+
"cannibalisation",
|
|
25896
|
+
"community-intent",
|
|
25897
|
+
"compare-crawls",
|
|
25898
|
+
"content-optimization",
|
|
25899
|
+
"crawl-diff",
|
|
25900
|
+
"site-crawl",
|
|
25901
|
+
"ctr-underperformers",
|
|
25902
|
+
"decaying-pages",
|
|
25903
|
+
"setup-check",
|
|
25904
|
+
"entity-readiness",
|
|
25905
|
+
"explain-crawl-issue",
|
|
25906
|
+
"geo-gaps",
|
|
25907
|
+
"crawl-report",
|
|
25908
|
+
"index-coverage",
|
|
25909
|
+
"index-coverage-plan",
|
|
25910
|
+
"index-monitor",
|
|
25911
|
+
"index-watch",
|
|
25912
|
+
"internal-links",
|
|
25913
|
+
"link-recovery",
|
|
25914
|
+
"crawl-history",
|
|
25915
|
+
"crawler-rules",
|
|
25916
|
+
"llms-txt-audit",
|
|
25917
|
+
"generate-llms-txt",
|
|
25918
|
+
"measure-change",
|
|
25919
|
+
"monthly-report",
|
|
25920
|
+
"okf-build",
|
|
25921
|
+
"okf-validate",
|
|
25922
|
+
"page-opportunities",
|
|
25923
|
+
"performance-audit",
|
|
25924
|
+
"pseo-audit",
|
|
25925
|
+
"query-clusters",
|
|
25926
|
+
"quick-wins",
|
|
25927
|
+
"redirect-trace",
|
|
25928
|
+
"narrative-report",
|
|
25929
|
+
"second-page",
|
|
25930
|
+
"segment-impact",
|
|
25931
|
+
"striking-distance",
|
|
25932
|
+
"seo-to-ai-query",
|
|
25933
|
+
"top-fixes",
|
|
25934
|
+
"traffic-anomaly",
|
|
25935
|
+
"update-correlation",
|
|
25936
|
+
"search-performance-overview",
|
|
25937
|
+
"monthly-action-plan",
|
|
25938
|
+
"refresh-priorities",
|
|
25939
|
+
"technical-watch",
|
|
25940
|
+
"update-postmortem"
|
|
25941
|
+
];
|
|
25942
|
+
const RETENTION_MILESTONES = [
|
|
25943
|
+
{
|
|
25944
|
+
days: 1,
|
|
25945
|
+
event: "active_d1"
|
|
25946
|
+
},
|
|
25947
|
+
{
|
|
25948
|
+
days: 7,
|
|
25949
|
+
event: "active_d7"
|
|
25950
|
+
},
|
|
25951
|
+
{
|
|
25952
|
+
days: 30,
|
|
25953
|
+
event: "active_d30"
|
|
25954
|
+
}
|
|
25955
|
+
];
|
|
25956
|
+
const CI_ENVIRONMENT_KEYS = [
|
|
25957
|
+
"GITHUB_ACTIONS",
|
|
25958
|
+
"GITLAB_CI",
|
|
25959
|
+
"CIRCLECI",
|
|
25960
|
+
"TRAVIS",
|
|
25961
|
+
"BUILDKITE",
|
|
25962
|
+
"JENKINS_URL",
|
|
25963
|
+
"TEAMCITY_VERSION",
|
|
25964
|
+
"TF_BUILD",
|
|
25965
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
25966
|
+
"CODEBUILD_BUILD_ID"
|
|
25967
|
+
];
|
|
25968
|
+
const TELEMETRY_NOTICE = `SEO Skill collects anonymous usage data to improve the tool (event name,
|
|
25969
|
+
report id, version, agent, OS, architecture, Node major, and install week;
|
|
25970
|
+
never URLs, report data, or identifiers of any kind).
|
|
25971
|
+
Docs: https://seoskill.dev/telemetry
|
|
25972
|
+
Disable: seo telemetry disable (or set DO_NOT_TRACK=1)\n`;
|
|
25973
|
+
function optionEnv(options) {
|
|
25974
|
+
return options.env ?? process.env;
|
|
25975
|
+
}
|
|
25976
|
+
function optionNow(options) {
|
|
25977
|
+
return options.now ?? /* @__PURE__ */ new Date();
|
|
25978
|
+
}
|
|
25979
|
+
function optionStateFile(options) {
|
|
25980
|
+
return options.stateFile ?? getSeoCliPaths().telemetryStateFile;
|
|
25981
|
+
}
|
|
25982
|
+
function isEnvEnabled(value) {
|
|
25983
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
25984
|
+
}
|
|
25985
|
+
function isTelemetryCi(env = process.env) {
|
|
25986
|
+
return env.NODE_ENV === "test" || Boolean(env.NODE_TEST_CONTEXT) || isEnvEnabled(env.CI) || CI_ENVIRONMENT_KEYS.some((key) => Boolean(env[key]));
|
|
25987
|
+
}
|
|
25988
|
+
function environmentDisableReason(env) {
|
|
25989
|
+
if (isTelemetryCi(env)) return "ci";
|
|
25990
|
+
if (isEnvEnabled(env.DO_NOT_TRACK)) return "do_not_track";
|
|
25991
|
+
if (isEnvEnabled(env.SEOSKILL_TELEMETRY_DISABLED) || isEnvEnabled(env.SEO_TELEMETRY_DISABLED)) return "environment";
|
|
25992
|
+
}
|
|
25993
|
+
function isTelemetryMilestone(value) {
|
|
25994
|
+
return typeof value === "string" && [
|
|
25995
|
+
"first_run",
|
|
25996
|
+
"setup_complete",
|
|
25997
|
+
"first_audit_complete",
|
|
25998
|
+
"active_d1",
|
|
25999
|
+
"active_d7",
|
|
26000
|
+
"active_d30"
|
|
26001
|
+
].includes(value);
|
|
26002
|
+
}
|
|
26003
|
+
function isTelemetryReport(value) {
|
|
26004
|
+
return typeof value === "string" && TELEMETRY_REPORTS.includes(value);
|
|
26005
|
+
}
|
|
26006
|
+
function isValidCohort(value) {
|
|
26007
|
+
return typeof value === "string" && /^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(value);
|
|
26008
|
+
}
|
|
26009
|
+
function isTelemetryState(value) {
|
|
26010
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
26011
|
+
const state = value;
|
|
26012
|
+
return typeof state.telemetryEnabled === "boolean" && typeof state.firstRunAt === "string" && Number.isFinite(Date.parse(state.firstRunAt)) && isValidCohort(state.cohort) && Array.isArray(state.sentMilestones) && state.sentMilestones.every(isTelemetryMilestone);
|
|
26013
|
+
}
|
|
26014
|
+
function readState(path) {
|
|
26015
|
+
if (!existsSync(path)) return { kind: "missing" };
|
|
26016
|
+
const value = readJsonFile(path);
|
|
26017
|
+
return isTelemetryState(value) ? {
|
|
26018
|
+
kind: "valid",
|
|
26019
|
+
state: value
|
|
26020
|
+
} : { kind: "invalid" };
|
|
26021
|
+
}
|
|
26022
|
+
function writeState(path, state) {
|
|
26023
|
+
writeJsonAtomic(path, state, 384);
|
|
26024
|
+
}
|
|
26025
|
+
function isoWeek(date) {
|
|
26026
|
+
const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
26027
|
+
const day = utc.getUTCDay() || 7;
|
|
26028
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - day);
|
|
26029
|
+
const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
|
|
26030
|
+
const week = Math.ceil(((utc.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
26031
|
+
return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
26032
|
+
}
|
|
26033
|
+
function newState(options, enabled = true) {
|
|
26034
|
+
const now = optionNow(options);
|
|
26035
|
+
return {
|
|
26036
|
+
telemetryEnabled: enabled,
|
|
26037
|
+
firstRunAt: now.toISOString(),
|
|
26038
|
+
cohort: isoWeek(now),
|
|
26039
|
+
sentMilestones: []
|
|
26040
|
+
};
|
|
26041
|
+
}
|
|
26042
|
+
function detectTelemetryAgent(input = {}) {
|
|
26043
|
+
const env = input.env ?? process.env;
|
|
26044
|
+
const clientName = input.clientName?.toLowerCase() ?? "";
|
|
26045
|
+
if (clientName.includes("claude")) return "claude-code";
|
|
26046
|
+
if (clientName.includes("cursor")) return "cursor";
|
|
26047
|
+
if (clientName.includes("codex")) return "codex";
|
|
26048
|
+
if (input.clientName) return "unknown";
|
|
26049
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude-code";
|
|
26050
|
+
if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
|
|
26051
|
+
if (env.CODEX_HOME || env.CODEX_THREAD_ID || env.CODEX_SANDBOX) return "codex";
|
|
26052
|
+
return "cli";
|
|
26053
|
+
}
|
|
26054
|
+
function telemetryAgent(options) {
|
|
26055
|
+
return options.agent ?? detectTelemetryAgent({
|
|
26056
|
+
clientName: options.clientName,
|
|
26057
|
+
env: optionEnv(options)
|
|
26058
|
+
});
|
|
26059
|
+
}
|
|
26060
|
+
function nodeMajor(version) {
|
|
26061
|
+
return version.replace(/^v/, "").split(".")[0] || "unknown";
|
|
26062
|
+
}
|
|
26063
|
+
function buildTelemetryPayload(event, state, fields = {}, options = {}) {
|
|
26064
|
+
return {
|
|
26065
|
+
event,
|
|
26066
|
+
version: options.version ?? SEO_VERSION,
|
|
26067
|
+
agent: telemetryAgent(options),
|
|
26068
|
+
os: options.processPlatform ?? platform(),
|
|
26069
|
+
arch: options.processArch ?? arch(),
|
|
26070
|
+
node: nodeMajor(options.nodeVersion ?? process.version),
|
|
26071
|
+
cohort: state.cohort,
|
|
26072
|
+
schema: 1,
|
|
26073
|
+
...fields.errorCategory ? { errorCategory: fields.errorCategory } : {},
|
|
26074
|
+
...fields.report ? { report: fields.report } : {}
|
|
26075
|
+
};
|
|
26076
|
+
}
|
|
26077
|
+
function dispatchTelemetry(payload, options) {
|
|
26078
|
+
const fetchTelemetry = options.fetch ?? globalThis.fetch;
|
|
26079
|
+
try {
|
|
26080
|
+
fetchTelemetry(options.endpoint ?? "https://seoskill.dev/api/t", {
|
|
26081
|
+
method: "POST",
|
|
26082
|
+
headers: { "content-type": "application/json" },
|
|
26083
|
+
body: JSON.stringify(payload),
|
|
26084
|
+
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS)
|
|
26085
|
+
}).catch(() => void 0);
|
|
26086
|
+
} catch {}
|
|
26087
|
+
}
|
|
26088
|
+
function statusFromState(options) {
|
|
26089
|
+
const stateFile = optionStateFile(options);
|
|
26090
|
+
const envReason = environmentDisableReason(optionEnv(options));
|
|
26091
|
+
const stored = readState(stateFile);
|
|
26092
|
+
if (envReason) return {
|
|
26093
|
+
enabled: false,
|
|
26094
|
+
reason: envReason,
|
|
26095
|
+
stateFile,
|
|
26096
|
+
...stored.kind === "valid" ? { state: stored.state } : {}
|
|
26097
|
+
};
|
|
26098
|
+
if (stored.kind === "invalid") return {
|
|
26099
|
+
enabled: false,
|
|
26100
|
+
reason: "invalid_state",
|
|
26101
|
+
stateFile
|
|
26102
|
+
};
|
|
26103
|
+
if (stored.kind === "valid" && !stored.state.telemetryEnabled) return {
|
|
26104
|
+
enabled: false,
|
|
26105
|
+
reason: "local_setting",
|
|
26106
|
+
stateFile,
|
|
26107
|
+
state: stored.state
|
|
26108
|
+
};
|
|
26109
|
+
return {
|
|
26110
|
+
enabled: true,
|
|
26111
|
+
stateFile,
|
|
26112
|
+
...stored.kind === "valid" ? { state: stored.state } : {}
|
|
26113
|
+
};
|
|
26114
|
+
}
|
|
26115
|
+
function enabledState(options) {
|
|
26116
|
+
const status = statusFromState(options);
|
|
26117
|
+
return status.enabled ? status.state : void 0;
|
|
26118
|
+
}
|
|
26119
|
+
function initializeTelemetry(options = {}) {
|
|
26120
|
+
const initialStatus = statusFromState(options);
|
|
26121
|
+
if (!initialStatus.enabled) return initialStatus;
|
|
26122
|
+
const stateFile = initialStatus.stateFile;
|
|
26123
|
+
const state = initialStatus.state ?? newState(options);
|
|
26124
|
+
const events = [];
|
|
26125
|
+
if (!state.sentMilestones.includes("first_run")) {
|
|
26126
|
+
try {
|
|
26127
|
+
(options.notice ?? ((message) => process.stderr.write(message)))(TELEMETRY_NOTICE);
|
|
26128
|
+
} catch {
|
|
26129
|
+
return {
|
|
26130
|
+
enabled: false,
|
|
26131
|
+
reason: "invalid_state",
|
|
26132
|
+
stateFile
|
|
26133
|
+
};
|
|
26134
|
+
}
|
|
26135
|
+
state.sentMilestones.push("first_run");
|
|
26136
|
+
events.push("first_run");
|
|
26137
|
+
}
|
|
26138
|
+
const daysSinceFirstRun = (optionNow(options).getTime() - Date.parse(state.firstRunAt)) / 864e5;
|
|
26139
|
+
for (const milestone of RETENTION_MILESTONES) if (daysSinceFirstRun >= milestone.days && !state.sentMilestones.includes(milestone.event)) {
|
|
26140
|
+
state.sentMilestones.push(milestone.event);
|
|
26141
|
+
events.push(milestone.event);
|
|
26142
|
+
}
|
|
26143
|
+
try {
|
|
26144
|
+
writeState(stateFile, state);
|
|
26145
|
+
} catch {
|
|
26146
|
+
return {
|
|
26147
|
+
enabled: false,
|
|
26148
|
+
reason: "invalid_state",
|
|
26149
|
+
stateFile
|
|
26150
|
+
};
|
|
26151
|
+
}
|
|
26152
|
+
for (const event of events) dispatchTelemetry(buildTelemetryPayload(event, state, {}, options), options);
|
|
26153
|
+
return {
|
|
26154
|
+
enabled: true,
|
|
26155
|
+
stateFile,
|
|
26156
|
+
state
|
|
26157
|
+
};
|
|
26158
|
+
}
|
|
26159
|
+
function sendEvent(event, fields, options) {
|
|
26160
|
+
const state = enabledState(options);
|
|
26161
|
+
if (!state) return;
|
|
26162
|
+
dispatchTelemetry(buildTelemetryPayload(event, state, fields, options), options);
|
|
26163
|
+
}
|
|
26164
|
+
function trackTelemetryReportStart(report, options = {}) {
|
|
26165
|
+
if (!isTelemetryReport(report)) return;
|
|
26166
|
+
sendEvent("audit_start", { report }, options);
|
|
26167
|
+
}
|
|
26168
|
+
function trackTelemetryReportComplete(report, options = {}) {
|
|
26169
|
+
if (!isTelemetryReport(report)) return;
|
|
26170
|
+
const state = enabledState(options);
|
|
26171
|
+
if (!state) return;
|
|
26172
|
+
dispatchTelemetry(buildTelemetryPayload("audit_complete", state, { report }, options), options);
|
|
26173
|
+
if (state.sentMilestones.includes("first_audit_complete")) return;
|
|
26174
|
+
state.sentMilestones.push("first_audit_complete");
|
|
26175
|
+
try {
|
|
26176
|
+
writeState(optionStateFile(options), state);
|
|
26177
|
+
} catch {
|
|
26178
|
+
return;
|
|
26179
|
+
}
|
|
26180
|
+
dispatchTelemetry(buildTelemetryPayload("first_audit_complete", state, { report }, options), options);
|
|
26181
|
+
}
|
|
26182
|
+
function trackTelemetryReportFailed(report, errorCategory, options = {}) {
|
|
26183
|
+
if (!isTelemetryReport(report)) return;
|
|
26184
|
+
sendEvent("audit_failed", {
|
|
26185
|
+
errorCategory,
|
|
26186
|
+
report
|
|
26187
|
+
}, options);
|
|
26188
|
+
}
|
|
26189
|
+
function telemetryErrorCategory(error) {
|
|
26190
|
+
if (error instanceof Error && error.name === "AbortError") return "crawl_timeout";
|
|
26191
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
26192
|
+
if (code === "ACCESS_DENIED" || code === "AUTH_CONFIG_REQUIRED" || code === "AUTH_EXPIRED" || code === "AUTH_REQUIRED") return "auth";
|
|
26193
|
+
if (code === "INVALID_INPUT" || code === "PROPERTY_NOT_FOUND") return "config";
|
|
26194
|
+
if (code === "OPTIONAL_PROVIDER_UNAVAILABLE" || code === "PROVIDER_UNAVAILABLE" || code === "RATE_LIMITED") return "network";
|
|
26195
|
+
return "unknown";
|
|
26196
|
+
}
|
|
25870
26197
|
//#endregion
|
|
25871
26198
|
//#region packages/mcp/src/input-schemas.ts
|
|
25872
26199
|
const calendarDateSchema = z$1.iso.date("Use a YYYY-MM-DD date.");
|
|
@@ -25971,7 +26298,7 @@ function toolSuccess(summaryText, structuredContent, options = {}) {
|
|
|
25971
26298
|
}
|
|
25972
26299
|
//#endregion
|
|
25973
26300
|
//#region packages/mcp/src/ai-opportunity-tools.ts
|
|
25974
|
-
const
|
|
26301
|
+
const googleAnalyticsDateSchema = z$1.union([calendarDateSchema, z$1.string().regex(/^(?:today|yesterday|\d+daysAgo)$/)]).describe("Use YYYY-MM-DD or a Google Analytics relative date.");
|
|
25975
26302
|
const queryOpportunitySchema = {
|
|
25976
26303
|
site: z$1.string().trim().min(1),
|
|
25977
26304
|
days: z$1.number().int().min(1).max(548).optional(),
|
|
@@ -25986,12 +26313,12 @@ const queryOpportunitySchema = {
|
|
|
25986
26313
|
};
|
|
25987
26314
|
function registerAiOpportunityTools(server, dependencies = {}) {
|
|
25988
26315
|
server.registerTool("seo_ai_referrals", {
|
|
25989
|
-
description: "Find AI referral traffic detected in
|
|
26316
|
+
description: "Find AI referral traffic detected in Google Analytics. maxRows bounds retained provider rows; resultLimit bounds returned landing pages.",
|
|
25990
26317
|
inputSchema: {
|
|
25991
26318
|
property: z$1.string().trim().min(1),
|
|
25992
|
-
startDate:
|
|
25993
|
-
endDate:
|
|
25994
|
-
maxRows: z$1.number().int().min(1).max(1e5).describe("Maximum retained
|
|
26319
|
+
startDate: googleAnalyticsDateSchema.optional(),
|
|
26320
|
+
endDate: googleAnalyticsDateSchema.optional(),
|
|
26321
|
+
maxRows: z$1.number().int().min(1).max(1e5).describe("Maximum retained Google Analytics rows. If limit is also supplied, both values must match.").optional(),
|
|
25995
26322
|
limit: z$1.number().int().min(1).max(1e5).describe("Legacy alias for maxRows. If maxRows is also supplied, both values must match.").optional(),
|
|
25996
26323
|
resultLimit: z$1.number().int().min(1).max(1e3).describe("Maximum ranked landing pages returned. Defaults to 25.").optional(),
|
|
25997
26324
|
refresh: z$1.boolean().optional()
|
|
@@ -26209,7 +26536,7 @@ function registerCrawlerTools(server) {
|
|
|
26209
26536
|
inputSchema: {
|
|
26210
26537
|
url: z$1.string().url(),
|
|
26211
26538
|
site: z$1.string().optional(),
|
|
26212
|
-
|
|
26539
|
+
googleAnalyticsPropertyId: z$1.string().optional(),
|
|
26213
26540
|
maxPages: z$1.number().int().positive().optional(),
|
|
26214
26541
|
maxDepth: z$1.number().int().nonnegative().optional(),
|
|
26215
26542
|
concurrency: z$1.number().int().positive().optional(),
|
|
@@ -26225,12 +26552,12 @@ function registerCrawlerTools(server) {
|
|
|
26225
26552
|
includeIssues: z$1.boolean().optional(),
|
|
26226
26553
|
saveReport: z$1.boolean().optional()
|
|
26227
26554
|
}
|
|
26228
|
-
}, async ({ url, site,
|
|
26555
|
+
}, async ({ url, site, googleAnalyticsPropertyId, maxPages, maxDepth, concurrency, fetchIntervalCap, fetchIntervalMs, refresh, include, exclude, useSitemap, respectRobots, js, includePages, includeIssues, saveReport }) => {
|
|
26229
26556
|
try {
|
|
26230
26557
|
const report = await crawlSite({
|
|
26231
26558
|
url,
|
|
26232
26559
|
site,
|
|
26233
|
-
|
|
26560
|
+
googleAnalyticsPropertyId,
|
|
26234
26561
|
maxPages,
|
|
26235
26562
|
maxDepth,
|
|
26236
26563
|
concurrency,
|
|
@@ -26263,7 +26590,7 @@ function registerCrawlerTools(server) {
|
|
|
26263
26590
|
inputSchema: {
|
|
26264
26591
|
urls: z$1.array(z$1.string().url()).min(1),
|
|
26265
26592
|
site: z$1.string().optional(),
|
|
26266
|
-
|
|
26593
|
+
googleAnalyticsPropertyId: z$1.string().optional(),
|
|
26267
26594
|
maxPages: z$1.number().int().positive().optional(),
|
|
26268
26595
|
concurrency: z$1.number().int().positive().optional(),
|
|
26269
26596
|
fetchIntervalCap: z$1.number().int().positive().optional(),
|
|
@@ -26273,14 +26600,14 @@ function registerCrawlerTools(server) {
|
|
|
26273
26600
|
includeIssues: z$1.boolean().optional(),
|
|
26274
26601
|
saveReport: z$1.boolean().optional()
|
|
26275
26602
|
}
|
|
26276
|
-
}, async ({ urls, site,
|
|
26603
|
+
}, async ({ urls, site, googleAnalyticsPropertyId, maxPages, concurrency, fetchIntervalCap, fetchIntervalMs, refresh, includePages, includeIssues, saveReport }) => {
|
|
26277
26604
|
try {
|
|
26278
26605
|
const report = await crawlSite({
|
|
26279
26606
|
url: urls[0] ?? "",
|
|
26280
26607
|
urls,
|
|
26281
26608
|
mode: "list",
|
|
26282
26609
|
site,
|
|
26283
|
-
|
|
26610
|
+
googleAnalyticsPropertyId,
|
|
26284
26611
|
maxPages: maxPages ?? urls.length,
|
|
26285
26612
|
concurrency,
|
|
26286
26613
|
refresh,
|
|
@@ -27048,7 +27375,7 @@ function registerExperimentTools(server) {
|
|
|
27048
27375
|
}
|
|
27049
27376
|
});
|
|
27050
27377
|
server.registerTool("seo_measure_change", {
|
|
27051
|
-
description: "Measure a saved or ad hoc SEO change with equal finalized GSC windows and optional
|
|
27378
|
+
description: "Measure a saved or ad hoc SEO change with equal finalized GSC windows and optional Google Analytics/control evidence",
|
|
27052
27379
|
inputSchema: {
|
|
27053
27380
|
id: z$1.string().min(1).optional(),
|
|
27054
27381
|
site: z$1.string().min(1).optional(),
|
|
@@ -27061,7 +27388,7 @@ function registerExperimentTools(server) {
|
|
|
27061
27388
|
target: z$1.string().min(1).optional(),
|
|
27062
27389
|
title: z$1.string().min(1).optional(),
|
|
27063
27390
|
changedAt: calendarDateSchema.optional(),
|
|
27064
|
-
|
|
27391
|
+
googleAnalyticsPropertyId: z$1.string().min(1).optional(),
|
|
27065
27392
|
controlScope: z$1.enum([
|
|
27066
27393
|
"site",
|
|
27067
27394
|
"page",
|
|
@@ -27074,7 +27401,7 @@ function registerExperimentTools(server) {
|
|
|
27074
27401
|
afterDays: z$1.number().int().min(1).max(548).optional(),
|
|
27075
27402
|
refresh: z$1.boolean().optional()
|
|
27076
27403
|
}
|
|
27077
|
-
}, async ({ id, site, scope, target, title, changedAt,
|
|
27404
|
+
}, async ({ id, site, scope, target, title, changedAt, googleAnalyticsPropertyId, controlScope, controlTarget, controlTitle, beforeDays, afterDays, refresh }) => {
|
|
27078
27405
|
try {
|
|
27079
27406
|
const result = await measureChange({
|
|
27080
27407
|
id,
|
|
@@ -27083,7 +27410,7 @@ function registerExperimentTools(server) {
|
|
|
27083
27410
|
target,
|
|
27084
27411
|
title,
|
|
27085
27412
|
changedAt,
|
|
27086
|
-
|
|
27413
|
+
googleAnalyticsPropertyId,
|
|
27087
27414
|
controlScope,
|
|
27088
27415
|
controlTarget,
|
|
27089
27416
|
controlTitle,
|
|
@@ -27737,7 +28064,7 @@ const REPORT_DEPTH = {
|
|
|
27737
28064
|
"Referral sessions did not necessarily cause conversions and are not all AI traffic.",
|
|
27738
28065
|
"totalUsers is not additive and must not be reconstructed by summing rows."
|
|
27739
28066
|
],
|
|
27740
|
-
verify: "Compare complete like-for-like
|
|
28067
|
+
verify: "Compare complete like-for-like Google Analytics periods and check whether high-session landing pages answer the referring context.",
|
|
27741
28068
|
related: [{
|
|
27742
28069
|
id: "seo-to-ai-query",
|
|
27743
28070
|
reason: "Builds monitoring prompts."
|
|
@@ -27892,7 +28219,7 @@ const REPORT_DEPTH = {
|
|
|
27892
28219
|
],
|
|
27893
28220
|
doNotClaim: [
|
|
27894
28221
|
"An absent page from a capped, skipped, failed, or differently scoped crawl is not proven removed.",
|
|
27895
|
-
"Source row caps limit joined GSC or
|
|
28222
|
+
"Source row caps limit joined GSC or Google Analytics evidence even when the document crawl completed.",
|
|
27896
28223
|
"A change between snapshots is not proof the release caused it."
|
|
27897
28224
|
],
|
|
27898
28225
|
verify: "Re-run the same definition and request scope after deployment and confirm the delta.",
|
|
@@ -28398,7 +28725,7 @@ const REPORT_DEPTH = {
|
|
|
28398
28725
|
"dataStatus, window, source, warnings, and caveats",
|
|
28399
28726
|
"window.effectiveDays versus requestedDays and afterWindowTruncated",
|
|
28400
28727
|
"GSC before and after metrics",
|
|
28401
|
-
"
|
|
28728
|
+
"Google Analytics source.analytics.status and timezone and any control adjusted deltas"
|
|
28402
28729
|
],
|
|
28403
28730
|
doNotClaim: [
|
|
28404
28731
|
"Timing does not prove the recorded change caused the movement.",
|
|
@@ -28716,8 +29043,8 @@ const REPORT_DEPTH = {
|
|
|
28716
29043
|
"the top few URLs manually"
|
|
28717
29044
|
],
|
|
28718
29045
|
doNotClaim: [
|
|
28719
|
-
"A click decline, low CTR, second-page position, and
|
|
28720
|
-
"Missing
|
|
29046
|
+
"A click decline, low CTR, second-page position, and Google Analytics session signal have different semantics and are not interchangeable.",
|
|
29047
|
+
"Missing Google Analytics or failed fetches stay unavailable, not zero-weight proof.",
|
|
28721
29048
|
"The score is not expected clicks, revenue, ranking lift, or causal confidence."
|
|
28722
29049
|
],
|
|
28723
29050
|
verify: "Confirm query intent, current content, and internal links for the top URLs before choosing a bounded edit.",
|
|
@@ -28835,7 +29162,7 @@ const REPORT_DEPTH = {
|
|
|
28835
29162
|
related: [
|
|
28836
29163
|
{
|
|
28837
29164
|
id: "ai-referrals",
|
|
28838
|
-
reason: "Adds
|
|
29165
|
+
reason: "Adds Google Analytics referral evidence."
|
|
28839
29166
|
},
|
|
28840
29167
|
{
|
|
28841
29168
|
id: "community-intent",
|
|
@@ -29082,9 +29409,9 @@ const REPORT_GUIDANCE = {
|
|
|
29082
29409
|
},
|
|
29083
29410
|
"ai-referrals": {
|
|
29084
29411
|
name: "AI referral traffic",
|
|
29085
|
-
description: "Find visits from known AI products in
|
|
29086
|
-
useWhen: ["You want measured AI referral traffic from
|
|
29087
|
-
avoidWhen: ["
|
|
29412
|
+
description: "Find visits from known AI products in Google Analytics and see a ranked, bounded set of landing pages that received them.",
|
|
29413
|
+
useWhen: ["You want measured AI referral traffic from Google Analytics.", "You need the landing pages and sources behind those visits."],
|
|
29414
|
+
avoidWhen: ["Google Analytics is not connected or does not track the site.", "You want to measure citations or unclicked mentions inside AI answers."],
|
|
29088
29415
|
outcome: "A source and landing-page breakdown of identifiable AI referrals."
|
|
29089
29416
|
},
|
|
29090
29417
|
"ai-search-scorecard": {
|
|
@@ -29763,9 +30090,9 @@ function registerWorkflowTools(server) {
|
|
|
29763
30090
|
"verifyLimit",
|
|
29764
30091
|
"refresh"
|
|
29765
30092
|
]),
|
|
29766
|
-
|
|
30093
|
+
googleAnalyticsPropertyId: z$1.string().optional()
|
|
29767
30094
|
}
|
|
29768
|
-
}, async ({ site, days, recentDays, limit, includeBrand,
|
|
30095
|
+
}, async ({ site, days, recentDays, limit, includeBrand, googleAnalyticsPropertyId, verifyContent, verifyLimit, refresh }) => {
|
|
29769
30096
|
try {
|
|
29770
30097
|
return workflowSuccess(await refreshPrioritiesWorkflow({
|
|
29771
30098
|
site,
|
|
@@ -29773,7 +30100,7 @@ function registerWorkflowTools(server) {
|
|
|
29773
30100
|
recentDays,
|
|
29774
30101
|
limit,
|
|
29775
30102
|
includeBrand,
|
|
29776
|
-
|
|
30103
|
+
googleAnalyticsPropertyId,
|
|
29777
30104
|
verifyContent,
|
|
29778
30105
|
verifyLimit,
|
|
29779
30106
|
refresh
|
|
@@ -30038,7 +30365,8 @@ async function runReport(id, params = {}) {
|
|
|
30038
30365
|
//#endregion
|
|
30039
30366
|
//#region packages/mcp/src/discovery-tools.ts
|
|
30040
30367
|
const openOutputSchema = z$1.looseObject({});
|
|
30041
|
-
|
|
30368
|
+
const reportIds = new Set(listReports().map((report) => report.id));
|
|
30369
|
+
function registerDiscoveryTools(server, options = {}) {
|
|
30042
30370
|
server.registerTool("seo_list_reports", {
|
|
30043
30371
|
description: "List compact SEO report ids, names, and purposes, optionally by category",
|
|
30044
30372
|
inputSchema: { category: z$1.enum(REPORT_CATEGORIES).optional() },
|
|
@@ -30082,17 +30410,31 @@ function registerDiscoveryTools(server) {
|
|
|
30082
30410
|
outputSchema: openOutputSchema,
|
|
30083
30411
|
annotations: { destructiveHint: false }
|
|
30084
30412
|
}, async ({ id, params }) => {
|
|
30085
|
-
|
|
30413
|
+
const telemetry = reportIds.has(id) ? options.telemetry?.() : void 0;
|
|
30414
|
+
if (telemetry) trackTelemetryReportStart(id, telemetry);
|
|
30415
|
+
try {
|
|
30416
|
+
const result = await runReport(id, params);
|
|
30417
|
+
if (telemetry) if (result.isError) trackTelemetryReportFailed(id, telemetryErrorCategory(result.structuredContent?.error), telemetry);
|
|
30418
|
+
else trackTelemetryReportComplete(id, telemetry);
|
|
30419
|
+
return result;
|
|
30420
|
+
} catch (error) {
|
|
30421
|
+
if (telemetry) trackTelemetryReportFailed(id, telemetryErrorCategory(error), telemetry);
|
|
30422
|
+
throw error;
|
|
30423
|
+
}
|
|
30086
30424
|
});
|
|
30087
30425
|
}
|
|
30088
30426
|
//#endregion
|
|
30089
30427
|
//#region packages/mcp/src/index.ts
|
|
30090
|
-
function createServer() {
|
|
30428
|
+
function createServer(options = {}) {
|
|
30091
30429
|
const server = new McpServer({
|
|
30092
30430
|
name: "seo",
|
|
30093
30431
|
version: SEO_VERSION
|
|
30094
30432
|
}, { capabilities: { logging: {} } });
|
|
30095
|
-
|
|
30433
|
+
const telemetryOptions = () => ({ clientName: server.server.getClientVersion()?.name });
|
|
30434
|
+
registerDiscoveryTools(server, { telemetry: options.telemetry === false ? void 0 : telemetryOptions });
|
|
30435
|
+
if (options.telemetry !== false) server.server.oninitialized = () => {
|
|
30436
|
+
initializeTelemetry(telemetryOptions());
|
|
30437
|
+
};
|
|
30096
30438
|
return server;
|
|
30097
30439
|
}
|
|
30098
30440
|
async function startMcpServer(opts = {}) {
|