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/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-D6bCOqrD.js";
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
- ga4PropertyId: z.string().optional(),
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
- defaultGa4PropertyId: z.string().optional(),
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
- ga4PropertyId: z.string(),
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 oauthCallbackPage() {
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 name="color-scheme" content="light dark">
889
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
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
- <header>
1024
- <div class="frame nav">
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
- const tokenJson = await exchangeCode({
1104
- clientConfig,
1105
- code: await waitForCode({
985
+ let callback;
986
+ try {
987
+ callback = await waitForCode({
1106
988
  server,
1107
989
  redirectUri,
1108
990
  state
1109
- }).finally(() => server.close()),
1110
- redirectUri,
1111
- verifier
1112
- });
1113
- const accountEmail = decodeJwtEmail(tokenJson.id_token) ?? await fetchUserEmail(tokenJson.access_token);
1114
- if (!accountEmail) throw new Error("Google login succeeded but account email could not be determined.");
1115
- const tokens = {
1116
- provider: "google",
1117
- account_email: accountEmail,
1118
- scope: tokenJson.scope,
1119
- token_type: tokenJson.token_type,
1120
- access_token: tokenJson.access_token,
1121
- refresh_token: tokenJson.refresh_token,
1122
- expires_at: Date.now() + tokenJson.expires_in * 1e3,
1123
- obtained_at: Date.now(),
1124
- client_source: clientConfig.source
1125
- };
1126
- await writeTokens(tokens);
1127
- return tokens;
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
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1141
- res.end(oauthCallbackPage());
1041
+ let responded = false;
1142
1042
  clearTimeout(timer);
1143
- resolve(incomingCode);
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
- ga4_cache: database.prepare("SELECT COUNT(*) AS count FROM ga4_cache").get(),
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 === "ga4" ? ["ga4_cache"] : provider === "semrush" ? ["semrush_cache"] : provider === "http" ? ["http_cache"] : [
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(`GA4 report failed with 403. Check access to property ${propertyId}.`);
1524
- throw new Error(`GA4 report failed with ${response.status}.`);
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("GA4 account summary fetch failed with 403. Enable the Google Analytics Admin API and check Analytics access.");
1537
- throw new Error(`GA4 account summary fetch failed with ${response.status}.`);
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(`GA4 data stream discovery exceeded 20 pages for property ${propertyId}.`);
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(`GA4 data stream fetch failed with 403 for property ${propertyId}. Check Analytics access.`);
1554
- throw new Error(`GA4 data stream fetch failed with ${response.status} for property ${propertyId}.`);
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 = "GA4 report") {
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", `GA4 returned an invalid ${name} value.`);
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", "GA4 returned an invalid date value.");
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", "GA4 detail rows contained an unexpected session source.");
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 ? ["GA4 detail totals differ from source-discovery totals; landing-page and daily breakdowns are incomplete."] : [],
1763
- ...timeZones.size > 1 ? ["GA4 queries returned inconsistent property time zones."] : []
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 = "GA4 only attributes visits that arrive with a recognized session source; some AI visits appear as direct, unassigned, or another source.";
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: "ga4-ai-referrals",
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-4",
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 GA4." : "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 GA4 data; this partial report cannot establish absence.",
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 GA4 sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
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 GA4 relative dates such as 28daysAgo and yesterday.");
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 ga4Range = {
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: [ga4Range],
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: [ga4Range],
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: [ga4Range],
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
- ga4PropertyId: input.ga4PropertyId ?? null
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
- ga4PropertyId: input.ga4PropertyId
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
- ga4PropertyId: input.ga4PropertyId,
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
- ga4PropertyId: report.ga4PropertyId
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 = `GA4 metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
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 = `GA4 metrics unavailable: ${analytics.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 ? `GA4 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 ? `GA4 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 ? "GA4 metrics joined for 0 crawled pages." : void 0;
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 ? "GA4 join skipped because the crawl was cancelled." : "GA4 join skipped because no property was selected."
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.ga4PropertyId) dataSources.analytics = await joinAnalytics({
10918
- propertyId: input.ga4PropertyId,
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 ? [`GA4: ${input.warning}`] : [],
11741
- ...input.source?.retainedRowLimitReached ? [`GA4: the retained-row limit was reached. ${warningSuffix}`] : [],
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(`GA4: landing-page completeness was not reported. ${warningSuffix}`);
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, "GA4 landing-page report");
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
- ga4PropertyId: input.ga4PropertyId,
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
- ga4PropertyId: input.ga4PropertyId,
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
- ga4PropertyId: input.ga4PropertyId,
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
- "ga4_sessions",
13071
- "ga4_total_users",
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
- "ga4_sessions",
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
- ga4_sessions: item.analytics?.sessions,
13548
- ga4_total_users: item.analytics?.totalUsers,
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
- ga4_sessions: finding.analytics?.sessions,
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 ? ` GA4 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}.`}`;
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
- ga4PropertyId: report.ga4PropertyId,
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
- ga4PropertyId: z.string().optional(),
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 ga4LandingPageFilterForChange(change, group) {
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 summarizeGa4Rows(rows) {
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 queryGa4Metrics(input) {
19809
- const result = await input.ga4Report(input.propertyId, {
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: summarizeGa4Rows(rows),
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 queryGa4ChangeWindows(input) {
19834
- const [before, after] = await Promise.all([queryGa4Metrics({
19743
+ async function queryGoogleAnalyticsChangeWindows(input) {
19744
+ const [before, after] = await Promise.all([queryGoogleAnalyticsMetrics({
19835
19745
  ...input,
19836
19746
  window: input.before,
19837
- label: "GA4 before window"
19838
- }), queryGa4Metrics({
19747
+ label: "Google Analytics before window"
19748
+ }), queryGoogleAnalyticsMetrics({
19839
19749
  ...input,
19840
19750
  window: input.after,
19841
- label: "GA4 after window"
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("GA4 returned different property timezones across the two windows.");
19845
- if (before.source.currencyCode && after.source.currencyCode && before.source.currencyCode !== after.source.currencyCode) metadataWarnings.push("GA4 returned different currency codes across the two windows.");
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: "GA4 attribution is landing-page based. Query-level tests use GSC only."
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
- ga4Report: runGa4Report,
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 ga4Filter = ga4LandingPageFilterForChange(change, group);
20095
+ const googleAnalyticsFilter = googleAnalyticsLandingPageFilterForChange(change, group);
20186
20096
  const queryDimension = change.scope === "query" || group?.dimension === "query";
20187
- const analytics = input.ga4PropertyId && !queryDimension ? await queryGa4ChangeWindows({
20188
- propertyId: input.ga4PropertyId,
20097
+ const analytics = input.googleAnalyticsPropertyId && !queryDimension ? await queryGoogleAnalyticsChangeWindows({
20098
+ propertyId: input.googleAnalyticsPropertyId,
20189
20099
  before,
20190
20100
  after,
20191
- filter: ga4Filter,
20101
+ filter: googleAnalyticsFilter,
20192
20102
  refresh: input.refresh,
20193
- ga4Report: dependencies.ga4Report
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 ? [`GA4 attribution uses landing pages and the GA4 property timezone${analytics.source.before.timeZone ? ` (${analytics.source.before.timeZone})` : ""}; its day boundaries may differ from Search Console Pacific dates.`] : []
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
- "GA4: not included in this narrative; use refresh-priorities when GA4 value should influence prioritisation.",
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: "seo_ga4_landing_page_values",
26840
- enabled: Boolean(input.ga4PropertyId),
26841
- warningPrefix: "GA4 metrics skipped:",
26842
- enabledSummary: "Joined GA4 landing-page value where URLs matched.",
26843
- skippedSummary: "No GA4 property was selected.",
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, GA4 value, and effort."
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.ga4PropertyId,
27112
+ propertyId: input.googleAnalyticsPropertyId,
27203
27113
  startDate: range.startDate,
27204
27114
  endDate: range.endDate
27205
27115
  });
27206
27116
  const analyticsPolicy = landingPageRankingPolicy({
27207
- propertyId: input.ga4PropertyId,
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
- ga4PropertyId: input.ga4PropertyId ?? existing?.ga4PropertyId,
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") ? "GSC scope" : "GA4 scope",
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-ga4",
27895
- label: "Saved GA4 property",
27807
+ id: "default-google-analytics",
27808
+ label: "Saved Google Analytics property",
27896
27809
  status: "pass",
27897
- detail: config.google.defaultGa4PropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
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 GA4 web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
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
- //#region packages/core/src/version.ts
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