@analitikisler/ai-tag-debugger 0.20.0 → 0.21.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/CHANGELOG.md +39 -0
- package/README.md +22 -11
- package/README.tr.md +22 -11
- package/package.json +1 -1
- package/src/agent.js +5 -1
- package/src/browser.js +211 -19
- package/src/campaign.js +7 -4
- package/src/checks.js +118 -32
- package/src/cli.js +2 -1
- package/src/cmp.js +166 -0
- package/src/consent.js +200 -26
- package/src/help.js +1 -0
- package/src/hit-params.js +70 -0
- package/src/html/consent.js +34 -9
- package/src/html/journeys.js +36 -71
- package/src/html/summary.js +1 -1
- package/src/i18n.js +169 -4
- package/src/mcp.js +335 -28
- package/src/metrics.js +13 -7
- package/src/parsers.js +65 -8
- package/src/report.js +9 -3
- package/src/verify.js +3 -0
package/src/parsers.js
CHANGED
|
@@ -18,14 +18,63 @@ function ga4ParamsFrom(searchParams) {
|
|
|
18
18
|
return params;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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 (
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
|
@@ -64,14 +113,18 @@ export function parseGa4(url, postData) {
|
|
|
64
113
|
for (const [k, v] of new URLSearchParams(line)) merged.set(k, v);
|
|
65
114
|
const items = [...merged].filter(([k]) => /^pr\d+$/.test(k)).map(([, v]) => v);
|
|
66
115
|
const user = [...merged].filter(([k]) => isUserDataKey(k));
|
|
116
|
+
// GA4 makes first_visit and session_start from these flags on another hit; they are never sent by name.
|
|
117
|
+
const autoEvents = [["_fv", "first_visit"], ["_ss", "session_start"]].filter(([flag]) => merged.has(flag)).map(([, name]) => name);
|
|
67
118
|
return {
|
|
68
119
|
platform: "ga4",
|
|
69
120
|
delivery: ga4Delivery(u),
|
|
70
121
|
name: merged.get("en") || "(unnamed)",
|
|
71
122
|
id: merged.get("tid") || undefined,
|
|
72
123
|
params: ga4ParamsFrom(merged),
|
|
73
|
-
consent:
|
|
124
|
+
consent: googleConsent(merged),
|
|
125
|
+
protocol: protocolOf(merged),
|
|
74
126
|
...(items.length ? { items } : {}),
|
|
127
|
+
...(autoEvents.length ? { autoEvents } : {}),
|
|
75
128
|
...(user.length ? { userFields: Object.fromEntries(user) } : {}),
|
|
76
129
|
url,
|
|
77
130
|
};
|
|
@@ -109,7 +162,9 @@ export function parseGoogleAds(url) {
|
|
|
109
162
|
for (const key of ["label", "value", "currency_code", "oid"]) {
|
|
110
163
|
if (u.searchParams.has(key)) params[key === "currency_code" ? "currency" : key === "oid" ? "transaction_id" : key] = u.searchParams.get(key);
|
|
111
164
|
}
|
|
112
|
-
|
|
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 }];
|
|
113
168
|
}
|
|
114
169
|
|
|
115
170
|
/** @returns {Hit[] | null} */
|
|
@@ -432,12 +487,14 @@ export const DEFAULT_PROVIDERS = ["ga4", "google_ads", "meta", "tiktok"];
|
|
|
432
487
|
// Tag managers and consent platforms are always shown: they explain how the tags loaded.
|
|
433
488
|
// Server-side tagging providers are too: their requests show where events are sent from.
|
|
434
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;
|
|
435
492
|
|
|
436
493
|
const HOSTS = [
|
|
437
494
|
[/(^|\.)stape\.(io|jp)$/, "stape"],
|
|
438
495
|
[/(^|\.)getelevar\.com$/, "elevar"],
|
|
439
496
|
...VENDORS.filter((v) => v.hosts).map((v) => [v.hosts, v.key]),
|
|
440
|
-
[/(^|\.)(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"],
|
|
441
498
|
];
|
|
442
499
|
|
|
443
500
|
/**
|
package/src/report.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -293,8 +294,10 @@ function mdCell(m, col, cell, raw, adPlatform) {
|
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
/** The consent section: the Consent Mode timing sentence, then the scenario table with a reason in every cell. */
|
|
296
|
-
function mdConsent(m, metrics, scenarios) {
|
|
297
|
+
function mdConsent(m, metrics, scenarios, runs = []) {
|
|
297
298
|
const lines = [`## ${m.ui.md.consent}`, ""];
|
|
299
|
+
const platform = cmpFacts(m, runs);
|
|
300
|
+
if (platform) lines.push(`${m.ui.cmp.title}: ${mdText(platform.vendor)}. ${platform.facts.map(mdText).join(" ")}`, "");
|
|
298
301
|
const cm = metrics.consent?.consentMode;
|
|
299
302
|
const tested = Boolean(cm?.runs?.length);
|
|
300
303
|
if (tested) {
|
|
@@ -307,6 +310,8 @@ function mdConsent(m, metrics, scenarios) {
|
|
|
307
310
|
lines.push(`| ${m.ui.scenarioTag} | ${cols.map((c) => m.ui.scenarioCols[c]).join(" | ")} |`, `|${" --- |".repeat(cols.length + 1)}`);
|
|
308
311
|
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
312
|
lines.push("", m.ui.md.consentLegend, "");
|
|
313
|
+
const matrix = consentMatrixMarkdown(m, scenarios);
|
|
314
|
+
if (matrix.length) lines.push(`### ${m.ui.matrix.title}`, "", ...matrix, "");
|
|
310
315
|
} else if (!tested) lines.push(m.ui.md.consentNotChecked, "");
|
|
311
316
|
return lines;
|
|
312
317
|
}
|
|
@@ -417,6 +422,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
|
|
|
417
422
|
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
423
|
const status = `**${m.verdict.status[metrics.verdict.status]}**${metrics.score?.score === null || metrics.score?.score === undefined ? "" : ` · ${m.ui.md.score(metrics.score.score)}`}`;
|
|
419
424
|
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}` : ""}`, ""];
|
|
425
|
+
if (runs.some((r) => r.live)) lines.push(`**${mdText(m.ui.liveRun)}**`, "");
|
|
420
426
|
if (accepted) lines.push(mdText(m.ui.acceptedLine(accepted)), "");
|
|
421
427
|
if (planChanges) lines.push(mdText(m.ui.planChangesLine(planChanges)), "", ...planChangeItems(planChanges).map((i) => `- ${mdText(i)}`), ...(planChangeItems(planChanges).length ? [""] : []));
|
|
422
428
|
const sum = summaryParts(explained?.summary);
|
|
@@ -436,7 +442,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
|
|
|
436
442
|
for (const f of problems.slice(0, MD_BLOCKS)) lines.push(...mdFinding(m, runs, f, ids));
|
|
437
443
|
const rest = problems.slice(MD_BLOCKS);
|
|
438
444
|
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));
|
|
445
|
+
lines.push(...mdConsent(m, metrics, consentScenarios, runs));
|
|
440
446
|
lines.push(...mdPassed(m, runs, findings, metrics, planCheck));
|
|
441
447
|
lines.push(...mdJourneys(m, runs, rows));
|
|
442
448
|
lines.push(...mdNotChecked(m, runs, rows, plan, consentScenarios));
|
package/src/verify.js
CHANGED
|
@@ -14,6 +14,9 @@ const OWNER_BY_CHECK = {
|
|
|
14
14
|
server_side: "developer",
|
|
15
15
|
unplanned_events: "marketing",
|
|
16
16
|
planned_out_of_scope: "marketing",
|
|
17
|
+
ga4_automatic_event: "marketing",
|
|
18
|
+
page_blocked: "developer",
|
|
19
|
+
page_crashed: "developer",
|
|
17
20
|
};
|
|
18
21
|
|
|
19
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. */
|