seo 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as load, t as version } from "./package-Jl7x3QBx.js";
2
+ import { n as load, t as version } from "./package-BF_XgK7-.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -222,6 +222,7 @@ const siteSchema = z.object({
222
222
  isDefault: z.boolean().optional()
223
223
  });
224
224
  const providerPreferenceSchema = z.enum(["cheap", "authoritative"]);
225
+ const analyticsConnectionsSchema = z.object({ google: z.object({ propertyId: z.string() }).optional() }).default({});
225
226
  const clientProfileSchema = z.object({
226
227
  id: z.string(),
227
228
  name: z.string(),
@@ -229,7 +230,7 @@ const clientProfileSchema = z.object({
229
230
  startUrl: z.string().optional(),
230
231
  watchUrls: z.array(z.string()).default([]),
231
232
  brandTerms: z.array(z.string()).default([]),
232
- ga4PropertyId: z.string().optional(),
233
+ analytics: analyticsConnectionsSchema,
233
234
  reportDay: z.number().int().min(1).max(31).optional(),
234
235
  technicalWeekday: z.number().int().min(0).max(7).optional(),
235
236
  isDefault: z.boolean().optional(),
@@ -240,14 +241,14 @@ const configSchema = z.object({
240
241
  defaultSite: z.string().optional(),
241
242
  sites: z.array(siteSchema).default([]),
242
243
  clients: z.array(clientProfileSchema).default([]),
243
- google: z.object({
244
- defaultGa4PropertyId: z.string().optional(),
244
+ analytics: z.object({ google: z.object({
245
+ defaultPropertyId: z.string().optional(),
245
246
  propertyMappings: z.array(z.object({
246
247
  siteUrl: z.string(),
247
- ga4PropertyId: z.string(),
248
+ propertyId: z.string(),
248
249
  addedAt: z.number().int().optional()
249
250
  })).default([])
250
- }).default({ propertyMappings: [] }),
251
+ }).default({ propertyMappings: [] }) }).default({ google: { propertyMappings: [] } }),
251
252
  providers: z.object({
252
253
  semrushApiKey: z.string().optional(),
253
254
  dataForSeoLogin: z.string().optional(),
@@ -566,6 +567,14 @@ function missingOAuthClientMessage(source) {
566
567
  //#endregion
567
568
  //#region packages/core/src/gsc/auth/types.ts
568
569
  const GOOGLE_READONLY_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
570
+ const GOOGLE_READONLY_SCOPE_LABELS = {
571
+ "https://www.googleapis.com/auth/webmasters.readonly": "Search Console",
572
+ "https://www.googleapis.com/auth/analytics.readonly": "Google Analytics"
573
+ };
574
+ function missingGoogleReadonlyScopes(scope) {
575
+ const granted = new Set(scope.split(/\s+/u).filter(Boolean));
576
+ return GOOGLE_READONLY_SCOPES.filter((required) => !granted.has(required));
577
+ }
569
578
  const GOOGLE_SCOPE = [
570
579
  ...GOOGLE_READONLY_SCOPES,
571
580
  "openid",
@@ -763,6 +772,11 @@ function authRequired() {
763
772
  function authExpired() {
764
773
  return new SeoError("AUTH_EXPIRED", "Google refresh token is no longer valid. Run `seo auth login` again.");
765
774
  }
775
+ function requireReadonlyScopes(tokens) {
776
+ const missing = missingGoogleReadonlyScopes(tokens.scope);
777
+ if (missing.length === 0) return;
778
+ 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.`);
779
+ }
766
780
  function needsRefresh(tokens, now) {
767
781
  return !tokens.access_token || tokens.expires_at - now < REFRESH_BUFFER_MS;
768
782
  }
@@ -822,6 +836,7 @@ async function createAuthorizedClient() {
822
836
  let tokens = await readTokens();
823
837
  if (!tokens) throw authRequired();
824
838
  requireClientConfig(tokens);
839
+ requireReadonlyScopes(tokens);
825
840
  if (needsRefresh(tokens, Date.now())) tokens = await refreshStoredToken({ onlyIfExpiring: true });
826
841
  return {
827
842
  client: new LocalGoogleAccessTokenClient(tokens),
@@ -879,176 +894,36 @@ async function authStatus() {
879
894
  }
880
895
  //#endregion
881
896
  //#region packages/core/src/gsc/auth/callback-page.ts
882
- function oauthCallbackPage() {
897
+ function callbackCopy(options) {
898
+ if (options.status === "connected") return {
899
+ title: "Google account connected",
900
+ heading: "Google account connected.",
901
+ detail: "This tab can be closed."
902
+ };
903
+ if (options.status === "permissions-missing") return {
904
+ title: "Google permissions not granted",
905
+ heading: "Google permissions were not granted.",
906
+ detail: `Return to your terminal and run seo auth login again. Select all permission boxes for ${options.missing.join(" and ")}.`
907
+ };
908
+ return {
909
+ title: "Google connection failed",
910
+ heading: "Google connection failed.",
911
+ detail: "Return to your terminal for the error and try again."
912
+ };
913
+ }
914
+ function oauthCallbackPage(options = { status: "connected" }) {
915
+ const copy = callbackCopy(options);
883
916
  return `<!doctype html>
884
917
  <html lang="en">
885
918
  <head>
886
919
  <meta charset="utf-8">
887
920
  <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>
921
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'">
922
+ <title>${copy.title}</title>
1021
923
  </head>
1022
924
  <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>
925
+ <p>${copy.heading}</p>
926
+ <p>${copy.detail}</p>
1052
927
  </body>
1053
928
  </html>`;
1054
929
  }
@@ -1100,31 +975,50 @@ async function loginWithLoopback(clientConfig = getClientConfig()) {
1100
975
  code_challenge_method: "S256"
1101
976
  }).toString();
1102
977
  await open(authUrl.toString());
1103
- const tokenJson = await exchangeCode({
1104
- clientConfig,
1105
- code: await waitForCode({
978
+ let callback;
979
+ try {
980
+ callback = await waitForCode({
1106
981
  server,
1107
982
  redirectUri,
1108
983
  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;
984
+ });
985
+ const tokenJson = await exchangeCode({
986
+ clientConfig,
987
+ code: callback.code,
988
+ redirectUri,
989
+ verifier
990
+ });
991
+ const missingScopes = missingGoogleReadonlyScopes(tokenJson.scope);
992
+ if (missingScopes.length > 0) {
993
+ const labels = missingScopes.map((scope) => GOOGLE_READONLY_SCOPE_LABELS[scope]);
994
+ callback.respond(400, oauthCallbackPage({
995
+ status: "permissions-missing",
996
+ missing: labels
997
+ }));
998
+ 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.`);
999
+ }
1000
+ const accountEmail = decodeJwtEmail(tokenJson.id_token) ?? await fetchUserEmail(tokenJson.access_token);
1001
+ if (!accountEmail) throw new Error("Google login succeeded but account email could not be determined.");
1002
+ const tokens = {
1003
+ provider: "google",
1004
+ account_email: accountEmail,
1005
+ scope: tokenJson.scope,
1006
+ token_type: tokenJson.token_type,
1007
+ access_token: tokenJson.access_token,
1008
+ refresh_token: tokenJson.refresh_token,
1009
+ expires_at: Date.now() + tokenJson.expires_in * 1e3,
1010
+ obtained_at: Date.now(),
1011
+ client_source: clientConfig.source
1012
+ };
1013
+ await writeTokens(tokens);
1014
+ callback.respond(200, oauthCallbackPage({ status: "connected" }));
1015
+ return tokens;
1016
+ } catch (error) {
1017
+ callback?.respond(400, oauthCallbackPage({ status: "failed" }));
1018
+ throw error;
1019
+ } finally {
1020
+ server.close();
1021
+ }
1128
1022
  }
1129
1023
  function waitForCode(input) {
1130
1024
  return new Promise((resolve, reject) => {
@@ -1137,11 +1031,20 @@ function waitForCode(input) {
1137
1031
  if (error) throw new Error(`OAuth error: ${error}`);
1138
1032
  const incomingCode = reqUrl.searchParams.get("code");
1139
1033
  if (!incomingCode) throw new Error("OAuth code missing.");
1140
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1141
- res.end(oauthCallbackPage());
1034
+ let responded = false;
1142
1035
  clearTimeout(timer);
1143
- resolve(incomingCode);
1036
+ resolve({
1037
+ code: incomingCode,
1038
+ respond: (status, page) => {
1039
+ if (responded) return;
1040
+ responded = true;
1041
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
1042
+ res.end(page);
1043
+ }
1044
+ });
1144
1045
  } catch (error) {
1046
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
1047
+ res.end(oauthCallbackPage({ status: "failed" }));
1145
1048
  clearTimeout(timer);
1146
1049
  reject(error);
1147
1050
  }
@@ -1463,7 +1366,7 @@ function getCacheStats() {
1463
1366
  const counts = {
1464
1367
  sites: database.prepare("SELECT COUNT(*) AS count FROM sites").get(),
1465
1368
  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(),
1369
+ google_analytics_cache: database.prepare("SELECT COUNT(*) AS count FROM ga4_cache").get(),
1467
1370
  semrush_cache: database.prepare("SELECT COUNT(*) AS count FROM semrush_cache").get(),
1468
1371
  http_cache: database.prepare("SELECT COUNT(*) AS count FROM http_cache").get()
1469
1372
  };
@@ -1476,7 +1379,7 @@ function getCacheStats() {
1476
1379
  function clearCache(provider, olderThanMs) {
1477
1380
  const database = getDb();
1478
1381
  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"] : [
1382
+ const tables = provider === "gsc" ? ["gsc_cache"] : provider === "google-analytics" ? ["ga4_cache"] : provider === "semrush" ? ["semrush_cache"] : provider === "http" ? ["http_cache"] : [
1480
1383
  "gsc_cache",
1481
1384
  "ga4_cache",
1482
1385
  "semrush_cache",
@@ -1520,8 +1423,8 @@ async function runGa4Report(propertyId, body, opts = {}) {
1520
1423
  body: JSON.stringify(body)
1521
1424
  });
1522
1425
  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}.`);
1426
+ if (response.status === 403) throw new Error(`Google Analytics report failed with 403. Check access to property ${propertyId}.`);
1427
+ throw new Error(`Google Analytics report failed with ${response.status}.`);
1525
1428
  }
1526
1429
  const result = await response.json();
1527
1430
  if (cacheable) db.prepare(`INSERT OR REPLACE INTO ga4_cache
@@ -1533,8 +1436,8 @@ async function listGa4AccountSummaries() {
1533
1436
  const { client } = await createGoogleAccessTokenClient();
1534
1437
  const response = await authedFetch$1(client, "https://analyticsadmin.googleapis.com/v1beta/accountSummaries");
1535
1438
  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}.`);
1439
+ 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.");
1440
+ throw new Error(`Google Analytics account summary fetch failed with ${response.status}.`);
1538
1441
  }
1539
1442
  return (await response.json()).accountSummaries ?? [];
1540
1443
  }
@@ -1544,14 +1447,14 @@ async function listGa4DataStreams(propertyId) {
1544
1447
  let pageToken;
1545
1448
  let pageCount = 0;
1546
1449
  do {
1547
- if (pageCount >= 20) throw new Error(`GA4 data stream discovery exceeded 20 pages for property ${propertyId}.`);
1450
+ if (pageCount >= 20) throw new Error(`Google Analytics data stream discovery exceeded 20 pages for property ${propertyId}.`);
1548
1451
  const url = new URL(`https://analyticsadmin.googleapis.com/v1beta/properties/${ga4PropertyIdFromName(propertyId)}/dataStreams`);
1549
1452
  url.searchParams.set("pageSize", "200");
1550
1453
  if (pageToken) url.searchParams.set("pageToken", pageToken);
1551
1454
  const response = await authedFetch$1(client, url.toString());
1552
1455
  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}.`);
1456
+ if (response.status === 403) throw new Error(`Google Analytics data stream fetch failed with 403 for property ${propertyId}. Check Analytics access.`);
1457
+ throw new Error(`Google Analytics data stream fetch failed with ${response.status} for property ${propertyId}.`);
1555
1458
  }
1556
1459
  const json = await response.json();
1557
1460
  streams.push(...json.dataStreams ?? []);
@@ -1577,7 +1480,7 @@ function ga4RowsToObjects(result) {
1577
1480
  return output;
1578
1481
  });
1579
1482
  }
1580
- function ga4ReportQualityWarnings(result, label = "GA4 report") {
1483
+ function ga4ReportQualityWarnings(result, label = "Google Analytics report") {
1581
1484
  const warnings = [];
1582
1485
  if (result.metadata?.dataLossFromOtherRow) warnings.push(`${label} grouped high-cardinality data into (other).`);
1583
1486
  if (result.metadata?.subjectToThresholding) warnings.push(`${label} was subject to Google Analytics thresholding.`);
@@ -1683,11 +1586,11 @@ function compareText$16(left, right) {
1683
1586
  function metric(row, name) {
1684
1587
  const value = row[name];
1685
1588
  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.`);
1589
+ if (value === void 0 || value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) throw new SeoError("PROVIDER_UNAVAILABLE", `Google Analytics returned an invalid ${name} value.`);
1687
1590
  return parsed;
1688
1591
  }
1689
1592
  function dateValue$1(value) {
1690
- if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "GA4 returned an invalid date value.");
1593
+ if (!value || !/^\d{8}$/.test(value)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics returned an invalid date value.");
1691
1594
  return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
1692
1595
  }
1693
1596
  function addMetrics(target, source) {
@@ -1726,7 +1629,7 @@ function analyzeAiReferralRows(input) {
1726
1629
  const detailTotal = emptyMetrics();
1727
1630
  for (const row of input.detailRows) {
1728
1631
  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.");
1632
+ if (!definition || !sourceTotals.has(definition.id)) throw new SeoError("PROVIDER_UNAVAILABLE", "Google Analytics detail rows contained an unexpected session source.");
1730
1633
  const metrics = {
1731
1634
  sessions: metric(row, "sessions"),
1732
1635
  eventCount: metric(row, "eventCount")
@@ -1759,11 +1662,11 @@ function analyzeAiReferralRows(input) {
1759
1662
  ...evidenceReasons("Source discovery", input.sourceDiscovery),
1760
1663
  ...evidenceReasons("Landing-page detail", input.detail),
1761
1664
  ...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."] : []
1665
+ ...detailMismatch ? ["Google Analytics detail totals differ from source-discovery totals; landing-page and daily breakdowns are incomplete."] : [],
1666
+ ...timeZones.size > 1 ? ["Google Analytics queries returned inconsistent property time zones."] : []
1764
1667
  ];
1765
1668
  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.";
1669
+ 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
1670
  const possiblyTruncated = input.sourceDiscovery.truncated || input.detail.truncated || input.totalUsersEvidence.truncated;
1768
1671
  const allLandingPages = [...pageTotals.entries()].map(([landingPage, metrics]) => {
1769
1672
  const topSourceId = [...metrics.sources.entries()].sort((left, right) => right[1] - left[1] || compareText$16(left[0], right[0]))[0]?.[0];
@@ -1795,13 +1698,13 @@ function analyzeAiReferralRows(input) {
1795
1698
  dataStatus,
1796
1699
  range: input.range,
1797
1700
  methodology: {
1798
- id: "ga4-ai-referrals",
1701
+ id: "google-analytics-ai-referrals",
1799
1702
  version: 2,
1800
1703
  attributionDimension: "sessionSource",
1801
1704
  sourceRulesVersion: AI_REFERRAL_SOURCES_VERSION
1802
1705
  },
1803
1706
  dataSource: {
1804
- provider: "google-analytics-4",
1707
+ provider: "google-analytics",
1805
1708
  api: "analyticsdata.v1beta.runReport",
1806
1709
  maxRows: input.maxRows,
1807
1710
  calls: input.sourceDiscovery.calls + input.detail.calls + input.totalUsersEvidence.calls,
@@ -1818,7 +1721,7 @@ function analyzeAiReferralRows(input) {
1818
1721
  totalUsersStatus: input.totalUsers === null ? "not-reported" : "available",
1819
1722
  sources: sourceTotals.size,
1820
1723
  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.",
1724
+ 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
1725
  caveat: generalCaveat
1823
1726
  },
1824
1727
  sources: [...sourceTotals.values()].map((source) => ({
@@ -1840,7 +1743,7 @@ function analyzeAiReferralRows(input) {
1840
1743
  })).sort((left, right) => compareText$16(left.date, right.date)),
1841
1744
  caveats: [
1842
1745
  generalCaveat,
1843
- "Attribution uses GA4 sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
1746
+ "Attribution uses Google Analytics sessionSource only; landing-page text, medium text, and event referrers are never treated as referral sources.",
1844
1747
  "Source totals come from a low-cardinality source query; landing-page and daily detail is queried separately for the exact observed source values.",
1845
1748
  "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
1749
  ...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 +1845,7 @@ function reportRange(input) {
1942
1845
  const endDate = input.endDate ?? "yesterday";
1943
1846
  const absolute = validIsoDate$1(startDate) && validIsoDate$1(endDate);
1944
1847
  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.");
1848
+ 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
1849
  if (absolute && startDate > endDate) throw new SeoError("INVALID_INPUT", "startDate must be on or before endDate.");
1947
1850
  return {
1948
1851
  startDate,
@@ -2001,7 +1904,7 @@ async function aiReferralsReport(input, dependencies = {}) {
2001
1904
  const property = input.property.trim();
2002
1905
  if (!property) throw new SeoError("INVALID_INPUT", "property is required.");
2003
1906
  const range = reportRange(input);
2004
- const ga4Range = {
1907
+ const googleAnalyticsDateRange = {
2005
1908
  startDate: range.startDate,
2006
1909
  endDate: range.endDate
2007
1910
  };
@@ -2011,7 +1914,7 @@ async function aiReferralsReport(input, dependencies = {}) {
2011
1914
  const sourceResult = await fetchAiReferralRows({
2012
1915
  property,
2013
1916
  request: {
2014
- dateRanges: [ga4Range],
1917
+ dateRanges: [googleAnalyticsDateRange],
2015
1918
  dimensions: [{ name: "sessionSource" }],
2016
1919
  metrics: [{ name: "sessions" }, { name: "eventCount" }],
2017
1920
  orderBys: [{
@@ -2036,7 +1939,7 @@ async function aiReferralsReport(input, dependencies = {}) {
2036
1939
  detailResult = await fetchAiReferralRows({
2037
1940
  property,
2038
1941
  request: {
2039
- dateRanges: [ga4Range],
1942
+ dateRanges: [googleAnalyticsDateRange],
2040
1943
  dimensions: [
2041
1944
  { name: "date" },
2042
1945
  { name: "sessionSource" },
@@ -2072,7 +1975,7 @@ async function aiReferralsReport(input, dependencies = {}) {
2072
1975
  usersResult = await fetchAiReferralRows({
2073
1976
  property,
2074
1977
  request: {
2075
- dateRanges: [ga4Range],
1978
+ dateRanges: [googleAnalyticsDateRange],
2076
1979
  metrics: [{ name: "totalUsers" }],
2077
1980
  dimensionFilter
2078
1981
  },
@@ -10033,7 +9936,7 @@ function crawlDefinitionId(input) {
10033
9936
  const normalized = {
10034
9937
  config: normalizeCrawlConfig(input.config),
10035
9938
  site: input.site ?? null,
10036
- ga4PropertyId: input.ga4PropertyId ?? null
9939
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId ?? null
10037
9940
  };
10038
9941
  return `crawl_def_${createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 20)}`;
10039
9942
  }
@@ -10236,7 +10139,7 @@ function createCrawlReport(input) {
10236
10139
  const definitionId = crawlDefinitionId({
10237
10140
  config,
10238
10141
  site: input.site,
10239
- ga4PropertyId: input.ga4PropertyId
10142
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId
10240
10143
  });
10241
10144
  const safePagesWithLinks = sortPages(sanitizePages(deriveInternalLinkAuthority(input.pages ?? [], input.linkGraph)));
10242
10145
  const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
@@ -10249,7 +10152,7 @@ function createCrawlReport(input) {
10249
10152
  definitionId,
10250
10153
  projectId: input.projectId,
10251
10154
  site: input.site,
10252
- ga4PropertyId: input.ga4PropertyId,
10155
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
10253
10156
  generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
10254
10157
  status: input.status ?? "completed",
10255
10158
  configHash: crawlConfigHash(config),
@@ -10287,7 +10190,7 @@ function normalizeLoadedCrawlReport(report) {
10287
10190
  definitionId: report.definitionId ?? crawlDefinitionId({
10288
10191
  config: report.config,
10289
10192
  site: report.site,
10290
- ga4PropertyId: report.ga4PropertyId
10193
+ googleAnalyticsPropertyId: report.googleAnalyticsPropertyId
10291
10194
  }),
10292
10195
  summary: summarizeCrawlReport({
10293
10196
  pages,
@@ -10700,7 +10603,7 @@ async function joinAnalytics(input) {
10700
10603
  limit: input.limit
10701
10604
  });
10702
10605
  } catch (error) {
10703
- const warning = `GA4 metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
10606
+ const warning = `Google Analytics metrics unavailable: ${error instanceof Error ? error.message : String(error)}`;
10704
10607
  input.warnings.push(warning);
10705
10608
  return {
10706
10609
  status: "unavailable",
@@ -10713,7 +10616,7 @@ async function joinAnalytics(input) {
10713
10616
  };
10714
10617
  }
10715
10618
  if (analytics.warning) {
10716
- const warning = `GA4 metrics unavailable: ${analytics.warning}`;
10619
+ const warning = `Google Analytics metrics unavailable: ${analytics.warning}`;
10717
10620
  input.warnings.push(warning);
10718
10621
  return {
10719
10622
  status: "unavailable",
@@ -10738,7 +10641,7 @@ async function joinAnalytics(input) {
10738
10641
  const sourceQualityKnown = analytics.source?.dataStatus !== void 0;
10739
10642
  const retainedRowLimitReached = analytics.source?.retainedRowLimitReached;
10740
10643
  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;
10644
+ 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
10645
  if (warning) input.warnings.push(warning);
10743
10646
  return {
10744
10647
  status: !sourceQualityKnown || analytics.source?.dataStatus === "partial" ? "partial" : joinedPages ? "joined" : "none",
@@ -10898,7 +10801,7 @@ async function crawlDataSources(input) {
10898
10801
  joinedPages: 0,
10899
10802
  retainedRowLimit: input.analyticsLimit,
10900
10803
  retainedRowLimitReached: false,
10901
- warning: input.cancelled ? "GA4 join skipped because the crawl was cancelled." : "GA4 join skipped because no property was selected."
10804
+ warning: input.cancelled ? "Google Analytics join skipped because the crawl was cancelled." : "Google Analytics join skipped because no property was selected."
10902
10805
  }
10903
10806
  };
10904
10807
  if (!input.cancelled && input.site) dataSources.searchConsole = await joinSearchMetrics({
@@ -10914,8 +10817,8 @@ async function crawlDataSources(input) {
10914
10817
  queryPagesMetricsBatch: input.queryPagesMetricsBatch,
10915
10818
  queryPagesTopQueriesBatch: input.queryPagesTopQueriesBatch
10916
10819
  });
10917
- if (!input.cancelled && input.ga4PropertyId) dataSources.analytics = await joinAnalytics({
10918
- propertyId: input.ga4PropertyId,
10820
+ if (!input.cancelled && input.googleAnalyticsPropertyId) dataSources.analytics = await joinAnalytics({
10821
+ propertyId: input.googleAnalyticsPropertyId,
10919
10822
  pages: input.pages,
10920
10823
  warnings: input.warnings,
10921
10824
  limit: input.analyticsLimit,
@@ -11737,11 +11640,11 @@ function landingPageRankingPolicy(input) {
11737
11640
  };
11738
11641
  const warningSuffix = "Observed landing-page values remain visible but do not affect priority scores.";
11739
11642
  const warnings = [
11740
- ...input.warning ? [`GA4: ${input.warning}`] : [],
11741
- ...input.source?.retainedRowLimitReached ? [`GA4: the retained-row limit was reached. ${warningSuffix}`] : [],
11643
+ ...input.warning ? [`Google Analytics: ${input.warning}`] : [],
11644
+ ...input.source?.retainedRowLimitReached ? [`Google Analytics: the retained-row limit was reached. ${warningSuffix}`] : [],
11742
11645
  ...input.source?.qualityWarnings?.map((warning) => `${warning} ${warningSuffix}`) ?? []
11743
11646
  ];
11744
- if (!input.source?.dataStatus && !input.warning) warnings.push(`GA4: landing-page completeness was not reported. ${warningSuffix}`);
11647
+ if (!input.source?.dataStatus && !input.warning) warnings.push(`Google Analytics: landing-page completeness was not reported. ${warningSuffix}`);
11745
11648
  return {
11746
11649
  canRank: !input.warning && warnings.length === 0 && landingPageValuesCanRank(input.source),
11747
11650
  warnings
@@ -11781,7 +11684,7 @@ async function fetchLandingPageValues(input, dependencies = defaultDependencies$
11781
11684
  const values = landingPageValuesFromRows(rows);
11782
11685
  const availableRows = result.rowCount;
11783
11686
  const retainedRowLimitReached = availableRows !== void 0 && availableRows > rows.length || rows.length >= retainedRowLimit;
11784
- const qualityWarnings = ga4ReportQualityWarnings(result, "GA4 landing-page report");
11687
+ const qualityWarnings = ga4ReportQualityWarnings(result, "Google Analytics landing-page report");
11785
11688
  return {
11786
11689
  values,
11787
11690
  source: {
@@ -12535,7 +12438,7 @@ async function crawlSite(input, dependencies) {
12535
12438
  const dataSources = await crawlDataSources({
12536
12439
  cancelled,
12537
12440
  site,
12538
- ga4PropertyId: input.ga4PropertyId,
12441
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
12539
12442
  pages,
12540
12443
  warnings,
12541
12444
  searchMetricsLimit,
@@ -12590,7 +12493,7 @@ async function crawlSite(input, dependencies) {
12590
12493
  config,
12591
12494
  projectId: input.projectId,
12592
12495
  site,
12593
- ga4PropertyId: input.ga4PropertyId,
12496
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
12594
12497
  dataSources,
12595
12498
  pages,
12596
12499
  requests,
@@ -12679,7 +12582,7 @@ function crawlInput$1(input) {
12679
12582
  url: input.url,
12680
12583
  ...input.searchSite ? { site: input.searchSite } : {},
12681
12584
  projectId: input.projectId,
12682
- ga4PropertyId: input.ga4PropertyId,
12585
+ googleAnalyticsPropertyId: input.googleAnalyticsPropertyId,
12683
12586
  mode: "site",
12684
12587
  maxPages: input.maxPages,
12685
12588
  maxDepth: input.maxDepth,
@@ -13067,8 +12970,8 @@ const CSV_SCHEMAS = {
13067
12970
  "target",
13068
12971
  "template",
13069
12972
  "template_count",
13070
- "ga4_sessions",
13071
- "ga4_total_users",
12973
+ "google_analytics_sessions",
12974
+ "google_analytics_total_users",
13072
12975
  "evidence",
13073
12976
  "action"
13074
12977
  ],
@@ -13097,7 +13000,7 @@ const CSV_SCHEMAS = {
13097
13000
  "title",
13098
13001
  "target",
13099
13002
  "template",
13100
- "ga4_sessions",
13003
+ "google_analytics_sessions",
13101
13004
  "evidence",
13102
13005
  "action"
13103
13006
  ],
@@ -13544,8 +13447,8 @@ function refreshPrioritiesCsvFiles(report) {
13544
13447
  target: item.target,
13545
13448
  template: item.template?.label,
13546
13449
  template_count: item.template?.count,
13547
- ga4_sessions: item.analytics?.sessions,
13548
- ga4_total_users: item.analytics?.totalUsers,
13450
+ google_analytics_sessions: item.analytics?.sessions,
13451
+ google_analytics_total_users: item.analytics?.totalUsers,
13549
13452
  evidence: item.evidence,
13550
13453
  action: item.action
13551
13454
  }))
@@ -13580,7 +13483,7 @@ function refreshPrioritiesCsvFiles(report) {
13580
13483
  title: finding.title,
13581
13484
  target: finding.target,
13582
13485
  template: finding.template?.label,
13583
- ga4_sessions: finding.analytics?.sessions,
13486
+ google_analytics_sessions: finding.analytics?.sessions,
13584
13487
  evidence: finding.evidence,
13585
13488
  action: finding.action
13586
13489
  })))
@@ -13829,7 +13732,7 @@ function searchValueForGroup(report, urls) {
13829
13732
  };
13830
13733
  }
13831
13734
  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}.`}`;
13735
+ 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
13736
  }
13834
13737
  function verificationCommand(report, severity) {
13835
13738
  return `seo crawl ${report.config.url} --severity ${severity} --max-pages ${Math.min(report.config.maxPages, 100)}`;
@@ -14139,7 +14042,7 @@ function snapshotInput(report) {
14139
14042
  generatedAt: report.generatedAt,
14140
14043
  projectId: report.projectId,
14141
14044
  site: report.site,
14142
- ga4PropertyId: report.ga4PropertyId,
14045
+ googleAnalyticsPropertyId: report.googleAnalyticsPropertyId,
14143
14046
  url: report.config.url,
14144
14047
  status: report.status,
14145
14048
  configHash: report.configHash,
@@ -15780,7 +15683,7 @@ const crawlReportBaseSchema = z.object({
15780
15683
  definitionId: z.string(),
15781
15684
  projectId: z.string().optional(),
15782
15685
  site: z.string().optional(),
15783
- ga4PropertyId: z.string().optional(),
15686
+ googleAnalyticsPropertyId: z.string().optional(),
15784
15687
  generatedAt: z.string().datetime(),
15785
15688
  status: z.enum([
15786
15689
  "completed",
@@ -19715,7 +19618,7 @@ function stringMatchType(matchType) {
19715
19618
  if (matchType === "equals") return "EXACT";
19716
19619
  return "CONTAINS";
19717
19620
  }
19718
- function ga4LandingPageFilterForChange(change, group) {
19621
+ function googleAnalyticsLandingPageFilterForChange(change, group) {
19719
19622
  if (change.scope === "site") return void 0;
19720
19623
  if (change.scope === "query") return void 0;
19721
19624
  if (change.scope === "group") {
@@ -19738,7 +19641,7 @@ function ga4LandingPageFilterForChange(change, group) {
19738
19641
  }
19739
19642
  } };
19740
19643
  }
19741
- function summarizeGa4Rows(rows) {
19644
+ function summarizeGoogleAnalyticsRows(rows) {
19742
19645
  return rows.reduce((sum, row) => ({
19743
19646
  sessions: fixed(sum.sessions + (Number(row.sessions) || 0)),
19744
19647
  engagedSessions: fixed(sum.engagedSessions + (Number(row.engagedSessions) || 0)),
@@ -19805,8 +19708,8 @@ function classify(input) {
19805
19708
  }
19806
19709
  //#endregion
19807
19710
  //#region packages/core/src/analyze/experiments/measurement/ga4-change-provider.ts
19808
- async function queryGa4Metrics(input) {
19809
- const result = await input.ga4Report(input.propertyId, {
19711
+ async function queryGoogleAnalyticsMetrics(input) {
19712
+ const result = await input.googleAnalyticsReport(input.propertyId, {
19810
19713
  dateRanges: [input.window],
19811
19714
  dimensions: [{ name: "date" }],
19812
19715
  metrics: [
@@ -19820,7 +19723,7 @@ async function queryGa4Metrics(input) {
19820
19723
  }, { refresh: input.refresh });
19821
19724
  const rows = ga4RowsToObjects(result);
19822
19725
  return {
19823
- metrics: summarizeGa4Rows(rows),
19726
+ metrics: summarizeGoogleAnalyticsRows(rows),
19824
19727
  source: {
19825
19728
  rows: rows.length,
19826
19729
  rowCount: result.rowCount ?? rows.length,
@@ -19830,19 +19733,19 @@ async function queryGa4Metrics(input) {
19830
19733
  warnings: ga4ReportQualityWarnings(result, input.label)
19831
19734
  };
19832
19735
  }
19833
- async function queryGa4ChangeWindows(input) {
19834
- const [before, after] = await Promise.all([queryGa4Metrics({
19736
+ async function queryGoogleAnalyticsChangeWindows(input) {
19737
+ const [before, after] = await Promise.all([queryGoogleAnalyticsMetrics({
19835
19738
  ...input,
19836
19739
  window: input.before,
19837
- label: "GA4 before window"
19838
- }), queryGa4Metrics({
19740
+ label: "Google Analytics before window"
19741
+ }), queryGoogleAnalyticsMetrics({
19839
19742
  ...input,
19840
19743
  window: input.after,
19841
- label: "GA4 after window"
19744
+ label: "Google Analytics after window"
19842
19745
  })]);
19843
19746
  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.");
19747
+ 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.");
19748
+ 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
19749
  return {
19847
19750
  report: {
19848
19751
  propertyId: input.propertyId,
@@ -19858,7 +19761,7 @@ async function queryGa4ChangeWindows(input) {
19858
19761
  before: before.metrics,
19859
19762
  after: after.metrics
19860
19763
  }),
19861
- note: "GA4 attribution is landing-page based. Query-level tests use GSC only."
19764
+ note: "Google Analytics attribution is landing-page based. Query-level tests use GSC only."
19862
19765
  },
19863
19766
  source: {
19864
19767
  before: before.source,
@@ -20096,7 +19999,7 @@ function measurementWindow(input) {
20096
19999
  //#region packages/core/src/analyze/experiments/measure-change.ts
20097
20000
  const defaultDependencies$5 = {
20098
20001
  searchAnalytics: querySearchAnalytics,
20099
- ga4Report: runGa4Report,
20002
+ googleAnalyticsReport: runGa4Report,
20100
20003
  contentGroup: getContentGroup,
20101
20004
  now: () => /* @__PURE__ */ new Date()
20102
20005
  };
@@ -20182,15 +20085,15 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
20182
20085
  confidence: "medium",
20183
20086
  note: `${classified.note} Confidence is capped because Search Console evidence is partial.`
20184
20087
  } : classified;
20185
- const ga4Filter = ga4LandingPageFilterForChange(change, group);
20088
+ const googleAnalyticsFilter = googleAnalyticsLandingPageFilterForChange(change, group);
20186
20089
  const queryDimension = change.scope === "query" || group?.dimension === "query";
20187
- const analytics = input.ga4PropertyId && !queryDimension ? await queryGa4ChangeWindows({
20188
- propertyId: input.ga4PropertyId,
20090
+ const analytics = input.googleAnalyticsPropertyId && !queryDimension ? await queryGoogleAnalyticsChangeWindows({
20091
+ propertyId: input.googleAnalyticsPropertyId,
20189
20092
  before,
20190
20093
  after,
20191
- filter: ga4Filter,
20094
+ filter: googleAnalyticsFilter,
20192
20095
  refresh: input.refresh,
20193
- ga4Report: dependencies.ga4Report
20096
+ googleAnalyticsReport: dependencies.googleAnalyticsReport
20194
20097
  }) : void 0;
20195
20098
  const warnings = [
20196
20099
  ...measured.warnings,
@@ -20269,7 +20172,7 @@ async function measureChange(input, dependencies = defaultDependencies$5) {
20269
20172
  ...window.afterWindowTruncated ? [`The requested ${window.requestedDays}-day after window is incomplete, so this run is partial and has no directional verdict.`] : [],
20270
20173
  ...shortWindow ? ["Fewer than 7 finalized days per window is too short for a directional verdict."] : [],
20271
20174
  ...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.`] : []
20175
+ ...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
20176
  ]
20274
20177
  };
20275
20178
  }
@@ -25940,7 +25843,7 @@ function reportCaveats(input) {
25940
25843
  `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
25844
  `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
25845
  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.",
25846
+ "Google Analytics: not included in this narrative; use refresh-priorities when Google Analytics value should influence prioritisation.",
25944
25847
  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
25848
  ];
25946
25849
  }
@@ -26836,17 +26739,17 @@ async function crawlImplementationQueueWorkflow(input, dependencies) {
26836
26739
  warnings: crawl.warnings
26837
26740
  }),
26838
26741
  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.",
26742
+ tool: "seo_google_analytics_landing_page_values",
26743
+ enabled: Boolean(input.googleAnalyticsPropertyId),
26744
+ warningPrefix: "Google Analytics metrics skipped:",
26745
+ enabledSummary: "Joined Google Analytics landing-page value where URLs matched.",
26746
+ skippedSummary: "No Google Analytics property was selected.",
26844
26747
  warnings: crawl.warnings
26845
26748
  }),
26846
26749
  {
26847
26750
  tool: "seo_top_crawl_fixes",
26848
26751
  status: "completed",
26849
- summary: "Grouped crawl issues, weighted them by severity, affected URLs, GSC visibility, GA4 value, and effort."
26752
+ summary: "Grouped crawl issues, weighted them by severity, affected URLs, GSC visibility, Google Analytics value, and effort."
26850
26753
  }
26851
26754
  ],
26852
26755
  actions: queue.slice(0, 5).map((item) => ({
@@ -27199,12 +27102,12 @@ async function refreshPrioritiesWorkflow(input) {
27199
27102
  });
27200
27103
  const range = defaultDateRange$1(input.days ?? 28);
27201
27104
  const analytics = await fetchLandingPageValues({
27202
- propertyId: input.ga4PropertyId,
27105
+ propertyId: input.googleAnalyticsPropertyId,
27203
27106
  startDate: range.startDate,
27204
27107
  endDate: range.endDate
27205
27108
  });
27206
27109
  const analyticsPolicy = landingPageRankingPolicy({
27207
- propertyId: input.ga4PropertyId,
27110
+ propertyId: input.googleAnalyticsPropertyId,
27208
27111
  source: analytics.source,
27209
27112
  warning: analytics.warning
27210
27113
  });
@@ -27761,6 +27664,9 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
27761
27664
  }
27762
27665
  //#endregion
27763
27666
  //#region packages/core/src/clients.ts
27667
+ function googleAnalyticsPropertyId(client) {
27668
+ return client?.analytics.google?.propertyId;
27669
+ }
27764
27670
  function slug(value) {
27765
27671
  return value.toLowerCase().replace(/^https?:\/\//, "").replace(/^sc-domain:/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
27766
27672
  }
@@ -27797,7 +27703,7 @@ function saveClient(input) {
27797
27703
  name: input.name ?? existing?.name,
27798
27704
  siteUrl: input.siteUrl
27799
27705
  })),
27800
- ga4PropertyId: input.ga4PropertyId ?? existing?.ga4PropertyId,
27706
+ analytics: input.analytics ?? existing?.analytics ?? {},
27801
27707
  reportDay: input.reportDay ?? existing?.reportDay,
27802
27708
  technicalWeekday: input.technicalWeekday ?? existing?.technicalWeekday,
27803
27709
  isDefault: input.isDefault ?? existing?.isDefault,
@@ -27879,7 +27785,7 @@ async function runDoctor() {
27879
27785
  const grantedScopes = new Set(tokens?.scope.split(/\s+/).filter(Boolean) ?? []);
27880
27786
  for (const scope of REQUIRED_SCOPES) checks.push({
27881
27787
  id: `scope:${scope}`,
27882
- label: scope.endsWith("webmasters.readonly") ? "GSC scope" : "GA4 scope",
27788
+ label: scope.endsWith("webmasters.readonly") ? "Search Console scope" : "Google Analytics scope",
27883
27789
  status: serviceAccount.configured || tokens && grantedScopes.has(scope) ? "pass" : tokens ? "fail" : "warn",
27884
27790
  detail: serviceAccount.configured ? `${scope} requested by the service account.` : tokens ? grantedScopes.has(scope) ? scope : `Missing ${scope}.` : "Cannot inspect scopes until login is complete.",
27885
27791
  fix: !serviceAccount.configured && tokens && !grantedScopes.has(scope) ? "Run `seo auth logout` then `seo auth login`." : void 0
@@ -27891,10 +27797,10 @@ async function runDoctor() {
27891
27797
  detail: config.defaultSite ?? "No saved default. Human CLI commands will prompt; agents can pass --site."
27892
27798
  });
27893
27799
  checks.push({
27894
- id: "default-ga4",
27895
- label: "Saved GA4 property",
27800
+ id: "default-google-analytics",
27801
+ label: "Saved Google Analytics property",
27896
27802
  status: "pass",
27897
- detail: config.google.defaultGa4PropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
27803
+ detail: config.analytics.google.defaultPropertyId ?? "No saved default. Human CLI commands will prompt; agents can pass --property."
27898
27804
  });
27899
27805
  return {
27900
27806
  ok: checks.every((check) => check.status !== "fail"),
@@ -28266,7 +28172,7 @@ function matchGa4WebStreams(site, candidates) {
28266
28172
  function ga4MatchReason(match, site) {
28267
28173
  const stream = match.stream.displayName ?? match.defaultUri;
28268
28174
  const location = match.match === "domain" ? "is inside" : "matches";
28269
- return `Matched GA4 web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
28175
+ return `Matched Google Analytics web stream ${stream} (${match.defaultUri}): its hostname ${location} ${site}.`;
28270
28176
  }
28271
28177
  //#endregion
28272
28178
  //#region packages/core/src/logger.ts
@@ -28611,6 +28517,6 @@ function isoDate(daysAgo = 0) {
28611
28517
  //#region packages/core/src/version.ts
28612
28518
  const SEO_VERSION = version;
28613
28519
  //#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 };
28520
+ 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, analyticsConnectionsSchema, 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, googleAnalyticsPropertyId, 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 };
28615
28521
 
28616
28522
  //# sourceMappingURL=index.js.map