@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.
@@ -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, isEventParam, paramTip } from "../glossary.js";
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
- /** The network request that carried the hit. Identical hits (duplicates) are paired with identical requests in order. */
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 ? ["ad_storage", "analytics_storage"].filter((k) => typeof c[k] === "string") : [];
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, stepCalls.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("")
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
- const live = (k, c) => c > 0 || (k === "req" && Boolean(reqNotes));
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>
@@ -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