@analitikisler/ai-tag-debugger 0.20.1 → 0.22.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.
package/src/parsers.js CHANGED
@@ -18,14 +18,63 @@ function ga4ParamsFrom(searchParams) {
18
18
  return params;
19
19
  }
20
20
 
21
- // gcs=G1xy: x = ad_storage, y = analytics_storage (1 granted, 0 denied).
22
- function ga4Consent(searchParams) {
21
+ export const CONSENT_SIGNALS = ["ad_storage", "analytics_storage", "ad_user_data", "ad_personalization"];
22
+
23
+ // gcd=13r3r3r2r5l1: one letter per signal, in CONSENT_SIGNALS order, each saying the default and the update
24
+ // the Google tag holds. Google publishes no key for the letters. Each one below was read off gtag.js in
25
+ // November 2026 by sending that consent sequence (test/parsers.test.js keeps the strings it sent); a letter
26
+ // not listed is kept as it is and never decides a check.
27
+ const GCD_STATES = {
28
+ l: {}, // consent mode not used for this signal
29
+ p: { default: "denied" },
30
+ q: { default: "denied", update: "denied" },
31
+ r: { default: "denied", update: "granted" },
32
+ t: { default: "granted" },
33
+ u: { default: "granted", update: "denied" },
34
+ v: { default: "granted", update: "granted" },
35
+ m: { update: "denied" },
36
+ n: { update: "granted" },
37
+ };
38
+
39
+ /** The gcd parameter as { signal: { letter, default?, update?, known } }, or null when it isn't one. */
40
+ export function decodeGcd(gcd) {
41
+ const m = /^\d{2}([a-z])\d([a-z])\d([a-z])\d([a-z])/i.exec(gcd ?? "");
42
+ if (!m) return null;
43
+ return Object.fromEntries(CONSENT_SIGNALS.map((signal, i) => {
44
+ const letter = m[i + 1].toLowerCase();
45
+ return [signal, { letter, ...(GCD_STATES[letter] ?? {}), known: Object.hasOwn(GCD_STATES, letter) }];
46
+ }));
47
+ }
48
+
49
+ /**
50
+ * The consent a Google hit says it was sent with. gcs=G1xy gives ad_storage (x) and analytics_storage (y),
51
+ * 1 granted and 0 denied; gcd adds ad_user_data and ad_personalization, and stands in for gcs when it is absent.
52
+ */
53
+ export function googleConsent(searchParams) {
54
+ const out = {};
23
55
  const gcs = searchParams.get("gcs");
24
- if (!gcs || !/^G1[01-]{2}$/.test(gcs)) return undefined;
25
- const state = (c) => (c === "1" ? "granted" : c === "0" ? "denied" : "unknown");
26
- return { ad_storage: state(gcs[2]), analytics_storage: state(gcs[3]) };
56
+ if (gcs && /^G1[01-]{2}$/.test(gcs)) {
57
+ const state = (c) => (c === "1" ? "granted" : c === "0" ? "denied" : "unknown");
58
+ out.ad_storage = state(gcs[2]);
59
+ out.analytics_storage = state(gcs[3]);
60
+ }
61
+ const gcd = decodeGcd(searchParams.get("gcd"));
62
+ for (const signal of CONSENT_SIGNALS) {
63
+ const state = gcd?.[signal].update ?? gcd?.[signal].default;
64
+ if (!(signal in out) && state) out[signal] = state;
65
+ }
66
+ return Object.keys(out).length ? out : undefined;
27
67
  }
28
68
 
69
+ // Fields of the Google protocol that are no event parameter but answer consent and session questions:
70
+ // the client and session ids, the session count and engagement, consent state, and the first visit,
71
+ // session start and new session id flags.
72
+ const GOOGLE_PROTOCOL = ["cid", "sid", "sct", "seg", "gcs", "gcd", "dma", "npa", "_fv", "_ss", "_nsi"];
73
+ const protocolOf = (searchParams) => {
74
+ const out = Object.fromEntries(GOOGLE_PROTOCOL.filter((k) => searchParams.has(k)).map((k) => [k, searchParams.get(k)]));
75
+ return Object.keys(out).length ? out : undefined;
76
+ };
77
+
29
78
  // Only www.google.<tld> and doubleclick.net: analytics.google.com is a primary GA4 endpoint.
30
79
  const GOOGLE_COMPANION_HOST = /^(www\.)?google\.[a-z]{2,3}(\.[a-z]{2})?$|(^|\.)doubleclick\.net$/;
31
80
 
@@ -72,7 +121,8 @@ export function parseGa4(url, postData) {
72
121
  name: merged.get("en") || "(unnamed)",
73
122
  id: merged.get("tid") || undefined,
74
123
  params: ga4ParamsFrom(merged),
75
- consent: ga4Consent(merged),
124
+ consent: googleConsent(merged),
125
+ protocol: protocolOf(merged),
76
126
  ...(items.length ? { items } : {}),
77
127
  ...(autoEvents.length ? { autoEvents } : {}),
78
128
  ...(user.length ? { userFields: Object.fromEntries(user) } : {}),
@@ -112,7 +162,9 @@ export function parseGoogleAds(url) {
112
162
  for (const key of ["label", "value", "currency_code", "oid"]) {
113
163
  if (u.searchParams.has(key)) params[key === "currency_code" ? "currency" : key === "oid" ? "transaction_id" : key] = u.searchParams.get(key);
114
164
  }
115
- return [{ platform: "google_ads", name: params.label ? `conversion:${params.label}` : "conversion", id: conversionId, params, url }];
165
+ const consent = googleConsent(u.searchParams);
166
+ const protocol = protocolOf(u.searchParams);
167
+ return [{ platform: "google_ads", name: params.label ? `conversion:${params.label}` : "conversion", id: conversionId, params, ...(consent ? { consent } : {}), ...(protocol ? { protocol } : {}), url }];
116
168
  }
117
169
 
118
170
  /** @returns {Hit[] | null} */
@@ -435,12 +487,14 @@ export const DEFAULT_PROVIDERS = ["ga4", "google_ads", "meta", "tiktok"];
435
487
  // Tag managers and consent platforms are always shown: they explain how the tags loaded.
436
488
  // Server-side tagging providers are too: their requests show where events are sent from.
437
489
  const ALWAYS_SHOWN = { gtm: "Google Tag Manager", consent: "Consent platform", stape: "Stape", elevar: "Elevar" };
490
+ /** The display name of any provider key a network call can carry: a tracking vendor or one that is always shown. */
491
+ export const providerLabel = (key) => PROVIDERS[key] ?? ALWAYS_SHOWN[key] ?? key;
438
492
 
439
493
  const HOSTS = [
440
494
  [/(^|\.)stape\.(io|jp)$/, "stape"],
441
495
  [/(^|\.)getelevar\.com$/, "elevar"],
442
496
  ...VENDORS.filter((v) => v.hosts).map((v) => [v.hosts, v.key]),
443
- [/(^|\.)(cookiebot\.com|cookielaw\.org|onetrust\.com|usercentrics\.(eu|com)|cookieyes\.com|didomi\.io|privacy-center\.org|trustarc\.com|truste\.com|consensu\.org|quantcast\.com|osano\.com|iubenda\.com|termly\.io|cookiefirst\.com|axeptio\.eu|sourcepoint\.com|sp-prod\.net|consentmanager\.net|cookiescript\.com|cookie-script\.com|cookieinformation\.com|civiccomputing\.com|piwik\.pro|complianz\.io|cdn\.privacy-mgmt\.com)$/, "consent"],
497
+ [/(^|\.)(cookiebot\.com|cookielaw\.org|onetrust\.com|usercentrics\.(eu|com)|cookieyes\.com|didomi\.io|privacy-center\.org|trustarc\.com|truste\.com|consensu\.org|quantcast\.com|osano\.com|iubenda\.com|termly\.io|cookiefirst\.com|axeptio\.eu|sourcepoint\.com|sp-prod\.net|consentmanager\.net|cookiescript\.com|cookie-script\.com|cookieinformation\.com|civiccomputing\.com|piwik\.pro|complianz\.io|cdn\.privacy-mgmt\.com|efilli\.com|cdn-cookieyes\.com)$/, "consent"],
444
498
  ];
445
499
 
446
500
  /**
package/src/report.js CHANGED
@@ -1,9 +1,11 @@
1
- import { AD_PLATFORMS, consentTable } from "./consent.js";
1
+ import { AD_PLATFORMS, consentMatrixMarkdown, consentTable } from "./consent.js";
2
+ import { cmpFacts } from "./cmp.js";
2
3
  import { ownerOf, verifyLinks } from "./verify.js";
3
4
  import { createProvider } from "./providers.js";
4
5
  import { canonicalEvent, groupFindings, summarize } from "./checks.js";
5
6
  import { messages } from "./i18n.js";
6
7
  import { buildInventory } from "./inventory.js";
8
+ import { containerRows } from "./containers.js";
7
9
  import { computeMetrics } from "./metrics.js";
8
10
  import { historySummaryLines, sparkline } from "./history.js";
9
11
  import { cardIds } from "./html/lib.js";
@@ -293,8 +295,10 @@ function mdCell(m, col, cell, raw, adPlatform) {
293
295
  }
294
296
 
295
297
  /** The consent section: the Consent Mode timing sentence, then the scenario table with a reason in every cell. */
296
- function mdConsent(m, metrics, scenarios) {
298
+ function mdConsent(m, metrics, scenarios, runs = []) {
297
299
  const lines = [`## ${m.ui.md.consent}`, ""];
300
+ const platform = cmpFacts(m, runs);
301
+ if (platform) lines.push(`${m.ui.cmp.title}: ${mdText(platform.vendor)}. ${platform.facts.map(mdText).join(" ")}`, "");
298
302
  const cm = metrics.consent?.consentMode;
299
303
  const tested = Boolean(cm?.runs?.length);
300
304
  if (tested) {
@@ -307,6 +311,8 @@ function mdConsent(m, metrics, scenarios) {
307
311
  lines.push(`| ${m.ui.scenarioTag} | ${cols.map((c) => m.ui.scenarioCols[c]).join(" | ")} |`, `|${" --- |".repeat(cols.length + 1)}`);
308
312
  for (const r of rows) lines.push(`| ${mdText(platformLabel(r.platform))} \`${mdCode(r.event)}\` | ${r.cells.map((c) => mdCell(m, c.state, c, rawOf(c.state, r.platform, r.event), AD_PLATFORMS.has(r.platform))).join(" | ")} |`);
309
313
  lines.push("", m.ui.md.consentLegend, "");
314
+ const matrix = consentMatrixMarkdown(m, scenarios);
315
+ if (matrix.length) lines.push(`### ${m.ui.matrix.title}`, "", ...matrix, "");
310
316
  } else if (!tested) lines.push(m.ui.md.consentNotChecked, "");
311
317
  return lines;
312
318
  }
@@ -391,6 +397,21 @@ function mdInventory(m, runs) {
391
397
  return lines;
392
398
  }
393
399
 
400
+ /** The Google containers and how each reached the page, one row per container and source. Omitted when none loaded. */
401
+ function mdContainers(m, runs) {
402
+ const rows = containerRows(runs);
403
+ if (!rows.length) return [];
404
+ const c = m.ui.containers;
405
+ const lines = [`## ${m.ui.containersTitle}`, "", `| ${m.ui.containerCol} | ${m.ui.sourceCol} | ${m.ui.layerCol} | ${m.ui.servedCol} | ${m.ui.pagesCol} |`, "|---|---|---|---|---|"];
406
+ for (const r of rows.slice(0, MD_INVENTORY_ROWS)) {
407
+ const served = r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`;
408
+ lines.push(`| ${mdText(r.id)} (${c.script[r.script]}) | ${mdText(c.kinds[r.kind](r.by))}${r.environment ? `, ${mdText(c.environment(r.environment))}` : ""} | ${mdText(r.layer)} | ${mdText(served)} | ${r.pages.length} |`);
409
+ }
410
+ if (rows.length > MD_INVENTORY_ROWS) lines.push("", m.ui.md.moreIn(rows.length - MD_INVENTORY_ROWS, "report.html"));
411
+ lines.push("");
412
+ return lines;
413
+ }
414
+
394
415
  /** The trend in up to five lines: the text sparkline with the direction, then the open-longest and flapping lines. */
395
416
  function mdTrend(m, trend, lang) {
396
417
  if (!trend || trend.runs < 2) return [];
@@ -417,6 +438,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
417
438
  const meta = [m.ui.md.meta(mdText(generatedAt), runs.length, m.ui.md.platformsSeen(platformsSeen.length, platformsSeen.map((p) => mdText(platformLabel(p))).join(", ")), VERSION), `[${m.ui.md.htmlReport}](report.html)`, ...(against ? [mdText(m.ui.checkedAgainst(against))] : [])];
418
439
  const status = `**${m.verdict.status[metrics.verdict.status]}**${metrics.score?.score === null || metrics.score?.score === undefined ? "" : ` · ${m.ui.md.score(metrics.score.score)}`}`;
419
440
  const lines = [`# ${mdText(m.ui.md.title(site ?? title, mdHeadline(m, runs, rows, metrics)))}`, "", meta.join(" · "), status, `**${m.ui.counts(counts)}**${changes && !changes.previous ? ` · ${m.ui.noPreviousRun}` : ""}`, ""];
441
+ if (runs.some((r) => r.live)) lines.push(`**${mdText(m.ui.liveRun)}**`, "");
420
442
  if (accepted) lines.push(mdText(m.ui.acceptedLine(accepted)), "");
421
443
  if (planChanges) lines.push(mdText(m.ui.planChangesLine(planChanges)), "", ...planChangeItems(planChanges).map((i) => `- ${mdText(i)}`), ...(planChangeItems(planChanges).length ? [""] : []));
422
444
  const sum = summaryParts(explained?.summary);
@@ -436,11 +458,12 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
436
458
  for (const f of problems.slice(0, MD_BLOCKS)) lines.push(...mdFinding(m, runs, f, ids));
437
459
  const rest = problems.slice(MD_BLOCKS);
438
460
  if (rest.length) lines.push(`### ${m.ui.md.also}`, ...rest.map((f) => `- **${m.severity[f.severity]}** ${mdDetail(f.detail).replace(/\.$/, "")} · ${mdWhere(m, runs, f.where, { full: false })}`), "");
439
- lines.push(...mdConsent(m, metrics, consentScenarios));
461
+ lines.push(...mdConsent(m, metrics, consentScenarios, runs));
440
462
  lines.push(...mdPassed(m, runs, findings, metrics, planCheck));
441
463
  lines.push(...mdJourneys(m, runs, rows));
442
464
  lines.push(...mdNotChecked(m, runs, rows, plan, consentScenarios));
443
465
  lines.push(...mdInventory(m, runs));
466
+ lines.push(...mdContainers(m, runs));
444
467
  // A report with its own name or logo carries no Analitik İşler credit; the ai tag debugger one stays.
445
468
  if (brand?.name || brand?.logo) lines.push("---", "", `_${brand.name ? `${brand.name} · ` : ""}${m.ui.generatedBy}_`, "");
446
469
  else if (branding) lines.push("---", "", `_${m.ui.generatedBy} · [analitikisler.com](https://analitikisler.com)_`, "");
package/src/verify.js CHANGED
@@ -15,6 +15,8 @@ const OWNER_BY_CHECK = {
15
15
  unplanned_events: "marketing",
16
16
  planned_out_of_scope: "marketing",
17
17
  ga4_automatic_event: "marketing",
18
+ page_blocked: "developer",
19
+ page_crashed: "developer",
18
20
  };
19
21
 
20
22
  /** Who most likely fixes this finding: the site's developer, whoever runs Tag Manager, or marketing (the plan itself). Tag and consent setup is the default. */