@analitikisler/ai-tag-debugger 0.22.4 → 0.22.6
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 +16 -0
- package/README.md +26 -3
- package/README.tr.md +26 -3
- package/package.json +1 -1
- package/src/agent.js +7 -2
- package/src/audit.js +4 -1
- package/src/browser.js +84 -17
- package/src/checks.js +18 -1
- package/src/cli.js +48 -0
- package/src/consent.js +1 -1
- package/src/containers.js +60 -3
- package/src/help.js +20 -1
- package/src/html/scope.js +1 -1
- package/src/i18n.js +30 -0
- package/src/journeys.js +8 -1
- package/src/login.js +91 -0
- package/src/mcp.js +29 -9
- package/src/parsers.js +2 -0
- package/src/report.js +4 -4
- package/src/schema.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.6 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
- Journeys can log in. Set a test account in `AITD_LOGIN_EMAIL` (or `AITD_LOGIN_USERNAME`) and `AITD_LOGIN_PASSWORD`: the model fills login forms with the placeholders `{{login.email}}`, `{{login.username}}` and `{{login.password}}`, and the browser types the real values, only into the audited site's pages, never into a frame from another site. The values are turned back into their placeholders in everything the tool keeps or shows (page views, steps, hits, dataLayer pushes, network calls, saved steps and reports), a password field's value is never read into the page view, and the browser doesn't get them in its environment. Over MCP, `start_audit` says whether a test account is set up.
|
|
6
|
+
- New ready-made journey `login` (or `signin`): logs in with the test account and expects GA4 `login`. A plan with a `login` event uses it with `--against`.
|
|
7
|
+
- New command `aitd login --site <url>`: log in by hand once in a browser window (a CAPTCHA, a phone code or single sign-on are fine) and the session is saved to `aitd/<domain>/session.json`, readable only by you. `aitd run --session saved` (or a file), the config's `session_file`, and `session: "saved"` on a local MCP server's `start_audit` start every journey logged in. An HTTP server never uses it.
|
|
8
|
+
- New check `user_id_missing` (info): after logging in with the test account, GA4 hits went out without the site's user ID (`uid`), so GA4 can't tie the visit to the account. A baseline written with 0.22.5 can then list new entries; `--update-baseline` rewrites it.
|
|
9
|
+
- ai-tag-debugger is a Claude plugin: `/plugin install ai-tag-debugger --marketplace analitikisler/ai-tag-debugger` in Claude Code adds the MCP server and an `audit-tracking` skill that walks Claude through an audit: what to ask first, which tool answers which question, how to walk the journey and how to explain the findings. The plugin lives in `plugin/`, pins the npm version it starts, and is ready for Anthropic's plugin directory. Its README lists everything it runs, fetches and writes.
|
|
10
|
+
|
|
11
|
+
## 0.22.5 (2026-11-04)
|
|
12
|
+
|
|
13
|
+
- Runs can report findings 0.22.4 did not on the same site (`container_not_run`, `container_held`). A baseline written with 0.22.4 can then list new entries; `--update-baseline` rewrites it.
|
|
14
|
+
- After every step the tool reads Google's own registry of the containers that ran (`google_tag_manager`), so the Google containers card, `report.md` and MCP say when a container never ran, however its script got into the page.
|
|
15
|
+
- New check `container_not_run` (warning): a GTM container was downloaded or asked for but never ran on any page of the run, so none of its tags fired. When a consent platform held it back and the run never consented, `container_held` (info) says so instead.
|
|
16
|
+
- Fixed: a container the page only preloaded (`<link rel="preload">`, as Next.js does before its own code adds the script) or that ran without the tool seeing it added (as when a consent platform's auto-blocking replaces the browser's own functions) was missing from the containers card. It now has a row saying which.
|
|
17
|
+
- Found on a live Next.js site: GTM was only preloaded and never ran, because the site's own scripts were served as HTML and its app crashed on start. `page_crashed` reported the crash; the containers card now also shows that GTM never ran.
|
|
18
|
+
|
|
3
19
|
## 0.22.4 (2026-11-04)
|
|
4
20
|
|
|
5
21
|
- Runs can report a finding 0.22.3 did not on the same site (`iframe_message_unhandled`). A baseline written with 0.22.3 can then list new entries; `--update-baseline` rewrites it.
|
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Manual tag QA catches this, but it's tedious, so it rarely happens after every r
|
|
|
53
53
|
| Click id and client id cookies | A Google Ads tag ran with ad storage granted, but `_gcl_aw` never held the landing page's `gclid`, so conversions can't be tied to the click; or the GA4 client id in `_ga` changed mid-visit, which splits one visitor into two users |
|
|
54
54
|
| Pages that aren't the site | A bot challenge (Cloudflare, DataDome, HUMAN, Akamai) shown instead of the page, or a page that crashed in the browser, such as a Next.js "Application error" after its own scripts came back as HTML. Events not seen on such a run are unverified rather than missing |
|
|
55
55
|
| 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 |
|
|
56
|
+
| Logged-in tracking | After logging in with the test account, GA4 hits went out without the site's user ID (`uid`), so GA4 can't tie the visit to the account |
|
|
56
57
|
|
|
57
58
|
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.
|
|
58
59
|
|
|
@@ -327,7 +328,7 @@ Under the inventory, **Google containers** lists every GTM container (`gtm.js`)
|
|
|
327
328
|
|
|
328
329
|
Each row also gives the container's published version (the number its `gtm.js` or `gtag/js` data opens with, as in GTM's Versions tab), the script's size and how long it took to load, the dataLayer the container reads (`l=`), whether it came from Google or from the site's own domain (the Google tag gateway or a server container), the GTM environment when the snippet names one, and the pages it loaded on. A container reading a dataLayer of another name has its pushes recorded too, marked with that name. `report.md` has the same table. Over MCP, step results and `get_step` list the containers each step loaded, `get_containers` lists them all while the audit is open, `finish_audit` and `run_audit` end with them, `get_report` returns them with `part: "containers"` and the JSON timeline carries them per step. `capture.json` keeps the loads under `containerLoads`. The environment token (`gtm_auth`) and the page's query string are never kept.
|
|
329
330
|
|
|
330
|
-
[The guide](docs/guides/containers-and-widgets.md) explains each finding below with what to do.
|
|
331
|
+
[The guide](docs/guides/containers-and-widgets.md) explains each finding below with what to do. After every step the tool also reads Google's own registry of the containers that ran (`google_tag_manager`), so a container that downloaded but never ran is reported, and a container the page only preloaded (`<link rel="preload">`), or that ran without the tool seeing it added, still gets a row. Seven checks read it: `container_not_run` (warning) for a GTM container that was downloaded or asked for but never ran, so none of its tags fired, or `container_held` (info) instead when a consent platform held it back and the run never consented; `container_failed` (warning) for a container script that failed to load or was answered with an error, so none of its tags ran, `container_duplicate` (warning) for a container loaded more than once into one page, `container_environment` (warning) for a GTM environment (`gtm_preview`, `gtm_auth`) loaded instead of Live, `datalayer_split` (info) for containers on one page reading different dataLayers, and `container_send_to_only` (info) for a destination that only an event's `send_to` loaded, with no `gtag('config')` for it.
|
|
331
332
|
|
|
332
333
|
### Embedded widgets and iframes
|
|
333
334
|
|
|
@@ -401,9 +402,25 @@ ai-tag-debugger run --config ai-tag-debugger.config.json --events purchase,add_t
|
|
|
401
402
|
| `sign_up` (or `signup`, `register`) | Creates an account with test data | GA4 `sign_up`; Meta `CompleteRegistration` |
|
|
402
403
|
| `lead` (or `contact`, `form`) | Fills in and sends a contact, quote or demo form | GA4 `generate_lead`; Meta `Lead` |
|
|
403
404
|
| `search` | Searches the site and opens a result | GA4 `search` with `search_term`; Meta `Search` |
|
|
405
|
+
| `login` (or `signin`) | Logs in with the [test account](#logging-in) | GA4 `login` |
|
|
404
406
|
|
|
405
407
|
`--journey` also picks journeys from your config by id. With a config, ready-made journeys add their events to your plan.
|
|
406
408
|
|
|
409
|
+
### Logging in
|
|
410
|
+
|
|
411
|
+
Journeys that need an account can log in two ways.
|
|
412
|
+
|
|
413
|
+
**A test account.** Set its email (or `AITD_LOGIN_USERNAME` where the site asks for a username) and password on the computer running the tool:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
export AITD_LOGIN_EMAIL=qa-test@yourshop.com
|
|
417
|
+
export AITD_LOGIN_PASSWORD='…'
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The model never sees them. It fills login forms with the placeholders `{{login.email}}`, `{{login.username}}` and `{{login.password}}`, and the browser types the real values, only into pages on the audited site (its subdomains and `allowed_hosts`), never into a frame from another site. Everything the tool keeps or shows has them turned back into their placeholders: the page views the model reads, the steps, hits, dataLayer pushes, network calls, saved steps and reports. The browser itself doesn't get them in its environment. The `login` journey uses the account, and any journey or MCP audit can. After a login, `user_id_missing` (info) reports GA4 hits that carry no user ID. Use an account made for testing, never a real customer's.
|
|
421
|
+
|
|
422
|
+
**A saved session**, for a login that needs a person: a CAPTCHA, a code sent to a phone, or single sign-on. `aitd login --site https://yourshop.com` opens the site in a browser window; log in by hand, press Enter in the terminal, and the session (cookies and storage) is saved to `aitd/<domain>/session.json`, readable only by you. `aitd run … --session saved`, the config's `session_file`, or `session: "saved"` on a local MCP server's `start_audit` start every journey logged in. The file holds a live login: keep it private, and save it again when the login expires.
|
|
423
|
+
|
|
407
424
|
Not sure what to test? `aitd plan --site https://yoursite.com` drafts a measurement plan, and `--against latest` checks the site against it.
|
|
408
425
|
|
|
409
426
|
### Keeping API usage down
|
|
@@ -497,6 +514,7 @@ The file holds one entry per finding, with a `key` the run matches on (the check
|
|
|
497
514
|
| `journeys[].start` | Optional start path (default `/`). |
|
|
498
515
|
| `journeys[].steps` | Optional fixed steps for `--scripted` runs: `{ "action": "click", "selector": "#add-to-cart" }`. Without them, `--scripted` repeats the steps of the site's last completed run of the journey. |
|
|
499
516
|
| `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`. |
|
|
517
|
+
| `session_file` | A session saved with `aitd login`, so every journey starts logged in: `"saved"` for the site's own, or a file path. See [Logging in](#logging-in). |
|
|
500
518
|
| `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`. |
|
|
501
519
|
| `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. |
|
|
502
520
|
| `block_hits` | Default `true`. Set to `false` to let hits reach your real endpoints. |
|
|
@@ -551,10 +569,11 @@ aitd run --site <url> (--against <plan> | --journey <id> | --goal "<text>") [--c
|
|
|
551
569
|
[--max-steps <n>] [--viewport desktop|mobile] [--concurrency <n>] [--platforms <list>] [--data-dir <dir>]
|
|
552
570
|
[--provider anthropic|openai|gemini|openai-compatible] [--model <id>] [--lang en|tr] [--headed]
|
|
553
571
|
[--fail-on broken|risk|warning|never] [--baseline <file>] [--update-baseline]
|
|
554
|
-
[--scripted] [--no-ai] [--save-site <dir>] [--save-steps <file>]
|
|
572
|
+
[--scripted] [--no-ai] [--save-site <dir>] [--save-steps <file>] [--session saved|<file>]
|
|
555
573
|
aitd replay <dir>... [--update] [--out <dir>]
|
|
556
574
|
aitd mcp [--http] [--port <n>] [--host <addr>] [--token <secret>] [--public-url <https url>] [--reports <dir>]
|
|
557
575
|
[--allow-private-network] [--allow-live]
|
|
576
|
+
aitd login --site <url> [--save <file>] [--allow-host <host>] [--data-dir <dir>]
|
|
558
577
|
aitd doctor [--site <url>] [--data-dir <dir>]
|
|
559
578
|
aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir>]
|
|
560
579
|
```
|
|
@@ -593,6 +612,8 @@ aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir
|
|
|
593
612
|
| `--out <dir>` | replay | Where to write the replay reports. For `run`, `--out` still works as a hidden alias for one minor version and prints a note; use `--data-dir`. |
|
|
594
613
|
| `--base-url <url>` | run | The API address for an OpenAI-compatible provider. |
|
|
595
614
|
| `--allow-host <host>` | run | Another host journeys may visit (repeatable). Same as `allowed_hosts`. |
|
|
615
|
+
| `--session saved\|<file>` | run | Start every journey logged in, from the session `aitd login` saved (`saved`) or a session file. Same as `session_file`. See [Logging in](#logging-in). |
|
|
616
|
+
| `--save <file>` | login | Where `aitd login` saves the session (default `aitd/<domain>/session.json`). |
|
|
596
617
|
| `--keep-bodies` | run | Store tracking requests as sent, without redacting user data. |
|
|
597
618
|
| `--no-branding` | plan, run | Leave the logo and credit line out of the reports and `plan.html`. |
|
|
598
619
|
| `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network`, `--allow-live` | mcp | See the MCP section below. |
|
|
@@ -662,7 +683,7 @@ Tools the server offers:
|
|
|
662
683
|
|---|---|
|
|
663
684
|
| `make_plan` | Drafts a tracking plan for a site (what should be tracked), the same as `aitd plan --site`. Pass `topic` (`custom-events`, `ecommerce`, `forms`, `consent`) to limit it to one kind of event. The plan is saved and its ID can be given as `against` to `finish_audit`. It uses the AI provider configured on the computer that runs the server and takes a few minutes. |
|
|
664
685
|
| `what_changed` | Returns only the "since last run" diff for a site: the problems fixed and the new ones in its newest saved run (`aitd run`) compared with the run before. No browser and no AI. Pass `language` for a Turkish answer. |
|
|
665
|
-
| `start_audit` | Opens a fresh browser on a URL and starts recording all tracking. Pass a ready-made journey (`purchase`, `sign_up`, …) to get its goal and expected events, and `providers` to choose the platforms to check. The assistant asks you which platforms to check first. `live: true` lets the tracking requests through to the site's real accounts, to check them in GA4 Realtime or DebugView, Meta Events Manager or Tag Assistant; the test traffic then shows in the site's reports, and the results and the report say the run was live. Over HTTP the server needs `--allow-live` for it. The first page opens the way an ad click does, with test campaign parameters (`utm_source=aitd`, `gclid=aitd-gclid`, `fbclid=aitd-fbclid`, …), so `finish_audit` checks that they survive redirects and reach the hits; `campaign: false` opens the URL as given, and `campaign: {"utm_source": "newsletter", "gclid": "test123"}` uses your own values. |
|
|
686
|
+
| `start_audit` | Opens a fresh browser on a URL and starts recording all tracking. Pass a ready-made journey (`purchase`, `sign_up`, …) to get its goal and expected events, and `providers` to choose the platforms to check. The assistant asks you which platforms to check first. `live: true` lets the tracking requests through to the site's real accounts, to check them in GA4 Realtime or DebugView, Meta Events Manager or Tag Assistant; the test traffic then shows in the site's reports, and the results and the report say the run was live. Over HTTP the server needs `--allow-live` for it. The first page opens the way an ad click does, with test campaign parameters (`utm_source=aitd`, `gclid=aitd-gclid`, `fbclid=aitd-fbclid`, …), so `finish_audit` checks that they survive redirects and reach the hits; `campaign: false` opens the URL as given, and `campaign: {"utm_source": "newsletter", "gclid": "test123"}` uses your own values. A test account set on the server's computer is used through its placeholders, and `session: "saved"` starts from the session `aitd login` saved (local servers only); see [Logging in](#logging-in). |
|
|
666
687
|
| `browser_action` | One click, fill, select, goto, scroll or wait. Returns the step number, the page and the tracking hits that step sent, each with an `[h]` number, its consent (from `gcs`, and `gcd` for `ad_user_data` and `ad_personalization`) and the ids and flags GA4 sends with it (`cid`, `sid`, `gcs`, `gcd`, `_fv`, `_ss`, `_nsi`, …), its dataLayer pushes (Consent Mode commands in full as `key=value`, page addresses never cut) and the calls it read no event from, such as Consent Mode pings with their `gcs`, `gcd` and click ids (`gclid`, `gclid=0 (redacted)` when ads data redaction blanks it, `gclaw` from `_gcl_aw`), and the cookies the step added, changed or removed, by name, with the GA4 client id and whether `_gcl_aw` and `_fbc` hold the landing click ids when that changes. A page_view that arrives during a later wait or scroll is listed on the step that loaded its page, marked late. The page view is compact by default, since long sessions lose their oldest results: up to 40 elements above or below the screen, and the page text or element list replaced by a note when they haven't changed since the last view. Pass `detail: "full"` for everything. |
|
|
667
688
|
| `get_page` | Shows the current page, in full: every element and the whole page text. |
|
|
668
689
|
| `get_hit` | Shows one hit in full, by its `step` and `[h]` number: every parameter its request carried, its place in a batched request, where it was sent, and `gcd` decoded per consent signal (default and update). |
|
|
@@ -697,6 +718,7 @@ The browser (Chromium) downloads itself the first time you run an audit, about a
|
|
|
697
718
|
|
|
698
719
|
One-click and one-line setup:
|
|
699
720
|
|
|
721
|
+
- **Claude Code, as a plugin:** `/plugin install ai-tag-debugger --marketplace analitikisler/ai-tag-debugger` adds the server and a skill that walks Claude through an audit; [plugin/README.md](plugin/README.md) lists everything it runs, fetches and writes. The plugin starts the exact version it was released with.
|
|
700
722
|
- **Claude Code:** `claude mcp add ai-tag-debugger -- npx -y @analitikisler/ai-tag-debugger mcp`
|
|
701
723
|
- **Cursor:** [Add to Cursor](https://cursor.com/en/install-mcp?name=ai-tag-debugger&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBhbmFsaXRpa2lzbGVyL2FpLXRhZy1kZWJ1Z2dlciIsIm1jcCJdfQ==)
|
|
702
724
|
- **VS Code:** [Install in VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=ai-tag-debugger&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40analitikisler%2Fai-tag-debugger%22%2C%22mcp%22%5D%7D), or `code --add-mcp '{"name":"ai-tag-debugger","command":"npx","args":["-y","@analitikisler/ai-tag-debugger","mcp"]}'`
|
|
@@ -733,6 +755,7 @@ A hosted server, so you don't need a tunnel, is part of the [Analitik İşler](h
|
|
|
733
755
|
- The browser stays on the audited site and its subdomains, plus any `allowed_hosts`. It opens only `http` and `https` pages.
|
|
734
756
|
- The model is told that page content is data, never instructions. A prompt rule can't guarantee a model is never misled, so the navigation limits above contain what a misled model can do.
|
|
735
757
|
- The browser runs without your API keys and other secrets in its environment.
|
|
758
|
+
- A [test account](#logging-in) is typed only into the audited site's pages, never shown to the model, and turned back into its placeholders in everything the tool keeps. A saved login session is readable only by you and is never used by an HTTP server.
|
|
736
759
|
- **User data sent for ad matching is redacted** before it is stored in `capture.json` or shown in the report: emails, phone numbers, names, addresses and user ids in GA4 and Google Ads enhanced conversions (`em`, `ph`, `uid`, …), Meta advanced matching (`ud[*]`, `udff[*]`), TikTok `context.user`, and the `user_data` the site pushes to the dataLayer for enhanced conversions. Any value that looks like an email address is redacted whatever its field is called, except the audited site's own address in a `mailto:` link or a link field (`link_url`, `link_text`, `gtm.elementUrl`, …), such as a contact link in a GTM click event, which is not visitor data. Each value becomes a marker such as `[redacted: 64 chars, looks hashed]`, and a value that wasn't hashed is reported as a personal data risk. `--keep-bodies` (`"keep_bodies": true`) stores requests as sent, for debugging.
|
|
737
760
|
- Stored request bodies are capped at 32 KB per request and 2 MB per journey. Past that, the report says the body wasn't stored.
|
|
738
761
|
- Only run it against sites you own or are authorized to test.
|
package/README.tr.md
CHANGED
|
@@ -53,6 +53,7 @@ Manuel etiket kontrolü bunu yakalar, ama zahmetlidir; bu yüzden her sürümden
|
|
|
53
53
|
| Tıklama kimliği ve istemci kimliği çerezleri | Bir Google Ads etiketi reklam depolamasına izin verilmişken çalıştı ama `_gcl_aw` açılış sayfasının `gclid`'ini hiç tutmadı, bu yüzden dönüşümler tıklamaya bağlanamıyor; ya da `_ga`'daki GA4 istemci kimliği ziyaret ortasında değişti ve bir ziyaretçi iki kullanıcıya bölündü |
|
|
54
54
|
| Site olmayan sayfalar | Sayfanın yerine gösterilen bir bot doğrulaması (Cloudflare, DataDome, HUMAN, Akamai) ya da tarayıcıda çöken bir sayfa, ör. kendi betikleri HTML olarak geldikten sonra bir Next.js "Application error" ekranı. Böyle bir çalıştırmada görülmeyen event'ler eksik değil, doğrulanmamış sayılır |
|
|
55
55
|
| 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 |
|
|
56
|
+
| Giriş yapılmış takip | Test hesabıyla giriş yapıldıktan sonra GA4 istekleri sitenin kullanıcı kimliği (`uid`) olmadan gönderildi; GA4 ziyareti hesaba bağlayamıyor |
|
|
56
57
|
|
|
57
58
|
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.
|
|
58
59
|
|
|
@@ -327,7 +328,7 @@ Envanterin altında **Google container'ları**, sayfaların yüklediği her GTM
|
|
|
327
328
|
|
|
328
329
|
Her satır ayrıca container'ın yayımlanmış sürümünü (`gtm.js` ya da `gtag/js` verisinin başladığı numara; GTM'deki Sürümler sekmesindekiyle aynı), betiğin boyutunu ve ne kadar sürede yüklendiğini, container'ın okuduğu dataLayer'ı (`l=`), Google'dan mı yoksa sitenin kendi alan adından mı (Google etiket ağ geçidi ya da bir sunucu container'ı) geldiğini, snippet bir GTM ortamı adlandırıyorsa onu ve yüklendiği sayfaları verir. Başka adlı bir dataLayer okuyan container'ın push'ları da o adla işaretlenerek kaydedilir. `report.md` aynı tabloyu içerir. MCP'de adım sonuçları ve `get_step` her adımın yüklediği container'ları listeler, `get_containers` denetim açıkken hepsini listeler, `finish_audit` ve `run_audit` sonuçları onlarla biter, `get_report` onları `part: "containers"` ile döndürür ve JSON zaman akışı onları adım adım taşır. `capture.json` yüklemeleri `containerLoads` altında saklar. Ortam belirteci (`gtm_auth`) ve sayfanın sorgu dizesi asla saklanmaz.
|
|
329
330
|
|
|
330
|
-
[Rehber](docs/guides/containers-and-widgets.tr.md) aşağıdaki her bulguyu ne yapılacağıyla anlatır.
|
|
331
|
+
[Rehber](docs/guides/containers-and-widgets.tr.md) aşağıdaki her bulguyu ne yapılacağıyla anlatır. Araç her adımdan sonra Google'ın çalışan container'lar kaydını (`google_tag_manager`) da okur; böylece indirilip hiç çalışmayan bir container bildirilir, sayfanın yalnızca ön yüklediği (`<link rel="preload">`) ya da eklendiği görülmeden çalışan bir container da yine bir satır alır. Yedi kontrol bunu okur: indirilen ya da istenen ama hiç çalışmayan, bu yüzden etiketlerinin hiçbiri çalışmayan bir GTM container'ı için `container_not_run` (uyarı), bir onay platformu onu beklettiyse ve çalıştırmada onay verilmediyse bunun yerine `container_held` (bilgi); yüklenemeyen ya da hatayla yanıtlanan, bu yüzden etiketlerinin hiçbiri çalışmayan bir container betiği için `container_failed` (uyarı), aynı sayfaya birden fazla kez yüklenen bir container için `container_duplicate` (uyarı), Live yerine yüklenen bir GTM ortamı (`gtm_preview`, `gtm_auth`) için `container_environment` (uyarı), aynı sayfada farklı dataLayer'ları okuyan container'lar için `datalayer_split` (bilgi) ve kendisi için `gtag('config')` olmadan yalnızca bir olayın `send_to` değeriyle yüklenen bir hedef için `container_send_to_only` (bilgi).
|
|
331
332
|
|
|
332
333
|
### Gömülü widget'lar ve iframe'ler
|
|
333
334
|
|
|
@@ -401,9 +402,25 @@ ai-tag-debugger run --config ai-tag-debugger.config.json --events purchase,add_t
|
|
|
401
402
|
| `sign_up` (ya da `signup`, `register`) | Test verileriyle bir hesap oluşturur | GA4 `sign_up`; Meta `CompleteRegistration` |
|
|
402
403
|
| `lead` (ya da `contact`, `form`) | Bir iletişim, teklif ya da demo formunu doldurup gönderir | GA4 `generate_lead`; Meta `Lead` |
|
|
403
404
|
| `search` | Sitede arama yapar ve bir sonucu açar | GA4 `search` (`search_term` ile); Meta `Search` |
|
|
405
|
+
| `login` (ya da `signin`) | [Test hesabıyla](#giriş-yapmak) giriş yapar | GA4 `login` |
|
|
404
406
|
|
|
405
407
|
`--journey`, ayar dosyanızdaki yolculukları id'leriyle de seçer. Bir ayar dosyasıyla, hazır yolculuklar kendi event'lerini planınıza ekler.
|
|
406
408
|
|
|
409
|
+
### Giriş yapmak
|
|
410
|
+
|
|
411
|
+
Hesap gerektiren yolculuklar iki şekilde giriş yapabilir.
|
|
412
|
+
|
|
413
|
+
**Bir test hesabı.** E-postasını (site bir kullanıcı adı istiyorsa `AITD_LOGIN_USERNAME`) ve parolasını aracı çalıştıran bilgisayarda ayarlayın:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
export AITD_LOGIN_EMAIL=qa-test@magazaniz.com
|
|
417
|
+
export AITD_LOGIN_PASSWORD='…'
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Model bunları hiç görmez. Giriş formlarını `{{login.email}}`, `{{login.username}}` ve `{{login.password}}` yer tutucularıyla doldurur; gerçek değerleri tarayıcı yazar, yalnızca denetlenen sitenin sayfalarına (alt alan adları ve `allowed_hosts`), başka bir siteden gelen bir çerçeveye asla. Aracın sakladığı ya da gösterdiği her şeyde bu değerler yeniden yer tutuculara çevrilir: modelin okuduğu sayfa görünümleri, adımlar, istekler, dataLayer push'ları, ağ çağrıları, kaydedilen adımlar ve raporlar. Tarayıcının kendisi de bunları ortamında almaz. `login` yolculuğu hesabı kullanır; her yolculuk ve MCP denetimi de kullanabilir. Bir girişten sonra `user_id_missing` (bilgi), kullanıcı kimliği taşımayan GA4 isteklerini bildirir. Test için açılmış bir hesap kullanın, asla gerçek bir müşterinin hesabını değil.
|
|
421
|
+
|
|
422
|
+
**Kaydedilmiş bir oturum**, bir insan gerektiren girişler için: bir CAPTCHA, telefona gelen bir kod ya da tek oturum açma (SSO). `aitd login --site https://magazaniz.com` siteyi bir tarayıcı penceresinde açar; elle giriş yapın, terminalde Enter'a basın; oturum (çerezler ve depolama) yalnızca sizin okuyabileceğiniz `aitd/<alan-adı>/session.json` dosyasına kaydedilir. `aitd run … --session saved`, yapılandırmadaki `session_file` ya da yerel bir MCP sunucusunda `start_audit` için `session: "saved"` her yolculuğu giriş yapılmış olarak başlatır. Dosya canlı bir oturum tutar: gizli tutun ve oturum sona erdiğinde yeniden kaydedin.
|
|
423
|
+
|
|
407
424
|
Ne test edeceğinizden emin değil misiniz? `aitd plan --site https://yoursite.com` bir ölçümleme planı taslağı çıkarır, `--against latest` de siteyi buna göre kontrol eder.
|
|
408
425
|
|
|
409
426
|
### API kullanımını düşük tutmak
|
|
@@ -497,6 +514,7 @@ Dosyada her bulgu için bir kayıt bulunur: çalıştırmanın eşleştirme yapt
|
|
|
497
514
|
| `journeys[].start` | İsteğe bağlı başlangıç yolu (varsayılan `/`). |
|
|
498
515
|
| `journeys[].steps` | `--scripted` çalıştırmaları için isteğe bağlı sabit adımlar: `{ "action": "click", "selector": "#add-to-cart" }`. Bunlar yoksa `--scripted`, sitenin o yolculuğu son tamamlayan çalıştırmasının adımlarını tekrarlar. |
|
|
499
516
|
| `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. |
|
|
517
|
+
| `session_file` | `aitd login` ile kaydedilmiş bir oturum; her yolculuk giriş yapılmış olarak başlar: sitenin kendi oturumu için `"saved"` ya da bir dosya yolu. Bkz. [Giriş yapmak](#giriş-yapmak). |
|
|
500
518
|
| `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. |
|
|
501
519
|
| `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. |
|
|
502
520
|
| `block_hits` | Varsayılan `true`. Takip isteklerinin gerçek uç noktalarınıza ulaşmasına izin vermek için `false` yapın. |
|
|
@@ -551,10 +569,11 @@ aitd run --site <url> (--against <plan> | --journey <id> | --goal "<metin>") [--
|
|
|
551
569
|
[--max-steps <n>] [--viewport desktop|mobile] [--concurrency <n>] [--platforms <liste>] [--data-dir <dir>]
|
|
552
570
|
[--provider anthropic|openai|gemini|openai-compatible] [--model <id>] [--lang en|tr] [--headed]
|
|
553
571
|
[--fail-on broken|risk|warning|never] [--baseline <dosya>] [--update-baseline]
|
|
554
|
-
[--scripted] [--no-ai] [--save-site <dir>] [--save-steps <dosya>]
|
|
572
|
+
[--scripted] [--no-ai] [--save-site <dir>] [--save-steps <dosya>] [--session saved|<dosya>]
|
|
555
573
|
aitd replay <dir>... [--update] [--out <dir>]
|
|
556
574
|
aitd mcp [--http] [--port <n>] [--host <adres>] [--token <gizli>] [--public-url <https url>] [--reports <dir>]
|
|
557
575
|
[--allow-private-network] [--allow-live]
|
|
576
|
+
aitd login --site <url> [--save <file>] [--allow-host <host>] [--data-dir <dir>]
|
|
558
577
|
aitd doctor [--site <url>] [--data-dir <dir>]
|
|
559
578
|
aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir>]
|
|
560
579
|
```
|
|
@@ -593,6 +612,8 @@ aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir
|
|
|
593
612
|
| `--out <dir>` | replay | Tekrar raporlarının yazılacağı yer. `run` için `--out` gizli bir takma ad olarak bir minör sürüm daha çalışır ve bir not yazdırır; `--data-dir` kullanın. |
|
|
594
613
|
| `--base-url <url>` | run | OpenAI uyumlu bir sağlayıcının API adresi. |
|
|
595
614
|
| `--allow-host <ana makine>` | run | Yolculukların ziyaret edebileceği başka bir ana makine (tekrarlanabilir). `allowed_hosts` ile aynıdır. |
|
|
615
|
+
| `--session saved\|<dosya>` | run | Her yolculuğu, `aitd login`'in kaydettiği oturumdan (`saved`) ya da bir oturum dosyasından giriş yapılmış olarak başlatır. `session_file` ile aynı. Bkz. [Giriş yapmak](#giriş-yapmak). |
|
|
616
|
+
| `--save <dosya>` | login | `aitd login`'in oturumu nereye kaydedeceği (varsayılan `aitd/<alan-adı>/session.json`). |
|
|
596
617
|
| `--keep-bodies` | run | Takip isteklerini gönderildiği gibi, kullanıcı verisi gizlenmeden sakla. |
|
|
597
618
|
| `--no-branding` | plan, run | Logo ve kredi satırını raporlardan ve `plan.html` dosyasından çıkar. |
|
|
598
619
|
| `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network`, `--allow-live` | mcp | Aşağıdaki MCP bölümüne bakın. |
|
|
@@ -662,7 +683,7 @@ Sunucunun sunduğu araçlar:
|
|
|
662
683
|
|---|---|
|
|
663
684
|
| `make_plan` | Bir site için takip planı çıkarır (neyin takip edilmesi gerektiği), `aitd plan --site` ile aynıdır. Yalnızca bir tür event için sınırlamak üzere `topic` verin (`custom-events`, `ecommerce`, `forms`, `consent`). Plan kaydedilir ve kimliği `finish_audit` için `against` olarak verilebilir. Sunucuyu çalıştıran bilgisayarda ayarlı yapay zekâ sağlayıcısını kullanır ve birkaç dakika sürer. |
|
|
664
685
|
| `what_changed` | Bir site için yalnızca "son çalıştırmadan bu yana" farkını döndürür: kayıtlı son çalıştırmanın (`aitd run`) bir öncekine göre düzelen ve yeni sorunları. Tarayıcı ve yapay zekâ kullanmaz. Türkçe yanıt için `language` verin. |
|
|
665
|
-
| `start_audit` | Bir URL'de yeni bir tarayıcı açar ve tüm takibi kaydetmeye başlar. Hedefini ve beklenen event'lerini almak için hazır bir yolculuk (`purchase`, `sign_up`, …) verin; kontrol edilecek platformları seçmek için `providers` verin. Asistan önce hangi platformların kontrol edileceğini size sorar. `live: true`, takip isteklerini sitenin gerçek hesaplarına geçirir; böylece GA4 Realtime ya da DebugView, Meta Events Manager veya Tag Assistant'ta kontrol edilebilirler. Test trafiği o zaman sitenin raporlarında görünür; sonuçlar ve rapor çalıştırmanın canlı olduğunu söyler. HTTP üzerinden sunucunun bunun için `--allow-live` ile başlatılması gerekir. İlk sayfa bir reklam tıklaması gibi, test kampanya parametreleriyle (`utm_source=aitd`, `gclid=aitd-gclid`, `fbclid=aitd-fbclid`, …) açılır; böylece `finish_audit` bunların yönlendirmelerden sağ çıkıp isteklere ulaştığını kontrol eder. `campaign: false` URL'yi olduğu gibi açar, `campaign: {"utm_source": "newsletter", "gclid": "test123"}` ise sizin değerlerinizi kullanır. |
|
|
686
|
+
| `start_audit` | Bir URL'de yeni bir tarayıcı açar ve tüm takibi kaydetmeye başlar. Hedefini ve beklenen event'lerini almak için hazır bir yolculuk (`purchase`, `sign_up`, …) verin; kontrol edilecek platformları seçmek için `providers` verin. Asistan önce hangi platformların kontrol edileceğini size sorar. `live: true`, takip isteklerini sitenin gerçek hesaplarına geçirir; böylece GA4 Realtime ya da DebugView, Meta Events Manager veya Tag Assistant'ta kontrol edilebilirler. Test trafiği o zaman sitenin raporlarında görünür; sonuçlar ve rapor çalıştırmanın canlı olduğunu söyler. HTTP üzerinden sunucunun bunun için `--allow-live` ile başlatılması gerekir. İlk sayfa bir reklam tıklaması gibi, test kampanya parametreleriyle (`utm_source=aitd`, `gclid=aitd-gclid`, `fbclid=aitd-fbclid`, …) açılır; böylece `finish_audit` bunların yönlendirmelerden sağ çıkıp isteklere ulaştığını kontrol eder. `campaign: false` URL'yi olduğu gibi açar, `campaign: {"utm_source": "newsletter", "gclid": "test123"}` ise sizin değerlerinizi kullanır. Sunucunun bilgisayarında ayarlı bir test hesabı yer tutucularıyla kullanılır; `session: "saved"` ise `aitd login`'in kaydettiği oturumdan başlar (yalnızca yerel sunucular); bkz. [Giriş yapmak](#giriş-yapmak). |
|
|
666
687
|
| `browser_action` | Tek bir tıklama, doldurma, seçme, gitme, kaydırma ya da bekleme. Adım numarasını, sayfayı, o adımın gönderdiği takip isteklerini (her biri bir `[h]` numarası, onay durumu — `gcs`'den, `ad_user_data` ve `ad_personalization` için `gcd`'den — ve GA4'ün onunla gönderdiği kimlik ve işaretlerle: `cid`, `sid`, `gcs`, `gcd`, `_fv`, `_ss`, `_nsi`, …), dataLayer push'larını (Consent Mode komutları `anahtar=değer` olarak eksiksiz, sayfa adresleri hiç kesilmeden) ve event olarak okunmayan çağrıları (ör. `gcs`, `gcd` ve tıklama kimlikleriyle Consent Mode ping'leri: `gclid`, reklam verisi karartma açıksa `gclid=0 (redacted)`, `_gcl_aw`'dan `gclaw`), ayrıca adımın eklediği, değiştirdiği ya da sildiği çerezleri adlarıyla, değiştiğinde de GA4 istemci kimliğini ve `_gcl_aw` ile `_fbc`'nin açılış tıklama kimliklerini tutup tutmadığını döndürür. Sonraki bir bekleme ya da kaydırma sırasında gelen page_view, sayfasını yükleyen adımda gecikmeli olarak işaretlenip gösterilir. Uzun oturumlar en eski sonuçlarını kaybettiği için sayfa görünümü varsayılan olarak kısadır: ekranın üstünde ya da altında en fazla 40 öğe; sayfa metni ya da öğe listesi son görünümden beri değişmediyse yerine bir not. Hepsi için `detail: "full"` verin. |
|
|
667
688
|
| `get_page` | Geçerli sayfayı eksiksiz gösterir: her öğe ve sayfanın tüm metni. |
|
|
668
689
|
| `get_hit` | Bir isteği `step` ve `[h]` numarasıyla eksiksiz gösterir: isteğin taşıdığı her parametre, toplu bir istekteki sırası, nereye gönderildiği ve her onay sinyali için çözümlenmiş `gcd` (varsayılan ve güncelleme). |
|
|
@@ -697,6 +718,7 @@ Tarayıcı (Chromium), bir denetimi ilk kez çalıştırdığınızda kendini in
|
|
|
697
718
|
|
|
698
719
|
Tek tıkla ya da tek satırla kurulum:
|
|
699
720
|
|
|
721
|
+
- **Claude Code, eklenti olarak:** `/plugin install ai-tag-debugger --marketplace analitikisler/ai-tag-debugger` sunucuyu ve Claude'a bir denetimi adım adım anlatan bir beceriyi ekler; [plugin/README.md](plugin/README.md) çalıştırdığı, indirdiği ve yazdığı her şeyi listeler. Eklenti, birlikte yayımlandığı sürümün tam olarak aynısını başlatır.
|
|
700
722
|
- **Claude Code:** `claude mcp add ai-tag-debugger -- npx -y @analitikisler/ai-tag-debugger mcp`
|
|
701
723
|
- **Cursor:** [Cursor'a ekle](https://cursor.com/en/install-mcp?name=ai-tag-debugger&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBhbmFsaXRpa2lzbGVyL2FpLXRhZy1kZWJ1Z2dlciIsIm1jcCJdfQ==)
|
|
702
724
|
- **VS Code:** [VS Code'a yükle](https://insiders.vscode.dev/redirect/mcp/install?name=ai-tag-debugger&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40analitikisler%2Fai-tag-debugger%22%2C%22mcp%22%5D%7D), ya da `code --add-mcp '{"name":"ai-tag-debugger","command":"npx","args":["-y","@analitikisler/ai-tag-debugger","mcp"]}'`
|
|
@@ -733,6 +755,7 @@ Tünel gerektirmeyen barındırılmış bir sunucu, [Analitik İşler](https://a
|
|
|
733
755
|
- Tarayıcı, denetlenen sitede ve alt alan adlarında kalır; ayrıca varsa `allowed_hosts` içinde. Yalnızca `http` ve `https` sayfalarını açar.
|
|
734
756
|
- Modele, sayfa içeriğinin veri olduğu ve talimat olmadığı söylenir. Bir istem kuralı, bir modelin asla yanıltılmayacağını garanti edemez; bu yüzden yukarıdaki gezinme sınırları, yanıltılmış bir modelin yapabileceklerini sınırlar.
|
|
735
757
|
- Tarayıcı, ortamında API anahtarlarınız ve diğer gizli bilgileriniz olmadan çalışır.
|
|
758
|
+
- Bir [test hesabı](#giriş-yapmak) yalnızca denetlenen sitenin sayfalarına yazılır, modele hiç gösterilmez ve aracın sakladığı her şeyde yeniden yer tutucularına çevrilir. Kaydedilmiş bir giriş oturumunu yalnızca siz okuyabilirsiniz ve bir HTTP sunucusu onu hiç kullanmaz.
|
|
736
759
|
- **Reklam eşleştirmesi için gönderilen kullanıcı verisi, `capture.json` dosyasına kaydedilmeden ya da raporda gösterilmeden önce gizlenir**: GA4 ve Google Ads geliştirilmiş dönüşümlerindeki e-postalar, telefonlar, adlar, adresler ve kullanıcı kimlikleri (`em`, `ph`, `uid`, …), Meta gelişmiş eşleştirme (`ud[*]`, `udff[*]`), TikTok `context.user` ve geliştirilmiş dönüşümler için sitenin dataLayer'a gönderdiği `user_data`. E-posta adresine benzeyen her değer, alanı ne olursa olsun gizlenir; yalnızca denetlenen sitenin `mailto:` bağlantısındaki ya da bir bağlantı alanındaki (`link_url`, `link_text`, `gtm.elementUrl`, …) kendi adresi istisnadır; örneğin bir GTM tıklama event'indeki iletişim bağlantısı, ziyaretçi verisi değildir. Her değer `[redacted: 64 chars, looks hashed]` gibi bir işaretçiye dönüşür; karma hâle getirilmemiş bir değer ise kişisel veri riski olarak raporlanır. `--keep-bodies` (`"keep_bodies": true`), istekleri gönderildiği gibi, hata ayıklama için saklar.
|
|
737
760
|
- Saklanan istek gövdeleri istek başına 32 KB, yolculuk başına 2 MB ile sınırlıdır. Bu sınırın üzerinde rapor, gövdenin saklanmadığını söyler.
|
|
738
761
|
- Yalnızca sahibi olduğunuz ya da test etme yetkiniz olan sitelere karşı çalıştırın.
|
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.6",
|
|
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
|
@@ -19,6 +19,7 @@ How to work:
|
|
|
19
19
|
- If a cookie or consent banner appears, accept all cookies (as most visitors do) and set accepts_consent to true on that click. (A journey whose goal says to reject or decline cookies does that instead and sets rejects_consent to true on that click; if the banner has no reject-all button, use the one that declines non-essential cookies, and never accept.)
|
|
20
20
|
- Use obvious test data in forms: name "Test User", email "qa-test@example.com", phone "5550100", address "1 Test Street", city "Testville", postcode "00000".
|
|
21
21
|
- Never enter real payment card details. If the site offers a test or demo payment option, use it. If completing the journey would require a real payment or a real account you don't have, stop and call finish with success false and explain why.
|
|
22
|
+
- To log in with the test account, fill the fields with the placeholders {{login.email}} (or {{login.username}} where the site asks for a username) and {{login.password}}, written exactly like that. The browser types the real values; you never see them, and pages show them back as the placeholders. If the fill says no test account is set up, stop and call finish with success false.
|
|
22
23
|
- If something is broken (an error, a button that does nothing), try a reasonable alternative once, then finish with success false and describe what happened.
|
|
23
24
|
- Call finish as soon as the goal is reached.`;
|
|
24
25
|
|
|
@@ -204,7 +205,7 @@ export const harName = (journeyId, viewport) => `${String(journeyId).replace(/[^
|
|
|
204
205
|
export async function runJourney(config, journey, viewport, opts = {}) {
|
|
205
206
|
const har = (dir) => (dir ? path.join(dir, harName(journey.id, viewport)) : undefined);
|
|
206
207
|
const session = new AuditSession({
|
|
207
|
-
viewport, site: config.site, allowedHosts: config.allowed_hosts, providers: config.providers, detect: config.detect_providers, keepBodies: config.keep_bodies === true,
|
|
208
|
+
viewport, site: config.site, allowedHosts: config.allowed_hosts, storageState: config.session_file, providers: config.providers, detect: config.detect_providers, keepBodies: config.keep_bodies === true,
|
|
208
209
|
blockHits: config.block_hits !== false, headless: !opts.headed, recordHar: har(opts.saveSite), replayHar: har(opts.replaySite), log: opts.log,
|
|
209
210
|
});
|
|
210
211
|
try {
|
|
@@ -238,7 +239,7 @@ export const journeyError = (err) => `The journey stopped with an error: ${err?.
|
|
|
238
239
|
export function failedRun(journey, viewport, err, providers) {
|
|
239
240
|
return {
|
|
240
241
|
journeyId: journey.id, viewport, completed: false, note: journeyError(err), hits: [], steps: [], consentStep: null, rejectStep: null,
|
|
241
|
-
cookieLog: [], cookieChanges: [], dataLayerLog: [], containerLoads: [], network: [], widgetHits: [], widgetDataLayerLog: [], containerScripts: [], frameMessages: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
|
|
242
|
+
cookieLog: [], cookieChanges: [], dataLayerLog: [], containerLoads: [], network: [], widgetHits: [], widgetDataLayerLog: [], containerScripts: [], containersRunning: [], containerPreloads: [], frameMessages: [], loginStep: null, providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
|
|
@@ -252,6 +253,8 @@ export function collect(session, journey, viewport) {
|
|
|
252
253
|
steps: session.steps,
|
|
253
254
|
consentStep: session.consentStep,
|
|
254
255
|
rejectStep: session.rejectStep,
|
|
256
|
+
// The first step that typed the test account's password: the user ID check starts after it.
|
|
257
|
+
loginStep: session.loginStep ?? null,
|
|
255
258
|
cookieLog: session.cookieLog,
|
|
256
259
|
cookieChanges: session.cookieChanges,
|
|
257
260
|
cmp: session.cmp,
|
|
@@ -259,6 +262,8 @@ export function collect(session, journey, viewport) {
|
|
|
259
262
|
...(session.opts.blockHits === false ? { live: true } : {}),
|
|
260
263
|
containerLoads: session.containerLoads,
|
|
261
264
|
containerScripts: session.containerScripts,
|
|
265
|
+
containersRunning: [...session.containersRunning].map(([id, step]) => ({ id, step })),
|
|
266
|
+
containerPreloads: [...session.containerPreloads.values()],
|
|
262
267
|
frameMessages: session.frameMessages,
|
|
263
268
|
network: session.network,
|
|
264
269
|
providers: session.providers,
|
package/src/audit.js
CHANGED
|
@@ -1,6 +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
|
-
import { newRunDir } from "./workspace.js";
|
|
3
|
+
import { DEFAULT_DATA_DIR, newRunDir } from "./workspace.js";
|
|
4
|
+
import { resolveSessionFile } from "./login.js";
|
|
4
5
|
import { compareRuns, previousPlanChanges, previousRun, routeHints } from "./changes.js";
|
|
5
6
|
import { sendAlerts } from "./notify.js";
|
|
6
7
|
import { readFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -133,6 +134,8 @@ export async function runAudit(opts) {
|
|
|
133
134
|
for (const w of checked.flatMap((c) => c.warnings)) log(w);
|
|
134
135
|
}
|
|
135
136
|
if (!config.site) throw new Error('No site to audit. Pass --site or set "site" in the config.');
|
|
137
|
+
// "saved" names the session `aitd login` saved for the site; a path must exist. Checked before any browser starts.
|
|
138
|
+
if (config.session_file) config = { ...config, session_file: resolveSessionFile(config.session_file, { site: config.site, dataDir: opts.dataDir ?? DEFAULT_DATA_DIR }) };
|
|
136
139
|
const lang = opts.lang ?? config.language ?? "en";
|
|
137
140
|
if (!LANGUAGES[lang]) throw new Error(`Language must be one of ${Object.keys(LANGUAGES).join(", ")}.`);
|
|
138
141
|
const useAi = opts.ai !== false;
|
package/src/browser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { chromium, devices } from "playwright";
|
|
@@ -10,8 +11,9 @@ import { hostScope, isPrivateAddress, navigationError, privateHostChecker } from
|
|
|
10
11
|
import { startEgressProxy } from "./egress.js";
|
|
11
12
|
import { campaignParamsIn } from "./campaign.js";
|
|
12
13
|
import { detectCmpInPage } from "./cmp.js";
|
|
13
|
-
import { callersOf, containerOf, containerVersion } from "./containers.js";
|
|
14
|
+
import { callersOf, containerOf, containerVersion, isContainerId } from "./containers.js";
|
|
14
15
|
import { frameInfo } from "./frames.js";
|
|
16
|
+
import { fillLoginValue, hasLoginPlaceholder, loginFromEnv, loginScrubber, scrubDeep } from "./login.js";
|
|
15
17
|
|
|
16
18
|
// Consent banners of the common consent platforms (CMPs), by their container. A cover that matches
|
|
17
19
|
// one of these always blocks the journey, and a reject click is checked against them afterwards.
|
|
@@ -40,7 +42,7 @@ const LOG_LIMITS = { hits: 5000, dataLayerLog: 5000, network: 10000, containerLo
|
|
|
40
42
|
|
|
41
43
|
// Secrets the browser process has no use for. It gets the rest of the environment
|
|
42
44
|
// (PATH, HOME, DISPLAY...) so Chromium still starts normally.
|
|
43
|
-
const SECRET_ENV = /(API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|_KEY$|^AWS_|^AZURE_|^GOOGLE_APPLICATION)/i;
|
|
45
|
+
const SECRET_ENV = /(API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|_KEY$|^AWS_|^AZURE_|^GOOGLE_APPLICATION|^AITD_LOGIN)/i;
|
|
44
46
|
export const browserEnv = (env = process.env) => Object.fromEntries(Object.entries(env).filter(([k]) => !SECRET_ENV.test(k)));
|
|
45
47
|
|
|
46
48
|
export const VIEWPORTS = {
|
|
@@ -106,6 +108,9 @@ const CONTAINER_HOOK = `
|
|
|
106
108
|
new MutationObserver((list) => {
|
|
107
109
|
for (const m of list) for (const n of m.addedNodes) {
|
|
108
110
|
if (n instanceof HTMLScriptElement && n.src && !seen.has(n)) { seen.add(n); report(n, n.src, "parser"); }
|
|
111
|
+
// A preload hint downloads a container without running it; caught as it is added, since a page's own code
|
|
112
|
+
// can rewrite <head> and take it out again.
|
|
113
|
+
else if (n instanceof HTMLLinkElement && /(^|\\s)(preload|modulepreload|prefetch)(\\s|$)/i.test(n.rel) && n.href && !seen.has(n)) { seen.add(n); report(n, n.href, "preload"); }
|
|
109
114
|
}
|
|
110
115
|
}).observe(document, { childList: true, subtree: true });
|
|
111
116
|
} catch {}
|
|
@@ -221,7 +226,7 @@ function snapshotInPage({ first, limit, textLimit, frameBox, offscreenLimit = nu
|
|
|
221
226
|
const forLabel = el.id ? document.querySelector(`label[for="${CSS.escape(el.id)}"]`)?.innerText : "";
|
|
222
227
|
const img = el.querySelector?.("img[alt], svg title");
|
|
223
228
|
return clean(
|
|
224
|
-
el.getAttribute("aria-label") || byId || el.innerText || el.value || forLabel || el.getAttribute("placeholder") ||
|
|
229
|
+
el.getAttribute("aria-label") || byId || el.innerText || (el.type === "password" ? "" : el.value) || forLabel || el.getAttribute("placeholder") ||
|
|
225
230
|
el.getAttribute("title") || (img ? img.getAttribute("alt") || img.textContent : "") || el.getAttribute("name") || "",
|
|
226
231
|
).slice(0, 80);
|
|
227
232
|
};
|
|
@@ -516,6 +521,17 @@ export function googleConsentStateInPage() {
|
|
|
516
521
|
return { entries, usedDefault: Boolean(ics.usedDefault), usedUpdate: Boolean(ics.usedUpdate), wasSetLate: Boolean(ics.wasSetLate), waitPeriodTimedOut: Boolean(ics.waitPeriodTimedOut) };
|
|
517
522
|
}
|
|
518
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Runs in the page: the IDs Google's own registry (window.google_tag_manager) holds, which are the containers and
|
|
526
|
+
* Google tags that actually ran, however their script got in; and the addresses the page only preloads
|
|
527
|
+
* (<link rel="preload">, "modulepreload", "prefetch"), which download a script without running it.
|
|
528
|
+
*/
|
|
529
|
+
export function containersInPage() {
|
|
530
|
+
const registry = window.google_tag_manager && typeof window.google_tag_manager === "object" ? Object.keys(window.google_tag_manager) : [];
|
|
531
|
+
const preloads = [...document.querySelectorAll('link[rel~="preload"], link[rel~="modulepreload"], link[rel~="prefetch"]')].map((l) => l.href).filter(Boolean);
|
|
532
|
+
return { registry: registry.slice(0, 200), preloads: preloads.slice(0, 200) };
|
|
533
|
+
}
|
|
534
|
+
|
|
519
535
|
/**
|
|
520
536
|
* Runs in the page: whether it is something other than the site, so what a step captured there says nothing about
|
|
521
537
|
* the site's tracking. A bot challenge in its place (Cloudflare, DataDome, HUMAN, Akamai), or a page that crashed in
|
|
@@ -561,6 +577,10 @@ export class AuditSession {
|
|
|
561
577
|
this.opts = { blockHits: true, headless: true, ...opts };
|
|
562
578
|
this.log = this.opts.log ?? (() => {});
|
|
563
579
|
this.scope = this.opts.site ? hostScope(this.opts.site, this.opts.allowedHosts) : null;
|
|
580
|
+
// The test account (see src/login.js): typed only where a placeholder asks for it, and scrubbed from everything kept.
|
|
581
|
+
this.login = this.opts.login !== undefined ? this.opts.login : loginFromEnv();
|
|
582
|
+
this.scrub = loginScrubber(this.login);
|
|
583
|
+
this.loginStep = null; // the first step that typed the test account's password
|
|
564
584
|
this.isBlockedAddress = typeof this.opts.blockPrivate === "function" ? this.opts.blockPrivate : isPrivateAddress;
|
|
565
585
|
this.isPrivate = this.opts.blockPrivate ? privateHostChecker(this.isBlockedAddress) : null;
|
|
566
586
|
this.blocked = null;
|
|
@@ -577,6 +597,8 @@ export class AuditSession {
|
|
|
577
597
|
this.dataLayerLog = [];
|
|
578
598
|
this.containerLoads = []; // each Google container script the page added, with how (see src/containers.js)
|
|
579
599
|
this.containerScripts = []; // each container script's answer: status, version, size and load time
|
|
600
|
+
this.containersRunning = new Map(); // container ID -> the step it was first seen running at (google_tag_manager)
|
|
601
|
+
this.containerPreloads = new Map(); // "id|host" -> { id, host, step } for containers the page only preloaded
|
|
580
602
|
this.frameMessages = []; // each named message an iframe posted to the page: origin, name and field names, never values
|
|
581
603
|
// The order hits, dataLayer pushes and iframe messages were recorded in. Several often share a millisecond, and
|
|
582
604
|
// telling which came first matters: a page's push answers the message just before it (see messageOutcomes).
|
|
@@ -597,6 +619,8 @@ export class AuditSession {
|
|
|
597
619
|
}
|
|
598
620
|
|
|
599
621
|
async start() {
|
|
622
|
+
// Checked before anything launches, so a wrong path leaves no browser behind.
|
|
623
|
+
if (this.opts.storageState && !existsSync(this.opts.storageState)) throw new Error(`No saved login session at ${this.opts.storageState}. Save one with: aitd login --site <url>`);
|
|
600
624
|
const device = VIEWPORTS[this.opts.viewport];
|
|
601
625
|
if (!device) throw new Error(`Unknown viewport "${this.opts.viewport}". Use: ${Object.keys(VIEWPORTS).join(", ")}`);
|
|
602
626
|
const launch = { headless: this.opts.headless, env: browserEnv() };
|
|
@@ -620,7 +644,9 @@ export class AuditSession {
|
|
|
620
644
|
this.browser = await chromium.launch(launch);
|
|
621
645
|
}
|
|
622
646
|
const record = this.opts.recordHar ? { recordHar: { path: this.opts.recordHar, mode: "minimal" } } : {};
|
|
623
|
-
|
|
647
|
+
// A session saved with `aitd login`: the browser starts logged in, for sites whose login needs a person.
|
|
648
|
+
const session = this.opts.storageState ? { storageState: this.opts.storageState } : {};
|
|
649
|
+
this.context = await this.browser.newContext({ ...device, locale: "en-US", ...record, ...session });
|
|
624
650
|
await this.context.exposeBinding("__atdRecord", (source, entry) => {
|
|
625
651
|
const frame = frameInfo(source?.frame);
|
|
626
652
|
if (frame) entry = { ...entry, frame };
|
|
@@ -631,7 +657,7 @@ export class AuditSession {
|
|
|
631
657
|
await this.context.exposeBinding("__atdLoad", (source, entry) => this.recordLoad(entry, frameInfo(source?.frame)));
|
|
632
658
|
await this.context.addInitScript(INIT_SCRIPT);
|
|
633
659
|
this.page = await this.context.newPage();
|
|
634
|
-
this.page.on("pageerror", (err) => this.errors.push({ step: this.currentStep, message: err.message }));
|
|
660
|
+
this.page.on("pageerror", (err) => this.errors.push({ step: this.currentStep, message: this.scrub(err.message) }));
|
|
635
661
|
// A script that comes back as an HTML page (a bot check, or files a new deployment no longer has) is refused.
|
|
636
662
|
this.page.on("console", (msg) => {
|
|
637
663
|
if (msg.type() === "error" && /^Refused to execute script from .*MIME type \('text\/html'\)/.test(msg.text())) this.refusedScripts.set(this.currentStep, (this.refusedScripts.get(this.currentStep) ?? 0) + 1);
|
|
@@ -659,7 +685,7 @@ export class AuditSession {
|
|
|
659
685
|
const frame = this.frameOf(request);
|
|
660
686
|
for (const hit of hits) {
|
|
661
687
|
if (!this.checks(hit.platform)) continue;
|
|
662
|
-
const entry = { ...this.redactHit(hit), step: this.stepForHit(hit), pageUrl: this.page.url(), t: Date.now(), seq: ++this.seq, ...(frame ? { frame } : {}) };
|
|
688
|
+
const entry = { ...this.redactHit(hit), step: this.stepForHit(hit), pageUrl: this.scrub(this.page.url()), t: Date.now(), seq: ++this.seq, ...(frame ? { frame } : {}) };
|
|
663
689
|
const late = this.lateFor(hit, entry);
|
|
664
690
|
if (late) Object.assign(entry, late);
|
|
665
691
|
if (this.record("hits", entry)) recorded.push(entry);
|
|
@@ -688,10 +714,10 @@ export class AuditSession {
|
|
|
688
714
|
const vendor = vendorOf(request.url());
|
|
689
715
|
const call = {
|
|
690
716
|
step: this.currentStep,
|
|
691
|
-
pageUrl: this.opts.keepBodies ? this.page.url() : redactUrl(this.page.url()).url,
|
|
717
|
+
pageUrl: this.scrub(this.opts.keepBodies ? this.page.url() : redactUrl(this.page.url()).url),
|
|
692
718
|
t: Date.now(),
|
|
693
719
|
method: request.method(),
|
|
694
|
-
url: this.opts.keepBodies ? request.url() : redactUrl(request.url()).url,
|
|
720
|
+
url: this.scrub(this.opts.keepBodies ? request.url() : redactUrl(request.url()).url),
|
|
695
721
|
type: request.resourceType(),
|
|
696
722
|
vendor,
|
|
697
723
|
provider: provider.key,
|
|
@@ -831,7 +857,16 @@ export class AuditSession {
|
|
|
831
857
|
return true;
|
|
832
858
|
}
|
|
833
859
|
|
|
834
|
-
|
|
860
|
+
/** A copy with the test account's values turned back into placeholders; the value itself when there is no test account. */
|
|
861
|
+
keepOut(value) {
|
|
862
|
+
return this.login ? scrubDeep(value, this.scrub) : value;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
redactHit(raw) {
|
|
866
|
+
return this.keepOut(this.redactUserData(raw));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
redactUserData({ userFields, ...hit }) {
|
|
835
870
|
const found = { fields: [], plain: [] };
|
|
836
871
|
const site = hostOf(hit.params?.page_location) ?? hostOf(this.page.url());
|
|
837
872
|
const url = redactUrl(hit.url, found, site).url;
|
|
@@ -845,9 +880,9 @@ export class AuditSession {
|
|
|
845
880
|
/** A dataLayer entry with user data redacted, and userData: { fields, plain } when there was any. */
|
|
846
881
|
redactEntry(entry) {
|
|
847
882
|
const found = redactPush(entry.value, undefined, hostOf(this.page.url()));
|
|
848
|
-
if (!found.fields.length) return entry;
|
|
883
|
+
if (!found.fields.length) return this.keepOut(entry);
|
|
849
884
|
const userData = { fields: [...new Set(found.fields)], plain: [...new Set(found.plain)] };
|
|
850
|
-
return this.opts.keepBodies ? { ...entry, userData } : { ...entry, value: found.value, userData };
|
|
885
|
+
return this.keepOut(this.opts.keepBodies ? { ...entry, userData } : { ...entry, value: found.value, userData });
|
|
851
886
|
}
|
|
852
887
|
|
|
853
888
|
/** { postData } for a tracking POST, redacted, within the per-audit budget; { bodyDropped } past it. */
|
|
@@ -859,7 +894,7 @@ export class AuditSession {
|
|
|
859
894
|
return { bodyDropped: true };
|
|
860
895
|
}
|
|
861
896
|
this.bodyBytes += body.length;
|
|
862
|
-
return { postData: this.opts.keepBodies ? body : redactBody(body).text };
|
|
897
|
+
return { postData: this.scrub(this.opts.keepBodies ? body : redactBody(body).text) };
|
|
863
898
|
}
|
|
864
899
|
|
|
865
900
|
/**
|
|
@@ -870,6 +905,12 @@ export class AuditSession {
|
|
|
870
905
|
recordLoad(entry, frame = null) {
|
|
871
906
|
const container = containerOf(entry?.src);
|
|
872
907
|
if (!container) return;
|
|
908
|
+
// A preload hint is not a load: it downloads the script without running it (see unseenContainers).
|
|
909
|
+
if (entry.how === "preload") {
|
|
910
|
+
const k = `${container.id}|${container.host}`;
|
|
911
|
+
if (!frame && !this.containerPreloads.has(k)) this.containerPreloads.set(k, { id: container.id, host: container.host, step: this.currentStep, page: pagePath(entry.page) });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
873
914
|
this.record("containerLoads", {
|
|
874
915
|
...container,
|
|
875
916
|
how: entry.how === "parser" ? "parser" : "script",
|
|
@@ -920,7 +961,7 @@ export class AuditSession {
|
|
|
920
961
|
this.record("frameMessages", {
|
|
921
962
|
origin: String(m.origin).slice(0, 200),
|
|
922
963
|
party,
|
|
923
|
-
name: String(m.name ?? "").slice(0, 80),
|
|
964
|
+
name: this.scrub(String(m.name ?? "")).slice(0, 80),
|
|
924
965
|
...(m.nameKey ? { nameKey: String(m.nameKey).slice(0, 20) } : {}),
|
|
925
966
|
keys: (Array.isArray(m.keys) ? m.keys : []).slice(0, 20).map((k) => String(k).slice(0, 40)),
|
|
926
967
|
step: this.currentStep,
|
|
@@ -1077,9 +1118,10 @@ export class AuditSession {
|
|
|
1077
1118
|
}
|
|
1078
1119
|
if (more) lines.push(`(more elements not listed: scroll to bring them on screen, or use text to click something by its visible text)`);
|
|
1079
1120
|
const note = this.steps[this.currentStep]?.note;
|
|
1080
|
-
|
|
1121
|
+
// What the model reads never holds the test account: a username a page shows back reads as its placeholder.
|
|
1122
|
+
return { url: this.scrub(main.url), title: this.scrub(main.title), elements: this.scrub(lines.join("\n")), text: this.scrub(main.text), note };
|
|
1081
1123
|
} catch (err) {
|
|
1082
|
-
return { url: this.page.url(), title: "", elements: "", text: `(snapshot failed: ${err.message})` };
|
|
1124
|
+
return { url: this.scrub(this.page.url()), title: "", elements: "", text: `(snapshot failed: ${this.scrub(err.message)})` };
|
|
1083
1125
|
}
|
|
1084
1126
|
}
|
|
1085
1127
|
|
|
@@ -1211,10 +1253,13 @@ export class AuditSession {
|
|
|
1211
1253
|
* @param {{ action: string, ref?: number, selector?: string, text?: string, value?: string, url?: string, reason?: string, accepts_consent?: boolean, rejects_consent?: boolean }} a
|
|
1212
1254
|
*/
|
|
1213
1255
|
async act(a) {
|
|
1214
|
-
const error = await this.perform(a);
|
|
1256
|
+
const error = this.scrub(await this.perform(a));
|
|
1215
1257
|
await this.observe();
|
|
1216
1258
|
// A scripted replay of this step may fail the same way, which must not end the journey there.
|
|
1217
1259
|
if (error) this.steps[this.currentStep].error = error;
|
|
1260
|
+
// A saved step keeps placeholders, never the test account (a click's text can be a username the page shows).
|
|
1261
|
+
const step = this.steps[this.currentStep];
|
|
1262
|
+
if (step.replay) step.replay = this.keepOut(step.replay);
|
|
1218
1263
|
this.steps[this.currentStep].pagePath = pagePath(this.page.url());
|
|
1219
1264
|
this.steps[this.currentStep].endedAt = Date.now(); // when the step was reported, so a late hit can say how late
|
|
1220
1265
|
return error;
|
|
@@ -1240,6 +1285,15 @@ export class AuditSession {
|
|
|
1240
1285
|
const tagConsent = await this.evaluateWithin(googleConsentStateInPage);
|
|
1241
1286
|
const before = this.steps.slice(0, this.currentStep).map((st) => st.tagConsent).filter(Boolean).at(-1);
|
|
1242
1287
|
if (tagConsent && JSON.stringify(tagConsent) !== JSON.stringify(before)) this.steps[this.currentStep].tagConsent = tagConsent;
|
|
1288
|
+
// Which containers ran, and which the page only preloaded: a container can download and never run.
|
|
1289
|
+
const found = await this.evaluateWithin(containersInPage);
|
|
1290
|
+
for (const id of (found?.registry ?? []).map((k) => String(k).toUpperCase()).filter(isContainerId)) {
|
|
1291
|
+
if (!this.containersRunning.has(id)) this.containersRunning.set(id, this.currentStep);
|
|
1292
|
+
}
|
|
1293
|
+
for (const c of (found?.preloads ?? []).map(containerOf).filter(Boolean)) {
|
|
1294
|
+
const k = `${c.id}|${c.host}`;
|
|
1295
|
+
if (!this.containerPreloads.has(k)) this.containerPreloads.set(k, { id: c.id, host: c.host, step: this.currentStep, page: pagePath(this.page.url()) });
|
|
1296
|
+
}
|
|
1243
1297
|
}
|
|
1244
1298
|
|
|
1245
1299
|
async perform(a) {
|
|
@@ -1285,7 +1339,20 @@ export class AuditSession {
|
|
|
1285
1339
|
} else return `${a.action} needs a ref or the element's visible text.`;
|
|
1286
1340
|
step.replay = { action: a.action, ...(await replayTarget(a, target)), ...(a.value !== undefined ? { value: a.value } : {}) };
|
|
1287
1341
|
if (a.action === "click") await this.click(target);
|
|
1288
|
-
else if (a.action === "fill"
|
|
1342
|
+
else if (a.action === "fill" && hasLoginPlaceholder(a.value)) {
|
|
1343
|
+
// The test account is typed only into the audited site's own pages, never a frame from elsewhere.
|
|
1344
|
+
const host = await target.evaluate(() => location.hostname).catch(() => "");
|
|
1345
|
+
if (this.scope && !this.scope.allows(host)) return `The test account is only filled in on ${this.scope.hosts.join(", ")}, not on ${host || "this frame"}.`;
|
|
1346
|
+
let value;
|
|
1347
|
+
try {
|
|
1348
|
+
value = fillLoginValue(a.value, this.login);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
if (err.code === "NO_LOGIN") return err.message;
|
|
1351
|
+
throw err;
|
|
1352
|
+
}
|
|
1353
|
+
await target.fill(value, { timeout: 10000 });
|
|
1354
|
+
if (/login\.password/.test(a.value) && this.loginStep === null) this.loginStep = this.currentStep;
|
|
1355
|
+
} else if (a.action === "fill") await target.fill(a.value ?? "", { timeout: 10000 });
|
|
1289
1356
|
else await target.selectOption(a.value ?? "", { timeout: 10000 });
|
|
1290
1357
|
break;
|
|
1291
1358
|
}
|