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/index.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, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, 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";
|
|
@@ -146,6 +146,7 @@ function resolveSeoCliPaths(input = {}) {
|
|
|
146
146
|
configFile: join(configDir, "config.json"),
|
|
147
147
|
tokensFile: join(configDir, "tokens.json"),
|
|
148
148
|
oauthClientFile: join(configDir, "oauth-client.json"),
|
|
149
|
+
telemetryStateFile: join(configDir, "telemetry.json"),
|
|
149
150
|
cacheDbFile: join(cacheDir, "cache.db")
|
|
150
151
|
};
|
|
151
152
|
}
|
|
@@ -222,6 +223,7 @@ const siteSchema = z.object({
|
|
|
222
223
|
isDefault: z.boolean().optional()
|
|
223
224
|
});
|
|
224
225
|
const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
|
|
226
|
+
const analyticsConnectionsSchema = z.object({ google: z.object({ propertyId: z.string() }).optional() }).default({});
|
|
225
227
|
const clientProfileSchema = z.object({
|
|
226
228
|
id: z.string(),
|
|
227
229
|
name: z.string(),
|
|
@@ -229,7 +231,7 @@ const clientProfileSchema = z.object({
|
|
|
229
231
|
startUrl: z.string().optional(),
|
|
230
232
|
watchUrls: z.array(z.string()).default([]),
|
|
231
233
|
brandTerms: z.array(z.string()).default([]),
|
|
232
|
-
|
|
234
|
+
analytics: analyticsConnectionsSchema,
|
|
233
235
|
reportDay: z.number().int().min(1).max(31).optional(),
|
|
234
236
|
technicalWeekday: z.number().int().min(0).max(7).optional(),
|
|
235
237
|
isDefault: z.boolean().optional(),
|
|
@@ -240,14 +242,14 @@ const configSchema = z.object({
|
|
|
240
242
|
defaultSite: z.string().optional(),
|
|
241
243
|
sites: z.array(siteSchema).default([]),
|
|
242
244
|
clients: z.array(clientProfileSchema).default([]),
|
|
243
|
-
google: z.object({
|
|
244
|
-
|
|
245
|
+
analytics: z.object({ google: z.object({
|
|
246
|
+
defaultPropertyId: z.string().optional(),
|
|
245
247
|
propertyMappings: z.array(z.object({
|
|
246
248
|
siteUrl: z.string(),
|
|
247
|
-
|
|
249
|
+
propertyId: z.string(),
|
|
248
250
|
addedAt: z.number().int().optional()
|
|
249
251
|
})).default([])
|
|
250
|
-
}).default({ propertyMappings: [] }),
|
|
252
|
+
}).default({ propertyMappings: [] }) }).default({ google: { propertyMappings: [] } }),
|
|
251
253
|
providers: z.object({
|
|
252
254
|
semrushApiKey: z.string().optional(),
|
|
253
255
|
dataForSeoLogin: z.string().optional(),
|
|
@@ -492,6 +494,12 @@ function getPrivacySnapshot() {
|
|
|
492
494
|
mode: fileMode(paths.oauthClientFile),
|
|
493
495
|
sizeBytes: sizeOf(paths.oauthClientFile)
|
|
494
496
|
},
|
|
497
|
+
{
|
|
498
|
+
label: "Telemetry state",
|
|
499
|
+
path: paths.telemetryStateFile,
|
|
500
|
+
mode: fileMode(paths.telemetryStateFile),
|
|
501
|
+
sizeBytes: sizeOf(paths.telemetryStateFile)
|
|
502
|
+
},
|
|
495
503
|
{
|
|
496
504
|
label: "Cache DB",
|
|
497
505
|
path: paths.cacheDbFile,
|
|
@@ -566,6 +574,14 @@ function missingOAuthClientMessage(source) {
|
|
|
566
574
|
//#endregion
|
|
567
575
|
//#region packages/core/src/gsc/auth/types.ts
|
|
568
576
|
const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
|
|
577
|
+
const GOOGLE_READONLY_SCOPE_LABELS = {
|
|
578
|
+
"https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
|
|
579
|
+
"https://www.googleapis.com/auth/analytics.readonly": "Google Analytics"
|
|
580
|
+
};
|
|
581
|
+
function missingGoogleReadonlyScopes(scope) {
|
|
582
|
+
const granted = new Set(scope.split(/\s+/u).filter(Boolean));
|
|
583
|
+
return GOOGLE_READONLY_SCOPES.filter((required) => !granted.has(required));
|
|
584
|
+
}
|
|
569
585
|
const GOOGLE_SCOPE = [
|
|
570
586
|
...GOOGLE_READONLY_SCOPES,
|
|
571
587
|
"openid",
|
|
@@ -763,6 +779,11 @@ function authRequired() {
|
|
|
763
779
|
function authExpired() {
|
|
764
780
|
return new SeoError("AUTH_EXPIRED", "Google refresh token is no longer valid. Run `seo auth login` again.");
|
|
765
781
|
}
|
|
782
|
+
function requireReadonlyScopes(tokens) {
|
|
783
|
+
const missing = missingGoogleReadonlyScopes(tokens.scope);
|
|
784
|
+
if (missing.length === 0) return;
|
|
785
|
+
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.`);
|
|
786
|
+
}
|
|
766
787
|
function needsRefresh(tokens, now) {
|
|
767
788
|
return !tokens.access_token || tokens.expires_at - now < REFRESH_BUFFER_MS;
|
|
768
789
|
}
|
|
@@ -822,6 +843,7 @@ async function createAuthorizedClient() {
|
|
|
822
843
|
let tokens = await readTokens();
|
|
823
844
|
if (!tokens) throw authRequired();
|
|
824
845
|
requireClientConfig(tokens);
|
|
846
|
+
requireReadonlyScopes(tokens);
|
|
825
847
|
if (needsRefresh(tokens, Date.now())) tokens = await refreshStoredToken({ onlyIfExpiring: true });
|
|
826
848
|
return {
|
|
827
849
|
client: new LocalGoogleAccessTokenClient(tokens),
|
|
@@ -879,176 +901,36 @@ async function authStatus() {
|
|
|
879
901
|
}
|
|
880
902
|
//#endregion
|
|
881
903
|
//#region packages/core/src/gsc/auth/callback-page.ts
|
|
882
|
-
function
|
|
904
|
+
function callbackCopy(options) {
|
|
905
|
+
if (options.status === "connected") return {
|
|
906
|
+
title: "Google account connected",
|
|
907
|
+
heading: "Google account connected.",
|
|
908
|
+
detail: "This tab can be closed."
|
|
909
|
+
};
|
|
910
|
+
if (options.status === "permissions-missing") return {
|
|
911
|
+
title: "Google permissions not granted",
|
|
912
|
+
heading: "Google permissions were not granted.",
|
|
913
|
+
detail: `Return to your terminal and run seo auth login again. Select all permission boxes for ${options.missing.join(" and ")}.`
|
|
914
|
+
};
|
|
915
|
+
return {
|
|
916
|
+
title: "Google connection failed",
|
|
917
|
+
heading: "Google connection failed.",
|
|
918
|
+
detail: "Return to your terminal for the error and try again."
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function oauthCallbackPage(options = { status: "connected" }) {
|
|
922
|
+
const copy = callbackCopy(options);
|
|
883
923
|
return `<!doctype html>
|
|
884
924
|
<html lang="en">
|
|
885
925
|
<head>
|
|
886
926
|
<meta charset="utf-8">
|
|
887
927
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
888
|
-
<meta
|
|
889
|
-
<
|
|
890
|
-
<title>seo is connected</title>
|
|
891
|
-
<style>
|
|
892
|
-
:root {
|
|
893
|
-
color-scheme: light dark;
|
|
894
|
-
--background: #fafafa;
|
|
895
|
-
--background-raised: #ffffff;
|
|
896
|
-
--foreground: #343434;
|
|
897
|
-
--foreground-muted: #737373;
|
|
898
|
-
--border: #eaeaea;
|
|
899
|
-
--primary: #2589b6;
|
|
900
|
-
--primary-soft: #e9f6fb;
|
|
901
|
-
}
|
|
902
|
-
* { box-sizing: border-box; }
|
|
903
|
-
body {
|
|
904
|
-
background: var(--background);
|
|
905
|
-
color: var(--foreground);
|
|
906
|
-
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
907
|
-
margin: 0;
|
|
908
|
-
min-height: 100vh;
|
|
909
|
-
}
|
|
910
|
-
.frame {
|
|
911
|
-
margin: 0 auto;
|
|
912
|
-
max-width: 768px;
|
|
913
|
-
padding: 0 24px;
|
|
914
|
-
width: 100%;
|
|
915
|
-
}
|
|
916
|
-
header { border-bottom: 1px solid var(--border); }
|
|
917
|
-
.nav {
|
|
918
|
-
align-items: center;
|
|
919
|
-
display: flex;
|
|
920
|
-
gap: 8px;
|
|
921
|
-
min-height: 65px;
|
|
922
|
-
}
|
|
923
|
-
.brand {
|
|
924
|
-
align-items: center;
|
|
925
|
-
color: inherit;
|
|
926
|
-
display: inline-flex;
|
|
927
|
-
font-size: 16px;
|
|
928
|
-
font-weight: 700;
|
|
929
|
-
gap: 6px;
|
|
930
|
-
letter-spacing: -0.02em;
|
|
931
|
-
text-decoration: none;
|
|
932
|
-
}
|
|
933
|
-
.brand svg { height: 20px; width: 20px; }
|
|
934
|
-
main { min-height: calc(100vh - 65px); }
|
|
935
|
-
.hero {
|
|
936
|
-
border-bottom: 1px solid var(--border);
|
|
937
|
-
padding: clamp(72px, 15vw, 144px) 0;
|
|
938
|
-
}
|
|
939
|
-
.content { max-width: 620px; }
|
|
940
|
-
.status {
|
|
941
|
-
align-items: center;
|
|
942
|
-
color: var(--primary);
|
|
943
|
-
display: flex;
|
|
944
|
-
font-size: 15px;
|
|
945
|
-
font-weight: 650;
|
|
946
|
-
gap: 8px;
|
|
947
|
-
margin: 0 0 18px;
|
|
948
|
-
}
|
|
949
|
-
.status-mark {
|
|
950
|
-
background: var(--primary);
|
|
951
|
-
border-radius: 50%;
|
|
952
|
-
height: 8px;
|
|
953
|
-
width: 8px;
|
|
954
|
-
}
|
|
955
|
-
h1 {
|
|
956
|
-
font-size: clamp(38px, 7vw, 58px);
|
|
957
|
-
font-weight: 550;
|
|
958
|
-
letter-spacing: -0.055em;
|
|
959
|
-
line-height: 0.98;
|
|
960
|
-
margin: 0;
|
|
961
|
-
text-wrap: balance;
|
|
962
|
-
}
|
|
963
|
-
.lead {
|
|
964
|
-
color: var(--foreground-muted);
|
|
965
|
-
font-size: clamp(20px, 3vw, 24px);
|
|
966
|
-
font-weight: 500;
|
|
967
|
-
letter-spacing: -0.02em;
|
|
968
|
-
line-height: 1.35;
|
|
969
|
-
margin: 24px 0 0;
|
|
970
|
-
max-width: 600px;
|
|
971
|
-
}
|
|
972
|
-
code {
|
|
973
|
-
background: var(--background-raised);
|
|
974
|
-
border: 1px solid var(--border);
|
|
975
|
-
color: var(--foreground);
|
|
976
|
-
display: inline-block;
|
|
977
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
978
|
-
font-size: 0.84em;
|
|
979
|
-
letter-spacing: -0.02em;
|
|
980
|
-
padding: 3px 7px;
|
|
981
|
-
}
|
|
982
|
-
.details {
|
|
983
|
-
display: grid;
|
|
984
|
-
gap: 16px;
|
|
985
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
986
|
-
padding: 36px 0 48px;
|
|
987
|
-
}
|
|
988
|
-
.detail {
|
|
989
|
-
border-left: 1px solid var(--border);
|
|
990
|
-
padding-left: 16px;
|
|
991
|
-
}
|
|
992
|
-
.detail h2 {
|
|
993
|
-
font-size: 15px;
|
|
994
|
-
font-weight: 650;
|
|
995
|
-
letter-spacing: -0.01em;
|
|
996
|
-
margin: 0;
|
|
997
|
-
}
|
|
998
|
-
.detail p {
|
|
999
|
-
color: var(--foreground-muted);
|
|
1000
|
-
font-size: 15px;
|
|
1001
|
-
line-height: 1.5;
|
|
1002
|
-
margin: 6px 0 0;
|
|
1003
|
-
}
|
|
1004
|
-
@media (prefers-color-scheme: dark) {
|
|
1005
|
-
:root {
|
|
1006
|
-
--background: #343434;
|
|
1007
|
-
--background-raised: #3d3d3d;
|
|
1008
|
-
--foreground: #fafafa;
|
|
1009
|
-
--foreground-muted: #b8b8b8;
|
|
1010
|
-
--border: #484848;
|
|
1011
|
-
--primary: #8ed2ef;
|
|
1012
|
-
--primary-soft: #16485f;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
@media (max-width: 560px) {
|
|
1016
|
-
.frame { padding: 0 20px; }
|
|
1017
|
-
.hero { padding: 72px 0; }
|
|
1018
|
-
.details { grid-template-columns: 1fr; }
|
|
1019
|
-
}
|
|
1020
|
-
</style>
|
|
928
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
|
|
929
|
+
<title>${copy.title}</title>
|
|
1021
930
|
</head>
|
|
1022
931
|
<body>
|
|
1023
|
-
<
|
|
1024
|
-
|
|
1025
|
-
<span class="brand">
|
|
1026
|
-
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
1027
|
-
<path fill="currentColor" d="M12,8c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4C16,9.8,14.2,8,12,8z M3,9L3,9c0.6,0,1-0.4,1-1V5c0-0.6,0.4-1,1-1h3 c0.6,0,1-0.4,1-1S8.6,2,8,2H5C3.3,2,2,3.3,2,5v3C2,8.6,2.4,9,3,9z M8,20H5c-0.6,0-1-0.4-1-1v-3c0-0.6-0.4-1-1-1s-1,0.4-1,1v3 c0,1.7,1.3,3,3,3h3c0.6,0,1-0.4,1-1S8.6,20,8,20z M21,15L21,15c-0.6,0-1,0.4-1,1v3c0,0.6-0.4,1-1,1h-3c-0.6,0-1,0.4-1,1s0.4,1,1,1h3 c1.7,0,3-1.3,3-3v-3C22,15.4,21.6,15,21,15z M19,2h-3c-0.6,0-1,0.4-1,1s0.4,1,1,1h3c0.6,0,1,0.4,1,1v3c0,0.6,0.4,1,1,1s1-0.4,1-1V5 C22,3.3,20.7,2,19,2z" />
|
|
1028
|
-
</svg>
|
|
1029
|
-
seo
|
|
1030
|
-
</span>
|
|
1031
|
-
</div>
|
|
1032
|
-
</header>
|
|
1033
|
-
<main>
|
|
1034
|
-
<section class="hero">
|
|
1035
|
-
<div class="frame content">
|
|
1036
|
-
<p class="status"><span class="status-mark" aria-hidden="true"></span>Google account connected</p>
|
|
1037
|
-
<h1>You can start your first SEO report.</h1>
|
|
1038
|
-
<p class="lead">Go back to your terminal to continue, and the CLI will help you choose a site and save a project profile before future reports.</p>
|
|
1039
|
-
</div>
|
|
1040
|
-
</section>
|
|
1041
|
-
<div class="frame details">
|
|
1042
|
-
<section class="detail">
|
|
1043
|
-
<h2>Safe to close</h2>
|
|
1044
|
-
<p>This tab has done its job. The CLI is waiting for you in the terminal.</p>
|
|
1045
|
-
</section>
|
|
1046
|
-
<section class="detail">
|
|
1047
|
-
<h2>Read-only access</h2>
|
|
1048
|
-
<p>The seo CLI can read the Google data you choose. It cannot change your site or Google account.</p>
|
|
1049
|
-
</section>
|
|
1050
|
-
</div>
|
|
1051
|
-
</main>
|
|
932
|
+
<p>${copy.heading}</p>
|
|
933
|
+
<p>${copy.detail}</p>
|
|
1052
934
|
</body>
|
|
1053
935
|
</html>`;
|
|
1054
936
|
}
|
|
@@ -1100,31 +982,50 @@ async function loginWithLoopback(clientConfig = getClientConfig()) {
|
|
|
1100
982
|
code_challenge_method: "S256"
|
|
1101
983
|
}).toString();
|
|
1102
984
|
await open(authUrl.toString());
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
985
|
+
let callback;
|
|
986
|
+
try {
|
|
987
|
+
callback = await waitForCode({
|
|
1106
988
|
server,
|
|
1107
989
|
redirectUri,
|
|
1108
990
|
state
|
|
1109
|
-
})
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
991
|
+
});
|
|
992
|
+
const tokenJson = await exchangeCode({
|
|
993
|
+
clientConfig,
|
|
994
|
+
code: callback.code,
|
|
995
|
+
redirectUri,
|
|
996
|
+
verifier
|
|
997
|
+
});
|
|
998
|
+
const missingScopes = missingGoogleReadonlyScopes(tokenJson.scope);
|
|
999
|
+
if (missingScopes.length > 0) {
|
|
1000
|
+
const labels = missingScopes.map((scope) => GOOGLE_READONLY_SCOPE_LABELS[scope]);
|
|
1001
|
+
callback.respond(400, oauthCallbackPage({
|
|
1002
|
+
status: "permissions-missing",
|
|
1003
|
+
missing: labels
|
|
1004
|
+
}));
|
|
1005
|
+
throw new SeoError("ACCESS_DENIED", `Google login did not grant ${labels.join(" and ")} read-only access. Run \`seo auth login\` again, choose Select all, and approve both permission boxes.`);
|
|
1006
|
+
}
|
|
1007
|
+
const accountEmail = decodeJwtEmail(tokenJson.id_token) ?? await fetchUserEmail(tokenJson.access_token);
|
|
1008
|
+
if (!accountEmail) throw new Error("Google login succeeded but account email could not be determined.");
|
|
1009
|
+
const tokens = {
|
|
1010
|
+
provider: "google",
|
|
1011
|
+
account_email: accountEmail,
|
|
1012
|
+
scope: tokenJson.scope,
|
|
1013
|
+
token_type: tokenJson.token_type,
|
|
1014
|
+
access_token: tokenJson.access_token,
|
|
1015
|
+
refresh_token: tokenJson.refresh_token,
|
|
1016
|
+
expires_at: Date.now() + tokenJson.expires_in * 1e3,
|
|
1017
|
+
obtained_at: Date.now(),
|
|
1018
|
+
client_source: clientConfig.source
|
|
1019
|
+
};
|
|
1020
|
+
await writeTokens(tokens);
|
|
1021
|
+
callback.respond(200, oauthCallbackPage({ status: "connected" }));
|
|
1022
|
+
return tokens;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
callback?.respond(400, oauthCallbackPage({ status: "failed" }));
|
|
1025
|
+
throw error;
|
|
1026
|
+
} finally {
|
|
1027
|
+
server.close();
|
|
1028
|
+
}
|
|
1128
1029
|
}
|
|
1129
1030
|
function waitForCode(input) {
|
|
1130
1031
|
return new Promise((resolve, reject) => {
|
|
@@ -1137,11 +1038,20 @@ function waitForCode(input) {
|
|
|
1137
1038
|
if (error) throw new Error(`OAuth error: ${error}`);
|
|
1138
1039
|
const incomingCode = reqUrl.searchParams.get("code");
|
|
1139
1040
|
if (!incomingCode) throw new Error("OAuth code missing.");
|
|
1140
|
-
|
|
1141
|
-
res.end(oauthCallbackPage());
|
|
1041
|
+
let responded = false;
|
|
1142
1042
|
clearTimeout(timer);
|
|
1143
|
-
resolve(
|
|
1043
|
+
resolve({
|
|
1044
|
+
code: incomingCode,
|
|
1045
|
+
respond: (status, page) => {
|
|
1046
|
+
if (responded) return;
|
|
1047
|
+
responded = true;
|
|
1048
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
1049
|
+
res.end(page);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1144
1052
|
} catch (error) {
|
|
1053
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1054
|
+
res.end(oauthCallbackPage({ status: "failed" }));
|
|
1145
1055
|
clearTimeout(timer);
|
|
1146
1056
|
reject(error);
|
|
1147
1057
|
}
|
|
@@ -1463,7 +1373,7 @@ function getCacheStats() {
|
|
|
1463
1373
|
const counts = {
|
|
1464
1374
|
sites: database.prepare("SELECT COUNT(*) AS count FROM sites").get(),
|
|
1465
1375
|
gsc_cache: database.prepare("SELECT COUNT(*) AS count FROM gsc_cache").get(),
|
|
1466
|
-
|
|
1376
|
+
google_analytics_cache: database.prepare("SELECT COUNT(*) AS count FROM ga4_cache").get(),
|
|
1467
1377
|
semrush_cache: database.prepare("SELECT COUNT(*) AS count FROM semrush_cache").get(),
|
|
1468
1378
|
http_cache: database.prepare("SELECT COUNT(*) AS count FROM http_cache").get()
|
|
1469
1379
|
};
|
|
@@ -1476,7 +1386,7 @@ function getCacheStats() {
|
|
|
1476
1386
|
function clearCache(provider, olderThanMs) {
|
|
1477
1387
|
const database = getDb();
|
|
1478
1388
|
const cutoff = olderThanMs ? Date.now() - olderThanMs : void 0;
|
|
1479
|
-
const tables = provider === "gsc" ? ["gsc_cache"] : provider === "
|
|
1389
|
+
const tables = provider === "gsc" ? ["gsc_cache"] : provider === "google-analytics" ? ["ga4_cache"] : provider === "semrush" ? ["semrush_cache"] : provider === "http" ? ["http_cache"] : [
|
|
1480
1390
|
"gsc_cache",
|
|
1481
1391
|
"ga4_cache",
|
|
1482
1392
|
"semrush_cache",
|
|
@@ -1520,8 +1430,8 @@ async function runGa4Report(propertyId, body, opts = {}) {
|
|
|
1520
1430
|
body: JSON.stringify(body)
|
|
1521
1431
|
});
|
|
1522
1432
|
if (!response.ok) {
|
|
1523
|
-
if (response.status === 403) throw new Error(`
|
|
1524
|
-
throw new Error(`
|
|
1433
|
+
if (response.status === 403) throw new Error(`Google Analytics report failed with 403. Check access to property ${propertyId}.`);
|
|
1434
|
+
throw new Error(`Google Analytics report failed with ${response.status}.`);
|
|
1525
1435
|
}
|
|
1526
1436
|
const result = await response.json();
|
|
1527
1437
|
if (cacheable) db.prepare(`INSERT OR REPLACE INTO ga4_cache
|
|
@@ -1533,8 +1443,8 @@ async function listGa4AccountSummaries() {
|
|
|
1533
1443
|
const { client } = await createGoogleAccessTokenClient();
|
|
1534
1444
|
const response = await authedFetch$1(client, "https://analyticsadmin.googleapis.com/v1beta/accountSummaries");
|
|
1535
1445
|
if (!response.ok) {
|
|
1536
|
-
if (response.status === 403) throw new Error("
|
|
1537
|
-
throw new Error(`
|
|
1446
|
+
if (response.status === 403) throw new Error("Google Analytics account summary fetch failed with 403. Enable the Google Analytics Admin API and check Analytics access.");
|
|
1447
|
+
throw new Error(`Google Analytics account summary fetch failed with ${response.status}.`);
|
|
1538
1448
|
}
|
|
1539
1449
|
return (await response.json()).accountSummaries ?? [];
|
|
1540
1450
|
}
|
|
@@ -1544,14 +1454,14 @@ async function listGa4DataStreams(propertyId) {
|
|
|
1544
1454
|
let pageToken;
|
|
1545
1455
|
let pageCount = 0;
|
|
1546
1456
|
do {
|
|
1547
|
-
if (pageCount >= 20) throw new Error(`
|
|
1457
|
+
if (pageCount >= 20) throw new Error(`Google Analytics data stream discovery exceeded 20 pages for property ${propertyId}.`);
|
|
1548
1458
|
const url = new URL(`https://analyticsadmin.googleapis.com/v1beta/properties/${ga4PropertyIdFromName(propertyId)}/dataStreams`);
|
|
1549
1459
|
url.searchParams.set("pageSize", "200");
|
|
1550
1460
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
1551
1461
|
const response = await authedFetch$1(client, url.toString());
|
|
1552
1462
|
if (!response.ok) {
|
|
1553
|
-
if (response.status === 403) throw new Error(`
|
|
1554
|
-
throw new Error(`
|
|
1463
|
+
if (response.status === 403) throw new Error(`Google Analytics data stream fetch failed with 403 for property ${propertyId}. Check Analytics access.`);
|
|
1464
|
+
throw new Error(`Google Analytics data stream fetch failed with ${response.status} for property ${propertyId}.`);
|
|
1555
1465
|
}
|
|
1556
1466
|
const json = await response.json();
|
|
1557
1467
|
streams.push(...json.dataStreams ?? []);
|
|
@@ -1577,7 +1487,7 @@ function ga4RowsToObjects(result) {
|
|
|
1577
1487
|
return output;
|
|
1578
1488
|
});
|
|
1579
1489
|
}
|
|
1580
|
-
function ga4ReportQualityWarnings(result, label = "
|
|
1490
|
+
function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
|
|
1581
1491
|
const warnings = [];
|
|
1582
1492
|
if (result.metadata?.dataLossFromOtherRow) warnings.push(`${label} grouped high-cardinality data into (other).`);
|
|
1583
1493
|
if (result.metadata?.subjectToThresholding) warnings.push(`${label} was subject to Google Analytics thresholding.`);
|
|
@@ -1683,11 +1593,11 @@ function compareText$16(left, right) {
|
|
|
1683
1593
|
function metric(row, name) {
|
|
1684
1594
|
const value = row[name];
|
|
1685
1595
|
const parsed = Number(value);
|
|
1686
|
-
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `
|
|
1596
|
+
if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
|
|
1687
1597
|
return parsed;
|
|
1688
1598
|
}
|
|
1689
1599
|
function dateValue$1(value) {
|
|
1690
|
-
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1600
|
+
if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics returned an invalid date value.");
|
|
1691
1601
|
return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
|
|
1692
1602
|
}
|
|
1693
1603
|
function addMetrics(target, source) {
|
|
@@ -1726,7 +1636,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1726
1636
|
const detailTotal = emptyMetrics();
|
|
1727
1637
|
for (const row of input.detailRows) {
|
|
1728
1638
|
const definition = aiReferralSourceForValue(row.sessionSource ?? "");
|
|
1729
|
-
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "
|
|
1639
|
+
if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
|
|
1730
1640
|
const metrics = {
|
|
1731
1641
|
sessions: metric(row, "sessions"),
|
|
1732
1642
|
eventCount: metric(row, "eventCount")
|
|
@@ -1759,11 +1669,11 @@ function analyzeAiReferralRows(input) {
|
|
|
1759
1669
|
...evidenceReasons("Source discovery", input.sourceDiscovery),
|
|
1760
1670
|
...evidenceReasons("Landing-page detail", input.detail),
|
|
1761
1671
|
...evidenceReasons("Total users", input.totalUsersEvidence),
|
|
1762
|
-
...detailMismatch ? ["
|
|
1763
|
-
...timeZones.size > 1 ? ["
|
|
1672
|
+
...detailMismatch ? ["Google Analytics detail totals differ from source-discovery totals; landing-page and daily breakdowns are incomplete."] : [],
|
|
1673
|
+
...timeZones.size > 1 ? ["Google Analytics queries returned inconsistent property time zones."] : []
|
|
1764
1674
|
];
|
|
1765
1675
|
const dataStatus = partialReasons.length ? "partial" : "complete";
|
|
1766
|
-
const generalCaveat = "
|
|
1676
|
+
const generalCaveat = "Google Analytics only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
|
|
1767
1677
|
const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
|
|
1768
1678
|
const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
|
|
1769
1679
|
const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$16(left[0], right[0]))[0]?.[0];
|
|
@@ -1795,13 +1705,13 @@ function analyzeAiReferralRows(input) {
|
|
|
1795
1705
|
dataStatus,
|
|
1796
1706
|
range: input.range,
|
|
1797
1707
|
methodology: {
|
|
1798
|
-
id: "
|
|
1708
|
+
id: "google-analytics-ai-referrals",
|
|
1799
1709
|
version: 2,
|
|
1800
1710
|
attributionDimension: "sessionSource",
|
|
1801
1711
|
sourceRulesVersion: AI_REFERRAL_SOURCES_VERSION
|
|
1802
1712
|
},
|
|
1803
1713
|
dataSource: {
|
|
1804
|
-
provider: "google-analytics
|
|
1714
|
+
provider: "google-analytics",
|
|
1805
1715
|
api: "analyticsdata.v1beta.runReport",
|
|
1806
1716
|
maxRows: input.maxRows,
|
|
1807
1717
|
calls: input.sourceDiscovery.calls + input.detail.calls + input.totalUsersEvidence.calls,
|
|
@@ -1818,7 +1728,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1818
1728
|
totalUsersStatus: input.totalUsers === null ? "not-reported" : "available",
|
|
1819
1729
|
sources: sourceTotals.size,
|
|
1820
1730
|
landingPages: pageTotals.size,
|
|
1821
|
-
verdict: total.sessions ? dataStatus === "complete" ? "Detected AI referral traffic in
|
|
1731
|
+
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.",
|
|
1822
1732
|
caveat: generalCaveat
|
|
1823
1733
|
},
|
|
1824
1734
|
sources: [...sourceTotals.values()].map((source) => ({
|
|
@@ -1840,7 +1750,7 @@ function analyzeAiReferralRows(input) {
|
|
|
1840
1750
|
})).sort((left, right) => compareText$16(left.date, right.date)),
|
|
1841
1751
|
caveats: [
|
|
1842
1752
|
generalCaveat,
|
|
1843
|
-
"Attribution uses
|
|
1753
|
+
"Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
|
|
1844
1754
|
"Source totals come from a low-cardinality source query; landing-page and daily detail is queried separately for the exact observed source values.",
|
|
1845
1755
|
"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.",
|
|
1846
1756
|
...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.`] : [],
|
|
@@ -1942,7 +1852,7 @@ function reportRange(input) {
|
|
|
1942
1852
|
const endDate = input.endDate ?? "yesterday";
|
|
1943
1853
|
const absolute = validIsoDate$1(startDate) && validIsoDate$1(endDate);
|
|
1944
1854
|
const relative = validRelativeDate(startDate) && validRelativeDate(endDate);
|
|
1945
|
-
if (!absolute && !relative) throw new SeoError("INVALID_INPUT", "startDate and endDate must both be YYYY-MM-DD dates or
|
|
1855
|
+
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.");
|
|
1946
1856
|
if (absolute && startDate > endDate) throw new SeoError("INVALID_INPUT", "startDate must be on or before endDate.");
|
|
1947
1857
|
return {
|
|
1948
1858
|
startDate,
|
|
@@ -2001,7 +1911,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
2001
1911
|
const property = input.property.trim();
|
|
2002
1912
|
if (!property) throw new SeoError("INVALID_INPUT", "property is required.");
|
|
2003
1913
|
const range = reportRange(input);
|
|
2004
|
-
const
|
|
1914
|
+
const googleAnalyticsDateRange = {
|
|
2005
1915
|
startDate: range.startDate,
|
|
2006
1916
|
endDate: range.endDate
|
|
2007
1917
|
};
|
|
@@ -2011,7 +1921,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
2011
1921
|
const sourceResult = await fetchAiReferralRows({
|
|
2012
1922
|
property,
|
|
2013
1923
|
request: {
|
|
2014
|
-
dateRanges: [
|
|
1924
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
2015
1925
|
dimensions: [{ name: "sessionSource" }],
|
|
2016
1926
|
metrics: [{ name: "sessions" }, { name: "eventCount" }],
|
|
2017
1927
|
orderBys: [{
|
|
@@ -2036,7 +1946,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
2036
1946
|
detailResult = await fetchAiReferralRows({
|
|
2037
1947
|
property,
|
|
2038
1948
|
request: {
|
|
2039
|
-
dateRanges: [
|
|
1949
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
2040
1950
|
dimensions: [
|
|
2041
1951
|
{ name: "date" },
|
|
2042
1952
|
{ name: "sessionSource" },
|
|
@@ -2072,7 +1982,7 @@ async function aiReferralsReport(input, dependencies = {}) {
|
|
|
2072
1982
|
usersResult = await fetchAiReferralRows({
|
|
2073
1983
|
property,
|
|
2074
1984
|
request: {
|
|
2075
|
-
dateRanges: [
|
|
1985
|
+
dateRanges: [googleAnalyticsDateRange],
|
|
2076
1986
|
metrics: [{ name: "totalUsers" }],
|
|
2077
1987
|
dimensionFilter
|
|
2078
1988
|
},
|
|
@@ -10033,7 +9943,7 @@ function crawlDefinitionId(input) {
|
|
|
10033
9943
|
const normalized = {
|
|
10034
9944
|
config: normalizeCrawlConfig(input.config),
|
|
10035
9945
|
site: input.site ?? null,
|
|
10036
|
-
|
|
9946
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId ?? null
|
|
10037
9947
|
};
|
|
10038
9948
|
return `crawl_def_${createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 20)}`;
|
|
10039
9949
|
}
|
|
@@ -10236,7 +10146,7 @@ function createCrawlReport(input) {
|
|
|
10236
10146
|
const definitionId = crawlDefinitionId({
|
|
10237
10147
|
config,
|
|
10238
10148
|
site: input.site,
|
|
10239
|
-
|
|
10149
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId
|
|
10240
10150
|
});
|
|
10241
10151
|
const safePagesWithLinks = sortPages(sanitizePages(deriveInternalLinkAuthority(input.pages ?? [], input.linkGraph)));
|
|
10242
10152
|
const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
|
|
@@ -10249,7 +10159,7 @@ function createCrawlReport(input) {
|
|
|
10249
10159
|
definitionId,
|
|
10250
10160
|
projectId: input.projectId,
|
|
10251
10161
|
site: input.site,
|
|
10252
|
-
|
|
10162
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
10253
10163
|
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10254
10164
|
status: input.status ?? "completed",
|
|
10255
10165
|
configHash: crawlConfigHash(config),
|
|
@@ -10287,7 +10197,7 @@ function normalizeLoadedCrawlReport(report) {
|
|
|
10287
10197
|
definitionId: report.definitionId ?? crawlDefinitionId({
|
|
10288
10198
|
config: report.config,
|
|
10289
10199
|
site: report.site,
|
|
10290
|
-
|
|
10200
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId
|
|
10291
10201
|
}),
|
|
10292
10202
|
summary: summarizeCrawlReport({
|
|
10293
10203
|
pages,
|
|
@@ -10700,7 +10610,7 @@ async function joinAnalytics(input) {
|
|
|
10700
10610
|
limit: input.limit
|
|
10701
10611
|
});
|
|
10702
10612
|
} catch (error) {
|
|
10703
|
-
const warning = `
|
|
10613
|
+
const warning = `Google Analytics metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
10704
10614
|
input.warnings.push(warning);
|
|
10705
10615
|
return {
|
|
10706
10616
|
status: "unavailable",
|
|
@@ -10713,7 +10623,7 @@ async function joinAnalytics(input) {
|
|
|
10713
10623
|
};
|
|
10714
10624
|
}
|
|
10715
10625
|
if (analytics.warning) {
|
|
10716
|
-
const warning = `
|
|
10626
|
+
const warning = `Google Analytics metrics unavailable: ${analytics.warning}`;
|
|
10717
10627
|
input.warnings.push(warning);
|
|
10718
10628
|
return {
|
|
10719
10629
|
status: "unavailable",
|
|
@@ -10738,7 +10648,7 @@ async function joinAnalytics(input) {
|
|
|
10738
10648
|
const sourceQualityKnown = analytics.source?.dataStatus !== void 0;
|
|
10739
10649
|
const retainedRowLimitReached = analytics.source?.retainedRowLimitReached;
|
|
10740
10650
|
const qualityWarnings = analytics.source?.qualityWarnings ?? [];
|
|
10741
|
-
const warning = !sourceQualityKnown ? `
|
|
10651
|
+
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;
|
|
10742
10652
|
if (warning) input.warnings.push(warning);
|
|
10743
10653
|
return {
|
|
10744
10654
|
status: !sourceQualityKnown || analytics.source?.dataStatus === "partial" ? "partial" : joinedPages ? "joined" : "none",
|
|
@@ -10898,7 +10808,7 @@ async function crawlDataSources(input) {
|
|
|
10898
10808
|
joinedPages: 0,
|
|
10899
10809
|
retainedRowLimit: input.analyticsLimit,
|
|
10900
10810
|
retainedRowLimitReached: false,
|
|
10901
|
-
warning: input.cancelled ? "
|
|
10811
|
+
warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
|
|
10902
10812
|
}
|
|
10903
10813
|
};
|
|
10904
10814
|
if (!input.cancelled && input.site) dataSources.searchConsole = await joinSearchMetrics({
|
|
@@ -10914,8 +10824,8 @@ async function crawlDataSources(input) {
|
|
|
10914
10824
|
queryPagesMetricsBatch: input.queryPagesMetricsBatch,
|
|
10915
10825
|
queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
|
|
10916
10826
|
});
|
|
10917
|
-
if (!input.cancelled && input.
|
|
10918
|
-
propertyId: input.
|
|
10827
|
+
if (!input.cancelled && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
|
|
10828
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
10919
10829
|
pages: input.pages,
|
|
10920
10830
|
warnings: input.warnings,
|
|
10921
10831
|
limit: input.analyticsLimit,
|
|
@@ -11737,11 +11647,11 @@ function landingPageRankingPolicy(input) {
|
|
|
11737
11647
|
};
|
|
11738
11648
|
const warningSuffix = "Observed landing-page values remain visible but do not affect priority scores.";
|
|
11739
11649
|
const warnings = [
|
|
11740
|
-
...input.warning ? [`
|
|
11741
|
-
...input.source?.retainedRowLimitReached ? [`
|
|
11650
|
+
...input.warning ? [`Google Analytics: ${input.warning}`] : [],
|
|
11651
|
+
...input.source?.retainedRowLimitReached ? [`Google Analytics: the retained-row limit was reached. ${warningSuffix}`] : [],
|
|
11742
11652
|
...input.source?.qualityWarnings?.map((warning) => `${warning} ${warningSuffix}`) ?? []
|
|
11743
11653
|
];
|
|
11744
|
-
if (!input.source?.dataStatus && !input.warning) warnings.push(`
|
|
11654
|
+
if (!input.source?.dataStatus && !input.warning) warnings.push(`Google Analytics: landing-page completeness was not reported. ${warningSuffix}`);
|
|
11745
11655
|
return {
|
|
11746
11656
|
canRank: !input.warning && warnings.length === 0 && landingPageValuesCanRank(input.source),
|
|
11747
11657
|
warnings
|
|
@@ -11781,7 +11691,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
|
|
|
11781
11691
|
const values = landingPageValuesFromRows(rows);
|
|
11782
11692
|
const availableRows = result.rowCount;
|
|
11783
11693
|
const retainedRowLimitReached = availableRows !== void 0 && availableRows > rows.length || rows.length >= retainedRowLimit;
|
|
11784
|
-
const qualityWarnings = ga4ReportQualityWarnings(result, "
|
|
11694
|
+
const qualityWarnings = ga4ReportQualityWarnings(result, "Google Analytics landing-page report");
|
|
11785
11695
|
return {
|
|
11786
11696
|
values,
|
|
11787
11697
|
source: {
|
|
@@ -12535,7 +12445,7 @@ async function crawlSite(input, dependencies) {
|
|
|
12535
12445
|
const dataSources = await crawlDataSources({
|
|
12536
12446
|
cancelled,
|
|
12537
12447
|
site,
|
|
12538
|
-
|
|
12448
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
12539
12449
|
pages,
|
|
12540
12450
|
warnings,
|
|
12541
12451
|
searchMetricsLimit,
|
|
@@ -12590,7 +12500,7 @@ async function crawlSite(input, dependencies) {
|
|
|
12590
12500
|
config,
|
|
12591
12501
|
projectId: input.projectId,
|
|
12592
12502
|
site,
|
|
12593
|
-
|
|
12503
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
12594
12504
|
dataSources,
|
|
12595
12505
|
pages,
|
|
12596
12506
|
requests,
|
|
@@ -12679,7 +12589,7 @@ function crawlInput$1(input) {
|
|
|
12679
12589
|
url: input.url,
|
|
12680
12590
|
...input.searchSite ? { site: input.searchSite } : {},
|
|
12681
12591
|
projectId: input.projectId,
|
|
12682
|
-
|
|
12592
|
+
googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
|
|
12683
12593
|
mode: "site",
|
|
12684
12594
|
maxPages: input.maxPages,
|
|
12685
12595
|
maxDepth: input.maxDepth,
|
|
@@ -13067,8 +12977,8 @@ const CSV_SCHEMAS = {
|
|
|
13067
12977
|
"target",
|
|
13068
12978
|
"template",
|
|
13069
12979
|
"template_count",
|
|
13070
|
-
"
|
|
13071
|
-
"
|
|
12980
|
+
"google_analytics_sessions",
|
|
12981
|
+
"google_analytics_total_users",
|
|
13072
12982
|
"evidence",
|
|
13073
12983
|
"action"
|
|
13074
12984
|
],
|
|
@@ -13097,7 +13007,7 @@ const CSV_SCHEMAS = {
|
|
|
13097
13007
|
"title",
|
|
13098
13008
|
"target",
|
|
13099
13009
|
"template",
|
|
13100
|
-
"
|
|
13010
|
+
"google_analytics_sessions",
|
|
13101
13011
|
"evidence",
|
|
13102
13012
|
"action"
|
|
13103
13013
|
],
|
|
@@ -13544,8 +13454,8 @@ function refreshPrioritiesCsvFiles(report) {
|
|
|
13544
13454
|
target: item.target,
|
|
13545
13455
|
template: item.template?.label,
|
|
13546
13456
|
template_count: item.template?.count,
|
|
13547
|
-
|
|
13548
|
-
|
|
13457
|
+
google_analytics_sessions: item.analytics?.sessions,
|
|
13458
|
+
google_analytics_total_users: item.analytics?.totalUsers,
|
|
13549
13459
|
evidence: item.evidence,
|
|
13550
13460
|
action: item.action
|
|
13551
13461
|
}))
|
|
@@ -13580,7 +13490,7 @@ function refreshPrioritiesCsvFiles(report) {
|
|
|
13580
13490
|
title: finding.title,
|
|
13581
13491
|
target: finding.target,
|
|
13582
13492
|
template: finding.template?.label,
|
|
13583
|
-
|
|
13493
|
+
google_analytics_sessions: finding.analytics?.sessions,
|
|
13584
13494
|
evidence: finding.evidence,
|
|
13585
13495
|
action: finding.action
|
|
13586
13496
|
})))
|
|
@@ -13829,7 +13739,7 @@ function searchValueForGroup(report, urls) {
|
|
|
13829
13739
|
};
|
|
13830
13740
|
}
|
|
13831
13741
|
function whyThisRanks(input, recommendation, searchStatus) {
|
|
13832
|
-
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 ? `
|
|
13742
|
+
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}.`}`;
|
|
13833
13743
|
}
|
|
13834
13744
|
function verificationCommand(report, severity) {
|
|
13835
13745
|
return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
|
|
@@ -14139,7 +14049,7 @@ function snapshotInput(report) {
|
|
|
14139
14049
|
generatedAt: report.generatedAt,
|
|
14140
14050
|
projectId: report.projectId,
|
|
14141
14051
|
site: report.site,
|
|
14142
|
-
|
|
14052
|
+
googleAnalyticsPropertyId: report.googleAnalyticsPropertyId,
|
|
14143
14053
|
url: report.config.url,
|
|
14144
14054
|
status: report.status,
|
|
14145
14055
|
configHash: report.configHash,
|
|
@@ -15780,7 +15690,7 @@ const crawlReportBaseSchema = z.object({
|
|
|
15780
15690
|
definitionId: z.string(),
|
|
15781
15691
|
projectId: z.string().optional(),
|
|
15782
15692
|
site: z.string().optional(),
|
|
15783
|
-
|
|
15693
|
+
googleAnalyticsPropertyId: z.string().optional(),
|
|
15784
15694
|
generatedAt: z.string().datetime(),
|
|
15785
15695
|
status: z.enum([
|
|
15786
15696
|
"completed",
|
|
@@ -19715,7 +19625,7 @@ function stringMatchType(matchType) {
|
|
|
19715
19625
|
if (matchType === "equals") return "EXACT";
|
|
19716
19626
|
return "CONTAINS";
|
|
19717
19627
|
}
|
|
19718
|
-
function
|
|
19628
|
+
function googleAnalyticsLandingPageFilterForChange(change, group) {
|
|
19719
19629
|
if (change.scope === "site") return void 0;
|
|
19720
19630
|
if (change.scope === "query") return void 0;
|
|
19721
19631
|
if (change.scope === "group") {
|
|
@@ -19738,7 +19648,7 @@ function ga4LandingPageFilterForChange(change, group) {
|
|
|
19738
19648
|
}
|
|
19739
19649
|
} };
|
|
19740
19650
|
}
|
|
19741
|
-
function
|
|
19651
|
+
function summarizeGoogleAnalyticsRows(rows) {
|
|
19742
19652
|
return rows.reduce((sum, row) => ({
|
|
19743
19653
|
sessions: fixed(sum.sessions + (Number(row.sessions) || 0)),
|
|
19744
19654
|
engagedSessions: fixed(sum.engagedSessions + (Number(row.engagedSessions) || 0)),
|
|
@@ -19805,8 +19715,8 @@ function classify(input) {
|
|
|
19805
19715
|
}
|
|
19806
19716
|
//#endregion
|
|
19807
19717
|
//#region packages/core/src/analyze/experiments/measurement/ga4-change-provider.ts
|
|
19808
|
-
async function
|
|
19809
|
-
const result = await input.
|
|
19718
|
+
async function queryGoogleAnalyticsMetrics(input) {
|
|
19719
|
+
const result = await input.googleAnalyticsReport(input.propertyId, {
|
|
19810
19720
|
dateRanges: [input.window],
|
|
19811
19721
|
dimensions: [{ name: "date" }],
|
|
19812
19722
|
metrics: [
|
|
@@ -19820,7 +19730,7 @@ async function queryGa4Metrics(input) {
|
|
|
19820
19730
|
}, { refresh: input.refresh });
|
|
19821
19731
|
const rows = ga4RowsToObjects(result);
|
|
19822
19732
|
return {
|
|
19823
|
-
metrics:
|
|
19733
|
+
metrics: summarizeGoogleAnalyticsRows(rows),
|
|
19824
19734
|
source: {
|
|
19825
19735
|
rows: rows.length,
|
|
19826
19736
|
rowCount: result.rowCount ?? rows.length,
|
|
@@ -19830,19 +19740,19 @@ async function queryGa4Metrics(input) {
|
|
|
19830
19740
|
warnings: ga4ReportQualityWarnings(result, input.label)
|
|
19831
19741
|
};
|
|
19832
19742
|
}
|
|
19833
|
-
async function
|
|
19834
|
-
const [before, after] = await Promise.all([
|
|
19743
|
+
async function queryGoogleAnalyticsChangeWindows(input) {
|
|
19744
|
+
const [before, after] = await Promise.all([queryGoogleAnalyticsMetrics({
|
|
19835
19745
|
...input,
|
|
19836
19746
|
window: input.before,
|
|
19837
|
-
label: "
|
|
19838
|
-
}),
|
|
19747
|
+
label: "Google Analytics before window"
|
|
19748
|
+
}), queryGoogleAnalyticsMetrics({
|
|
19839
19749
|
...input,
|
|
19840
19750
|
window: input.after,
|
|
19841
|
-
label: "
|
|
19751
|
+
label: "Google Analytics after window"
|
|
19842
19752
|
})]);
|
|
19843
19753
|
const metadataWarnings = [];
|
|
19844
|
-
if (before.source.timeZone && after.source.timeZone && before.source.timeZone !== after.source.timeZone) metadataWarnings.push("
|
|
19845
|
-
if (before.source.currencyCode && after.source.currencyCode && before.source.currencyCode !== after.source.currencyCode) metadataWarnings.push("
|
|
19754
|
+
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.");
|
|
19755
|
+
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.");
|
|
19846
19756
|
return {
|
|
19847
19757
|
report: {
|
|
19848
19758
|
propertyId: input.propertyId,
|
|
@@ -19858,7 +19768,7 @@ async function queryGa4ChangeWindows(input) {
|
|
|
19858
19768
|
before: before.metrics,
|
|
19859
19769
|
after: after.metrics
|
|
19860
19770
|
}),
|
|
19861
|
-
note: "
|
|
19771
|
+
note: "Google Analytics attribution is landing-page based. Query-level tests use GSC only."
|
|
19862
19772
|
},
|
|
19863
19773
|
source: {
|
|
19864
19774
|
before: before.source,
|
|
@@ -20096,7 +20006,7 @@ function measurementWindow(input) {
|
|
|
20096
20006
|
//#region packages/core/src/analyze/experiments/measure-change.ts
|
|
20097
20007
|
const defaultDependencies$5 = {
|
|
20098
20008
|
searchAnalytics: querySearchAnalytics,
|
|
20099
|
-
|
|
20009
|
+
googleAnalyticsReport: runGa4Report,
|
|
20100
20010
|
contentGroup: getContentGroup,
|
|
20101
20011
|
now: () => /* @__PURE__ */ new Date()
|
|
20102
20012
|
};
|
|
@@ -20182,15 +20092,15 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
20182
20092
|
confidence: "medium",
|
|
20183
20093
|
note: `${classified.note} Confidence is capped because Search Console evidence is partial.`
|
|
20184
20094
|
} : classified;
|
|
20185
|
-
const
|
|
20095
|
+
const googleAnalyticsFilter = googleAnalyticsLandingPageFilterForChange(change, group);
|
|
20186
20096
|
const queryDimension = change.scope === "query" || group?.dimension === "query";
|
|
20187
|
-
const analytics = input.
|
|
20188
|
-
propertyId: input.
|
|
20097
|
+
const analytics = input.googleAnalyticsPropertyId && !queryDimension ? await queryGoogleAnalyticsChangeWindows({
|
|
20098
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
20189
20099
|
before,
|
|
20190
20100
|
after,
|
|
20191
|
-
filter:
|
|
20101
|
+
filter: googleAnalyticsFilter,
|
|
20192
20102
|
refresh: input.refresh,
|
|
20193
|
-
|
|
20103
|
+
googleAnalyticsReport: dependencies.googleAnalyticsReport
|
|
20194
20104
|
}) : void 0;
|
|
20195
20105
|
const warnings = [
|
|
20196
20106
|
...measured.warnings,
|
|
@@ -20269,7 +20179,7 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
|
|
|
20269
20179
|
...window.afterWindowTruncated ? [`The requested ${window.requestedDays}-day after window is incomplete, so this run is partial and has no directional verdict.`] : [],
|
|
20270
20180
|
...shortWindow ? ["Fewer than 7 finalized days per window is too short for a directional verdict."] : [],
|
|
20271
20181
|
...measured.completeness === "retained-query-date-aggregates" ? ["Query-scoped Search Console data can omit anonymized queries; an absent retained row is not zero traffic."] : [],
|
|
20272
|
-
...analytics ? [`
|
|
20182
|
+
...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.`] : []
|
|
20273
20183
|
]
|
|
20274
20184
|
};
|
|
20275
20185
|
}
|
|
@@ -25940,7 +25850,7 @@ function reportCaveats(input) {
|
|
|
25940
25850
|
`Brand queries: ${input.includeBrand ? "included" : input.brandTerms?.length ? "excluded where saved brand terms matched" : "no saved brand terms, so no brand filter was applied"}.`,
|
|
25941
25851
|
`Data freshness: ${input.refresh ? "fresh fetch requested; local cache bypassed where supported" : "local cache allowed; rerun with --refresh to bypass cached GSC/HTTP data"}.`,
|
|
25942
25852
|
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.",
|
|
25943
|
-
"
|
|
25853
|
+
"Google Analytics: not included in this narrative; use refresh-priorities when Google Analytics value should influence prioritisation.",
|
|
25944
25854
|
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."
|
|
25945
25855
|
];
|
|
25946
25856
|
}
|
|
@@ -26836,17 +26746,17 @@ async function crawlImplementationQueueWorkflow(input, dependencies) {
|
|
|
26836
26746
|
warnings: crawl.warnings
|
|
26837
26747
|
}),
|
|
26838
26748
|
joinStep({
|
|
26839
|
-
tool: "
|
|
26840
|
-
enabled: Boolean(input.
|
|
26841
|
-
warningPrefix: "
|
|
26842
|
-
enabledSummary: "Joined
|
|
26843
|
-
skippedSummary: "No
|
|
26749
|
+
tool: "seo_google_analytics_landing_page_values",
|
|
26750
|
+
enabled: Boolean(input.googleAnalyticsPropertyId),
|
|
26751
|
+
warningPrefix: "Google Analytics metrics skipped:",
|
|
26752
|
+
enabledSummary: "Joined Google Analytics landing-page value where URLs matched.",
|
|
26753
|
+
skippedSummary: "No Google Analytics property was selected.",
|
|
26844
26754
|
warnings: crawl.warnings
|
|
26845
26755
|
}),
|
|
26846
26756
|
{
|
|
26847
26757
|
tool: "seo_top_crawl_fixes",
|
|
26848
26758
|
status: "completed",
|
|
26849
|
-
summary: "Grouped crawl issues, weighted them by severity, affected URLs, GSC visibility,
|
|
26759
|
+
summary: "Grouped crawl issues, weighted them by severity, affected URLs, GSC visibility, Google Analytics value, and effort."
|
|
26850
26760
|
}
|
|
26851
26761
|
],
|
|
26852
26762
|
actions: queue.slice(0, 5).map((item) => ({
|
|
@@ -27199,12 +27109,12 @@ async function refreshPrioritiesWorkflow(input) {
|
|
|
27199
27109
|
});
|
|
27200
27110
|
const range = defaultDateRange$1(input.days ?? 28);
|
|
27201
27111
|
const analytics = await fetchLandingPageValues({
|
|
27202
|
-
propertyId: input.
|
|
27112
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
27203
27113
|
startDate: range.startDate,
|
|
27204
27114
|
endDate: range.endDate
|
|
27205
27115
|
});
|
|
27206
27116
|
const analyticsPolicy = landingPageRankingPolicy({
|
|
27207
|
-
propertyId: input.
|
|
27117
|
+
propertyId: input.googleAnalyticsPropertyId,
|
|
27208
27118
|
source: analytics.source,
|
|
27209
27119
|
warning: analytics.warning
|
|
27210
27120
|
});
|
|
@@ -27761,6 +27671,9 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
|
|
|
27761
27671
|
}
|
|
27762
27672
|
//#endregion
|
|
27763
27673
|
//#region packages/core/src/clients.ts
|
|
27674
|
+
function googleAnalyticsPropertyId(client) {
|
|
27675
|
+
return client?.analytics.google?.propertyId;
|
|
27676
|
+
}
|
|
27764
27677
|
function slug(value) {
|
|
27765
27678
|
return value.toLowerCase().replace(/^https?:\/\//, "").replace(/^sc-domain:/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
27766
27679
|
}
|
|
@@ -27797,7 +27710,7 @@ function saveClient(input) {
|
|
|
27797
27710
|
name: input.name ?? existing?.name,
|
|
27798
27711
|
siteUrl: input.siteUrl
|
|
27799
27712
|
})),
|
|
27800
|
-
|
|
27713
|
+
analytics: input.analytics ?? existing?.analytics ?? {},
|
|
27801
27714
|
reportDay: input.reportDay ?? existing?.reportDay,
|
|
27802
27715
|
technicalWeekday: input.technicalWeekday ?? existing?.technicalWeekday,
|
|
27803
27716
|
isDefault: input.isDefault ?? existing?.isDefault,
|
|
@@ -27879,7 +27792,7 @@ async function runDoctor() {
|
|
|
27879
27792
|
const grantedScopes = new Set(tokens?.scope.split(/\s+/).filter(Boolean) ?? []);
|
|
27880
27793
|
for (const scope of REQUIRED_SCOPES) checks.push({
|
|
27881
27794
|
id: `scope:${scope}`,
|
|
27882
|
-
label: scope.endsWith("webmasters.readonly") ? "
|
|
27795
|
+
label: scope.endsWith("webmasters.readonly") ? "Search Console scope" : "Google Analytics scope",
|
|
27883
27796
|
status: serviceAccount.configured || tokens && grantedScopes.has(scope) ? "pass" : tokens ? "fail" : "warn",
|
|
27884
27797
|
detail: serviceAccount.configured ? `${scope} requested by the service account.` : tokens ? grantedScopes.has(scope) ? scope : `Missing ${scope}.` : "Cannot inspect scopes until login is complete.",
|
|
27885
27798
|
fix: !serviceAccount.configured && tokens && !grantedScopes.has(scope) ? "Run `seo auth logout` then `seo auth login`." : void 0
|
|
@@ -27891,10 +27804,10 @@ async function runDoctor() {
|
|
|
27891
27804
|
detail: config.defaultSite ?? "No saved default. Human CLI commands will prompt; agents can pass --site."
|
|
27892
27805
|
});
|
|
27893
27806
|
checks.push({
|
|
27894
|
-
id: "default-
|
|
27895
|
-
label: "Saved
|
|
27807
|
+
id: "default-google-analytics",
|
|
27808
|
+
label: "Saved Google Analytics property",
|
|
27896
27809
|
status: "pass",
|
|
27897
|
-
detail: config.google.
|
|
27810
|
+
detail: config.analytics.google.defaultPropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
|
|
27898
27811
|
});
|
|
27899
27812
|
return {
|
|
27900
27813
|
ok: checks.every((check) => check.status !== "fail"),
|
|
@@ -28266,7 +28179,7 @@ function matchGa4WebStreams(site, candidates) {
|
|
|
28266
28179
|
function ga4MatchReason(match, site) {
|
|
28267
28180
|
const stream = match.stream.displayName ?? match.defaultUri;
|
|
28268
28181
|
const location = match.match === "domain" ? "is inside" : "matches";
|
|
28269
|
-
return `Matched
|
|
28182
|
+
return `Matched Google Analytics web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
|
|
28270
28183
|
}
|
|
28271
28184
|
//#endregion
|
|
28272
28185
|
//#region packages/core/src/logger.ts
|
|
@@ -28574,6 +28487,371 @@ var JsonFileStorageAdapter = class {
|
|
|
28574
28487
|
};
|
|
28575
28488
|
const localCredentialsProvider = new LocalCredentialsProvider();
|
|
28576
28489
|
//#endregion
|
|
28490
|
+
//#region packages/core/src/version.ts
|
|
28491
|
+
const SEO_VERSION = version;
|
|
28492
|
+
//#endregion
|
|
28493
|
+
//#region packages/core/src/telemetry.ts
|
|
28494
|
+
const TELEMETRY_ENDPOINT = "https://seoskill.dev/api/t";
|
|
28495
|
+
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
28496
|
+
const TELEMETRY_TIMEOUT_MS = 2e3;
|
|
28497
|
+
const TELEMETRY_EVENTS = [
|
|
28498
|
+
"first_run",
|
|
28499
|
+
"setup_complete",
|
|
28500
|
+
"audit_start",
|
|
28501
|
+
"audit_complete",
|
|
28502
|
+
"audit_failed",
|
|
28503
|
+
"first_audit_complete",
|
|
28504
|
+
"active_d1",
|
|
28505
|
+
"active_d7",
|
|
28506
|
+
"active_d30"
|
|
28507
|
+
];
|
|
28508
|
+
const TELEMETRY_AGENTS = [
|
|
28509
|
+
"claude-code",
|
|
28510
|
+
"cursor",
|
|
28511
|
+
"codex",
|
|
28512
|
+
"cli",
|
|
28513
|
+
"unknown"
|
|
28514
|
+
];
|
|
28515
|
+
const TELEMETRY_ERROR_CATEGORIES = [
|
|
28516
|
+
"auth",
|
|
28517
|
+
"crawl_timeout",
|
|
28518
|
+
"network",
|
|
28519
|
+
"config",
|
|
28520
|
+
"unknown"
|
|
28521
|
+
];
|
|
28522
|
+
const TELEMETRY_REPORTS = [
|
|
28523
|
+
"affected-urls",
|
|
28524
|
+
"agent-readiness",
|
|
28525
|
+
"ai-readiness",
|
|
28526
|
+
"ai-referrals",
|
|
28527
|
+
"ai-search-scorecard",
|
|
28528
|
+
"audit-page",
|
|
28529
|
+
"audit-urls",
|
|
28530
|
+
"cannibalisation",
|
|
28531
|
+
"community-intent",
|
|
28532
|
+
"compare-crawls",
|
|
28533
|
+
"content-optimization",
|
|
28534
|
+
"crawl-diff",
|
|
28535
|
+
"site-crawl",
|
|
28536
|
+
"ctr-underperformers",
|
|
28537
|
+
"decaying-pages",
|
|
28538
|
+
"setup-check",
|
|
28539
|
+
"entity-readiness",
|
|
28540
|
+
"explain-crawl-issue",
|
|
28541
|
+
"geo-gaps",
|
|
28542
|
+
"crawl-report",
|
|
28543
|
+
"index-coverage",
|
|
28544
|
+
"index-coverage-plan",
|
|
28545
|
+
"index-monitor",
|
|
28546
|
+
"index-watch",
|
|
28547
|
+
"internal-links",
|
|
28548
|
+
"link-recovery",
|
|
28549
|
+
"crawl-history",
|
|
28550
|
+
"crawler-rules",
|
|
28551
|
+
"llms-txt-audit",
|
|
28552
|
+
"generate-llms-txt",
|
|
28553
|
+
"measure-change",
|
|
28554
|
+
"monthly-report",
|
|
28555
|
+
"okf-build",
|
|
28556
|
+
"okf-validate",
|
|
28557
|
+
"page-opportunities",
|
|
28558
|
+
"performance-audit",
|
|
28559
|
+
"pseo-audit",
|
|
28560
|
+
"query-clusters",
|
|
28561
|
+
"quick-wins",
|
|
28562
|
+
"redirect-trace",
|
|
28563
|
+
"narrative-report",
|
|
28564
|
+
"second-page",
|
|
28565
|
+
"segment-impact",
|
|
28566
|
+
"striking-distance",
|
|
28567
|
+
"seo-to-ai-query",
|
|
28568
|
+
"top-fixes",
|
|
28569
|
+
"traffic-anomaly",
|
|
28570
|
+
"update-correlation",
|
|
28571
|
+
"search-performance-overview",
|
|
28572
|
+
"monthly-action-plan",
|
|
28573
|
+
"refresh-priorities",
|
|
28574
|
+
"technical-watch",
|
|
28575
|
+
"update-postmortem"
|
|
28576
|
+
];
|
|
28577
|
+
const RETENTION_MILESTONES = [
|
|
28578
|
+
{
|
|
28579
|
+
days: 1,
|
|
28580
|
+
event: "active_d1"
|
|
28581
|
+
},
|
|
28582
|
+
{
|
|
28583
|
+
days: 7,
|
|
28584
|
+
event: "active_d7"
|
|
28585
|
+
},
|
|
28586
|
+
{
|
|
28587
|
+
days: 30,
|
|
28588
|
+
event: "active_d30"
|
|
28589
|
+
}
|
|
28590
|
+
];
|
|
28591
|
+
const CI_ENVIRONMENT_KEYS = [
|
|
28592
|
+
"GITHUB_ACTIONS",
|
|
28593
|
+
"GITLAB_CI",
|
|
28594
|
+
"CIRCLECI",
|
|
28595
|
+
"TRAVIS",
|
|
28596
|
+
"BUILDKITE",
|
|
28597
|
+
"JENKINS_URL",
|
|
28598
|
+
"TEAMCITY_VERSION",
|
|
28599
|
+
"TF_BUILD",
|
|
28600
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
28601
|
+
"CODEBUILD_BUILD_ID"
|
|
28602
|
+
];
|
|
28603
|
+
const TELEMETRY_NOTICE = `SEO Skill collects anonymous usage data to improve the tool (event name,
|
|
28604
|
+
report id, version, agent, OS, architecture, Node major, and install week;
|
|
28605
|
+
never URLs, report data, or identifiers of any kind).
|
|
28606
|
+
Docs: https://seoskill.dev/telemetry
|
|
28607
|
+
Disable: seo telemetry disable (or set DO_NOT_TRACK=1)\n`;
|
|
28608
|
+
function optionEnv(options) {
|
|
28609
|
+
return options.env ?? process.env;
|
|
28610
|
+
}
|
|
28611
|
+
function optionNow(options) {
|
|
28612
|
+
return options.now ?? /* @__PURE__ */ new Date();
|
|
28613
|
+
}
|
|
28614
|
+
function optionStateFile(options) {
|
|
28615
|
+
return options.stateFile ?? getSeoCliPaths().telemetryStateFile;
|
|
28616
|
+
}
|
|
28617
|
+
function isEnvEnabled(value) {
|
|
28618
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
28619
|
+
}
|
|
28620
|
+
function isTelemetryCi(env = process.env) {
|
|
28621
|
+
return env.NODE_ENV === "test" || Boolean(env.NODE_TEST_CONTEXT) || isEnvEnabled(env.CI) || CI_ENVIRONMENT_KEYS.some((key) => Boolean(env[key]));
|
|
28622
|
+
}
|
|
28623
|
+
function environmentDisableReason(env) {
|
|
28624
|
+
if (isTelemetryCi(env)) return "ci";
|
|
28625
|
+
if (isEnvEnabled(env.DO_NOT_TRACK)) return "do_not_track";
|
|
28626
|
+
if (isEnvEnabled(env.SEOSKILL_TELEMETRY_DISABLED) || isEnvEnabled(env.SEO_TELEMETRY_DISABLED)) return "environment";
|
|
28627
|
+
}
|
|
28628
|
+
function isTelemetryMilestone(value) {
|
|
28629
|
+
return typeof value === "string" && [
|
|
28630
|
+
"first_run",
|
|
28631
|
+
"setup_complete",
|
|
28632
|
+
"first_audit_complete",
|
|
28633
|
+
"active_d1",
|
|
28634
|
+
"active_d7",
|
|
28635
|
+
"active_d30"
|
|
28636
|
+
].includes(value);
|
|
28637
|
+
}
|
|
28638
|
+
function isTelemetryReport(value) {
|
|
28639
|
+
return typeof value === "string" && TELEMETRY_REPORTS.includes(value);
|
|
28640
|
+
}
|
|
28641
|
+
function isValidCohort(value) {
|
|
28642
|
+
return typeof value === "string" && /^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(value);
|
|
28643
|
+
}
|
|
28644
|
+
function isTelemetryState(value) {
|
|
28645
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
28646
|
+
const state = value;
|
|
28647
|
+
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);
|
|
28648
|
+
}
|
|
28649
|
+
function readState(path) {
|
|
28650
|
+
if (!existsSync(path)) return { kind: "missing" };
|
|
28651
|
+
const value = readJsonFile(path);
|
|
28652
|
+
return isTelemetryState(value) ? {
|
|
28653
|
+
kind: "valid",
|
|
28654
|
+
state: value
|
|
28655
|
+
} : { kind: "invalid" };
|
|
28656
|
+
}
|
|
28657
|
+
function writeState(path, state) {
|
|
28658
|
+
writeJsonAtomic(path, state, 384);
|
|
28659
|
+
}
|
|
28660
|
+
function isoWeek(date) {
|
|
28661
|
+
const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
28662
|
+
const day = utc.getUTCDay() || 7;
|
|
28663
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - day);
|
|
28664
|
+
const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
|
|
28665
|
+
const week = Math.ceil(((utc.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
28666
|
+
return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
28667
|
+
}
|
|
28668
|
+
function newState(options, enabled = true) {
|
|
28669
|
+
const now = optionNow(options);
|
|
28670
|
+
return {
|
|
28671
|
+
telemetryEnabled: enabled,
|
|
28672
|
+
firstRunAt: now.toISOString(),
|
|
28673
|
+
cohort: isoWeek(now),
|
|
28674
|
+
sentMilestones: []
|
|
28675
|
+
};
|
|
28676
|
+
}
|
|
28677
|
+
function detectTelemetryAgent(input = {}) {
|
|
28678
|
+
const env = input.env ?? process.env;
|
|
28679
|
+
const clientName = input.clientName?.toLowerCase() ?? "";
|
|
28680
|
+
if (clientName.includes("claude")) return "claude-code";
|
|
28681
|
+
if (clientName.includes("cursor")) return "cursor";
|
|
28682
|
+
if (clientName.includes("codex")) return "codex";
|
|
28683
|
+
if (input.clientName) return "unknown";
|
|
28684
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude-code";
|
|
28685
|
+
if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
|
|
28686
|
+
if (env.CODEX_HOME || env.CODEX_THREAD_ID || env.CODEX_SANDBOX) return "codex";
|
|
28687
|
+
return "cli";
|
|
28688
|
+
}
|
|
28689
|
+
function telemetryAgent(options) {
|
|
28690
|
+
return options.agent ?? detectTelemetryAgent({
|
|
28691
|
+
clientName: options.clientName,
|
|
28692
|
+
env: optionEnv(options)
|
|
28693
|
+
});
|
|
28694
|
+
}
|
|
28695
|
+
function nodeMajor(version) {
|
|
28696
|
+
return version.replace(/^v/, "").split(".")[0] || "unknown";
|
|
28697
|
+
}
|
|
28698
|
+
function buildTelemetryPayload(event, state, fields = {}, options = {}) {
|
|
28699
|
+
return {
|
|
28700
|
+
event,
|
|
28701
|
+
version: options.version ?? SEO_VERSION,
|
|
28702
|
+
agent: telemetryAgent(options),
|
|
28703
|
+
os: options.processPlatform ?? platform(),
|
|
28704
|
+
arch: options.processArch ?? arch(),
|
|
28705
|
+
node: nodeMajor(options.nodeVersion ?? process.version),
|
|
28706
|
+
cohort: state.cohort,
|
|
28707
|
+
schema: 1,
|
|
28708
|
+
...fields.errorCategory ? { errorCategory: fields.errorCategory } : {},
|
|
28709
|
+
...fields.report ? { report: fields.report } : {}
|
|
28710
|
+
};
|
|
28711
|
+
}
|
|
28712
|
+
function dispatchTelemetry(payload, options) {
|
|
28713
|
+
const fetchTelemetry = options.fetch ?? globalThis.fetch;
|
|
28714
|
+
try {
|
|
28715
|
+
fetchTelemetry(options.endpoint ?? "https://seoskill.dev/api/t", {
|
|
28716
|
+
method: "POST",
|
|
28717
|
+
headers: { "content-type": "application/json" },
|
|
28718
|
+
body: JSON.stringify(payload),
|
|
28719
|
+
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS)
|
|
28720
|
+
}).catch(() => void 0);
|
|
28721
|
+
} catch {}
|
|
28722
|
+
}
|
|
28723
|
+
function statusFromState(options) {
|
|
28724
|
+
const stateFile = optionStateFile(options);
|
|
28725
|
+
const envReason = environmentDisableReason(optionEnv(options));
|
|
28726
|
+
const stored = readState(stateFile);
|
|
28727
|
+
if (envReason) return {
|
|
28728
|
+
enabled: false,
|
|
28729
|
+
reason: envReason,
|
|
28730
|
+
stateFile,
|
|
28731
|
+
...stored.kind === "valid" ? { state: stored.state } : {}
|
|
28732
|
+
};
|
|
28733
|
+
if (stored.kind === "invalid") return {
|
|
28734
|
+
enabled: false,
|
|
28735
|
+
reason: "invalid_state",
|
|
28736
|
+
stateFile
|
|
28737
|
+
};
|
|
28738
|
+
if (stored.kind === "valid" && !stored.state.telemetryEnabled) return {
|
|
28739
|
+
enabled: false,
|
|
28740
|
+
reason: "local_setting",
|
|
28741
|
+
stateFile,
|
|
28742
|
+
state: stored.state
|
|
28743
|
+
};
|
|
28744
|
+
return {
|
|
28745
|
+
enabled: true,
|
|
28746
|
+
stateFile,
|
|
28747
|
+
...stored.kind === "valid" ? { state: stored.state } : {}
|
|
28748
|
+
};
|
|
28749
|
+
}
|
|
28750
|
+
function getTelemetryStatus(options = {}) {
|
|
28751
|
+
return statusFromState(options);
|
|
28752
|
+
}
|
|
28753
|
+
function setTelemetryEnabled(enabled, options = {}) {
|
|
28754
|
+
const stateFile = optionStateFile(options);
|
|
28755
|
+
const stored = readState(stateFile);
|
|
28756
|
+
const state = stored.kind === "valid" ? stored.state : newState(options, enabled);
|
|
28757
|
+
state.telemetryEnabled = enabled;
|
|
28758
|
+
writeState(stateFile, state);
|
|
28759
|
+
return statusFromState(options);
|
|
28760
|
+
}
|
|
28761
|
+
function enabledState(options) {
|
|
28762
|
+
const status = statusFromState(options);
|
|
28763
|
+
return status.enabled ? status.state : void 0;
|
|
28764
|
+
}
|
|
28765
|
+
function initializeTelemetry(options = {}) {
|
|
28766
|
+
const initialStatus = statusFromState(options);
|
|
28767
|
+
if (!initialStatus.enabled) return initialStatus;
|
|
28768
|
+
const stateFile = initialStatus.stateFile;
|
|
28769
|
+
const state = initialStatus.state ?? newState(options);
|
|
28770
|
+
const events = [];
|
|
28771
|
+
if (!state.sentMilestones.includes("first_run")) {
|
|
28772
|
+
try {
|
|
28773
|
+
(options.notice ?? ((message) => process.stderr.write(message)))(TELEMETRY_NOTICE);
|
|
28774
|
+
} catch {
|
|
28775
|
+
return {
|
|
28776
|
+
enabled: false,
|
|
28777
|
+
reason: "invalid_state",
|
|
28778
|
+
stateFile
|
|
28779
|
+
};
|
|
28780
|
+
}
|
|
28781
|
+
state.sentMilestones.push("first_run");
|
|
28782
|
+
events.push("first_run");
|
|
28783
|
+
}
|
|
28784
|
+
const daysSinceFirstRun = (optionNow(options).getTime() - Date.parse(state.firstRunAt)) / 864e5;
|
|
28785
|
+
for (const milestone of RETENTION_MILESTONES) if (daysSinceFirstRun >= milestone.days && !state.sentMilestones.includes(milestone.event)) {
|
|
28786
|
+
state.sentMilestones.push(milestone.event);
|
|
28787
|
+
events.push(milestone.event);
|
|
28788
|
+
}
|
|
28789
|
+
try {
|
|
28790
|
+
writeState(stateFile, state);
|
|
28791
|
+
} catch {
|
|
28792
|
+
return {
|
|
28793
|
+
enabled: false,
|
|
28794
|
+
reason: "invalid_state",
|
|
28795
|
+
stateFile
|
|
28796
|
+
};
|
|
28797
|
+
}
|
|
28798
|
+
for (const event of events) dispatchTelemetry(buildTelemetryPayload(event, state, {}, options), options);
|
|
28799
|
+
return {
|
|
28800
|
+
enabled: true,
|
|
28801
|
+
stateFile,
|
|
28802
|
+
state
|
|
28803
|
+
};
|
|
28804
|
+
}
|
|
28805
|
+
function sendEvent(event, fields, options) {
|
|
28806
|
+
const state = enabledState(options);
|
|
28807
|
+
if (!state) return;
|
|
28808
|
+
dispatchTelemetry(buildTelemetryPayload(event, state, fields, options), options);
|
|
28809
|
+
}
|
|
28810
|
+
function trackTelemetryReportStart(report, options = {}) {
|
|
28811
|
+
if (!isTelemetryReport(report)) return;
|
|
28812
|
+
sendEvent("audit_start", { report }, options);
|
|
28813
|
+
}
|
|
28814
|
+
function trackTelemetryReportComplete(report, options = {}) {
|
|
28815
|
+
if (!isTelemetryReport(report)) return;
|
|
28816
|
+
const state = enabledState(options);
|
|
28817
|
+
if (!state) return;
|
|
28818
|
+
dispatchTelemetry(buildTelemetryPayload("audit_complete", state, { report }, options), options);
|
|
28819
|
+
if (state.sentMilestones.includes("first_audit_complete")) return;
|
|
28820
|
+
state.sentMilestones.push("first_audit_complete");
|
|
28821
|
+
try {
|
|
28822
|
+
writeState(optionStateFile(options), state);
|
|
28823
|
+
} catch {
|
|
28824
|
+
return;
|
|
28825
|
+
}
|
|
28826
|
+
dispatchTelemetry(buildTelemetryPayload("first_audit_complete", state, { report }, options), options);
|
|
28827
|
+
}
|
|
28828
|
+
function trackTelemetryReportFailed(report, errorCategory, options = {}) {
|
|
28829
|
+
if (!isTelemetryReport(report)) return;
|
|
28830
|
+
sendEvent("audit_failed", {
|
|
28831
|
+
errorCategory,
|
|
28832
|
+
report
|
|
28833
|
+
}, options);
|
|
28834
|
+
}
|
|
28835
|
+
function trackTelemetrySetupComplete(options = {}) {
|
|
28836
|
+
const state = enabledState(options);
|
|
28837
|
+
if (!state || state.sentMilestones.includes("setup_complete")) return;
|
|
28838
|
+
state.sentMilestones.push("setup_complete");
|
|
28839
|
+
try {
|
|
28840
|
+
writeState(optionStateFile(options), state);
|
|
28841
|
+
} catch {
|
|
28842
|
+
return;
|
|
28843
|
+
}
|
|
28844
|
+
dispatchTelemetry(buildTelemetryPayload("setup_complete", state, {}, options), options);
|
|
28845
|
+
}
|
|
28846
|
+
function telemetryErrorCategory(error) {
|
|
28847
|
+
if (error instanceof Error && error.name === "AbortError") return "crawl_timeout";
|
|
28848
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
28849
|
+
if (code === "ACCESS_DENIED" || code === "AUTH_CONFIG_REQUIRED" || code === "AUTH_EXPIRED" || code === "AUTH_REQUIRED") return "auth";
|
|
28850
|
+
if (code === "INVALID_INPUT" || code === "PROPERTY_NOT_FOUND") return "config";
|
|
28851
|
+
if (code === "OPTIONAL_PROVIDER_UNAVAILABLE" || code === "PROVIDER_UNAVAILABLE" || code === "RATE_LIMITED") return "network";
|
|
28852
|
+
return "unknown";
|
|
28853
|
+
}
|
|
28854
|
+
//#endregion
|
|
28577
28855
|
//#region packages/core/src/utils/time.ts
|
|
28578
28856
|
function parseOlderThan(input) {
|
|
28579
28857
|
if (!input) return;
|
|
@@ -28608,9 +28886,6 @@ function isoDate(daysAgo = 0) {
|
|
|
28608
28886
|
return date.toISOString().slice(0, 10);
|
|
28609
28887
|
}
|
|
28610
28888
|
//#endregion
|
|
28611
|
-
|
|
28612
|
-
const SEO_VERSION = version;
|
|
28613
|
-
//#endregion
|
|
28614
|
-
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DataForSeoProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LocalCredentialsProvider, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyzeAiReferralRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, boundedSitemapInventory, buildOkfBundle, buildPseoAuditReportFromRows, cannibalReport, changeMeasurementCsvRows, classifyCommunityIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteTokens, deriveBrandTerms, detectBrandTerms, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getPreviousRun, getPrivacySnapshot, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTokenStorageStatus, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isoDate, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerPreferenceSchema, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readConfig, readOauthClient, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, renderCrawlCsv, renderCrawlHtml, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, saveClient, saveCrawlReport, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, sessionSourceInListFilter, setDefaultClient, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trafficAnomaly, unavailableSegmentImpactReport, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateOkfFiles, verifyQueryContent, workflowPresentation, writeConfig, writeOauthClient, writeTokens, xRobotsDirectives };
|
|
28889
|
+
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CRAWL_SKIP_REASONS, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DataForSeoProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LocalCredentialsProvider, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, TELEMETRY_AGENTS, TELEMETRY_ENDPOINT, TELEMETRY_ERROR_CATEGORIES, TELEMETRY_EVENTS, TELEMETRY_NOTICE, TELEMETRY_REPORTS, TELEMETRY_SCHEMA_VERSION, TELEMETRY_TIMEOUT_MS, affectedUrls, agentReadiness, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyticsConnectionsSchema, analyzeAiReferralRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, boundedSitemapInventory, buildOkfBundle, buildPseoAuditReportFromRows, buildTelemetryPayload, cannibalReport, changeMeasurementCsvRows, classifyCommunityIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, collectAgentDiscovery, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlSkipImpact, crawlSkipReasonCountsFromRecord, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteTokens, deriveBrandTerms, detectBrandTerms, detectTelemetryAgent, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getPreviousRun, getPrivacySnapshot, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTelemetryStatus, getTokenStorageStatus, googleAnalyticsPropertyId, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, initializeTelemetry, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isEntityEvidenceSchemaType, isSiteIdentitySchemaType, isSkippableReportError, isTelemetryCi, isTelemetryReport, isoDate, isoWeek, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeCrawlSkipReasonCounts, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerPreferenceSchema, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readConfig, readOauthClient, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, renderCrawlCsv, renderCrawlHtml, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, saveClient, saveCrawlReport, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, sessionSourceInListFilter, setDefaultClient, setTelemetryEnabled, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, telemetryErrorCategory, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trackTelemetryReportComplete, trackTelemetryReportFailed, trackTelemetryReportStart, trackTelemetrySetupComplete, trafficAnomaly, unavailableSegmentImpactReport, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateOkfFiles, verifyQueryContent, workflowPresentation, writeConfig, writeOauthClient, writeTokens, xRobotsDirectives };
|
|
28615
28890
|
|
|
28616
28891
|
//# sourceMappingURL=index.js.map
|