@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/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,51 +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
|
-
const calls = (run.network ?? []).filter((c) => c.step === hit.step && c.url === hit.url);
|
|
69
|
-
const twins = run.hits.filter((h) => h.step === hit.step && h.url === hit.url);
|
|
70
|
-
return twins.length > 1 && calls.length === twins.length ? [calls[twins.indexOf(hit)]] : calls;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/* ---------- request parameters, moved from the 0.17 renderer ---------- */
|
|
74
|
-
|
|
75
|
-
/** Nested JSON as dotted keys (properties.value); arrays stay JSON text. */
|
|
76
|
-
function flatten(obj, prefix = "", out = []) {
|
|
77
|
-
for (const [k, v] of Object.entries(obj ?? {})) {
|
|
78
|
-
const key = prefix ? `${prefix}.${k}` : k;
|
|
79
|
-
if (v && typeof v === "object" && !Array.isArray(v) && prefix.split(".").length < 3) flatten(v, key, out);
|
|
80
|
-
else out.push([key, cell(v)]);
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** 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. */
|
|
86
|
-
function paramSets(call) {
|
|
87
|
-
let u;
|
|
88
|
-
try {
|
|
89
|
-
u = new URL(call.url);
|
|
90
|
-
} catch {
|
|
91
|
-
return [[]];
|
|
92
|
-
}
|
|
93
|
-
const query = [...u.searchParams];
|
|
94
|
-
const body = call.postData;
|
|
95
|
-
if (!body) return [query];
|
|
96
|
-
const trimmed = body.trim();
|
|
97
|
-
if (trimmed.startsWith("{")) {
|
|
98
|
-
try {
|
|
99
|
-
return [[...query, ...flatten(JSON.parse(trimmed))]];
|
|
100
|
-
} catch { /* not JSON, or cut at the size limit */ }
|
|
101
|
-
}
|
|
102
|
-
if (/^[^\s{]*=/.test(trimmed)) {
|
|
103
|
-
return trimmed.split("\n").filter(Boolean).map((line) => {
|
|
104
|
-
const merged = new Map(query);
|
|
105
|
-
for (const [k, v] of new URLSearchParams(line)) merged.set(k, v);
|
|
106
|
-
return [...merged];
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return [query];
|
|
110
|
-
}
|
|
67
|
+
/* ---------- request parameters ---------- */
|
|
111
68
|
|
|
112
69
|
/** Parameter rows with a glossary tip: the "?" shows the tip on hover and takes no focus; screen readers read the hidden span. */
|
|
113
70
|
function paramRows(lang, entries) {
|
|
@@ -118,24 +75,6 @@ function paramRows(lang, entries) {
|
|
|
118
75
|
}).join("");
|
|
119
76
|
}
|
|
120
77
|
|
|
121
|
-
/**
|
|
122
|
-
* What a hit card shows as its parameters: the request's own parameter set (raw keys, with glossary tips), split
|
|
123
|
-
* into the event's parameters and the session, device, page and consent ones. A GA4 batch carries one set per
|
|
124
|
-
* event; the hit takes the set with its name. Without a captured request, the hit's parsed parameters.
|
|
125
|
-
*/
|
|
126
|
-
function hitParams(run, hit) {
|
|
127
|
-
const call = callsForHit(run, hit)[0] ?? null;
|
|
128
|
-
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: [] };
|
|
129
|
-
const sets = paramSets(call);
|
|
130
|
-
let i = 0;
|
|
131
|
-
if (sets.length > 1) {
|
|
132
|
-
const names = (parseTrackingRequest(call.url, call.postData ?? null) ?? []).map((h) => h.name);
|
|
133
|
-
i = Math.max(0, names.indexOf(hit.name));
|
|
134
|
-
}
|
|
135
|
-
const entries = sets[i] ?? [];
|
|
136
|
-
return { call, batch: sets.length > 1 ? [i + 1, sets.length] : null, event: entries.filter(([k]) => isEventParam(k)), context: entries.filter(([k]) => !isEventParam(k)) };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
78
|
/* ---------- items: GA4 item strings and dataLayer item objects as one table ---------- */
|
|
140
79
|
|
|
141
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"]);
|
|
@@ -176,7 +115,7 @@ function consentChip(t, key, v) {
|
|
|
176
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>`;
|
|
177
116
|
}
|
|
178
117
|
const hitConsent = (t, c) => {
|
|
179
|
-
const keys = c ?
|
|
118
|
+
const keys = c ? CONSENT_SIGNALS.filter((k) => typeof c[k] === "string") : [];
|
|
180
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>`;
|
|
181
120
|
};
|
|
182
121
|
const missingChip = (m, k) => `<span class="chip chip-sev broken jr-miss"><span class="k">${esc(k)}</span>${esc(m.ui.missing)}</span>`;
|
|
@@ -216,7 +155,7 @@ function runState(ctx, rows, run) {
|
|
|
216
155
|
/* ---------- step pane: hits ---------- */
|
|
217
156
|
|
|
218
157
|
/** The 1–3 parameters a practitioner scans first: missing required ones, value, items, the order id; a page title when there is nothing else. */
|
|
219
|
-
function keyBits(ctx, t, hit, flag) {
|
|
158
|
+
function keyBits(ctx, t, hit, flag, run) {
|
|
220
159
|
const { m } = ctx;
|
|
221
160
|
const p = hit.params ?? {};
|
|
222
161
|
const out = [];
|
|
@@ -228,6 +167,7 @@ function keyBits(ctx, t, hit, flag) {
|
|
|
228
167
|
if (p.transaction_id) out.push(kv("transaction_id", p.transaction_id));
|
|
229
168
|
if (hit.delivery === "server") out.push(`<span class="chip plain">${esc(m.ui.serverSideTag)}</span>`);
|
|
230
169
|
if (!out.length && p.page_title) out.push(kv("", p.page_title));
|
|
170
|
+
if (hit.late) out.unshift(`<span class="chip plain" title="${esc(m.ui.lateHitTip((run?.steps ?? []).find((s) => s.index === hit.late.during)?.label ?? hit.late.during))}">${esc(m.ui.lateHit(hit.late.ms))}</span>`);
|
|
231
171
|
if (!out.length) return `<span class="muted">${esc(m.ui.noParameters)}</span>`;
|
|
232
172
|
return out.slice(0, 3).join("");
|
|
233
173
|
}
|
|
@@ -261,10 +201,10 @@ function hitCard(ctx, t, rows, run, ri, step, hit, k) {
|
|
|
261
201
|
const label = PROVIDERS[safeKey(hit.platform)] ?? platformLabel(hit.platform);
|
|
262
202
|
const calls = callsForHit(run, hit);
|
|
263
203
|
const reqLine = calls.length
|
|
264
|
-
? calls.map((c) => `<a class="mono" href="#${requestId(ri, step.index,
|
|
204
|
+
? calls.map((c) => `<a class="mono" href="#${requestId(ri, c.step, (c.step === step.index ? stepCalls : requestsOf(run, c.step)).indexOf(c))}">${esc(c.method ?? "")} ${esc(hostPath(c.url))} ${esc(c.status ?? "")}</a>${c.blocked ? `<span class="muted">${esc(m.html.blockedBy(label))}</span>` : ""}`).join("")
|
|
265
205
|
: `<span class="mono">${esc(hostPath(hit.url))}</span>`;
|
|
266
206
|
const card = flag ? ids.get(flag) : null;
|
|
267
|
-
return `<details class="card jr-hit${sev ? ` sev-${sev}` : ""}" id="${id}"${sev ? ` data-flagged="${sev}" data-print-open` : ""}><summary><span class="jr-c-p">${platChip(hit.platform)}</span><code class="jr-ev">${esc(hit.name)}</code><span class="jr-c-k">${keyBits(ctx, t, hit, flag)}</span><span class="jr-c-c">${hitConsent(t, hit.consent)}</span><span class="jr-c-s">${sev ? badge(sev, m.severity[sev]) : ""}</span><span class="jr-tm">${esc(secs(run, hit.t))}</span></summary>
|
|
207
|
+
return `<details class="card jr-hit${sev ? ` sev-${sev}` : ""}" id="${id}"${sev ? ` data-flagged="${sev}" data-print-open` : ""}><summary><span class="jr-c-p">${platChip(hit.platform)}</span><code class="jr-ev">${esc(hit.name)}</code><span class="jr-c-k">${keyBits(ctx, t, hit, flag, run)}</span><span class="jr-c-c">${hitConsent(t, hit.consent)}</span><span class="jr-c-s">${sev ? badge(sev, m.severity[sev]) : ""}</span><span class="jr-tm">${esc(secs(run, hit.t))}</span></summary>
|
|
268
208
|
<div class="body">${flag ? `<p class="jr-flag sev-${sev}">${use(`sev-${sev}`, 14)}<span>${detailHtml(flag.detail)}</span>${card ? `<a href="#${card}">${esc(m.html.findingLink)}</a>` : ""}</p>` : ""}
|
|
269
209
|
<div class="jr-hb"><div><h4 class="jr-h">${esc(m.ui.parameters)}${n(event.length + missing.length)}</h4>${batch ? `<p class="muted xs jr-batch">${esc(m.ui.inBatch(batch[0], batch[1]))}</p>` : ""}${paramTable}${ctxTable}</div>
|
|
270
210
|
${items.length ? `<div><h4 class="jr-h">${esc(m.ui.items)}${n(items.length)}</h4>${itemsTable(m, t, items)}</div>` : ""}
|
|
@@ -336,6 +276,27 @@ function parityNote(m, run, step, parity) {
|
|
|
336
276
|
}).join("");
|
|
337
277
|
}
|
|
338
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
|
+
|
|
339
300
|
function stepPane(ctx, t, rows, run, ri, step, sev, selected) {
|
|
340
301
|
const { m } = ctx;
|
|
341
302
|
const id = stepAnchor(ri, step.index);
|
|
@@ -344,15 +305,19 @@ function stepPane(ctx, t, rows, run, ri, step, sev, selected) {
|
|
|
344
305
|
const calls = requestsOf(run, step.index);
|
|
345
306
|
const skipped = skippedOf(run, step.index);
|
|
346
307
|
const skippedN = skipped.reduce((a, [, c]) => a + c, 0);
|
|
308
|
+
const cookies = cookiePane(m, run, step);
|
|
347
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("");
|
|
348
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>` : ""}`;
|
|
349
311
|
const tabs = [
|
|
350
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>`],
|
|
351
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>`],
|
|
352
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]] : []),
|
|
353
317
|
];
|
|
354
|
-
// A tab with nothing in it is shown but disabled; the requests tab stays open when it only has the skipped-requests note
|
|
355
|
-
|
|
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);
|
|
356
321
|
const first = tabs.find(([k, , , c]) => live(k, c));
|
|
357
322
|
const body = first
|
|
358
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/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
|
|