@analitikisler/ai-tag-debugger 0.20.1 → 0.21.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 CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0 (2026-11-03)
4
+
5
+ - Runs can report findings 0.20.1 did not on the same site (hits with `ad_user_data` or `ad_personalization` granted before consent or after a reject, `gclid_not_stored`, `client_id_changed`, `consent_default_not_applied`, `page_blocked`, `page_crashed`, and tags sent with cookies when the visitor made no choice), and no longer reports `consent_default_missing` where a consent platform sets the default inside GTM. A baseline written with 0.20.1 can then list stale or new entries; `--update-baseline` rewrites it.
6
+ - OneTrust and Cookiebot in full. The Consent platform card gives their version, the consent model and region they applied to the visit (and says so when they counted it as consent without a choice), their groups or categories, whether their Consent Mode integration is on and which group or category grants each signal (OneTrust's from its own setting, Cookiebot's fixed), ads data redaction, auto-blocking, IAB TCF, and when the site last published its setup file.
7
+ - Fixed: Cookiebot's newer script exposes only `CookieConsent`, not `Cookiebot`, so the consent scenarios could not answer it and fell back to clicking. They now use either.
8
+ - Fixed: analytics only with OneTrust turned on group `C0002`, which sites that number their groups otherwise (onetrust.com uses `2`) do not have, so it did nothing there. It now turns on the group OneTrust's Consent Mode setting gives `analytics_storage`, and is skipped when ads share that group.
9
+ - The date of a consent platform's setup files fetched by script (OneTrust's, Cookiebot's), not only of its script tags, is recorded.
10
+ - CookieYes in full: the Consent platform card gives the law its banner follows, its categories, how it maps them to Consent Mode (the same on every site, read off its consent updates), how many providers it holds back and when its site script last changed. The consent scenarios answer it through its named buttons, which works in any banner language, and analytics only now works with it, through its preference center.
11
+ - Fixed: CookieYes's CDN (`cdn-cookieyes.com`) was not recognised as a consent platform host, so its calls were missing from the inventory and its script date from the report.
12
+ - MCP: `start_audit` takes `live: true` to let the tracking requests through to the site's real accounts, for checking them in GA4 Realtime or DebugView, Meta Events Manager or Tag Assistant. The start and `finish_audit` results begin with a LIVE line, and the report and `report.md` say the run was live (a CLI run with `block_hits: false` says so too). A local server allows it; an HTTP server only when started with the new `--allow-live` flag.
13
+ - A page that isn't the site is reported: a bot challenge shown in its place (Cloudflare, DataDome, HUMAN, Akamai) as `page_blocked`, and a page that crashed in the browser, such as a Next.js "Application error" after its own scripts came back as HTML, as `page_crashed`, both warnings. MCP step results warn on the step. Planned events not seen on such a run are unverified (info) rather than missing (broken).
14
+ - The consent scenarios open the start page like an ad click, with the same test campaign parameters as the journeys (none when `campaign_checks` is `false`), and gain a fifth column: no choice and on to the next page. A tag that sends with cookies there is flagged like after reject all, since browsing on is no consent under the GDPR or KVKK. The start page is opened once more for it.
15
+ - New **Consent matrix** under the consent scenarios, in the report and in `report.md`: per choice, from the page it leads to, the first Google hit and its consent, whether the campaign parameters reached it, whether the GA4 client id was kept, whether `_gcl_aw` holds the landing gclid, the cookies set, how long after the page started each tag was called, and the Google tag's own consent. Tracking cookies without consent, a client id that changes on accept and a UTM parameter lost from the first hit are marked.
16
+ - MCP: the new `consent_matrix` tool runs the consent scenarios on a site by itself, with `campaign` for the parameters to check, and returns the per-choice tag table and the matrix in about half a minute.
17
+ - MCP: `finish_audit` ends with a compact JSON timeline, one entry per step: the hits with their consent, consent commands, other tracking calls, the Google tag's consent when it changed, and the cookies and ids that changed. The new `get_report` tool reads a report this server wrote (`report.md`, `summary.json` or the timeline of every journey) by the name `finish_audit` or `run_audit` gave, so a chat app on another computer no longer needs the report file. It reads this server's own reports only, over HTTP too.
18
+ - MCP: page views are 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 unchanged since the last view (the refs stay the same). `detail: "full"` on `browser_action` brings everything back, and `get_page` shows everything by default.
19
+ - The Google tag's own consent state (`google_tag_data.ics`) is recorded on every step where it changes: per consent type the default and the update it holds, whether it applied a default and an update, whether the default came late and whether `wait_for_update` ran out. It shows how the tag sees consent wherever it was set, including a consent platform's GTM template, which never shows in the dataLayer. The Consent Mode card shows it per run and MCP step results show each change.
20
+ - Fixed: on sites whose consent platform sets the Consent Mode default through a GTM template, `consent_default_missing` was reported although the Google tag had a default. The default, its v2 signals, a late default, the step consent was granted and a re-grant after a reject are now also read from the Google tag's own state.
21
+ - New check `consent_default_not_applied` (warning): the page pushed a Consent Mode default the Google tag never applied, for example as a plain array instead of through `gtag()`, so its tags ran without one. This is the case where Tag Assistant says Consent Mode is not configured.
22
+ - Efilli is recognised. The consent scenarios answer it through its own SDK (accept, reject, and analytics only when Efilli's own Consent Mode integration is on, using the category it maps `analytics_storage` to), its scripts count as the consent platform in the inventory, and a reject button that leaves its banner open is reported as `reject_button_failed`: banners drawn inside an open shadow root, as Efilli's is, are checked like any other. "Tümünü Reddet" is read as a reject button.
23
+ - The report names the site's consent platform in a **Consent platform** card in the Consent section and in `report.md`. For Efilli: when the site's bundle was compiled, the language whose setup is in force, the categories, whether Efilli's own Consent Mode integration is on and what each signal maps to (when it is off, the signals come from somewhere else, such as a GTM template, and the configured mapping is not in force), auto-blocking and IAB TCF. OneTrust, Cookiebot, Usercentrics, Didomi and CookieYes are named, with the date their script last changed. MCP step results and `finish_audit` say the same.
24
+ - Each step records the cookies it added, changed or removed, by name, and what the analytics and ad cookies say: the GA4 client id in `_ga`, and whether `_gcl_aw` and `_fbc` hold the click ids the landing page was opened with. Cookie values are still never stored; a changed value is noticed through a hash kept in memory until the next step. The report has a **Cookies** tab per step under **Journeys**, and MCP step results list the same.
25
+ - New check `gclid_not_stored` (warning): a Google Ads tag ran with ad storage granted, but `_gcl_aw` never held the landing page's `gclid`, so a later conversion can't be tied to the click. GA4 alone never writes the cookie and ad storage denied rightly keeps it out, so neither is flagged.
26
+ - New check `client_id_changed` (warning): the GA4 client id in `_ga` changed mid-visit, which splits one visitor into two users. A change after the visitor rejected cookies is not flagged.
27
+ - MCP: Consent Mode pings show the click ids they carried: `gclid`, `gclid=0 (redacted)` when ads data redaction blanks it, and `gclaw`, the one `_gcl_aw` holds.
28
+ - MCP: `start_audit` opens the first page the way an ad click does, as the CLI already did, so `finish_audit` checks that the campaign parameters survive redirects and reach the hits. `campaign: false` opens the URL as given, and `campaign: {"utm_source": "newsletter", "gclid": "test123"}` uses a campaign's own values.
29
+ - MCP: each step result starts with its step number, and each hit has an `[h]` number and the ids and flags GA4 sends with it (`cid`, `sid`, `sct`, `seg`, `gcs`, `gcd`, `dma`, `npa`, `_fv`, `_ss`, `_nsi`). The new `get_hit` tool shows one hit in full: every parameter its request carried, its place in a batched request, where it was sent and `gcd` decoded per consent signal. The new `get_step` tool shows an earlier step's capture again.
30
+ - `gcd` is read on GA4 hits, Google Ads conversions and Consent Mode pings: each hit's consent now includes `ad_user_data` and `ad_personalization`, and `gcd` stands in for `gcs` when a hit has none. Google Ads conversions carry their consent too. The report shows all four signals on a hit.
31
+ - A Google hit that says `ad_user_data` or `ad_personalization` was granted counts as granted, like `ad_storage` and `analytics_storage`: before the visitor chose, after a reject, and in the consent scenarios. Runs on sites that grant those two early can show findings they did not before.
32
+
3
33
  ## 0.20.1 (2026-11-03)
4
34
 
5
35
  - Runs can report fewer findings than 0.20.0 on the same site: GA4's own events are no longer unplanned, and a reject that keeps essential storage granted is no longer a re-grant. A baseline written with 0.20.0 can then list those entries as stale; `--update-baseline` rewrites it.
package/README.md CHANGED
@@ -45,11 +45,13 @@ Manual tag QA catches this, but it's tedious, so it rarely happens after every r
45
45
  | Empty purchase items | `purchase` sent with an empty `items` list |
46
46
  | Purchase mismatch | GA4 sends `value` 120 and Meta sends 115 for the same order ID |
47
47
  | Duplicates | `add_to_cart` sent twice by one click |
48
- | Consent | Meta `PageView` fired before the visitor accepted cookies, Consent Mode default missing or set too late, Consent Mode v2 signals (`ad_user_data`, `ad_personalization`) missing, tracking cookies set before consent, tags or cookies still active after the `decline_cookies` journey rejects the banner, a reject button that does nothing |
48
+ | Consent | Meta `PageView` fired before the visitor accepted cookies, Consent Mode default missing or set too late, Consent Mode v2 signals (`ad_user_data`, `ad_personalization`) missing, tracking cookies set before consent, tags or cookies still active after the `decline_cookies` journey rejects the banner, a reject button that does nothing, a Consent Mode default the page pushed that the Google tag never applied (for example pushed as a plain array instead of through `gtag()`). Defaults and updates a consent platform sets inside GTM, which never show in the dataLayer, are read from the Google tag's own state |
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. GA4's automatic events (`first_visit`, `session_start`, `user_engagement`) and enhanced measurement events (`page_view`, `scroll`, `click`, `view_search_results`, `video_*`, `file_download`, `form_start`, `form_submit`) never count, since no tag sends them. When one of them is planned and missing, the finding names the enhanced measurement setting that sends it; `first_visit` and `session_start` are read from the flags GA4 puts on its other hits. |
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
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
+ | 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
+ | 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 |
53
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 |
54
56
 
55
57
  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.
@@ -66,7 +68,11 @@ Requests to platforms you didn't choose are still answered locally, so they neve
66
68
 
67
69
  ### Consent scenarios
68
70
 
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.
71
+ Each audit also opens the start page once per consent choice, each time in a fresh browser and like an ad click (with the same test campaign parameters as the journeys, unless `campaign_checks` is `false`): no choice and on to the next page, accept all, reject all and analytics only. The choice goes through the consent platform's own API (OneTrust, Cookiebot, Usercentrics, Didomi or Efilli) or, for CookieYes, its named buttons and switches, 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 (it turns on the group OneTrust's Consent Mode setting gives `analytics_storage`), Cookiebot and CookieYes, and with Efilli when its own Consent Mode integration is on (it turns on the category Efilli maps `analytics_storage` to); with other platforms that column says "Not supported here". A tag that sends after reject all, or on the next page when the visitor made no choice (browsing on is no consent under the GDPR or KVKK), 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.
72
+
73
+ Under it, the **Consent matrix** puts the choices side by side, from the page each one leads to: the first Google hit and the consent it carries, whether the campaign parameters reached it, whether the GA4 client id the visitor had before choosing was kept, whether `_gcl_aw` holds the landing `gclid`, the cookies set, how long after the page started each tag was called, and the Google tag's own consent. What should not happen with a choice is marked: tracking cookies before a choice, without one or after reject all, a client id that changes on accept, and a UTM parameter lost from the first hit. The same matrix is in `report.md`, and the `consent_matrix` MCP tool runs it on its own.
74
+
75
+ The **Consent** section also names the consent platform the site runs. For Efilli it says when the site's bundle was compiled, which language's setup is in force, its categories, whether Efilli's own Consent Mode integration is on and the category each signal maps to (and, when it is off, that the signals come from somewhere else, such as a GTM template), and whether auto-blocking and IAB TCF are on. For CookieYes it gives the law its banner follows, its categories, how it maps them to Consent Mode (the same on every site: advertisement to `ad_storage`, `ad_user_data` and `ad_personalization`, analytics to `analytics_storage`, functional to `functionality_storage` and `personalization_storage`, necessary to `security_storage`), how many providers it holds back, and when its site script last changed. For OneTrust: its version, the consent model and region it applied to the visit, its groups, whether its Consent Mode setting is on and which group grants each signal, ads data redaction, IAB TCF, and when the site last published its setup. For Cookiebot: its version, consent model and region, its fixed mapping (marketing to the three ad signals, statistics to `analytics_storage`, preferences to `functionality_storage` and `personalization_storage`), whether its Consent Mode integration is on, ads data redaction, auto-blocking, IAB TCF, and when its setup was last published. When a platform counted the visit as consent given without a choice (an opt-out region), the card says so, since tags then run before any choice. Other platforms are named, with the date their script last changed. A reject button inside a banner drawn in a shadow root, as Efilli's is, is checked like any other. 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
76
 
71
77
  ## How it works
72
78
 
@@ -301,7 +307,7 @@ When the plan is not the site's first, the header compares it with the version s
301
307
 
302
308
  ### Tag inventory
303
309
 
304
- Every report also has a **tag inventory**: each vendor the site sent requests to, marked first party when its requests went to the site's own domain, third party when they went to the vendor, or both. It shows how many pages the vendor was seen on and the cookies tied to it. Cookies that no vendor owns are listed apart, as first or third party. Only cookie names are recorded, never their values. It is the **Inventory** section of `report.html` and a table in `report.md`.
310
+ 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, with two exceptions that are derived rather than copied: the GA4 client id that `_ga` holds (GA4 hits carry it anyway), and whether `_gcl_aw` and `_fbc` hold the click ids the landing page was opened with. Each step also notes which cookies it added, changed or removed, by name, in its **Cookies** tab under **Journeys**. It is the **Inventory** section of `report.html` and a table in `report.md`.
305
311
 
306
312
  ### Folder layout and plan IDs
307
313
 
@@ -516,7 +522,7 @@ aitd run --site <url> (--against <plan> | --journey <id> | --goal "<text>") [--c
516
522
  [--scripted] [--no-ai] [--save-site <dir>] [--save-steps <file>]
517
523
  aitd replay <dir>... [--update] [--out <dir>]
518
524
  aitd mcp [--http] [--port <n>] [--host <addr>] [--token <secret>] [--public-url <https url>] [--reports <dir>]
519
- [--allow-private-network]
525
+ [--allow-private-network] [--allow-live]
520
526
  aitd doctor [--site <url>] [--data-dir <dir>]
521
527
  aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir>]
522
528
  ```
@@ -557,7 +563,7 @@ aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir
557
563
  | `--allow-host <host>` | run | Another host journeys may visit (repeatable). Same as `allowed_hosts`. |
558
564
  | `--keep-bodies` | run | Store tracking requests as sent, without redacting user data. |
559
565
  | `--no-branding` | plan, run | Leave the logo and credit line out of the reports and `plan.html`. |
560
- | `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network` | mcp | See the MCP section below. |
566
+ | `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network`, `--allow-live` | mcp | See the MCP section below. |
561
567
 
562
568
  Removed in 0.10.0: `plan --from`, `plan --out` and `plan --force`. Drafting a plan always saves a new folder, and a removed flag prints what to run instead.
563
569
 
@@ -624,10 +630,14 @@ Tools the server offers:
624
630
  |---|---|
625
631
  | `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. |
626
632
  | `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. |
627
- | `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. |
628
- | `browser_action` | One click, fill, select, goto, scroll or wait. Returns the page and the tracking hits that step sent, 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` and `gcd`. A page_view that arrives during a later wait or scroll is listed on the step that loaded its page, marked late. |
629
- | `get_page` | Shows the current page. |
630
- | `finish_audit` | Checks everything against your measurement plan (JSON), a saved one given as `against` (`latest`, `first`, a plan ID, or on a local server a path), or the ready-made journey's events, optionally only the `events` you name, and writes the full HTML report with the timeline. The site comes from the audit. |
633
+ | `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. |
634
+ | `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. |
635
+ | `get_page` | Shows the current page, in full: every element and the whole page text. |
636
+ | `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). |
637
+ | `get_step` | Shows what an earlier `step` captured again, as `browser_action` showed it, including a page_view that came late and was added to it. |
638
+ | `finish_audit` | Checks everything against your measurement plan (JSON), a saved one given as `against` (`latest`, `first`, a plan ID, or on a local server a path), or the ready-made journey's events, optionally only the `events` you name, and writes the full HTML report with the timeline. The site comes from the audit. The result ends with a compact JSON timeline, one entry per step: the hits with their consent, consent commands, other tracking calls, the Google tag's consent when it changed, and the cookies and ids that changed. |
639
+ | `consent_matrix` | Opens a site's start page like an ad click once per consent choice (no choice and on to the next page, accept all, reject all, analytics only), each in a fresh browser, and returns which tags sent after each choice and the [consent matrix](#consent-scenarios). `campaign` takes the campaign parameters to check (as in `start_audit`), `concurrency` how many choices run at once (default 2, at most 4). About half a minute. |
640
+ | `get_report` | Reads a report this server wrote, by the name `finish_audit` or `run_audit` gave: `part` `markdown` (default, `report.md`), `summary` (`summary.json`) or `timeline` (the JSON timeline of every journey). Only this server's own reports, over HTTP too. |
631
641
  | `run_audit` | Runs a config file end to end. With `against`, it checks the run against a saved plan and, when the config has no journeys, walks the journeys derived from that plan. `concurrency` runs that many journeys at once, each in its own browser (default 1), and `baseline` names a file of accepted findings written with `--update-baseline` (default: the config's `baseline`; see [Accept known findings](#accept-known-findings)). Local only for file paths. |
632
642
 
633
643
  `against` takes the same values as the CLI's `--against`. Over HTTP it takes only `latest`, `first` or a plan ID, never a path. Plans are the ones saved by `aitd plan --site <url>` or by `make_plan`. Ask for a plan when you want to know what to track, and for an audit when you want to know what the site sends.
@@ -677,6 +687,7 @@ In HTTP mode, the server is more locked down than locally:
677
687
 
678
688
  - It doesn't read measurement plans or configs from your disk (pass the plan as JSON text in the chat, or name a saved plan with `against: latest`, `first` or its ID), and `run_audit` isn't offered. It only serves the reports it wrote itself.
679
689
  - The browser opens only `http` and `https` pages, and refuses `localhost` and private network addresses (such as `192.168.x.x` or `169.254.169.254`), so the URL can't be used to look into your network. All browser traffic, WebSockets included, goes through a small built-in proxy that resolves each host once and connects only to the address it checked, so a DNS answer that changes mid-audit can't get around the check. To audit a local staging site this way anyway, start the server with `--allow-private-network`.
690
+ - Tracking requests are answered inside the browser, so audits never reach the audited site's accounts. `start_audit` with `live: true` lets them through only when the server was started with `--allow-live`.
680
691
  - Audits left open for 15 minutes are closed, and request bodies are limited to 1 MB.
681
692
 
682
693
  A hosted server, so you don't need a tunnel, is part of the [Analitik İşler](https://analitikisler.com) Cloud version.
package/README.tr.md CHANGED
@@ -45,11 +45,13 @@ Manuel etiket kontrolü bunu yakalar, ama zahmetlidir; bu yüzden her sürümden
45
45
  | Boş satın alma ürünleri | `items` listesi boş gönderilen `purchase` |
46
46
  | Satın alma uyuşmazlığı | Aynı sipariş numarası için GA4'ün 120, Meta'nın 115 gönderdiği durum |
47
47
  | Tekrarlar | Tek bir tıklamayla iki kez gönderilen `add_to_cart` |
48
- | Çerez onayı | Ziyaretçi çerezleri kabul etmeden fire olan Meta `PageView`, eksik ya da çok geç ayarlanmış Consent Mode varsayılanı, eksik Consent Mode v2 sinyalleri (`ad_user_data`, `ad_personalization`), onaydan önce ayarlanan takip çerezleri, `decline_cookies` yolculuğu reddettikten sonra hâlâ aktif olan etiket ya da çerezler, hiçbir şey yapmayan bir reddetme düğmesi |
48
+ | Çerez onayı | Ziyaretçi çerezleri kabul etmeden fire olan Meta `PageView`, eksik ya da çok geç ayarlanmış Consent Mode varsayılanı, eksik Consent Mode v2 sinyalleri (`ad_user_data`, `ad_personalization`), onaydan önce ayarlanan takip çerezleri, `decline_cookies` yolculuğu reddettikten sonra hâlâ aktif olan etiket ya da çerezler, hiçbir şey yapmayan bir reddetme düğmesi, sayfanın gönderdiği ama Google etiketinin hiç uygulamadığı bir Consent Mode varsayılanı (ör. `gtag()` yerine düz bir dizi olarak gönderilen). Bir onay platformunun GTM içinde ayarladığı ve dataLayer'da hiç görünmeyen varsayılan ve güncellemeler, Google etiketinin kendi durumundan okunur |
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. GA4'ün otomatik event'leri (`first_visit`, `session_start`, `user_engagement`) ve geliştirilmiş ölçüm event'leri (`page_view`, `scroll`, `click`, `view_search_results`, `video_*`, `file_download`, `form_start`, `form_submit`) hiçbir etiket göndermediği için sayılmaz. Bunlardan biri planda olup gelmediğinde bulgu, onu gönderen geliştirilmiş ölçüm ayarının adını verir; `first_visit` ve `session_start`, GA4'ün diğer isteklerine koyduğu işaretlerden okunur. |
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
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
+ | 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
+ | 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 |
53
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 |
54
56
 
55
57
  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.
@@ -66,7 +68,11 @@ Seçmediğiniz platformlara giden istekler de yerelde yanıtlanır, yani o hesap
66
68
 
67
69
  ### Onay senaryoları
68
70
 
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.
71
+ 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 ve bir reklam tıklaması gibi (`campaign_checks` `false` değilse yolculuklardaki test kampanya parametreleriyle) yapılır: seçim yapmadan sonraki sayfaya geçmek, tümünü kabul, tümünü reddet ve yalnızca analitik. Seçim, consent platformunun kendi API'siyle (OneTrust, Cookiebot, Usercentrics, Didomi ya da Efilli) ya da CookieYes için adlandırılmış düğme ve anahtarlarıyla yapılır; 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 (OneTrust'ın Consent Mode ayarının `analytics_storage`'a verdiği grubu açar), Cookiebot ve CookieYes ile, Efilli'de ise kendi Consent Mode entegrasyonu açıkken çalışır (Efilli'nin `analytics_storage` için eşlediği kategoriyi açar); diğer platformlarda o sütun "Burada desteklenmiyor" yazar. Tümünü reddet'ten sonra ya da ziyaretçi seçim yapmadan sonraki sayfaya geçtiğinde (gezinmeye devam etmek GDPR ve KVKK'ya göre onay değildir) 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.
72
+
73
+ Altındaki **Onay matrisi** seçimleri, her birinin götürdüğü sayfadan yan yana koyar: ilk Google isteği ve taşıdığı onay, kampanya parametrelerinin ona ulaşıp ulaşmadığı, ziyaretçinin seçimden önceki GA4 istemci kimliğinin korunup korunmadığı, `_gcl_aw`'nın açılış `gclid`'ini tutup tutmadığı, yazılan çerezler, sayfa başladıktan ne kadar sonra her etiketin çağrıldığı ve Google etiketinin kendi onay durumu. O seçimle olmaması gereken işaretlenir: seçimden önce, seçim yapılmadan ya da tümünü reddet'ten sonra yazılan takip çerezleri, kabulde değişen istemci kimliği ve ilk istekte kaybolan bir UTM parametresi. Aynı matris `report.md` içinde de yer alır; `consent_matrix` MCP aracı onu tek başına çalıştırır.
74
+
75
+ **Onay** bölümü ayrıca sitenin kullandığı onay platformunu adlandırır. Efilli için sitenin paketinin ne zaman derlendiğini, hangi dilin kurulumunun geçerli olduğunu, kategorilerini, Efilli'nin kendi Consent Mode entegrasyonunun açık olup olmadığını ve her sinyalin eşlendiği kategoriyi (kapalıysa sinyallerin başka bir yerden, örneğin bir GTM şablonundan geldiğini), otomatik engellemenin ve IAB TCF'nin açık olup olmadığını söyler. CookieYes için bannerının uyduğu yasayı, kategorilerini, onları Consent Mode'a nasıl eşlediğini (her sitede aynı: advertisement → `ad_storage`, `ad_user_data` ve `ad_personalization`; analytics → `analytics_storage`; functional → `functionality_storage` ve `personalization_storage`; necessary → `security_storage`), kaç sağlayıcıyı beklettiğini ve site betiğinin en son ne zaman değiştiğini verir. OneTrust için: sürümünü, ziyarete uyguladığı onay modelini ve bölgeyi, gruplarını, Consent Mode ayarının açık olup olmadığını ve her sinyali hangi grubun verdiğini, reklam verisi karartmayı, IAB TCF'yi ve sitenin kurulumunu en son ne zaman yayımladığını verir. Cookiebot için: sürümünü, onay modelini ve bölgeyi, sabit eşlemesini (marketing → üç reklam sinyali, statistics → `analytics_storage`, preferences → `functionality_storage` ve `personalization_storage`), Consent Mode entegrasyonunun açık olup olmadığını, reklam verisi karartmayı, otomatik engellemeyi, IAB TCF'yi ve kurulumunun en son ne zaman yayımlandığını verir. Bir platform ziyareti seçim yapılmadan verilmiş onay saydığında (açık onay gerektirmeyen bir bölge), kart bunu söyler; etiketler o zaman herhangi bir seçimden önce çalışır. Diğer platformlar, betiklerinin en son değiştiği tarihle birlikte adlandırılır. Efilli'deki gibi shadow root içinde çizilen bir bannerdaki reddet düğmesi de diğerleri gibi kontrol edilir. 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
76
 
71
77
  ## Nasıl çalışır
72
78
 
@@ -301,7 +307,7 @@ Kontrol edilen plan sitenin ilk planı değilse, rapor başlığı onu bir önce
301
307
 
302
308
  ### Etiket envanteri
303
309
 
304
- Her raporda ayrıca bir **etiket envanteri** bulunur: sitenin istek gönderdiği her sağlayıcı; istekleri sitenin kendi alan adına gittiyse birinci taraf, sağlayıcıya gittiyse üçüncü taraf, ikisine de gittiyse ikisi olarak işaretlenir. Sağlayıcının görüldüğü sayfa sayısını ve ona bağlı çerezleri gösterir. Hiçbir sağlayıcıya ait olmayan çerezler ayrıca birinci ya da üçüncü taraf olarak listelenir. Yalnızca çerez adları kaydedilir, değerleri asla. Envanter `report.html` içindeki **Envanter** bölümünde, `report.md` içinde ise bir tablo olarak yer alır.
310
+ 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; kopyalanmayıp türetilen iki istisna dışında: `_ga`'nın tuttuğu GA4 istemci kimliği (GA4 istekleri onu zaten taşır) ve `_gcl_aw` ile `_fbc`'nin açılış sayfasının açıldığı tıklama kimliklerini tutup tutmadığı. Her adım ayrıca eklediği, değiştirdiği ya da sildiği çerezleri adlarıyla, **Yolculuklar** altındaki **Çerezler** sekmesinde not eder. Envanter `report.html` içindeki **Envanter** bölümünde, `report.md` içinde ise bir tablo olarak yer alır.
305
311
 
306
312
  ### Klasör yapısı ve plan kimlikleri
307
313
 
@@ -516,7 +522,7 @@ aitd run --site <url> (--against <plan> | --journey <id> | --goal "<metin>") [--
516
522
  [--scripted] [--no-ai] [--save-site <dir>] [--save-steps <dosya>]
517
523
  aitd replay <dir>... [--update] [--out <dir>]
518
524
  aitd mcp [--http] [--port <n>] [--host <adres>] [--token <gizli>] [--public-url <https url>] [--reports <dir>]
519
- [--allow-private-network]
525
+ [--allow-private-network] [--allow-live]
520
526
  aitd doctor [--site <url>] [--data-dir <dir>]
521
527
  aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir>]
522
528
  ```
@@ -557,7 +563,7 @@ aitd history --site <url> [--limit <n>] [--json] [--lang en|tr] [--data-dir <dir
557
563
  | `--allow-host <ana makine>` | run | Yolculukların ziyaret edebileceği başka bir ana makine (tekrarlanabilir). `allowed_hosts` ile aynıdır. |
558
564
  | `--keep-bodies` | run | Takip isteklerini gönderildiği gibi, kullanıcı verisi gizlenmeden sakla. |
559
565
  | `--no-branding` | plan, run | Logo ve kredi satırını raporlardan ve `plan.html` dosyasından çıkar. |
560
- | `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network` | mcp | Aşağıdaki MCP bölümüne bakın. |
566
+ | `--http`, `--port`, `--host`, `--token`, `--public-url`, `--reports`, `--allow-private-network`, `--allow-live` | mcp | Aşağıdaki MCP bölümüne bakın. |
561
567
 
562
568
  0.10.0 sürümünde kaldırıldı: `plan --from`, `plan --out` ve `plan --force`. Plan taslağı her zaman yeni bir klasör kaydeder; kaldırılmış bir bayrak ne çalıştırılacağını yazdırır.
563
569
 
@@ -624,10 +630,14 @@ Sunucunun sunduğu araçlar:
624
630
  |---|---|
625
631
  | `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. |
626
632
  | `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. |
627
- | `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. |
628
- | `browser_action` | Tek bir tıklama, doldurma, seçme, gitme, kaydırma ya da bekleme. Sayfayı, o adımın gönderdiği takip isteklerini, 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` ve `gcd` değerleriyle Consent Mode ping'leri) 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. |
629
- | `get_page` | Geçerli sayfayı gösterir. |
630
- | `finish_audit` | Her şeyi ölçümleme planınıza (JSON) ya da `against` olarak verilen kayıtlı bir plana (`latest`, `first`, bir plan kimliği ya da yerel bir sunucuda bir yol) ya da hazır yolculuğun event'lerine karşı kontrol eder; isterseniz yalnızca adını verdiğiniz `events` için, ve zaman akışıyla birlikte tam HTML raporunu yazar. Site denetimden alınır. |
633
+ | `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. |
634
+ | `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. |
635
+ | `get_page` | Geçerli sayfayı eksiksiz gösterir: her öğe ve sayfanın tüm metni. |
636
+ | `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). |
637
+ | `get_step` | Önceki bir adımın (`step`) yakaladıklarını `browser_action`'ın gösterdiği gibi yeniden gösterir; geç gelip o adıma eklenen page_view da dahil. |
638
+ | `finish_audit` | Her şeyi ölçümleme planınıza (JSON) ya da `against` olarak verilen kayıtlı bir plana (`latest`, `first`, bir plan kimliği ya da yerel bir sunucuda bir yol) ya da hazır yolculuğun event'lerine karşı kontrol eder; isterseniz yalnızca adını verdiğiniz `events` için, ve zaman akışıyla birlikte tam HTML raporunu yazar. Site denetimden alınır. Sonuç, adım başına bir kayıt içeren kısa bir JSON zaman akışıyla biter: onay durumlarıyla istekler, onay komutları, diğer takip çağrıları, değiştiğinde Google etiketinin onay durumu, değişen çerezler ve kimlikler. |
639
+ | `consent_matrix` | Bir sitenin başlangıç sayfasını her onay seçimi için (seçim yapmadan sonraki sayfa, tümünü kabul, tümünü reddet, yalnızca analitik) bir reklam tıklaması gibi, her seferinde yeni bir tarayıcıda açar; her seçimden sonra hangi etiketlerin gönderdiğini ve [onay matrisini](#onay-senaryoları) döndürür. `campaign` kontrol edilecek kampanya parametrelerini (`start_audit`'teki gibi), `concurrency` aynı anda kaç seçimin çalışacağını alır (varsayılan 2, en fazla 4). Yaklaşık yarım dakika sürer. |
640
+ | `get_report` | Bu sunucunun yazdığı bir raporu, `finish_audit` ya da `run_audit`'in verdiği adla okur: `part` `markdown` (varsayılan, `report.md`), `summary` (`summary.json`) ya da `timeline` (her yolculuğun JSON zaman akışı). Yalnızca bu sunucunun kendi raporları, HTTP üzerinden de. |
631
641
  | `run_audit` | Bir ayar dosyasını baştan sona çalıştırır. `against` ile çalıştırmayı kayıtlı bir plana karşı kontrol eder; ayar dosyasında yolculuk yoksa, plandan türetilen yolculukları gezer. `concurrency` o kadar yolculuğu aynı anda, her birini kendi tarayıcısında çalıştırır (varsayılan 1); `baseline`, `--update-baseline` ile yazılmış kabul edilen bulgular dosyasını adlandırır (varsayılan: ayar dosyasındaki `baseline`; bkz. [Bilinen bulguları kabul edin](#bilinen-bulguları-kabul-edin)). Dosya yolları yalnızca yerelde çalışır. |
632
642
 
633
643
  `against`, CLI'deki `--against` ile aynı değerleri alır. HTTP üzerinden yalnızca `latest`, `first` ya da bir plan kimliği kabul edilir, asla bir yol değil. Planlar, `aitd plan --site <url>` ya da `make_plan` ile kaydedilmiş olanlardır. Neyin takip edileceğini öğrenmek için plan, sitenin ne gönderdiğini öğrenmek için denetim isteyin.
@@ -677,6 +687,7 @@ HTTP modunda sunucu yerelde olduğundan daha kısıtlıdır:
677
687
 
678
688
  - Ölçümleme planlarını ya da ayar dosyalarını diskinizden okumaz (planı sohbette JSON metni olarak verin ya da `against: latest`, `first` ya da kimliğiyle kayıtlı bir planı adlandırın) ve `run_audit` sunulmaz. Yalnızca kendi yazdığı raporları sunar.
679
689
  - Tarayıcı yalnızca `http` ve `https` sayfalarını açar ve `localhost` ile özel ağ adreslerini (`192.168.x.x` ya da `169.254.169.254` gibi) reddeder; böylece URL, ağınızı incelemek için kullanılamaz. WebSocket'ler dahil tüm tarayıcı trafiği, her ana makineyi bir kez çözüp yalnızca kontrol ettiği adrese bağlanan küçük bir yerleşik proxy üzerinden geçer; denetim sırasında değişen bir DNS yanıtı kontrolü atlatamaz. Yerel bir staging sitesini bu şekilde yine de denetlemek için sunucuyu `--allow-private-network` ile başlatın.
690
+ - Takip istekleri tarayıcının içinde yanıtlanır, bu yüzden denetimler denetlenen sitenin hesaplarına hiç ulaşmaz. `start_audit`'teki `live: true` onları yalnızca sunucu `--allow-live` ile başlatıldıysa geçirir.
680
691
  - 15 dakika açık kalan denetimler kapatılır ve istek gövdeleri 1 MB ile sınırlıdır.
681
692
 
682
693
  Tünel gerektirmeyen barındırılmış bir sunucu, [Analitik İşler](https://analitikisler.com) Cloud sürümünün bir parçasıdı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.20.1",
4
+ "version": "0.21.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
@@ -237,7 +237,7 @@ export const journeyError = (err) => `The journey stopped with an error: ${err?.
237
237
  export function failedRun(journey, viewport, err, providers) {
238
238
  return {
239
239
  journeyId: journey.id, viewport, completed: false, note: journeyError(err), hits: [], steps: [], consentStep: null, rejectStep: null,
240
- cookieLog: [], dataLayerLog: [], network: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
240
+ cookieLog: [], cookieChanges: [], dataLayerLog: [], network: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
241
241
  };
242
242
  }
243
243
 
@@ -251,6 +251,10 @@ export function collect(session, journey, viewport) {
251
251
  consentStep: session.consentStep,
252
252
  rejectStep: session.rejectStep,
253
253
  cookieLog: session.cookieLog,
254
+ cookieChanges: session.cookieChanges,
255
+ cmp: session.cmp,
256
+ // Hits let through to the real accounts (block_hits false, or a live MCP audit): the reports say so.
257
+ ...(session.opts.blockHits === false ? { live: true } : {}),
254
258
  dataLayerLog: session.dataLayerLog,
255
259
  network: session.network,
256
260
  providers: session.providers,
package/src/browser.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { createRequire } from "node:module";
3
4
  import { readFile, rm, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
@@ -7,6 +8,8 @@ import { parseTrackingRequest, isCollectionEndpoint, providerOf, vendorOf, resol
7
8
  import { redactBody, redactParams, redactPush, redactUrl } from "./redact.js";
8
9
  import { hostScope, isPrivateAddress, navigationError, privateHostChecker } from "./guard.js";
9
10
  import { startEgressProxy } from "./egress.js";
11
+ import { campaignParamsIn } from "./campaign.js";
12
+ import { detectCmpInPage } from "./cmp.js";
10
13
 
11
14
  // Consent banners of the common consent platforms (CMPs), by their container. A cover that matches
12
15
  // one of these always blocks the journey, and a reject click is checked against them afterwards.
@@ -26,7 +29,9 @@ export function installBrowser() {
26
29
  export const CMP_BANNER_SELECTORS = `#onetrust-banner-sdk, #onetrust-consent-sdk, #CybotCookiebotDialog, #usercentrics-root, #didomi-host, #cookiescript_injected, .cky-consent-container, .cc-window, #cmpbox, .qc-cmp2-container, \
27
30
  #truste-consent-track, #consent_blackbar, .osano-cm-window, #iubenda-cs-banner, .iubenda-cs-container, #cmplz-cookiebanner-container, .cmplz-cookiebanner, #cookiefirst-root, .cookiefirst-root, #axeptio_overlay, \
28
31
  #sp_message_container, [id^="sp_message_container_"], #cookie-law-info-bar, #BorlabsCookieBox, .klaro, #termly-code-snippet-support, #cookie-information-template-wrapper, #coiOverlay, \
29
- #cmpwrapper, #cmpbox2, .fc-consent-root, #ppms_cm_popup_overlay, [aria-label*="cookie" i][role=dialog]`;
32
+ #cmpwrapper, #cmpbox2, .fc-consent-root, #ppms_cm_popup_overlay, [aria-label*="cookie" i][role=dialog], \
33
+ [data-name^="Banner"][efl-consent-not-saved]`;
34
+ // Efilli draws its banner inside the open shadow root of <efilli-layout-dynamic>, so the checks below also look inside open shadow roots.
30
35
 
31
36
  // Caps on what one audit records, so a looping page can't grow memory without limit.
32
37
  const LOG_LIMITS = { hits: 5000, dataLayerLog: 5000, network: 10000 };
@@ -113,7 +118,7 @@ const INIT_SCRIPT = `(() => {
113
118
  // and form fields, it finds custom controls that sites build from divs and spans
114
119
  // (role attributes, tabindex, or a pointer cursor). Elements on screen come first,
115
120
  // so a long menu or footer can't push the button the agent needs out of the list.
116
- function snapshotInPage({ first, limit, textLimit, frameBox }) {
121
+ function snapshotInPage({ first, limit, textLimit, frameBox, offscreenLimit = null }) {
117
122
  const visible = (el) => {
118
123
  const r = el.getBoundingClientRect();
119
124
  if (r.width <= 0 || r.height <= 0) return false;
@@ -187,8 +192,11 @@ function snapshotInPage({ first, limit, textLimit, frameBox }) {
187
192
  .map((el, order) => ({ el, order, tier: tierOf(el) }))
188
193
  .sort((a, b) => a.tier - b.tier || a.order - b.order);
189
194
  const firstCovered = ranked.findIndex((x) => x.tier === 2);
195
+ let offscreen = 0;
190
196
  const items = ranked
191
197
  .filter((item, i) => item.tier < 2 || i - firstCovered < 30) // only a few covered or clipped ones
198
+ // A compact view keeps a few elements above or below the screen; scrolling brings the rest on screen.
199
+ .filter((item) => item.tier !== 1 || offscreenLimit === null || offscreen++ < offscreenLimit)
192
200
  .slice(0, limit)
193
201
  .map((item) => ({ ...item, onScreen: item.tier === 0 }));
194
202
  const lines = [];
@@ -377,6 +385,72 @@ export function isTransientNavigationError(err, failedLoad) {
377
385
  return code === "timeout" || TRANSIENT_NET_ERRORS.test(code);
378
386
  }
379
387
 
388
+ /**
389
+ * What a step's analytics and ad cookies say, without keeping their values: the GA4 client id from _ga
390
+ * (the cid GA4 hits carry anyway), whether _gcl_aw and _fbc hold the click ids the landing page was opened
391
+ * with ("landing") or other ones ("other"), and whether _gcl_au and _fbp exist. Null when none of them do.
392
+ */
393
+ export function identityOf(cookies, landingUrl) {
394
+ const value = (name) => cookies.find((c) => c.name === name)?.value ?? "";
395
+ const sent = landingUrl ? campaignParamsIn(landingUrl) : {};
396
+ const out = {};
397
+ const ga = /^GA\d\.\d+\.(\d+\.\d+)$/.exec(value("_ga"));
398
+ if (ga) out.ga_client_id = ga[1];
399
+ // _gcl_aw is GCL.<time>.<gclid>; _fbc is fb.<n>.<time>.<fbclid>.
400
+ const aw = /^GCL\.\d+\.(.+)$/.exec(value("_gcl_aw"));
401
+ if (aw) out.gcl_aw = aw[1] === sent.gclid ? "landing" : "other";
402
+ const fbc = /^fb\.\d+\.\d+\.(.+)$/.exec(value("_fbc"));
403
+ if (fbc) out.fbc = fbc[1] === sent.fbclid ? "landing" : "other";
404
+ if (value("_gcl_au")) out.gcl_au = true;
405
+ if (value("_fbp")) out.fbp = true;
406
+ return Object.keys(out).length ? out : null;
407
+ }
408
+
409
+ /**
410
+ * Runs in the page: the Google tag's own consent state (google_tag_data.ics), or null before a Google tag loaded.
411
+ * Per consent type the default and update it holds, and whether it applied a default and an update at all, whether
412
+ * the default came after a tag had already read consent (wasSetLate), and whether wait_for_update ran out. It is how
413
+ * the tag sees consent, wherever it was set: gtag() in the page, or a consent platform's GTM template, which never
414
+ * shows in the dataLayer. Not documented by Google; read defensively.
415
+ */
416
+ export function googleConsentStateInPage() {
417
+ const ics = window.google_tag_data?.ics;
418
+ if (!ics || typeof ics !== "object") return null;
419
+ const word = (v) => (v === true ? "granted" : v === false ? "denied" : undefined);
420
+ const entries = {};
421
+ for (const [type, e] of Object.entries(ics.entries ?? {})) {
422
+ if (!e || typeof e !== "object") continue;
423
+ const d = word(e.default);
424
+ const u = word(e.update);
425
+ entries[type] = { ...(d ? { default: d } : {}), ...(u ? { update: u } : {}) };
426
+ }
427
+ return { entries, usedDefault: Boolean(ics.usedDefault), usedUpdate: Boolean(ics.usedUpdate), wasSetLate: Boolean(ics.wasSetLate), waitPeriodTimedOut: Boolean(ics.waitPeriodTimedOut) };
428
+ }
429
+
430
+ /**
431
+ * Runs in the page: whether it is something other than the site, so what a step captured there says nothing about
432
+ * the site's tracking. A bot challenge in its place (Cloudflare, DataDome, HUMAN, Akamai), or a page that crashed in
433
+ * the browser (the error screen a Next.js app shows when its scripts fail). Null for a page that looks like itself.
434
+ * @returns {null | { kind: "challenge"|"crash", vendor?: string }}
435
+ */
436
+ export function pageProblemInPage() {
437
+ const title = (document.title || "").trim();
438
+ const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim().slice(0, 3000);
439
+ const has = (selector) => {
440
+ try {
441
+ return Boolean(document.querySelector(selector));
442
+ } catch {
443
+ return false;
444
+ }
445
+ };
446
+ if (/^just a moment\.\.\.$|^attention required! \| cloudflare$/i.test(title) || has("#challenge-form, #challenge-running, #cf-challenge-running")) return { kind: "challenge", vendor: "Cloudflare" };
447
+ if (has('iframe[src*="captcha-delivery.com"]')) return { kind: "challenge", vendor: "DataDome" };
448
+ if (has("#px-captcha")) return { kind: "challenge", vendor: "HUMAN" };
449
+ if (/^access denied$/i.test(title) && /reference\s*#\s*[\d.a-f]+/i.test(text)) return { kind: "challenge", vendor: "Akamai" };
450
+ if (/^application error: a client-side exception has occurred/i.test(text)) return { kind: "crash", vendor: "Next.js" };
451
+ return null;
452
+ }
453
+
380
454
  export class AuditSession {
381
455
  /**
382
456
  * @param {{ viewport: string, site?: string, allowedHosts?: string[], providers?: string[], keepBodies?: boolean, blockPrivate?: boolean | ((ip: string) => boolean), blockHits?: boolean, headless?: boolean }} opts
@@ -406,6 +480,10 @@ export class AuditSession {
406
480
  this.consentStep = null;
407
481
  this.rejectStep = null;
408
482
  this.cookieLog = []; // each cookie name and domain with the step it was first seen at (never values)
483
+ this.cookieChanges = []; // { step, name, domain, change: added|changed|removed }, never values
484
+ this.cookieHashes = new Map(); // domain|name -> a hash of the value at the last step, kept in memory only
485
+ this.cmp = null; // the consent platform and its setup, from detectCmpInPage, with the step it was first seen at
486
+ this.refusedScripts = new Map(); // step -> how many of the page's scripts were refused for coming back as HTML
409
487
  this.errors = [];
410
488
  this.dataLayerLog = [];
411
489
  this.network = [];
@@ -455,6 +533,10 @@ export class AuditSession {
455
533
  await this.context.addInitScript(INIT_SCRIPT);
456
534
  this.page = await this.context.newPage();
457
535
  this.page.on("pageerror", (err) => this.errors.push({ step: this.currentStep, message: err.message }));
536
+ // A script that comes back as an HTML page (a bot check, or files a new deployment no longer has) is refused.
537
+ this.page.on("console", (msg) => {
538
+ 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);
539
+ });
458
540
  this.page.on("framenavigated", (frame) => {
459
541
  if (frame !== this.page.mainFrame()) return;
460
542
  if (frame.url().startsWith("chrome-error:")) this.errorPageShown = true;
@@ -521,7 +603,14 @@ export class AuditSession {
521
603
  });
522
604
  this.context.on("response", (response) => {
523
605
  const call = calls.get(response.request());
524
- if (call) call.status = response.status();
606
+ if (!call) return;
607
+ call.status = response.status();
608
+ // A consent platform's script or setup file date tells which build of its setup the site serves. Setup files
609
+ // (OneTrust's, Cookiebot's) are fetched by script rather than loaded as one.
610
+ if (call.provider === "consent" && ["script", "fetch", "xhr"].includes(call.type)) {
611
+ const lastModified = response.headers()["last-modified"];
612
+ if (lastModified) call.lastModified = lastModified;
613
+ }
525
614
  });
526
615
  this.context.on("requestfailed", (request) => {
527
616
  const call = calls.get(request);
@@ -783,20 +872,21 @@ export class AuditSession {
783
872
  }
784
873
  }
785
874
 
786
- async snapshot() {
875
+ /** @param {{ offscreenLimit?: number|null }} [opts] offscreenLimit: how many elements above or below the screen to list; null for all. */
876
+ async snapshot({ offscreenLimit = null } = {}) {
787
877
  const blocked = await this.enforce();
788
878
  if (blocked && this.page.url() === "about:blank") return { url: "about:blank", title: "", elements: "", text: `(${blocked})` };
789
879
  try {
790
880
  // The main page first, then iframes (booking and payment widgets often live in one).
791
881
  const frames = await this.visibleFrames();
792
- const main = await this.page.evaluate(snapshotInPage, { first: 1, limit: frames.length ? MAX_ELEMENTS - 60 : MAX_ELEMENTS, textLimit: 3000 });
882
+ const main = await this.page.evaluate(snapshotInPage, { first: 1, limit: frames.length ? MAX_ELEMENTS - 60 : MAX_ELEMENTS, textLimit: 3000, offscreenLimit });
793
883
  this.refFrames = new Map();
794
884
  const lines = [...main.lines];
795
885
  let next = main.next;
796
886
  let more = main.more;
797
887
  for (const { frame, frameBox } of frames) {
798
888
  if (next > MAX_ELEMENTS) break;
799
- const sub = await frame.evaluate(snapshotInPage, { first: next, limit: MAX_ELEMENTS - next + 1, textLimit: 0, frameBox }).catch(() => null);
889
+ const sub = await frame.evaluate(snapshotInPage, { first: next, limit: MAX_ELEMENTS - next + 1, textLimit: 0, frameBox, offscreenLimit }).catch(() => null);
800
890
  if (!sub?.lines.length) continue;
801
891
  lines.push(`(inside an embedded frame from ${new URL(sub.url).host})`, ...sub.lines);
802
892
  for (let r = next; r < sub.next; r++) this.refFrames.set(r, frame);
@@ -940,7 +1030,7 @@ export class AuditSession {
940
1030
  */
941
1031
  async act(a) {
942
1032
  const error = await this.perform(a);
943
- await this.trackCookies();
1033
+ await this.observe();
944
1034
  // A scripted replay of this step may fail the same way, which must not end the journey there.
945
1035
  if (error) this.steps[this.currentStep].error = error;
946
1036
  this.steps[this.currentStep].pagePath = pagePath(this.page.url());
@@ -948,6 +1038,28 @@ export class AuditSession {
948
1038
  return error;
949
1039
  }
950
1040
 
1041
+ /**
1042
+ * What the step just taken left behind, beyond its requests: the cookies it changed, the consent platform the
1043
+ * first time one is seen, and the Google tag's consent state when it changed. act() calls it after every action;
1044
+ * the consent scenarios call it after the steps they take themselves.
1045
+ */
1046
+ async observe() {
1047
+ await this.trackCookies();
1048
+ if (!this.cmp) {
1049
+ const cmp = await this.evaluateWithin(detectCmpInPage);
1050
+ if (cmp) this.cmp = { ...cmp, step: this.currentStep };
1051
+ }
1052
+ const problem = await this.evaluateWithin(pageProblemInPage);
1053
+ if (problem) {
1054
+ const refused = this.refusedScripts.get(this.currentStep) ?? 0;
1055
+ this.steps[this.currentStep].pageProblem = { ...problem, ...(refused ? { refusedScripts: refused } : {}) };
1056
+ }
1057
+ // Kept on the step only when it changed, so the steps that hold one are where the Google tag's state moved.
1058
+ const tagConsent = await this.evaluateWithin(googleConsentStateInPage);
1059
+ const before = this.steps.slice(0, this.currentStep).map((st) => st.tagConsent).filter(Boolean).at(-1);
1060
+ if (tagConsent && JSON.stringify(tagConsent) !== JSON.stringify(before)) this.steps[this.currentStep].tagConsent = tagConsent;
1061
+ }
1062
+
951
1063
  async perform(a) {
952
1064
  const describe = a.reason || `${a.action} ${a.selector ?? (a.ref != null ? `#${a.ref}` : a.text != null ? `"${a.text}"` : a.url ?? "")}`.trim();
953
1065
  this.beginStep(describe, { action: a.action, acceptsConsent: !!a.accepts_consent, rejectsConsent: !!a.rejects_consent });
@@ -1021,10 +1133,14 @@ export class AuditSession {
1021
1133
  if (a.rejects_consent && this.rejectStep === null) {
1022
1134
  this.rejectStep = this.currentStep;
1023
1135
  // A rejected banner should close; one that is still showing means the button did nothing.
1024
- step.bannerOpenAfter = await this.page.evaluate((sel) => [...document.querySelectorAll(sel)].some((n) => {
1025
- const r = n.getBoundingClientRect();
1026
- return r.width > 0 && r.height > 0 && getComputedStyle(n).visibility !== "hidden";
1027
- }), CMP_BANNER_SELECTORS).catch(() => false);
1136
+ step.bannerOpenAfter = await this.page.evaluate((sel) => {
1137
+ const roots = [document];
1138
+ for (let i = 0; i < roots.length; i++) for (const n of roots[i].querySelectorAll("*")) if (n.shadowRoot) roots.push(n.shadowRoot);
1139
+ return roots.flatMap((r) => [...r.querySelectorAll(sel)]).some((n) => {
1140
+ const r = n.getBoundingClientRect();
1141
+ return r.width > 0 && r.height > 0 && getComputedStyle(n).visibility !== "hidden";
1142
+ });
1143
+ }, CMP_BANNER_SELECTORS).catch(() => false);
1028
1144
  }
1029
1145
  return null;
1030
1146
  } catch (err) {
@@ -1032,20 +1148,56 @@ export class AuditSession {
1032
1148
  }
1033
1149
  }
1034
1150
 
1035
- /** Notes which cookies appeared during the step just taken (names and domains only). */
1151
+ /**
1152
+ * Notes the cookies the step just taken added, changed or removed, and what the analytics and ad cookies
1153
+ * say (identityOf). Values never leave this function: a short hash of each, held until the next step,
1154
+ * is enough to tell that one changed.
1155
+ */
1036
1156
  async trackCookies() {
1157
+ const all = await this.rawCookies();
1037
1158
  const seen = new Set(this.cookieLog.map((c) => `${c.domain}|${c.name}`));
1038
- for (const c of await this.cookies()) {
1039
- if (!seen.has(`${c.domain}|${c.name}`) && this.cookieLog.length < 500) this.cookieLog.push({ ...c, step: this.currentStep });
1159
+ const now = new Map();
1160
+ for (const c of all) {
1161
+ const k = `${c.domain}|${c.name}`;
1162
+ const hash = createHash("sha256").update(c.value).digest("base64").slice(0, 16);
1163
+ now.set(k, hash);
1164
+ if (!seen.has(k) && this.cookieLog.length < 500) this.cookieLog.push({ name: c.name, domain: c.domain, step: this.currentStep });
1165
+ const before = this.cookieHashes.get(k);
1166
+ const change = before === undefined ? "added" : before !== hash ? "changed" : null;
1167
+ if (change && this.cookieChanges.length < 2000) this.cookieChanges.push({ step: this.currentStep, name: c.name, domain: c.domain, change });
1040
1168
  }
1169
+ for (const k of this.cookieHashes.keys()) {
1170
+ if (now.has(k) || this.cookieChanges.length >= 2000) continue;
1171
+ const [domain, ...name] = k.split("|");
1172
+ this.cookieChanges.push({ step: this.currentStep, name: name.join("|"), domain, change: "removed" });
1173
+ }
1174
+ this.cookieHashes = now;
1175
+ const identity = identityOf(all, this.landing?.requested);
1176
+ if (identity) this.steps[this.currentStep].identity = identity;
1041
1177
  }
1042
1178
 
1043
- async cookies() {
1179
+ /** page.evaluate(fn), or null when it fails or the page does not answer within ms: a page stuck in a script never answers, and the next page read reports that. */
1180
+ async evaluateWithin(fn, ms = 2000) {
1181
+ let timer;
1182
+ const result = await Promise.race([
1183
+ this.page.evaluate(fn).catch(() => null),
1184
+ new Promise((resolve) => (timer = setTimeout(() => resolve(null), ms))),
1185
+ ]);
1186
+ clearTimeout(timer);
1187
+ return result;
1188
+ }
1189
+
1190
+ /** The browser's cookies with their values, for trackCookies only; [] when the browser takes too long. */
1191
+ async rawCookies() {
1044
1192
  let timer;
1045
1193
  const tooLong = new Promise((resolve) => (timer = setTimeout(() => resolve([]), 10_000)));
1046
1194
  const cookies = await Promise.race([this.context.cookies().catch(() => []), tooLong]);
1047
1195
  clearTimeout(timer);
1048
- return cookies.map((c) => ({ name: c.name, domain: c.domain }));
1196
+ return cookies;
1197
+ }
1198
+
1199
+ async cookies() {
1200
+ return (await this.rawCookies()).map((c) => ({ name: c.name, domain: c.domain }));
1049
1201
  }
1050
1202
 
1051
1203
  async close() {
package/src/campaign.js CHANGED
@@ -19,10 +19,13 @@ export function campaignParamsIn(url) {
19
19
  }
20
20
  }
21
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;
22
+ /**
23
+ * The address with test campaign parameters added, unless it already carries campaign parameters.
24
+ * Given parameters of the caller's own (a real campaign's UTM values, a gclid), those are set instead, always.
25
+ */
26
+ export function withCampaignParams(url, own = null) {
27
+ if (!own && Object.keys(campaignParamsIn(url)).length) return url;
25
28
  const u = new URL(url);
26
- for (const [k, v] of Object.entries(TEST_PARAMS)) u.searchParams.set(k, v);
29
+ for (const [k, v] of Object.entries(own ?? TEST_PARAMS)) u.searchParams.set(k, v);
27
30
  return u.href;
28
31
  }