# Capture protocol: browser evidence without side effects or leaks

What leaves the browser is the primary evidence of a tracking audit. This file says how to capture it so that it can be trusted, repeated and shared. Tool facts were checked on 2026-11-06 against the Playwright MCP README and source, Playwright's recordHar reference and the Chrome DevTools MCP tool reference; tool parameters change, so read the current tool reference before relying on a name.

## The rig

- Mode A, the agent drives its own browser. It runs the scenarios, reads cookie names, snapshots the data layer and saves the request lists as supporting evidence. Chrome DevTools MCP cannot save a HAR; Playwright MCP records one only when `recordHar` is set in its config (see Getting HAR files). Neither tool's request list carries start times, so a list alone is never har-audit input.
  - Playwright MCP started with `--isolated`, which keeps the profile in memory so each session starts clean. Start it with `--proxy-server` pointing at the regional egress, and with `--blocked-origins` listing the ad conversion hosts when ads are not under test (the README says the list is not a security boundary, so it is test hygiene, not protection). Save each scenario's request list with `browser_network_requests` and `static: true`, `all: true` and `filename`. `static` defaults to false, which drops successful images, scripts and documents, so image beacons (Meta /tr, LinkedIn collect, UET, HubSpot __ptq.gif), loader scripts and redirect hops would be missing. Read one request's headers and body with `browser_network_request`. It has no bandwidth throttling.
  - Chrome DevTools MCP: open each run with `new_page` and a new `isolatedContext` name, because pages that share a context name share cookies and storage. `list_network_requests` returns requests since the last navigation, and `includePreservedRequests` covers the last 3 navigations, so save the list after each navigation on longer journeys. `get_network_request` returns headers including Cookie and Set-Cookie (treat its output as sensitive) and saves bodies with `requestFilePath`. Throttle with `emulate` and `networkConditions` set to Slow 4G.
- Mode B, a person captures: the user, or a colleague with the approvals, exports HAR files with the steps below. This is the route for the throttled run, and for every run when Playwright MCP recording is not set up.
- Cookie names: take them from the Set-Cookie and Cookie header names in a request's details, or the user reads them from the Application panel. `document.cookie` misses HttpOnly cookies such as FPID, so it can never pass CON-04 alone. Record names, never values.
- Never the user's everyday browser profile, saved sessions, password manager or extensions. A logged-in profile carries real identities into vendor requests and turns test evidence into personal data.
- No ad blockers or privacy extensions in the test browser; record the browser and version, because Safari and Firefox protections change what tags can do.
- Region comes from IP egress (a VPN or proxy in the region). Geolocation emulation alone does not change the IP the CMP sees. Record the egress region for every file.
- Times in UTC. Cache disabled and the log preserved across navigations.

## Getting HAR files

har-audit reads HAR 1.2 files and needs the real `startedDateTime` of every request: the consent phase, the duplicate windows and the route pairs depend on it. It refuses a file with missing times or with one time shared by every entry.

- Chrome or Edge: open a fresh test profile or a guest window, open DevTools on the Network panel, tick Preserve log and Disable cache, run the scenario, then export with the download control in the Network panel's action bar. Chrome labels the default export "Export HAR (sanitized)"; it drops Cookie, Set-Cookie and Authorization headers. A HAR "with sensitive data" needs a DevTools setting, explicit approval, a fresh test profile, and storage with the project only.
- Playwright as a library, where the team already runs it in tests, can record a HAR with the browser context's `recordHar` option.
- Playwright MCP can record har-audit input. The user adds `--isolated --config <file>` to the Playwright MCP server entry once per audit. The file sets `{"browser": {"contextOptions": {"recordHar": {"path": "<absolute project path>/tracking-audit/<date>/captures/current.har.json"}}}}`; use an absolute path, because the server's working directory may not be the project. Playwright writes the HAR with real request times when the context closes, so call `browser_close` at the end of each run. Rename the file to its evidence ID after each run and keep one scenario per browser session. On the first run confirm that the file appears, that har-audit accepts it and that the next tool call opens a fresh session; if it does not, restart the server between runs. A recorded HAR keeps cookie headers, so treat it as a sensitive export from a fresh test session, and it has no initiators, so read loaders from the page source and container. Playwright MCP cannot throttle, so the throttled run still comes from DevTools.
- Never build a HAR from MCP request lists or fill in times by hand: invented or copied times silently move a request from CON-02 to CON-03 or hide a duplicate.
- For long journeys, ask the user for an exported HAR even in Mode A, unless Playwright MCP recording is on.
- If no HAR can be exported, apply the checks by hand from the saved request lists (saved with `static: true` on Playwright MCP, before and after each click, so order shows the phase), label them "script not run", and mark the checks that need request times (duplicate windows, route pairs, the 2-second page view pairs) NOT_CHECKED with that reason.
- One file per scenario and run, named after its evidence ID: `E-01_2026-10-28_no-choice_home-pricing_run1.har.json`. Keep them in the project's audit folder (`tracking-audit/<date>/captures/`), never in the skill folder.

## Scenarios

Run each in a fresh context. Never mix scenarios in one file or one har-audit run.

- No choice: load the page, wait 30 seconds without touching the banner, make one internal navigation, wait again. This is the opt-in test: nothing optional may be sent or stored.
- Reject: click the CMP's reject all control on the first page, visit a second page, then start the essential journey (form, signup or checkout) to prove it still works. har-audit marks the moment of choice from the CMP's logging POST; pass the CMP host with `--cmp-hosts` unless it is a known CMP. Record the click time for each run. If the capture line's choice time differs from it by more than 2 seconds, or the line says the time was taken from another allowed host, rerun with `--choice-at` (seconds from the first request to the click). Some CMPs log the choice with a GET or not at all: `--choice-at` again.
- Accept: click accept all on the first page, then visit a second page. Capture journeys (forms, signup, checkout) in their own files under `--scenario journey`, also after accept, so a conversion never sits inside a consent capture, where har-audit would rate it CON-06 FAIL.
- Opt-out regions: when the privacy owner's decision grants signals by default, pass them with `--granted-defaults`. Requests those grants allow before a choice print as INFO for review. Run reject as the CMP's opt-out of sale or sharing, and once more with Global Privacy Control on: confirm `navigator.globalPrivacyControl` is true in the page and requests carry `Sec-GPC: 1` (a browser tool that only adds the header leaves the property a CMP may read unset). Vendors behind sale or sharing must then send nothing (CON-03). Never pass `--granted-defaults` for an opt-in region.
- No banner prompt in the documented model: run no-choice with the documented defaults, test the opt-out mechanism the decision names as the reject case, and mark CON-05 NA with the decision reference.
- Race-sensitive cases (consent, SPA routing, cross-domain) run three times, one of them throttled. A check passes only on 3 of 3 runs.
- Tag Assistant: the agent never signs in. A person runs one session on the first page of each region in a separate test profile and shares a screenshot of the Consent tab for the first event, which shows the default and update state of all four signals (CON-01). Without it, read the first request's `gcd` and `google_tag_data.ics` with consent-debugging's consent-snapshot.js in the agent's browser (undocumented, a supplement only) and say so in notes.

## Page types and journeys

- Capture each page type in scope: landing, pricing or product, lead form, checkout start, confirmation, app entry, and any page reached from a third party (payment, booking, sign-in).
- Choose landing routes from the Google Ads read in [platform-reads.md](platform-reads.md#google-ads-google-ads-mcp-server): the final URLs carrying most paid clicks, plus every distinct host among them (landing page builders, locale sites). Build each test URL from the account, campaign or ad tracking template and final URL suffix returned, then put the markers where the ValueTrack or dynamic values would be. Never request an ad click URL (for example a Google `aclk` redirect): that records a click against a live campaign. If the tracking template starts with a third-party click tracker, trace from the hop after it and record that the first hop was not requested.
- Test every landing route the ads use: with and without a trailing slash, locale prefixes, http, the bare domain, and any short link. Redirects that rebuild the URL are the commonest place markers die.
- Include every embedded scheduler or form in the journeys and record its host in the scope sheet. Pass its submission host with `--form-hosts` so its POST is judged as a form under CID-04; har-audit lists embedded frames and warns when a frame URL carries no marker.

## Markers and test data

- `gclid=TeSter-123` exactly, mixed case on purpose: a lowercasing step anywhere downstream shows up as `tester-123`.
- Other click IDs and the campaign carry the same prefix: `fbclid=TeSter-123-fb`, `msclkid=TeSter-123-ms`, `ttclid=TeSter-123-tt`, `li_fat_id=TeSter-123-li`, `oppref=TeSter-123-oa` (the OpenAI Ads click reference), `utm_campaign=TeSter-123-campaign`; `utm_source=tester` and `utm_medium=tester`.
- Test identities: `qa-audit-NN@example.com` and `+15555550100`, typed into forms and passed to har-audit with `--pii`.
- Test order references agreed with billing, for example `ACME-TEST-0042`.
- A synthetic marker tests your own redirects, forms and CRM. It cannot create an attributable ad click, and it must never enter a production conversion upload. Flag every record it creates.

## SPA route fixture

Write the expected page view count before running, then capture the fixture on its own or under one URL prefix and pass `--route-fixture host/path=N`. Template, adapted per app:

1. Direct load of a list route, for example /projects (expected: 1 page view).
2. Open a detail route, /projects/42 (expected: 1).
3. Change a filter that keeps the route (expected: 0 unless the plan says filters are page views).
4. Trigger the app's ready or route-complete notification twice (expected: 0).
5. Open another route, /projects/42/settings (expected: 1).
6. Browser back to /projects/42 (expected: 1).
7. Navigate to a route that fails or is cancelled by a guard (expected: 0).
8. Reload /projects/42 (expected: 1).

The template expects 5. Two page views for one location within 2 seconds mean two owners (enhanced measurement history changes plus a GTM history trigger or an app event). Also note titles and referrers on each virtual page view (XDS-06).

## Cross-domain runs

- Fresh context and a fresh click on the real link or button for every run: the linker parameter `_gl` expires after two minutes, so a copied URL fails by design.
- Run each boundary three times and record the GA4 client ID and session ID before and after; har-audit prints them per boundary.
- For third-party returns (payment, booking, sign-in), capture the outbound page, the third-party page and the return page in one file. har-audit judges the return under XDS-03 whether the host belongs to a third party or is an owned subdomain that cannot be tagged.

## HAR facts that change verdicts

- Sanitized exports drop Cookie and Set-Cookie headers. Cookies written by JavaScript never appear as Set-Cookie at all; they only show in later Cookie request headers of a sensitive export. Read cookie names as described in The rig, names only.
- Firefox, Safari and Playwright HARs have no `_initiator`, so loaders are unknown; read the page source and the container instead.
- Requests answered by a service worker can hide others (`_fetchedViaServiceWorker`); confirm with the service worker bypassed before calling a tag absent.
- Very large POST bodies can be missing from an export; check the entry before judging a payload.
- Session replay and some product analytics send compressed or binary bodies. har-audit cannot search them and raises PII-01 WARN, so confirm in the vendor's own debugger or a playback of the test session. Base64 values it can read are decoded and searched.
- A collector on an owned host (a CNAME, a proxied vendor path, a server container data client) looks like the site's own traffic. Pass the scope sheet's first-party collection endpoints with `--collector-hosts`; har-audit raises other known collector paths as candidates and lists the remaining own-host requests with data under not checked.
- GA4 can batch several events into one POST, one event per body line, sharing the URL parameters. Request count is not event count.
- A GA4 hit to `stats.g.doubleclick.net` is a Google signals copy of an event already sent (observed, not documented); har-audit searches it for consent state and personal data but never counts it as a second event or route.
- With page IDs in the export, a document that loads inside a page is a frame. har-audit lists third-party frames as embeds instead of treating them as page loads.

## Data layer snapshot

Read-only, in the page context (the browser tool's evaluate function or the console):

```js
(window.dataLayer || []).map((m, i) => ({ i, event: m && m.event, keys: m ? Object.keys(m) : [] }))
```

This shows structure without values. Take full values only on test journeys, and check that each ecommerce push follows a push that sets `ecommerce` to null (EVT-04).

## Published container read

The live container is a public file. Fetch it read-only and list tag functions and destination IDs (community knowledge: the format is not a documented contract):

```bash
curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX" -o tracking-audit/<YYYY-MM-DD>/captures/E-20_gtm.js
grep -oE '"function":"__[a-z_]+"' tracking-audit/<YYYY-MM-DD>/captures/E-20_gtm.js | sort | uniq -c
grep -oE '\b(G|AW|DC)-[A-Z0-9]{6,12}\b' tracking-audit/<YYYY-MM-DD>/captures/E-20_gtm.js | sort -u
```

Paused tags are absent, exception triggers are hard to read, and unpublished workspaces are invisible, so this is the light path for INV-08. The full review is gtm-container-audit on an export.

## Evidence ledger

One line per item, cited by ID in the checklist and the report:

- `E-nn: type (HAR, request list, script output, Tag Assistant record, MCP read, export, screenshot), file or tool call, scenario, page or journey, run number, UTC time, captured by, checks supported`.
- E-00 is the signed scope sheet. Name each file after its ID (`outputs/E-01.txt` for the har-audit output of capture E-01). Keep raw captures with the project, not in tickets or chat.

## Approvals and stop rules

- A named yes, with a date, before: clicking a consent banner on a live site, placing synthetic click IDs on live landing pages, submitting a test lead, creating a test signup or account (where, how many, cleanup owner), placing a test order, sending platform test events. Meta events sent with a test event code still reach ads measurement, so block ad conversion hosts in the test browser when ads are not under test.
- Stop when the container, property or account you can see is not the one in scope.
- Stop when a test would touch a real customer, a real payment or a live campaign without approval.
- Stop when real personal data appears in a vendor request: restrict the evidence and tell the user the same day, with a draft note for the privacy owner. Never send it yourself.
