@analitikisler/ai-tag-debugger 0.11.3 → 0.13.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 +19 -0
- package/README.md +9 -8
- package/README.tr.md +9 -8
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/audit.js +11 -7
- package/src/brand.js +3 -3
- package/src/browser.js +27 -5
- package/src/changes.js +29 -1
- package/src/checks.js +7 -3
- package/src/i18n.js +56 -2
- package/src/mcp.js +3 -3
- package/src/parsers.js +184 -33
- package/src/redact.js +20 -3
- package/src/report.js +144 -35
- package/src/verify.js +48 -0
package/src/report.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OWNERS, ownerOf, verifyLinks } from "./verify.js";
|
|
1
2
|
import { createProvider } from "./providers.js";
|
|
2
3
|
import { groupFindings, hitMatchesEvent, summarize } from "./checks.js";
|
|
3
4
|
import { messages } from "./i18n.js";
|
|
@@ -149,11 +150,13 @@ const formatPush = (value) => (isConsentEntry(value) ? `gtag(${value.map((v) =>
|
|
|
149
150
|
const mdText = (v) => String(v ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
150
151
|
const mdCode = (v) => String(v ?? "").replace(/`/g, "'").replace(/\n/g, " ");
|
|
151
152
|
|
|
153
|
+
const routeList = (ch) => (ch?.routes ?? []).map((r) => `${r.journeyId} (${r.viewport})`).join(", ");
|
|
154
|
+
|
|
152
155
|
export function renderMarkdown({ title, generatedAt, findings, explained, runs, lang = "en", branding = true, against = null, planCheck = null, changes = null }) {
|
|
153
156
|
const m = messages(lang);
|
|
154
157
|
const rows = attachExplanations(findings, explained);
|
|
155
158
|
const counts = summarize(rows);
|
|
156
|
-
const lines = [`# ${mdText(title)}`, "", m.ui.generated(generatedAt), "", ...(against ? [mdText(m.ui.checkedAgainst(against)), ""] : []), `**${m.ui.counts(counts)}**`, ""];
|
|
159
|
+
const lines = [`# ${mdText(title)}`, "", m.ui.generated(generatedAt), "", ...(against ? [mdText(m.ui.checkedAgainst(against)), ""] : []), ...(changes?.routes?.length ? [mdText(m.ui.routeChanged(routeList(changes))), ""] : []), `**${m.ui.counts(counts)}**`, ""];
|
|
157
160
|
if (changes) lines.push(...changesMd(m, changes));
|
|
158
161
|
if (planCheck) lines.push(...planVsSiteMd(m, planCheck));
|
|
159
162
|
const sum = summaryParts(explained?.summary);
|
|
@@ -166,6 +169,9 @@ export function renderMarkdown({ title, generatedAt, findings, explained, runs,
|
|
|
166
169
|
for (const f of rows.filter((r) => r.severity !== "ok")) {
|
|
167
170
|
lines.push(`### ${m.severity[f.severity]}: ${mdText(f.detail)}`, "");
|
|
168
171
|
lines.push(`- ${m.ui.where}: ${mdText(f.where.map((w) => m.ui.whereStep(w.journey, w.viewport, w.step)).join("; "))}`);
|
|
172
|
+
lines.push(`- ${m.ui.owners[ownerOf(f)]}`);
|
|
173
|
+
const vl = verifyLinks(f);
|
|
174
|
+
if (vl.length) lines.push(`- ${m.ui.verify}: ${vl.map((l) => `[${l.name}](${l.url})`).join(", ")}`);
|
|
169
175
|
if (f.evidence) lines.push(`- ${m.ui.captured}: \`${mdCode(f.evidence)}\``);
|
|
170
176
|
if (f.explanation) {
|
|
171
177
|
if (f.explanation.impact) lines.push(`- ${m.ui.impact}: ${mdText(f.explanation.impact)}`);
|
|
@@ -194,7 +200,7 @@ export function renderMarkdown({ title, generatedAt, findings, explained, runs,
|
|
|
194
200
|
return lines.join("\n");
|
|
195
201
|
}
|
|
196
202
|
|
|
197
|
-
const PLATFORM_ORDER = ["ga4", "google_ads", "meta", "tiktok", "x", "pinterest", "snapchat", "klaviyo", "hotjar"];
|
|
203
|
+
const PLATFORM_ORDER = ["ga4", "google_ads", "meta", "tiktok", "x", "linkedin", "microsoft_ads", "pinterest", "snapchat", "criteo", "klaviyo", "adobe", "matomo", "piwik_pro", "clarity", "hotjar"];
|
|
198
204
|
export const platformLabel = (p) => PROVIDERS[p] ?? String(p).replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
199
205
|
const platformRank = (p) => (PLATFORM_ORDER.includes(p) ? PLATFORM_ORDER.indexOf(p) : PLATFORM_ORDER.length);
|
|
200
206
|
|
|
@@ -203,6 +209,7 @@ const EVENT_ALIASES = {
|
|
|
203
209
|
pageview: "pageview", viewcontent: "viewitem", addtocart: "addtocart", addtowishlist: "addtowishlist", initiatecheckout: "begincheckout",
|
|
204
210
|
addpaymentinfo: "addpaymentinfo", purchase: "purchase", completepayment: "purchase", placeanorder: "purchase", lead: "generatelead",
|
|
205
211
|
completeregistration: "signup", search: "search", contact: "generatelead", submitform: "generatelead", subscribe: "subscribe", starttrial: "starttrial",
|
|
212
|
+
viewbasket: "viewcart", tracktransaction: "purchase",
|
|
206
213
|
};
|
|
207
214
|
const normName = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
208
215
|
const eventKey = (s) => EVENT_ALIASES[normName(s)] ?? normName(s);
|
|
@@ -468,6 +475,37 @@ function whereTarget(runs, w, f) {
|
|
|
468
475
|
return { ri, step };
|
|
469
476
|
}
|
|
470
477
|
|
|
478
|
+
/** The push a hit came from: the latest matching push at or before the hit, else the first match. */
|
|
479
|
+
function sourcePush(run, hit) {
|
|
480
|
+
const pushes = pushesForHit(run, hit);
|
|
481
|
+
return pushes.filter((e) => e.t <= hit.t).at(-1) ?? pushes[0] ?? null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** The step in order: each dataLayer push, the GTM event it raised, then the platform hits it caused. Hits with no push come last. */
|
|
485
|
+
function timelinePanel(m, findings, run, step, pushes, hits) {
|
|
486
|
+
const bySource = new Map();
|
|
487
|
+
const orphans = [];
|
|
488
|
+
for (const h of [...hits].sort((a, b) => a.t - b.t)) {
|
|
489
|
+
const src = sourcePush(run, h);
|
|
490
|
+
if (src && src.step === step.index) {
|
|
491
|
+
if (!bySource.has(src)) bySource.set(src, []);
|
|
492
|
+
bySource.get(src).push(h);
|
|
493
|
+
} else orphans.push(h);
|
|
494
|
+
}
|
|
495
|
+
const hitList = (list) => (list.length ? `<ul class="evlist tl-hits">${list.map((h) => eventRow(m, findings, run, step, h)).join("")}</ul>` : `<p class="muted xs tl-none">${m.ui.timelineNoHits}</p>`);
|
|
496
|
+
const nodes = [...pushes].sort((a, b) => a.t - b.t).map((e) => {
|
|
497
|
+
const name = pushEvent(e.value);
|
|
498
|
+
const gtm = isConsentEntry(e.value)
|
|
499
|
+
? `<span class="tl-k">${m.ui.timelineConsent}</span><code>${escapeHtml(`consent ${e.value[1] ?? ""}`.trim())}</code>`
|
|
500
|
+
: typeof name === "string" && name
|
|
501
|
+
? `<span class="tl-k">${m.ui.timelineGtmEvent}</span><code>${escapeHtml(name)}</code>`
|
|
502
|
+
: `<span class="tl-k muted">${m.ui.timelineNoEvent}</span>`;
|
|
503
|
+
return `<li class="tl-node"><div class="tl-lbl">dataLayer</div><ul class="evlist">${pushRow(m, run, e)}</ul><div class="tl-gtm">${gtm}</div>${hitList(bySource.get(e) ?? [])}</li>`;
|
|
504
|
+
});
|
|
505
|
+
if (orphans.length) nodes.push(`<li class="tl-node tl-orphan"><div class="tl-lbl">${m.ui.timelineOther}</div>${hitList(orphans)}</li>`);
|
|
506
|
+
return nodes.length ? `<ol class="timeline">${nodes.join("")}</ol>` : `<p class="empty">${m.ui.noHits}</p>`;
|
|
507
|
+
}
|
|
508
|
+
|
|
471
509
|
function stepDetail(m, lang, findings, run, ri, step, isDefault) {
|
|
472
510
|
const hits = run.hits.filter((h) => h.step === step.index);
|
|
473
511
|
const id = `s${ri}-${step.index}`;
|
|
@@ -480,7 +518,8 @@ function stepDetail(m, lang, findings, run, ri, step, isDefault) {
|
|
|
480
518
|
const tab = (n, label, count) => `<input type="radio" name="${id}" id="${id}-${n}"${n === 1 ? " checked" : ""}><label for="${id}-${n}">${label} <span class="n">${count}</span></label>`;
|
|
481
519
|
return `<section class="sdetail${isDefault ? " default" : ""}" id="${stepAnchor(ri, step.index)}" aria-label="${escapeHtml(step.label)}">
|
|
482
520
|
<header class="shead"><span class="sidx">${step.index}</span><div><h3>${escapeHtml(step.label)}</h3><div class="muted xs">${escapeHtml(m.ui.stepMeta(seconds(run, step.t), hits.length, pushes.length, skippedN))}${step.forced ? ` · ${m.ui.clickedThrough}` : ""}</div></div></header>
|
|
483
|
-
<div class="tabs seg">${tab(1, m.ui.tabEvents, eventRows.length)}${tab(
|
|
521
|
+
<div class="tabs seg">${tab(1, m.ui.tabTimeline, pushes.length + hits.length)}${tab(2, m.ui.tabEvents, eventRows.length)}${tab(3, "dataLayer", pushes.length)}${tab(4, m.ui.tabNetwork, reqRows.length)}
|
|
522
|
+
<div class="panel">${timelinePanel(m, findings, run, step, pushes, hits)}</div>
|
|
484
523
|
<div class="panel">${eventRows.length ? filtered(m, `${id}e`, eventRows, eventRows.map((r) => r.html).join("")) : `<p class="empty">${m.ui.noHits}</p>`}</div>
|
|
485
524
|
<div class="panel">${pushes.length ? `<ul class="evlist">${pushes.map((e) => pushRow(m, run, e)).join("")}</ul>` : `<p class="empty">${m.ui.noPushes}</p>`}</div>
|
|
486
525
|
<div class="panel">${reqRows.length ? filtered(m, `${id}n`, reqRows, reqRows.map((r) => r.html).join("")) : `<p class="empty">${m.ui.noRequests}</p>`}${skippedN ? `<p class="muted xs scope-note">${escapeHtml(m.ui.skippedInStep(skipped.map(([v, n]) => `${v} ${n}`).join(", "), skippedN))}</p>` : ""}</div>
|
|
@@ -488,6 +527,19 @@ function stepDetail(m, lang, findings, run, ri, step, isDefault) {
|
|
|
488
527
|
}
|
|
489
528
|
|
|
490
529
|
/** One journey: a sticky step list on the left and the selected step on the right. It opens on the first step with a problem. */
|
|
530
|
+
/** Every Consent Mode command of a journey (default, then updates), one tile per signal. */
|
|
531
|
+
function consentCard(m, run) {
|
|
532
|
+
const cmds = run.dataLayerLog.filter((e) => isConsentEntry(e.value) && (e.value[1] === "default" || e.value[1] === "update"));
|
|
533
|
+
const head = `<div class="card-h"><div class="card-t">${m.ui.consent}</div><div class="card-d">${cmds.length ? m.ui.consentLead : m.ui.consentNone}</div></div>`;
|
|
534
|
+
if (!cmds.length) return `<div class="card jconsent">${head}<div class="card-c"></div></div>`;
|
|
535
|
+
const items = cmds.map((e) => {
|
|
536
|
+
const step = run.steps.find((s) => s.index === e.step);
|
|
537
|
+
const sigs = Object.entries(e.value[2] ?? {}).filter(([, v]) => typeof v === "string");
|
|
538
|
+
return `<li><div class="ch"><b>${e.value[1] === "default" ? m.ui.consentDefault : m.ui.consentUpdate}</b><span class="muted xs">${step ? `${step.index} · ${escapeHtml(step.label)}` : ""}</span></div><div class="sigs">${sigs.map(([k, v]) => `<span class="sig"><span>${escapeHtml(k)}</span><span class="${v === "granted" ? "granted" : "denied"}">${escapeHtml(v)}</span></span>`).join("")}</div></li>`;
|
|
539
|
+
});
|
|
540
|
+
return `<div class="card jconsent">${head}<div class="card-c"><ol class="ctl">${items.join("")}</ol></div></div>`;
|
|
541
|
+
}
|
|
542
|
+
|
|
491
543
|
function journeyPanel(m, lang, findings, run, ri, flagged) {
|
|
492
544
|
const steps = run.steps.filter((s) => s.index > 0);
|
|
493
545
|
const def = steps.find((s) => flagged.has(stepAnchor(ri, s.index))) ?? steps[0];
|
|
@@ -503,7 +555,7 @@ function journeyPanel(m, lang, findings, run, ri, flagged) {
|
|
|
503
555
|
? `<div class="jgrid2"><nav class="stepnav" aria-label="${m.ui.stepsNav}"><ol>${nav}</ol></nav><div class="sdetails">${steps.map((s) => stepDetail(m, lang, findings, run, ri, s, s === def)).join("\n")}</div></div>`
|
|
504
556
|
: `<p class="empty">${m.ui.noSteps}</p>`;
|
|
505
557
|
return `<article class="jpanel" id="j${ri}"><header class="jhead"><div><h2>${escapeHtml(run.journeyId)} <span class="muted">· ${escapeHtml(run.viewport)}</span></h2>${run.note ? `<p class="muted xs">${escapeHtml(run.note)}</p>` : ""}</div>${run.completed ? badge("ok", m.ui.completed) : badge("warning", m.ui.notCompleted)}</header>
|
|
506
|
-
${body}</article>`;
|
|
558
|
+
${consentCard(m, run)}${body}</article>`;
|
|
507
559
|
}
|
|
508
560
|
|
|
509
561
|
function journeysTab(m, lang, findings, runs, flagged) {
|
|
@@ -521,7 +573,13 @@ function evidenceHtml(evidence) {
|
|
|
521
573
|
return `<pre>${escapeHtml(evidence)}</pre>`;
|
|
522
574
|
}
|
|
523
575
|
|
|
524
|
-
function
|
|
576
|
+
function verifyHtml(m, f) {
|
|
577
|
+
const links = verifyLinks(f);
|
|
578
|
+
if (!links.length) return "";
|
|
579
|
+
return `${sec("cause", m.ui.verify)}<ul class="verify">${links.map((l) => `<li><a href="${l.url}" target="_blank" rel="noopener noreferrer">${escapeHtml(l.name)}</a></li>`).join("")}</ul>`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function findingCard(m, runs, f, open, aiRan) {
|
|
525
583
|
const e = f.explanation;
|
|
526
584
|
const steps = fixSteps(e);
|
|
527
585
|
const journeysN = new Set(f.where.map((w) => `${w.journey}|${w.viewport}`)).size;
|
|
@@ -543,14 +601,14 @@ function findingCard(m, runs, f, open) {
|
|
|
543
601
|
<span class="fhead"><span class="ftitle">${detailHtml(f.detail)}</span><span class="fmeta">${f.platform ? `<span class="ptag p-${safeKey(f.platform)}">${escapeHtml(platformLabel(f.platform))}</span>` : ""}<span>${escapeHtml(m.ui.spread(journeysN, stepsN))}</span></span></span>
|
|
544
602
|
${badge(f.severity, m.severity[f.severity])}${icon.chevron}</summary>
|
|
545
603
|
<div class="fbody">${e?.impact ? `<div class="impact">${ic(ICON.impact, 16)}<div><span class="impact-l">${m.ui.impact}</span>${fmt(e.impact)}</div></div>` : ""}
|
|
546
|
-
<div class="fcols"><div class="fcol">${sec("cause", m.ui.likelyCause)}
|
|
547
|
-
<div class="fcol">${steps.length ? `${sec("fix", m.ui.fixSteps)}<ol class="fix-steps">${steps.map((t) => `<li><span>${fmt(t)}</span></li>`).join("")}</ol>` : ""}${sec("where", m.ui.where)}<div class="where">${chips}</div
|
|
604
|
+
<div class="fcols"><div class="fcol">${e?.likely_cause ? `${sec("cause", m.ui.likelyCause)}<p>${fmt(e.likely_cause)}</p>` : aiRan || f.severity === "info" ? "" : `${sec("cause", m.ui.likelyCause)}<p class="muted">${m.ui.noExplanation}</p>`}${evidence ? `${sec("evidence", m.ui.evidence)}${evidence}` : ""}</div>
|
|
605
|
+
<div class="fcol">${steps.length ? `${sec("fix", m.ui.fixSteps)}<ol class="fix-steps">${steps.map((t) => `<li><span>${fmt(t)}</span></li>`).join("")}</ol>` : ""}${sec("where", m.ui.where)}<div class="where">${chips}</div>${verifyHtml(m, f)}</div></div></div></details></li>`;
|
|
548
606
|
}
|
|
549
607
|
|
|
550
608
|
const FINDING_SEVERITIES = ["broken", "risk", "warning", "info"];
|
|
551
609
|
const SEV_COLOR = { broken: "var(--danger-dot)", risk: "var(--risk)", warning: "var(--warn-dot)", info: "var(--accent)" };
|
|
552
610
|
|
|
553
|
-
function findingsTab(m, runs, rows) {
|
|
611
|
+
function findingsTab(m, runs, rows, aiRan) {
|
|
554
612
|
const problems = rows.filter((f) => f.severity !== "ok");
|
|
555
613
|
if (!problems.length) return `<p class="empty">${m.ui.noProblems}</p>`;
|
|
556
614
|
const n = Object.fromEntries(FINDING_SEVERITIES.map((s) => [s, problems.filter((f) => f.severity === s).length]));
|
|
@@ -559,11 +617,21 @@ function findingsTab(m, runs, rows) {
|
|
|
559
617
|
const bar = present.map((s) => `<span style="flex:${n[s]};background:${SEV_COLOR[s]}"></span>`).join("");
|
|
560
618
|
const legend = present.map((s) => `<span><i style="background:${SEV_COLOR[s]}"></i>${m.severity[s]} <strong>${n[s]}</strong></span>`).join("");
|
|
561
619
|
const labels = [`<label for="fs-all">${m.ui.all} <span class="n">${problems.length}</span></label>`, ...present.map((s) => `<label for="fs-${s}">${m.severity[s]} <span class="n">${n[s]}</span></label>`)].join("");
|
|
562
|
-
|
|
620
|
+
let first = true;
|
|
621
|
+
const groups = OWNERS.map((o) => {
|
|
622
|
+
const mine = FINDING_SEVERITIES.flatMap((s) => problems.filter((f) => f.severity === s && ownerOf(f) === o));
|
|
623
|
+
if (!mine.length) return "";
|
|
624
|
+
const cards = mine.map((f) => {
|
|
625
|
+
const open = first && f.severity !== "info";
|
|
626
|
+
if (open) first = false;
|
|
627
|
+
return findingCard(m, runs, f, open, aiRan);
|
|
628
|
+
}).join("");
|
|
629
|
+
return `<section class="ogroup" data-owner="${o}"><h3 class="ogroup-h">${m.ui.owners[o]} <span class="n">${mine.length}</span><span class="muted xs">${m.ui.ownerHint[o]}</span></h3><ul class="flist">${cards}</ul></section>`;
|
|
630
|
+
}).join("");
|
|
563
631
|
return `<div class="filter-host">${radios}
|
|
564
632
|
<div class="sevbar" aria-hidden="true">${bar}</div><div class="sevlegend">${legend}</div>
|
|
565
633
|
<div class="toolbar"><div class="sevfilter" role="group" aria-label="${m.ui.filterBy}">${labels}</div><span class="muted xs">${m.ui.findingHint}</span></div>
|
|
566
|
-
|
|
634
|
+
${groups}</div>`;
|
|
567
635
|
}
|
|
568
636
|
|
|
569
637
|
function summaryCard(m, explained, problems) {
|
|
@@ -592,7 +660,23 @@ function skippedTotals(runs) {
|
|
|
592
660
|
|
|
593
661
|
const callProvider = (c) => safeKey(c.provider ?? providerOf(c.url)?.key ?? c.vendor);
|
|
594
662
|
|
|
595
|
-
|
|
663
|
+
/** The run's settings: platforms checked, AI model, plan, journeys, viewports, language and version. */
|
|
664
|
+
function runCard(m, runs, provs, { ai, plan, lang }) {
|
|
665
|
+
const uniq = (xs) => [...new Set(xs)];
|
|
666
|
+
const ptags = provs.map((k) => `<span class="ptag p-${safeKey(k)}">${escapeHtml(platformLabel(k))}</span>`).join("");
|
|
667
|
+
const rows = [
|
|
668
|
+
[m.ui.runPlatforms, ptags || '<span class="muted">–</span>'],
|
|
669
|
+
[m.ui.runAi, escapeHtml(ai ? m.ui.runAiOn(ai.name, ai.model) : m.ui.runAiOff)],
|
|
670
|
+
[m.ui.runPlan, escapeHtml(plan?.events?.length ? m.ui.runPlanEvents(plan.events.length) : m.ui.runNoPlan)],
|
|
671
|
+
[m.ui.runJourneys, escapeHtml(uniq(runs.map((r) => r.journeyId)).join(", ") || "–")],
|
|
672
|
+
[m.ui.runViewports, escapeHtml(uniq(runs.map((r) => r.viewport)).join(", ") || "–")],
|
|
673
|
+
[m.ui.runLanguage, escapeHtml(messages(lang).name)],
|
|
674
|
+
[m.ui.runVersion, `<code>${escapeHtml(VERSION)}</code>`],
|
|
675
|
+
];
|
|
676
|
+
return `<div class="card"><div class="card-h"><div class="card-t">${m.ui.runCard}</div></div><div class="card-c"><dl class="run">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("")}</dl></div></div>`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function overviewTab(m, findings, rows, counts, explained, runs, info) {
|
|
596
680
|
const problems = rows.filter((f) => f.severity !== "ok").length;
|
|
597
681
|
const metric = (sev) => `<div class="card metric ${sev}"><div class="k">${m.severity[sev]}</div><div class="v">${counts[sev]}</div></div>`;
|
|
598
682
|
const journeyRows = runs.map((r, ri) => {
|
|
@@ -607,7 +691,7 @@ function overviewTab(m, findings, rows, counts, explained, runs) {
|
|
|
607
691
|
<div class="ov-grid">${summaryCard(m, explained, problems)}
|
|
608
692
|
<div class="stack">
|
|
609
693
|
${runs.length ? `<div class="card"><div class="card-h"><div class="card-t">${m.ui.tabJourneys}</div></div><div class="card-c tight"><div class="tscroll"><table class="dt"><thead><tr><th>${m.ui.journey}</th><th>${m.ui.stepsCol}</th><th>${m.ui.problemsCol}</th></tr></thead><tbody>${journeyRows.join("")}</tbody></table></div></div></div>` : ""}
|
|
610
|
-
${
|
|
694
|
+
${runCard(m, runs, provs, info)}${skippedN ? `<p class="muted xs scope-skipped">${escapeHtml(m.ui.skippedLine(skipped.map(([v]) => v).join(", "), skippedN))}</p>` : ""}
|
|
611
695
|
</div></div></div>`;
|
|
612
696
|
}
|
|
613
697
|
|
|
@@ -633,6 +717,7 @@ function changesMd(m, ch) {
|
|
|
633
717
|
lines.push(`**${mdText(m.ui.changesLine(ch))}**`, "");
|
|
634
718
|
const items = changeItems(m, ch);
|
|
635
719
|
if (items.length) lines.push(...items.map((i) => `- ${i.tag}: ${mdText(i.text)}`), "");
|
|
720
|
+
if (ch.routes?.length) lines.push(...ch.routes.map((r) => `- ${mdText(`${r.journeyId} (${r.viewport}): ${r.before.join(" → ")} ⇒ ${r.after.join(" → ")}`)}`), "");
|
|
636
721
|
return lines;
|
|
637
722
|
}
|
|
638
723
|
|
|
@@ -640,7 +725,7 @@ function changesCard(m, ch) {
|
|
|
640
725
|
const head = `<div class="card-h"><div class="card-t">${m.ui.changesTitle}</div>${ch.previous ? `<div class="card-d">${escapeHtml(m.ui.changesLead(ch.previous, ch.mode))}</div>` : ""}</div>`;
|
|
641
726
|
if (!ch.previous) return `<div class="card" id="changes">${head}<div class="card-c"><p class="muted">${m.ui.noPreviousRun}</p></div></div>`;
|
|
642
727
|
const items = changeItems(m, ch);
|
|
643
|
-
return `<div class="card" id="changes">${head}<div class="card-c"><p><strong>${escapeHtml(m.ui.changesLine(ch))}</strong></p>${items.length ? `<ul class="changes">${items.map((i) => `<li>${badge(i.sev, i.tag)} ${escapeHtml(i.text)}</li>`).join("")}</ul>` : ""}</div></div>`;
|
|
728
|
+
return `<div class="card" id="changes">${head}<div class="card-c"><p><strong>${escapeHtml(m.ui.changesLine(ch))}</strong></p>${items.length ? `<ul class="changes">${items.map((i) => `<li>${badge(i.sev, i.tag)} ${escapeHtml(i.text)}</li>`).join("")}</ul>` : ""}${ch.routes?.length ? `<ul class="changes">${ch.routes.map((r) => `<li>${escapeHtml(`${r.journeyId} (${r.viewport}): ${r.before.join(" → ")} ⇒ ${r.after.join(" → ")}`)}</li>`).join("")}</ul>` : ""}</div></div>`;
|
|
644
729
|
}
|
|
645
730
|
|
|
646
731
|
function planVsSiteMd(m, pc) {
|
|
@@ -723,22 +808,23 @@ function navCss(runs) {
|
|
|
723
808
|
const sev = ["all", ...FINDING_SEVERITIES];
|
|
724
809
|
out.push(`${sev.map((s) => `#fs-${s}:checked~.toolbar label[for=fs-${s}]`).join(",")}{background:var(--bg);color:var(--fg);box-shadow:var(--shadow-sm)}`);
|
|
725
810
|
out.push(`${sev.map((s) => `#fs-${s}:focus-visible~.toolbar label[for=fs-${s}]`).join(",")}{outline:2px solid var(--accent)}`);
|
|
726
|
-
out.push(`${FINDING_SEVERITIES.map((s) => `#fs-${s}:checked~.flist>li:not([data-sev=${s}])`).join(",")}{display:none}`);
|
|
811
|
+
out.push(`${FINDING_SEVERITIES.map((s) => `#fs-${s}:checked~.ogroup>.flist>li:not([data-sev=${s}])`).join(",")}{display:none}`);
|
|
812
|
+
out.push(`${FINDING_SEVERITIES.map((s) => `#fs-${s}:checked~.ogroup:not(:has(li[data-sev=${s}]))`).join(",")}{display:none}`);
|
|
727
813
|
return out.join("\n");
|
|
728
814
|
}
|
|
729
815
|
|
|
730
816
|
export const CSS = `
|
|
731
817
|
:root{
|
|
732
|
-
--bg:#FFFFFF;--page:#F7F9FC;--fg:#0A0E1A;--muted:#5A6480;--muted-bg:#
|
|
733
|
-
--accent:#
|
|
734
|
-
--danger:#
|
|
735
|
-
--shadow-sm:
|
|
818
|
+
--bg:#FFFFFF;--page:#F7F9FC;--fg:#0A0E1A;--muted:#5A6480;--muted-bg:#EEF2F8;--border:#DCE2EE;--input:#848EA6;
|
|
819
|
+
--accent:#2E6CDB;--accent-fg:#2459C0;--accent-bg:#EBF1FE;--orange:#D9531E;
|
|
820
|
+
--danger:#C8323A;--danger-bg:#FCE9EA;--warn:#8C6106;--warn-bg:#FBF1DC;--warn-dot:#C99A2E;--risk:#B8441A;--risk-bg:#FFF0E8;--danger-dot:#C8323A;--ok:#17805A;--ok-bg:#E3F3EC;
|
|
821
|
+
--shadow-sm:none;--radius:6px;
|
|
736
822
|
--font-sans:'Google Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;--font-mono:'Google Sans Code',ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
737
823
|
@media (prefers-color-scheme:dark){:root{
|
|
738
|
-
--bg:#141927;--page:#0A0E1A;--fg:#F0F4FF;--muted:#8892AA;--muted-bg:#182035;--border:#1E2640;--input:#
|
|
739
|
-
--accent-fg:#
|
|
824
|
+
--bg:#141927;--page:#0A0E1A;--fg:#F0F4FF;--muted:#8892AA;--muted-bg:#182035;--border:#1E2640;--input:#5F6B93;--accent:#4F8EF7;--orange:#FF6D33;
|
|
825
|
+
--accent-fg:#8CBBFF;--accent-bg:#1A2F5C;--danger:#F2545B;--danger-bg:#3A1F22;--warn:#E8B44A;--warn-bg:#33290F;--warn-dot:#E8B44A;--risk:#FF6D33;--risk-bg:#2A1A0F;--danger-dot:#F2545B;--ok:#3FB98A;--ok-bg:#12302A;--shadow-sm:none}}
|
|
740
826
|
*{box-sizing:border-box}
|
|
741
|
-
html{scroll-padding-top:64px}
|
|
827
|
+
html{scroll-padding-top:64px;scrollbar-gutter:stable;overflow-y:scroll}
|
|
742
828
|
body{margin:0;min-height:100vh;display:flex;flex-direction:column;background:var(--page);color:var(--fg);font:400 14px/1.6 var(--font-sans);-webkit-font-smoothing:antialiased}
|
|
743
829
|
a{color:var(--accent-fg);text-decoration:none}
|
|
744
830
|
h1,h2,h3,h4,h5,h6{margin:0;font-weight:500;line-height:1.3}
|
|
@@ -755,7 +841,21 @@ code{font-size:12px;background:var(--muted-bg);padding:1px 5px;border-radius:4px
|
|
|
755
841
|
.product-logo img{display:block;height:26px;width:auto}
|
|
756
842
|
.byline{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}.byline:hover{opacity:.85}
|
|
757
843
|
.logo-h img{display:block;height:18px;width:auto}
|
|
758
|
-
.title{margin-top:
|
|
844
|
+
.title{margin-top:20px;display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px}
|
|
845
|
+
.site{display:flex;align-items:center;gap:10px;min-width:0}
|
|
846
|
+
.site .k{font:500 11px/1.4 var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--muted);background:var(--muted-bg);border:1px solid var(--border);border-radius:2px;padding:2px 6px}
|
|
847
|
+
.site .v{font-size:20px;font-weight:700;letter-spacing:-.01em;color:var(--fg);overflow-wrap:anywhere}
|
|
848
|
+
.run{margin:0;display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px 16px;font-size:13px}
|
|
849
|
+
.run dt{color:var(--muted)}.run dd{margin:0;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;min-width:0;overflow-wrap:anywhere}
|
|
850
|
+
.ptag.off{opacity:.55;text-decoration:line-through}
|
|
851
|
+
.ctl{list-style:none;margin:0;padding:0;display:grid;gap:12px}
|
|
852
|
+
.ctl>li{border:1px solid var(--border);border-radius:var(--radius);padding:12px;background:var(--page)}
|
|
853
|
+
.ctl .ch{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 12px;font-size:13px;margin-bottom:8px}
|
|
854
|
+
.ctl .ch b{font-weight:600}
|
|
855
|
+
.sigs{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:6px}
|
|
856
|
+
.sig{display:flex;justify-content:space-between;gap:8px;align-items:center;border:1px solid var(--border);border-radius:4px;padding:5px 10px;font:12px var(--font-mono);background:var(--bg)}
|
|
857
|
+
.sig .granted{color:var(--ok)}.sig .denied{color:var(--danger)}
|
|
858
|
+
.jconsent{margin-bottom:16px}
|
|
759
859
|
.eyebrow{font:500 12px var(--font-mono);color:var(--accent-fg);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
|
760
860
|
.title .meta{margin-top:4px;font-size:12px;color:var(--muted)}
|
|
761
861
|
.toptabs{position:sticky;top:0;z-index:10;background:var(--page);border-bottom:1px solid var(--border);margin-top:16px}
|
|
@@ -828,7 +928,9 @@ table.dt a{color:var(--fg)}table.dt a:hover{color:var(--accent-fg)}
|
|
|
828
928
|
.missing{display:inline-block;padding:0 6px;border-radius:4px;background:var(--danger-bg);color:var(--danger)}
|
|
829
929
|
.fix-steps{list-style:none;margin:0;padding:0;display:grid;gap:10px;counter-reset:fx}
|
|
830
930
|
.fix-steps li{display:grid;grid-template-columns:22px 1fr;gap:10px;counter-increment:fx}
|
|
831
|
-
.fix-steps li::before{content:counter(fx);display:inline-grid;place-items:center;width:22px;height:22px;border-radius:
|
|
931
|
+
.fix-steps li::before{content:counter(fx);display:inline-grid;place-items:center;width:22px;height:22px;border-radius:4px;background:var(--accent-bg);color:var(--accent-fg);font:500 12px var(--font-mono)}
|
|
932
|
+
.ogroup{margin-top:20px}.ogroup-h{display:flex;align-items:baseline;gap:8px;font-size:14px;margin:0 0 10px}.ogroup-h .n{font-size:12px;color:var(--muted)}
|
|
933
|
+
.verify{display:flex;flex-wrap:wrap;gap:6px 14px;margin:0;padding:0;list-style:none;font-size:13px}.verify a{color:var(--accent-fg)}
|
|
832
934
|
.where{display:flex;flex-wrap:wrap;gap:6px}
|
|
833
935
|
.chip{display:inline-flex;align-items:center;gap:8px;padding:4px 10px 4px 4px;border:1px solid var(--border);border-radius:var(--radius);color:var(--fg);font-size:12px;background:var(--bg)}
|
|
834
936
|
span.chip{padding-left:10px}
|
|
@@ -872,15 +974,23 @@ body>main{flex:1 0 auto;width:100%}
|
|
|
872
974
|
.shead h3{overflow-wrap:anywhere}
|
|
873
975
|
.shead .sidx{background:var(--accent);border-color:var(--accent);color:#fff;width:26px;height:26px;flex:none}
|
|
874
976
|
.empty{color:var(--muted);padding:8px 0}
|
|
875
|
-
.tabs.seg{display:grid;grid-template-columns:repeat(
|
|
977
|
+
.tabs.seg{display:grid;grid-template-columns:repeat(4,auto) 1fr;padding:0 16px 16px}
|
|
876
978
|
.tabs.seg>input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
|
877
979
|
.tabs.seg>label{padding:6px 12px;font-weight:500;color:var(--muted);cursor:pointer;background:var(--muted-bg);white-space:nowrap;display:inline-flex;gap:6px;align-items:center}
|
|
878
980
|
.tabs.seg>label:nth-of-type(1){border-radius:var(--radius) 0 0 var(--radius)}
|
|
879
|
-
.tabs.seg>label:nth-of-type(
|
|
981
|
+
.tabs.seg>label:nth-of-type(4){border-radius:0 var(--radius) var(--radius) 0}
|
|
880
982
|
.tabs.seg>input:checked+label{background:var(--bg);color:var(--fg);box-shadow:inset 0 0 0 1px var(--border)}
|
|
881
983
|
.tabs.seg>input:focus-visible+label{outline:2px solid var(--accent)}
|
|
882
984
|
.tabs.seg>.panel{display:none;grid-column:1/-1;padding-top:12px;min-width:0}
|
|
883
|
-
.tabs.seg>input:nth-of-type(1):checked~.panel:nth-of-type(1),.tabs.seg>input:nth-of-type(2):checked~.panel:nth-of-type(2),.tabs.seg>input:nth-of-type(3):checked~.panel:nth-of-type(3){display:block}
|
|
985
|
+
.tabs.seg>input:nth-of-type(1):checked~.panel:nth-of-type(1),.tabs.seg>input:nth-of-type(2):checked~.panel:nth-of-type(2),.tabs.seg>input:nth-of-type(3):checked~.panel:nth-of-type(3),.tabs.seg>input:nth-of-type(4):checked~.panel:nth-of-type(4){display:block}
|
|
986
|
+
.timeline{list-style:none;margin:0;padding:0 0 0 18px;border-left:2px solid var(--border);display:grid;gap:14px}
|
|
987
|
+
.tl-node{position:relative;display:grid;gap:6px;min-width:0}
|
|
988
|
+
.tl-node::before{content:"";position:absolute;left:-24px;top:4px;width:10px;height:10px;border-radius:50%;background:var(--bg);border:2px solid var(--accent)}
|
|
989
|
+
.tl-orphan::before{border-color:var(--muted)}
|
|
990
|
+
.tl-lbl{font-size:12px;font-weight:500;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
|
|
991
|
+
.tl-gtm{display:flex;gap:8px;align-items:center;font-size:13px;padding-left:12px}
|
|
992
|
+
.tl-k{color:var(--muted)}
|
|
993
|
+
.tl-hits,.tl-none{margin-left:12px}
|
|
884
994
|
.filter{display:flex;flex-wrap:wrap;gap:6px;position:relative}
|
|
885
995
|
.filter>input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
|
886
996
|
.filter>label{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border:1px solid var(--input);border-radius:var(--radius);font-size:12px;cursor:pointer;color:var(--muted);background:var(--bg)}
|
|
@@ -889,8 +999,8 @@ body>main{flex:1 0 auto;width:100%}
|
|
|
889
999
|
.filter>input:checked+label .n{background:transparent;color:inherit}
|
|
890
1000
|
.filter>input:focus-visible+label{outline:2px solid var(--accent);outline-offset:2px}
|
|
891
1001
|
.filter>label:last-of-type{margin-bottom:4px}
|
|
892
|
-
.dot{display:inline-block;width:
|
|
893
|
-
.dot.p-ga4,.ptag.p-ga4::before{background:var(--orange)}.dot.p-meta,.ptag.p-meta::before{background:var(--accent)}.dot.p-google_ads,.ptag.p-google_ads::before{background:var(--ok)}.dot.p-tiktok,.ptag.p-tiktok::before{background:var(--fg)}
|
|
1002
|
+
.dot{display:inline-block;width:12px;height:12px;border-radius:2px;background:var(--muted);vertical-align:-2px}
|
|
1003
|
+
.dot.p-ga4,.ptag.p-ga4::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjIwIiByeD0iMyIgZmlsbD0iI0Y5QUIwMCIvPjxyZWN0IHg9IjkiIHk9IjkiIHdpZHRoPSI2IiBoZWlnaHQ9IjEzIiByeD0iMyIgZmlsbD0iI0UzNzQwMCIvPjxjaXJjbGUgY3g9IjYiIGN5PSIxOSIgcj0iMyIgZmlsbD0iI0UzNzQwMCIvPjwvc3ZnPg==") center/contain no-repeat}.dot.p-google_ads,.ptag.p-google_ads::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iOS41IiB5PSIxLjUiIHdpZHRoPSI2IiBoZWlnaHQ9IjIxIiByeD0iMyIgdHJhbnNmb3JtPSJyb3RhdGUoMzAgMTIuNSAxMikiIGZpbGw9IiNGQkJDMDQiLz48cmVjdCB4PSI4LjUiIHk9IjEuNSIgd2lkdGg9IjYiIGhlaWdodD0iMjEiIHJ4PSIzIiB0cmFuc2Zvcm09InJvdGF0ZSgtMzAgMTEuNSAxMikiIGZpbGw9IiM0Mjg1RjQiLz48Y2lyY2xlIGN4PSI1LjUiIGN5PSIxOC41IiByPSIzIiBmaWxsPSIjMzRBODUzIi8+PC9zdmc+") center/contain no-repeat}.dot.p-meta,.ptag.p-meta::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTIuNSAxNS41YzAtNC40IDIuMy05IDUtOSAzLjQgMCA2IDExIDkuNSAxMSAyLjIgMCA0LjUtMi42IDQuNS02LjVTMTkuOCA2LjUgMTcuNSA2LjVjLTMuNCAwLTYgMTEtOS44IDExLTIuOSAwLTUuMi0uNy01LjItMnoiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzA4NjZGRiIgc3Ryb2tlLXdpZHRoPSIyLjQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=") center/contain no-repeat}.dot.p-tiktok,.ptag.p-tiktok::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyLjYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTEzIDN2MTEuNWEzLjUgMy41IDAgMSAxLTMtMy40Nk0xMyAzYy41IDIuNSAyLjUgNCA1IDQiIHN0cm9rZT0iIzI1RjRFRSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEgLTEpIi8+PHBhdGggZD0iTTEzIDN2MTEuNWEzLjUgMy41IDAgMSAxLTMtMy40Nk0xMyAzYy41IDIuNSAyLjUgNCA1IDQiIHN0cm9rZT0iI0ZFMkM1NSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMSAxKSIvPjxwYXRoIGQ9Ik0xMyAzdjExLjVhMy41IDMuNSAwIDEgMS0zLTMuNDZNMTMgM2MuNSAyLjUgMi41IDQgNSA0IiBzdHJva2U9IiMxMTEiLz48L2c+PC9zdmc+") center/contain no-repeat}.dot.p-x,.ptag.p-x::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjkwMSAxLjE1M2gzLjY4bC04LjA0IDkuMTlMMjQgMjIuODQ2aC03LjQwNmwtNS44LTcuNTg0LTYuNjM4IDcuNTg0SC40NzRsOC42LTkuODNMMCAxLjE1NGg3LjU5NGw1LjI0MyA2LjkzMlpNMTcuNjEgMjAuNjQ0aDIuMDM5TDYuNDg2IDMuMjRINC4yOThaIiBmaWxsPSIjMDAwIi8+PC9zdmc+") center/contain no-repeat}.dot.p-linkedin,.ptag.p-linkedin::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTIwLjQ0NyAyMC40NTJoLTMuNTU0di01LjU2OWMwLTEuMzI4LS4wMjctMy4wMzctMS44NTItMy4wMzctMS44NTMgMC0yLjEzNiAxLjQ0NS0yLjEzNiAyLjkzOXY1LjY2N0g5LjM1MVY5aDMuNDE0djEuNTYxaC4wNDZjLjQ3Ny0uOSAxLjYzNy0xLjg1IDMuMzctMS44NSAzLjYwMSAwIDQuMjY3IDIuMzcgNC4yNjcgNS40NTV2Ni4yODZ6TTUuMzM3IDcuNDMzYy0xLjE0NCAwLTIuMDYzLS45MjYtMi4wNjMtMi4wNjUgMC0xLjEzOC45Mi0yLjA2MyAyLjA2My0yLjA2MyAxLjE0IDAgMi4wNjQuOTI1IDIuMDY0IDIuMDYzIDAgMS4xMzktLjkyNSAyLjA2NS0yLjA2NCAyLjA2NXptMS43ODIgMTMuMDE5SDMuNTU1VjloMy41NjR2MTEuNDUyek0yMi4yMjUgMEgxLjc3MUMuNzkyIDAgMCAuNzc0IDAgMS43Mjl2MjAuNTQyQzAgMjMuMjI3Ljc5MiAyNCAxLjc3MSAyNGgyMC40NTFDMjMuMiAyNCAyNCAyMy4yMjcgMjQgMjIuMjcxVjEuNzI5QzI0IC43NzQgMjMuMiAwIDIyLjIyMiAwaC4wMDN6IiBmaWxsPSIjMEE2NkMyIi8+PC9zdmc+") center/contain no-repeat}.dot.p-microsoft_ads,.ptag.p-microsoft_ads::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjEwLjUiIGhlaWdodD0iMTAuNSIgZmlsbD0iI0YyNTAyMiIvPjxyZWN0IHg9IjEyLjUiIHk9IjEiIHdpZHRoPSIxMC41IiBoZWlnaHQ9IjEwLjUiIGZpbGw9IiM3RkJBMDAiLz48cmVjdCB4PSIxIiB5PSIxMi41IiB3aWR0aD0iMTAuNSIgaGVpZ2h0PSIxMC41IiBmaWxsPSIjMDBBNEVGIi8+PHJlY3QgeD0iMTIuNSIgeT0iMTIuNSIgd2lkdGg9IjEwLjUiIGhlaWdodD0iMTAuNSIgZmlsbD0iI0ZGQjkwMCIvPjwvc3ZnPg==") center/contain no-repeat}.dot.p-pinterest,.ptag.p-pinterest::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyLjAxNyAwQzUuMzk2IDAgLjAyOSA1LjM2Ny4wMjkgMTEuOTg3YzAgNS4wNzkgMy4xNTggOS40MTcgNy42MTggMTEuMTYyLS4xMDUtLjk0OS0uMTk5LTIuNDAzLjA0MS0zLjQzOS4yMTktLjkzNyAxLjQwNi01Ljk1NyAxLjQwNi01Ljk1N3MtLjM1OS0uNzItLjM1OS0xLjc4MWMwLTEuNjYzLjk2Ny0yLjkxMSAyLjE2OC0yLjkxMSAxLjAyNCAwIDEuNTE4Ljc2OSAxLjUxOCAxLjY4OCAwIDEuMDI5LS42NTMgMi41NjctLjk5MiAzLjk5Mi0uMjg1IDEuMTkzLjYgMi4xNjUgMS43NzUgMi4xNjUgMi4xMjggMCAzLjc2OC0yLjI0NSAzLjc2OC01LjQ4NyAwLTIuODYxLTIuMDYzLTQuODY5LTUuMDA4LTQuODY5LTMuNDEgMC01LjQwOSAyLjU2Mi01LjQwOSA1LjE5OSAwIDEuMDMzLjM5NCAyLjE0My44ODkgMi43NDEuMDk5LjEyLjExMi4yMjUuMDg1LjM0NS0uMDkuMzc1LS4yOTMgMS4xOTktLjMzNCAxLjM2My0uMDUzLjIyNS0uMTcyLjI3MS0uNDAxLjE2NS0xLjQ5NS0uNjktMi40MzMtMi44NzgtMi40MzMtNC42NDYgMC0zLjc3NiAyLjc0OC03LjI1MiA3LjkyLTcuMjUyIDQuMTU4IDAgNy4zOTIgMi45NjcgNy4zOTIgNi45MjMgMCA0LjEzNS0yLjYwNyA3LjQ2Mi02LjIzMyA3LjQ2Mi0xLjIxNCAwLTIuMzU0LS42MjktMi43NTgtMS4zNzlsLS43NDkgMi44NDhjLS4yNjkgMS4wNDUtMS4wMDQgMi4zNTItMS40OTggMy4xNDYgMS4xMjMuMzQ1IDIuMzA2LjUzNSAzLjU1LjUzNSA2LjYwNyAwIDExLjk4NS01LjM2NSAxMS45ODUtMTEuOTg3QzIzLjk3IDUuMzkgMTguNTkyLjAyNiAxMS45ODUuMDI2TDEyLjAxNyAweiIgZmlsbD0iI0JEMDgxQyIvPjwvc3ZnPg==") center/contain no-repeat}.dot.p-snapchat,.ptag.p-snapchat::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNSIgZmlsbD0iI0ZGRkMwMCIvPjxwYXRoIGQ9Ik0xMi4yMDYuNzkzYy45OSAwIDQuMzQ3LjI3NiA1LjkzIDMuODIxLjUyOSAxLjE5My40MDMgMy4yMTkuMjk5IDQuODQ3bC0uMDAzLjA2Yy0uMDEyLjE4LS4wMjIuMzQ1LS4wMy41MS4wNzUuMDQ1LjIwMy4wOS40MDEuMDkuMy0uMDE2LjY1OS0uMTIgMS4wMzMtLjMwMS4xNjUtLjA4OC4zNDQtLjEwNC40NjQtLjEwNC4xODIgMCAuMzU5LjAyOS41MDkuMDkuNDUuMTQ5LjczNC40NzkuNzM0LjgzOC4wMTUuNDQ5LS4zOS44MzktMS4yMTMgMS4xNjgtLjA4OS4wMjktLjIwOS4wNzUtLjM0NC4xMTktLjQ1LjEzNS0xLjEzOS4zNi0xLjMzMy44MS0uMDkuMjI0LS4wNjEuNTI0LjEyLjg2OGwuMDE1LjAxNWMuMDYuMTM2IDEuNTI2IDMuNDc1IDQuNzkxIDQuMDE0LjI1NS4wNDQuNDM1LjI3LjQyLjUwOSAwIC4wNzUtLjAxNS4xNDktLjA0NS4yMjUtLjI0LjU2OS0xLjI3My45ODgtMy4xNDYgMS4yNzEtLjA1OS4wOTEtLjEyLjM3NS0uMTY0LjU3LS4wMjkuMTc5LS4wNzQuMzYtLjEzNC41NTMtLjA3Ni4yNzEtLjI3LjQwNS0uNTU1LjQwNWgtLjAzYy0uMTM1IDAtLjMxMy0uMDMxLS41MzgtLjA3NC0uMzYtLjA3NS0uNzY1LS4xMzUtMS4yNzMtLjEzNS0uMyAwLS41OTkuMDE1LS45MTMuMDc0LS42LjEwNC0xLjEyMy40NjQtMS43MjMuODg0LS44NTMuNTk5LTEuODI2IDEuMjg4LTMuMjk0IDEuMjg4LS4wNiAwLS4xMTktLjAxNS0uMTgtLjAxNWgtLjE0OWMtMS40NjggMC0yLjQyNy0uNjc1LTMuMjc5LTEuMjg4LS41OTktLjQyLTEuMTA3LS43NzktMS43MDctLjg4NC0uMzE0LS4wNDUtLjYyOS0uMDc0LS45MjgtLjA3NC0uNTQgMC0uOTU4LjA4OS0xLjI3Mi4xNDktLjIxMS4wNDMtLjM5MS4wNzQtLjU0LjA3NC0uMzc0IDAtLjUyMy0uMjI0LS41ODMtLjQyLS4wNjEtLjE5Mi0uMDktLjM4OS0uMTM1LS41NjctLjA0Ni0uMTgxLS4xMDUtLjQ5NC0uMTY2LS41Ny0xLjkxOC0uMjIyLTIuOTUtLjY0Mi0zLjE4OS0xLjIyNi0uMDMxLS4wNjMtLjA1Mi0uMTUtLjA1NS0uMjI1LS4wMTUtLjI0My4xNjUtLjQ2NS40Mi0uNTA5IDMuMjY0LS41NCA0LjczLTMuODc5IDQuNzkxLTQuMDJsLjAxNi0uMDI5Yy4xOC0uMzQ1LjIyNC0uNjQ1LjExOS0uODY5LS4xOTUtLjQzNC0uODg0LS42NTgtMS4zMzItLjgwOS0uMTIxLS4wMjktLjI0LS4wNzQtLjM0Ni0uMTE5LTEuMTA3LS40MzUtMS4yNTctLjkzLTEuMTk3LTEuMjczLjA5LS40NzkuNjc0LS43OTMgMS4xNjgtLjc5My4xNDYgMCAuMjcuMDI5LjM4My4wNzQuNDIuMTk0Ljc4OS4zIDEuMTA0LjMuMjM0IDAgLjM4NC0uMDYuNDY1LS4xMDVsLS4wNDYtLjU2OWMtLjA5OC0xLjYyNi0uMjI1LTMuNjUxLjMwNy00LjgzN0M3LjM5MiAxLjA3NyAxMC43MzkuODA3IDExLjcyNy44MDdsLjQxOS0uMDE1aC4wNnoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMgMykgc2NhbGUoLjc1KSIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEuMyIvPjwvc3ZnPg==") center/contain no-repeat}.dot.p-criteo,.ptag.p-criteo::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTEiIGZpbGw9IiNGRTUwMDAiLz48cGF0aCBkPSJNMTYuMiA4LjZhNSA1IDAgMSAwIDAgNi44IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMi42IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=") center/contain no-repeat}.dot.p-hotjar,.ptag.p-hotjar::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjExOSA5LjgxNEMxMi44OTkgOC4yNyAxNi43MDQgNi4xNTUgMTYuNzA0IDBoLTQuNjA5YzAgMy40NDQtMS42NzYgNC4zNzUtNC4yMTQgNS43ODZDNS4xIDcuMzMgMS4yOTUgOS40NDQgMS4yOTUgMTUuNmg0LjYxYzAtMy40NDQgMS42NzYtNC4zNzYgNC4yMTQtNS43ODZ6TTE4LjA5NiA4LjRjMCAzLjQ0NC0xLjY3NyA0LjM3Ni00LjIxNSA1Ljc4NS0yLjc3OCAxLjU0NC02LjU4NSAzLjY2LTYuNTg1IDkuODE1aDQuNjA5YzAtMy40NDQgMS42NzYtNC4zNzYgNC4yMTQtNS43ODYgMi43OC0xLjU0NCA2LjU4Ni0zLjY1OCA2LjU4Ni05LjgxNGgtNC42MDl6IiBmaWxsPSIjRkYzQzAwIi8+PC9zdmc+") center/contain no-repeat}.dot.p-adobe,.ptag.p-adobe::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTAgMmg4LjVMMCAyMnpNMjQgMmgtOC41TDI0IDIyek0xMiA5bDUuNSAxM2gtMy42bC0xLjctNC4ySDguOXoiIGZpbGw9IiNFQjEwMDAiLz48L3N2Zz4=") center/contain no-repeat}.dot.p-matomo,.ptag.p-matomo::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMThsNi05IDYgOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzE1MkEwIiBzdHJva2Utd2lkdGg9IjIiLz48Y2lyY2xlIGN4PSI2IiBjeT0iMTgiIHI9IjMiIGZpbGw9IiM5NUM3NDgiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjkiIHI9IjMiIGZpbGw9IiMzMTUyQTAiLz48Y2lyY2xlIGN4PSIxOCIgY3k9IjE4IiByPSIzIiBmaWxsPSIjMzE1MkEwIi8+PC9zdmc+") center/contain no-repeat}.dot.p-piwik_pro,.ptag.p-piwik_pro::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNSIgZmlsbD0iIzBDMkQ1QiIvPjxwYXRoIGQ9Ik04IDE4VjZoNWEzLjUgMy41IDAgMDEwIDdIOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+") center/contain no-repeat}.dot.p-clarity,.ptag.p-clarity::before{background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNSIgZmlsbD0iIzAwNzhENCIvPjxwYXRoIGQ9Ik0xMiA1YTcgNyAwIDEwMCAxNE0xMiA5YTMgMyAwIDEwMCA2IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMi40IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=") center/contain no-repeat}
|
|
894
1004
|
.evlist{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:var(--radius);flex-basis:100%;min-width:0}
|
|
895
1005
|
.evlist>li+li{border-top:1px solid var(--border)}
|
|
896
1006
|
summary{cursor:pointer;list-style:none}summary::-webkit-details-marker{display:none}
|
|
@@ -905,7 +1015,7 @@ summary:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-of
|
|
|
905
1015
|
.event .meta{font-size:12px;color:var(--muted)}
|
|
906
1016
|
.event .time,.time{font-size:12px;color:var(--muted);margin-left:auto}
|
|
907
1017
|
.ptag{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
908
|
-
.ptag::before{content:"";width:
|
|
1018
|
+
.ptag::before{content:"";width:14px;height:14px;flex:none;border-radius:2px;background:var(--muted)}
|
|
909
1019
|
.event-body{padding:4px 12px 16px}
|
|
910
1020
|
.flagnote{margin:4px 0 12px;padding:8px 12px;border-radius:var(--radius);background:var(--warn-bg);color:var(--warn)}
|
|
911
1021
|
.flagnote.broken{background:var(--danger-bg);color:var(--danger)}.flagnote.risk{background:var(--risk-bg);color:var(--risk)}
|
|
@@ -965,7 +1075,7 @@ pre{margin:8px 0 0;font-size:12px;line-height:1.5;background:var(--muted-bg);bor
|
|
|
965
1075
|
@media print{.toptabs,.jtabs,.stepnav,.filter>label,.filter>input,.tabs.seg>label,.sevfilter{display:none}.tabpanel,.jpanel,.sdetail,.tabs.seg>.panel{display:block!important}.jpanel{margin-bottom:32px}.sdetail{margin-bottom:16px;break-inside:avoid-page}details>*{display:block}}
|
|
966
1076
|
`;
|
|
967
1077
|
|
|
968
|
-
export function renderHtml({ title, generatedAt, findings, explained, runs, plan = null, lang = "en", branding = true, against = null, planCheck = null, changes = null }) {
|
|
1078
|
+
export function renderHtml({ title, site = null, ai = null, generatedAt, findings, explained, runs, plan = null, lang = "en", branding = true, against = null, planCheck = null, changes = null }) {
|
|
969
1079
|
const m = messages(lang);
|
|
970
1080
|
const rows = attachExplanations(findings, explained);
|
|
971
1081
|
const counts = summarize(rows);
|
|
@@ -985,15 +1095,14 @@ export function renderHtml({ title, generatedAt, findings, explained, runs, plan
|
|
|
985
1095
|
${navCss(runs)}</style></head><body>
|
|
986
1096
|
<div class="topbar"><div class="wrap"><span class="product-logo">${logo("atd-lockup", "AI Tag Debugger", 26, "logo-atd")}</span>${branding ? `<a href="https://analitikisler.com" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("horizontal", "Analitik İşler", 18, "logo-h")}</a>` : ""}</div></div>
|
|
987
1097
|
<header class="wrap title">
|
|
988
|
-
<
|
|
989
|
-
<
|
|
990
|
-
<div class="meta">${escapeHtml(generatedAt)} · ${escapeHtml(m.ui.headerMeta(runs.length, checkedProviders(runs).length))} · ai-tag-debugger ${VERSION}</div>${against ? `\n<div class="meta">${escapeHtml(m.ui.checkedAgainst(against))}</div>` : ""}
|
|
1098
|
+
<h1 class="site"><span class="k">${m.ui.site}</span><span class="v">${escapeHtml(site ?? title)}</span></h1>
|
|
1099
|
+
<div class="meta">${escapeHtml(generatedAt)} · ${escapeHtml(m.ui.headerMeta(runs.length, checkedProviders(runs).length))} · ai-tag-debugger ${VERSION}</div>${against ? `\n<div class="meta">${escapeHtml(m.ui.checkedAgainst(against))}</div>` : ""}${changes?.routes?.length ? `\n<div class="meta"><a href="#changes">${escapeHtml(m.ui.routeChanged(routeList(changes)))}</a></div>` : ""}
|
|
991
1100
|
</header>
|
|
992
1101
|
<nav class="toptabs" aria-label="${m.ui.sections}"><div class="wrap">
|
|
993
1102
|
<a href="#overview">${m.ui.tabOverview}</a><a href="#findings">${m.ui.findings} <span class="n">${problems}</span></a><a href="#journeys">${m.ui.tabJourneys} <span class="n">${runs.length}</span></a><a href="#scope">${m.ui.tabScope}</a></div></nav>
|
|
994
1103
|
<main class="wrap">
|
|
995
|
-
<section class="tabpanel" id="overview" aria-label="${m.ui.tabOverview}">${changes ? changesCard(m, changes) : ""}${planCheck ? planVsSiteCard(m, planCheck) : ""}${overviewTab(m, findings, rows, counts, explained, runs)}</section>
|
|
996
|
-
<section class="tabpanel" id="findings" aria-label="${m.ui.findings}">${findingsTab(m, runs, rows)}</section>
|
|
1104
|
+
<section class="tabpanel" id="overview" aria-label="${m.ui.tabOverview}">${changes ? changesCard(m, changes) : ""}${planCheck ? planVsSiteCard(m, planCheck) : ""}${overviewTab(m, findings, rows, counts, explained, runs, { ai, plan, lang })}</section>
|
|
1105
|
+
<section class="tabpanel" id="findings" aria-label="${m.ui.findings}">${findingsTab(m, runs, rows, Boolean(explained))}</section>
|
|
997
1106
|
<section class="tabpanel" id="journeys" aria-label="${m.ui.tabJourneys}">${journeysTab(m, lang, findings, runs, flagged)}</section>
|
|
998
1107
|
<section class="tabpanel" id="scope" aria-label="${m.ui.tabScope}"><div class="stack">${coverageCard(m, plan, runs)}${providersCard(m, runs)}</div></section>
|
|
999
1108
|
</main>
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Who fixes a finding, and where to check the fix. Both are fixed tables over
|
|
2
|
+
// the check names, so they work with or without the AI explanation.
|
|
3
|
+
|
|
4
|
+
export const OWNERS = ["developer", "gtm", "marketing"];
|
|
5
|
+
|
|
6
|
+
const OWNER_BY_CHECK = {
|
|
7
|
+
missing_params: "developer",
|
|
8
|
+
purchase_value_invalid: "developer",
|
|
9
|
+
purchase_currency_invalid: "developer",
|
|
10
|
+
purchase_items_empty: "developer",
|
|
11
|
+
purchase_mismatch: "developer",
|
|
12
|
+
pii_datalayer: "developer",
|
|
13
|
+
journey_incomplete: "developer",
|
|
14
|
+
server_side: "developer",
|
|
15
|
+
unplanned_events: "marketing",
|
|
16
|
+
planned_out_of_scope: "marketing",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** 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. */
|
|
20
|
+
export function ownerOf(finding) {
|
|
21
|
+
return OWNER_BY_CHECK[finding.check] ?? "gtm";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LINKS = {
|
|
25
|
+
tagAssistant: { name: "Tag Assistant", url: "https://tagassistant.google.com/" },
|
|
26
|
+
ga4Debug: { name: "GA4 DebugView", url: "https://support.google.com/analytics/answer/7201382" },
|
|
27
|
+
metaTest: { name: "Meta Test Events", url: "https://www.facebook.com/business/help/2040882565969969" },
|
|
28
|
+
tiktokHelper: { name: "TikTok Pixel Helper", url: "https://chromewebstore.google.com/detail/tiktok-pixel-helper/aelgobmabdmlfmiblddjfnjodalhidnn" },
|
|
29
|
+
consentMode: { name: "Consent Mode", url: "https://developers.google.com/tag-platform/security/guides/consent" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const PLATFORM_LINKS = {
|
|
33
|
+
ga4: ["tagAssistant", "ga4Debug"],
|
|
34
|
+
google_ads: ["tagAssistant"],
|
|
35
|
+
meta: ["metaTest"],
|
|
36
|
+
tiktok: ["tiktokHelper"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Tools where the person fixing this finding can see the result: a list of { key, name, url }. Empty when no tool fits. */
|
|
40
|
+
export function verifyLinks(finding) {
|
|
41
|
+
const keys = new Set(PLATFORM_LINKS[finding.platform] ?? []);
|
|
42
|
+
if (/^(consent_|cookies_|reject_|before_consent|after_reject)/.test(finding.check)) {
|
|
43
|
+
keys.add("tagAssistant");
|
|
44
|
+
keys.add("consentMode");
|
|
45
|
+
}
|
|
46
|
+
if (!keys.size && ["unplanned_events", "journey_incomplete"].includes(finding.check)) keys.add("tagAssistant");
|
|
47
|
+
return [...keys].map((key) => ({ key, ...LINKS[key] }));
|
|
48
|
+
}
|