@analitikisler/ai-tag-debugger 0.22.2 → 0.22.3
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 +7 -0
- package/README.md +4 -1
- package/README.tr.md +4 -1
- package/package.json +1 -1
- package/src/checks.js +5 -1
- package/src/frames.js +66 -0
- package/src/html/scope.js +26 -1
- package/src/i18n.js +38 -0
- package/src/mcp.js +50 -6
- package/src/report.js +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.3 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
- Runs can report a finding 0.22.2 did not on the same site (`widget_events_unshared`). A baseline written with 0.22.2 can then list new entries; `--update-baseline` rewrites it.
|
|
6
|
+
- New **Embedded widgets** card under the inventory and a table in `report.md`: per third-party widget (a booking, chat or payment widget, a video), the pages it was embedded in, the containers it loaded, the tracking it sent to its own accounts and to the site's, the events and consent commands in its own dataLayer, and the events it pushed that never reached the site's accounts.
|
|
7
|
+
- New check `widget_events_unshared` (info): a widget pushed events to its own dataLayer, such as a completed booking, that none of its hits to the site's accounts carried. The page's tags can't read a widget's dataLayer, so those conversions can be missing from the site's analytics and ad accounts.
|
|
8
|
+
- MCP: the new `get_widgets` tool lists the embedded widgets an open audit's pages had so far. `finish_audit` and `run_audit` end with them, `get_report` returns them with `part: "widgets"`, and the JSON timeline lists a widget's dataLayer events per step under `widget_pushes`.
|
|
9
|
+
|
|
3
10
|
## 0.22.2 (2026-11-04)
|
|
4
11
|
|
|
5
12
|
- Runs can report a finding 0.22.1 did not on the same site (`container_failed`). A baseline written with 0.22.1 can then list new entries; `--update-baseline` rewrites it.
|
package/README.md
CHANGED
|
@@ -335,6 +335,8 @@ Hits, dataLayer pushes, vendor calls and container loads sent from inside an ifr
|
|
|
335
335
|
|
|
336
336
|
A widget's own hits are left out of everything that judges the site's tracking: plan matching, coverage, duplicates, unplanned events and the scores. A widget's `purchase` to its vendor's property can't pass for the site's, and its events aren't listed as the site's. They still count for consent and personal data, since the site answers for what the widgets it embeds collect; those findings begin with "In an embedded widget from …". Each widget that tracked is named once in a `widget_tracking` (info) finding with its accounts and events, and a container running in a same-site iframe beside one in the page is noted as `iframe_container` (info): the page's dataLayer pushes and consent never reach it. A widget's dataLayer pushes are kept apart too, so its consent commands don't stand in for the site's.
|
|
337
337
|
|
|
338
|
+
Each widget also gets a line in the **Embedded widgets** card under the inventory and a table in `report.md`: the pages it was embedded in, the containers it loaded itself, the tracking it sent to its own accounts and to the site's, and the events and consent commands in its own dataLayer. The page's tags can't read a widget's dataLayer, so an event the widget pushed there (a `booking_complete`, say) reaches the site's accounts only if the widget sends it to them. The events no hit to the site's accounts carried are listed as not sent, and reported as `widget_events_unshared` (info): a booking or payment completed in the widget can be missing from the site's analytics and ad accounts.
|
|
339
|
+
|
|
338
340
|
`capture.json` keeps the widget's hits under `widgetHits` and its pushes under `widgetDataLayerLog`, and every iframe entry has a `frame`. The Google containers card says which ran in an iframe. Over MCP, step results mark each hit from an iframe and whether the plan checks will count it, pushes from an iframe are prefixed with it, and the JSON timeline lists a widget's hits per step under `widget_hits`. Captures from older versions have no frames, so all of their hits stay the site's.
|
|
339
341
|
|
|
340
342
|
### Folder layout and plan IDs
|
|
@@ -666,7 +668,8 @@ Tools the server offers:
|
|
|
666
668
|
| `finish_audit` | Checks everything against your measurement plan (JSON), a saved one given as `against` (`latest`, `first`, a plan ID, or on a local server a path), or the ready-made journey's events, optionally only the `events` you name, and writes the full HTML report with the timeline. The site comes from the audit. The result ends with a compact JSON timeline, one entry per step: the hits with their consent, consent commands, other tracking calls, the Google tag's consent when it changed, and the cookies and ids that changed. |
|
|
667
669
|
| `consent_matrix` | Opens a site's start page like an ad click once per consent choice (no choice and on to the next page, accept all, reject all, analytics only), each in a fresh browser, and returns which tags sent after each choice and the [consent matrix](#consent-scenarios). `campaign` takes the campaign parameters to check (as in `start_audit`), `concurrency` how many choices run at once (default 2, at most 4). About half a minute. |
|
|
668
670
|
| `get_containers` | Lists every GTM container and Google tag the open audit's pages loaded so far, with how each reached the page (see [Google containers](#google-containers)), its dataLayer, host, GTM environment and the step it loaded at, as text and JSON. |
|
|
669
|
-
| `
|
|
671
|
+
| `get_widgets` | Lists every embedded third-party widget the open audit's pages had so far (see [Embedded widgets and iframes](#embedded-widgets-and-iframes)): where it was embedded, its containers, the tracking it sent to its own accounts and to the site's, its own dataLayer events, and the events that never reached the site's accounts, as text and JSON. |
|
|
672
|
+
| `get_report` | Reads a report this server wrote, by the name `finish_audit` or `run_audit` gave: `part` `markdown` (default, `report.md`), `summary` (`summary.json`), `timeline` (the JSON timeline of every journey, with the containers each step loaded) `containers` (every Google container with how it reached the page, as JSON) or `widgets` (every embedded widget with its tracking and dataLayer events, as JSON). Only this server's own reports, over HTTP too. |
|
|
670
673
|
| `run_audit` | Runs a config file end to end. With `against`, it checks the run against a saved plan and, when the config has no journeys, walks the journeys derived from that plan. `concurrency` runs that many journeys at once, each in its own browser (default 1), and `baseline` names a file of accepted findings written with `--update-baseline` (default: the config's `baseline`; see [Accept known findings](#accept-known-findings)). Local only for file paths. |
|
|
671
674
|
|
|
672
675
|
`against` takes the same values as the CLI's `--against`. Over HTTP it takes only `latest`, `first` or a plan ID, never a path. Plans are the ones saved by `aitd plan --site <url>` or by `make_plan`. Ask for a plan when you want to know what to track, and for an audit when you want to know what the site sends.
|
package/README.tr.md
CHANGED
|
@@ -335,6 +335,8 @@ Bir iframe içinden gönderilen istekler, dataLayer push'ları, sağlayıcı ça
|
|
|
335
335
|
|
|
336
336
|
Bir widget'ın kendi istekleri, sitenin izlemesini değerlendiren her şeyin dışında tutulur: plan eşleştirme, kapsam, tekrarlar, planda olmayan olaylar ve puanlar. Widget'ın sağlayıcısının mülküne giden bir `purchase` sitenin yerine geçemez ve olayları sitenin olayları olarak listelenmez. Yine de onay ve kişisel veri için sayılırlar, çünkü site gömdüğü widget'ların topladıklarından sorumludur; bu bulgular "… kaynaklı gömülü bir widget içinde:" ile başlar. İzleme yapan her widget, hesapları ve olaylarıyla bir kez `widget_tracking` (bilgi) bulgusunda adlandırılır; sayfadakinin yanında aynı sitedeki bir iframe'de çalışan container ise `iframe_container` (bilgi) olarak not edilir: sayfanın dataLayer push'ları ve onayı ona hiç ulaşmaz. Widget'ın dataLayer push'ları da ayrı tutulur, böylece onay komutları sitenin komutlarının yerine geçmez.
|
|
337
337
|
|
|
338
|
+
Her widget ayrıca envanterin altındaki **Gömülü widget'lar** kartında bir satır, `report.md` içinde de bir tablo alır: gömülü olduğu sayfalar, kendi yüklediği container'lar, kendi hesaplarına ve sitenin hesaplarına gönderdiği izleme ve kendi dataLayer'ındaki olaylar ve onay komutları. Sayfanın etiketleri bir widget'ın dataLayer'ını okuyamaz; bu yüzden widget'ın oraya gönderdiği bir olay (örneğin `booking_complete`) sitenin hesaplarına ancak widget onu oraya gönderirse ulaşır. Sitenin hesaplarına giden hiçbir isteğin taşımadığı olaylar gönderilmemiş olarak listelenir ve `widget_events_unshared` (bilgi) olarak bildirilir: widget içinde tamamlanan bir rezervasyon ya da ödeme sitenin analitik ve reklam hesaplarında eksik kalabilir.
|
|
339
|
+
|
|
338
340
|
`capture.json` widget'ın isteklerini `widgetHits`, push'larını `widgetDataLayerLog` altında saklar ve her iframe kaydında bir `frame` bulunur. Google container'ları kartı hangilerinin bir iframe'de çalıştığını söyler. MCP'de adım sonuçları iframe'den gelen her isteği ve plan kontrollerinin onu sayıp saymayacağını işaretler, iframe'den gelen push'ların başına iframe yazılır ve JSON zaman akışı bir widget'ın isteklerini adım adım `widget_hits` altında listeler. Eski sürümlerin kayıtlarında frame bilgisi yoktur, bu yüzden tüm istekleri sitenin olarak kalır.
|
|
339
341
|
|
|
340
342
|
### Klasör yapısı ve plan kimlikleri
|
|
@@ -666,7 +668,8 @@ Sunucunun sunduğu araçlar:
|
|
|
666
668
|
| `finish_audit` | Her şeyi ölçümleme planınıza (JSON) ya da `against` olarak verilen kayıtlı bir plana (`latest`, `first`, bir plan kimliği ya da yerel bir sunucuda bir yol) ya da hazır yolculuğun event'lerine karşı kontrol eder; isterseniz yalnızca adını verdiğiniz `events` için, ve zaman akışıyla birlikte tam HTML raporunu yazar. Site denetimden alınır. Sonuç, adım başına bir kayıt içeren kısa bir JSON zaman akışıyla biter: onay durumlarıyla istekler, onay komutları, diğer takip çağrıları, değiştiğinde Google etiketinin onay durumu, değişen çerezler ve kimlikler. |
|
|
667
669
|
| `consent_matrix` | Bir sitenin başlangıç sayfasını her onay seçimi için (seçim yapmadan sonraki sayfa, tümünü kabul, tümünü reddet, yalnızca analitik) bir reklam tıklaması gibi, her seferinde yeni bir tarayıcıda açar; her seçimden sonra hangi etiketlerin gönderdiğini ve [onay matrisini](#onay-senaryoları) döndürür. `campaign` kontrol edilecek kampanya parametrelerini (`start_audit`'teki gibi), `concurrency` aynı anda kaç seçimin çalışacağını alır (varsayılan 2, en fazla 4). Yaklaşık yarım dakika sürer. |
|
|
668
670
|
| `get_containers` | Açık denetimin sayfalarının şimdiye kadar yüklediği her GTM container'ını ve Google etiketini, her birinin sayfaya nasıl geldiğiyle ([Google container'ları](#google-containerları)), dataLayer'ı, sunucusu, GTM ortamı ve yüklendiği adımla birlikte metin ve JSON olarak listeler. |
|
|
669
|
-
| `
|
|
671
|
+
| `get_widgets` | Açık denetimin sayfalarında şimdiye kadar bulunan her gömülü üçüncü taraf widget'ı ([Gömülü widget'lar ve iframe'ler](#gömülü-widgetlar-ve-iframeler)) listeler: gömülü olduğu yer, container'ları, kendi hesaplarına ve sitenin hesaplarına gönderdiği izleme, kendi dataLayer olayları ve sitenin hesaplarına hiç ulaşmayan olaylar; metin ve JSON olarak. |
|
|
672
|
+
| `get_report` | Bu sunucunun yazdığı bir raporu, `finish_audit` ya da `run_audit`'in verdiği adla okur: `part` `markdown` (varsayılan, `report.md`), `summary` (`summary.json`), `timeline` (her yolculuğun JSON zaman akışı; her adımın yüklediği container'larla) `containers` (sayfaya nasıl geldiğiyle her Google container'ı, JSON olarak) ya da `widgets` (izlemesi ve dataLayer olaylarıyla her gömülü widget, JSON olarak). Yalnızca bu sunucunun kendi raporları, HTTP üzerinden de. |
|
|
670
673
|
| `run_audit` | Bir ayar dosyasını baştan sona çalıştırır. `against` ile çalıştırmayı kayıtlı bir plana karşı kontrol eder; ayar dosyasında yolculuk yoksa, plandan türetilen yolculukları gezer. `concurrency` o kadar yolculuğu aynı anda, her birini kendi tarayıcısında çalıştırır (varsayılan 1); `baseline`, `--update-baseline` ile yazılmış kabul edilen bulgular dosyasını adlandırır (varsayılan: ayar dosyasındaki `baseline`; bkz. [Bilinen bulguları kabul edin](#bilinen-bulguları-kabul-edin)). Dosya yolları yalnızca yerelde çalışır. |
|
|
671
674
|
|
|
672
675
|
`against`, CLI'deki `--against` ile aynı değerleri alır. HTTP üzerinden yalnızca `latest`, `first` ya da bir plan kimliği kabul edilir, asla bir yol değil. Planlar, `aitd plan --site <url>` ya da `make_plan` ile kaydedilmiş olanlardır. Neyin takip edileceğini öğrenmek için plan, sitenin ne gönderdiğini öğrenmek için denetim isteyin.
|
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.3",
|
|
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/checks.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isOwnAddress } from "./redact.js";
|
|
|
6
6
|
import { campaignParamsIn } from "./campaign.js";
|
|
7
7
|
import { CONSENT_SIGNALS } from "./parsers.js";
|
|
8
8
|
import { duplicateLoads, loadSources, scriptFailed, splitLayers } from "./containers.js";
|
|
9
|
-
import { widgetsOf } from "./frames.js";
|
|
9
|
+
import { widgetSummaries, widgetsOf } from "./frames.js";
|
|
10
10
|
|
|
11
11
|
const platformName = (p) => ({ meta: "Meta", tiktok: "TikTok" })[p] ?? p;
|
|
12
12
|
const NON_GOOGLE_AD_PLATFORMS = new Set(["meta", "tiktok"]);
|
|
@@ -399,6 +399,10 @@ export function checkRun(plan, run, { lang = "en" } = {}) {
|
|
|
399
399
|
const first = (run.widgetHits ?? []).find((h) => h.frame.origin === w.origin);
|
|
400
400
|
findings.push({ ...base, severity: "info", check: "widget_tracking", step: stepLabel(first.step), detail: d.widgetTracking(w.origin, w.platforms) });
|
|
401
401
|
}
|
|
402
|
+
// Events a widget pushed to its own dataLayer that never reached the site's accounts: the page's tags can't see them.
|
|
403
|
+
for (const w of widgetSummaries([run], (a, b) => canonicalEvent(a) === canonicalEvent(b)).filter((x) => x.unshared.length)) {
|
|
404
|
+
findings.push({ ...base, severity: "info", check: "widget_events_unshared", step: stepLabel(w.step), detail: d.widgetEventsUnshared(w.origin, w.unshared.slice(0, 10)) });
|
|
405
|
+
}
|
|
402
406
|
// A container in a same-site iframe beside one in the page: two documents, two dataLayers.
|
|
403
407
|
const iframeLoad = loads.find((l) => l.frame?.party === "site" && loads.some((t) => !t.frame && t.step <= l.step));
|
|
404
408
|
if (iframeLoad) findings.push({ ...base, severity: "info", check: "iframe_container", step: stepLabel(iframeLoad.step), detail: d.iframeContainer(iframeLoad.id, iframeLoad.frame.origin, iframeLoad.frame.parent) });
|
package/src/frames.js
CHANGED
|
@@ -88,3 +88,69 @@ export function widgetsOf(widgetHits = []) {
|
|
|
88
88
|
platforms: [...w.platforms.values()].map((p) => ({ platform: p.platform, ids: [...p.ids], events: [...p.events] })),
|
|
89
89
|
}));
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/** The event a dataLayer push names, { event: "x" } or gtag's ["event", "x", ...], or null. GTM's own (gtm.js, gtm.dom...) are left out. */
|
|
93
|
+
export function pushedEvent(value) {
|
|
94
|
+
const name = Array.isArray(value) ? (value[0] === "event" ? value[1] : null) : value?.event;
|
|
95
|
+
return typeof name === "string" && name && !name.startsWith("gtm.") ? name : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const plainName = (n) => String(n).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
99
|
+
const isConsentPush = (v) => Array.isArray(v) && v[0] === "consent";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One summary per embedded widget (third-party iframe origin) across runs: { origin, parents, containers,
|
|
103
|
+
* own, shared, pushes, unshared, step }.
|
|
104
|
+
* - parents: the pages it was embedded in; containers: [{ id, script }] it loaded itself
|
|
105
|
+
* - own: [{ platform, id, events }] it sent to its own accounts; shared: the same for what it sent to the site's
|
|
106
|
+
* - pushes: { total, consent, events: [{ name, count }] } of its own dataLayer
|
|
107
|
+
* - unshared: the events it pushed that none of its hits to the site's accounts carried
|
|
108
|
+
* - step: the first step it was seen at, in the first run it was seen in
|
|
109
|
+
* same(a, b) compares event names; checks.js passes canonicalEvent so add_to_cart and addToCart are one event.
|
|
110
|
+
*/
|
|
111
|
+
export function widgetSummaries(runs, same = (a, b) => plainName(a) === plainName(b)) {
|
|
112
|
+
const byOrigin = new Map();
|
|
113
|
+
const of = (frame, step) => {
|
|
114
|
+
const w = byOrigin.get(frame.origin) ?? {
|
|
115
|
+
origin: frame.origin, parents: new Set(), containers: new Map(), own: new Map(), shared: new Map(),
|
|
116
|
+
total: 0, consent: 0, events: new Map(), step,
|
|
117
|
+
};
|
|
118
|
+
if (frame.parent) w.parents.add(frame.parent);
|
|
119
|
+
byOrigin.set(frame.origin, w);
|
|
120
|
+
return w;
|
|
121
|
+
};
|
|
122
|
+
const addHit = (map, h) => {
|
|
123
|
+
const k = `${h.platform}|${h.id ?? ""}`;
|
|
124
|
+
const e = map.get(k) ?? { platform: h.platform, ...(h.id ? { id: h.id } : {}), events: new Set() };
|
|
125
|
+
e.events.add(h.name);
|
|
126
|
+
map.set(k, e);
|
|
127
|
+
};
|
|
128
|
+
for (const run of runs) {
|
|
129
|
+
for (const l of run.containerLoads ?? []) if (l.frame?.party === "third") of(l.frame, l.step).containers.set(l.id, { id: l.id, script: l.script });
|
|
130
|
+
for (const h of run.widgetHits ?? []) addHit(of(h.frame, h.step).own, h);
|
|
131
|
+
for (const h of run.hits ?? []) if (h.frame?.party === "third") addHit(of(h.frame, h.step).shared, h);
|
|
132
|
+
for (const e of run.widgetDataLayerLog ?? []) {
|
|
133
|
+
if (!e.frame) continue;
|
|
134
|
+
const w = of(e.frame, e.step);
|
|
135
|
+
w.total++;
|
|
136
|
+
if (isConsentPush(e.value)) w.consent++;
|
|
137
|
+
const name = pushedEvent(e.value);
|
|
138
|
+
if (name) w.events.set(name, (w.events.get(name) ?? 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const list = (map) => [...map.values()].map((e) => ({ ...e, events: [...e.events] }));
|
|
142
|
+
return [...byOrigin.values()].map((w) => {
|
|
143
|
+
const shared = list(w.shared);
|
|
144
|
+
const sent = shared.flatMap((e) => e.events);
|
|
145
|
+
return {
|
|
146
|
+
origin: w.origin,
|
|
147
|
+
parents: [...w.parents].sort(),
|
|
148
|
+
containers: [...w.containers.values()],
|
|
149
|
+
own: list(w.own),
|
|
150
|
+
shared,
|
|
151
|
+
pushes: { total: w.total, consent: w.consent, events: [...w.events].map(([name, count]) => ({ name, count })) },
|
|
152
|
+
unshared: [...w.events.keys()].filter((name) => !sent.some((s) => same(s, name))),
|
|
153
|
+
step: w.step,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/html/scope.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// vendors, the trend) is the 0.17 renderer's, so the numbers match the Markdown report and the CLI.
|
|
5
5
|
import { buildInventory } from "../inventory.js";
|
|
6
6
|
import { containerRows } from "../containers.js";
|
|
7
|
+
import { widgetSummaries } from "../frames.js";
|
|
8
|
+
import { canonicalEvent } from "../checks.js";
|
|
7
9
|
import { providerOf } from "../parsers.js";
|
|
8
10
|
import { siteOf } from "../redact.js";
|
|
9
11
|
import { VERSION, BUCKET_ORDER, coverageLines, pcSort, planChangeItems, routeList } from "../report-shared.js";
|
|
@@ -205,13 +207,35 @@ function containersCard(ctx) {
|
|
|
205
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>`;
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
/** The embedded widgets card: per third-party iframe, where it was embedded, its containers, its tracking and its dataLayer. Null when none. */
|
|
211
|
+
function widgetsCard(ctx) {
|
|
212
|
+
const { m, runs } = ctx;
|
|
213
|
+
const widgets = widgetSummaries(runs, (a, b) => canonicalEvent(a) === canonicalEvent(b));
|
|
214
|
+
if (!widgets.length) return null;
|
|
215
|
+
const w = m.ui.widgets;
|
|
216
|
+
const line = (label, html) => `<li><span class="inv-n">${esc(label)}</span>${html}</li>`;
|
|
217
|
+
const hits = (list) => (list.length ? list.map((e) => code(w.hits(e.platform, e.id, e.events))).join(" ") : `<span class="muted xs">${esc(w.none)}</span>`);
|
|
218
|
+
const items = widgets.map((x) => {
|
|
219
|
+
const events = x.pushes.events.map((e) => code(w.event(e.name, e.count))).join(" ");
|
|
220
|
+
return `<li><details class="card inv"><summary><code>${esc(x.origin)}</code>${plain(esc(w.pushes(x.pushes.total, x.pushes.consent)))}${x.unshared.length ? badge("info", `${m.ui.widgets.unshared}: ${x.unshared.length}`) : ""}</summary><div class="body"><ul class="inv-list">
|
|
221
|
+
${line(m.ui.embeddedCol, x.parents.map((p) => `${use("ui-page", 13)}<code>${esc(p)}</code>`).join(" ") || `<span class="muted xs">${esc(w.none)}</span>`)}
|
|
222
|
+
${line(w.containers, x.containers.length ? x.containers.map((c) => code(c.id)).join(" ") : `<span class="muted xs">${esc(w.none)}</span>`)}
|
|
223
|
+
${line(m.ui.ownCol, hits(x.own))}
|
|
224
|
+
${line(m.ui.sharedCol, hits(x.shared))}
|
|
225
|
+
${line(m.ui.pushesCol, events || `<span class="muted xs">${esc(w.none)}</span>`)}
|
|
226
|
+
${x.unshared.length ? line(w.unshared, x.unshared.map(code).join(" ")) : ""}</ul></div></details></li>`;
|
|
227
|
+
}).join("");
|
|
228
|
+
return `<li><details class="card inv" id="inv-widgets"><summary><span class="t">${esc(m.ui.widgetsTitle)}</span>${plain(n(widgets.length))}<span class="inv-m">${esc(widgets.slice(0, 3).map((x) => x.origin.replace(/^https?:\/\//, "")).join(", "))}${widgets.length > 3 ? " …" : ""}</span></summary><div class="body"><p class="xs muted">${esc(m.ui.widgetsIntro)}</p><ul class="inv-rows">${items}</ul></div></details></li>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
function inventorySection(ctx) {
|
|
209
232
|
const { m, runs } = ctx;
|
|
210
233
|
const t = m.html.scope;
|
|
211
234
|
const { lines, otherCookies } = inventoryLines(ctx);
|
|
212
235
|
const skipped = skippedTotals(runs);
|
|
213
236
|
const containers = containersCard(ctx);
|
|
214
|
-
|
|
237
|
+
const widgets = widgetsCard(ctx);
|
|
238
|
+
if (!lines.length && !skipped.length && !containers && !widgets) return sectionBlock("inventory", esc(m.html.shell.sections.inventory), `<p class="muted">${esc(m.ui.noInventory)}</p>`);
|
|
215
239
|
const counts = (x) => `<span class="inv-m">${t.requests(x.requests, bold(x.requests))}</span><span class="inv-m">${t.pages(x.pages.size, bold(x.pages.size))}</span><span class="inv-m${x.cookies.length ? "" : " zero"}">${t.cookies(x.cookies.length, bold(x.cookies.length))}</span>`;
|
|
216
240
|
const out = lines.map((x) => {
|
|
217
241
|
const id = `inv-${safeKey(x.p)}`;
|
|
@@ -227,6 +251,7 @@ function inventorySection(ctx) {
|
|
|
227
251
|
<div role="tabpanel" id="${tid("pages")}" data-label="${esc(m.ui.pagesCol)}">${pages ? `<ul class="inv-list">${pages}</ul>` : none}</div>
|
|
228
252
|
<div role="tabpanel" id="${tid("cookies")}" data-label="${esc(m.ui.cookiesCol)}">${x.cookies.length ? `<ul class="inv-list">${x.cookies.map((ck) => `<li>${code(ck)}</li>`).join("")}</ul>` : none}</div></div></div></details></li>`;
|
|
229
253
|
});
|
|
254
|
+
if (widgets) out.unshift(widgets);
|
|
230
255
|
if (containers) out.unshift(containers);
|
|
231
256
|
const skippedN = skipped.reduce((a, [, k]) => a + k, 0);
|
|
232
257
|
if (skipped.length) {
|
package/src/i18n.js
CHANGED
|
@@ -210,6 +210,21 @@ const en = {
|
|
|
210
210
|
layerCol: "dataLayer",
|
|
211
211
|
servedCol: "Served from",
|
|
212
212
|
scriptCol: "Version and size",
|
|
213
|
+
widgetsTitle: "Embedded widgets",
|
|
214
|
+
widgetsIntro: "Third-party iframes on the site's pages, such as booking, chat or payment widgets: the containers they loaded, the tracking they sent to their own accounts and to the site's, and the events in their own dataLayer, which the page's tags never see.",
|
|
215
|
+
widgetCol: "Widget",
|
|
216
|
+
embeddedCol: "Embedded in",
|
|
217
|
+
ownCol: "Own tracking",
|
|
218
|
+
sharedCol: "To the site's accounts",
|
|
219
|
+
pushesCol: "dataLayer events",
|
|
220
|
+
widgets: {
|
|
221
|
+
containers: "Containers",
|
|
222
|
+
unshared: "Not sent to the site's accounts",
|
|
223
|
+
pushes: (n, consent) => `${n} ${n === 1 ? "push" : "pushes"}${consent ? `, ${consent} consent ${consent === 1 ? "command" : "commands"}` : ""}`,
|
|
224
|
+
event: (name, count) => (count > 1 ? `${name} ×${count}` : name),
|
|
225
|
+
hits: (platform, id, events) => `${label(platform)}${id ? ` ${id}` : ""}: ${events.join(", ")}`,
|
|
226
|
+
none: "none",
|
|
227
|
+
},
|
|
213
228
|
containers: {
|
|
214
229
|
script: { gtm: "GTM container", gtag: "Google tag" },
|
|
215
230
|
served: { google: "Google", site: "The site's own domain" },
|
|
@@ -360,6 +375,7 @@ const en = {
|
|
|
360
375
|
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.`,
|
|
361
376
|
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.`,
|
|
362
377
|
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.`,
|
|
378
|
+
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.`,
|
|
363
379
|
consentDefaultMissing: () => "No Google Consent Mode default command (gtag('consent', 'default', ...)) was found in the dataLayer.",
|
|
364
380
|
consentDefaultLate: () => "The Consent Mode default was set after the first tracking hit was sent.",
|
|
365
381
|
consentDefaultNotApplied: () => "The page pushed a Consent Mode default, but the Google tag never applied it, so its tags ran without one. Tag Assistant shows Consent Mode as not configured.",
|
|
@@ -422,6 +438,7 @@ const en = {
|
|
|
422
438
|
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.",
|
|
423
439
|
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.",
|
|
424
440
|
container_failed: "None of the container's tags ran: page views, conversions and consent signals it sends were all lost on that page.",
|
|
441
|
+
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.",
|
|
425
442
|
campaign_params: "Campaign parameters survived the landing redirect and reached the hits, so paid clicks are credited to their campaigns.",
|
|
426
443
|
event_id_reused: "Two different events share one event ID, so the platform keeps the first and silently drops the other: a real event is lost.",
|
|
427
444
|
event_id_mismatch: "The browser and server copies of this event carry different IDs, so Meta cannot pair them and counts the conversion twice.",
|
|
@@ -471,6 +488,7 @@ const en = {
|
|
|
471
488
|
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.",
|
|
472
489
|
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.",
|
|
473
490
|
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.",
|
|
491
|
+
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.",
|
|
474
492
|
campaign_params: "Nothing to fix. Check the redirects again when the landing pages change.",
|
|
475
493
|
event_id_reused: "Generate a new event ID for every event, not once per page load.",
|
|
476
494
|
event_id_mismatch: "Use one event ID for the pixel event and the server copy: pass the same value to both tags.",
|
|
@@ -520,6 +538,7 @@ const en = {
|
|
|
520
538
|
widget_tracking: "hygiene",
|
|
521
539
|
iframe_container: "hygiene",
|
|
522
540
|
container_failed: "money",
|
|
541
|
+
widget_events_unshared: "money",
|
|
523
542
|
campaign_params: "money",
|
|
524
543
|
event_id_reused: "money",
|
|
525
544
|
event_id_mismatch: "money",
|
|
@@ -1046,6 +1065,21 @@ const tr = {
|
|
|
1046
1065
|
layerCol: "dataLayer",
|
|
1047
1066
|
servedCol: "Sunulduğu yer",
|
|
1048
1067
|
scriptCol: "Sürüm ve boyut",
|
|
1068
|
+
widgetsTitle: "Gömülü widget'lar",
|
|
1069
|
+
widgetsIntro: "Sitenin sayfalarındaki üçüncü taraf iframe'ler, örneğin rezervasyon, sohbet ya da ödeme widget'ları: yükledikleri container'lar, kendi hesaplarına ve sitenin hesaplarına gönderdikleri izleme ve sayfanın etiketlerinin hiç görmediği kendi dataLayer'larındaki olaylar.",
|
|
1070
|
+
widgetCol: "Widget",
|
|
1071
|
+
embeddedCol: "Gömülü olduğu yer",
|
|
1072
|
+
ownCol: "Kendi izlemesi",
|
|
1073
|
+
sharedCol: "Sitenin hesaplarına",
|
|
1074
|
+
pushesCol: "dataLayer olayları",
|
|
1075
|
+
widgets: {
|
|
1076
|
+
containers: "Container'lar",
|
|
1077
|
+
unshared: "Sitenin hesaplarına gönderilmeyen",
|
|
1078
|
+
pushes: (n, consent) => `${n} push${consent ? `, ${consent} onay komutu` : ""}`,
|
|
1079
|
+
event: (name, count) => (count > 1 ? `${name} ×${count}` : name),
|
|
1080
|
+
hits: (platform, id, events) => `${label(platform)}${id ? ` ${id}` : ""}: ${events.join(", ")}`,
|
|
1081
|
+
none: "yok",
|
|
1082
|
+
},
|
|
1049
1083
|
containers: {
|
|
1050
1084
|
script: { gtm: "GTM container'ı", gtag: "Google etiketi" },
|
|
1051
1085
|
served: { google: "Google", site: "Sitenin kendi alan adı" },
|
|
@@ -1192,6 +1226,7 @@ const tr = {
|
|
|
1192
1226
|
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.`,
|
|
1193
1227
|
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.`,
|
|
1194
1228
|
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ı.`,
|
|
1229
|
+
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.`,
|
|
1195
1230
|
consentDefaultMissing: () => "dataLayer'da Google Consent Mode varsayılan komutu (gtag('consent', 'default', ...)) bulunamadı.",
|
|
1196
1231
|
consentDefaultLate: () => "Consent Mode varsayılanı, ilk izleme isteği gönderildikten sonra ayarlandı.",
|
|
1197
1232
|
consentDefaultNotApplied: () => "Sayfa bir Consent Mode varsayılanı gönderdi, ancak Google etiketi onu hiç uygulamadı; etiketleri varsayılan olmadan çalıştı. Tag Assistant, Consent Mode'u yapılandırılmamış gösterir.",
|
|
@@ -1253,6 +1288,7 @@ const tr = {
|
|
|
1253
1288
|
widget_tracking: "Sitenin izlemesinde bir sorun yok: widget'ın kendi istekleri sitenin isteklerinin yerine geçmesin diye plan kontrollerinin dışında tutuldu.",
|
|
1254
1289
|
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.",
|
|
1255
1290
|
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.",
|
|
1291
|
+
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.",
|
|
1256
1292
|
campaign_params: "Kampanya parametreleri açılış yönlendirmesinden geçip isteklere ulaştı; ücretli tıklamalar kendi kampanyalarına yazılıyor.",
|
|
1257
1293
|
event_id_reused: "İki farklı olay tek bir olay kimliğini paylaşıyor; platform ilkini tutup diğerini sessizce atıyor, gerçek bir olay kayboluyor.",
|
|
1258
1294
|
event_id_mismatch: "Bu olayın tarayıcı ve sunucu kopyaları farklı kimlik taşıyor; Meta ikisini eşleştiremiyor ve dönüşümü iki kez sayıyor.",
|
|
@@ -1300,6 +1336,7 @@ const tr = {
|
|
|
1300
1336
|
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.",
|
|
1301
1337
|
iframe_container: "Olayları ve onay durumunu iframe içinde de gönderin (ya da postMessage ile iletin) veya iframe'deki adımları sayfadan izleyin.",
|
|
1302
1338
|
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.",
|
|
1339
|
+
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.",
|
|
1303
1340
|
campaign_params: "Düzeltilecek bir şey yok. Açılış sayfaları değiştiğinde yönlendirmeleri yeniden kontrol edin.",
|
|
1304
1341
|
event_id_reused: "Her olay için yeni bir olay kimliği üretin; sayfa yüklemesi başına bir kez değil.",
|
|
1305
1342
|
event_id_mismatch: "Piksel olayı ve sunucu kopyası için tek bir olay kimliği kullanın: aynı değeri iki etikete de verin.",
|
|
@@ -1348,6 +1385,7 @@ const tr = {
|
|
|
1348
1385
|
widget_tracking: "hygiene",
|
|
1349
1386
|
iframe_container: "hygiene",
|
|
1350
1387
|
container_failed: "money",
|
|
1388
|
+
widget_events_unshared: "money",
|
|
1351
1389
|
campaign_params: "money",
|
|
1352
1390
|
event_id_reused: "money",
|
|
1353
1391
|
event_id_mismatch: "money",
|
package/src/mcp.js
CHANGED
|
@@ -17,7 +17,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
17
17
|
import { AuditSession } from "./browser.js";
|
|
18
18
|
import { navigationError, privateHostChecker } from "./guard.js";
|
|
19
19
|
import { collect, pageView, SYSTEM_PROMPT } from "./agent.js";
|
|
20
|
-
import { checkRun, groupFindings, isConsentCommand, summarize } from "./checks.js";
|
|
20
|
+
import { canonicalEvent, checkRun, groupFindings, isConsentCommand, summarize } from "./checks.js";
|
|
21
21
|
import { LANGUAGES, messages } from "./i18n.js";
|
|
22
22
|
import { loadConfig, readJson, runAudit, writeReports } from "./audit.js";
|
|
23
23
|
import { validatePlan } from "./schema.js";
|
|
@@ -34,7 +34,7 @@ import { hitParams } from "./hit-params.js";
|
|
|
34
34
|
import { CAMPAIGN_PARAMS, campaignParamsIn, withCampaignParams } from "./campaign.js";
|
|
35
35
|
import { cmpFacts } from "./cmp.js";
|
|
36
36
|
import { containerRows, loadSources } from "./containers.js";
|
|
37
|
-
import { isWidgetHit, siteIds } from "./frames.js";
|
|
37
|
+
import { isWidgetHit, 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";
|
|
40
40
|
|
|
@@ -269,6 +269,27 @@ export function containerLines(runs) {
|
|
|
269
269
|
];
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/** The embedded widgets of the runs as summaries (see widgetSummaries), compared by canonical event name. */
|
|
273
|
+
const widgetsIn = (runs) => widgetSummaries(runs, (a, b) => canonicalEvent(a) === canonicalEvent(b));
|
|
274
|
+
|
|
275
|
+
/** The embedded widgets as lines for a tool result; [] when there were none. */
|
|
276
|
+
export function widgetLines(runs) {
|
|
277
|
+
const widgets = widgetsIn(runs);
|
|
278
|
+
if (!widgets.length) return [];
|
|
279
|
+
const w = messages("en").ui.widgets;
|
|
280
|
+
const hits = (list) => list.map((e) => w.hits(e.platform, e.id, e.events)).join("; ") || w.none;
|
|
281
|
+
return [
|
|
282
|
+
"Embedded widgets (third-party iframes; their own dataLayer is invisible to the page's tags):",
|
|
283
|
+
...widgets.map((x) => [
|
|
284
|
+
` ${x.origin}, embedded in ${x.parents.join(", ") || "?"}${x.containers.length ? `; containers ${x.containers.map((c) => c.id).join(", ")}` : ""}`,
|
|
285
|
+
` own tracking: ${hits(x.own)}`,
|
|
286
|
+
` to the site's accounts: ${hits(x.shared)}`,
|
|
287
|
+
` dataLayer: ${w.pushes(x.pushes.total, x.pushes.consent)}${x.pushes.events.length ? `: ${x.pushes.events.map((e) => w.event(e.name, e.count)).join(", ")}` : ""}`,
|
|
288
|
+
...(x.unshared.length ? [` not sent to the site's accounts: ${x.unshared.join(", ")}`] : []),
|
|
289
|
+
].join("\n")),
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
|
|
272
293
|
export function timelineOf(run) {
|
|
273
294
|
let lastIds = null;
|
|
274
295
|
return (run.steps ?? []).map((st) => {
|
|
@@ -282,6 +303,8 @@ export function timelineOf(run) {
|
|
|
282
303
|
if (hits.length) entry.hits = hits.map((h) => ({ platform: h.platform, event: h.name, ...(h.consent ? { consent: h.consent } : {}), ...(h.late ? { late_ms: h.late.ms } : {}), ...(h.frame ? { iframe: h.frame.origin } : {}) }));
|
|
283
304
|
// An embedded widget's own hits, kept apart from the site's (see src/frames.js).
|
|
284
305
|
const widget = (run.widgetHits ?? []).filter((h) => h.step === st.index);
|
|
306
|
+
const widgetPushes = (run.widgetDataLayerLog ?? []).filter((e) => e.step === st.index && e.frame && pushedEvent(e.value));
|
|
307
|
+
if (widgetPushes.length) entry.widget_pushes = widgetPushes.map((e) => ({ widget: e.frame.origin, event: pushedEvent(e.value) }));
|
|
285
308
|
if (widget.length) entry.widget_hits = widget.map((h) => ({ widget: h.frame.origin, platform: h.platform, event: h.name, ...(h.id ? { id: h.id } : {}), ...(h.consent ? { consent: h.consent } : {}) }));
|
|
286
309
|
if (others.length) entry.other_tracking_calls = others.length;
|
|
287
310
|
if (commands.length) entry.consent_commands = commands.map((e) => ({ [e.value[1]]: e.value[2] }));
|
|
@@ -544,15 +567,33 @@ export function buildServer(opts) {
|
|
|
544
567
|
},
|
|
545
568
|
);
|
|
546
569
|
|
|
570
|
+
server.registerTool(
|
|
571
|
+
"get_widgets",
|
|
572
|
+
{
|
|
573
|
+
title: "List the embedded widgets seen so far",
|
|
574
|
+
description:
|
|
575
|
+
"Returns every third-party iframe (a booking, chat or payment widget, a video) this audit's pages embedded that tracked or loaded a Google container so far: the pages it was embedded in, the containers it loaded, the tracking it sent to its own accounts and to the site's, the events and consent commands in its own dataLayer, and the events it pushed that never reached the site's accounts. The page's tags can't read a widget's dataLayer, so those events only reach the widget's own tracking. Widgets' own hits are left out of the plan checks; finish_audit still checks them for consent and personal data.",
|
|
576
|
+
inputSchema: { audit_id: z.string() },
|
|
577
|
+
annotations: { readOnlyHint: true },
|
|
578
|
+
},
|
|
579
|
+
async ({ audit_id }) => {
|
|
580
|
+
const { session } = getAudit(audit_id);
|
|
581
|
+
const run = { ...splitRun(session), containerLoads: session.containerLoads };
|
|
582
|
+
const lines = widgetLines([run]);
|
|
583
|
+
if (!lines.length) return text("No embedded third-party widget has tracked or loaded a Google container on this audit's pages yet.");
|
|
584
|
+
return text(`${lines.join("\n")}\n\nAs JSON: ${JSON.stringify(widgetsIn([run]))}`);
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
|
|
547
588
|
server.registerTool(
|
|
548
589
|
"get_report",
|
|
549
590
|
{
|
|
550
591
|
title: "Read a report this server wrote",
|
|
551
592
|
description:
|
|
552
|
-
"Returns a report finish_audit or run_audit wrote, so it can be read without access to the computer running this server. part markdown (default): report.md, the verdict, findings, consent and journeys in about 120 lines. summary: summary.json, the counts, journeys and findings as JSON. timeline: one JSON entry per step of each journey (hits with their consent, consent commands, the Google tag's consent, cookies and ids, the Google containers loaded). containers: every GTM container and Google tag the journeys loaded as JSON, with how each reached the page (source html, inline, script, custom_html, google_tag, nested, config, send_to or destination, and by: the container or script that loaded it), its dataLayer, host, GTM environment and pages.",
|
|
593
|
+
"Returns a report finish_audit or run_audit wrote, so it can be read without access to the computer running this server. part markdown (default): report.md, the verdict, findings, consent and journeys in about 120 lines. summary: summary.json, the counts, journeys and findings as JSON. timeline: one JSON entry per step of each journey (hits with their consent, consent commands, the Google tag's consent, cookies and ids, the Google containers loaded). containers: every GTM container and Google tag the journeys loaded as JSON, with how each reached the page (source html, inline, script, custom_html, google_tag, nested, config, send_to or destination, and by: the container or script that loaded it), its dataLayer, host, GTM environment, version, size, load time and pages. widgets: every embedded third-party widget as JSON: where it was embedded, the containers it loaded, the tracking it sent to its own accounts and to the site's, its own dataLayer events, and the events it pushed that never reached the site's accounts (unshared).",
|
|
553
594
|
inputSchema: {
|
|
554
595
|
report: z.string().describe("The report name finish_audit or run_audit gave."),
|
|
555
|
-
part: z.enum(["markdown", "summary", "timeline", "containers"]).optional().describe("Default markdown."),
|
|
596
|
+
part: z.enum(["markdown", "summary", "timeline", "containers", "widgets"]).optional().describe("Default markdown."),
|
|
556
597
|
},
|
|
557
598
|
annotations: { readOnlyHint: true },
|
|
558
599
|
},
|
|
@@ -564,6 +605,7 @@ export function buildServer(opts) {
|
|
|
564
605
|
if (part === "summary") return text(await readFile(path.join(dir, "summary.json"), "utf8"));
|
|
565
606
|
const capture = JSON.parse(await readFile(path.join(dir, "capture.json"), "utf8"));
|
|
566
607
|
if (part === "containers") return text(JSON.stringify(containerRows(capture.runs ?? []).map(containerJson)));
|
|
608
|
+
if (part === "widgets") return text(JSON.stringify(widgetsIn(capture.runs ?? [])));
|
|
567
609
|
return text(JSON.stringify((capture.runs ?? []).map((r) => ({ journey: r.journeyId, viewport: r.viewport, steps: timelineOf(r) }))));
|
|
568
610
|
} catch (err) {
|
|
569
611
|
return fail(`Could not read the report: ${err.message}`);
|
|
@@ -674,13 +716,14 @@ export function buildServer(opts) {
|
|
|
674
716
|
skippedLine(run),
|
|
675
717
|
...((platform) => (platform ? [`Consent platform: ${platform.vendor}. ${platform.facts.join(" ")}`] : []))(cmpFacts(messages("en"), [run])),
|
|
676
718
|
...containerLines([run]),
|
|
719
|
+
...widgetLines([run]),
|
|
677
720
|
opts.allowFiles
|
|
678
721
|
? `Full report with the event timeline: ${reportPath}`
|
|
679
722
|
: opts.reportsUrl
|
|
680
723
|
? `Full report with the event timeline: ${opts.reportsUrl}/${path.basename(outDir)}/report.html`
|
|
681
724
|
: "",
|
|
682
725
|
"",
|
|
683
|
-
`To read the report here: get_report with report "${path.basename(outDir)}" (markdown, summary, timeline or
|
|
726
|
+
`To read the report here: get_report with report "${path.basename(outDir)}" (markdown, summary, timeline, containers or widgets).`,
|
|
684
727
|
"Explain each problem to the user with its likely cause and fix.",
|
|
685
728
|
];
|
|
686
729
|
return {
|
|
@@ -827,8 +870,9 @@ export function buildServer(opts) {
|
|
|
827
870
|
...result.runs.map((r) => `${r.journeyId} (${r.viewport}): ${r.completed ? "completed" : "not completed"}, ${r.note}`),
|
|
828
871
|
...grouped.map((f) => `- ${m.severity[f.severity]}${f.accepted ? ` (${m.ui.accepted})` : ""}: ${f.detail}`),
|
|
829
872
|
...containerLines(result.runs),
|
|
873
|
+
...widgetLines(result.runs),
|
|
830
874
|
`Report: ${result.reportPath}`,
|
|
831
|
-
`To read the report here: get_report with report "${path.basename(result.outDir)}" (markdown, summary, timeline or
|
|
875
|
+
`To read the report here: get_report with report "${path.basename(result.outDir)}" (markdown, summary, timeline, containers or widgets).`,
|
|
832
876
|
].join("\n"),
|
|
833
877
|
);
|
|
834
878
|
},
|
package/src/report.js
CHANGED
|
@@ -6,6 +6,7 @@ import { canonicalEvent, groupFindings, summarize } from "./checks.js";
|
|
|
6
6
|
import { messages } from "./i18n.js";
|
|
7
7
|
import { buildInventory } from "./inventory.js";
|
|
8
8
|
import { containerRows } from "./containers.js";
|
|
9
|
+
import { widgetSummaries } from "./frames.js";
|
|
9
10
|
import { computeMetrics } from "./metrics.js";
|
|
10
11
|
import { historySummaryLines, sparkline } from "./history.js";
|
|
11
12
|
import { cardIds } from "./html/lib.js";
|
|
@@ -403,6 +404,23 @@ export function scriptCell(c, r) {
|
|
|
403
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
405
|
}
|
|
405
406
|
|
|
407
|
+
/** The embedded widgets, one row each: where they were embedded, their tracking and their own dataLayer events. Omitted when none. */
|
|
408
|
+
function mdWidgets(m, runs) {
|
|
409
|
+
const widgets = widgetSummaries(runs, (a, b) => canonicalEvent(a) === canonicalEvent(b));
|
|
410
|
+
if (!widgets.length) return [];
|
|
411
|
+
const w = m.ui.widgets;
|
|
412
|
+
const hits = (list) => list.map((e) => w.hits(e.platform, e.id, e.events)).join("; ") || w.none;
|
|
413
|
+
const lines = [`## ${m.ui.widgetsTitle}`, "", `| ${m.ui.widgetCol} | ${m.ui.embeddedCol} | ${m.ui.ownCol} | ${m.ui.sharedCol} | ${m.ui.pushesCol} |`, "|---|---|---|---|---|"];
|
|
414
|
+
for (const x of widgets.slice(0, MD_INVENTORY_ROWS)) {
|
|
415
|
+
const events = x.pushes.events.map((e) => w.event(e.name, e.count)).join(", ") || w.none;
|
|
416
|
+
const unshared = x.unshared.length ? ` (${w.unshared}: ${x.unshared.join(", ")})` : "";
|
|
417
|
+
lines.push(`| ${mdText(x.origin)} | ${mdText(x.parents.join(", ")) || "–"} | ${mdText(hits(x.own))} | ${mdText(hits(x.shared))} | ${mdText(events + unshared)} |`);
|
|
418
|
+
}
|
|
419
|
+
if (widgets.length > MD_INVENTORY_ROWS) lines.push("", m.ui.md.moreIn(widgets.length - MD_INVENTORY_ROWS, "report.html"));
|
|
420
|
+
lines.push("");
|
|
421
|
+
return lines;
|
|
422
|
+
}
|
|
423
|
+
|
|
406
424
|
/** The Google containers and how each reached the page, one row per container and source. Omitted when none loaded. */
|
|
407
425
|
function mdContainers(m, runs) {
|
|
408
426
|
const rows = containerRows(runs);
|
|
@@ -470,6 +488,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
|
|
|
470
488
|
lines.push(...mdNotChecked(m, runs, rows, plan, consentScenarios));
|
|
471
489
|
lines.push(...mdInventory(m, runs));
|
|
472
490
|
lines.push(...mdContainers(m, runs));
|
|
491
|
+
lines.push(...mdWidgets(m, runs));
|
|
473
492
|
// A report with its own name or logo carries no Analitik İşler credit; the ai tag debugger one stays.
|
|
474
493
|
if (brand?.name || brand?.logo) lines.push("---", "", `_${brand.name ? `${brand.name} · ` : ""}${m.ui.generatedBy}_`, "");
|
|
475
494
|
else if (branding) lines.push("---", "", `_${m.ui.generatedBy} · [analitikisler.com](https://analitikisler.com)_`, "");
|