@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/CHANGELOG.md +40 -0
- package/README.md +43 -11
- package/README.tr.md +42 -10
- package/package.json +4 -1
- package/src/agent.js +6 -1
- package/src/browser.js +262 -29
- package/src/campaign.js +7 -4
- package/src/checks.js +91 -20
- package/src/cli.js +2 -1
- package/src/cmp.js +166 -0
- package/src/consent.js +200 -26
- package/src/containers.js +163 -0
- 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 +32 -70
- package/src/html/scope.js +15 -1
- package/src/html/summary.js +1 -1
- package/src/i18n.js +230 -4
- package/src/mcp.js +341 -22
- package/src/metrics.js +13 -7
- package/src/parsers.js +62 -8
- package/src/report.js +26 -3
- package/src/verify.js +2 -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
|
|
|
@@ -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:
|
|
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
|
-
|
|
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. */
|