@analitikisler/ai-tag-debugger 0.17.0 → 0.19.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 +17 -0
- package/README.md +16 -17
- package/README.tr.md +16 -17
- package/package.json +3 -3
- package/src/brand.js +17 -11
- package/src/browser.js +44 -8
- package/src/html/consent.js +152 -0
- package/src/html/css.js +1182 -0
- package/src/html/findings.js +493 -0
- package/src/html/journeys.js +449 -0
- package/src/html/lib.js +283 -0
- package/src/html/scope.js +350 -0
- package/src/html/script.js +300 -0
- package/src/html/shell.js +191 -0
- package/src/html/summary.js +288 -0
- package/src/i18n.js +385 -171
- package/src/metrics.js +9 -3
- package/src/plan-html.js +74 -27
- package/src/report-shared.js +123 -0
- package/src/report.js +24 -1505
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The Consent section: one status line per platform, the Consent Mode card and the scenarios card. Denied is red,
|
|
2
|
+
// granted green and "no signal" grey, all by token through chip("consent", ...).
|
|
3
|
+
import { consentTable } from "../consent.js";
|
|
4
|
+
import { isConsentEntry } from "../report-shared.js";
|
|
5
|
+
import { PLATFORM_ART, chip, esc, platformLabel, safeKey, sectionBlock, sortPlatforms, stepTitle, secs, use, badge } from "./lib.js";
|
|
6
|
+
|
|
7
|
+
// Google tags carry Consent Mode signals on the hit; every other platform sends none, so its state is "none".
|
|
8
|
+
const GOOGLE = new Set(["ga4", "google_ads"]);
|
|
9
|
+
const SCENARIOS = ["before", "accept", "reject", "analytics"];
|
|
10
|
+
const LINE_CHECKS = new Set(["before_consent", "after_reject", "consent_default_missing"]);
|
|
11
|
+
const MODE_CHECKS = new Set(["consent_default_missing", "consent_default_late", "consent_v2_missing", "consent_v2_update"]);
|
|
12
|
+
const SEVERITY_RANK = { broken: 0, risk: 1, warning: 2, info: 3 };
|
|
13
|
+
|
|
14
|
+
/** A platform's artwork, or its initials when it has none. Decorative: the chip's text names it once for screen readers. */
|
|
15
|
+
function platArt(key) {
|
|
16
|
+
const k = safeKey(key);
|
|
17
|
+
if (PLATFORM_ART[k]) return use(`p-${k}`, 13);
|
|
18
|
+
const initials = platformLabel(key).split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
|
|
19
|
+
return `<span class="plat-fb s14" aria-hidden="true">${esc(initials)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The worst problem row among these, or null. Accepted rows (the baseline's) do not count, as everywhere else. */
|
|
23
|
+
const worstRow = (rows) => rows.filter((f) => !f.accepted && f.severity in SEVERITY_RANK).sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0] ?? null;
|
|
24
|
+
|
|
25
|
+
/** denied / granted for a Google platform, none for the rest; null when the tag sent nothing in that scenario. */
|
|
26
|
+
function stateOf(platform, c) {
|
|
27
|
+
if (!c?.n) return null;
|
|
28
|
+
if (!GOOGLE.has(platform)) return "none";
|
|
29
|
+
return c.cookieless >= c.n || c.adDenied >= c.n ? "denied" : "granted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What was sent after a scenario's choice (before: with no choice yet); undefined when the scenario did not run. */
|
|
33
|
+
const afterOf = (sc, s) => (s === "before" ? sc.before : sc.states?.[s]?.after);
|
|
34
|
+
|
|
35
|
+
/** The consent signals of a Consent Mode command, as the chips a hit carries: key and value, coloured by value. */
|
|
36
|
+
function signalChips(m, signals) {
|
|
37
|
+
return Object.entries(signals ?? {}).filter(([, v]) => typeof v === "string")
|
|
38
|
+
.map(([k, v]) => {
|
|
39
|
+
const state = v === "granted" || v === "denied" ? v : "none";
|
|
40
|
+
return chip("consent", state, `<span class="k">${esc(k)}</span>${esc(v)}`);
|
|
41
|
+
}).join("");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A neutral outlined chip for a plain label or number. */
|
|
45
|
+
const plain = (html, cls = "") => `<span class="chip plain${cls ? ` ${cls}` : ""}">${html}</span>`;
|
|
46
|
+
|
|
47
|
+
/** @param {import("./shell.js").Ctx} ctx */
|
|
48
|
+
export function render(ctx) {
|
|
49
|
+
const { m, runs, rows, ids, metrics, consentScenarios: sc } = ctx;
|
|
50
|
+
const t = m.html.consent;
|
|
51
|
+
const sevChip = (sev, label) => chip("sev", sev, `${use(`sev-${sev}`, 13)}${esc(label)}`);
|
|
52
|
+
const stateWord = (state) => t.state[state];
|
|
53
|
+
const stateChip = (state, label = stateWord(state)) => chip("consent", state, esc(label));
|
|
54
|
+
const platChip = (key) => chip("plat", key, `${platArt(key)}${esc(platformLabel(key))}`);
|
|
55
|
+
const findingLink = (row, cls = "") => (row && ids.get(row) ? `<a${cls ? ` class="${cls}"` : ""} href="#${ids.get(row)}">${esc(m.html.findingLink)}</a>` : "");
|
|
56
|
+
|
|
57
|
+
/* ---- one status line per platform ---- */
|
|
58
|
+
const total = (platform, s) => {
|
|
59
|
+
const after = sc ? afterOf(sc, s) : null;
|
|
60
|
+
if (!after) return "";
|
|
61
|
+
const byEvent = after[platform] ?? {};
|
|
62
|
+
const count = Object.values(byEvent).reduce((a, c) => a + c.n, 0);
|
|
63
|
+
if (!count) return `<span class="chip zero">0</span>`;
|
|
64
|
+
const states = new Set(Object.values(byEvent).map((c) => stateOf(platform, c)).filter(Boolean));
|
|
65
|
+
// One state for every event, else the strictest: a mix that holds a denied event reads denied.
|
|
66
|
+
const state = states.size === 1 ? [...states][0] : states.has("denied") ? "denied" : "none";
|
|
67
|
+
return stateChip(state, t.tally(count, stateWord(state)));
|
|
68
|
+
};
|
|
69
|
+
const platforms = sortPlatforms(new Set(metrics.consent.platforms.map((p) => p.platform)));
|
|
70
|
+
const lines = platforms.map((platform) => {
|
|
71
|
+
const own = rows.filter((f) => LINE_CHECKS.has(f.check) && f.platform === platform);
|
|
72
|
+
const row = worstRow(own);
|
|
73
|
+
// The scenarios can show a tag sent after Reject with no finding row for it; with no row at all (not even an
|
|
74
|
+
// accepted one) the metric decides, so the line agrees with the flagged cell below.
|
|
75
|
+
const sev = row?.severity ?? (!own.length && metrics.consent.platforms.find((p) => p.platform === platform)?.status !== "pass" ? "risk" : "ok");
|
|
76
|
+
const cells = SCENARIOS.slice(0, 3).filter((s) => sc && afterOf(sc, s))
|
|
77
|
+
.map((s) => `<span class="cs-p"><span class="cs-k">${esc(t.cols[s])}</span>${total(platform, s)}</span>`).join("");
|
|
78
|
+
return `<li class="card sev-${sev} cs-row" id="consent-${safeKey(platform)}">${platChip(platform)}${sevChip(sev, m.severity[sev])}${cells ? `<span class="cs-cells">${cells}</span>` : ""}${findingLink(row, "cs-go")}</li>`;
|
|
79
|
+
}).join("");
|
|
80
|
+
const list = lines ? `<ul class="cs-list" aria-label="${esc(t.statusLabel)}">${lines}</ul>` : "";
|
|
81
|
+
|
|
82
|
+
/* ---- Consent Mode ---- */
|
|
83
|
+
const mode = metrics.consent.consentMode;
|
|
84
|
+
const perRun = runs.map((run, ri) => {
|
|
85
|
+
const cmds = (run.dataLayerLog ?? []).filter((e) => isConsentEntry(e.value) && (e.value[1] === "default" || e.value[1] === "update"));
|
|
86
|
+
const first = cmds.find((e) => e.value[1] === "default");
|
|
87
|
+
const update = cmds.find((e) => e.value[1] === "update" && (!first || e.t >= first.t));
|
|
88
|
+
return { run, info: mode.runs[ri], first, update, resent: cmds.filter((e) => e !== first && e !== update).length };
|
|
89
|
+
});
|
|
90
|
+
const runChip = (run) => chip("vp", run.viewport, `${run.viewport === "desktop" || run.viewport === "mobile" ? use(`ui-${run.viewport}`, 13) : ""}${esc(run.journeyId)}<span class="sr"> ${esc(run.viewport)}</span>`);
|
|
91
|
+
const modeFinding = worstRow(rows.filter((f) => MODE_CHECKS.has(f.check)));
|
|
92
|
+
const withDefault = perRun.filter((p) => p.info?.present);
|
|
93
|
+
const ms = withDefault.map((p) => p.info.ms).filter((x) => x !== null);
|
|
94
|
+
const late = mode.defaultOnTime === false;
|
|
95
|
+
let modeHead;
|
|
96
|
+
if (!runs.length) modeHead = chip("consent", "none", esc(m.ui.notChecked));
|
|
97
|
+
else if (!mode.defaultPresent) modeHead = modeFinding ? sevChip(modeFinding.severity, t.noDefault) : chip("consent", "none", esc(t.noDefault));
|
|
98
|
+
else modeHead = late ? sevChip(modeFinding?.severity ?? "risk", t.defaultLate) : sevChip("ok", t.defaultFirst);
|
|
99
|
+
const modeChips = `${ms.length ? plain(`${use("ui-clock", 13)}<b>${esc(t.timing(Math.min(...ms), Math.max(...ms)))}</b>`, "mono") : ""}${withDefault.length && mode.v2Signals.present ? sevChip("ok", t.v2) : withDefault.length ? sevChip("warning", t.v2Missing) : ""}`;
|
|
100
|
+
|
|
101
|
+
// Runs that pushed the same commands on the same steps share one pair of rows; a run that differs gets its own.
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
for (const p of withDefault) {
|
|
104
|
+
const key = JSON.stringify([p.first?.value[2], p.update?.value[2], p.info.stepLabel, p.update?.step]);
|
|
105
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
106
|
+
groups.get(key).push(p);
|
|
107
|
+
}
|
|
108
|
+
const commandRow = (label, p, e) => {
|
|
109
|
+
if (!e) return "";
|
|
110
|
+
const step = p.run.steps.find((s) => s.index === e.step);
|
|
111
|
+
const where = `${step ? `${m.ui.step} ${step.index} · ${esc(stepTitle(p.run, step))} · ` : ""}${esc(secs(p.run, e.t))}`;
|
|
112
|
+
return `<li><b class="cm-l">${esc(label)}</b><span class="cm-w">${where}</span><span class="cm-s">${signalChips(m, e.value[2])}</span></li>`;
|
|
113
|
+
};
|
|
114
|
+
const groupHtml = [...groups.values()].map((g) => `${groups.size > 1 ? `<div class="chips cm-runs">${g.map((p) => runChip(p.run)).join("")}</div>` : ""}<ol class="cm-list">${commandRow(m.ui.consentDefault, g[0], g[0].first)}${commandRow(m.ui.consentUpdate, g[0], g[0].update)}</ol>`).join("");
|
|
115
|
+
const timing = withDefault.map((p) => `<li>${runChip(p.run)}<span class="xs">${esc(m.html.consentTiming(p.info.ms, p.info.beforeFirstGoogleHit, p.info.v2Signals.present, p.info.v2Signals.missing))}</span></li>`).join("");
|
|
116
|
+
const resent = Math.max(0, ...withDefault.map((p) => p.resent));
|
|
117
|
+
const modeFindings = rows.filter((f) => MODE_CHECKS.has(f.check) && !f.accepted && ids.get(f)).map((f) => findingLink(f)).join(" ");
|
|
118
|
+
const none = !runs.length ? m.ui.md.consentNotChecked : m.ui.md.consentModeNone;
|
|
119
|
+
const modeBody = withDefault.length
|
|
120
|
+
? `${timing ? `<ul class="cm-timing">${timing}</ul>` : ""}${groupHtml}${resent ? `<p class="xs muted cm-rep">${use("ui-clock", 12)} ${esc(t.resent(resent))}</p>` : ""}${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`
|
|
121
|
+
: `<p class="muted xs">${esc(none)}</p>${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`;
|
|
122
|
+
const modeCard = `<details class="card" id="consent-mode"><summary><span class="t">${esc(m.html.consentMode)}</span>${modeHead}${modeChips}<span class="end muted xs">${esc(t.runsWith(withDefault.length, runs.length))}</span></summary><div class="body">${modeBody}</div></details>`;
|
|
123
|
+
|
|
124
|
+
/* ---- scenarios ---- */
|
|
125
|
+
if (!sc) return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${modeCard}\n<p class="muted xs cs-none" id="consent-scenarios">${esc(m.ui.md.noScenarios)}</p>`);
|
|
126
|
+
const table = consentTable(sc);
|
|
127
|
+
const cols = table.cols.filter((s) => s !== "analytics" || sc.states?.analytics?.via);
|
|
128
|
+
const before = rows.filter((f) => f.check === "before_consent" && !f.accepted);
|
|
129
|
+
const problemRow = (platform, event) => rows.find((f) => f.platform === platform && f.event === event && !f.accepted && /^(after_reject|before_consent)$/.test(f.check));
|
|
130
|
+
const reasonOf = (c) => (c.cookieless >= c.n ? m.html.cellCookieless : c.adDenied >= c.n ? m.html.cellAdDenied : m.html.cellWithCookies);
|
|
131
|
+
let problemCount = 0;
|
|
132
|
+
const cellHtml = (r, cell) => {
|
|
133
|
+
const after = afterOf(sc, cell.state);
|
|
134
|
+
if (!after || cell.count === null) return `<td class="muted">–<span class="sr"> ${esc(m.ui.scenarioNotRun)}</span></td>`;
|
|
135
|
+
const c = after[r.platform]?.[r.event];
|
|
136
|
+
if (!c?.n) return `<td class="muted">0</td>`;
|
|
137
|
+
const state = stateOf(r.platform, c);
|
|
138
|
+
const reason = reasonOf(c);
|
|
139
|
+
const flagged = cell.problem || (cell.state === "before" && before.some((f) => f.platform === r.platform && f.event === r.event));
|
|
140
|
+
const body = `<b class="num">${c.n}</b><span title="${esc(reason)}">${stateChip(state)}</span><span class="sr"> ${esc(reason)}</span>`;
|
|
141
|
+
if (!flagged) return `<td><span class="cs-c">${body}</span></td>`;
|
|
142
|
+
problemCount++;
|
|
143
|
+
const f = problemRow(r.platform, r.event);
|
|
144
|
+
return `<td class="cs-bad"><span class="cs-c">${body}${badge(f?.severity ?? "risk", t.sent)}<span class="sr"> ${esc(m.html.cellProblem)} ${esc(m.html.cellShouldNot)}</span>${findingLink(f)}</span></td>`;
|
|
145
|
+
};
|
|
146
|
+
const body = table.rows.map((r) => `<tr><th scope="row"><span class="cs-tag">${platChip(r.platform)}<code>${esc(r.event)}</code></span></th>${cols.map((s) => cellHtml(r, r.cells.find((c) => c.state === s))).join("")}</tr>`).join("");
|
|
147
|
+
const supported = (s) => (s === "before" ? true : Boolean(sc.states?.[s]?.after));
|
|
148
|
+
const summaryChips = SCENARIOS.map((s) => (supported(s) ? plain(esc(t.cols[s])) : chip("consent", "none", esc(`${t.cols[s]} · ${t.notSupported}`)))).join("");
|
|
149
|
+
const probChip = problemCount ? sevChip("risk", t.problems(problemCount)) : sevChip("ok", t.noProblems);
|
|
150
|
+
const scenarios = `<details class="card" id="consent-scenarios"><summary><span class="t">${esc(t.scenarios)}</span>${summaryChips}<span class="end">${probChip}</span></summary><div class="body"><div class="tscroll"><table class="tbl cs-tbl"><caption class="sr">${esc(m.ui.scenarios)}</caption><thead><tr><th scope="col">${esc(m.ui.scenarioTag)}</th>${cols.map((s) => `<th scope="col">${esc(t.cols[s])}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div></div></details>`;
|
|
151
|
+
return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${modeCard}\n${scenarios}`);
|
|
152
|
+
}
|