@analitikisler/ai-tag-debugger 0.22.4 → 0.22.5
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 +8 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/package.json +1 -1
- package/src/agent.js +3 -1
- package/src/browser.js +32 -1
- package/src/checks.js +10 -1
- package/src/containers.js +60 -3
- package/src/html/scope.js +1 -1
- package/src/i18n.js +22 -0
- package/src/mcp.js +17 -6
- package/src/report.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.5 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
- Runs can report findings 0.22.4 did not on the same site (`container_not_run`, `container_held`). A baseline written with 0.22.4 can then list new entries; `--update-baseline` rewrites it.
|
|
6
|
+
- After every step the tool reads Google's own registry of the containers that ran (`google_tag_manager`), so the Google containers card, `report.md` and MCP say when a container never ran, however its script got into the page.
|
|
7
|
+
- New check `container_not_run` (warning): a GTM container was downloaded or asked for but never ran on any page of the run, so none of its tags fired. When a consent platform held it back and the run never consented, `container_held` (info) says so instead.
|
|
8
|
+
- Fixed: a container the page only preloaded (`<link rel="preload">`, as Next.js does before its own code adds the script) or that ran without the tool seeing it added (as when a consent platform's auto-blocking replaces the browser's own functions) was missing from the containers card. It now has a row saying which.
|
|
9
|
+
- Found on a live Next.js site: GTM was only preloaded and never ran, because the site's own scripts were served as HTML and its app crashed on start. `page_crashed` reported the crash; the containers card now also shows that GTM never ran.
|
|
10
|
+
|
|
3
11
|
## 0.22.4 (2026-11-04)
|
|
4
12
|
|
|
5
13
|
- Runs can report a finding 0.22.3 did not on the same site (`iframe_message_unhandled`). A baseline written with 0.22.3 can then list new entries; `--update-baseline` rewrites it.
|
package/README.md
CHANGED
|
@@ -327,7 +327,7 @@ Under the inventory, **Google containers** lists every GTM container (`gtm.js`)
|
|
|
327
327
|
|
|
328
328
|
Each row also gives the container's published version (the number its `gtm.js` or `gtag/js` data opens with, as in GTM's Versions tab), the script's size and how long it took to load, the dataLayer the container reads (`l=`), whether it came from Google or from the site's own domain (the Google tag gateway or a server container), the GTM environment when the snippet names one, and the pages it loaded on. A container reading a dataLayer of another name has its pushes recorded too, marked with that name. `report.md` has the same table. Over MCP, step results and `get_step` list the containers each step loaded, `get_containers` lists them all while the audit is open, `finish_audit` and `run_audit` end with them, `get_report` returns them with `part: "containers"` and the JSON timeline carries them per step. `capture.json` keeps the loads under `containerLoads`. The environment token (`gtm_auth`) and the page's query string are never kept.
|
|
329
329
|
|
|
330
|
-
[The guide](docs/guides/containers-and-widgets.md) explains each finding below with what to do.
|
|
330
|
+
[The guide](docs/guides/containers-and-widgets.md) explains each finding below with what to do. After every step the tool also reads Google's own registry of the containers that ran (`google_tag_manager`), so a container that downloaded but never ran is reported, and a container the page only preloaded (`<link rel="preload">`), or that ran without the tool seeing it added, still gets a row. Seven checks read it: `container_not_run` (warning) for a GTM container that was downloaded or asked for but never ran, so none of its tags fired, or `container_held` (info) instead when a consent platform held it back and the run never consented; `container_failed` (warning) for a container script that failed to load or was answered with an error, so none of its tags ran, `container_duplicate` (warning) for a container loaded more than once into one page, `container_environment` (warning) for a GTM environment (`gtm_preview`, `gtm_auth`) loaded instead of Live, `datalayer_split` (info) for containers on one page reading different dataLayers, and `container_send_to_only` (info) for a destination that only an event's `send_to` loaded, with no `gtag('config')` for it.
|
|
331
331
|
|
|
332
332
|
### Embedded widgets and iframes
|
|
333
333
|
|
package/README.tr.md
CHANGED
|
@@ -327,7 +327,7 @@ Envanterin altında **Google container'ları**, sayfaların yüklediği her GTM
|
|
|
327
327
|
|
|
328
328
|
Her satır ayrıca container'ın yayımlanmış sürümünü (`gtm.js` ya da `gtag/js` verisinin başladığı numara; GTM'deki Sürümler sekmesindekiyle aynı), betiğin boyutunu ve ne kadar sürede yüklendiğini, container'ın okuduğu dataLayer'ı (`l=`), Google'dan mı yoksa sitenin kendi alan adından mı (Google etiket ağ geçidi ya da bir sunucu container'ı) geldiğini, snippet bir GTM ortamı adlandırıyorsa onu ve yüklendiği sayfaları verir. Başka adlı bir dataLayer okuyan container'ın push'ları da o adla işaretlenerek kaydedilir. `report.md` aynı tabloyu içerir. MCP'de adım sonuçları ve `get_step` her adımın yüklediği container'ları listeler, `get_containers` denetim açıkken hepsini listeler, `finish_audit` ve `run_audit` sonuçları onlarla biter, `get_report` onları `part: "containers"` ile döndürür ve JSON zaman akışı onları adım adım taşır. `capture.json` yüklemeleri `containerLoads` altında saklar. Ortam belirteci (`gtm_auth`) ve sayfanın sorgu dizesi asla saklanmaz.
|
|
329
329
|
|
|
330
|
-
[Rehber](docs/guides/containers-and-widgets.tr.md) aşağıdaki her bulguyu ne yapılacağıyla anlatır.
|
|
330
|
+
[Rehber](docs/guides/containers-and-widgets.tr.md) aşağıdaki her bulguyu ne yapılacağıyla anlatır. Araç her adımdan sonra Google'ın çalışan container'lar kaydını (`google_tag_manager`) da okur; böylece indirilip hiç çalışmayan bir container bildirilir, sayfanın yalnızca ön yüklediği (`<link rel="preload">`) ya da eklendiği görülmeden çalışan bir container da yine bir satır alır. Yedi kontrol bunu okur: indirilen ya da istenen ama hiç çalışmayan, bu yüzden etiketlerinin hiçbiri çalışmayan bir GTM container'ı için `container_not_run` (uyarı), bir onay platformu onu beklettiyse ve çalıştırmada onay verilmediyse bunun yerine `container_held` (bilgi); yüklenemeyen ya da hatayla yanıtlanan, bu yüzden etiketlerinin hiçbiri çalışmayan bir container betiği için `container_failed` (uyarı), aynı sayfaya birden fazla kez yüklenen bir container için `container_duplicate` (uyarı), Live yerine yüklenen bir GTM ortamı (`gtm_preview`, `gtm_auth`) için `container_environment` (uyarı), aynı sayfada farklı dataLayer'ları okuyan container'lar için `datalayer_split` (bilgi) ve kendisi için `gtag('config')` olmadan yalnızca bir olayın `send_to` değeriyle yüklenen bir hedef için `container_send_to_only` (bilgi).
|
|
331
331
|
|
|
332
332
|
### Gömülü widget'lar ve iframe'ler
|
|
333
333
|
|
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.22.
|
|
4
|
+
"version": "0.22.5",
|
|
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": {
|
package/src/agent.js
CHANGED
|
@@ -238,7 +238,7 @@ export const journeyError = (err) => `The journey stopped with an error: ${err?.
|
|
|
238
238
|
export function failedRun(journey, viewport, err, providers) {
|
|
239
239
|
return {
|
|
240
240
|
journeyId: journey.id, viewport, completed: false, note: journeyError(err), hits: [], steps: [], consentStep: null, rejectStep: null,
|
|
241
|
-
cookieLog: [], cookieChanges: [], dataLayerLog: [], containerLoads: [], network: [], widgetHits: [], widgetDataLayerLog: [], containerScripts: [], frameMessages: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
|
|
241
|
+
cookieLog: [], cookieChanges: [], dataLayerLog: [], containerLoads: [], network: [], widgetHits: [], widgetDataLayerLog: [], containerScripts: [], containersRunning: [], containerPreloads: [], frameMessages: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -259,6 +259,8 @@ export function collect(session, journey, viewport) {
|
|
|
259
259
|
...(session.opts.blockHits === false ? { live: true } : {}),
|
|
260
260
|
containerLoads: session.containerLoads,
|
|
261
261
|
containerScripts: session.containerScripts,
|
|
262
|
+
containersRunning: [...session.containersRunning].map(([id, step]) => ({ id, step })),
|
|
263
|
+
containerPreloads: [...session.containerPreloads.values()],
|
|
262
264
|
frameMessages: session.frameMessages,
|
|
263
265
|
network: session.network,
|
|
264
266
|
providers: session.providers,
|
package/src/browser.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hostScope, isPrivateAddress, navigationError, privateHostChecker } from
|
|
|
10
10
|
import { startEgressProxy } from "./egress.js";
|
|
11
11
|
import { campaignParamsIn } from "./campaign.js";
|
|
12
12
|
import { detectCmpInPage } from "./cmp.js";
|
|
13
|
-
import { callersOf, containerOf, containerVersion } from "./containers.js";
|
|
13
|
+
import { callersOf, containerOf, containerVersion, isContainerId } from "./containers.js";
|
|
14
14
|
import { frameInfo } from "./frames.js";
|
|
15
15
|
|
|
16
16
|
// Consent banners of the common consent platforms (CMPs), by their container. A cover that matches
|
|
@@ -106,6 +106,9 @@ const CONTAINER_HOOK = `
|
|
|
106
106
|
new MutationObserver((list) => {
|
|
107
107
|
for (const m of list) for (const n of m.addedNodes) {
|
|
108
108
|
if (n instanceof HTMLScriptElement && n.src && !seen.has(n)) { seen.add(n); report(n, n.src, "parser"); }
|
|
109
|
+
// A preload hint downloads a container without running it; caught as it is added, since a page's own code
|
|
110
|
+
// can rewrite <head> and take it out again.
|
|
111
|
+
else if (n instanceof HTMLLinkElement && /(^|\\s)(preload|modulepreload|prefetch)(\\s|$)/i.test(n.rel) && n.href && !seen.has(n)) { seen.add(n); report(n, n.href, "preload"); }
|
|
109
112
|
}
|
|
110
113
|
}).observe(document, { childList: true, subtree: true });
|
|
111
114
|
} catch {}
|
|
@@ -516,6 +519,17 @@ export function googleConsentStateInPage() {
|
|
|
516
519
|
return { entries, usedDefault: Boolean(ics.usedDefault), usedUpdate: Boolean(ics.usedUpdate), wasSetLate: Boolean(ics.wasSetLate), waitPeriodTimedOut: Boolean(ics.waitPeriodTimedOut) };
|
|
517
520
|
}
|
|
518
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Runs in the page: the IDs Google's own registry (window.google_tag_manager) holds, which are the containers and
|
|
524
|
+
* Google tags that actually ran, however their script got in; and the addresses the page only preloads
|
|
525
|
+
* (<link rel="preload">, "modulepreload", "prefetch"), which download a script without running it.
|
|
526
|
+
*/
|
|
527
|
+
export function containersInPage() {
|
|
528
|
+
const registry = window.google_tag_manager && typeof window.google_tag_manager === "object" ? Object.keys(window.google_tag_manager) : [];
|
|
529
|
+
const preloads = [...document.querySelectorAll('link[rel~="preload"], link[rel~="modulepreload"], link[rel~="prefetch"]')].map((l) => l.href).filter(Boolean);
|
|
530
|
+
return { registry: registry.slice(0, 200), preloads: preloads.slice(0, 200) };
|
|
531
|
+
}
|
|
532
|
+
|
|
519
533
|
/**
|
|
520
534
|
* Runs in the page: whether it is something other than the site, so what a step captured there says nothing about
|
|
521
535
|
* the site's tracking. A bot challenge in its place (Cloudflare, DataDome, HUMAN, Akamai), or a page that crashed in
|
|
@@ -577,6 +591,8 @@ export class AuditSession {
|
|
|
577
591
|
this.dataLayerLog = [];
|
|
578
592
|
this.containerLoads = []; // each Google container script the page added, with how (see src/containers.js)
|
|
579
593
|
this.containerScripts = []; // each container script's answer: status, version, size and load time
|
|
594
|
+
this.containersRunning = new Map(); // container ID -> the step it was first seen running at (google_tag_manager)
|
|
595
|
+
this.containerPreloads = new Map(); // "id|host" -> { id, host, step } for containers the page only preloaded
|
|
580
596
|
this.frameMessages = []; // each named message an iframe posted to the page: origin, name and field names, never values
|
|
581
597
|
// The order hits, dataLayer pushes and iframe messages were recorded in. Several often share a millisecond, and
|
|
582
598
|
// telling which came first matters: a page's push answers the message just before it (see messageOutcomes).
|
|
@@ -870,6 +886,12 @@ export class AuditSession {
|
|
|
870
886
|
recordLoad(entry, frame = null) {
|
|
871
887
|
const container = containerOf(entry?.src);
|
|
872
888
|
if (!container) return;
|
|
889
|
+
// A preload hint is not a load: it downloads the script without running it (see unseenContainers).
|
|
890
|
+
if (entry.how === "preload") {
|
|
891
|
+
const k = `${container.id}|${container.host}`;
|
|
892
|
+
if (!frame && !this.containerPreloads.has(k)) this.containerPreloads.set(k, { id: container.id, host: container.host, step: this.currentStep, page: pagePath(entry.page) });
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
873
895
|
this.record("containerLoads", {
|
|
874
896
|
...container,
|
|
875
897
|
how: entry.how === "parser" ? "parser" : "script",
|
|
@@ -1240,6 +1262,15 @@ export class AuditSession {
|
|
|
1240
1262
|
const tagConsent = await this.evaluateWithin(googleConsentStateInPage);
|
|
1241
1263
|
const before = this.steps.slice(0, this.currentStep).map((st) => st.tagConsent).filter(Boolean).at(-1);
|
|
1242
1264
|
if (tagConsent && JSON.stringify(tagConsent) !== JSON.stringify(before)) this.steps[this.currentStep].tagConsent = tagConsent;
|
|
1265
|
+
// Which containers ran, and which the page only preloaded: a container can download and never run.
|
|
1266
|
+
const found = await this.evaluateWithin(containersInPage);
|
|
1267
|
+
for (const id of (found?.registry ?? []).map((k) => String(k).toUpperCase()).filter(isContainerId)) {
|
|
1268
|
+
if (!this.containersRunning.has(id)) this.containersRunning.set(id, this.currentStep);
|
|
1269
|
+
}
|
|
1270
|
+
for (const c of (found?.preloads ?? []).map(containerOf).filter(Boolean)) {
|
|
1271
|
+
const k = `${c.id}|${c.host}`;
|
|
1272
|
+
if (!this.containerPreloads.has(k)) this.containerPreloads.set(k, { id: c.id, host: c.host, step: this.currentStep, page: pagePath(this.page.url()) });
|
|
1273
|
+
}
|
|
1243
1274
|
}
|
|
1244
1275
|
|
|
1245
1276
|
async perform(a) {
|
package/src/checks.js
CHANGED
|
@@ -5,7 +5,7 @@ import { messages } from "./i18n.js";
|
|
|
5
5
|
import { isOwnAddress } from "./redact.js";
|
|
6
6
|
import { campaignParamsIn } from "./campaign.js";
|
|
7
7
|
import { CONSENT_SIGNALS } from "./parsers.js";
|
|
8
|
-
import { duplicateLoads, loadSources, scriptFailed, splitLayers } from "./containers.js";
|
|
8
|
+
import { containersNotRun, duplicateLoads, loadSources, scriptFailed, splitLayers } from "./containers.js";
|
|
9
9
|
import { messageOutcomes, widgetSummaries, widgetsOf } from "./frames.js";
|
|
10
10
|
|
|
11
11
|
const platformName = (p) => ({ meta: "Meta", tiktok: "TikTok" })[p] ?? p;
|
|
@@ -394,6 +394,15 @@ export function checkRun(plan, run, { lang = "en" } = {}) {
|
|
|
394
394
|
findings.push({ ...base, severity: "warning", check: "container_failed", step: stepLabel(sc.step), detail: d.containerFailed(sc.id, sc.host, String(sc.status)) });
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
// A GTM container downloaded or asked for that never ran. Expected while a consent platform holds it back and the
|
|
398
|
+
// run never consented; otherwise none of its tags fired.
|
|
399
|
+
const consented = consentGrantedStep(run) !== null;
|
|
400
|
+
for (const c of containersNotRun(run)) {
|
|
401
|
+
findings.push(run.cmp?.vendor && !consented
|
|
402
|
+
? { ...base, severity: "info", check: "container_held", step: stepLabel(c.step), detail: d.containerHeld(c.id, run.cmp.vendor) }
|
|
403
|
+
: { ...base, severity: "warning", check: "container_not_run", step: stepLabel(c.step), detail: d.containerNotRun(c.id, c.kind) });
|
|
404
|
+
}
|
|
405
|
+
|
|
397
406
|
// Widgets that tracked on their own, left out of the plan checks: said once per widget, so nothing goes missing silently.
|
|
398
407
|
for (const w of widgetsOf(run.widgetHits ?? [])) {
|
|
399
408
|
const first = (run.widgetHits ?? []).find((h) => h.frame.origin === w.origin);
|
package/src/containers.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// Container and destination IDs gtm.js and gtag/js are loaded with.
|
|
8
8
|
const CONTAINER_ID = /^(GTM|G|GT|AW|DC|MC|HA)-[A-Z0-9]+$/;
|
|
9
9
|
|
|
10
|
+
/** Whether a string is a container or Google tag ID: GTM-…, G-…, GT-…, AW-…, DC-… and the like. */
|
|
11
|
+
export const isContainerId = (id) => CONTAINER_ID.test(String(id));
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* The container a script URL loads, or null: its ID, gtm or gtag, the dataLayer it reads (`l`), the host it
|
|
12
15
|
* came from (Google's, or the site's own through the Google tag gateway or a server container) and the GTM
|
|
@@ -148,13 +151,16 @@ export function loadSources(run) {
|
|
|
148
151
|
|
|
149
152
|
/**
|
|
150
153
|
* One row per container and source across runs, in the order first seen: { id, script, layer, served, host,
|
|
151
|
-
* environment?, kind, by?, frame?, version?, bytes?, transferBytes?, ms?, failed?, pages, journeys }. frame is
|
|
152
|
-
* iframe it loaded in; version to failed are its script's answer (see scriptFacts), from the first load in the
|
|
154
|
+
* environment?, kind, by?, frame?, version?, bytes?, transferBytes?, ms?, failed?, ran?, pages, journeys }. frame is
|
|
155
|
+
* the iframe it loaded in; version to failed are its script's answer (see scriptFacts), from the first load in the
|
|
156
|
+
* row; ran says whether Google's registry held it on some step (left out for captures that didn't record it).
|
|
157
|
+
* Containers the page asked for without the tool seeing a script added are rows too (see unseenContainers).
|
|
153
158
|
*/
|
|
154
159
|
export function containerRows(runs) {
|
|
155
160
|
const rows = new Map();
|
|
156
161
|
for (const run of runs) {
|
|
157
|
-
|
|
162
|
+
const ran = Array.isArray(run.containersRunning) ? new Set(run.containersRunning.map((c) => c.id)) : null;
|
|
163
|
+
for (const l of [...loadSources(run), ...unseenContainers(run)]) {
|
|
158
164
|
const k = [l.id, l.kind, l.by ?? "", l.host, l.layer, l.environment ?? "", l.frame?.origin ?? ""].join("|");
|
|
159
165
|
const row = rows.get(k) ?? {
|
|
160
166
|
id: l.id, script: l.script, layer: l.layer, served: l.served, host: l.host,
|
|
@@ -165,12 +171,63 @@ export function containerRows(runs) {
|
|
|
165
171
|
};
|
|
166
172
|
if (l.page) row.pages.add(l.page);
|
|
167
173
|
row.journeys.add(`${run.journeyId} (${run.viewport})`);
|
|
174
|
+
// Whether it ran (Google's registry held it) in any run that recorded the registry; unknown for older captures.
|
|
175
|
+
if (ran) row.ran = Boolean(row.ran) || ran.has(l.id);
|
|
168
176
|
rows.set(k, row);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
return [...rows.values()].map((r) => ({ ...r, pages: [...r.pages].sort(), journeys: [...r.journeys] }));
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Containers a run met without seeing a script added for them: a script the page requested or preloaded, or an ID
|
|
184
|
+
* that ran (Google's registry held it), with no load recorded. kind "preload": only a <link rel="preload"> asked for
|
|
185
|
+
* it, which downloads a script without running it; "unseen": something added it in a way the tool couldn't follow,
|
|
186
|
+
* as when a consent platform's auto-blocking replaces the browser's own functions. One per container and host.
|
|
187
|
+
*/
|
|
188
|
+
export function unseenContainers(run) {
|
|
189
|
+
const loads = run.containerLoads ?? [];
|
|
190
|
+
const loaded = (id, host) => loads.some((l) => l.id === id && (!host || l.host === host));
|
|
191
|
+
const preloads = run.containerPreloads ?? [];
|
|
192
|
+
const out = new Map();
|
|
193
|
+
// The page a step was on: the preload's own, else the step's (see pagePath in browser.js).
|
|
194
|
+
const pageAt = (step, page) => page ?? run.steps?.[step]?.pagePath ?? undefined;
|
|
195
|
+
const add = (id, host, kind, step, facts = {}, page) => {
|
|
196
|
+
const k = `${id}|${host ?? ""}`;
|
|
197
|
+
if (out.has(k) || loaded(id, host)) return;
|
|
198
|
+
const at = pageAt(step, page);
|
|
199
|
+
out.set(k, {
|
|
200
|
+
id, script: id.startsWith("GTM-") ? "gtm" : "gtag", layer: null,
|
|
201
|
+
...(host ? { host, served: /(^|\.)googletagmanager\.com$/.test(host) ? "google" : "site" } : {}),
|
|
202
|
+
kind, step, ...(at ? { page: at } : {}), ...facts,
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
for (const sc of run.containerScripts ?? []) {
|
|
206
|
+
const pre = preloads.find((p) => p.id === sc.id && p.host === sc.host);
|
|
207
|
+
add(sc.id, sc.host, pre ? "preload" : "unseen", pre?.step ?? sc.step, scriptFacts(sc), pre?.page);
|
|
208
|
+
}
|
|
209
|
+
for (const p of preloads) add(p.id, p.host, "preload", p.step, {}, p.page);
|
|
210
|
+
for (const c of run.containersRunning ?? []) {
|
|
211
|
+
if (!loaded(c.id) && ![...out.values()].some((x) => x.id === c.id)) add(c.id, null, "unseen", c.step);
|
|
212
|
+
}
|
|
213
|
+
return [...out.values()];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* GTM containers a run downloaded or asked for that never ran: Google's registry never held them on any step. Only
|
|
218
|
+
* GTM containers, whose registry entry is certain; [] for captures that didn't record the registry.
|
|
219
|
+
*/
|
|
220
|
+
export function containersNotRun(run) {
|
|
221
|
+
if (!Array.isArray(run.containersRunning)) return [];
|
|
222
|
+
const ran = new Set(run.containersRunning.map((c) => c.id));
|
|
223
|
+
const seen = new Map();
|
|
224
|
+
for (const l of [...loadSources(run), ...unseenContainers(run)]) {
|
|
225
|
+
if (!l.id.startsWith("GTM-") || ran.has(l.id) || l.failed || l.frame?.party === "third" || seen.has(l.id)) continue;
|
|
226
|
+
seen.set(l.id, { id: l.id, kind: l.kind, step: l.step ?? 0 });
|
|
227
|
+
}
|
|
228
|
+
return [...seen.values()];
|
|
229
|
+
}
|
|
230
|
+
|
|
174
231
|
/** The containers loaded more than once into one page, as [{ id, loads }] with the loads in order. */
|
|
175
232
|
export function duplicateLoads(run) {
|
|
176
233
|
const byDoc = new Map();
|
package/src/html/scope.js
CHANGED
|
@@ -202,7 +202,7 @@ function containersCard(ctx) {
|
|
|
202
202
|
const rows = containerRows(runs);
|
|
203
203
|
if (!rows.length) return null;
|
|
204
204
|
const c = m.ui.containers;
|
|
205
|
-
const items = rows.map((r) => `<li>${code(r.id)}${plain(esc(c.script[r.script]))}<span>${esc(c.kinds[r.kind](r.by))}</span>${plain(`${esc(m.ui.layerCol)} ${code(r.layer)}`)}${plain(esc(r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`))}${r.version ? plain(esc(c.version(r.version)), "mono") : ""}${r.bytes ? plain(esc(c.size(r.bytes)), "mono") : ""}${r.ms != null ? plain(esc(c.loadTime(r.ms))) : ""}${r.failed ? badge("broken", c.failed(r.failed)) : ""}${r.frame ? plain(esc(c.iframe(r.frame.origin, r.frame.party))) : ""}${r.environment ? badge("warning", c.environment(r.environment)) : ""}<span class="inv-n">${m.html.scope.pages(r.pages.length, bold(r.pages.length))}</span></li>`).join("");
|
|
205
|
+
const items = rows.map((r) => `<li>${code(r.id)}${plain(esc(c.script[r.script]))}<span>${esc(c.kinds[r.kind](r.by))}</span>${r.ran === false ? badge("broken", c.neverRan) : ""}${r.layer ? plain(`${esc(m.ui.layerCol)} ${code(r.layer)}`) : ""}${r.served ? plain(esc(r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`)) : ""}${r.version ? plain(esc(c.version(r.version)), "mono") : ""}${r.bytes ? plain(esc(c.size(r.bytes)), "mono") : ""}${r.ms != null ? plain(esc(c.loadTime(r.ms))) : ""}${r.failed ? badge("broken", c.failed(r.failed)) : ""}${r.frame ? plain(esc(c.iframe(r.frame.origin, r.frame.party))) : ""}${r.environment ? badge("warning", c.environment(r.environment)) : ""}<span class="inv-n">${m.html.scope.pages(r.pages.length, bold(r.pages.length))}</span></li>`).join("");
|
|
206
206
|
const ids = uniq(rows.map((r) => r.id));
|
|
207
207
|
return `<li><details class="card inv" id="inv-containers"><summary><span class="t">${esc(m.ui.containersTitle)}</span>${plain(n(ids.length))}<span class="inv-m">${esc(ids.slice(0, 3).join(", "))}${ids.length > 3 ? " …" : ""}</span></summary><div class="body"><p class="xs muted">${esc(m.ui.containersIntro)}</p><ul class="inv-list">${items}</ul></div></details></li>`;
|
|
208
208
|
}
|
package/src/i18n.js
CHANGED
|
@@ -244,6 +244,7 @@ const en = {
|
|
|
244
244
|
size: (bytes) => `${(bytes / 1024).toFixed(1)} KB`,
|
|
245
245
|
loadTime: (ms) => `loaded in ${ms} ms`,
|
|
246
246
|
failed: (status) => (status === "failed" ? "failed to load" : `failed to load (HTTP ${status})`),
|
|
247
|
+
neverRan: "never ran",
|
|
247
248
|
kinds: {
|
|
248
249
|
html: () => "A script tag in the page's HTML",
|
|
249
250
|
inline: () => "Inline code in the page (the snippet)",
|
|
@@ -254,6 +255,8 @@ const en = {
|
|
|
254
255
|
config: (by) => `A gtag('config') command, loaded by ${by}`,
|
|
255
256
|
send_to: (by) => `An event's send_to with no config, loaded by ${by}`,
|
|
256
257
|
destination: (by) => `A destination of the Google tag ${by}`,
|
|
258
|
+
preload: () => "Only a preload hint in the page (<link rel=\"preload\">): downloaded, never added as a script",
|
|
259
|
+
unseen: () => "Not seen being added: a script on the page may have replaced the browser's own functions, as consent platforms that block scripts do",
|
|
257
260
|
},
|
|
258
261
|
},
|
|
259
262
|
provider: "Provider",
|
|
@@ -385,6 +388,8 @@ const en = {
|
|
|
385
388
|
widgetTracking: (origin, platforms) => `An embedded widget from ${origin} sent its own tracking: ${platforms.map((p) => `${label(p.platform)}${p.ids.length ? ` (${p.ids.join(", ")})` : ""} ${p.events.map(q).join(", ")}`).join("; ")}. It goes to accounts the site's own tags don't use, so it was left out of the plan, coverage and duplicate checks; the consent and personal data checks still include it.`,
|
|
386
389
|
iframeContainer: (id, origin, parent) => `${id} runs in an iframe from ${origin} embedded in ${parent}. The iframe has its own dataLayer: events and consent commands pushed in the page never reach it, so it needs its own.`,
|
|
387
390
|
containerFailed: (id, host, status) => `The ${id} script from ${host} ${status === "failed" ? "failed to load" : `was answered with HTTP ${status}`}, so none of its tags ran on that page.`,
|
|
391
|
+
containerNotRun: (id, kind) => `${id} ${kind === "preload" ? "was downloaded because of a preload hint in the page, but nothing added it as a script, and it" : "was loaded but"} never ran on any page of this run: Google's own registry never held it, so none of its tags fired.`,
|
|
392
|
+
containerHeld: (id, vendor) => `${id} never ran, which is expected while ${vendor} holds it back until the visitor consents. This run never consented, so its tags weren't checked.`,
|
|
388
393
|
widgetEventsUnshared: (origin, names) => `An embedded widget from ${origin} pushed ${names.map(q).join(", ")} to its own dataLayer, but none of its hits to the site's accounts carried ${names.length === 1 ? "it" : "them"}. The page's tags can't read a widget's dataLayer, so ${names.length === 1 ? "this event reaches" : "these events reach"} only the widget's own tracking.`,
|
|
389
394
|
iframeMessageUnhandled: (origin, name, party) => `${party === "site" ? "An iframe on the site" : "An embedded widget"} from ${origin} posted ${q(name)} to the page, but the page pushed nothing to its dataLayer and sent no hit in the 3 seconds after, nor ${q(name)} later. If it is a conversion, the page isn't relaying it to its tags.`,
|
|
390
395
|
consentDefaultMissing: () => "No Google Consent Mode default command (gtag('consent', 'default', ...)) was found in the dataLayer.",
|
|
@@ -449,6 +454,8 @@ const en = {
|
|
|
449
454
|
widget_tracking: "Nothing is wrong with the site's tracking: the widget's own hits were kept out of the plan checks so they can't pass for the site's.",
|
|
450
455
|
iframe_container: "Tags in the iframe miss what the page pushes, so conversions completed in it can go untracked or be sent without the visitor's consent.",
|
|
451
456
|
container_failed: "None of the container's tags ran: page views, conversions and consent signals it sends were all lost on that page.",
|
|
457
|
+
container_not_run: "None of the container's tags fired: page views, conversions and remarketing were all lost wherever it never ran.",
|
|
458
|
+
container_held: "The container's tags weren't checked on this run, since it waited for a consent the run never gave.",
|
|
452
459
|
widget_events_unshared: "Conversions completed in the widget, such as a booking, a payment or a form, can be missing from the site's analytics and ad accounts.",
|
|
453
460
|
iframe_message_unhandled: "A conversion completed in the iframe, such as a form sent or a booking made, can go untracked although the iframe told the page about it.",
|
|
454
461
|
campaign_params: "Campaign parameters survived the landing redirect and reached the hits, so paid clicks are credited to their campaigns.",
|
|
@@ -500,6 +507,8 @@ const en = {
|
|
|
500
507
|
widget_tracking: "Nothing to fix for the plan. If the widget sets cookies or sends data before consent, the consent findings name it; ask its vendor how to hold it back.",
|
|
501
508
|
iframe_container: "Push the events and the consent state inside the iframe as well (or pass them in with postMessage), or track the iframe's steps from the page.",
|
|
502
509
|
container_failed: "Check the container ID and the address in the snippet; for a server container or the Google tag gateway, check that it serves gtm.js and gtag/js.",
|
|
510
|
+
container_not_run: "Check that something actually adds the container's script, the snippet or the framework component that loads it (a preload alone runs nothing); look for script errors that stop the page's own code, and for a consent platform that never releases it.",
|
|
511
|
+
container_held: "Run the journey with consent accepted, or the consent scenarios, to check the container's tags.",
|
|
503
512
|
widget_events_unshared: "If the site needs these events, have the widget send them to the site's accounts (most booking and form widgets take a GA4 or GTM ID), or pass them to the page with postMessage and push them to the page's dataLayer.",
|
|
504
513
|
iframe_message_unhandled: "Add a message listener on the page that checks event.origin and pushes the event to the dataLayer, so a GTM trigger can fire on it; or have the iframe send the event to the site's accounts itself.",
|
|
505
514
|
campaign_params: "Nothing to fix. Check the redirects again when the landing pages change.",
|
|
@@ -551,6 +560,8 @@ const en = {
|
|
|
551
560
|
widget_tracking: "hygiene",
|
|
552
561
|
iframe_container: "hygiene",
|
|
553
562
|
container_failed: "money",
|
|
563
|
+
container_not_run: "money",
|
|
564
|
+
container_held: "hygiene",
|
|
554
565
|
widget_events_unshared: "money",
|
|
555
566
|
iframe_message_unhandled: "money",
|
|
556
567
|
campaign_params: "money",
|
|
@@ -1113,6 +1124,7 @@ const tr = {
|
|
|
1113
1124
|
size: (bytes) => `${(bytes / 1024).toFixed(1).replace(".", ",")} KB`,
|
|
1114
1125
|
loadTime: (ms) => `${ms} ms'de yüklendi`,
|
|
1115
1126
|
failed: (status) => (status === "failed" ? "yüklenemedi" : `yüklenemedi (HTTP ${status})`),
|
|
1127
|
+
neverRan: "hiç çalışmadı",
|
|
1116
1128
|
kinds: {
|
|
1117
1129
|
html: () => "Sayfanın HTML'indeki bir script etiketi",
|
|
1118
1130
|
inline: () => "Sayfadaki satır içi kod (snippet)",
|
|
@@ -1123,6 +1135,8 @@ const tr = {
|
|
|
1123
1135
|
config: (by) => `Bir gtag('config') komutu, ${by} yükledi`,
|
|
1124
1136
|
send_to: (by) => `config olmadan bir olayın send_to değeri, ${by} yükledi`,
|
|
1125
1137
|
destination: (by) => `${by} Google etiketinin bir hedefi`,
|
|
1138
|
+
preload: () => "Sayfada yalnızca bir ön yükleme ipucu (<link rel=\"preload\">): indirildi, hiç betik olarak eklenmedi",
|
|
1139
|
+
unseen: () => "Eklendiği görülmedi: sayfadaki bir betik tarayıcının kendi işlevlerini değiştirmiş olabilir; betikleri engelleyen onay platformlarının yaptığı gibi",
|
|
1126
1140
|
},
|
|
1127
1141
|
},
|
|
1128
1142
|
provider: "Sağlayıcı",
|
|
@@ -1250,6 +1264,8 @@ const tr = {
|
|
|
1250
1264
|
widgetTracking: (origin, platforms) => `${origin} kaynaklı gömülü bir widget kendi izlemesini gönderdi: ${platforms.map((p) => `${label(p.platform)}${p.ids.length ? ` (${p.ids.join(", ")})` : ""} ${p.events.map(q).join(", ")}`).join("; ")}. Bunlar sitenin kendi etiketlerinin kullanmadığı hesaplara gidiyor; bu yüzden plan, kapsam ve tekrar kontrollerinin dışında tutuldu, onay ve kişisel veri kontrolleri ise onları yine içeriyor.`,
|
|
1251
1265
|
iframeContainer: (id, origin, parent) => `${id}, ${parent} içine gömülü ${origin} kaynaklı bir iframe içinde çalışıyor. iframe'in kendi dataLayer'ı var: sayfada gönderilen olaylar ve onay komutları ona hiç ulaşmaz, bu yüzden kendi olaylarına ve onayına ihtiyacı var.`,
|
|
1252
1266
|
containerFailed: (id, host, status) => `${host} adresinden gelen ${id} betiği ${status === "failed" ? "yüklenemedi" : `HTTP ${status} ile yanıtlandı`}; bu yüzden o sayfada etiketlerinin hiçbiri çalışmadı.`,
|
|
1267
|
+
containerNotRun: (id, kind) => `${id} ${kind === "preload" ? "sayfadaki bir ön yükleme ipucu yüzünden indirildi, ama hiçbir şey onu betik olarak eklemedi ve" : "yüklendi ama"} bu çalıştırmanın hiçbir sayfasında çalışmadı: Google'ın kendi kaydı onu hiç tutmadı, bu yüzden etiketlerinin hiçbiri çalışmadı.`,
|
|
1268
|
+
containerHeld: (id, vendor) => `${id} hiç çalışmadı; ${vendor} onu ziyaretçi onay verene kadar beklettiği sürece bu beklenen bir durum. Bu çalıştırmada onay hiç verilmedi, bu yüzden etiketleri kontrol edilmedi.`,
|
|
1253
1269
|
widgetEventsUnshared: (origin, names) => `${origin} kaynaklı gömülü bir widget kendi dataLayer'ına ${names.map(q).join(", ")} gönderdi, ancak sitenin hesaplarına giden isteklerinin hiçbiri ${names.length === 1 ? "onu" : "onları"} taşımadı. Sayfanın etiketleri bir widget'ın dataLayer'ını okuyamaz; bu yüzden ${names.length === 1 ? "bu olay" : "bu olaylar"} yalnızca widget'ın kendi izlemesine ulaşıyor.`,
|
|
1254
1270
|
iframeMessageUnhandled: (origin, name, party) => `${origin} kaynaklı ${party === "site" ? "sitedeki bir iframe" : "gömülü bir widget"} sayfaya ${q(name)} gönderdi, ancak sayfa ardından 3 saniye içinde dataLayer'a hiçbir şey göndermedi ve istek yollamadı, sonradan ${q(name)} da göndermedi. Bu bir dönüşümse sayfa onu etiketlerine iletmiyor.`,
|
|
1255
1271
|
consentDefaultMissing: () => "dataLayer'da Google Consent Mode varsayılan komutu (gtag('consent', 'default', ...)) bulunamadı.",
|
|
@@ -1313,6 +1329,8 @@ const tr = {
|
|
|
1313
1329
|
widget_tracking: "Sitenin izlemesinde bir sorun yok: widget'ın kendi istekleri sitenin isteklerinin yerine geçmesin diye plan kontrollerinin dışında tutuldu.",
|
|
1314
1330
|
iframe_container: "iframe içindeki etiketler sayfanın gönderdiklerini görmez; içinde tamamlanan dönüşümler izlenmeyebilir ya da ziyaretçinin onayı olmadan gönderilebilir.",
|
|
1315
1331
|
container_failed: "Container'ın etiketlerinin hiçbiri çalışmadı: gönderdiği sayfa görüntülemeleri, dönüşümler ve onay sinyalleri o sayfada kayboldu.",
|
|
1332
|
+
container_not_run: "Container'ın etiketlerinin hiçbiri çalışmadı: hiç çalışmadığı her yerde sayfa görüntülemeleri, dönüşümler ve yeniden pazarlama kayboldu.",
|
|
1333
|
+
container_held: "Container onay beklediği ve bu çalıştırma onay vermediği için etiketleri kontrol edilmedi.",
|
|
1316
1334
|
widget_events_unshared: "Widget içinde tamamlanan dönüşümler, örneğin bir rezervasyon, ödeme ya da form, sitenin analitik ve reklam hesaplarında eksik kalabilir.",
|
|
1317
1335
|
iframe_message_unhandled: "iframe içinde tamamlanan bir dönüşüm (gönderilen bir form, yapılan bir rezervasyon), iframe sayfaya bildirdiği hâlde izlenmeyebilir.",
|
|
1318
1336
|
campaign_params: "Kampanya parametreleri açılış yönlendirmesinden geçip isteklere ulaştı; ücretli tıklamalar kendi kampanyalarına yazılıyor.",
|
|
@@ -1362,6 +1380,8 @@ const tr = {
|
|
|
1362
1380
|
widget_tracking: "Plan için düzeltilecek bir şey yok. Widget onaydan önce çerez yazıyor ya da veri gönderiyorsa onay bulguları onu adlandırır; nasıl durdurulacağını sağlayıcısına sorun.",
|
|
1363
1381
|
iframe_container: "Olayları ve onay durumunu iframe içinde de gönderin (ya da postMessage ile iletin) veya iframe'deki adımları sayfadan izleyin.",
|
|
1364
1382
|
container_failed: "Snippet'teki container kimliğini ve adresi kontrol edin; sunucu container'ı ya da Google etiket ağ geçidi için gtm.js ve gtag/js sunduğunu kontrol edin.",
|
|
1383
|
+
container_not_run: "Container betiğini gerçekten bir şeyin eklediğini kontrol edin: snippet ya da onu yükleyen çatı bileşeni (yalnızca ön yükleme hiçbir şeyi çalıştırmaz); sayfanın kendi kodunu durduran betik hatalarına ve onu hiç serbest bırakmayan bir onay platformuna bakın.",
|
|
1384
|
+
container_held: "Container'ın etiketlerini kontrol etmek için yolculuğu onay kabul edilmiş olarak ya da onay senaryolarıyla çalıştırın.",
|
|
1365
1385
|
widget_events_unshared: "Site bu olaylara ihtiyaç duyuyorsa widget'ın onları sitenin hesaplarına göndermesini sağlayın (çoğu rezervasyon ve form widget'ı bir GA4 ya da GTM kimliği kabul eder) ya da onları postMessage ile sayfaya iletip sayfanın dataLayer'ına gönderin.",
|
|
1366
1386
|
iframe_message_unhandled: "Sayfaya event.origin'i kontrol eden ve olayı dataLayer'a gönderen bir message dinleyicisi ekleyin, böylece bir GTM tetikleyicisi onunla çalışabilir; ya da iframe'in olayı sitenin hesaplarına kendisi göndermesini sağlayın.",
|
|
1367
1387
|
campaign_params: "Düzeltilecek bir şey yok. Açılış sayfaları değiştiğinde yönlendirmeleri yeniden kontrol edin.",
|
|
@@ -1412,6 +1432,8 @@ const tr = {
|
|
|
1412
1432
|
widget_tracking: "hygiene",
|
|
1413
1433
|
iframe_container: "hygiene",
|
|
1414
1434
|
container_failed: "money",
|
|
1435
|
+
container_not_run: "money",
|
|
1436
|
+
container_held: "hygiene",
|
|
1415
1437
|
widget_events_unshared: "money",
|
|
1416
1438
|
iframe_message_unhandled: "money",
|
|
1417
1439
|
campaign_params: "money",
|
package/src/mcp.js
CHANGED
|
@@ -33,7 +33,7 @@ import { decodeGcd, DEFAULT_PROVIDERS, googleConsent, isCollectionEndpoint, pars
|
|
|
33
33
|
import { hitParams } from "./hit-params.js";
|
|
34
34
|
import { CAMPAIGN_PARAMS, campaignParamsIn, withCampaignParams } from "./campaign.js";
|
|
35
35
|
import { cmpFacts } from "./cmp.js";
|
|
36
|
-
import { containerRows, loadSources } from "./containers.js";
|
|
36
|
+
import { containerRows, containersNotRun, loadSources, unseenContainers } from "./containers.js";
|
|
37
37
|
import { isEventMessage, isWidgetHit, messageOutcomes, pushedEvent, siteIds, splitRun, widgetSummaries } from "./frames.js";
|
|
38
38
|
import { consentMatrixMarkdown, consentTable, runConsentScenarios } from "./consent.js";
|
|
39
39
|
import { expectEvents, filterFindings, JOURNEY_PRESETS, onlyEvents, presetFor, withPresetEvents } from "./journeys.js";
|
|
@@ -244,7 +244,7 @@ export function hitDetail(audit, step, index) {
|
|
|
244
244
|
function containerJson(l) {
|
|
245
245
|
return {
|
|
246
246
|
id: l.id, source: l.kind, ...(l.by ? { by: l.by } : {}),
|
|
247
|
-
...(l.layer !== "dataLayer" ? { data_layer: l.layer } : {}),
|
|
247
|
+
...(l.layer && l.layer !== "dataLayer" ? { data_layer: l.layer } : {}),
|
|
248
248
|
...(l.served === "site" ? { served_from: l.host } : {}),
|
|
249
249
|
...(l.environment ? { gtm_environment: l.environment } : {}),
|
|
250
250
|
...(l.frame ? { iframe: { origin: l.frame.origin, party: l.frame.party } } : {}),
|
|
@@ -253,6 +253,7 @@ function containerJson(l) {
|
|
|
253
253
|
...(l.transferBytes ? { transfer_bytes: l.transferBytes } : {}),
|
|
254
254
|
...(l.ms != null ? { load_ms: l.ms } : {}),
|
|
255
255
|
...(l.failed ? { failed: l.failed } : {}),
|
|
256
|
+
...(typeof l.ran === "boolean" ? { ran: l.ran } : {}),
|
|
256
257
|
...(l.pages ? { pages: l.pages } : {}),
|
|
257
258
|
};
|
|
258
259
|
}
|
|
@@ -265,7 +266,7 @@ export function containerLines(runs) {
|
|
|
265
266
|
return [
|
|
266
267
|
"Google containers (how each reached the page):",
|
|
267
268
|
...rows.map((r) => {
|
|
268
|
-
const extra = [scriptCell(c, r), r.layer !== "dataLayer" && `dataLayer ${r.layer}`, r.served === "site" && `served from ${r.host}`, r.environment && c.environment(r.environment), r.frame && c.iframe(r.frame.origin, r.frame.party), `${r.pages.length} ${r.pages.length === 1 ? "page" : "pages"}`].filter(Boolean);
|
|
269
|
+
const extra = [scriptCell(c, r), r.layer && r.layer !== "dataLayer" && `dataLayer ${r.layer}`, r.served === "site" && `served from ${r.host}`, r.environment && c.environment(r.environment), r.frame && c.iframe(r.frame.origin, r.frame.party), `${r.pages.length} ${r.pages.length === 1 ? "page" : "pages"}`].filter(Boolean);
|
|
269
270
|
return ` ${r.id} (${c.script[r.script]}): ${c.kinds[r.kind](r.by)}; ${extra.join(", ")}`;
|
|
270
271
|
}),
|
|
271
272
|
];
|
|
@@ -575,10 +576,20 @@ export function buildServer(opts) {
|
|
|
575
576
|
},
|
|
576
577
|
async ({ audit_id }) => {
|
|
577
578
|
const { session } = getAudit(audit_id);
|
|
578
|
-
const
|
|
579
|
-
|
|
579
|
+
const run = {
|
|
580
|
+
containerLoads: session.containerLoads ?? [], containerScripts: session.containerScripts, dataLayerLog: session.dataLayerLog,
|
|
581
|
+
containerPreloads: [...(session.containerPreloads?.values() ?? [])],
|
|
582
|
+
containersRunning: [...(session.containersRunning ?? [])].map(([id, step]) => ({ id, step })),
|
|
583
|
+
};
|
|
584
|
+
const loads = loadSources(run);
|
|
585
|
+
const unseen = unseenContainers(run);
|
|
586
|
+
const notRun = containersNotRun(run);
|
|
587
|
+
if (!loads.length && !unseen.length) return text("No GTM container or Google tag script has loaded on this audit's pages yet.");
|
|
588
|
+
const c = messages("en").ui.containers;
|
|
580
589
|
const lines = loads.map((l) => `Step ${l.step} ("${session.steps[l.step]?.label ?? ""}"), ${l.page ?? "?"}:\n${describeLoad(l)}`);
|
|
581
|
-
|
|
590
|
+
if (unseen.length) lines.push("Asked for without a script being seen added:", ...unseen.map((u) => ` ${u.id} (${c.script[u.script]}), step ${u.step}: ${c.kinds[u.kind]()}`));
|
|
591
|
+
if (notRun.length) lines.push(`Never ran so far (Google's registry never held ${notRun.length === 1 ? "it" : "them"}): ${notRun.map((x) => x.id).join(", ")}. If a consent platform holds GTM back, accept consent to see it run.`);
|
|
592
|
+
return text(`${loads.length} container ${loads.length === 1 ? "load" : "loads"}:\n${lines.join("\n")}\n\nAs JSON: ${JSON.stringify([...loads, ...unseen].map((l) => ({ step: l.step, page: l.page, ...containerJson(l), ...(l.id.startsWith("GTM-") ? { ran: run.containersRunning.some((r) => r.id === l.id) } : {}) })))}`);
|
|
582
593
|
},
|
|
583
594
|
);
|
|
584
595
|
|
package/src/report.js
CHANGED
|
@@ -398,10 +398,10 @@ function mdInventory(m, runs) {
|
|
|
398
398
|
return lines;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
/** A container row's version, size and load time, or that it failed to load, in a few words. */
|
|
401
|
+
/** A container row's version, size and load time, or that it failed to load, and whether it never ran, in a few words. */
|
|
402
402
|
export function scriptCell(c, r) {
|
|
403
403
|
if (r.failed) return c.failed(r.failed);
|
|
404
|
-
return [r.version && c.version(r.version), r.bytes && c.size(r.bytes), r.ms != null && c.loadTime(r.ms)].filter(Boolean).join(", ");
|
|
404
|
+
return [r.version && c.version(r.version), r.bytes && c.size(r.bytes), r.ms != null && c.loadTime(r.ms), r.ran === false && c.neverRan].filter(Boolean).join(", ");
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/** The events iframes posted to the page and what the page did next, one row each. Omitted when there were none. */
|
|
@@ -444,8 +444,8 @@ function mdContainers(m, runs) {
|
|
|
444
444
|
const c = m.ui.containers;
|
|
445
445
|
const lines = [`## ${m.ui.containersTitle}`, "", `| ${m.ui.containerCol} | ${m.ui.sourceCol} | ${m.ui.layerCol} | ${m.ui.servedCol} | ${m.ui.scriptCol} | ${m.ui.pagesCol} |`, "|---|---|---|---|---|---|"];
|
|
446
446
|
for (const r of rows.slice(0, MD_INVENTORY_ROWS)) {
|
|
447
|
-
const served = r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`;
|
|
448
|
-
lines.push(`| ${mdText(r.id)} (${c.script[r.script]}) | ${mdText(c.kinds[r.kind](r.by))}${r.frame ? `, ${mdText(c.iframe(r.frame.origin, r.frame.party))}` : ""}${r.environment ? `, ${mdText(c.environment(r.environment))}` : ""} | ${mdText(r.layer)} | ${mdText(served)} | ${mdText(scriptCell(c, r)) || "–"} | ${r.pages.length} |`);
|
|
447
|
+
const served = !r.served ? "–" : r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`;
|
|
448
|
+
lines.push(`| ${mdText(r.id)} (${c.script[r.script]}) | ${mdText(c.kinds[r.kind](r.by))}${r.frame ? `, ${mdText(c.iframe(r.frame.origin, r.frame.party))}` : ""}${r.environment ? `, ${mdText(c.environment(r.environment))}` : ""} | ${r.layer ? mdText(r.layer) : "–"} | ${mdText(served)} | ${mdText(scriptCell(c, r)) || "–"} | ${r.pages.length} |`);
|
|
449
449
|
}
|
|
450
450
|
if (rows.length > MD_INVENTORY_ROWS) lines.push("", m.ui.md.moreIn(rows.length - MD_INVENTORY_ROWS, "report.html"));
|
|
451
451
|
lines.push("");
|