@analitikisler/ai-tag-debugger 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +32 -4
- package/README.tr.md +32 -4
- package/package.json +1 -1
- package/src/agent.js +6 -2
- package/src/audit.js +28 -10
- package/src/browser.js +11 -1
- package/src/campaign.js +28 -0
- package/src/changes.js +38 -1
- package/src/checks.js +82 -2
- package/src/cli.js +22 -0
- package/src/consent.js +154 -0
- package/src/gtm.js +92 -0
- package/src/i18n.js +60 -0
- package/src/inventory.js +69 -0
- package/src/learn.js +34 -0
- package/src/redact.js +1 -1
- package/src/report.js +91 -15
- package/src/sheet.js +8 -5
- package/src/whitelabel.js +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
- Campaign checks (`campaign_checks`, on by default): each run opens the first page like an ad click, with UTM parameters, `gclid` or `fbclid` (or the ones already in the start address), and reports when a redirect drops them or the hits show a page address without them. Set `campaign_checks` to `false` to open the plain address.
|
|
6
|
+
- Server-side checks: one event ID reused on two different events is flagged, and so is a Meta event ID that doesn't match the `event_id` on the GA4 hit a server container forwards. A server container is noted as an info finding.
|
|
7
|
+
- GTM container awareness: with `gtm_container`, each broken event names the tag that sends it and the trigger it fires on, without an AI model.
|
|
8
|
+
- White label: the `report` setting takes a `name`, `logo`, `color` and `pdf`. A name or logo replaces the Analitik İşler branding in the HTML report, and a small AI Tag Debugger logo and credit stay. `pdf: true` also writes `report.pdf`.
|
|
9
|
+
|
|
10
|
+
## 0.15.0 (2026-10-17)
|
|
11
|
+
|
|
12
|
+
- The first run learns the plan. When a site has no saved plan, `aitd run` lists the events it already sends and asks whether to save them as its plan. Add `--yes` to save without asking; in a script without `--yes`, nothing is saved.
|
|
13
|
+
- With `--against`, the report header says how the plan changed since the version before it: `Plan changed since <id>: 1 added, 0 removed, 2 changed`, or `Plan unchanged since <id>`. Each change is listed. `capture.json` has the same comparison under `planChanges`.
|
|
14
|
+
- `aitd plan import` reads Google Sheets downloads: the events CSV with its `<Doc> - user_properties.csv` beside it, and checkboxes as yes and no (TRUE and FALSE).
|
|
15
|
+
- Consent scenarios: each audit opens the start page once per consent choice (accept all, reject all, analytics only) and shows the hits each tag sent after each choice. Analytics only works with OneTrust and Cookiebot. Set `"consent_scenarios": false` in the config to turn them off.
|
|
16
|
+
- Tag inventory: the report lists every vendor the site sent requests to, first or third party, the pages it was seen on, and its cookie names (never values). Each network call in `capture.json` now has a `pageUrl`, redacted unless `keep_bodies` is on.
|
|
17
|
+
|
|
3
18
|
## 0.14.0 (2026-10-17)
|
|
4
19
|
|
|
5
20
|
- The MCP server has a `what_changed` tool that returns only the "since last run" diff for a site: what was fixed and what is new, without browsing or an AI.
|
package/README.md
CHANGED
|
@@ -49,15 +49,25 @@ Manual tag QA catches this, but it's tedious, so it rarely happens after every r
|
|
|
49
49
|
| Personal data | An email address sent to an ad platform in plain text |
|
|
50
50
|
| Unplanned events | Events firing that aren't in your measurement plan |
|
|
51
51
|
| Plan coverage | Each planned event sorted into implemented, missing, missing parameters, wrong values or not checked (see [Plan vs. site](#plan-vs-site)) |
|
|
52
|
+
| Campaign parameters | `utm_campaign` or `gclid` on the landing address dropped by a redirect, or missing from the page address the hits report. The visit would be credited to direct traffic |
|
|
53
|
+
| Server-side tracking | One event ID used for two different events, or a Meta event ID that doesn't match the `event_id` on the GA4 hit a server container turns into a Conversions API event |
|
|
52
54
|
|
|
53
55
|
Platforms: GA4 (including server-side GTM endpoints and Google tag gateway paths on the site's own domain; hits to a server container are marked server-side, and Stape and Elevar requests are recognised), Google Ads conversions, Meta Pixel, TikTok Pixel, basic X Pixel events, LinkedIn Insight Tag, Microsoft Advertising UET, Criteo OneTag, and Pinterest, Snapchat and Klaviyo events. Adobe Analytics, Matomo and Piwik PRO events are read too (page views, links, goals, site search and ecommerce orders), and Microsoft Clarity and Hotjar tag loads name the project or site id.
|
|
54
56
|
|
|
57
|
+
When GA4 hits go to a server container on another host, or its loader is served from one, the report adds an info finding: the events it forwards to ad platforms never pass through the browser, so they can't be seen here.
|
|
58
|
+
|
|
59
|
+
With a `gtm_container` export, each broken event also names the GTM tag that sends it and the trigger it fires on, or says that no tag sends it. This needs no AI model.
|
|
60
|
+
|
|
55
61
|
### Choose the platforms to check
|
|
56
62
|
|
|
57
63
|
By default an audit checks GA4, Google Ads, Meta and TikTok, and also any other platform the site turns out to use. That platform is checked from its first request, with no setting needed. Choosing platforms turns this detection off. Pick a fixed list with `--platforms ga4,meta,x`, `"providers"` in the config, or `providers` on the MCP `start_audit` and `run_audit` tools. You can choose from `ga4`, `google_ads`, `meta`, `tiktok`, `x`, `linkedin`, `microsoft_ads`, `pinterest`, `snapchat`, `criteo`, `klaviyo`, `adobe`, `matomo`, `piwik_pro`, `clarity`, `hotjar`, `doubleclick` and `adsense`. Events are parsed for GA4, Google Ads, Meta, TikTok, X, LinkedIn, Microsoft Ads, Pinterest, Snapchat, Criteo, Klaviyo, Adobe Analytics, Matomo and Piwik PRO. Clarity and Hotjar show their tag loads, and the others show their network requests. `--providers` still works as the old name of `--platforms` for one more minor version and prints a note.
|
|
58
64
|
|
|
59
65
|
Requests to platforms you didn't choose are still answered locally, so they never reach those accounts either. They are left out of the timeline, the CSV and the checks, and only counted: the report lists them with their request counts (for example "61 out-of-scope requests were skipped: Microsoft Clarity, DoubleClick"), and each step says how many it skipped. Without `--platforms`, any platform your measurement plan names or the site sends to is added to the default list. Planned events for platforms you did leave out are listed in one info finding instead of being reported as missing. Tag manager and consent platform requests are always shown.
|
|
60
66
|
|
|
67
|
+
### Consent scenarios
|
|
68
|
+
|
|
69
|
+
Each audit also opens the start page once per consent choice, each time in a fresh browser: accept all, reject all and analytics only. The choice goes through the consent platform's own API (OneTrust, Cookiebot, Usercentrics or Didomi) when it has one, or else by clicking the banner's buttons. The report then shows how many hits each tag sent after each choice. Analytics only works with OneTrust and Cookiebot; with other platforms that column says "Not supported here". A tag that sends after reject all is flagged, except cookieless Consent Mode pings. An advertising tag that sends after analytics only is flagged unless it was sent with ad storage denied. The table is at the top of the **Journeys** tab and above the findings in `report.md`. It is skipped when the page shows no banner the tool can answer, and when a saved site is replayed. No AI model is called, but the start page is visited three more times. Set `"consent_scenarios": false` in the config to turn it off.
|
|
70
|
+
|
|
61
71
|
## How it works
|
|
62
72
|
|
|
63
73
|
1. **Browse:** the AI model gets a goal in plain language ("add any product to the cart and complete checkout with the test payment") and a text view of the page. It acts through two tools, `browser_action` (goto, click, fill, select, scroll, wait) and `finish`, one step at a time with Playwright.
|
|
@@ -238,7 +248,7 @@ Events your site already sends are marked `"seen": true`.
|
|
|
238
248
|
|
|
239
249
|
Review the plan before you use it. The examples and suggestions come from the pages the AI saw, so check that each event fits your business. The extra fields are ignored by the checks.
|
|
240
250
|
|
|
241
|
-
**Spreadsheet import and export.** `aitd plan export <plan> --site <url>` writes `plan.xlsx` next to the plan, with one row per event parameter on an `events` tab (`event, platform, parameter, required, example, trigger, description`) and one row per user property on a `user_properties` tab. Add `--format csv` to get `events.csv` and `user_properties.csv` instead; CSV needs no extra package. `.xlsx` uses one small library, `fflate`, which is installed with the tool as an optional dependency. `aitd plan import <file> --site <url>` reads the spreadsheet back and saves it as a new plan folder. Import never edits an existing plan. Unknown columns are ignored with a warning, and an error names the tab, row and fix, for example `events, row 7: "required" must be yes or no (found "maybe").`
|
|
251
|
+
**Spreadsheet import and export.** `aitd plan export <plan> --site <url>` writes `plan.xlsx` next to the plan, with one row per event parameter on an `events` tab (`event, platform, parameter, required, example, trigger, description`) and one row per user property on a `user_properties` tab. Add `--format csv` to get `events.csv` and `user_properties.csv` instead; CSV needs no extra package. `.xlsx` uses one small library, `fflate`, which is installed with the tool as an optional dependency. `aitd plan import <file> --site <url>` reads the spreadsheet back (from Google Sheets, use File > Download > Microsoft Excel (.xlsx), or download each tab as CSV: `Shop plan - events.csv` is imported together with `Shop plan - user_properties.csv` beside it, and checkbox cells work as yes/no) and saves it as a new plan folder. Import never edits an existing plan. Unknown columns are ignored with a warning, and an error names the tab, row and fix, for example `events, row 7: "required" must be yes or no (found "maybe").`
|
|
242
252
|
|
|
243
253
|
`measurement-plan.json` looks like this:
|
|
244
254
|
|
|
@@ -263,6 +273,15 @@ Event names are matched loosely, so `dl_add_to_cart` (Elevar), `addToCart` and `
|
|
|
263
273
|
|
|
264
274
|
`aitd plan list --site <url>` lists the site's plan IDs, oldest first.
|
|
265
275
|
|
|
276
|
+
**Learn the plan from the first run.** When a site has no saved plan, a run without `--against` or `--out` lists the events the site already sends and asks whether to save them as its plan:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
No plan yet for this site. It already sends 3 events: ga4:page_view, ga4:purchase, meta:PageView
|
|
280
|
+
Save them as the site's plan? (Y/n)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Say yes, and the next runs can check against it with `--against latest`. A parameter that every hit of an event carries becomes one of its required parameters. No values are copied, so nothing a visitor typed ends up in the plan. In a script or CI job nothing is saved unless you add `--yes`; the run then prints the `aitd plan --site` command for drafting a fuller plan.
|
|
284
|
+
|
|
266
285
|
### Plan vs. site
|
|
267
286
|
|
|
268
287
|
When a run uses `--against`, the report starts with a **Plan vs. site** section, in `report.html` and `report.md`. Each planned event lands in one bucket:
|
|
@@ -278,6 +297,12 @@ When a run uses `--against`, the report starts with a **Plan vs. site** section,
|
|
|
278
297
|
|
|
279
298
|
The coverage score is implemented divided by the planned events that were checkable (all except not checked), shown overall and per platform, for example `Coverage 7 of 11 (64%), 2 events not checked`. The same entries are in `plan-check.json`, one per planned event with its bucket. Exit codes don't change: `--fail-on` still looks at finding severity. Without `--against`, the report is exactly as before.
|
|
280
299
|
|
|
300
|
+
When the plan is not the site's first, the header compares it with the version saved before it: `Plan changed since 2026-10-01_0900: 1 added, 0 removed, 2 changed`, or `Plan unchanged since 2026-10-01_0900`. The header in `report.html` and `report.md` then lists each change: `+` for an added event, `−` for a removed one and `~` for a changed one, followed by what changed (required parameters, parameters or journeys). The first version has nothing to compare with, and `capture.json` keeps the same comparison under `planChanges`.
|
|
301
|
+
|
|
302
|
+
### Tag inventory
|
|
303
|
+
|
|
304
|
+
Every report also has a **tag inventory**: each vendor the site sent requests to, marked first party when its requests went to the site's own domain, third party when they went to the vendor, or both. It shows how many pages the vendor was seen on and the cookies tied to it. Cookies that no vendor owns are listed apart, as first or third party. Only cookie names are recorded, never their values. It is in the **Scope and plan** tab of `report.html` and as a table in `report.md`.
|
|
305
|
+
|
|
281
306
|
### Folder layout and plan IDs
|
|
282
307
|
|
|
283
308
|
Plans and runs are kept in `aitd/` in the folder you run the tool from:
|
|
@@ -384,7 +409,7 @@ When a journey ran on both viewports, only the first viewport's steps are kept.
|
|
|
384
409
|
|---|---|
|
|
385
410
|
| `site` | Base URL. Use staging when you can. |
|
|
386
411
|
| `plan` | Measurement plan: a file path or an inline object (below). Optional with ready-made journeys. A run with `--against` uses that plan instead. |
|
|
387
|
-
| `gtm_container` | Optional GTM container export (Admin → Export container)
|
|
412
|
+
| `gtm_container` | Optional GTM container export (Admin → Export container). Each broken event then names the tag that sends it and the trigger it fires on (or says no tag sends it), even without an AI model. |
|
|
388
413
|
| `viewports` | `desktop` and/or `mobile`. |
|
|
389
414
|
| `journeys` | Optional when you pick ready-made journeys with `--journey`, describe one with `--goal`, or check against a plan with `--against`. |
|
|
390
415
|
| `journeys[].goal` | What the AI should do, in plain language. |
|
|
@@ -392,10 +417,13 @@ When a journey ran on both viewports, only the first viewport's steps are kept.
|
|
|
392
417
|
| `journeys[].steps` | Optional fixed steps for `--scripted` runs: `{ "action": "click", "selector": "#add-to-cart" }`. |
|
|
393
418
|
| `allowed_hosts` | Other hosts journeys may visit, e.g. `["checkout.payments-provider.com"]` for a hosted checkout. The browser otherwise stays on the `site` host and its subdomains. Same as `--allow-host`. |
|
|
394
419
|
| `providers` | Tracking platforms to check, e.g. `["ga4", "meta"]`. Default `["ga4", "google_ads", "meta", "tiktok"]`, plus any other platform the site sends to while this is unset. Same as `--platforms`. |
|
|
420
|
+
| `campaign_checks` | Default `true`. Each run opens the first page like an ad click (UTM parameters, `gclid`, `fbclid`, or the ones already in the start address) and checks they survive redirects and reach the hits. Set to `false` to open the plain address. |
|
|
395
421
|
| `block_hits` | Default `true`. Set to `false` to let hits reach your real endpoints. |
|
|
422
|
+
| `consent_scenarios` | Default `true`. Set to `false` to skip the [consent scenarios](#consent-scenarios), which visit the start page once per consent choice. |
|
|
396
423
|
| `language` | Report language, `en` (default) or `tr`. Same as `--lang`. |
|
|
397
424
|
| `keep_bodies` | Default `false`. Set to `true` to store tracking requests as sent, without redacting user data. Same as `--keep-bodies`. |
|
|
398
425
|
| `branding` | Default `true`. Set to `false` to leave the Analitik İşler logo and the "Generated by AI Tag Debugger · analitikisler.com" line out of the reports. Same as `--no-branding`, or `branding: false` on the MCP `finish_audit` and `run_audit` tools. |
|
|
426
|
+
| `report` | Optional white label: `{ "name": "Acme", "logo": "logo.png", "color": "#0a7d5a", "pdf": true }`. `logo` (PNG, JPG, WebP, GIF or SVG, up to 512 KB, relative to the config file) and `name` replace the Analitik İşler branding in the HTML report (a small "by AI Tag Debugger" logo and credit stay); `color` sets the accent color; `pdf: true` also writes `report.pdf` next to `report.html`. |
|
|
399
427
|
| `provider` | Optional: `anthropic`, `openai`, `gemini`, `vercel`, `cloudflare` or `openai-compatible`. Same as `--provider`. |
|
|
400
428
|
| `base_url`, `model` | Optional: the API address and model id, e.g. for `openai-compatible`. Same as `--base-url` and `--model`. |
|
|
401
429
|
| `notify` | Optional alerts after each saved run: `slack` (a webhook URL, or `true` to read `SLACK_WEBHOOK_URL`) and `email` (an address or a list). See [Alerts](#alerts). |
|
|
@@ -477,12 +505,12 @@ Output:
|
|
|
477
505
|
- `report.html`: one file with four tabs. Links work across tabs (a finding's "Where" chip opens that step), and every view has its own URL you can bookmark or share.
|
|
478
506
|
- **Overview**: problem counts, the AI summary (most important problem first, plus things worth checking that the findings don't cover), the journeys with their problem counts, a **This run** card (platforms, AI provider and model, plan, journeys, viewports, language, version), and, with `--against`, the [Plan vs. site](#plan-vs-site) coverage. When the AI took a different route through the site than in the last run, the header names the journeys whose route changed, and the **Changed since last run** section shows the pages before and after.
|
|
479
507
|
- **Findings**: grouped by who fixes them (Developer, Tag Manager, Marketing) and filterable by severity. Each one has its impact, likely cause, evidence, numbered fix steps, the steps where it happened, and a **How to verify** list of tools to check the fix in, such as Tag Assistant, GA4 DebugView, Meta Test Events or TikTok Pixel Helper.
|
|
480
|
-
- **Journeys**: one journey at a time. A **Consent** card at the top lists the Consent Mode default and every update, one tile per signal. Below it are the steps on the left and the selected step on the right. It opens on the first step with a problem. Each step has four tabs:
|
|
508
|
+
- **Journeys**: one journey at a time. Above them, when the start page has a consent banner, the [consent scenarios](#consent-scenarios) table. A **Consent** card at the top lists the Consent Mode default and every update, one tile per signal. Below it are the steps on the left and the selected step on the right. It opens on the first step with a problem. Each step has four tabs:
|
|
481
509
|
- **Timeline**: the step in order. Each dataLayer push comes first, then the GTM event it raised (or the consent command), then the platform hits it caused. Hits with no push are listed last.
|
|
482
510
|
- **Events**: each tracking hit with its parameters, consent state, the dataLayer push and request behind it, and a note when a finding points at it.
|
|
483
511
|
- **dataLayer**: every push in the step.
|
|
484
512
|
- **Network requests**: every request to the chosen platforms, decoded into a parameter table with a `?` tip on each parameter. A batched GA4 request shows each of its events.
|
|
485
|
-
- **Scope and plan**: whether each planned event was sent on each journey, and the platforms checked or left out, with request counts.
|
|
513
|
+
- **Scope and plan**: whether each planned event was sent on each journey, and the platforms checked or left out, with request counts. The [tag inventory](#tag-inventory) lists each vendor with its party, pages and cookies.
|
|
486
514
|
|
|
487
515
|
Events and requests can be filtered by source. GA4 item strings (`pr1`…) and dataLayer `ecommerce.items` are shown as item cards, with custom item parameters in their own group. The report has no scripts: tabs and filters are plain HTML and CSS (they need Chrome 105, Safari 15.4 or Firefox 121 or newer), and printing shows every tab. The logos are embedded, so nothing is loaded from the internet. `--no-branding` leaves out the Analitik İşler logo and credit line.
|
|
488
516
|
- `timeline.csv`: the same timeline as one row per hit, dataLayer push and network call, ready for Excel or Google Sheets.
|
package/README.tr.md
CHANGED
|
@@ -49,15 +49,25 @@ Manuel etiket kontrolü bunu yakalar, ama zahmetlidir; bu yüzden her sürümden
|
|
|
49
49
|
| Kişisel veri | Bir reklam platformuna düz metin olarak gönderilen e-posta adresi |
|
|
50
50
|
| Planda olmayan event'ler | Ölçümleme planınızda bulunmayan, yine de fire olan event'ler |
|
|
51
51
|
| Plan kapsamı | Planlanan her event'in uygulandı, eksik, parametresi eksik, değeri hatalı ya da kontrol edilmedi olarak sınıflandırılması (bkz. [Plan ve site](#plan-ve-site)) |
|
|
52
|
+
| Kampanya parametreleri | Açılış adresindeki `utm_campaign` ya da `gclid`'in bir yönlendirmede düşmesi, ya da isteklerin bildirdiği sayfa adresinde eksik olması. Ziyaret doğrudan trafiğe yazılır |
|
|
53
|
+
| Sunucu tarafı takip | Aynı event kimliğinin iki farklı event'te kullanılması, ya da Meta event kimliğinin sunucu konteynerinin Conversions API olayına dönüştürdüğü GA4 isteğindeki `event_id` ile eşleşmemesi |
|
|
52
54
|
|
|
53
55
|
Platformlar: GA4 (sitenin kendi alan adındaki sunucu tarafı GTM uç noktaları ve Google etiket geçidi yolları dahil; bir sunucu konteynerine giden istekler sunucu tarafı olarak işaretlenir, Stape ve Elevar istekleri tanınır), Google Ads dönüşümleri, Meta Pixel, TikTok Pixel, temel X Pixel event'leri, LinkedIn Insight Tag, Microsoft Advertising UET, Criteo OneTag ile Pinterest, Snapchat ve Klaviyo event'leri. Adobe Analytics, Matomo ve Piwik PRO event'leri de okunur (sayfa görüntülemeleri, bağlantılar, hedefler, site içi arama ve e-ticaret siparişleri); Microsoft Clarity ve Hotjar etiket yüklemeleri ise proje ya da site kimliğini gösterir.
|
|
54
56
|
|
|
57
|
+
GA4 istekleri başka bir ana makinedeki bir sunucu konteynerine gidiyorsa ya da yükleyicisi oradan sunuluyorsa, rapor bir bilgi bulgusu ekler: konteynerin ad platformlarına ilettiği event'ler tarayıcıdan geçmez, bu yüzden burada görülemez.
|
|
58
|
+
|
|
59
|
+
`gtm_container` dışa aktarımı verildiğinde, her hatalı event onu gönderen GTM etiketini ve tetiklendiği tetikleyiciyi adlandırır (ya da hiçbir etiketin göndermediğini söyler). Yapay zekâ modeli gerekmez.
|
|
60
|
+
|
|
55
61
|
### Kontrol edilecek platformları seçin
|
|
56
62
|
|
|
57
63
|
Varsayılan olarak bir denetim GA4, Google Ads, Meta ve TikTok'u kontrol eder; ayrıca sitenin kullandığı başka her platformu, ilk isteğinden itibaren kontrol eder. Bunun için bir ayar gerekmez. Platform seçmek bu otomatik algılamayı kapatır. Sabit bir liste için `--platforms ga4,meta,x` kullanın, ayar dosyasında `"providers"` ya da MCP `start_audit` ve `run_audit` araçlarında `providers` verin. Seçenekler: `ga4`, `google_ads`, `meta`, `tiktok`, `x`, `linkedin`, `microsoft_ads`, `pinterest`, `snapchat`, `criteo`, `klaviyo`, `adobe`, `matomo`, `piwik_pro`, `clarity`, `hotjar`, `doubleclick` ve `adsense`. GA4, Google Ads, Meta, TikTok, X, LinkedIn, Microsoft Ads, Pinterest, Snapchat, Criteo, Klaviyo, Adobe Analytics, Matomo ve Piwik PRO için event'ler ayrıştırılır. Clarity ve Hotjar etiket yüklemelerini gösterir; diğerlerinin ağ istekleri gösterilir. `--providers` eski adı olarak bir minör sürüm daha çalışır ve bir not yazdırır.
|
|
58
64
|
|
|
59
65
|
Seçmediğiniz platformlara giden istekler de yerelde yanıtlanır, yani o hesaplara da ulaşmaz. Bu istekler zaman akışına, CSV'ye ve kontrollere alınmaz, sadece sayılır: rapor bunları istek sayılarıyla listeler (örneğin "Kapsam dışı 61 istek atlandı: Microsoft Clarity, DoubleClick") ve her adım kaç istek atladığını gösterir. `--platforms` vermezseniz, ölçümleme planınızda geçen ya da sitenin gönderdiği her platform varsayılan listeye eklenir. Dışarıda bıraktığınız platformlar için planlanmış event'ler eksik olarak raporlanmaz, tek bir bilgi bulgusunda listelenir. Etiket yöneticisi ve çerez onayı platformu istekleri her zaman gösterilir.
|
|
60
66
|
|
|
67
|
+
### Onay senaryoları
|
|
68
|
+
|
|
69
|
+
Her denetim ayrıca başlangıç sayfasını her çerez onay seçimi için bir kez açar; her seçim yeni bir tarayıcıda yapılır: tümünü kabul, tümünü reddet ve yalnızca analitik. Seçim, consent platformunun kendi API'siyle (OneTrust, Cookiebot, Usercentrics ya da Didomi) yapılır; API yoksa bannerın düğmelerine tıklanır. Rapor, her seçimden sonra her etiketin kaç istek gönderdiğini gösterir. Yalnızca analitik seçeneği OneTrust ve Cookiebot ile çalışır; diğer platformlarda o sütun "Burada desteklenmiyor" yazar. Tümünü reddet'ten sonra gönderilen bir etiket işaretlenir; Consent Mode'un çerezsiz ping'leri hariç. Yalnızca analitik'ten sonra gönderilen bir reklam etiketi, reklam depolaması reddedilmiş olarak gönderilmediği sürece işaretlenir. Tablo **Yolculuklar** sekmesinin en üstünde ve `report.md` içinde bulgulardan önce yer alır. Başlangıç sayfasında aracın yanıtlayabileceği bir banner yoksa ya da kayıtlı bir site tekrar oynatılıyorsa atlanır. Yapay zekâ modeli çağrılmaz, ama başlangıç sayfası üç kez daha ziyaret edilir. Kapatmak için ayar dosyasına `"consent_scenarios": false` yazın.
|
|
70
|
+
|
|
61
71
|
## Nasıl çalışır
|
|
62
72
|
|
|
63
73
|
1. **Gezinme:** Yapay zekâ modeli, sade bir dille yazılmış bir hedef ("sepete herhangi bir ürün ekle ve test ödeme yöntemiyle siparişi tamamla") ve sayfanın metin görünümünü alır. İki araçla çalışır: `browser_action` (goto, click, fill, select, scroll, wait) ve `finish`. Playwright ile her seferinde bir adım atar.
|
|
@@ -238,7 +248,7 @@ Siteniz zaten gönderdiği event'ler `"seen": true` ile işaretlenir.
|
|
|
238
248
|
|
|
239
249
|
Kullanmadan önce planı gözden geçirin. Örnekler ve öneriler yapay zekânın gördüğü sayfalardan gelir, bu yüzden her event'in işinize uyduğunu kontrol edin. Ek alanları kontroller yok sayar.
|
|
240
250
|
|
|
241
|
-
**Elektronik tablo ile içe ve dışa aktarma.** `aitd plan export <plan> --site <url>`, planın yanına `plan.xlsx` yazar: `events` sekmesinde her event parametresi için bir satır (`event, platform, parameter, required, example, trigger, description`) ve `user_properties` sekmesinde her kullanıcı özelliği için bir satır. Bunun yerine `events.csv` ve `user_properties.csv` almak için `--format csv` ekleyin; CSV ek bir paket gerektirmez. `.xlsx`, araçla birlikte isteğe bağlı bir bağımlılık olarak gelen küçük bir kütüphane, `fflate` kullanır. `aitd plan import <dosya> --site <url>` elektronik tabloyu okuyup yeni bir plan klasörü olarak kaydeder. İçe aktarma mevcut bir planı asla düzenlemez. Tanınmayan sütunlar bir uyarıyla yok sayılır; bir hata, sekmeyi, satırı ve düzeltmeyi belirtir, örneğin `events, row 7: "required" must be yes or no (found "maybe").`
|
|
251
|
+
**Elektronik tablo ile içe ve dışa aktarma.** `aitd plan export <plan> --site <url>`, planın yanına `plan.xlsx` yazar: `events` sekmesinde her event parametresi için bir satır (`event, platform, parameter, required, example, trigger, description`) ve `user_properties` sekmesinde her kullanıcı özelliği için bir satır. Bunun yerine `events.csv` ve `user_properties.csv` almak için `--format csv` ekleyin; CSV ek bir paket gerektirmez. `.xlsx`, araçla birlikte isteğe bağlı bir bağımlılık olarak gelen küçük bir kütüphane, `fflate` kullanır. `aitd plan import <dosya> --site <url>` elektronik tabloyu okuyup yeni bir plan klasörü olarak kaydeder. Google E-Tablolar'dan indirirken Dosya > İndir > Microsoft Excel (.xlsx) seçin ya da her sekmeyi CSV olarak indirin: `Shop plan - events.csv` dosyası, yanındaki `Shop plan - user_properties.csv` ile birlikte okunur ve onay kutuları evet/hayır olarak tanınır. İçe aktarma mevcut bir planı asla düzenlemez. Tanınmayan sütunlar bir uyarıyla yok sayılır; bir hata, sekmeyi, satırı ve düzeltmeyi belirtir, örneğin `events, row 7: "required" must be yes or no (found "maybe").`
|
|
242
252
|
|
|
243
253
|
`measurement-plan.json` şöyle görünür:
|
|
244
254
|
|
|
@@ -263,6 +273,15 @@ Event adları gevşek eşleştirilir: `dl_add_to_cart` (Elevar), `addToCart` ve
|
|
|
263
273
|
|
|
264
274
|
`aitd plan list --site <url>`, sitenin plan kimliklerini en eskiden en yeniye listeler.
|
|
265
275
|
|
|
276
|
+
**Planı ilk çalıştırmadan öğrenin.** Sitenin kayıtlı bir planı yokken, `--against` ya da `--out` olmadan yapılan bir çalıştırma, sitenin zaten gönderdiği event'leri listeler ve bunları sitenin planı olarak kaydedip kaydetmeyeceğinizi sorar:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
No plan yet for this site. It already sends 3 events: ga4:page_view, ga4:purchase, meta:PageView
|
|
280
|
+
Save them as the site's plan? (Y/n)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Evet derseniz sonraki çalıştırmalar `--against latest` ile bu plana karşı kontrol yapabilir. Bir event'in her isteğinde bulunan bir parametre, o event'in zorunlu parametrelerinden biri olur. Hiçbir değer kopyalanmaz, bu yüzden bir ziyaretçinin yazdıkları plana girmez. Bir betikte ya da CI işinde `--yes` eklemedikçe hiçbir şey kaydedilmez; çalıştırma bunun yerine daha ayrıntılı bir plan taslağı için `aitd plan --site` komutunu yazdırır.
|
|
284
|
+
|
|
266
285
|
### Plan ve site
|
|
267
286
|
|
|
268
287
|
Bir çalıştırma `--against` kullandığında rapor, `report.html` ve `report.md` dosyalarında **Plan ve site** bölümüyle başlar. Planlanan her event bir kovaya düşer:
|
|
@@ -278,6 +297,12 @@ Bir çalıştırma `--against` kullandığında rapor, `report.html` ve `report.
|
|
|
278
297
|
|
|
279
298
|
Kapsam puanı, uygulanmış olanların kontrol edilebilir planlanmış event'lere (kontrol edilmedi hariç hepsi) bölünmesiyle bulunur; genel ve platform başına gösterilir, örneğin `Kapsam 11 olaydan 7 (%64), 2 olay kontrol edilmedi`. Aynı kayıtlar `plan-check.json` dosyasında da bulunur: planlanan her event için kovasıyla birlikte bir kayıt. Çıkış kodları değişmez: `--fail-on` yine bulgu önemine bakar. `--against` olmadan rapor tam olarak eskisi gibidir.
|
|
280
299
|
|
|
300
|
+
Kontrol edilen plan sitenin ilk planı değilse, rapor başlığı onu bir önceki kayıtlı sürümle karşılaştırır: `Plan 2026-10-01_0900 sürümünden bu yana değişti: 1 eklendi, 0 çıkarıldı, 2 değişti` ya da `Plan 2026-10-01_0900 sürümünden bu yana değişmedi`. Başlık her değişikliği `report.html` ve `report.md` içinde listeler: eklenen event için `+`, çıkarılan için `−`, değişen için `~`, ardından neyin değiştiği (zorunlu parametreler, parametreler ya da yolculuklar). İlk sürümün karşılaştırılacak bir şeyi yoktur; `capture.json` aynı karşılaştırmayı `planChanges` altında da saklar.
|
|
301
|
+
|
|
302
|
+
### Etiket envanteri
|
|
303
|
+
|
|
304
|
+
Her raporda ayrıca bir **etiket envanteri** bulunur: sitenin istek gönderdiği her sağlayıcı; istekleri sitenin kendi alan adına gittiyse birinci taraf, sağlayıcıya gittiyse üçüncü taraf, ikisine de gittiyse ikisi olarak işaretlenir. Sağlayıcının görüldüğü sayfa sayısını ve ona bağlı çerezleri gösterir. Hiçbir sağlayıcıya ait olmayan çerezler ayrıca birinci ya da üçüncü taraf olarak listelenir. Yalnızca çerez adları kaydedilir, değerleri asla. Envanter `report.html` içindeki **Kapsam ve plan** sekmesinde, `report.md` içinde ise bir tablo olarak yer alır.
|
|
305
|
+
|
|
281
306
|
### Klasör yapısı ve plan kimlikleri
|
|
282
307
|
|
|
283
308
|
Planlar ve çalıştırmalar, aracı çalıştırdığınız klasördeki `aitd/` içinde tutulur:
|
|
@@ -384,7 +409,7 @@ Bir yolculuk her iki görünümde de çalıştıysa yalnızca ilk görünümün
|
|
|
384
409
|
|---|---|
|
|
385
410
|
| `site` | Temel URL. Mümkünse staging kullanın. |
|
|
386
411
|
| `plan` | Ölçümleme planı: bir dosya yolu ya da satır içi bir nesne (aşağıya bakın). Hazır yolculuklarla isteğe bağlıdır. `--against` ile çalışan bir çalıştırma bunun yerine o planı kullanır. |
|
|
387
|
-
| `gtm_container` | İsteğe bağlı GTM konteyner dışa aktarımı (Yönetici → Konteyneri dışa aktar)
|
|
412
|
+
| `gtm_container` | İsteğe bağlı GTM konteyner dışa aktarımı (Yönetici → Konteyneri dışa aktar). Her hatalı event, onu gönderen etiketi ve tetikleyicisini adlandırır (ya da hiçbir etiketin göndermediğini söyler), yapay zekâ modeli olmadan da. |
|
|
388
413
|
| `viewports` | `desktop` ve/veya `mobile`. |
|
|
389
414
|
| `journeys` | `--journey` ile hazır yolculuk seçtiğinizde, `--goal` ile bir yolculuk tarif ettiğinizde ya da `--against` ile bir plana karşı kontrol ettiğinizde isteğe bağlıdır. |
|
|
390
415
|
| `journeys[].goal` | Yapay zekânın sade bir dille ne yapacağı. |
|
|
@@ -392,10 +417,13 @@ Bir yolculuk her iki görünümde de çalıştıysa yalnızca ilk görünümün
|
|
|
392
417
|
| `journeys[].steps` | `--scripted` çalıştırmaları için isteğe bağlı sabit adımlar: `{ "action": "click", "selector": "#add-to-cart" }`. |
|
|
393
418
|
| `allowed_hosts` | Yolculukların ziyaret edebileceği başka ana makineler, örneğin barındırılan bir ödeme için `["checkout.payments-provider.com"]`. Tarayıcı aksi halde `site` ana makinesinde ve alt alan adlarında kalır. `--allow-host` ile aynıdır. |
|
|
394
419
|
| `providers` | Kontrol edilecek takip platformları, örneğin `["ga4", "meta"]`. Varsayılan `["ga4", "google_ads", "meta", "tiktok"]`; bu ayar boşken sitenin gönderdiği diğer platformlar da eklenir. `--platforms` ile aynıdır. |
|
|
420
|
+
| `campaign_checks` | Varsayılan `true`. Her çalıştırma ilk sayfayı bir reklam tıklaması gibi açar (UTM parametreleri, `gclid`, `fbclid` ya da başlangıç adresinde zaten olanlar) ve bunların yönlendirmelerden geçip isteklere ulaştığını denetler. Düz adresi açmak için `false` yapın. |
|
|
395
421
|
| `block_hits` | Varsayılan `true`. Takip isteklerinin gerçek uç noktalarınıza ulaşmasına izin vermek için `false` yapın. |
|
|
422
|
+
| `consent_scenarios` | Varsayılan `true`. [Onay senaryolarını](#onay-senaryoları) atlamak için `false` yapın; her onay seçimi için başlangıç sayfasını bir kez ziyaret ederler. |
|
|
396
423
|
| `language` | Rapor dili, `en` (varsayılan) ya da `tr`. `--lang` ile aynıdır. |
|
|
397
424
|
| `keep_bodies` | Varsayılan `false`. Takip isteklerini gönderildiği gibi, kullanıcı verisi gizlenmeden saklamak için `true` yapın. `--keep-bodies` ile aynıdır. |
|
|
398
425
|
| `branding` | Varsayılan `true`. Analitik İşler logosunu ve "Generated by AI Tag Debugger · analitikisler.com" satırını raporlardan çıkarmak için `false` yapın. `--no-branding` ile aynıdır, ya da MCP `finish_audit` ve `run_audit` araçlarında `branding: false`. |
|
|
426
|
+
| `report` | İsteğe bağlı beyaz etiket: `{ "name": "Acme", "logo": "logo.png", "color": "#0a7d5a", "pdf": true }`. `logo` (PNG, JPG, WebP, GIF veya SVG; en fazla 512 KB; ayar dosyasına göre bağıl) ve `name`, HTML raporundaki Analitik İşler markasının yerine geçer (küçük bir AI Tag Debugger logosu ve kredisi kalır). `color` vurgu rengini ayarlar; `pdf: true` ise `report.html` yanına `report.pdf` de yazar. |
|
|
399
427
|
| `provider` | İsteğe bağlı: `anthropic`, `openai`, `gemini`, `vercel`, `cloudflare` ya da `openai-compatible`. `--provider` ile aynıdır. |
|
|
400
428
|
| `base_url`, `model` | İsteğe bağlı: API adresi ve model kimliği, örneğin `openai-compatible` için. `--base-url` ve `--model` ile aynıdır. |
|
|
401
429
|
| `notify` | İsteğe bağlı uyarılar, kayıtlı her çalıştırmadan sonra: `slack` (bir webhook adresi ya da `SLACK_WEBHOOK_URL` değişkenini okumak için `true`) ve `email` (bir adres ya da liste). Bkz. [Uyarılar](#uyarılar). |
|
|
@@ -479,12 +507,12 @@ aitd mcp [--http] [--port <n>] [--host <adres>] [--token <gizli>] [--public-url
|
|
|
479
507
|
- `report.html`: dört sekmeli tek bir dosya. Bağlantılar sekmeler arasında çalışır (bir bulgunun "Nerede" çipi o adımı açar) ve her görünümün paylaşılabilir ya da yer imine eklenebilir kendi URL'si vardır.
|
|
480
508
|
- **Genel bakış**: sorun sayıları, yapay zekâ özeti (en önemli sorun önce, ve bulguların kapsamadığı kontrol edilmeye değer şeyler), yolculuklar ve sorun sayıları, bir **Bu çalıştırma** kartı (platformlar, yapay zekâ sağlayıcısı ve modeli, plan, yolculuklar, ekranlar, dil, sürüm) `--against` ile [Plan ve site](#plan-ve-site) kapsamı. Yapay zekâ siteyi son çalıştırmadan farklı bir yoldan gezdiyse, başlıkta yolu değişen yolculukları gösteren bir not ve **Son çalıştırmadan bu yana değişenler** bölümünde değişiklikten önceki ve sonraki sayfalar bulunur.
|
|
481
509
|
- **Bulgular**: düzeltmesini kimin yapacağına göre gruplu (Yazılımcı, Tag Manager, Pazarlama) ve önem düzeyine göre filtrelenebilir. Her birinde etkisi, olası nedeni, kanıtı, numaralı düzeltme adımları, olayın yaşandığı adımlar ve **Nasıl doğrulanır** listesi bulunur; bu liste düzeltmeyi kontrol edebileceğiniz araçlara bağlantı verir (Tag Assistant, GA4 DebugView, Meta Test Events, TikTok Pixel Helper).
|
|
482
|
-
- **Yolculuklar**: her seferinde bir yolculuk. Üstteki **Onay** kartı Consent Mode varsayılanını ve her güncellemeyi, sinyal başına bir kutuyla gösterir. Altında solda adımları, sağda seçili adım. Sorunlu ilk adımda açılır. Her adımın dört sekmesi vardır:
|
|
510
|
+
- **Yolculuklar**: her seferinde bir yolculuk. Başlangıç sayfasında bir onay bannerı varsa, yolculukların üstünde [onay senaryoları](#onay-senaryoları) tablosu yer alır. Üstteki **Onay** kartı Consent Mode varsayılanını ve her güncellemeyi, sinyal başına bir kutuyla gösterir. Altında solda adımları, sağda seçili adım. Sorunlu ilk adımda açılır. Her adımın dört sekmesi vardır:
|
|
483
511
|
- **Zaman akışı**: adım sırasıyla. Önce her dataLayer gönderimi, ardından onu tetikleyen GTM event'i (ya da çerez onayı komutu), sonra da yol açtığı platform istekleri gelir. Gönderimi olmayan istekler en sonda listelenir.
|
|
484
512
|
- **Event'ler**: her takip isteği, parametreleri, çerez onayı durumu, arkasındaki dataLayer gönderimi ve isteği ve bir bulgu buraya işaret ediyorsa bir not.
|
|
485
513
|
- **dataLayer**: adımdaki her gönderim.
|
|
486
514
|
- **Ağ istekleri**: seçilen platformlara giden her istek; her parametrede bir `?` ipucuyla bir parametre tablosuna çözülmüş. Toplu bir GA4 isteği, içindeki her event'i gösterir.
|
|
487
|
-
- **Kapsam ve plan**: planlanan her event'in her yolculukta gönderilip gönderilmediği ve kontrol edilen ya da dışarıda bırakılan platformlar, istek sayılarıyla.
|
|
515
|
+
- **Kapsam ve plan**: planlanan her event'in her yolculukta gönderilip gönderilmediği ve kontrol edilen ya da dışarıda bırakılan platformlar, istek sayılarıyla. Ayrıca [etiket envanteri](#etiket-envanteri): her sağlayıcı, birinci ya da üçüncü taraf, sayfaları ve çerezleri.
|
|
488
516
|
|
|
489
517
|
Event'ler ve istekler kaynağa göre filtrelenebilir. GA4 ürün dizgileri (`pr1`…) ve dataLayer `ecommerce.items` ürün kartları olarak gösterilir; özel ürün parametreleri kendi grubunda yer alır. Raporda betik yoktur: sekmeler ve filtreler düz HTML ve CSS'tir (Chrome 105, Safari 15.4 ya da Firefox 121 ve sonrası gerekir) ve yazdırırken her sekme görünür. Logolar gömülüdür, bu yüzden internetten hiçbir şey yüklenmez. `--no-branding`, Analitik İşler logosunu ve kredi satırını çıkarır.
|
|
490
518
|
- `timeline.csv`: aynı zaman akışı, her isteğe, dataLayer gönderimine ve ağ çağrısına bir satır olarak; Excel ya da Google Sheets için hazır.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analitikisler/ai-tag-debugger",
|
|
3
3
|
"mcpName": "io.github.analitikisler/ai-tag-debugger",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "Open-source AI agent that QA-tests your website's analytics, ad and consent tracking (GA4, GTM, Google Ads, Meta, TikTok). Bring your own model (Claude, OpenAI, Gemini) or use it from your chat app over MCP.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/src/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { AuditSession } from "./browser.js";
|
|
3
|
+
import { withCampaignParams } from "./campaign.js";
|
|
3
4
|
import { messages } from "./i18n.js";
|
|
4
5
|
import { createProvider, PROVIDERS } from "./providers.js";
|
|
5
6
|
|
|
@@ -200,8 +201,10 @@ export async function runJourney(config, journey, viewport, opts = {}) {
|
|
|
200
201
|
});
|
|
201
202
|
try {
|
|
202
203
|
await session.start();
|
|
203
|
-
|
|
204
|
-
const
|
|
204
|
+
// The first page is opened the way an ad click opens it, so the campaign checks have something to follow.
|
|
205
|
+
const start = new URL(journey.start ?? "/", config.site).href;
|
|
206
|
+
const startUrl = config.campaign_checks === false ? start : withCampaignParams(start);
|
|
207
|
+
const startError = await session.act({ action: "goto", url: startUrl, reason: `Open ${start}` });
|
|
205
208
|
// A replayed journey that was unfinished when saved keeps its saved note, however it fails this time.
|
|
206
209
|
if (startError) return { ...collect(session, journey, viewport), completed: false, note: journey.incomplete ?? `Could not open the start page: ${startError}` };
|
|
207
210
|
|
|
@@ -248,5 +251,6 @@ export function collect(session, journey, viewport) {
|
|
|
248
251
|
bodyBudgetStep: session.bodyBudgetStep,
|
|
249
252
|
startedAt: session.startedAt,
|
|
250
253
|
pageErrors: session.errors,
|
|
254
|
+
landing: session.landing,
|
|
251
255
|
};
|
|
252
256
|
}
|
package/src/audit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A full audit from a config file: run every journey, check, explain, write the reports.
|
|
2
2
|
// Shared by the CLI and the MCP server's run_audit tool.
|
|
3
3
|
import { newRunDir } from "./workspace.js";
|
|
4
|
-
import { compareRuns, previousRun, routeHints } from "./changes.js";
|
|
4
|
+
import { compareRuns, previousPlanChanges, previousRun, routeHints } from "./changes.js";
|
|
5
5
|
import { sendAlerts } from "./notify.js";
|
|
6
6
|
import { readFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
@@ -10,10 +10,13 @@ import { VIEWPORTS } from "./browser.js";
|
|
|
10
10
|
import { checkRun, groupFindings, planCheck, summarize } from "./checks.js";
|
|
11
11
|
import { explainFindings, renderCsv, renderHtml, renderMarkdown } from "./report.js";
|
|
12
12
|
import { LANGUAGES, messages } from "./i18n.js";
|
|
13
|
+
import { loadReportBrand, writePdf } from "./whitelabel.js";
|
|
13
14
|
import { createProvider, alsoSet } from "./providers.js";
|
|
14
15
|
import { filterFindings, JOURNEY_PRESETS, planRun } from "./journeys.js";
|
|
15
16
|
import { resolveProviders } from "./parsers.js";
|
|
17
|
+
import { annotateFindings } from "./gtm.js";
|
|
16
18
|
import { SAVED_SITE_FILE, writeSavedSite } from "./replay.js";
|
|
19
|
+
import { runConsentScenarios } from "./consent.js";
|
|
17
20
|
|
|
18
21
|
export async function readJson(file, baseDir = ".") {
|
|
19
22
|
const full = path.resolve(baseDir, file);
|
|
@@ -32,7 +35,8 @@ export async function loadConfig(configFile) {
|
|
|
32
35
|
const plan = typeof config.plan === "string" ? await readJson(config.plan, configDir) : config.plan;
|
|
33
36
|
if (plan && !Array.isArray(plan.events)) throw new Error('The "plan" needs an "events" list.');
|
|
34
37
|
const gtmContainer = config.gtm_container ? await readJson(config.gtm_container, configDir) : null;
|
|
35
|
-
|
|
38
|
+
const brand = await loadReportBrand(config.report, configDir);
|
|
39
|
+
return { config, plan, gtmContainer, brand };
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
|
|
@@ -41,26 +45,35 @@ export async function loadConfig(configFile) {
|
|
|
41
45
|
* previous: the site's last saved run (see previousRun), null when there is none, or undefined to skip
|
|
42
46
|
* the "Changed since last run" section. Returns { reportPath, changes }.
|
|
43
47
|
*/
|
|
44
|
-
export async function writeReports({ outDir, config, plan, runs, findings, explained, lang, ai = null, against = null, branding = config.branding !== false, previous = undefined }) {
|
|
48
|
+
export async function writeReports({ outDir, config, plan, runs, findings, explained, lang, ai = null, against = null, branding = config.branding !== false, previous = undefined, planChanges = null, consentScenarios = null, brand = null }) {
|
|
45
49
|
await mkdir(outDir, { recursive: true });
|
|
46
50
|
const site = new URL(config.site).hostname;
|
|
47
51
|
const title = messages(lang).ui.title(site);
|
|
48
52
|
const generatedAt = new Date().toISOString().slice(0, 16).replace("T", " ") + " UTC";
|
|
49
53
|
const check = against ? { plan_id: against.id ?? null, ...planCheck(plan, runs, findings) } : null;
|
|
50
54
|
const changes = previous === undefined ? null : compareRuns(previous, { findings, planCheck: check, runs });
|
|
51
|
-
const report = { title, site, ai, generatedAt, findings, explained, runs, plan, lang, branding, against: against?.label ?? null, planCheck: check, changes };
|
|
55
|
+
const report = { title, site, ai, generatedAt, findings, explained, runs, plan, lang, branding, against: against?.label ?? null, planCheck: check, changes, planChanges, consentScenarios, brand };
|
|
52
56
|
await writeFile(path.join(outDir, "report.md"), renderMarkdown(report));
|
|
53
57
|
await writeFile(path.join(outDir, "report.html"), renderHtml(report));
|
|
54
58
|
await writeFile(path.join(outDir, "timeline.csv"), renderCsv(report));
|
|
55
59
|
if (check) await writeFile(path.join(outDir, "plan-check.json"), JSON.stringify(check, null, 2));
|
|
56
|
-
await writeFile(path.join(outDir, "capture.json"), JSON.stringify({ config: { ...config, plan }, ...(against ? { against: { plan_id: against.id, file: against.file } } : {}), runs, findings, explained, ...(changes ? { changes } : {}) }, null, 2));
|
|
57
|
-
|
|
60
|
+
await writeFile(path.join(outDir, "capture.json"), JSON.stringify({ config: { ...config, plan }, ...(against ? { against: { plan_id: against.id, file: against.file } } : {}), runs, findings, explained, ...(changes ? { changes } : {}), ...(planChanges ? { planChanges } : {}), ...(consentScenarios ? { consentScenarios } : {}) }, null, 2));
|
|
61
|
+
const reportPath = path.join(outDir, "report.html");
|
|
62
|
+
if (brand?.pdf) {
|
|
63
|
+
try {
|
|
64
|
+
await writePdf(reportPath, path.join(outDir, "report.pdf"));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// The HTML report is the main output; a PDF that can't be made is a warning, not a failed run.
|
|
67
|
+
console.error(`Could not write report.pdf: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { reportPath, changes };
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
/**
|
|
61
74
|
* @param {{ config: any, plan?: any, gtmContainer?: any, journeys?: string[], goals?: string[], events?: string[], viewports?: string[],
|
|
62
75
|
* scripted?: boolean, ai?: boolean, provider?: string, model?: string, baseUrl?: string, maxSteps?: number, lang?: string, headed?: boolean,
|
|
63
|
-
* outDir?: string, dataDir?: string, branding?: boolean, providers?: string[], saveSite?: string, replaySite?: string, log?: (m: string) => void }} opts
|
|
76
|
+
* outDir?: string, dataDir?: string, branding?: boolean, brand?: Awaited<ReturnType<typeof loadReportBrand>>, providers?: string[], saveSite?: string, replaySite?: string, log?: (m: string) => void }} opts
|
|
64
77
|
* journeys: config journey ids or ready-made journeys (purchase, add_to_cart, sign_up, lead, search).
|
|
65
78
|
* goals: extra journeys described in plain language. events: only check these events.
|
|
66
79
|
* providers: tracking providers to check (default: config "providers", else GA4, Google Ads, Meta, TikTok plus
|
|
@@ -127,7 +140,12 @@ export async function runAudit(opts) {
|
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
|
|
143
|
+
// Consent scenarios need the live site, so a replay from a saved site skips them. "consent_scenarios": false turns them off.
|
|
144
|
+
const consentScenarios = opts.replaySite || config.consent_scenarios === false
|
|
145
|
+
? null
|
|
146
|
+
: await runConsentScenarios(config, { headed: opts.headed, log }).catch((err) => (log(` Consent scenarios failed: ${err.message}`), null));
|
|
147
|
+
|
|
148
|
+
const findings = annotateFindings(filterFindings(runs.flatMap((run) => checkRun(plan, run, { lang })), events), gtmContainer);
|
|
131
149
|
let saveError = null;
|
|
132
150
|
if (opts.saveSite) {
|
|
133
151
|
try {
|
|
@@ -149,12 +167,12 @@ export async function runAudit(opts) {
|
|
|
149
167
|
// Runs saved in the site's runs folder are compared with the one before; a run sent to --out is not.
|
|
150
168
|
const previous = opts.outDir ? undefined : await previousRun(opts.dataDir, config.site);
|
|
151
169
|
const outDir = path.resolve(opts.outDir ?? (await newRunDir(opts.dataDir, config.site)).dir);
|
|
152
|
-
const { reportPath, changes } = await writeReports({ outDir, config, plan, runs, findings, explained, lang, ai: provider ? { name: provider.name, model: provider.model } : null, against, branding: opts.branding ?? config.branding !== false, previous });
|
|
170
|
+
const { reportPath, changes } = await writeReports({ outDir, config, plan, runs, findings, explained, lang, ai: provider ? { name: provider.name, model: provider.model } : null, against, branding: opts.branding ?? config.branding !== false, previous, planChanges: against ? await previousPlanChanges(opts.dataDir, config.site, against.id, plan).catch(() => null) : null, consentScenarios, brand: opts.brand ?? null });
|
|
153
171
|
if (config.notify && changes) {
|
|
154
172
|
const { sent, errors } = await sendAlerts({ notify: config.notify, site: new URL(config.site).hostname, changes: { run: path.basename(outDir), ...changes }, lang, reportPath });
|
|
155
173
|
if (sent.length) log(`\nSent the changes since the last run to ${sent.join(" and ")}.`);
|
|
156
174
|
for (const e of errors) log(`\nCould not send an alert. ${e}`);
|
|
157
175
|
}
|
|
158
176
|
const counts = summarize(groupFindings(findings));
|
|
159
|
-
return { journeys, plan, runs, findings, explained, counts, reportPath, outDir, lang, ...(against ? { againstId: against.id } : {}), ...(saveError ? { saveError } : {}) };
|
|
177
|
+
return { journeys, plan, runs, findings, explained, counts, reportPath, outDir, lang, consentScenarios, ...(against ? { againstId: against.id } : {}), ...(saveError ? { saveError } : {}) };
|
|
160
178
|
}
|
package/src/browser.js
CHANGED
|
@@ -382,6 +382,7 @@ export class AuditSession {
|
|
|
382
382
|
this.dataLayerLog = [];
|
|
383
383
|
this.network = [];
|
|
384
384
|
this.providers = resolveProviders(this.opts.providers);
|
|
385
|
+
this.landing = null; // the first page: { step, requested, chain, final }
|
|
385
386
|
this.detect = this.opts.detect ?? !this.opts.providers?.length;
|
|
386
387
|
this.skipped = {}; // step index -> { vendor label: requests }
|
|
387
388
|
this.loadedIn = new Map(); // page address -> the last step whose main-frame navigation reached it
|
|
@@ -426,6 +427,8 @@ export class AuditSession {
|
|
|
426
427
|
if (frame !== this.page.mainFrame()) return;
|
|
427
428
|
this.loadedIn.set(pageKey(frame.url()), this.currentStep);
|
|
428
429
|
this.leaving = null;
|
|
430
|
+
// Script redirects on the first page move where it ends up.
|
|
431
|
+
if (this.landing && this.landing.step === this.currentStep) this.landing.final = frame.url();
|
|
429
432
|
});
|
|
430
433
|
// From the moment the main frame starts loading another document, the open page is leaving.
|
|
431
434
|
this.page.on("request", (request) => {
|
|
@@ -464,6 +467,7 @@ export class AuditSession {
|
|
|
464
467
|
const vendor = vendorOf(request.url());
|
|
465
468
|
const call = {
|
|
466
469
|
step: this.currentStep,
|
|
470
|
+
pageUrl: this.opts.keepBodies ? this.page.url() : redactUrl(this.page.url()).url,
|
|
467
471
|
t: Date.now(),
|
|
468
472
|
method: request.method(),
|
|
469
473
|
url: this.opts.keepBodies ? request.url() : redactUrl(request.url()).url,
|
|
@@ -844,7 +848,13 @@ export class AuditSession {
|
|
|
844
848
|
step.replay = { action: "goto", url: target };
|
|
845
849
|
const refused = await this.checkUrl(target);
|
|
846
850
|
if (refused) return refused;
|
|
847
|
-
await this.page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
851
|
+
const response = await this.page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
852
|
+
if (!this.landing) {
|
|
853
|
+
// The first page: the address asked for, each redirect on the way, and where it ended up.
|
|
854
|
+
const chain = [];
|
|
855
|
+
for (let r = response?.request(); r; r = r.redirectedFrom()) chain.unshift(r.url());
|
|
856
|
+
this.landing = { step: this.currentStep, requested: target, chain, final: this.page.url() };
|
|
857
|
+
}
|
|
848
858
|
break;
|
|
849
859
|
}
|
|
850
860
|
case "click":
|
package/src/campaign.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Campaign parameters an ad click puts on the landing address. They have to survive the site's
|
|
2
|
+
// redirects and show up in the page address its tags report, or the visit is credited to "direct".
|
|
3
|
+
|
|
4
|
+
export const CLICK_IDS = ["gclid", "wbraid", "gbraid", "fbclid", "ttclid", "msclkid"];
|
|
5
|
+
export const UTM_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
|
6
|
+
export const CAMPAIGN_PARAMS = [...CLICK_IDS, ...UTM_PARAMS];
|
|
7
|
+
|
|
8
|
+
// Values a run adds to a landing address that has none of its own. Hits are held back by default
|
|
9
|
+
// (block_hits), so these never reach a real analytics property.
|
|
10
|
+
const TEST_PARAMS = { utm_source: "aitd", utm_medium: "check", utm_campaign: "aitd-campaign-check", gclid: "aitd-gclid", fbclid: "aitd-fbclid" };
|
|
11
|
+
|
|
12
|
+
/** The campaign parameters in an address, as {name: value}. */
|
|
13
|
+
export function campaignParamsIn(url) {
|
|
14
|
+
try {
|
|
15
|
+
const u = new URL(url);
|
|
16
|
+
return Object.fromEntries(CAMPAIGN_PARAMS.filter((n) => u.searchParams.get(n)).map((n) => [n, u.searchParams.get(n)]));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The address with test campaign parameters added, unless it already carries campaign parameters. */
|
|
23
|
+
export function withCampaignParams(url) {
|
|
24
|
+
if (Object.keys(campaignParamsIn(url)).length) return url;
|
|
25
|
+
const u = new URL(url);
|
|
26
|
+
for (const [k, v] of Object.entries(TEST_PARAMS)) u.searchParams.set(k, v);
|
|
27
|
+
return u.href;
|
|
28
|
+
}
|
package/src/changes.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { messages } from "./i18n.js";
|
|
4
4
|
import { platformLabel } from "./report.js";
|
|
5
|
-
import { listRunIds, runsDir } from "./workspace.js";
|
|
5
|
+
import { PLAN_FILE, listPlanIds, listRunIds, plansDir, runsDir } from "./workspace.js";
|
|
6
6
|
|
|
7
7
|
const PROBLEMS = new Set(["broken", "risk", "warning"]);
|
|
8
8
|
const PLAN_PROBLEMS = new Set(["missing", "missing_params", "wrong_values"]);
|
|
@@ -143,3 +143,40 @@ export function changesLines(ch, lang = "en") {
|
|
|
143
143
|
...(ch.routes?.length ? [m.ui.routeChanged(ch.routes.map((r) => `${r.journeyId} (${r.viewport})`).join(", "))] : []),
|
|
144
144
|
];
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
const planKey = (e) => [e.platform ?? "ga4", e.name].join("|");
|
|
148
|
+
const sortedList = (v) => JSON.stringify([...(v ?? [])].sort());
|
|
149
|
+
|
|
150
|
+
/** What differs for an event kept in both plans: its required parameters, parameter list or journeys. */
|
|
151
|
+
function eventChanges(a, b) {
|
|
152
|
+
const out = [];
|
|
153
|
+
if (sortedList(a.required_params) !== sortedList(b.required_params)) out.push("required parameters");
|
|
154
|
+
if (sortedList(Object.keys(a.parameters ?? {})) !== sortedList(Object.keys(b.parameters ?? {}))) out.push("parameters");
|
|
155
|
+
if (sortedList(a.journeys) !== sortedList(b.journeys)) out.push("journeys");
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How a plan differs from the version before it: events added, removed and changed.
|
|
161
|
+
* Returns null when there is no earlier version, else { previous, added, removed, changed, same }.
|
|
162
|
+
*/
|
|
163
|
+
export function comparePlans(prevPlan, plan, previousId = null) {
|
|
164
|
+
if (!prevPlan || !plan) return null;
|
|
165
|
+
const before = new Map((prevPlan.events ?? []).map((e) => [planKey(e), e]));
|
|
166
|
+
const now = new Map((plan.events ?? []).map((e) => [planKey(e), e]));
|
|
167
|
+
const label = (e) => ({ platform: e.platform ?? "ga4", event: e.name });
|
|
168
|
+
const added = [...now].filter(([k]) => !before.has(k)).map(([, e]) => label(e));
|
|
169
|
+
const removed = [...before].filter(([k]) => !now.has(k)).map(([, e]) => label(e));
|
|
170
|
+
const changed = [...now].filter(([k]) => before.has(k)).map(([k, e]) => ({ ...label(e), what: eventChanges(before.get(k), e) })).filter((c) => c.what.length);
|
|
171
|
+
return { previous: previousId, added, removed, changed, same: !added.length && !removed.length && !changed.length };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The saved plan right before `planId` for the site, compared with `plan`; null for the first version or an unsaved plan. */
|
|
175
|
+
export async function previousPlanChanges(dataDir, site, planId, plan) {
|
|
176
|
+
if (!planId) return null;
|
|
177
|
+
const ids = await listPlanIds(dataDir, site);
|
|
178
|
+
const prevId = ids.filter((id) => id < planId).at(-1);
|
|
179
|
+
if (!prevId) return null;
|
|
180
|
+
const prevPlan = await readJson(path.join(plansDir(dataDir, site), prevId, PLAN_FILE));
|
|
181
|
+
return comparePlans(prevPlan, plan, prevId);
|
|
182
|
+
}
|