# Consent test matrix

How to build the region-by-choice matrix, run each case so the result is repeatable, and decide what counts as a pass. The working file is [assets/consent-test-matrix.csv](../assets/consent-test-matrix.csv).

## Five-minute first check (triage, not sign-off)

Finds the obvious faults before the full matrix. It never replaces it.

1. Fresh profile, egress in the main opt-in region, Tag Assistant connected, Network panel filtered on `gcs=`.
2. Before touching the banner: the earliest Consent event lists all four signals with the specified defaults; no `_ga`, `_gcl_*` or `_fbp` cookies; Google requests either absent (basic) or carrying denied state (advanced).
3. Reject: no new vendor requests or cookies.
4. New profile, accept: the next Google requests carry granted state and each blocked tag fires once on that page.
5. Reload: the first Google request already carries the stored state.
6. Reopen preferences and withdraw: later requests carry denied state and vendors stop.

Any deviation means running the full matrix below for that region.

## How the agent runs each case

Pick the mode from the tools available, say which one in the write-up, and use the same mode for every run of a case.

- Mode A, the agent has its own browser (an agent-owned browser tool or Playwright). Never drive the user's everyday browser or profile.
  - One fresh browser context per case and per run, with no stored state.
  - Region comes from a proxy or VPN egress only. Geolocation emulation does not change the IP that the CMP and Google see.
  - Capture the network log as a HAR and read cookies through the tool's cookie API, which includes HttpOnly cookies and their flags.
  - Run [scripts/consent-snapshot.js](../scripts/consent-snapshot.js) through the tool's page evaluate call at each checkpoint.
  - For sign-off runs, click the real banner buttons. Calling the CMP's JavaScript API is acceptable for triage only, because it skips the banner code that visitors run.
  - Throttle through the tool's network and CPU emulation for the race runs.
- Mode B, no browser tool. The user runs the cases and the agent analyses.
  - Give the user the exact steps for each case from "Running one case" below, one case at a time.
  - Ask for the HAR (with cookies included in the export), the snapshot output, the Application > Cookies list with the HttpOnly column, and a screenshot or copy of the Tag Assistant Consent tab.
  - Analyse every HAR with [scripts/har-consent-extract.mjs](../scripts/har-consent-extract.mjs), then fill the observed columns.
- In both modes, the agent reads the timeline from the extractor output, not by scrolling hundreds of requests: match each `gcs` and `gcd` change to the Consent events, and find the first vendor event before the CMP configuration response.

## Rows: regions

Add one row group per distinct rule in the specification, and per separate configuration row in the CMP or the Google defaults (a branch that is configured separately can break separately, even when its values match). Typical groups:

- R1: the main opt-in region (for example `DE`).
- R2: `GB`, when its rule or its configuration row is separate from R1.
- R3: an opt-out regime region (for example `US-CA`) when the banner, defaults or a Global Privacy Control rule differ there.
- R4: rest of the world (the default with no region).
- R5: unknown location. Use an egress the CMP cannot place. If that is not possible, mark R5 "not run" and say why.

Rules:

- Region codes: ISO 3166-1 alpha-2 for countries and ISO 3166-2 for subdivisions. Google and Cookiebot both document this. `UK` is not a valid country code; `GB` is.
- When two Google defaults overlap, the more specific region wins (`US-CA` over `US`).
- Before a case counts, confirm the CMP placed you in the intended region. Cookiebot exposes `Cookiebot.regulations.gdprApplies`, `ccpaApplies` and `lgpdApplies`; a `CookieConsent` cookie value of `-1` means the visitor is outside any region that requires consent and every category is accepted. A test run from the wrong egress looks like a perfect "accept all".
- A CMP location override (a test geolocation parameter or setting) changes only which banner rule the CMP applies. Google's region defaults still follow the IP that Google sees. Use an override for banner and category behaviour only; a Google region default is proven only from a matching IP egress.
- Record two things for every region case: the CMP's region flag and the default letter in the first request's `gcd` (`p` denied, `t` granted). In basic mode the first request comes after the choice, so read the default half of the letter (`q` or `r` mean denied by default, `u` or `v` granted). They must agree with each other and with the specification.
- A region the specification does not define is a specification gap, not a test row. List it for the privacy owner in the write-up.

## Columns: choices

- C0: no interaction.
- C1: reject all.
- C2: accept all.
- C3: analytics only (statistics granted, marketing denied).
- C4: marketing only, if the banner offers it.
- C5: returning visitor with a stored reject (reload, then a new page).
- C6: returning visitor with a stored accept.
- C7: accept, then withdraw.
- C8: reject on page 1, accept on page 3.
- C9: make a choice, then convert within a few seconds (lead form or purchase). Runs only under the conversion gate below.
- C10: dismiss or close the banner (X, Escape, click outside). Expected to behave as reject unless the specification says otherwise. Any grant is S1.
- C11: navigate or scroll with no choice. Any grant recorded without an explicit choice (for Cookiebot, `Cookiebot.consent.method` `implied`) in an opt-in region is S1, reported to the privacy owner as a specification conflict.
- C12: convert before any choice. At the conversion, consent is still in its default state; gated vendors send no lead or purchase event; nothing is replayed after a later grant (watch TikTok `holdConsent` and Amplitude's pre-`init` queue). Runs only under the conversion gate.
- C13: Global Privacy Control on. Check `navigator.globalPrivacyControl` is `true` and requests carry the `Sec-GPC: 1` header. Expected: the CMP records the opt-out and the tags behind sale or sharing stay off, as the specification says.
- C14: CMP blocked (DevTools request blocking on the CMP host). Expected: defaults stay denied, no optional tag sends, and the site stays usable.

Every reject case (C1, C5, C7 after withdrawal, C10, C14) also checks that the essential journey still completes: the form submits, checkout reaches payment, login works. A broken essential journey is graded like S1, with the site owner as owner.

Mark a cell N/A when the region's banner does not offer that choice, with the reason.

## Conversion and click ID gate

C9, C12 and any landing URL with a test click ID write to live systems: CRM records that can trigger sales follow-up, orders, conversions that Google Ads and Meta bid on, and GA4 sessions labelled `google / cpc`. They run only after the site owner approves in writing. Before running, record:

- Where it runs: staging or a GTM environment first; production only if the approval names it.
- The marker on every test record, for example name "Consent Test C9" and `jane+consent-test@example.com`.
- How test conversions are kept out of bidding and reports, agreed with the ads and analytics owners (for example a test conversion action, Meta's Test Events tool, or an exclusion or adjustment after the run). Confirm the mechanism with the platform owner at run time.
- Who deletes the CRM and order records afterwards, and by when.
- Purchases use the platform's test payment method only.

Without written approval, mark C9 and C12 "not run: conversion gate not approved", and leave the click ID off the landing URL.

## Minimum set before sign-off

- Every region: C0, C1, C2, C6, C7.
- Every region with a banner: add C10, and C11 when the CMP offers an implied-consent setting.
- The main opt-in region: add C3, C5, C8 and C14.
- The main conversion journey: add C9 and C12, once the gate is approved.
- R3 (opt-out regime): add C13.
- Race-sensitive cases (C0, C6, C9, C12): three runs each, at least one with throttling. Pass only on three of three. Intermittent failures are the normal shape of a consent race.
- Any minimum-set case not run caps the verdict at Incomplete (see the write-up rules).

## Running one case

1. Open a fresh browser profile with no stored consent and no ad blocker. Set the egress. Confirm the CMP's region flag.
2. Open DevTools Network with "Preserve log" on and cache disabled. Connect Tag Assistant.
3. Load the landing URL, for example `https://www.example.com/pricing?utm_source=consent-test`. Add a test click ID (`&gclid=TEST-CLICK`) only once the conversion gate is approved; it makes `url_passthrough` and Ads behaviour visible.
4. Before touching the banner: run `scripts/consent-snapshot.js`, read the earliest Consent event in Tag Assistant, list cookies from Application > Cookies with the HttpOnly column.
5. Perform the choice. Run the snapshot again on the same page.
6. Navigate to a second page. Go through the conversion journey only under the approved gate, with the agreed test markers. Snapshot on each page.
7. Save the HAR as `<case>-run<n>.har` and run `scripts/har-consent-extract.mjs` on it. Record observed results in the CSV next to the expected ones.
8. Close the profile. Never reuse it for another case.

## Expected patterns to copy into the matrix

Fill from the specification; these are the usual shapes.

- Opt-in region, advanced mode, C0: Google requests with `gcs=G100` and `gcd` letters showing denied defaults; no Google, Meta or TikTok cookies; gated non-Google vendors send nothing (TikTok may queue under `holdConsent` if the specification allows it).
- Opt-in region, basic mode, C0: no Google measurement requests at all.
- C2: the next requests show `G111`; vendor cookies appear; each tag that was blocked fires once on the same page.
- C3: `G101` style state, `ad_user_data` and `ad_personalization` denied, advertising vendors silent.
- C6: the first Google request on the first page already carries the stored grant.
- C7: an update to denied on the same page; later requests denied; gated vendors stop.
- C9: the conversion request carries the state chosen seconds earlier and is not lost to the navigation.
- C10: same as C1 unless the specification says otherwise.
- C12: the conversion request carries the default state; gated vendors send nothing for it, then or after a later grant.
- C14: same as C0 with no banner; the essential journey completes.
- Microsoft UET: `asc=D` or no UET events before a grant and after a reject; `asc=G` on every event after a grant.
- Rest of world with a granted default: granted state from the first request, possibly no banner.

## Timing and race checks

- Throttle with a DevTools network preset and add CPU slowdown. Late banners and slow consent restores only show up here.
- From the extractor output, note the start times of: the CMP script, the CMP configuration response, the first Google request, the first non-Google vendor request. Any vendor request before the CMP configuration response needs an owner.
- Returning visitors (C5, C6): decide on the outcome, not on a timer.
  - Pass after a stored grant: the first Google request's `gcd` letters show `r` or `v` (update granted), not `p` (default only), and Tag Assistant shows the update before the first tag.
  - Pass after a stored reject: `q` or `u` (update denied), or `p` if the CMP sends no update when the stored choice equals the default. Never `r`, `t` or `v` in an opt-in region.
  - Google documents `wait_for_update` only as how long tags wait before sending data; it does not document when the wait starts. If you report timing, measure from the consent default and label it approximate.
  - A stored choice should be applied at Consent Initialization from the CMP's own cookie, before any tag fires. When it is not, find out why (a CMP outside GTM, an asynchronous restore, a hardcoded tag that runs first) before anyone proposes a longer `wait_for_update`, which delays every Google tag for every visitor.
- Google requires the update on the page where the choice is made, before any page transition, and warns that updates sent during unload may be cancelled by the browser. C9 tests exactly this.

## Journeys that hide failures

- Single-page apps: route changes after a choice must carry the new state. Cookiebot's `runScripts()` re-evaluates marked-up scripts for dynamically loaded content; check whether the site calls it.
- Cross-domain hops and separate checkout domains: consent may not carry across. The expected behaviour on the second domain belongs in the specification.
- Embedded forms, schedulers, chat and video: they set their own cookies and are often loaded late or in iframes. Exercise them on their real routes.
- Scanners do not cover these paths. Cookiebot documents that its scanner fills shopping carts but does not submit forms or complete payments.

## Browsers

- Use a Chromium-based browser as the baseline for DevTools and Tag Assistant.
- Add Safari for returning-visitor cases where the audience justifies it. OneTrust documents that Safari and iOS visitors are prompted for preferences every seven days, so a "returning visitor" there may see the banner again and should be tested as such.
