@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/html/consent.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// The Consent section: one status line per platform, the Consent Mode card and the scenarios card. Denied is red,
|
|
2
2
|
// granted green and "no signal" grey, all by token through chip("consent", ...).
|
|
3
|
-
import { consentTable } from "../consent.js";
|
|
3
|
+
import { consentMatrix, consentTable, IGNORE, SCENARIO_COLUMNS } from "../consent.js";
|
|
4
|
+
import { cmpFacts } from "../cmp.js";
|
|
4
5
|
import { isConsentEntry } from "../report-shared.js";
|
|
6
|
+
import { tagConsentStates } from "../checks.js";
|
|
5
7
|
import { PLATFORM_ART, chip, esc, platformLabel, safeKey, sectionBlock, sortPlatforms, stepTitle, secs, use, badge } from "./lib.js";
|
|
6
8
|
|
|
7
9
|
// Google tags carry Consent Mode signals on the hit; every other platform sends none, so its state is "none".
|
|
8
10
|
const GOOGLE = new Set(["ga4", "google_ads"]);
|
|
9
|
-
|
|
11
|
+
// The status line per platform keeps to no choice, accept and reject; the card lists every column that ran.
|
|
12
|
+
const STATUS_SCENARIOS = ["before", "accept", "reject"];
|
|
10
13
|
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"]);
|
|
14
|
+
const MODE_CHECKS = new Set(["consent_default_missing", "consent_default_late", "consent_default_not_applied", "consent_v2_missing", "consent_v2_update"]);
|
|
12
15
|
const SEVERITY_RANK = { broken: 0, risk: 1, warning: 2, info: 3 };
|
|
13
16
|
|
|
14
17
|
/** A platform's artwork, or its initials when it has none. Decorative: the chip's text names it once for screen readers. */
|
|
@@ -73,12 +76,18 @@ export function render(ctx) {
|
|
|
73
76
|
// The scenarios can show a tag sent after Reject with no finding row for it; with no row at all (not even an
|
|
74
77
|
// accepted one) the metric decides, so the line agrees with the flagged cell below.
|
|
75
78
|
const sev = row?.severity ?? (!own.length && metrics.consent.platforms.find((p) => p.platform === platform)?.status !== "pass" ? "risk" : "ok");
|
|
76
|
-
const cells =
|
|
79
|
+
const cells = STATUS_SCENARIOS.filter((s) => sc && afterOf(sc, s))
|
|
77
80
|
.map((s) => `<span class="cs-p"><span class="cs-k">${esc(t.cols[s])}</span>${total(platform, s)}</span>`).join("");
|
|
78
81
|
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
82
|
}).join("");
|
|
80
83
|
const list = lines ? `<ul class="cs-list" aria-label="${esc(t.statusLabel)}">${lines}</ul>` : "";
|
|
81
84
|
|
|
85
|
+
/* ---- consent platform ---- */
|
|
86
|
+
const platform = cmpFacts(m, runs);
|
|
87
|
+
const platformCard = platform
|
|
88
|
+
? `<details class="card" id="consent-platform"><summary><span class="t">${esc(m.ui.cmp.title)}</span>${plain(esc(platform.vendor))}</summary><div class="body"><ul class="xs cm-timing">${platform.facts.map((f) => `<li>${esc(f)}</li>`).join("")}</ul></div></details>\n`
|
|
89
|
+
: "";
|
|
90
|
+
|
|
82
91
|
/* ---- Consent Mode ---- */
|
|
83
92
|
const mode = metrics.consent.consentMode;
|
|
84
93
|
const perRun = runs.map((run, ri) => {
|
|
@@ -116,13 +125,24 @@ export function render(ctx) {
|
|
|
116
125
|
const resent = Math.max(0, ...withDefault.map((p) => p.resent));
|
|
117
126
|
const modeFindings = rows.filter((f) => MODE_CHECKS.has(f.check) && !f.accepted && ids.get(f)).map((f) => findingLink(f)).join(" ");
|
|
118
127
|
const none = !runs.length ? m.ui.md.consentNotChecked : m.ui.md.consentModeNone;
|
|
128
|
+
// The Google tag's last recorded state per run; runs that ended in the same state share one line.
|
|
129
|
+
const tagGroups = new Map();
|
|
130
|
+
for (const run of runs) {
|
|
131
|
+
const last = tagConsentStates(run).at(-1);
|
|
132
|
+
if (!last) continue;
|
|
133
|
+
const k = JSON.stringify({ ...last, step: undefined });
|
|
134
|
+
if (!tagGroups.has(k)) tagGroups.set(k, { last, runs: [] });
|
|
135
|
+
tagGroups.get(k).runs.push(run);
|
|
136
|
+
}
|
|
137
|
+
const viaTag = withDefault.some((p) => p.info.source === "tag") ? `<p class="xs muted">${esc(t.defaultViaTag)}</p>` : "";
|
|
138
|
+
const tagHtml = [...tagGroups.values()].map(({ last, runs: rs }) => `<div class="cm-tag">${tagGroups.size > 1 ? `<div class="chips cm-runs">${rs.map(runChip).join("")}</div>` : ""}<p class="xs"><b>${esc(t.tagState)}</b> · ${esc(t.tagFlags(last.usedDefault, last.usedUpdate, last.wasSetLate, last.waitPeriodTimedOut))}</p><div class="chips">${signalChips(m, Object.fromEntries(Object.entries(last.entries).map(([k, e]) => [k, e.update ?? e.default ?? "none"])))}</div></div>`).join("");
|
|
119
139
|
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>` : ""}`;
|
|
140
|
+
? `${timing ? `<ul class="cm-timing">${timing}</ul>` : ""}${viaTag}${groupHtml}${tagHtml}${resent ? `<p class="xs muted cm-rep">${use("ui-clock", 12)} ${esc(t.resent(resent))}</p>` : ""}${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`
|
|
141
|
+
: `<p class="muted xs">${esc(none)}</p>${tagHtml}${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`;
|
|
122
142
|
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
143
|
|
|
124
144
|
/* ---- 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>`);
|
|
145
|
+
if (!sc) return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${platformCard}${modeCard}\n<p class="muted xs cs-none" id="consent-scenarios">${esc(m.ui.md.noScenarios)}</p>`);
|
|
126
146
|
const table = consentTable(sc);
|
|
127
147
|
const cols = table.cols.filter((s) => s !== "analytics" || sc.states?.analytics?.via);
|
|
128
148
|
const before = rows.filter((f) => f.check === "before_consent" && !f.accepted);
|
|
@@ -145,8 +165,13 @@ export function render(ctx) {
|
|
|
145
165
|
};
|
|
146
166
|
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
167
|
const supported = (s) => (s === "before" ? true : Boolean(sc.states?.[s]?.after));
|
|
148
|
-
|
|
168
|
+
// Captures from before the no-choice scenario existed have no column for it.
|
|
169
|
+
const summaryChips = SCENARIO_COLUMNS.filter((s) => s !== IGNORE || sc.states?.[IGNORE]).map((s) => (supported(s) ? plain(esc(t.cols[s])) : chip("consent", "none", esc(`${t.cols[s]} · ${t.notSupported}`)))).join("");
|
|
149
170
|
const probChip = problemCount ? sevChip("risk", t.problems(problemCount)) : sevChip("ok", t.noProblems);
|
|
150
171
|
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
|
-
|
|
172
|
+
const matrix = consentMatrix(m, sc);
|
|
173
|
+
const matrixCard = matrix
|
|
174
|
+
? `\n<details class="card" id="consent-matrix"><summary><span class="t">${esc(m.ui.matrix.title)}</span>${matrix.rows.some((r) => r.cells.some((c) => c.problem)) ? sevChip("risk", t.problems(matrix.rows.reduce((n, r) => n + r.cells.filter((c) => c.problem).length, 0))) : sevChip("ok", t.noProblems)}</summary><div class="body">${matrix.landing ? `<p class="xs muted">${esc(m.ui.matrix.intro(matrix.landing))}</p>` : ""}<div class="tscroll"><table class="tbl cs-tbl"><caption class="sr">${esc(m.ui.matrix.title)}</caption><thead><tr><th scope="col"></th>${matrix.cols.map((c) => `<th scope="col">${esc(t.cols[c])}</th>`).join("")}</tr></thead><tbody>${matrix.rows.map((r) => `<tr><th scope="row">${esc(r.label)}</th>${r.cells.map((c) => (c.problem ? `<td class="cs-bad"><span class="cs-c">${esc(c.text)}${badge("risk", "⚠")}<span class="sr"> ${esc(m.ui.matrix.problem)}</span></span></td>` : `<td class="xs">${esc(c.text)}</td>`)).join("")}</tr>`).join("")}</tbody></table></div><p class="xs muted">${esc(m.ui.matrix.legend)}</p></div></details>`
|
|
175
|
+
: "";
|
|
176
|
+
return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${platformCard}${modeCard}\n${scenarios}${matrixCard}`);
|
|
152
177
|
}
|
package/src/html/journeys.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// [data-vp-switch], details.card), so shell.js drives it; the JOURNEYS script in script.js only keeps the viewport
|
|
4
4
|
// when the journey changes. Ids come from lib.js (stepAnchor, hitId, pushId, requestId) so Findings links land here.
|
|
5
5
|
import { canonicalEvent } from "../checks.js";
|
|
6
|
-
import { parseTrackingRequest, providerOf, PROVIDERS } from "../parsers.js";
|
|
7
|
-
import { decodeGa4Item,
|
|
6
|
+
import { CONSENT_SIGNALS, parseTrackingRequest, providerOf, PROVIDERS } from "../parsers.js";
|
|
7
|
+
import { decodeGa4Item, paramTip } from "../glossary.js";
|
|
8
|
+
import { callsForHit, hitParams, paramSets } from "../hit-params.js";
|
|
8
9
|
import { SEVERITY_RANK, isConsentEntry } from "../report-shared.js";
|
|
9
10
|
import {
|
|
10
11
|
sectionBlock, use, plat, platIcon, badge, chip, n, esc, secs, pathOf, hostPath, safeKey, platformLabel, sortPlatforms, detailHtml,
|
|
@@ -63,53 +64,7 @@ function pushesForHit(run, hit) {
|
|
|
63
64
|
return twins.length > 1 && pushes.length === twins.length ? [pushes[twins.indexOf(hit)]] : pushes;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
function callsForHit(run, hit) {
|
|
68
|
-
// A late hit was sent during a later step, so its request is recorded there.
|
|
69
|
-
const sentIn = hit.late?.during ?? hit.step;
|
|
70
|
-
const calls = (run.network ?? []).filter((c) => c.step === sentIn && c.url === hit.url);
|
|
71
|
-
const twins = run.hits.filter((h) => (h.late?.during ?? h.step) === sentIn && h.url === hit.url);
|
|
72
|
-
return twins.length > 1 && calls.length === twins.length ? [calls[twins.indexOf(hit)]] : calls;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* ---------- request parameters, moved from the 0.17 renderer ---------- */
|
|
76
|
-
|
|
77
|
-
/** Nested JSON as dotted keys (properties.value); arrays stay JSON text. */
|
|
78
|
-
function flatten(obj, prefix = "", out = []) {
|
|
79
|
-
for (const [k, v] of Object.entries(obj ?? {})) {
|
|
80
|
-
const key = prefix ? `${prefix}.${k}` : k;
|
|
81
|
-
if (v && typeof v === "object" && !Array.isArray(v) && prefix.split(".").length < 3) flatten(v, key, out);
|
|
82
|
-
else out.push([key, cell(v)]);
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** The parameter sets a request carried: one per event in a GA4 batch (query plus each body line), the query plus a flattened JSON body, or just the query. */
|
|
88
|
-
function paramSets(call) {
|
|
89
|
-
let u;
|
|
90
|
-
try {
|
|
91
|
-
u = new URL(call.url);
|
|
92
|
-
} catch {
|
|
93
|
-
return [[]];
|
|
94
|
-
}
|
|
95
|
-
const query = [...u.searchParams];
|
|
96
|
-
const body = call.postData;
|
|
97
|
-
if (!body) return [query];
|
|
98
|
-
const trimmed = body.trim();
|
|
99
|
-
if (trimmed.startsWith("{")) {
|
|
100
|
-
try {
|
|
101
|
-
return [[...query, ...flatten(JSON.parse(trimmed))]];
|
|
102
|
-
} catch { /* not JSON, or cut at the size limit */ }
|
|
103
|
-
}
|
|
104
|
-
if (/^[^\s{]*=/.test(trimmed)) {
|
|
105
|
-
return trimmed.split("\n").filter(Boolean).map((line) => {
|
|
106
|
-
const merged = new Map(query);
|
|
107
|
-
for (const [k, v] of new URLSearchParams(line)) merged.set(k, v);
|
|
108
|
-
return [...merged];
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return [query];
|
|
112
|
-
}
|
|
67
|
+
/* ---------- request parameters ---------- */
|
|
113
68
|
|
|
114
69
|
/** Parameter rows with a glossary tip: the "?" shows the tip on hover and takes no focus; screen readers read the hidden span. */
|
|
115
70
|
function paramRows(lang, entries) {
|
|
@@ -120,24 +75,6 @@ function paramRows(lang, entries) {
|
|
|
120
75
|
}).join("");
|
|
121
76
|
}
|
|
122
77
|
|
|
123
|
-
/**
|
|
124
|
-
* What a hit card shows as its parameters: the request's own parameter set (raw keys, with glossary tips), split
|
|
125
|
-
* into the event's parameters and the session, device, page and consent ones. A GA4 batch carries one set per
|
|
126
|
-
* event; the hit takes the set with its name. Without a captured request, the hit's parsed parameters.
|
|
127
|
-
*/
|
|
128
|
-
function hitParams(run, hit) {
|
|
129
|
-
const call = callsForHit(run, hit)[0] ?? null;
|
|
130
|
-
if (!call) return { call, batch: null, event: Object.entries(hit.params ?? {}).filter(([k]) => !(k === "items" && hit.items?.length)).map(([k, v]) => [k, cell(v)]), context: [] };
|
|
131
|
-
const sets = paramSets(call);
|
|
132
|
-
let i = 0;
|
|
133
|
-
if (sets.length > 1) {
|
|
134
|
-
const names = (parseTrackingRequest(call.url, call.postData ?? null) ?? []).map((h) => h.name);
|
|
135
|
-
i = Math.max(0, names.indexOf(hit.name));
|
|
136
|
-
}
|
|
137
|
-
const entries = sets[i] ?? [];
|
|
138
|
-
return { call, batch: sets.length > 1 ? [i + 1, sets.length] : null, event: entries.filter(([k]) => isEventParam(k)), context: entries.filter(([k]) => !isEventParam(k)) };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
78
|
/* ---------- items: GA4 item strings and dataLayer item objects as one table ---------- */
|
|
142
79
|
|
|
143
80
|
const STANDARD_ITEM_KEYS = new Set(["item_id", "item_name", "affiliation", "coupon", "discount", "index", "item_brand", "item_category", "item_category2", "item_category3", "item_category4", "item_category5", "item_list_id", "item_list_name", "item_variant", "location_id", "price", "quantity", "promotion_id", "promotion_name", "creative_name", "creative_slot"]);
|
|
@@ -178,7 +115,7 @@ function consentChip(t, key, v) {
|
|
|
178
115
|
return `<span class="chip chip-consent ${safeKey(v)}" title="${esc(`${key}: ${v}`)}"><span class="k" aria-hidden="true">${esc(t.signals[key] ?? key)}</span><span aria-hidden="true">${mark}</span><span class="sr">${esc(`${key} ${v}`)}</span></span>`;
|
|
179
116
|
}
|
|
180
117
|
const hitConsent = (t, c) => {
|
|
181
|
-
const keys = c ?
|
|
118
|
+
const keys = c ? CONSENT_SIGNALS.filter((k) => typeof c[k] === "string") : [];
|
|
182
119
|
return keys.length ? keys.map((k) => consentChip(t, k, c[k])).join("") : `<span class="chip chip-consent none" title="${esc(t.noSignalTitle)}">${esc(t.noSignal)}</span>`;
|
|
183
120
|
};
|
|
184
121
|
const missingChip = (m, k) => `<span class="chip chip-sev broken jr-miss"><span class="k">${esc(k)}</span>${esc(m.ui.missing)}</span>`;
|
|
@@ -339,6 +276,27 @@ function parityNote(m, run, step, parity) {
|
|
|
339
276
|
}).join("");
|
|
340
277
|
}
|
|
341
278
|
|
|
279
|
+
/** The step's cookie changes by name, and what the analytics and ad cookies say when the step changed it. */
|
|
280
|
+
function cookiePane(m, run, step) {
|
|
281
|
+
const changes = (run.cookieChanges ?? []).filter((c) => c.step === step.index);
|
|
282
|
+
const id = step.identity;
|
|
283
|
+
const before = (run.steps ?? []).filter((st) => st.index < step.index && st.identity).at(-1)?.identity;
|
|
284
|
+
const facts = [];
|
|
285
|
+
if (id && (changes.length || JSON.stringify(id) !== JSON.stringify(before))) {
|
|
286
|
+
if (id.ga_client_id) {
|
|
287
|
+
const prev = before?.ga_client_id;
|
|
288
|
+
const since = (run.steps ?? []).find((st) => st.identity?.ga_client_id === id.ga_client_id)?.index;
|
|
289
|
+
facts.push(m.ui.gaClientId(id.ga_client_id, prev ? since : null, prev && prev !== id.ga_client_id ? prev : null));
|
|
290
|
+
}
|
|
291
|
+
if (id.gcl_aw) facts.push(m.ui.gclAw(id.gcl_aw === "landing"));
|
|
292
|
+
if (id.fbc) facts.push(m.ui.fbc(id.fbc === "landing"));
|
|
293
|
+
}
|
|
294
|
+
const table = changes.length
|
|
295
|
+
? `<table class="tbl jr-req" aria-label="${esc(m.ui.tabCookies)}"><thead><tr><th scope="col">${esc(m.ui.cookieCol)}</th><th scope="col">${esc(m.ui.domainCol)}</th><th scope="col">${esc(m.ui.changeCol)}</th></tr></thead><tbody>${changes.map((c) => `<tr><td><code>${esc(c.name)}</code></td><td class="mono xs">${esc(c.domain)}</td><td>${esc(m.ui.cookieChange[c.change] ?? c.change)}</td></tr>`).join("")}</tbody></table>`
|
|
296
|
+
: "";
|
|
297
|
+
return { count: changes.length, live: changes.length > 0 || facts.length > 0, html: `${facts.map((f) => `<p class="xs jr-note-s">${esc(f)}</p>`).join("")}${table ? `<div class="tscroll">${table}</div>` : ""}<p class="muted xs jr-note-s">${esc(m.ui.cookieNamesOnly)}</p>` };
|
|
298
|
+
}
|
|
299
|
+
|
|
342
300
|
function stepPane(ctx, t, rows, run, ri, step, sev, selected) {
|
|
343
301
|
const { m } = ctx;
|
|
344
302
|
const id = stepAnchor(ri, step.index);
|
|
@@ -347,15 +305,19 @@ function stepPane(ctx, t, rows, run, ri, step, sev, selected) {
|
|
|
347
305
|
const calls = requestsOf(run, step.index);
|
|
348
306
|
const skipped = skippedOf(run, step.index);
|
|
349
307
|
const skippedN = skipped.reduce((a, [, c]) => a + c, 0);
|
|
308
|
+
const cookies = cookiePane(m, run, step);
|
|
350
309
|
const meta = [step.pagePath ? pathOf(step.pagePath) : null, ...actionText(m, step), secs(run, step.t)].filter(Boolean).map((x) => `<span>${esc(x)}</span>`).join("");
|
|
351
310
|
const reqNotes = `${skippedN ? `<p class="muted xs jr-note-s">${esc(m.ui.skippedInStep(skipped.map(([v, c]) => `${v} ${c}`).join(", "), skippedN))}</p>` : ""}${calls.some((c) => c.bodyDropped) ? `<p class="muted xs jr-note-s">${esc(m.ui.bodyDropped)}</p>` : ""}`;
|
|
352
311
|
const tabs = [
|
|
353
312
|
["hits", esc(m.html.hitsCol), m.html.hitsInStep, hits.length, hits.length ? `<div class="jr-list">${hits.map((h, k) => hitCard(ctx, t, rows, run, ri, step, h, k)).join("")}</div>` : `<p class="muted xs jr-empty">${esc(m.ui.noHits)}</p>`],
|
|
354
313
|
["dl", esc(t.dataLayer), t.dataLayer, pushes.length, pushes.length ? `<div class="jr-list">${pushes.map((e, k) => pushCard(ctx, t, run, ri, step, e, k)).join("")}</div>` : `<p class="muted xs jr-empty">${esc(m.ui.noPushes)}</p>`],
|
|
355
314
|
["req", esc(m.ui.tabNetwork), m.ui.tabNetwork, calls.length, `${calls.length ? requestTable(ctx, t, run, ri, step, calls) : `<p class="muted xs jr-empty">${esc(m.ui.noRequests)}</p>`}${reqNotes}`],
|
|
315
|
+
// Captures from before cookies were tracked per step have no cookieChanges, and no tab for them.
|
|
316
|
+
...(run.cookieChanges ? [["ck", esc(m.ui.tabCookies), m.ui.tabCookies, cookies.count, cookies.html]] : []),
|
|
356
317
|
];
|
|
357
|
-
// A tab with nothing in it is shown but disabled; the requests tab stays open when it only has the skipped-requests note
|
|
358
|
-
|
|
318
|
+
// A tab with nothing in it is shown but disabled; the requests tab stays open when it only has the skipped-requests note,
|
|
319
|
+
// and the cookies tab when it only says what the click id and client id cookies hold.
|
|
320
|
+
const live = (k, c) => c > 0 || (k === "req" && Boolean(reqNotes)) || (k === "ck" && cookies.live);
|
|
359
321
|
const first = tabs.find(([k, , , c]) => live(k, c));
|
|
360
322
|
const body = first
|
|
361
323
|
? `<div class="tabs jr-tabs" data-tabs><div role="tablist" aria-label="${esc(t.stepOf(step.index))}">${tabs.map(([k, label, , c]) => `<button type="button" role="tab" aria-controls="${id}-${k}" aria-selected="${first[0] === k}"${live(k, c) ? "" : " disabled"}>${label} ${n(c)}</button>`).join("")}</div>
|
package/src/html/scope.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// handling (which runs check which platform, the funnel cells, the inventory's parties and cookies, the skipped
|
|
4
4
|
// vendors, the trend) is the 0.17 renderer's, so the numbers match the Markdown report and the CLI.
|
|
5
5
|
import { buildInventory } from "../inventory.js";
|
|
6
|
+
import { containerRows } from "../containers.js";
|
|
6
7
|
import { providerOf } from "../parsers.js";
|
|
7
8
|
import { siteOf } from "../redact.js";
|
|
8
9
|
import { VERSION, BUCKET_ORDER, coverageLines, pcSort, planChangeItems, routeList } from "../report-shared.js";
|
|
@@ -193,12 +194,24 @@ function inventoryLines(ctx) {
|
|
|
193
194
|
return { lines, otherCookies: inv.otherCookies };
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
/** The Google containers card: one line per container and the way it reached the page. Null when none was recorded. */
|
|
198
|
+
function containersCard(ctx) {
|
|
199
|
+
const { m, runs } = ctx;
|
|
200
|
+
const rows = containerRows(runs);
|
|
201
|
+
if (!rows.length) return null;
|
|
202
|
+
const c = m.ui.containers;
|
|
203
|
+
const items = rows.map((r) => `<li>${code(r.id)}${plain(esc(c.script[r.script]))}<span>${esc(c.kinds[r.kind](r.by))}</span>${plain(`${esc(m.ui.layerCol)} ${code(r.layer)}`)}${plain(esc(r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`))}${r.environment ? badge("warning", c.environment(r.environment)) : ""}<span class="inv-n">${m.html.scope.pages(r.pages.length, bold(r.pages.length))}</span></li>`).join("");
|
|
204
|
+
const ids = uniq(rows.map((r) => r.id));
|
|
205
|
+
return `<li><details class="card inv" id="inv-containers"><summary><span class="t">${esc(m.ui.containersTitle)}</span>${plain(n(ids.length))}<span class="inv-m">${esc(ids.slice(0, 3).join(", "))}${ids.length > 3 ? " …" : ""}</span></summary><div class="body"><p class="xs muted">${esc(m.ui.containersIntro)}</p><ul class="inv-list">${items}</ul></div></details></li>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
196
208
|
function inventorySection(ctx) {
|
|
197
209
|
const { m, runs } = ctx;
|
|
198
210
|
const t = m.html.scope;
|
|
199
211
|
const { lines, otherCookies } = inventoryLines(ctx);
|
|
200
212
|
const skipped = skippedTotals(runs);
|
|
201
|
-
|
|
213
|
+
const containers = containersCard(ctx);
|
|
214
|
+
if (!lines.length && !skipped.length && !containers) return sectionBlock("inventory", esc(m.html.shell.sections.inventory), `<p class="muted">${esc(m.ui.noInventory)}</p>`);
|
|
202
215
|
const counts = (x) => `<span class="inv-m">${t.requests(x.requests, bold(x.requests))}</span><span class="inv-m">${t.pages(x.pages.size, bold(x.pages.size))}</span><span class="inv-m${x.cookies.length ? "" : " zero"}">${t.cookies(x.cookies.length, bold(x.cookies.length))}</span>`;
|
|
203
216
|
const out = lines.map((x) => {
|
|
204
217
|
const id = `inv-${safeKey(x.p)}`;
|
|
@@ -214,6 +227,7 @@ function inventorySection(ctx) {
|
|
|
214
227
|
<div role="tabpanel" id="${tid("pages")}" data-label="${esc(m.ui.pagesCol)}">${pages ? `<ul class="inv-list">${pages}</ul>` : none}</div>
|
|
215
228
|
<div role="tabpanel" id="${tid("cookies")}" data-label="${esc(m.ui.cookiesCol)}">${x.cookies.length ? `<ul class="inv-list">${x.cookies.map((ck) => `<li>${code(ck)}</li>`).join("")}</ul>` : none}</div></div></div></details></li>`;
|
|
216
229
|
});
|
|
230
|
+
if (containers) out.unshift(containers);
|
|
217
231
|
const skippedN = skipped.reduce((a, [, k]) => a + k, 0);
|
|
218
232
|
if (skipped.length) {
|
|
219
233
|
out.push(`<li><details class="card inv" id="inv-out-of-scope"><summary><span class="t">${esc(m.ui.outOfScopeStatus)}</span>${plain(t.requests(skippedN, bold(skippedN)))}<span class="inv-m">${esc(skipped.slice(0, 3).map(([v]) => v).join(", "))}${skipped.length > 3 ? " …" : ""}</span></summary><div class="body"><ul class="inv-list">${skipped.map(([v, k]) => `<li><span>${esc(v)}</span>${badge("muted", m.ui.outOfScopeStatus)}<span class="inv-n">${t.requests(k, bold(k))}</span></li>`).join("")}</ul></div></details></li>`);
|
package/src/html/summary.js
CHANGED
|
@@ -172,7 +172,7 @@ function scorecard(ctx) {
|
|
|
172
172
|
<p class="sc-verdict sev-${sev}">${use(`sev-${sev}`, 26)}${esc(verdict.statusText)}</p>
|
|
173
173
|
<p class="sc-head">${esc(headline)}</p>
|
|
174
174
|
${aiLine(ctx)}${costLines(ctx, noScore)}
|
|
175
|
-
${statusChips(ctx)}${accepted > 0 ? `\n<p class="meta accepted-line">${esc(m.ui.acceptedLine(accepted))}</p>` : ""}
|
|
175
|
+
${statusChips(ctx)}${accepted > 0 ? `\n<p class="meta accepted-line">${esc(m.ui.acceptedLine(accepted))}</p>` : ""}${ctx.runs.some((r) => r.live) ? `\n<p class="meta sev-warning">${use("sev-warning", 14)} ${esc(m.ui.liveRun)}</p>` : ""}
|
|
176
176
|
</div>`;
|
|
177
177
|
}
|
|
178
178
|
|