@analitikisler/ai-tag-debugger 0.17.0 → 0.18.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 +9 -0
- package/README.md +12 -13
- package/README.tr.md +12 -13
- package/package.json +2 -2
- package/src/html/consent.js +152 -0
- package/src/html/css.js +1180 -0
- package/src/html/findings.js +493 -0
- package/src/html/journeys.js +449 -0
- package/src/html/lib.js +272 -0
- package/src/html/scope.js +350 -0
- package/src/html/script.js +300 -0
- package/src/html/shell.js +191 -0
- package/src/html/summary.js +288 -0
- package/src/i18n.js +380 -166
- package/src/metrics.js +9 -3
- package/src/plan-html.js +71 -25
- package/src/report-shared.js +123 -0
- package/src/report.js +23 -1504
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
- The HTML report was rebuilt as an inspector: a collapsible rail with seven sections (Summary, Findings, Journeys, Consent, Coverage, Inventory, Run), one screen at a time. The Summary is a score ring banded red, amber and green, the verdict, up to three lines saying what cost the points, status chips that open the findings pre-filtered, and six cards about the run. Findings have a filter bar (severity, owner, platform, journey, viewport, search) over failed, unverified and passed groups, every row collapsed and opening into Evidence, Fix, Where and Verify tabs. Journeys show the steps by page title on the left and the selected step's Hits, Data Layer and Requests tabs on the right, with a consent chip on every hit. Every card starts collapsed except the Summary; every section, filter and card has its own URL. With a model, the Summary shows its one-line headline and an AI summary card in Findings keeps its points and its "worth checking" list; a route that differs from the last run is named under the Run facts. Severity is never colour alone, one hue per kind of chip, light and dark mode, keyboard (`[` folds the rail, `1`–`7` jump), no horizontal scroll on phones, and the stacked no-JavaScript document is the print layout. Design notes and the mockups it was built from are under `docs/design/directions/`.
|
|
6
|
+
- `plan.html` uses the same stylesheet and chips as the report.
|
|
7
|
+
- The Trend table's last column is labelled "Problem points": it is the weighted count of open problems per run (lower is better), not the 0–100 data quality score.
|
|
8
|
+
- Fixed: in `report.md`, the link under a finding with no platform (a journey that did not finish, no Consent Mode default) or under a second finding with the same platform, event and check now opens its card in `report.html` instead of the Summary.
|
|
9
|
+
- Fixed: a `value` that is not a number (an unresolved template, a product name) showed as `NaN` in the platform parity table; the report now shows the text the tag sent, and a number on one platform against text on the other counts as a value difference.
|
|
10
|
+
- Development: the HTML report lives in `src/html/` (one module per section, the stylesheet and the hashed script as constants); the real demo capture is a test fixture; the design record under `docs/design/directions/` holds the UX research, the four directions with their rationales, and the chosen mockup.
|
|
11
|
+
|
|
3
12
|
## 0.17.0 (2026-10-19)
|
|
4
13
|
|
|
5
14
|
- The report opens with a verdict. A deterministic sentence names the lead problem and where it is ("purchase tracking is broken on mobile"), with a data quality score whose weights are printed next to it, so a run without an AI model still says what matters first. The same verdict heads report.md, the CLI output, `summary.json` and the Slack and email alerts.
|
package/README.md
CHANGED
|
@@ -284,7 +284,7 @@ Say yes, and the next runs can check against it with `--against latest`. A param
|
|
|
284
284
|
|
|
285
285
|
### Plan vs. site
|
|
286
286
|
|
|
287
|
-
When a run uses `--against`, the report
|
|
287
|
+
When a run uses `--against`, the report has a **Plan vs. site** section: at the top of `report.md`, and in the Coverage section of `report.html`. Each planned event lands in one bucket:
|
|
288
288
|
|
|
289
289
|
| Bucket | Meaning |
|
|
290
290
|
|---|---|
|
|
@@ -301,7 +301,7 @@ When the plan is not the site's first, the header compares it with the version s
|
|
|
301
301
|
|
|
302
302
|
### Tag inventory
|
|
303
303
|
|
|
304
|
-
Every report also has a **tag inventory**: each vendor the site sent requests to, marked first party when its requests went to the site's own domain, third party when they went to the vendor, or both. It shows how many pages the vendor was seen on and the cookies tied to it. Cookies that no vendor owns are listed apart, as first or third party. Only cookie names are recorded, never their values. It is
|
|
304
|
+
Every report also has a **tag inventory**: each vendor the site sent requests to, marked first party when its requests went to the site's own domain, third party when they went to the vendor, or both. It shows how many pages the vendor was seen on and the cookies tied to it. Cookies that no vendor owns are listed apart, as first or third party. Only cookie names are recorded, never their values. It is the **Inventory** section of `report.html` and a table in `report.md`.
|
|
305
305
|
|
|
306
306
|
### Folder layout and plan IDs
|
|
307
307
|
|
|
@@ -592,17 +592,16 @@ jq -r '.verdict.status + " " + (.score | tostring)' aitd/example.com/runs/*/summ
|
|
|
592
592
|
|
|
593
593
|
Output:
|
|
594
594
|
|
|
595
|
-
- `report.html`: one file
|
|
596
|
-
- **
|
|
597
|
-
- **Findings**:
|
|
598
|
-
- **Journeys**: one journey at a time.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
Events and requests can be filtered by source. GA4 item strings (`pr1`…) and dataLayer `ecommerce.items` are shown as item cards, with custom item parameters in their own group. The report has no scripts: tabs and filters are plain HTML and CSS (they need Chrome 105, Safari 15.4 or Firefox 121 or newer), and printing shows every tab. The logos are embedded, so nothing is loaded from the internet. `--no-branding` leaves out the Analitik İşler logo and credit line.
|
|
595
|
+
- `report.html`: one file, one screen at a time. A collapsible rail on the left lists seven sections; every section, filter and card has its own URL you can bookmark or share, and a link from one section opens the right card in another (a finding's "Where" chip opens that hit in its step).
|
|
596
|
+
- **Summary**: the data quality score as a ring, coloured red below 50, amber to 89 and green from 90, the verdict and its headline, up to three lines saying what cost the points (none for a perfect score), status chips that open the findings filtered by severity, and six cards about the run: vendors checked, journeys with the platforms seen on each, events checked, consent, the AI provider and the run itself.
|
|
597
|
+
- **Findings**: a filter bar (severity, owner, platform, journey, viewport, search) over three groups: failed, unverified and passed. Every row starts collapsed and opens into **Evidence**, **Fix**, **Where** and **Verify** tabs: the impact, the likely cause, the evidence with missing parameters marked, numbered fix steps, the steps where it happened, and the tools to check the fix in, such as Tag Assistant, GA4 DebugView, Meta Test Events or TikTok Pixel Helper. **Changed since last run** sits above the groups when the site has an earlier run.
|
|
598
|
+
- **Journeys**: one journey and viewport at a time. The steps on the left, named after the pages ("Open Home", "Add to cart") and grouped by page, with the hits each step produced; the selected step on the right with **Hits**, **Data Layer** and **Requests** tabs. Each hit shows its event, key parameters and consent state, and opens into the decoded parameters, item cards and the raw request. A batched GA4 request shows each of its events. It opens on the first step with a problem.
|
|
599
|
+
- **Consent**: one line per platform with what it received before a choice, after accept and after reject, the Consent Mode default and updates with their timing, and the [consent scenarios](#consent-scenarios) table.
|
|
600
|
+
- **Coverage**: whether each planned event was sent on each journey, with the parameters it still needs, conversion integrity, and, with `--against`, the [Plan vs. site](#plan-vs-site) buckets.
|
|
601
|
+
- **Inventory**: the [tag inventory](#tag-inventory), one line per platform, opening into its hosts, pages and cookies.
|
|
602
|
+
- **Run**: the facts of the run (site, version, AI provider and model, plan, journeys, viewports, platforms, timing, files) and, when the site has earlier runs, the **Trend**.
|
|
603
|
+
|
|
604
|
+
Every card starts collapsed except the Summary; each section has Expand all and Collapse all. The page carries one small script, allowed by its hash in the page's Content Security Policy, for the rail, the filters, the tabs and the deep links; without JavaScript the sections stack into one document, which is also the print layout. Keyboard: `[` folds the rail, `1`–`7` jump to a section. The logos are embedded, so nothing is loaded from the internet. `--no-branding` leaves out the Analitik İşler logo and credit line.
|
|
606
605
|
- `timeline.csv`: the same timeline as one row per hit, dataLayer push and network call, ready for Excel or Google Sheets.
|
|
607
606
|
- `report.md`: the report in Markdown for a pull request comment, a Slack message or a mail, around 100 lines and under 8 KB for a four-journey run. Its title is the verdict (`# shop.example: purchase tracking is broken on mobile`), then one meta line, the status with the data quality score, and the counts. Then, in this order: **Changed since last run** and **Trend** (when the site has earlier runs), **Plan vs. site** (with `--against`), **Priorities** (the to-do list per owner: Developer, Tag Manager, Marketing), **Findings** (up to five full blocks with Where, Impact, Cause, Evidence, Also fine on, Owner, Fix, How to verify and a link into `report.html`; the rest one line each under *Also*), **Consent** (the Consent Mode timing sentence and the scenario table with a reason in every cell), **What passed**, **Journeys** (one row per run, then only the steps that carry a problem), **Not checked** (what the run could not judge, with the config key that enables it) and the **Tag inventory**. The per-hit timeline is not in the file; `timeline.csv` has it.
|
|
608
607
|
- `capture.json`: everything captured, for your own analysis, including the tool `version`, the `ai` provider and the computed `metrics` (verdict, score, plan coverage, funnel, platform parity, consent compliance, conversion integrity, timing, priorities).
|
package/README.tr.md
CHANGED
|
@@ -284,7 +284,7 @@ Evet derseniz sonraki çalıştırmalar `--against latest` ile bu plana karşı
|
|
|
284
284
|
|
|
285
285
|
### Plan ve site
|
|
286
286
|
|
|
287
|
-
Bir çalıştırma `--against` kullandığında
|
|
287
|
+
Bir çalıştırma `--against` kullandığında raporda bir **Plan ve site** bölümü bulunur: `report.md` dosyasının başında ve `report.html` içinde Kapsam bölümünde. Planlanan her event bir kovaya düşer:
|
|
288
288
|
|
|
289
289
|
| Kova | Anlamı |
|
|
290
290
|
|---|---|
|
|
@@ -301,7 +301,7 @@ Kontrol edilen plan sitenin ilk planı değilse, rapor başlığı onu bir önce
|
|
|
301
301
|
|
|
302
302
|
### Etiket envanteri
|
|
303
303
|
|
|
304
|
-
Her raporda ayrıca bir **etiket envanteri** bulunur: sitenin istek gönderdiği her sağlayıcı; istekleri sitenin kendi alan adına gittiyse birinci taraf, sağlayıcıya gittiyse üçüncü taraf, ikisine de gittiyse ikisi olarak işaretlenir. Sağlayıcının görüldüğü sayfa sayısını ve ona bağlı çerezleri gösterir. Hiçbir sağlayıcıya ait olmayan çerezler ayrıca birinci ya da üçüncü taraf olarak listelenir. Yalnızca çerez adları kaydedilir, değerleri asla. Envanter `report.html` içindeki **
|
|
304
|
+
Her raporda ayrıca bir **etiket envanteri** bulunur: sitenin istek gönderdiği her sağlayıcı; istekleri sitenin kendi alan adına gittiyse birinci taraf, sağlayıcıya gittiyse üçüncü taraf, ikisine de gittiyse ikisi olarak işaretlenir. Sağlayıcının görüldüğü sayfa sayısını ve ona bağlı çerezleri gösterir. Hiçbir sağlayıcıya ait olmayan çerezler ayrıca birinci ya da üçüncü taraf olarak listelenir. Yalnızca çerez adları kaydedilir, değerleri asla. Envanter `report.html` içindeki **Envanter** bölümünde, `report.md` içinde ise bir tablo olarak yer alır.
|
|
305
305
|
|
|
306
306
|
### Klasör yapısı ve plan kimlikleri
|
|
307
307
|
|
|
@@ -592,17 +592,16 @@ jq -r '.verdict.status + " " + (.score | tostring)' aitd/example.com/runs/*/summ
|
|
|
592
592
|
|
|
593
593
|
Çıktı:
|
|
594
594
|
|
|
595
|
-
- `report.html`:
|
|
596
|
-
-
|
|
597
|
-
- **Bulgular**:
|
|
598
|
-
- **Yolculuklar**: her seferinde bir yolculuk.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
Event'ler ve istekler kaynağa göre filtrelenebilir. GA4 ürün dizgileri (`pr1`…) ve dataLayer `ecommerce.items` ürün kartları olarak gösterilir; özel ürün parametreleri kendi grubunda yer alır. Raporda betik yoktur: sekmeler ve filtreler düz HTML ve CSS'tir (Chrome 105, Safari 15.4 ya da Firefox 121 ve sonrası gerekir) ve yazdırırken her sekme görünür. Logolar gömülüdür, bu yüzden internetten hiçbir şey yüklenmez. `--no-branding`, Analitik İşler logosunu ve kredi satırını çıkarır.
|
|
595
|
+
- `report.html`: tek bir dosya, her seferinde tek bir ekran. Soldaki katlanabilir şerit yedi bölümü listeler; her bölümün, filtrenin ve kartın paylaşılabilir ya da yer imine eklenebilir kendi URL'si vardır ve bir bölümdeki bağlantı diğerinde doğru kartı açar (bir bulgunun "Nerede" çipi o isteği kendi adımında açar).
|
|
596
|
+
- **Özet**: veri kalitesi puanı bir halka olarak; 50'nin altı kırmızı, 89'a kadar sarı, 90 ve üstü yeşil. Yanında karar ve başlığı, puanı neyin düşürdüğünü söyleyen en fazla üç satır (tam puanda hiç satır yok), bulguları önem düzeyine göre filtrelenmiş açan durum çipleri ve çalıştırmayı anlatan altı kart: kontrol edilen sağlayıcılar, her birinde görülen platformlarla yolculuklar, kontrol edilen event'ler, çerez onayı, yapay zekâ sağlayıcısı ve çalıştırmanın kendisi.
|
|
597
|
+
- **Bulgular**: üç grubun (başarısız, doğrulanmamış, geçen) üstünde bir filtre çubuğu (önem düzeyi, sorumlu, platform, yolculuk, ekran, arama). Her satır kapalı başlar ve **Kanıt**, **Düzeltme**, **Nerede** ve **Doğrula** sekmelerine açılır: etkisi, olası nedeni, eksik parametreleri işaretli kanıtı, numaralı düzeltme adımları, yaşandığı adımlar ve düzeltmeyi kontrol edebileceğiniz araçlar (Tag Assistant, GA4 DebugView, Meta Test Events, TikTok Pixel Helper). Sitenin önceki bir çalıştırması varsa **Son çalıştırmadan bu yana değişenler** grupların üstünde yer alır.
|
|
598
|
+
- **Yolculuklar**: her seferinde bir yolculuk ve ekran. Solda sayfalara göre adlandırılmış ("Ana sayfayı aç", "Sepete ekle") ve sayfaya göre gruplanmış adımlar, her adımın ürettiği isteklerle; sağda seçili adım, **İstekler**, **Data Layer** ve **Ağ** sekmeleriyle. Her istek event'ini, temel parametrelerini ve çerez onayı durumunu gösterir; çözülmüş parametrelere, ürün kartlarına ve ham isteğe açılır. Toplu bir GA4 isteği içindeki her event'i gösterir. Sorunlu ilk adımda açılır.
|
|
599
|
+
- **Onay**: her platform için bir satır: seçimden önce, kabulden sonra ve retten sonra ne aldığı; Consent Mode varsayılanı ve güncellemeleri zamanlamalarıyla; ve [onay senaryoları](#onay-senaryoları) tablosu.
|
|
600
|
+
- **Kapsam**: planlanan her event'in her yolculukta gönderilip gönderilmediği, hâlâ ihtiyaç duyduğu parametrelerle; dönüşüm bütünlüğü; ve `--against` ile [Plan ve site](#plan-ve-site) kovaları.
|
|
601
|
+
- **Envanter**: [etiket envanteri](#etiket-envanteri), platform başına bir satır; sunucularına, sayfalarına ve çerezlerine açılır.
|
|
602
|
+
- **Çalıştırma**: çalıştırmanın künyesi (site, sürüm, yapay zekâ sağlayıcısı ve modeli, plan, yolculuklar, ekranlar, platformlar, süreler, dosyalar) ve sitenin önceki çalıştırmaları varsa **Eğilim**.
|
|
603
|
+
|
|
604
|
+
Özet dışında her kart kapalı başlar; her bölümde Tümünü aç ve Tümünü kapat düğmeleri vardır. Sayfada şerit, filtreler, sekmeler ve derin bağlantılar için, sayfanın Content Security Policy'sinde özetiyle izin verilmiş küçük bir betik bulunur; JavaScript olmadan bölümler tek bir belge olarak alt alta dizilir, yazdırma düzeni de budur. Klavye: `[` şeridi katlar, `1`–`7` bir bölüme atlar. Logolar gömülüdür, bu yüzden internetten hiçbir şey yüklenmez. `--no-branding`, Analitik İşler logosunu ve kredi satırını çıkarır.
|
|
606
605
|
- `timeline.csv`: aynı zaman akışı, her isteğe, dataLayer gönderimine ve ağ çağrısına bir satır olarak; Excel ya da Google Sheets için hazır.
|
|
607
606
|
- `report.md`: raporun Markdown hali; bir çekme isteği yorumu, bir Slack mesajı ya da bir e-posta için, dört yolculuklu bir çalıştırmada yaklaşık 100 satır ve 8 KB'ın altında. Başlığı sonuçtur (`# shop.example: purchase izlemesi bozuk (mobile)`), ardından tek bir meta satırı, veri kalitesi puanıyla birlikte durum ve sayılar gelir. Sonra sırasıyla: **Son çalıştırmadan bu yana değişenler** ve **Eğilim** (sitenin önceki çalıştırmaları varsa), **Plan ve site** (`--against` ile), **Öncelikler** (sorumlu başına yapılacaklar listesi: Yazılımcı, Tag Manager, Pazarlama), **Bulgular** (Nerede, Etkisi, Neden, Kanıt, Şurada sorunsuz, Sorumlu, Çözüm, Nasıl doğrulanır ve `report.html` içine bir bağlantıyla en fazla beş tam blok; geri kalanı *Ayrıca* altında birer satır), **Onay** (Consent Mode zamanlama cümlesi ve her hücresinde bir neden bulunan senaryo tablosu), **Sorunsuz olanlar**, **Yolculuklar** (çalıştırma başına bir satır, sonra yalnızca sorun taşıyan adımlar), **Kontrol edilmeyenler** (çalıştırmanın yargılayamadıkları, bunu açan ayar anahtarıyla birlikte) ve **Etiket envanteri**. İstek başına zaman akışı dosyada yoktur; `timeline.csv` içindedir.
|
|
608
607
|
- `capture.json`: yakalanan her şey, kendi analiziniz için; aracın `version` bilgisi, `ai` sağlayıcısı ve hesaplanan `metrics` (sonuç, puan, plan kapsamı, huni, platform tutarlılığı, onay uyumu, dönüşüm bütünlüğü, zamanlama, öncelikler) dahil.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analitikisler/ai-tag-debugger",
|
|
3
3
|
"mcpName": "io.github.analitikisler/ai-tag-debugger",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"description": "Open-source AI agent that QA-tests your website's analytics, ad and consent tracking (GA4, GTM, Google Ads, Meta, TikTok). Bring your own model (Claude, OpenAI, Gemini) or use it from your chat app over MCP.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"setup": "node scripts/setup.js",
|
|
19
19
|
"test": "node --test test/*.test.js",
|
|
20
|
-
"test:unit": "node --test test/campaign.test.js test/changes.test.js test/ci-output.test.js test/checks.test.js test/consent-scenarios.test.js test/doctor.test.js test/gtm.test.js test/history-cli.test.js test/history.test.js test/i18n.test.js test/inventory.test.js test/journeys.test.js test/learn.test.js test/metrics.test.js test/notify.test.js test/parsers.test.js test/plan-check.test.js test/plan-html.test.js test/plan.test.js test/report-md.test.js test/report.test.js test/schema.test.js test/server-json.test.js test/server-side.test.js test/setup.test.js test/workspace.test.js",
|
|
20
|
+
"test:unit": "node --test test/campaign.test.js test/changes.test.js test/ci-output.test.js test/checks.test.js test/consent-scenarios.test.js test/doctor.test.js test/gtm.test.js test/history-cli.test.js test/history.test.js test/html-consent.test.js test/html-findings.test.js test/html-journeys.test.js test/html-scope.test.js test/html-shell.test.js test/html-summary.test.js test/i18n.test.js test/inventory.test.js test/journeys.test.js test/learn.test.js test/metrics.test.js test/notify.test.js test/parsers.test.js test/plan-check.test.js test/plan-html.test.js test/plan.test.js test/report-md.test.js test/report.test.js test/schema.test.js test/server-json.test.js test/server-side.test.js test/setup.test.js test/workspace.test.js",
|
|
21
21
|
"lint": "eslint .",
|
|
22
22
|
"demo:serve": "node examples/serve.js",
|
|
23
23
|
"demo": "node src/cli.js run --config examples/demo.config.json --scripted --no-ai"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// The Consent section: one status line per platform, the Consent Mode card and the scenarios card. Denied is red,
|
|
2
|
+
// granted green and "no signal" grey, all by token through chip("consent", ...).
|
|
3
|
+
import { consentTable } from "../consent.js";
|
|
4
|
+
import { isConsentEntry } from "../report-shared.js";
|
|
5
|
+
import { PLATFORM_ART, chip, esc, platformLabel, safeKey, sectionBlock, sortPlatforms, stepTitle, secs, use, badge } from "./lib.js";
|
|
6
|
+
|
|
7
|
+
// Google tags carry Consent Mode signals on the hit; every other platform sends none, so its state is "none".
|
|
8
|
+
const GOOGLE = new Set(["ga4", "google_ads"]);
|
|
9
|
+
const SCENARIOS = ["before", "accept", "reject", "analytics"];
|
|
10
|
+
const LINE_CHECKS = new Set(["before_consent", "after_reject", "consent_default_missing"]);
|
|
11
|
+
const MODE_CHECKS = new Set(["consent_default_missing", "consent_default_late", "consent_v2_missing", "consent_v2_update"]);
|
|
12
|
+
const SEVERITY_RANK = { broken: 0, risk: 1, warning: 2, info: 3 };
|
|
13
|
+
|
|
14
|
+
/** A platform's artwork, or its initials when it has none. Decorative: the chip's text names it once for screen readers. */
|
|
15
|
+
function platArt(key) {
|
|
16
|
+
const k = safeKey(key);
|
|
17
|
+
if (PLATFORM_ART[k]) return use(`p-${k}`, 13);
|
|
18
|
+
const initials = platformLabel(key).split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
|
|
19
|
+
return `<span class="plat-fb s14" aria-hidden="true">${esc(initials)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The worst problem row among these, or null. Accepted rows (the baseline's) do not count, as everywhere else. */
|
|
23
|
+
const worstRow = (rows) => rows.filter((f) => !f.accepted && f.severity in SEVERITY_RANK).sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0] ?? null;
|
|
24
|
+
|
|
25
|
+
/** denied / granted for a Google platform, none for the rest; null when the tag sent nothing in that scenario. */
|
|
26
|
+
function stateOf(platform, c) {
|
|
27
|
+
if (!c?.n) return null;
|
|
28
|
+
if (!GOOGLE.has(platform)) return "none";
|
|
29
|
+
return c.cookieless >= c.n || c.adDenied >= c.n ? "denied" : "granted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What was sent after a scenario's choice (before: with no choice yet); undefined when the scenario did not run. */
|
|
33
|
+
const afterOf = (sc, s) => (s === "before" ? sc.before : sc.states?.[s]?.after);
|
|
34
|
+
|
|
35
|
+
/** The consent signals of a Consent Mode command, as the chips a hit carries: key and value, coloured by value. */
|
|
36
|
+
function signalChips(m, signals) {
|
|
37
|
+
return Object.entries(signals ?? {}).filter(([, v]) => typeof v === "string")
|
|
38
|
+
.map(([k, v]) => {
|
|
39
|
+
const state = v === "granted" || v === "denied" ? v : "none";
|
|
40
|
+
return chip("consent", state, `<span class="k">${esc(k)}</span>${esc(v)}`);
|
|
41
|
+
}).join("");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A neutral outlined chip for a plain label or number. */
|
|
45
|
+
const plain = (html, cls = "") => `<span class="chip plain${cls ? ` ${cls}` : ""}">${html}</span>`;
|
|
46
|
+
|
|
47
|
+
/** @param {import("./shell.js").Ctx} ctx */
|
|
48
|
+
export function render(ctx) {
|
|
49
|
+
const { m, runs, rows, ids, metrics, consentScenarios: sc } = ctx;
|
|
50
|
+
const t = m.html.consent;
|
|
51
|
+
const sevChip = (sev, label) => chip("sev", sev, `${use(`sev-${sev}`, 13)}${esc(label)}`);
|
|
52
|
+
const stateWord = (state) => t.state[state];
|
|
53
|
+
const stateChip = (state, label = stateWord(state)) => chip("consent", state, esc(label));
|
|
54
|
+
const platChip = (key) => chip("plat", key, `${platArt(key)}${esc(platformLabel(key))}`);
|
|
55
|
+
const findingLink = (row, cls = "") => (row && ids.get(row) ? `<a${cls ? ` class="${cls}"` : ""} href="#${ids.get(row)}">${esc(m.html.findingLink)}</a>` : "");
|
|
56
|
+
|
|
57
|
+
/* ---- one status line per platform ---- */
|
|
58
|
+
const total = (platform, s) => {
|
|
59
|
+
const after = sc ? afterOf(sc, s) : null;
|
|
60
|
+
if (!after) return "";
|
|
61
|
+
const byEvent = after[platform] ?? {};
|
|
62
|
+
const count = Object.values(byEvent).reduce((a, c) => a + c.n, 0);
|
|
63
|
+
if (!count) return `<span class="chip zero">0</span>`;
|
|
64
|
+
const states = new Set(Object.values(byEvent).map((c) => stateOf(platform, c)).filter(Boolean));
|
|
65
|
+
// One state for every event, else the strictest: a mix that holds a denied event reads denied.
|
|
66
|
+
const state = states.size === 1 ? [...states][0] : states.has("denied") ? "denied" : "none";
|
|
67
|
+
return stateChip(state, t.tally(count, stateWord(state)));
|
|
68
|
+
};
|
|
69
|
+
const platforms = sortPlatforms(new Set(metrics.consent.platforms.map((p) => p.platform)));
|
|
70
|
+
const lines = platforms.map((platform) => {
|
|
71
|
+
const own = rows.filter((f) => LINE_CHECKS.has(f.check) && f.platform === platform);
|
|
72
|
+
const row = worstRow(own);
|
|
73
|
+
// The scenarios can show a tag sent after Reject with no finding row for it; with no row at all (not even an
|
|
74
|
+
// accepted one) the metric decides, so the line agrees with the flagged cell below.
|
|
75
|
+
const sev = row?.severity ?? (!own.length && metrics.consent.platforms.find((p) => p.platform === platform)?.status !== "pass" ? "risk" : "ok");
|
|
76
|
+
const cells = SCENARIOS.slice(0, 3).filter((s) => sc && afterOf(sc, s))
|
|
77
|
+
.map((s) => `<span class="cs-p"><span class="cs-k">${esc(t.cols[s])}</span>${total(platform, s)}</span>`).join("");
|
|
78
|
+
return `<li class="card sev-${sev} cs-row" id="consent-${safeKey(platform)}">${platChip(platform)}${sevChip(sev, m.severity[sev])}${cells ? `<span class="cs-cells">${cells}</span>` : ""}${findingLink(row, "cs-go")}</li>`;
|
|
79
|
+
}).join("");
|
|
80
|
+
const list = lines ? `<ul class="cs-list" aria-label="${esc(t.statusLabel)}">${lines}</ul>` : "";
|
|
81
|
+
|
|
82
|
+
/* ---- Consent Mode ---- */
|
|
83
|
+
const mode = metrics.consent.consentMode;
|
|
84
|
+
const perRun = runs.map((run, ri) => {
|
|
85
|
+
const cmds = (run.dataLayerLog ?? []).filter((e) => isConsentEntry(e.value) && (e.value[1] === "default" || e.value[1] === "update"));
|
|
86
|
+
const first = cmds.find((e) => e.value[1] === "default");
|
|
87
|
+
const update = cmds.find((e) => e.value[1] === "update" && (!first || e.t >= first.t));
|
|
88
|
+
return { run, info: mode.runs[ri], first, update, resent: cmds.filter((e) => e !== first && e !== update).length };
|
|
89
|
+
});
|
|
90
|
+
const runChip = (run) => chip("vp", run.viewport, `${run.viewport === "desktop" || run.viewport === "mobile" ? use(`ui-${run.viewport}`, 13) : ""}${esc(run.journeyId)}<span class="sr"> ${esc(run.viewport)}</span>`);
|
|
91
|
+
const modeFinding = worstRow(rows.filter((f) => MODE_CHECKS.has(f.check)));
|
|
92
|
+
const withDefault = perRun.filter((p) => p.info?.present);
|
|
93
|
+
const ms = withDefault.map((p) => p.info.ms).filter((x) => x !== null);
|
|
94
|
+
const late = mode.defaultOnTime === false;
|
|
95
|
+
let modeHead;
|
|
96
|
+
if (!runs.length) modeHead = chip("consent", "none", esc(m.ui.notChecked));
|
|
97
|
+
else if (!mode.defaultPresent) modeHead = modeFinding ? sevChip(modeFinding.severity, t.noDefault) : chip("consent", "none", esc(t.noDefault));
|
|
98
|
+
else modeHead = late ? sevChip(modeFinding?.severity ?? "risk", t.defaultLate) : sevChip("ok", t.defaultFirst);
|
|
99
|
+
const modeChips = `${ms.length ? plain(`${use("ui-clock", 13)}<b>${esc(t.timing(Math.min(...ms), Math.max(...ms)))}</b>`, "mono") : ""}${withDefault.length && mode.v2Signals.present ? sevChip("ok", t.v2) : withDefault.length ? sevChip("warning", t.v2Missing) : ""}`;
|
|
100
|
+
|
|
101
|
+
// Runs that pushed the same commands on the same steps share one pair of rows; a run that differs gets its own.
|
|
102
|
+
const groups = new Map();
|
|
103
|
+
for (const p of withDefault) {
|
|
104
|
+
const key = JSON.stringify([p.first?.value[2], p.update?.value[2], p.info.stepLabel, p.update?.step]);
|
|
105
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
106
|
+
groups.get(key).push(p);
|
|
107
|
+
}
|
|
108
|
+
const commandRow = (label, p, e) => {
|
|
109
|
+
if (!e) return "";
|
|
110
|
+
const step = p.run.steps.find((s) => s.index === e.step);
|
|
111
|
+
const where = `${step ? `${m.ui.step} ${step.index} · ${esc(stepTitle(p.run, step))} · ` : ""}${esc(secs(p.run, e.t))}`;
|
|
112
|
+
return `<li><b class="cm-l">${esc(label)}</b><span class="cm-w">${where}</span><span class="cm-s">${signalChips(m, e.value[2])}</span></li>`;
|
|
113
|
+
};
|
|
114
|
+
const groupHtml = [...groups.values()].map((g) => `${groups.size > 1 ? `<div class="chips cm-runs">${g.map((p) => runChip(p.run)).join("")}</div>` : ""}<ol class="cm-list">${commandRow(m.ui.consentDefault, g[0], g[0].first)}${commandRow(m.ui.consentUpdate, g[0], g[0].update)}</ol>`).join("");
|
|
115
|
+
const timing = withDefault.map((p) => `<li>${runChip(p.run)}<span class="xs">${esc(m.html.consentTiming(p.info.ms, p.info.beforeFirstGoogleHit, p.info.v2Signals.present, p.info.v2Signals.missing))}</span></li>`).join("");
|
|
116
|
+
const resent = Math.max(0, ...withDefault.map((p) => p.resent));
|
|
117
|
+
const modeFindings = rows.filter((f) => MODE_CHECKS.has(f.check) && !f.accepted && ids.get(f)).map((f) => findingLink(f)).join(" ");
|
|
118
|
+
const none = !runs.length ? m.ui.md.consentNotChecked : m.ui.md.consentModeNone;
|
|
119
|
+
const modeBody = withDefault.length
|
|
120
|
+
? `${timing ? `<ul class="cm-timing">${timing}</ul>` : ""}${groupHtml}${resent ? `<p class="xs muted cm-rep">${use("ui-clock", 12)} ${esc(t.resent(resent))}</p>` : ""}${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`
|
|
121
|
+
: `<p class="muted xs">${esc(none)}</p>${modeFindings ? `<p class="xs cm-find">${modeFindings}</p>` : ""}`;
|
|
122
|
+
const modeCard = `<details class="card" id="consent-mode"><summary><span class="t">${esc(m.html.consentMode)}</span>${modeHead}${modeChips}<span class="end muted xs">${esc(t.runsWith(withDefault.length, runs.length))}</span></summary><div class="body">${modeBody}</div></details>`;
|
|
123
|
+
|
|
124
|
+
/* ---- scenarios ---- */
|
|
125
|
+
if (!sc) return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${modeCard}\n<p class="muted xs cs-none" id="consent-scenarios">${esc(m.ui.md.noScenarios)}</p>`);
|
|
126
|
+
const table = consentTable(sc);
|
|
127
|
+
const cols = table.cols.filter((s) => s !== "analytics" || sc.states?.analytics?.via);
|
|
128
|
+
const before = rows.filter((f) => f.check === "before_consent" && !f.accepted);
|
|
129
|
+
const problemRow = (platform, event) => rows.find((f) => f.platform === platform && f.event === event && !f.accepted && /^(after_reject|before_consent)$/.test(f.check));
|
|
130
|
+
const reasonOf = (c) => (c.cookieless >= c.n ? m.html.cellCookieless : c.adDenied >= c.n ? m.html.cellAdDenied : m.html.cellWithCookies);
|
|
131
|
+
let problemCount = 0;
|
|
132
|
+
const cellHtml = (r, cell) => {
|
|
133
|
+
const after = afterOf(sc, cell.state);
|
|
134
|
+
if (!after || cell.count === null) return `<td class="muted">–<span class="sr"> ${esc(m.ui.scenarioNotRun)}</span></td>`;
|
|
135
|
+
const c = after[r.platform]?.[r.event];
|
|
136
|
+
if (!c?.n) return `<td class="muted">0</td>`;
|
|
137
|
+
const state = stateOf(r.platform, c);
|
|
138
|
+
const reason = reasonOf(c);
|
|
139
|
+
const flagged = cell.problem || (cell.state === "before" && before.some((f) => f.platform === r.platform && f.event === r.event));
|
|
140
|
+
const body = `<b class="num">${c.n}</b><span title="${esc(reason)}">${stateChip(state)}</span><span class="sr"> ${esc(reason)}</span>`;
|
|
141
|
+
if (!flagged) return `<td><span class="cs-c">${body}</span></td>`;
|
|
142
|
+
problemCount++;
|
|
143
|
+
const f = problemRow(r.platform, r.event);
|
|
144
|
+
return `<td class="cs-bad"><span class="cs-c">${body}${badge(f?.severity ?? "risk", t.sent)}<span class="sr"> ${esc(m.html.cellProblem)} ${esc(m.html.cellShouldNot)}</span>${findingLink(f)}</span></td>`;
|
|
145
|
+
};
|
|
146
|
+
const body = table.rows.map((r) => `<tr><th scope="row"><span class="cs-tag">${platChip(r.platform)}<code>${esc(r.event)}</code></span></th>${cols.map((s) => cellHtml(r, r.cells.find((c) => c.state === s))).join("")}</tr>`).join("");
|
|
147
|
+
const supported = (s) => (s === "before" ? true : Boolean(sc.states?.[s]?.after));
|
|
148
|
+
const summaryChips = SCENARIOS.map((s) => (supported(s) ? plain(esc(t.cols[s])) : chip("consent", "none", esc(`${t.cols[s]} · ${t.notSupported}`)))).join("");
|
|
149
|
+
const probChip = problemCount ? sevChip("risk", t.problems(problemCount)) : sevChip("ok", t.noProblems);
|
|
150
|
+
const scenarios = `<details class="card" id="consent-scenarios"><summary><span class="t">${esc(t.scenarios)}</span>${summaryChips}<span class="end">${probChip}</span></summary><div class="body"><div class="tscroll"><table class="tbl cs-tbl"><caption class="sr">${esc(m.ui.scenarios)}</caption><thead><tr><th scope="col">${esc(m.ui.scenarioTag)}</th>${cols.map((s) => `<th scope="col">${esc(t.cols[s])}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div></div></details>`;
|
|
151
|
+
return sectionBlock("consent", esc(m.html.shell.sections.consent), `${list}\n${modeCard}\n${scenarios}`);
|
|
152
|
+
}
|