seo 0.2.4 → 0.2.5
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 +1 -1
- package/README.md +19 -11
- package/dist/cli.js +91 -92
- package/dist/cli.js.map +1 -1
- package/dist/{dist-BhdgeyGe.js → dist-oHPXlHac.js} +1 -1
- package/dist/{dist-DMSBrZYd.js → dist-oOrG0Vub.js} +69 -227
- package/dist/dist-oOrG0Vub.js.map +1 -0
- package/dist/index.d.ts +48 -32
- package/dist/index.js +187 -281
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +112 -97
- package/dist/mcp.js.map +1 -1
- package/dist/{package-D6bCOqrD.js → package-BF_XgK7-.js} +2 -2
- package/dist/{package-D6bCOqrD.js.map → package-BF_XgK7-.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 +2 -2
- package/skills/seo/SKILL.md +7 -6
- package/skills/seo/agents/openai.yaml +1 -1
- package/dist/dist-DMSBrZYd.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-BF_XgK7-.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
5
|
import { homedir } from "node:os";
|
|
@@ -218,6 +218,7 @@ const siteSchema = z.object({
|
|
|
218
218
|
isDefault: z.boolean().optional()
|
|
219
219
|
});
|
|
220
220
|
const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
|
|
221
|
+
const analyticsConnectionsSchema = z.object({ google: z.object({ propertyId: z.string() }).optional() }).default({});
|
|
221
222
|
const clientProfileSchema = z.object({
|
|
222
223
|
id: z.string(),
|
|
223
224
|
name: z.string(),
|
|
@@ -225,7 +226,7 @@ const clientProfileSchema = z.object({
|
|
|
225
226
|
startUrl: z.string().optional(),
|
|
226
227
|
watchUrls: z.array(z.string()).default([]),
|
|
227
228
|
brandTerms: z.array(z.string()).default([]),
|
|
228
|
-
|
|
229
|
+
analytics: analyticsConnectionsSchema,
|
|
229
230
|
reportDay: z.number().int().min(1).max(31).optional(),
|
|
230
231
|
technicalWeekday: z.number().int().min(0).max(7).optional(),
|
|
231
232
|
isDefault: z.boolean().optional(),
|
|
@@ -236,14 +237,14 @@ const configSchema = z.object({
|
|
|
236
237
|
defaultSite: z.string().optional(),
|
|
237
238
|
sites: z.array(siteSchema).default([]),
|
|
238
239
|
clients: z.array(clientProfileSchema).default([]),
|
|
239
|
-
google: z.object({
|
|
240
|
-
|
|
240
|
+
analytics: z.object({ google: z.object({
|
|
241
|
+
defaultPropertyId: z.string().optional(),
|
|
241
242
|
propertyMappings: z.array(z.object({
|
|
242
243
|
siteUrl: z.string(),
|
|
243
|
-
|
|
244
|
+
propertyId: z.string(),
|
|
244
245
|
addedAt: z.number().int().optional()
|
|
245
246
|
})).default([])
|
|
246
|
-
}).default({ propertyMappings: [] }),
|
|
247
|
+
}).default({ propertyMappings: [] }) }).default({ google: { propertyMappings: [] } }),
|
|
247
248
|
providers: z.object({
|
|
248
249
|
semrushApiKey: z.string().optional(),
|
|
249
250
|
dataForSeoLogin: z.string().optional(),
|
|
@@ -460,6 +461,14 @@ function missingOAuthClientMessage(source) {
|
|
|
460
461
|
//#endregion
|
|
461
462
|
//#region packages/core/dist/gsc/auth/types.js
|
|
462
463
|
const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
|
|
464
|
+
const GOOGLE_READONLY_SCOPE_LABELS = {
|
|
465
|
+
"https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
|
|
466
|
+
"https://www.googleapis.com/auth/analytics.readonly": "Google Analytics"
|
|
467
|
+
};
|
|
468
|
+
function missingGoogleReadonlyScopes(scope) {
|
|
469
|
+
const granted = new Set(scope.split(/\s+/u).filter(Boolean));
|
|
470
|
+
return GOOGLE_READONLY_SCOPES.filter((required) => !granted.has(required));
|
|
471
|
+
}
|
|
463
472
|
[
|
|
464
473
|
...GOOGLE_READONLY_SCOPES,
|
|
465
474
|
"openid",
|
|
@@ -657,6 +666,11 @@ function authRequired() {
|
|
|
657
666
|
function authExpired() {
|
|
658
667
|
return new SeoError("AUTH_EXPIRED", "Google refresh token is no longer valid. Run `seo auth login` again.");
|
|
659
668
|
}
|
|
669
|
+
function requireReadonlyScopes(tokens) {
|
|
670
|
+
const missing = missingGoogleReadonlyScopes(tokens.scope);
|
|
671
|
+
if (missing.length === 0) return;
|
|
672
|
+
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.`);
|
|
673
|
+
}
|
|
660
674
|
function needsRefresh(tokens, now) {
|
|
661
675
|
return !tokens.access_token || tokens.expires_at - now < REFRESH_BUFFER_MS;
|
|
662
676
|
}
|
|
@@ -716,6 +730,7 @@ async function createAuthorizedClient() {
|
|
|
716
730
|
let tokens = await readTokens();
|
|
717
731
|
if (!tokens) throw authRequired();
|
|
718
732
|
requireClientConfig(tokens);
|
|
733
|
+
requireReadonlyScopes(tokens);
|
|
719
734
|
if (needsRefresh(tokens, Date.now())) tokens = await refreshStoredToken({ onlyIfExpiring: true });
|
|
720
735
|
return {
|
|
721
736
|
client: new LocalGoogleAccessTokenClient(tokens),
|
|
@@ -1068,8 +1083,8 @@ async function runGa4Report(propertyId, body, opts = {}) {
|
|
|
1068
1083
|
body: JSON.stringify(body)
|
|
1069
1084
|
});
|
|
1070
1085
|
if (!response.ok) {
|
|
1071
|
-
if (response.status === 403) throw new Error(`
|
|
1072
|
-
throw new Error(`
|
|
1086
|
+
if (response.status === 403) throw new Error(`Google Analytics report failed with 403. Check access to property ${propertyId}.`);
|
|
1087
|
+
throw new Error(`Google Analytics report failed with ${response.status}.`);
|
|
1073
1088
|
}
|
|
1074
1089
|
const result = await response.json();
|
|
1075
1090
|
if (cacheable) db.prepare(`INSERT OR REPLACE INTO ga4_cache
|
|
@@ -1091,7 +1106,7 @@ function ga4RowsToObjects(result) {
|
|
|
1091
1106
|
return output;
|
|
1092
1107
|
});
|
|
1093
1108
|
}
|
|
1094
|
-
function ga4ReportQualityWarnings(result, label = "
|
|
1109
|
+
function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
|
|
1095
1110
|
const warnings = [];
|
|
1096
1111
|
if (result.metadata?.dataLossFromOtherRow) warnings.push(`${label} grouped high-cardinality data into (other).`);
|
|
1097
1112
|
if (result.metadata?.subjectToThresholding) warnings.push(`${label} was subject to Google Analytics thresholding.`);
|
|
@@ -1197,11 +1212,11 @@ function compareText$16(left, right) {
|
|
|
1197
1212
|
function metric(row, name) {
|
|
1198
1213
|
const value = row[name];
|
|
1199
1214
|
const parsed = Number(value);
|
|
1200
|
-
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `
|
|
1215
|
+
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
|
|
1201
1216
|
return parsed;
|
|
1202
1217
|
}
|
|
1203
1218
|
function dateValue$1(value) {
|
|
1204
|
-
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1219
|
+
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics returned an invalid date value.");
|
|
1205
1220
|
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
1206
1221
|
}
|
|
1207
1222
|
function addMetrics(target, source) {
|
|
@@ -1240,7 +1255,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1240
1255
|
const detailTotal = emptyMetrics();
|
|
1241
1256
|
for (const row of input.detailRows) {
|
|
1242
1257
|
const definition = aiReferralSourceForValue(row.sessionSource ?? "");
|
|
1243
|
-
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1258
|
+
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
|
|
1244
1259
|
const metrics = {
|
|
1245
1260
|
sessions: metric(row, "sessions"),
|
|
1246
1261
|
eventCount: metric(row, "eventCount")
|
|
@@ -1273,11 +1288,11 @@ function analyzeAiReferralRows(input) {
|
|
|
1273
1288
|
...evidenceReasons("Source discovery", input.sourceDiscovery),
|
|
1274
1289
|
...evidenceReasons("Landing-page detail", input.detail),
|
|
1275
1290
|
...evidenceReasons("Total users", input.totalUsersEvidence),
|
|
1276
|
-
...detailMismatch ? ["
|
|
1277
|
-
...timeZones.size > 1 ? ["
|
|
1291
|
+
...detailMismatch ? ["Google Analytics detail totals differ from source-discovery totals; landing-page and daily breakdowns are incomplete."] : [],
|
|
1292
|
+
...timeZones.size > 1 ? ["Google Analytics queries returned inconsistent property time zones."] : []
|
|
1278
1293
|
];
|
|
1279
1294
|
const dataStatus = partialReasons.length ? "partial" : "complete";
|
|
1280
|
-
const generalCaveat = "
|
|
1295
|
+
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
1296
|
const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
|
|
1282
1297
|
const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
|
|
1283
1298
|
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$16(left[0], right[0]))[0]?.[0];
|
|
@@ -1309,13 +1324,13 @@ function analyzeAiReferralRows(input) {
|
|
|
1309
1324
|
dataStatus,
|
|
1310
1325
|
range: input.range,
|
|
1311
1326
|
methodology: {
|
|
1312
|
-
id: "
|
|
1327
|
+
id: "google-analytics-ai-referrals",
|
|
1313
1328
|
version: 2,
|
|
1314
1329
|
attributionDimension: "sessionSource",
|
|
1315
1330
|
sourceRulesVersion: AI_REFERRAL_SOURCES_VERSION
|
|
1316
1331
|
},
|
|
1317
1332
|
dataSource: {
|
|
1318
|
-
provider: "google-analytics
|
|
1333
|
+
provider: "google-analytics",
|
|
1319
1334
|
api: "analyticsdata.v1beta.runReport",
|
|
1320
1335
|
maxRows: input.maxRows,
|
|
1321
1336
|
calls: input.sourceDiscovery.calls + input.detail.calls + input.totalUsersEvidence.calls,
|
|
@@ -1332,7 +1347,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1332
1347
|
totalUsersStatus: input.totalUsers === null ? "not-reported" : "available",
|
|
1333
1348
|
sources: sourceTotals.size,
|
|
1334
1349
|
landingPages: pageTotals.size,
|
|
1335
|
-
verdict: total.sessions ? dataStatus === "complete" ? "Detected AI referral traffic in
|
|
1350
|
+
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
1351
|
caveat: generalCaveat
|
|
1337
1352
|
},
|
|
1338
1353
|
sources: [...sourceTotals.values()].map((source) => ({
|
|
@@ -1354,7 +1369,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1354
1369
|
})).sort((left, right) => compareText$16(left.date, right.date)),
|
|
1355
1370
|
caveats: [
|
|
1356
1371
|
generalCaveat,
|
|
1357
|
-
"Attribution uses
|
|
1372
|
+
"Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
|
|
1358
1373
|
"Source totals come from a low-cardinality source query; landing-page and daily detail is queried separately for the exact observed source values.",
|
|
1359
1374
|
"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
1375
|
...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 +1471,7 @@ function reportRange(input) {
|
|
|
1456
1471
|
const endDate = input.endDate ?? "yesterday";
|
|
1457
1472
|
const absolute = validIsoDate$1(startDate) && validIsoDate$1(endDate);
|
|
1458
1473
|
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
|
|
1474
|
+
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
1475
|
if (absolute && startDate > endDate) throw new SeoError("INVALID_INPUT", "startDate must be on or before endDate.");
|
|
1461
1476
|
return {
|
|
1462
1477
|
startDate,
|
|
@@ -1515,7 +1530,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1515
1530
|
const property = input.property.trim();
|
|
1516
1531
|
if (!property) throw new SeoError("INVALID_INPUT", "property is required.");
|
|
1517
1532
|
const range = reportRange(input);
|
|
1518
|
-
const
|
|
1533
|
+
const googleAnalyticsDateRange = {
|
|
1519
1534
|
startDate: range.startDate,
|
|
1520
1535
|
endDate: range.endDate
|
|
1521
1536
|
};
|
|
@@ -1525,7 +1540,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1525
1540
|
const sourceResult = await fetchAiReferralRows({
|
|
1526
1541
|
property,
|
|
1527
1542
|
request: {
|
|
1528
|
-
dateRanges: [
|
|
1543
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1529
1544
|
dimensions: [{ name: "sessionSource" }],
|
|
1530
1545
|
metrics: [{ name: "sessions" }, { name: "eventCount" }],
|
|
1531
1546
|
orderBys: [{
|
|
@@ -1550,7 +1565,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1550
1565
|
detailResult = await fetchAiReferralRows({
|
|
1551
1566
|
property,
|
|
1552
1567
|
request: {
|
|
1553
|
-
dateRanges: [
|
|
1568
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1554
1569
|
dimensions: [
|
|
1555
1570
|
{ name: "date" },
|
|
1556
1571
|
{ name: "sessionSource" },
|
|
@@ -1586,7 +1601,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
1586
1601
|
usersResult = await fetchAiReferralRows({
|
|
1587
1602
|
property,
|
|
1588
1603
|
request: {
|
|
1589
|
-
dateRanges: [
|
|
1604
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
1590
1605
|
metrics: [{ name: "totalUsers" }],
|
|
1591
1606
|
dimensionFilter
|
|
1592
1607
|
},
|
|
@@ -9414,7 +9429,7 @@ function crawlDefinitionId(input) {
|
|
|
9414
9429
|
const normalized = {
|
|
9415
9430
|
config: normalizeCrawlConfig(input.config),
|
|
9416
9431
|
site: input.site ?? null,
|
|
9417
|
-
|
|
9432
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId ?? null
|
|
9418
9433
|
};
|
|
9419
9434
|
return `crawl_def_${createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 20)}`;
|
|
9420
9435
|
}
|
|
@@ -9617,7 +9632,7 @@ function createCrawlReport(input) {
|
|
|
9617
9632
|
const definitionId = crawlDefinitionId({
|
|
9618
9633
|
config,
|
|
9619
9634
|
site: input.site,
|
|
9620
|
-
|
|
9635
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId
|
|
9621
9636
|
});
|
|
9622
9637
|
const safePagesWithLinks = sortPages(sanitizePages(deriveInternalLinkAuthority(input.pages ?? [], input.linkGraph)));
|
|
9623
9638
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
@@ -9630,7 +9645,7 @@ function createCrawlReport(input) {
|
|
|
9630
9645
|
definitionId,
|
|
9631
9646
|
projectId: input.projectId,
|
|
9632
9647
|
site: input.site,
|
|
9633
|
-
|
|
9648
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
9634
9649
|
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9635
9650
|
status: input.status ?? "completed",
|
|
9636
9651
|
configHash: crawlConfigHash(config),
|
|
@@ -9668,7 +9683,7 @@ function normalizeLoadedCrawlReport(report) {
|
|
|
9668
9683
|
definitionId: report.definitionId ?? crawlDefinitionId({
|
|
9669
9684
|
config: report.config,
|
|
9670
9685
|
site: report.site,
|
|
9671
|
-
|
|
9686
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId
|
|
9672
9687
|
}),
|
|
9673
9688
|
summary: summarizeCrawlReport({
|
|
9674
9689
|
pages,
|
|
@@ -10074,7 +10089,7 @@ async function joinAnalytics(input) {
|
|
|
10074
10089
|
limit: input.limit
|
|
10075
10090
|
});
|
|
10076
10091
|
} catch (error) {
|
|
10077
|
-
const warning = `
|
|
10092
|
+
const warning = `Google Analytics metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
10078
10093
|
input.warnings.push(warning);
|
|
10079
10094
|
return {
|
|
10080
10095
|
status: "unavailable",
|
|
@@ -10087,7 +10102,7 @@ async function joinAnalytics(input) {
|
|
|
10087
10102
|
};
|
|
10088
10103
|
}
|
|
10089
10104
|
if (analytics.warning) {
|
|
10090
|
-
const warning = `
|
|
10105
|
+
const warning = `Google Analytics metrics unavailable: ${analytics.warning}`;
|
|
10091
10106
|
input.warnings.push(warning);
|
|
10092
10107
|
return {
|
|
10093
10108
|
status: "unavailable",
|
|
@@ -10112,7 +10127,7 @@ async function joinAnalytics(input) {
|
|
|
10112
10127
|
const sourceQualityKnown = analytics.source?.dataStatus !== void 0;
|
|
10113
10128
|
const retainedRowLimitReached = analytics.source?.retainedRowLimitReached;
|
|
10114
10129
|
const qualityWarnings = analytics.source?.qualityWarnings ?? [];
|
|
10115
|
-
const warning = !sourceQualityKnown ? `
|
|
10130
|
+
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
10131
|
if (warning) input.warnings.push(warning);
|
|
10117
10132
|
return {
|
|
10118
10133
|
status: !sourceQualityKnown || analytics.source?.dataStatus === "partial" ? "partial" : joinedPages ? "joined" : "none",
|
|
@@ -10272,7 +10287,7 @@ async function crawlDataSources(input) {
|
|
|
10272
10287
|
joinedPages: 0,
|
|
10273
10288
|
retainedRowLimit: input.analyticsLimit,
|
|
10274
10289
|
retainedRowLimitReached: false,
|
|
10275
|
-
warning: input.cancelled ? "
|
|
10290
|
+
warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
10276
10291
|
}
|
|
10277
10292
|
};
|
|
10278
10293
|
if (!input.cancelled && input.site) dataSources.searchConsole = await joinSearchMetrics({
|
|
@@ -10288,8 +10303,8 @@ async function crawlDataSources(input) {
|
|
|
10288
10303
|
queryPagesMetricsBatch: input.queryPagesMetricsBatch,
|
|
10289
10304
|
queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
|
|
10290
10305
|
});
|
|
10291
|
-
if (!input.cancelled && input.
|
|
10292
|
-
propertyId: input.
|
|
10306
|
+
if (!input.cancelled && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
|
|
10307
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
10293
10308
|
pages: input.pages,
|
|
10294
10309
|
warnings: input.warnings,
|
|
10295
10310
|
limit: input.analyticsLimit,
|
|
@@ -11111,11 +11126,11 @@ function landingPageRankingPolicy(input) {
|
|
|
11111
11126
|
};
|
|
11112
11127
|
const warningSuffix = "Observed landing-page values remain visible but do not affect priority scores.";
|
|
11113
11128
|
const warnings = [
|
|
11114
|
-
...input.warning ? [`
|
|
11115
|
-
...input.source?.retainedRowLimitReached ? [`
|
|
11129
|
+
...input.warning ? [`Google Analytics: ${input.warning}`] : [],
|
|
11130
|
+
...input.source?.retainedRowLimitReached ? [`Google Analytics: the retained-row limit was reached. ${warningSuffix}`] : [],
|
|
11116
11131
|
...input.source?.qualityWarnings?.map((warning) => `${warning} ${warningSuffix}`) ?? []
|
|
11117
11132
|
];
|
|
11118
|
-
if (!input.source?.dataStatus && !input.warning) warnings.push(`
|
|
11133
|
+
if (!input.source?.dataStatus && !input.warning) warnings.push(`Google Analytics: landing-page completeness was not reported. ${warningSuffix}`);
|
|
11119
11134
|
return {
|
|
11120
11135
|
canRank: !input.warning && warnings.length === 0 && landingPageValuesCanRank(input.source),
|
|
11121
11136
|
warnings
|
|
@@ -11155,7 +11170,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
|
11155
11170
|
const values = landingPageValuesFromRows(rows);
|
|
11156
11171
|
const availableRows = result.rowCount;
|
|
11157
11172
|
const retainedRowLimitReached = availableRows !== void 0 && availableRows > rows.length || rows.length >= retainedRowLimit;
|
|
11158
|
-
const qualityWarnings = ga4ReportQualityWarnings(result, "
|
|
11173
|
+
const qualityWarnings = ga4ReportQualityWarnings(result, "Google Analytics landing-page report");
|
|
11159
11174
|
return {
|
|
11160
11175
|
values,
|
|
11161
11176
|
source: {
|
|
@@ -11909,7 +11924,7 @@ async function crawlSite(input, dependencies) {
|
|
|
11909
11924
|
const dataSources = await crawlDataSources({
|
|
11910
11925
|
cancelled,
|
|
11911
11926
|
site,
|
|
11912
|
-
|
|
11927
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
11913
11928
|
pages,
|
|
11914
11929
|
warnings,
|
|
11915
11930
|
searchMetricsLimit,
|
|
@@ -11964,7 +11979,7 @@ async function crawlSite(input, dependencies) {
|
|
|
11964
11979
|
config,
|
|
11965
11980
|
projectId: input.projectId,
|
|
11966
11981
|
site,
|
|
11967
|
-
|
|
11982
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
11968
11983
|
dataSources,
|
|
11969
11984
|
pages,
|
|
11970
11985
|
requests,
|
|
@@ -12107,7 +12122,7 @@ function searchValueForGroup(report, urls) {
|
|
|
12107
12122
|
};
|
|
12108
12123
|
}
|
|
12109
12124
|
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 ? `
|
|
12125
|
+
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
12126
|
}
|
|
12112
12127
|
function verificationCommand(report, severity) {
|
|
12113
12128
|
return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
|
|
@@ -12223,7 +12238,7 @@ function snapshotInput(report) {
|
|
|
12223
12238
|
generatedAt: report.generatedAt,
|
|
12224
12239
|
projectId: report.projectId,
|
|
12225
12240
|
site: report.site,
|
|
12226
|
-
|
|
12241
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId,
|
|
12227
12242
|
url: report.config.url,
|
|
12228
12243
|
status: report.status,
|
|
12229
12244
|
configHash: report.configHash,
|
|
@@ -13861,7 +13876,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
13861
13876
|
definitionId: z.string(),
|
|
13862
13877
|
projectId: z.string().optional(),
|
|
13863
13878
|
site: z.string().optional(),
|
|
13864
|
-
|
|
13879
|
+
googleAnalyticsPropertyId: z.string().optional(),
|
|
13865
13880
|
generatedAt: z.string().datetime(),
|
|
13866
13881
|
status: z.enum([
|
|
13867
13882
|
"completed",
|
|
@@ -17727,7 +17742,7 @@ function stringMatchType(matchType) {
|
|
|
17727
17742
|
if (matchType === "equals") return "EXACT";
|
|
17728
17743
|
return "CONTAINS";
|
|
17729
17744
|
}
|
|
17730
|
-
function
|
|
17745
|
+
function googleAnalyticsLandingPageFilterForChange(change, group) {
|
|
17731
17746
|
if (change.scope === "site") return void 0;
|
|
17732
17747
|
if (change.scope === "query") return void 0;
|
|
17733
17748
|
if (change.scope === "group") {
|
|
@@ -17750,7 +17765,7 @@ function ga4LandingPageFilterForChange(change, group) {
|
|
|
17750
17765
|
}
|
|
17751
17766
|
} };
|
|
17752
17767
|
}
|
|
17753
|
-
function
|
|
17768
|
+
function summarizeGoogleAnalyticsRows(rows) {
|
|
17754
17769
|
return rows.reduce((sum, row) => ({
|
|
17755
17770
|
sessions: fixed(sum.sessions + (Number(row.sessions) || 0)),
|
|
17756
17771
|
engagedSessions: fixed(sum.engagedSessions + (Number(row.engagedSessions) || 0)),
|
|
@@ -17817,8 +17832,8 @@ function classify(input) {
|
|
|
17817
17832
|
}
|
|
17818
17833
|
//#endregion
|
|
17819
17834
|
//#region packages/core/dist/analyze/experiments/measurement/ga4-change-provider.js
|
|
17820
|
-
async function
|
|
17821
|
-
const result = await input.
|
|
17835
|
+
async function queryGoogleAnalyticsMetrics(input) {
|
|
17836
|
+
const result = await input.googleAnalyticsReport(input.propertyId, {
|
|
17822
17837
|
dateRanges: [input.window],
|
|
17823
17838
|
dimensions: [{ name: "date" }],
|
|
17824
17839
|
metrics: [
|
|
@@ -17832,7 +17847,7 @@ async function queryGa4Metrics(input) {
|
|
|
17832
17847
|
}, { refresh: input.refresh });
|
|
17833
17848
|
const rows = ga4RowsToObjects(result);
|
|
17834
17849
|
return {
|
|
17835
|
-
metrics:
|
|
17850
|
+
metrics: summarizeGoogleAnalyticsRows(rows),
|
|
17836
17851
|
source: {
|
|
17837
17852
|
rows: rows.length,
|
|
17838
17853
|
rowCount: result.rowCount ?? rows.length,
|
|
@@ -17842,19 +17857,19 @@ async function queryGa4Metrics(input) {
|
|
|
17842
17857
|
warnings: ga4ReportQualityWarnings(result, input.label)
|
|
17843
17858
|
};
|
|
17844
17859
|
}
|
|
17845
|
-
async function
|
|
17846
|
-
const [before, after] = await Promise.all([
|
|
17860
|
+
async function queryGoogleAnalyticsChangeWindows(input) {
|
|
17861
|
+
const [before, after] = await Promise.all([queryGoogleAnalyticsMetrics({
|
|
17847
17862
|
...input,
|
|
17848
17863
|
window: input.before,
|
|
17849
|
-
label: "
|
|
17850
|
-
}),
|
|
17864
|
+
label: "Google Analytics before window"
|
|
17865
|
+
}), queryGoogleAnalyticsMetrics({
|
|
17851
17866
|
...input,
|
|
17852
17867
|
window: input.after,
|
|
17853
|
-
label: "
|
|
17868
|
+
label: "Google Analytics after window"
|
|
17854
17869
|
})]);
|
|
17855
17870
|
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("
|
|
17871
|
+
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.");
|
|
17872
|
+
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
17873
|
return {
|
|
17859
17874
|
report: {
|
|
17860
17875
|
propertyId: input.propertyId,
|
|
@@ -17870,7 +17885,7 @@ async function queryGa4ChangeWindows(input) {
|
|
|
17870
17885
|
before: before.metrics,
|
|
17871
17886
|
after: after.metrics
|
|
17872
17887
|
}),
|
|
17873
|
-
note: "
|
|
17888
|
+
note: "Google Analytics attribution is landing-page based. Query-level tests use GSC only."
|
|
17874
17889
|
},
|
|
17875
17890
|
source: {
|
|
17876
17891
|
before: before.source,
|
|
@@ -18108,7 +18123,7 @@ function measurementWindow(input) {
|
|
|
18108
18123
|
//#region packages/core/dist/analyze/experiments/measure-change.js
|
|
18109
18124
|
const defaultDependencies$5 = {
|
|
18110
18125
|
searchAnalytics: querySearchAnalytics,
|
|
18111
|
-
|
|
18126
|
+
googleAnalyticsReport: runGa4Report,
|
|
18112
18127
|
contentGroup: getContentGroup,
|
|
18113
18128
|
now: () => /* @__PURE__ */ new Date()
|
|
18114
18129
|
};
|
|
@@ -18194,15 +18209,15 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
18194
18209
|
confidence: "medium",
|
|
18195
18210
|
note: `${classified.note} Confidence is capped because Search Console evidence is partial.`
|
|
18196
18211
|
} : classified;
|
|
18197
|
-
const
|
|
18212
|
+
const googleAnalyticsFilter = googleAnalyticsLandingPageFilterForChange(change, group);
|
|
18198
18213
|
const queryDimension = change.scope === "query" || group?.dimension === "query";
|
|
18199
|
-
const analytics = input.
|
|
18200
|
-
propertyId: input.
|
|
18214
|
+
const analytics = input.googleAnalyticsPropertyId && !queryDimension ? await queryGoogleAnalyticsChangeWindows({
|
|
18215
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
18201
18216
|
before,
|
|
18202
18217
|
after,
|
|
18203
|
-
filter:
|
|
18218
|
+
filter: googleAnalyticsFilter,
|
|
18204
18219
|
refresh: input.refresh,
|
|
18205
|
-
|
|
18220
|
+
googleAnalyticsReport: dependencies.googleAnalyticsReport
|
|
18206
18221
|
}) : void 0;
|
|
18207
18222
|
const warnings = [
|
|
18208
18223
|
...measured.warnings,
|
|
@@ -18281,7 +18296,7 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
18281
18296
|
...window.afterWindowTruncated ? [`The requested ${window.requestedDays}-day after window is incomplete, so this run is partial and has no directional verdict.`] : [],
|
|
18282
18297
|
...shortWindow ? ["Fewer than 7 finalized days per window is too short for a directional verdict."] : [],
|
|
18283
18298
|
...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 ? [`
|
|
18299
|
+
...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
18300
|
]
|
|
18286
18301
|
};
|
|
18287
18302
|
}
|
|
@@ -23866,7 +23881,7 @@ function reportCaveats(input) {
|
|
|
23866
23881
|
`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
23882
|
`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
23883
|
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
|
-
"
|
|
23884
|
+
"Google Analytics: not included in this narrative; use refresh-priorities when Google Analytics value should influence prioritisation.",
|
|
23870
23885
|
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
23886
|
];
|
|
23872
23887
|
}
|
|
@@ -24988,12 +25003,12 @@ async function refreshPrioritiesWorkflow(input) {
|
|
|
24988
25003
|
});
|
|
24989
25004
|
const range = defaultDateRange$1(input.days ?? 28);
|
|
24990
25005
|
const analytics = await fetchLandingPageValues({
|
|
24991
|
-
propertyId: input.
|
|
25006
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
24992
25007
|
startDate: range.startDate,
|
|
24993
25008
|
endDate: range.endDate
|
|
24994
25009
|
});
|
|
24995
25010
|
const analyticsPolicy = landingPageRankingPolicy({
|
|
24996
|
-
propertyId: input.
|
|
25011
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
24997
25012
|
source: analytics.source,
|
|
24998
25013
|
warning: analytics.warning
|
|
24999
25014
|
});
|
|
@@ -25590,7 +25605,7 @@ async function runDoctor() {
|
|
|
25590
25605
|
const grantedScopes = new Set(tokens?.scope.split(/\s+/).filter(Boolean) ?? []);
|
|
25591
25606
|
for (const scope of REQUIRED_SCOPES) checks.push({
|
|
25592
25607
|
id: `scope:${scope}`,
|
|
25593
|
-
label: scope.endsWith("webmasters.readonly") ? "
|
|
25608
|
+
label: scope.endsWith("webmasters.readonly") ? "Search Console scope" : "Google Analytics scope",
|
|
25594
25609
|
status: serviceAccount.configured || tokens && grantedScopes.has(scope) ? "pass" : tokens ? "fail" : "warn",
|
|
25595
25610
|
detail: serviceAccount.configured ? `${scope} requested by the service account.` : tokens ? grantedScopes.has(scope) ? scope : `Missing ${scope}.` : "Cannot inspect scopes until login is complete.",
|
|
25596
25611
|
fix: !serviceAccount.configured && tokens && !grantedScopes.has(scope) ? "Run `seo auth logout` then `seo auth login`." : void 0
|
|
@@ -25602,10 +25617,10 @@ async function runDoctor() {
|
|
|
25602
25617
|
detail: config.defaultSite ?? "No saved default. Human CLI commands will prompt; agents can pass --site."
|
|
25603
25618
|
});
|
|
25604
25619
|
checks.push({
|
|
25605
|
-
id: "default-
|
|
25606
|
-
label: "Saved
|
|
25620
|
+
id: "default-google-analytics",
|
|
25621
|
+
label: "Saved Google Analytics property",
|
|
25607
25622
|
status: "pass",
|
|
25608
|
-
detail: config.google.
|
|
25623
|
+
detail: config.analytics.google.defaultPropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
|
|
25609
25624
|
});
|
|
25610
25625
|
return {
|
|
25611
25626
|
ok: checks.every((check) => check.status !== "fail"),
|
|
@@ -25971,7 +25986,7 @@ function toolSuccess(summaryText, structuredContent, options = {}) {
|
|
|
25971
25986
|
}
|
|
25972
25987
|
//#endregion
|
|
25973
25988
|
//#region packages/mcp/src/ai-opportunity-tools.ts
|
|
25974
|
-
const
|
|
25989
|
+
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
25990
|
const queryOpportunitySchema = {
|
|
25976
25991
|
site: z$1.string().trim().min(1),
|
|
25977
25992
|
days: z$1.number().int().min(1).max(548).optional(),
|
|
@@ -25986,12 +26001,12 @@ const queryOpportunitySchema = {
|
|
|
25986
26001
|
};
|
|
25987
26002
|
function registerAiOpportunityTools(server, dependencies = {}) {
|
|
25988
26003
|
server.registerTool("seo_ai_referrals", {
|
|
25989
|
-
description: "Find AI referral traffic detected in
|
|
26004
|
+
description: "Find AI referral traffic detected in Google Analytics. maxRows bounds retained provider rows; resultLimit bounds returned landing pages.",
|
|
25990
26005
|
inputSchema: {
|
|
25991
26006
|
property: z$1.string().trim().min(1),
|
|
25992
|
-
startDate:
|
|
25993
|
-
endDate:
|
|
25994
|
-
maxRows: z$1.number().int().min(1).max(1e5).describe("Maximum retained
|
|
26007
|
+
startDate: googleAnalyticsDateSchema.optional(),
|
|
26008
|
+
endDate: googleAnalyticsDateSchema.optional(),
|
|
26009
|
+
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
26010
|
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
26011
|
resultLimit: z$1.number().int().min(1).max(1e3).describe("Maximum ranked landing pages returned. Defaults to 25.").optional(),
|
|
25997
26012
|
refresh: z$1.boolean().optional()
|
|
@@ -26209,7 +26224,7 @@ function registerCrawlerTools(server) {
|
|
|
26209
26224
|
inputSchema: {
|
|
26210
26225
|
url: z$1.string().url(),
|
|
26211
26226
|
site: z$1.string().optional(),
|
|
26212
|
-
|
|
26227
|
+
googleAnalyticsPropertyId: z$1.string().optional(),
|
|
26213
26228
|
maxPages: z$1.number().int().positive().optional(),
|
|
26214
26229
|
maxDepth: z$1.number().int().nonnegative().optional(),
|
|
26215
26230
|
concurrency: z$1.number().int().positive().optional(),
|
|
@@ -26225,12 +26240,12 @@ function registerCrawlerTools(server) {
|
|
|
26225
26240
|
includeIssues: z$1.boolean().optional(),
|
|
26226
26241
|
saveReport: z$1.boolean().optional()
|
|
26227
26242
|
}
|
|
26228
|
-
}, async ({ url, site,
|
|
26243
|
+
}, async ({ url, site, googleAnalyticsPropertyId, maxPages, maxDepth, concurrency, fetchIntervalCap, fetchIntervalMs, refresh, include, exclude, useSitemap, respectRobots, js, includePages, includeIssues, saveReport }) => {
|
|
26229
26244
|
try {
|
|
26230
26245
|
const report = await crawlSite({
|
|
26231
26246
|
url,
|
|
26232
26247
|
site,
|
|
26233
|
-
|
|
26248
|
+
googleAnalyticsPropertyId,
|
|
26234
26249
|
maxPages,
|
|
26235
26250
|
maxDepth,
|
|
26236
26251
|
concurrency,
|
|
@@ -26263,7 +26278,7 @@ function registerCrawlerTools(server) {
|
|
|
26263
26278
|
inputSchema: {
|
|
26264
26279
|
urls: z$1.array(z$1.string().url()).min(1),
|
|
26265
26280
|
site: z$1.string().optional(),
|
|
26266
|
-
|
|
26281
|
+
googleAnalyticsPropertyId: z$1.string().optional(),
|
|
26267
26282
|
maxPages: z$1.number().int().positive().optional(),
|
|
26268
26283
|
concurrency: z$1.number().int().positive().optional(),
|
|
26269
26284
|
fetchIntervalCap: z$1.number().int().positive().optional(),
|
|
@@ -26273,14 +26288,14 @@ function registerCrawlerTools(server) {
|
|
|
26273
26288
|
includeIssues: z$1.boolean().optional(),
|
|
26274
26289
|
saveReport: z$1.boolean().optional()
|
|
26275
26290
|
}
|
|
26276
|
-
}, async ({ urls, site,
|
|
26291
|
+
}, async ({ urls, site, googleAnalyticsPropertyId, maxPages, concurrency, fetchIntervalCap, fetchIntervalMs, refresh, includePages, includeIssues, saveReport }) => {
|
|
26277
26292
|
try {
|
|
26278
26293
|
const report = await crawlSite({
|
|
26279
26294
|
url: urls[0] ?? "",
|
|
26280
26295
|
urls,
|
|
26281
26296
|
mode: "list",
|
|
26282
26297
|
site,
|
|
26283
|
-
|
|
26298
|
+
googleAnalyticsPropertyId,
|
|
26284
26299
|
maxPages: maxPages ?? urls.length,
|
|
26285
26300
|
concurrency,
|
|
26286
26301
|
refresh,
|
|
@@ -27048,7 +27063,7 @@ function registerExperimentTools(server) {
|
|
|
27048
27063
|
}
|
|
27049
27064
|
});
|
|
27050
27065
|
server.registerTool("seo_measure_change", {
|
|
27051
|
-
description: "Measure a saved or ad hoc SEO change with equal finalized GSC windows and optional
|
|
27066
|
+
description: "Measure a saved or ad hoc SEO change with equal finalized GSC windows and optional Google Analytics/control evidence",
|
|
27052
27067
|
inputSchema: {
|
|
27053
27068
|
id: z$1.string().min(1).optional(),
|
|
27054
27069
|
site: z$1.string().min(1).optional(),
|
|
@@ -27061,7 +27076,7 @@ function registerExperimentTools(server) {
|
|
|
27061
27076
|
target: z$1.string().min(1).optional(),
|
|
27062
27077
|
title: z$1.string().min(1).optional(),
|
|
27063
27078
|
changedAt: calendarDateSchema.optional(),
|
|
27064
|
-
|
|
27079
|
+
googleAnalyticsPropertyId: z$1.string().min(1).optional(),
|
|
27065
27080
|
controlScope: z$1.enum([
|
|
27066
27081
|
"site",
|
|
27067
27082
|
"page",
|
|
@@ -27074,7 +27089,7 @@ function registerExperimentTools(server) {
|
|
|
27074
27089
|
afterDays: z$1.number().int().min(1).max(548).optional(),
|
|
27075
27090
|
refresh: z$1.boolean().optional()
|
|
27076
27091
|
}
|
|
27077
|
-
}, async ({ id, site, scope, target, title, changedAt,
|
|
27092
|
+
}, async ({ id, site, scope, target, title, changedAt, googleAnalyticsPropertyId, controlScope, controlTarget, controlTitle, beforeDays, afterDays, refresh }) => {
|
|
27078
27093
|
try {
|
|
27079
27094
|
const result = await measureChange({
|
|
27080
27095
|
id,
|
|
@@ -27083,7 +27098,7 @@ function registerExperimentTools(server) {
|
|
|
27083
27098
|
target,
|
|
27084
27099
|
title,
|
|
27085
27100
|
changedAt,
|
|
27086
|
-
|
|
27101
|
+
googleAnalyticsPropertyId,
|
|
27087
27102
|
controlScope,
|
|
27088
27103
|
controlTarget,
|
|
27089
27104
|
controlTitle,
|
|
@@ -27737,7 +27752,7 @@ const REPORT_DEPTH = {
|
|
|
27737
27752
|
"Referral sessions did not necessarily cause conversions and are not all AI traffic.",
|
|
27738
27753
|
"totalUsers is not additive and must not be reconstructed by summing rows."
|
|
27739
27754
|
],
|
|
27740
|
-
verify: "Compare complete like-for-like
|
|
27755
|
+
verify: "Compare complete like-for-like Google Analytics periods and check whether high-session landing pages answer the referring context.",
|
|
27741
27756
|
related: [{
|
|
27742
27757
|
id: "seo-to-ai-query",
|
|
27743
27758
|
reason: "Builds monitoring prompts."
|
|
@@ -27892,7 +27907,7 @@ const REPORT_DEPTH = {
|
|
|
27892
27907
|
],
|
|
27893
27908
|
doNotClaim: [
|
|
27894
27909
|
"An absent page from a capped, skipped, failed, or differently scoped crawl is not proven removed.",
|
|
27895
|
-
"Source row caps limit joined GSC or
|
|
27910
|
+
"Source row caps limit joined GSC or Google Analytics evidence even when the document crawl completed.",
|
|
27896
27911
|
"A change between snapshots is not proof the release caused it."
|
|
27897
27912
|
],
|
|
27898
27913
|
verify: "Re-run the same definition and request scope after deployment and confirm the delta.",
|
|
@@ -28398,7 +28413,7 @@ const REPORT_DEPTH = {
|
|
|
28398
28413
|
"dataStatus, window, source, warnings, and caveats",
|
|
28399
28414
|
"window.effectiveDays versus requestedDays and afterWindowTruncated",
|
|
28400
28415
|
"GSC before and after metrics",
|
|
28401
|
-
"
|
|
28416
|
+
"Google Analytics source.analytics.status and timezone and any control adjusted deltas"
|
|
28402
28417
|
],
|
|
28403
28418
|
doNotClaim: [
|
|
28404
28419
|
"Timing does not prove the recorded change caused the movement.",
|
|
@@ -28716,8 +28731,8 @@ const REPORT_DEPTH = {
|
|
|
28716
28731
|
"the top few URLs manually"
|
|
28717
28732
|
],
|
|
28718
28733
|
doNotClaim: [
|
|
28719
|
-
"A click decline, low CTR, second-page position, and
|
|
28720
|
-
"Missing
|
|
28734
|
+
"A click decline, low CTR, second-page position, and Google Analytics session signal have different semantics and are not interchangeable.",
|
|
28735
|
+
"Missing Google Analytics or failed fetches stay unavailable, not zero-weight proof.",
|
|
28721
28736
|
"The score is not expected clicks, revenue, ranking lift, or causal confidence."
|
|
28722
28737
|
],
|
|
28723
28738
|
verify: "Confirm query intent, current content, and internal links for the top URLs before choosing a bounded edit.",
|
|
@@ -28835,7 +28850,7 @@ const REPORT_DEPTH = {
|
|
|
28835
28850
|
related: [
|
|
28836
28851
|
{
|
|
28837
28852
|
id: "ai-referrals",
|
|
28838
|
-
reason: "Adds
|
|
28853
|
+
reason: "Adds Google Analytics referral evidence."
|
|
28839
28854
|
},
|
|
28840
28855
|
{
|
|
28841
28856
|
id: "community-intent",
|
|
@@ -29082,9 +29097,9 @@ const REPORT_GUIDANCE = {
|
|
|
29082
29097
|
},
|
|
29083
29098
|
"ai-referrals": {
|
|
29084
29099
|
name: "AI referral traffic",
|
|
29085
|
-
description: "Find visits from known AI products in
|
|
29086
|
-
useWhen: ["You want measured AI referral traffic from
|
|
29087
|
-
avoidWhen: ["
|
|
29100
|
+
description: "Find visits from known AI products in Google Analytics and see a ranked, bounded set of landing pages that received them.",
|
|
29101
|
+
useWhen: ["You want measured AI referral traffic from Google Analytics.", "You need the landing pages and sources behind those visits."],
|
|
29102
|
+
avoidWhen: ["Google Analytics is not connected or does not track the site.", "You want to measure citations or unclicked mentions inside AI answers."],
|
|
29088
29103
|
outcome: "A source and landing-page breakdown of identifiable AI referrals."
|
|
29089
29104
|
},
|
|
29090
29105
|
"ai-search-scorecard": {
|
|
@@ -29763,9 +29778,9 @@ function registerWorkflowTools(server) {
|
|
|
29763
29778
|
"verifyLimit",
|
|
29764
29779
|
"refresh"
|
|
29765
29780
|
]),
|
|
29766
|
-
|
|
29781
|
+
googleAnalyticsPropertyId: z$1.string().optional()
|
|
29767
29782
|
}
|
|
29768
|
-
}, async ({ site, days, recentDays, limit, includeBrand,
|
|
29783
|
+
}, async ({ site, days, recentDays, limit, includeBrand, googleAnalyticsPropertyId, verifyContent, verifyLimit, refresh }) => {
|
|
29769
29784
|
try {
|
|
29770
29785
|
return workflowSuccess(await refreshPrioritiesWorkflow({
|
|
29771
29786
|
site,
|
|
@@ -29773,7 +29788,7 @@ function registerWorkflowTools(server) {
|
|
|
29773
29788
|
recentDays,
|
|
29774
29789
|
limit,
|
|
29775
29790
|
includeBrand,
|
|
29776
|
-
|
|
29791
|
+
googleAnalyticsPropertyId,
|
|
29777
29792
|
verifyContent,
|
|
29778
29793
|
verifyLimit,
|
|
29779
29794
|
refresh
|