@agentsean/google 1.0.0

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.
Files changed (112) hide show
  1. package/LICENSE +661 -0
  2. package/dist/audit-extras.d.ts +21 -0
  3. package/dist/audit-extras.d.ts.map +1 -0
  4. package/dist/audit-extras.js +62 -0
  5. package/dist/audit-extras.js.map +1 -0
  6. package/dist/broker.d.ts +47 -0
  7. package/dist/broker.d.ts.map +1 -0
  8. package/dist/broker.js +146 -0
  9. package/dist/broker.js.map +1 -0
  10. package/dist/causes.d.ts +157 -0
  11. package/dist/causes.d.ts.map +1 -0
  12. package/dist/causes.js +175 -0
  13. package/dist/causes.js.map +1 -0
  14. package/dist/connect-flow.d.ts +63 -0
  15. package/dist/connect-flow.d.ts.map +1 -0
  16. package/dist/connect-flow.js +150 -0
  17. package/dist/connect-flow.js.map +1 -0
  18. package/dist/crux.d.ts +37 -0
  19. package/dist/crux.d.ts.map +1 -0
  20. package/dist/crux.js +112 -0
  21. package/dist/crux.js.map +1 -0
  22. package/dist/data/changepoints.json +174 -0
  23. package/dist/data/quotas.json +17 -0
  24. package/dist/errors.d.ts +46 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +110 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/ga4.d.ts +46 -0
  29. package/dist/ga4.d.ts.map +1 -0
  30. package/dist/ga4.js +126 -0
  31. package/dist/ga4.js.map +1 -0
  32. package/dist/gsc.d.ts +98 -0
  33. package/dist/gsc.d.ts.map +1 -0
  34. package/dist/gsc.js +141 -0
  35. package/dist/gsc.js.map +1 -0
  36. package/dist/http.d.ts +14 -0
  37. package/dist/http.d.ts.map +1 -0
  38. package/dist/http.js +47 -0
  39. package/dist/http.js.map +1 -0
  40. package/dist/incidents.d.ts +51 -0
  41. package/dist/incidents.d.ts.map +1 -0
  42. package/dist/incidents.js +221 -0
  43. package/dist/incidents.js.map +1 -0
  44. package/dist/index.d.ts +27 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +27 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/integrity.d.ts +32 -0
  49. package/dist/integrity.d.ts.map +1 -0
  50. package/dist/integrity.js +52 -0
  51. package/dist/integrity.js.map +1 -0
  52. package/dist/oauth-broker.d.ts +38 -0
  53. package/dist/oauth-broker.d.ts.map +1 -0
  54. package/dist/oauth-broker.js +81 -0
  55. package/dist/oauth-broker.js.map +1 -0
  56. package/dist/oauth-config.d.ts +16 -0
  57. package/dist/oauth-config.d.ts.map +1 -0
  58. package/dist/oauth-config.js +24 -0
  59. package/dist/oauth-config.js.map +1 -0
  60. package/dist/oauth-desktop.d.ts +29 -0
  61. package/dist/oauth-desktop.d.ts.map +1 -0
  62. package/dist/oauth-desktop.js +76 -0
  63. package/dist/oauth-desktop.js.map +1 -0
  64. package/dist/pending.d.ts +18 -0
  65. package/dist/pending.d.ts.map +1 -0
  66. package/dist/pending.js +29 -0
  67. package/dist/pending.js.map +1 -0
  68. package/dist/persist.d.ts +48 -0
  69. package/dist/persist.d.ts.map +1 -0
  70. package/dist/persist.js +298 -0
  71. package/dist/persist.js.map +1 -0
  72. package/dist/pkce.d.ts +9 -0
  73. package/dist/pkce.d.ts.map +1 -0
  74. package/dist/pkce.js +13 -0
  75. package/dist/pkce.js.map +1 -0
  76. package/dist/psi.d.ts +19 -0
  77. package/dist/psi.d.ts.map +1 -0
  78. package/dist/psi.js +47 -0
  79. package/dist/psi.js.map +1 -0
  80. package/dist/quota.d.ts +22 -0
  81. package/dist/quota.d.ts.map +1 -0
  82. package/dist/quota.js +136 -0
  83. package/dist/quota.js.map +1 -0
  84. package/dist/reconcile.d.ts +19 -0
  85. package/dist/reconcile.d.ts.map +1 -0
  86. package/dist/reconcile.js +89 -0
  87. package/dist/reconcile.js.map +1 -0
  88. package/dist/scopes.d.ts +25 -0
  89. package/dist/scopes.d.ts.map +1 -0
  90. package/dist/scopes.js +49 -0
  91. package/dist/scopes.js.map +1 -0
  92. package/dist/seal.d.ts +4 -0
  93. package/dist/seal.d.ts.map +1 -0
  94. package/dist/seal.js +27 -0
  95. package/dist/seal.js.map +1 -0
  96. package/dist/sync.d.ts +29 -0
  97. package/dist/sync.d.ts.map +1 -0
  98. package/dist/sync.js +266 -0
  99. package/dist/sync.js.map +1 -0
  100. package/dist/tokens.d.ts +57 -0
  101. package/dist/tokens.d.ts.map +1 -0
  102. package/dist/tokens.js +122 -0
  103. package/dist/tokens.js.map +1 -0
  104. package/dist/verification.d.ts +17 -0
  105. package/dist/verification.d.ts.map +1 -0
  106. package/dist/verification.js +51 -0
  107. package/dist/verification.js.map +1 -0
  108. package/dist/waterfall.d.ts +39 -0
  109. package/dist/waterfall.d.ts.map +1 -0
  110. package/dist/waterfall.js +167 -0
  111. package/dist/waterfall.js.map +1 -0
  112. package/package.json +52 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-extras.d.ts","sourceRoot":"","sources":["../src/audit-extras.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAQpD,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF,wBAAgB,eAAe,CAC7B,EAAE,EAAE,cAAc,EAClB,MAAM,EAAE,MAAM,GACb;IAAE,GAAG,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CA2DtE"}
@@ -0,0 +1,62 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { cruxRecords, gscConnections, gscPageDaily, gscUrlInspections, } from "@agentsean/db";
3
+ export function loadAuditExtras(db, siteId) {
4
+ const connected = db
5
+ .select()
6
+ .from(gscConnections)
7
+ .where(eq(gscConnections.siteId, siteId))
8
+ .get();
9
+ const clicksByUrl = {};
10
+ const impressionsByUrl = {};
11
+ if (connected) {
12
+ for (const row of db
13
+ .select()
14
+ .from(gscPageDaily)
15
+ .where(eq(gscPageDaily.siteId, siteId))
16
+ .all()) {
17
+ clicksByUrl[row.page] = (clicksByUrl[row.page] ?? 0) + row.clicks;
18
+ impressionsByUrl[row.page] = (impressionsByUrl[row.page] ?? 0) + row.impressions;
19
+ }
20
+ }
21
+ const googleCanonicalByUrl = {};
22
+ const coverageStateByUrl = {};
23
+ for (const row of db
24
+ .select()
25
+ .from(gscUrlInspections)
26
+ .where(eq(gscUrlInspections.siteId, siteId))
27
+ .all()) {
28
+ if (row.googleCanonical)
29
+ googleCanonicalByUrl[row.url] = row.googleCanonical;
30
+ if (row.coverageState)
31
+ coverageStateByUrl[row.url] = row.coverageState;
32
+ }
33
+ const cruxRows = db
34
+ .select()
35
+ .from(cruxRecords)
36
+ .where(eq(cruxRecords.siteId, siteId))
37
+ .all();
38
+ const phone = cruxRows.find((r) => r.formFactor === "PHONE");
39
+ const desktop = cruxRows.find((r) => r.formFactor === "DESKTOP");
40
+ const any = phone ?? desktop ?? cruxRows[0];
41
+ const gsc = connected || Object.keys(clicksByUrl).length > 0
42
+ ? {
43
+ clicksByUrl,
44
+ impressionsByUrl,
45
+ googleCanonicalByUrl,
46
+ coverageStateByUrl,
47
+ }
48
+ : undefined;
49
+ const crux = any
50
+ ? {
51
+ lcpP75Ms: any.lcpP75 ?? undefined,
52
+ inpP75Ms: any.inpP75 ?? undefined,
53
+ clsP75: any.clsP75 ?? undefined,
54
+ ttfbP75Ms: any.ttfbP75 ?? undefined,
55
+ phoneLcpP75Ms: phone?.lcpP75 ?? undefined,
56
+ desktopLcpP75Ms: desktop?.lcpP75 ?? undefined,
57
+ urlLevel: any.identifierKind === "url",
58
+ }
59
+ : undefined;
60
+ return { gsc, crux };
61
+ }
62
+ //# sourceMappingURL=audit-extras.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-extras.js","sourceRoot":"","sources":["../src/audit-extras.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * First-party stateless OAuth broker (D9).
3
+ *
4
+ * Holds GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in process env on
5
+ * agentsean.com — never in this repository (Google APIs ToS §4(b)).
6
+ *
7
+ * Flow: local connect page POSTs wrap_key + loopback redirect_uri here.
8
+ * Broker runs Google OAuth as a confidential Web client, encrypts tokens
9
+ * with wrap_key, 302s to http://127.0.0.1:<port>/oauth/callback?payload=...
10
+ * The hosted origin never fetches the daemon (Chrome 142 Local Network Access).
11
+ */
12
+ export type BrokerSecrets = {
13
+ clientId: string;
14
+ clientSecret: string;
15
+ /** HMAC key for the broker's own Google `state` blob. */
16
+ stateKey: Buffer;
17
+ };
18
+ export declare function brokerStartUrl(opts: {
19
+ secrets: BrokerSecrets;
20
+ redirectUri: string;
21
+ wrapKeyB64: string;
22
+ localState: string;
23
+ scopes?: readonly string[] | undefined;
24
+ }): string;
25
+ export declare function setBrokerRegisteredCallback(url: string): void;
26
+ export declare function brokerCallbackPlaceholder(): string;
27
+ export declare function brokerHandleGoogleCallback(opts: {
28
+ secrets: BrokerSecrets;
29
+ code: string;
30
+ state: string;
31
+ fetch: typeof fetch;
32
+ }): Promise<{
33
+ location: string;
34
+ }>;
35
+ export declare function brokerHandleRefresh(opts: {
36
+ secrets: BrokerSecrets;
37
+ refreshToken: string;
38
+ wrapKeyB64: string;
39
+ fetch: typeof fetch;
40
+ }): Promise<{
41
+ payload: string;
42
+ }>;
43
+ export declare function loadBrokerSecrets(env?: NodeJS.ProcessEnv): BrokerSecrets | null;
44
+ /** Used only by tests that round-trip a sealed payload without HTTP. */
45
+ export declare function sealWithWrapKey(wrapKeyB64: string, value: unknown): string;
46
+ export declare function peekUnseal(wrapKeyB64: string, payload: string): unknown;
47
+ //# sourceMappingURL=broker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAuCF,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACxC,GAAG,MAAM,CA+BT;AAKD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE7D;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,KAAK,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA4BhC;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,KAAK,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkB/B;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,aAAa,GAAG,IAAI,CAUtB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAE1E;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvE"}
package/dist/broker.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * First-party stateless OAuth broker (D9).
3
+ *
4
+ * Holds GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in process env on
5
+ * agentsean.com — never in this repository (Google APIs ToS §4(b)).
6
+ *
7
+ * Flow: local connect page POSTs wrap_key + loopback redirect_uri here.
8
+ * Broker runs Google OAuth as a confidential Web client, encrypts tokens
9
+ * with wrap_key, 302s to http://127.0.0.1:<port>/oauth/callback?payload=...
10
+ * The hosted origin never fetches the daemon (Chrome 142 Local Network Access).
11
+ */
12
+ import { createHmac } from "node:crypto";
13
+ import { CONNECT_SCOPES, GOOGLE_AUTH_URL, GOOGLE_TOKEN_URL } from "./scopes.js";
14
+ import { generatePkce } from "./pkce.js";
15
+ import { seal, unseal } from "./seal.js";
16
+ function b64urlJson(value) {
17
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
18
+ }
19
+ function fromB64urlJson(raw) {
20
+ return JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
21
+ }
22
+ function signState(stateKey, payload) {
23
+ return createHmac("sha256", stateKey).update(payload).digest("base64url");
24
+ }
25
+ function encodeBrokerState(secrets, inner) {
26
+ const payload = b64urlJson(inner);
27
+ return `${payload}.${signState(secrets.stateKey, payload)}`;
28
+ }
29
+ function decodeBrokerState(secrets, state) {
30
+ const dot = state.lastIndexOf(".");
31
+ if (dot <= 0)
32
+ throw new Error("invalid broker state");
33
+ const payload = state.slice(0, dot);
34
+ const sig = state.slice(dot + 1);
35
+ if (sig !== signState(secrets.stateKey, payload))
36
+ throw new Error("bad broker state signature");
37
+ const inner = fromB64urlJson(payload);
38
+ if (inner.exp < Date.now())
39
+ throw new Error("broker state expired");
40
+ return inner;
41
+ }
42
+ export function brokerStartUrl(opts) {
43
+ const hostedOrigin = process.env["SEAN_PUBLIC_ORIGIN"]?.replace(/\/$/, "");
44
+ const hosted = process.env["SEAN_HOSTED"] === "1" &&
45
+ hostedOrigin &&
46
+ opts.redirectUri.startsWith(hostedOrigin);
47
+ if (!opts.redirectUri.startsWith("http://127.0.0.1:") && !hosted) {
48
+ throw new Error("Broker will only hand tokens to http://127.0.0.1 loopback.");
49
+ }
50
+ const pkce = generatePkce();
51
+ const inner = {
52
+ redirectUri: opts.redirectUri,
53
+ wrapKeyB64: opts.wrapKeyB64,
54
+ localState: opts.localState,
55
+ verifier: pkce.verifier,
56
+ exp: Date.now() + 10 * 60_000,
57
+ };
58
+ const state = encodeBrokerState(opts.secrets, inner);
59
+ const params = new URLSearchParams({
60
+ client_id: opts.secrets.clientId,
61
+ redirect_uri: brokerCallbackPlaceholder(),
62
+ response_type: "code",
63
+ scope: (opts.scopes ?? CONNECT_SCOPES).join(" "),
64
+ state,
65
+ code_challenge: pkce.challenge,
66
+ code_challenge_method: "S256",
67
+ access_type: "offline",
68
+ prompt: "consent",
69
+ include_granted_scopes: "true",
70
+ });
71
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
72
+ }
73
+ /** Web client redirect registered in Cloud Console, e.g. https://oauth.agentsean.com/google/callback */
74
+ let registeredCallback = "https://oauth.agentsean.com/google/callback";
75
+ export function setBrokerRegisteredCallback(url) {
76
+ registeredCallback = url;
77
+ }
78
+ export function brokerCallbackPlaceholder() {
79
+ return registeredCallback;
80
+ }
81
+ export async function brokerHandleGoogleCallback(opts) {
82
+ const inner = decodeBrokerState(opts.secrets, opts.state);
83
+ const body = new URLSearchParams({
84
+ client_id: opts.secrets.clientId,
85
+ client_secret: opts.secrets.clientSecret,
86
+ code: opts.code,
87
+ code_verifier: inner.verifier,
88
+ grant_type: "authorization_code",
89
+ redirect_uri: registeredCallback,
90
+ });
91
+ const response = await opts.fetch(GOOGLE_TOKEN_URL, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
94
+ body,
95
+ });
96
+ if (!response.ok) {
97
+ const text = await response.text().catch(() => "");
98
+ throw new Error(`token exchange failed (${response.status}): ${text.slice(0, 200)}`);
99
+ }
100
+ const json = (await response.json());
101
+ const wrapKey = Buffer.from(inner.wrapKeyB64, "base64url");
102
+ const payload = seal(wrapKey, JSON.stringify(json));
103
+ const loc = new URL(inner.redirectUri);
104
+ loc.searchParams.set("state", inner.localState);
105
+ loc.searchParams.set("payload", payload);
106
+ return { location: loc.toString() };
107
+ }
108
+ export async function brokerHandleRefresh(opts) {
109
+ const body = new URLSearchParams({
110
+ client_id: opts.secrets.clientId,
111
+ client_secret: opts.secrets.clientSecret,
112
+ refresh_token: opts.refreshToken,
113
+ grant_type: "refresh_token",
114
+ });
115
+ const response = await opts.fetch(GOOGLE_TOKEN_URL, {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
118
+ body,
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error(`broker refresh failed (${response.status})`);
122
+ }
123
+ const json = await response.json();
124
+ const wrapKey = Buffer.from(opts.wrapKeyB64, "base64url");
125
+ return { payload: seal(wrapKey, JSON.stringify(json)) };
126
+ }
127
+ export function loadBrokerSecrets(env = process.env) {
128
+ const clientId = env["GOOGLE_CLIENT_ID"]?.trim();
129
+ const clientSecret = env["GOOGLE_CLIENT_SECRET"]?.trim();
130
+ const stateSecret = env["SEAN_BROKER_STATE_KEY"]?.trim();
131
+ if (!clientId || !clientSecret || !stateSecret)
132
+ return null;
133
+ return {
134
+ clientId,
135
+ clientSecret,
136
+ stateKey: Buffer.from(stateSecret.padEnd(32, "0")).subarray(0, 32),
137
+ };
138
+ }
139
+ /** Used only by tests that round-trip a sealed payload without HTTP. */
140
+ export function sealWithWrapKey(wrapKeyB64, value) {
141
+ return seal(Buffer.from(wrapKeyB64, "base64url"), JSON.stringify(value));
142
+ }
143
+ export function peekUnseal(wrapKeyB64, payload) {
144
+ return JSON.parse(unseal(Buffer.from(wrapKeyB64, "base64url"), payload));
145
+ }
146
+ //# sourceMappingURL=broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.js","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAiBzC,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,cAAc,CAAI,GAAW;IACpC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAM,CAAC;AACzE,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,OAAe;IAClD,OAAO,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAsB,EAAE,KAAkB;IACnE,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,GAAG,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAsB,EAAE,KAAa;IAC9D,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,IAAI,GAAG,KAAK,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,cAAc,CAAc,OAAO,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAM9B;IACC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3E,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,GAAG;QAClC,YAAY;QACZ,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAgB;QACzB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM;KAC9B,CAAC;IACF,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;QAChC,YAAY,EAAE,yBAAyB,EAAE;QACzC,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAChD,KAAK;QACL,cAAc,EAAE,IAAI,CAAC,SAAS;QAC9B,qBAAqB,EAAE,MAAM;QAC7B,WAAW,EAAE,SAAS;QACtB,MAAM,EAAE,SAAS;QACjB,sBAAsB,EAAE,MAAM;KAC/B,CAAC,CAAC;IACH,OAAO,GAAG,eAAe,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AACnD,CAAC;AAED,wGAAwG;AACxG,IAAI,kBAAkB,GAAG,6CAA6C,CAAC;AAEvE,MAAM,UAAU,2BAA2B,CAAC,GAAW;IACrD,kBAAkB,GAAG,GAAG,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,IAKhD;IACC,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;QAChC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY;QACxC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,aAAa,EAAE,KAAK,CAAC,QAAQ;QAC7B,UAAU,EAAE,oBAAoB;QAChC,YAAY,EAAE,kBAAkB;KACjC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,EAAE;QAClD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI;KACL,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,0BAA0B,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CACpE,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA4B,CAAC;IAChE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACvC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAChD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAKzC;IACC,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;QAChC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY;QACxC,aAAa,EAAE,IAAI,CAAC,YAAY;QAChC,UAAU,EAAE,eAAe;KAC5B,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,EAAE;QAClD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI;KACL,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAC1D,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,QAAQ,GAAG,GAAG,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,CAAC;IACjD,MAAM,YAAY,GAAG,GAAG,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,CAAC;IACzD,MAAM,WAAW,GAAG,GAAG,CAAC,uBAAuB,CAAC,EAAE,IAAI,EAAE,CAAC;IACzD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC5D,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC;KACnE,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,KAAc;IAChE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,OAAe;IAC5D,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * GA4 ↔ GSC discrepancy causes. PLAN Phase 7: 17 named codes, waterfall
3
+ * closes to an explicit residual. Magnitudes from research/gap-08.
4
+ */
5
+ export declare const GSC_ANONYMIZED_SHARE = 0.468;
6
+ export declare const EU_GA4_INVISIBLE_LOW = 0.4;
7
+ export declare const EU_GA4_INVISIBLE_HIGH = 0.65;
8
+ export type CauseGroup = "definitional" | "gsc" | "ga4_collection" | "ga4_config" | "ga4_reporting";
9
+ export type DiscrepancyCause = {
10
+ code: string;
11
+ group: CauseGroup;
12
+ name: string;
13
+ direction: "gsc_gt_ga4" | "ga4_gt_gsc" | "either" | "n/a";
14
+ typicalMagnitude: string;
15
+ alwaysPresent: boolean;
16
+ notes: string;
17
+ };
18
+ export declare const DISCREPANCY_CAUSES: readonly [{
19
+ readonly code: "DEF_CLICK_VS_SESSION";
20
+ readonly group: "definitional";
21
+ readonly name: "Click vs session";
22
+ readonly direction: "gsc_gt_ga4";
23
+ readonly typicalMagnitude: "2–8% (pogo-sticking)";
24
+ readonly alwaysPresent: true;
25
+ readonly notes: "Two SERP clicks inside a 30-minute window are 2 GSC clicks and 1 GA4 session.";
26
+ }, {
27
+ readonly code: "DEF_TIMEZONE";
28
+ readonly group: "definitional";
29
+ readonly name: "Timezone bucket";
30
+ readonly direction: "either";
31
+ readonly typicalMagnitude: "up to ~37% of one day (EU vs Pacific); ~0 over 28 days";
32
+ readonly alwaysPresent: true;
33
+ readonly notes: "GSC days are US/Pacific. GA4 days are the property timezone. Never compare single days.";
34
+ }, {
35
+ readonly code: "DEF_CANONICAL_VS_LANDING";
36
+ readonly group: "definitional";
37
+ readonly name: "Canonical vs landing URL";
38
+ readonly direction: "either";
39
+ readonly typicalMagnitude: "20–60% at page level on parameterised sites; ~0 at property level";
40
+ readonly alwaysPresent: true;
41
+ readonly notes: "GSC assigns to Google-selected canonical. GA4 records the URL that loaded.";
42
+ }, {
43
+ readonly code: "DEF_AGGREGATION";
44
+ readonly group: "definitional";
45
+ readonly name: "Chart vs table aggregation";
46
+ readonly direction: "either";
47
+ readonly typicalMagnitude: "property totals vs page-table totals";
48
+ readonly alwaysPresent: true;
49
+ readonly notes: "GSC chart totals aggregate by property; the table aggregates by page.";
50
+ }, {
51
+ readonly code: "DEF_REDIRECT_CHAIN";
52
+ readonly group: "definitional";
53
+ readonly name: "Redirect chain";
54
+ readonly direction: "gsc_gt_ga4";
55
+ readonly typicalMagnitude: "100% of affected URLs; property-level = abandon rate";
56
+ readonly alwaysPresent: false;
57
+ readonly notes: "GSC click lands on A, 301 to B, GA4 fires on B. Slow hops abandon mid-chain.";
58
+ }, {
59
+ readonly code: "GSC_ANONYMIZED_QUERY";
60
+ readonly group: "gsc";
61
+ readonly name: "Anonymized queries";
62
+ readonly direction: "n/a";
63
+ readonly typicalMagnitude: "46.8% of GSC clicks have no query (Ahrefs, 22B clicks / 887,534 properties)";
64
+ readonly alwaysPresent: true;
65
+ readonly notes: "Query-dimensioned totals omit rare queries. Page-level clicks stay complete. Never compute site traffic from a query pull.";
66
+ }, {
67
+ readonly code: "GSC_ROW_LIMIT";
68
+ readonly group: "gsc";
69
+ readonly name: "API row limit";
70
+ readonly direction: "n/a";
71
+ readonly typicalMagnitude: "API does not guarantee completeness past top rows";
72
+ readonly alwaysPresent: true;
73
+ readonly notes: "searchanalytics.query rowLimit 1–25,000; paging to exhaustion is still a sample. Only BigQuery bulk export is complete.";
74
+ }, {
75
+ readonly code: "GSC_16_MONTH_WINDOW";
76
+ readonly group: "gsc";
77
+ readonly name: "16-month retention";
78
+ readonly direction: "n/a";
79
+ readonly typicalMagnitude: "exactly one rolling YoY pair";
80
+ readonly alwaysPresent: true;
81
+ readonly notes: "Archive locally from day one. The API window cannot be backfilled.";
82
+ }, {
83
+ readonly code: "GSC_DISCOVER_NEWS_SPLIT";
84
+ readonly group: "gsc";
85
+ readonly name: "Discover / News split";
86
+ readonly direction: "ga4_gt_gsc";
87
+ readonly typicalMagnitude: "publishers 30–70% of 'organic'; B2B SaaS ~0";
88
+ readonly alwaysPresent: false;
89
+ readonly notes: "Discover and Google News are not in type=web. GA4 still counts them as google / organic.";
90
+ }, {
91
+ readonly code: "GSC_GENAI_SURFACES";
92
+ readonly group: "gsc";
93
+ readonly name: "Generative AI surfaces";
94
+ readonly direction: "either";
95
+ readonly typicalMagnitude: "impressions definitional change from mid-2026";
96
+ readonly alwaysPresent: false;
97
+ readonly notes: "AI Overviews / AI Mode sit in overall totals. Dedicated report is impressions-only and not in the Search Analytics API.";
98
+ }, {
99
+ readonly code: "GA4_CONSENT_DENIED";
100
+ readonly group: "ga4_collection";
101
+ readonly name: "Consent Mode denied";
102
+ readonly direction: "gsc_gt_ga4";
103
+ readonly typicalMagnitude: "EU/UK compliant reject-all: 40–65% of organic permanently invisible in GA4";
104
+ readonly alwaysPresent: false;
105
+ readonly notes: "A compliant EU property has 40–65% of organic traffic missing from GA4. GSC is unaffected.";
106
+ }, {
107
+ readonly code: "GA4_ADBLOCK";
108
+ readonly group: "ga4_collection";
109
+ readonly name: "Ad blockers";
110
+ readonly direction: "gsc_gt_ga4";
111
+ readonly typicalMagnitude: "vertical-dependent; often 10–30%";
112
+ readonly alwaysPresent: false;
113
+ readonly notes: "Requests to google-analytics.com / googletagmanager.com never fire.";
114
+ }, {
115
+ readonly code: "GA4_JS_FAIL";
116
+ readonly group: "ga4_collection";
117
+ readonly name: "Tag never fired";
118
+ readonly direction: "gsc_gt_ga4";
119
+ readonly typicalMagnitude: "template / CSP / bounce-before-tag";
120
+ readonly alwaysPresent: false;
121
+ readonly notes: "JS error before gtag(), missing tag, CSP, or bounce before the library loads.";
122
+ }, {
123
+ readonly code: "GA4_CHANNEL_MISCLASS";
124
+ readonly group: "ga4_config";
125
+ readonly name: "Default channel group";
126
+ readonly direction: "gsc_gt_ga4";
127
+ readonly typicalMagnitude: "campaigns named *shop* silently leave Organic Search";
128
+ readonly alwaysPresent: false;
129
+ readonly notes: "Reconcile against sessionSourceMedium == 'google / organic', never the channel group.";
130
+ }, {
131
+ readonly code: "GA4_ATTRIBUTION_MODEL";
132
+ readonly group: "ga4_config";
133
+ readonly name: "Data-driven attribution";
134
+ readonly direction: "either";
135
+ readonly typicalMagnitude: "key-event counts will not equal organic-started sessions";
136
+ readonly alwaysPresent: true;
137
+ readonly notes: "Sessions are not attribution-modelled. Key events are. Mixing them in one table is a bug.";
138
+ }, {
139
+ readonly code: "GA4_SAMPLING";
140
+ readonly group: "ga4_reporting";
141
+ readonly name: "Sampled API response";
142
+ readonly direction: "n/a";
143
+ readonly typicalMagnitude: "standard properties sample above 10M events / query";
144
+ readonly alwaysPresent: false;
145
+ readonly notes: "Refuse to report a sampled number without a warning. Read ResponseMetaData.samplingMetadatas.";
146
+ }, {
147
+ readonly code: "GA4_THRESHOLDING";
148
+ readonly group: "ga4_reporting";
149
+ readonly name: "Thresholded rows";
150
+ readonly direction: "n/a";
151
+ readonly typicalMagnitude: "undisclosed; triggered by Google Signals";
152
+ readonly alwaysPresent: false;
153
+ readonly notes: "Refuse thresholded numbers. subjectToThresholding on the Data API is a hard gate.";
154
+ }];
155
+ export type DiscrepancyCauseCode = (typeof DISCREPANCY_CAUSES)[number]["code"];
156
+ export declare function isEuTimeZone(timeZone: string | null | undefined): boolean;
157
+ //# sourceMappingURL=causes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"causes.d.ts","sourceRoot":"","sources":["../src/causes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAC1C,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,MAAM,MAAM,UAAU,GACpB,cAAc,GAAG,KAAK,GAAG,gBAAgB,GAAG,YAAY,GAAG,eAAe,CAAC;AAE7E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,YAAY,GAAG,YAAY,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC1D,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0KiB,CAAC;AAEjD,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAYzE"}
package/dist/causes.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * GA4 ↔ GSC discrepancy causes. PLAN Phase 7: 17 named codes, waterfall
3
+ * closes to an explicit residual. Magnitudes from research/gap-08.
4
+ */
5
+ export const GSC_ANONYMIZED_SHARE = 0.468;
6
+ export const EU_GA4_INVISIBLE_LOW = 0.4;
7
+ export const EU_GA4_INVISIBLE_HIGH = 0.65;
8
+ export const DISCREPANCY_CAUSES = [
9
+ {
10
+ code: "DEF_CLICK_VS_SESSION",
11
+ group: "definitional",
12
+ name: "Click vs session",
13
+ direction: "gsc_gt_ga4",
14
+ typicalMagnitude: "2–8% (pogo-sticking)",
15
+ alwaysPresent: true,
16
+ notes: "Two SERP clicks inside a 30-minute window are 2 GSC clicks and 1 GA4 session.",
17
+ },
18
+ {
19
+ code: "DEF_TIMEZONE",
20
+ group: "definitional",
21
+ name: "Timezone bucket",
22
+ direction: "either",
23
+ typicalMagnitude: "up to ~37% of one day (EU vs Pacific); ~0 over 28 days",
24
+ alwaysPresent: true,
25
+ notes: "GSC days are US/Pacific. GA4 days are the property timezone. Never compare single days.",
26
+ },
27
+ {
28
+ code: "DEF_CANONICAL_VS_LANDING",
29
+ group: "definitional",
30
+ name: "Canonical vs landing URL",
31
+ direction: "either",
32
+ typicalMagnitude: "20–60% at page level on parameterised sites; ~0 at property level",
33
+ alwaysPresent: true,
34
+ notes: "GSC assigns to Google-selected canonical. GA4 records the URL that loaded.",
35
+ },
36
+ {
37
+ code: "DEF_AGGREGATION",
38
+ group: "definitional",
39
+ name: "Chart vs table aggregation",
40
+ direction: "either",
41
+ typicalMagnitude: "property totals vs page-table totals",
42
+ alwaysPresent: true,
43
+ notes: "GSC chart totals aggregate by property; the table aggregates by page.",
44
+ },
45
+ {
46
+ code: "DEF_REDIRECT_CHAIN",
47
+ group: "definitional",
48
+ name: "Redirect chain",
49
+ direction: "gsc_gt_ga4",
50
+ typicalMagnitude: "100% of affected URLs; property-level = abandon rate",
51
+ alwaysPresent: false,
52
+ notes: "GSC click lands on A, 301 to B, GA4 fires on B. Slow hops abandon mid-chain.",
53
+ },
54
+ {
55
+ code: "GSC_ANONYMIZED_QUERY",
56
+ group: "gsc",
57
+ name: "Anonymized queries",
58
+ direction: "n/a",
59
+ typicalMagnitude: "46.8% of GSC clicks have no query (Ahrefs, 22B clicks / 887,534 properties)",
60
+ alwaysPresent: true,
61
+ notes: "Query-dimensioned totals omit rare queries. Page-level clicks stay complete. Never compute site traffic from a query pull.",
62
+ },
63
+ {
64
+ code: "GSC_ROW_LIMIT",
65
+ group: "gsc",
66
+ name: "API row limit",
67
+ direction: "n/a",
68
+ typicalMagnitude: "API does not guarantee completeness past top rows",
69
+ alwaysPresent: true,
70
+ notes: "searchanalytics.query rowLimit 1–25,000; paging to exhaustion is still a sample. Only BigQuery bulk export is complete.",
71
+ },
72
+ {
73
+ code: "GSC_16_MONTH_WINDOW",
74
+ group: "gsc",
75
+ name: "16-month retention",
76
+ direction: "n/a",
77
+ typicalMagnitude: "exactly one rolling YoY pair",
78
+ alwaysPresent: true,
79
+ notes: "Archive locally from day one. The API window cannot be backfilled.",
80
+ },
81
+ {
82
+ code: "GSC_DISCOVER_NEWS_SPLIT",
83
+ group: "gsc",
84
+ name: "Discover / News split",
85
+ direction: "ga4_gt_gsc",
86
+ typicalMagnitude: "publishers 30–70% of 'organic'; B2B SaaS ~0",
87
+ alwaysPresent: false,
88
+ notes: "Discover and Google News are not in type=web. GA4 still counts them as google / organic.",
89
+ },
90
+ {
91
+ code: "GSC_GENAI_SURFACES",
92
+ group: "gsc",
93
+ name: "Generative AI surfaces",
94
+ direction: "either",
95
+ typicalMagnitude: "impressions definitional change from mid-2026",
96
+ alwaysPresent: false,
97
+ notes: "AI Overviews / AI Mode sit in overall totals. Dedicated report is impressions-only and not in the Search Analytics API.",
98
+ },
99
+ {
100
+ code: "GA4_CONSENT_DENIED",
101
+ group: "ga4_collection",
102
+ name: "Consent Mode denied",
103
+ direction: "gsc_gt_ga4",
104
+ typicalMagnitude: "EU/UK compliant reject-all: 40–65% of organic permanently invisible in GA4",
105
+ alwaysPresent: false,
106
+ notes: "A compliant EU property has 40–65% of organic traffic missing from GA4. GSC is unaffected.",
107
+ },
108
+ {
109
+ code: "GA4_ADBLOCK",
110
+ group: "ga4_collection",
111
+ name: "Ad blockers",
112
+ direction: "gsc_gt_ga4",
113
+ typicalMagnitude: "vertical-dependent; often 10–30%",
114
+ alwaysPresent: false,
115
+ notes: "Requests to google-analytics.com / googletagmanager.com never fire.",
116
+ },
117
+ {
118
+ code: "GA4_JS_FAIL",
119
+ group: "ga4_collection",
120
+ name: "Tag never fired",
121
+ direction: "gsc_gt_ga4",
122
+ typicalMagnitude: "template / CSP / bounce-before-tag",
123
+ alwaysPresent: false,
124
+ notes: "JS error before gtag(), missing tag, CSP, or bounce before the library loads.",
125
+ },
126
+ {
127
+ code: "GA4_CHANNEL_MISCLASS",
128
+ group: "ga4_config",
129
+ name: "Default channel group",
130
+ direction: "gsc_gt_ga4",
131
+ typicalMagnitude: "campaigns named *shop* silently leave Organic Search",
132
+ alwaysPresent: false,
133
+ notes: "Reconcile against sessionSourceMedium == 'google / organic', never the channel group.",
134
+ },
135
+ {
136
+ code: "GA4_ATTRIBUTION_MODEL",
137
+ group: "ga4_config",
138
+ name: "Data-driven attribution",
139
+ direction: "either",
140
+ typicalMagnitude: "key-event counts will not equal organic-started sessions",
141
+ alwaysPresent: true,
142
+ notes: "Sessions are not attribution-modelled. Key events are. Mixing them in one table is a bug.",
143
+ },
144
+ {
145
+ code: "GA4_SAMPLING",
146
+ group: "ga4_reporting",
147
+ name: "Sampled API response",
148
+ direction: "n/a",
149
+ typicalMagnitude: "standard properties sample above 10M events / query",
150
+ alwaysPresent: false,
151
+ notes: "Refuse to report a sampled number without a warning. Read ResponseMetaData.samplingMetadatas.",
152
+ },
153
+ {
154
+ code: "GA4_THRESHOLDING",
155
+ group: "ga4_reporting",
156
+ name: "Thresholded rows",
157
+ direction: "n/a",
158
+ typicalMagnitude: "undisclosed; triggered by Google Signals",
159
+ alwaysPresent: false,
160
+ notes: "Refuse thresholded numbers. subjectToThresholding on the Data API is a hard gate.",
161
+ },
162
+ ];
163
+ export function isEuTimeZone(timeZone) {
164
+ if (!timeZone)
165
+ return false;
166
+ const tz = timeZone.toLowerCase();
167
+ return (tz.startsWith("europe/") ||
168
+ tz === "gmt" ||
169
+ tz.includes("london") ||
170
+ tz.includes("dublin") ||
171
+ tz.includes("amsterdam") ||
172
+ tz.includes("berlin") ||
173
+ tz.includes("paris"));
174
+ }
175
+ //# sourceMappingURL=causes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"causes.js","sourceRoot":"","sources":["../src/causes.ts"],"names":[],"mappings":"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"}