Investigation
Watching what loads before the consent banner does
A consent banner is a piece of interface. What the page actually sends, stores and reads is a different record, and only one of the two can be read from a screenshot. This is a method for capturing the load sequence, and a hard boundary around what a technical observation can settle.

A measurement request left the browser 4.6 milliseconds into the page load. The banner asking whether measurement was wanted became visible about 148 milliseconds later.
Both of those are true at once, and only one of them is visible in a screenshot. This article is a method for reading the other one.
Everything below comes from a synthetic fixture I built and ran on a laptop: a local page on 127.0.0.1 carrying a banner component and two tag snippets, one loaded with async in the head and one gated behind the choice. No client site was observed. No third party was observed. No real product, vendor or platform is named here, favourably or otherwise, because the fixture is the evidence and a fixture cannot libel anyone.
| Order | Request or write | Host | Triggered by | Relative to the banner |
|---|---|---|---|---|
| 1 | GET / | 127.0.0.1 | Navigation | before it rendered |
| 2 | GET /style.css | 127.0.0.1 | Parser and preload scanner | before it rendered |
| 3 | GET /banner.js | 127.0.0.1 | defer script at the end of the body | before it rendered |
| 4 | GET /vendor/measure.js | 127.0.0.1 | async script in the head | before it rendered |
| 5 | Cookie fx_mid written | 127.0.0.1 | Set-Cookie on the measure.js response | before it rendered |
| 6 | localStorage key fx.visitor written | None, same origin | measure.js executing | before it rendered |
| 7 | GET /collect?e=pageview | 127.0.0.1 | measure.js executing | before it rendered |
| 8 | Banner element becomes visible | None | banner.js executing after its delayed response | while it was open |
| 9 | localStorage key fx.consent written | None, same origin | Accept click | after a choice was recorded |
| 10 | Cookie fx_consent written | None, same origin | Accept click, via document.cookie | after a choice was recorded |
| 11 | GET /vendor/remarketing.js | 127.0.0.1 | The Accept click handler | after a choice was recorded |
| 12 | Cookie fx_aud written | 127.0.0.1 | Set-Cookie on the remarketing.js response | after a choice was recorded |
| 13 | GET /collect?e=audience | 127.0.0.1 | remarketing.js executing | after a choice was recorded |
| 14 | POST /beacon | 127.0.0.1 | sendBeacon on visibility change | on the next page view |
| 15 | GET /collect?e=pageview | 127.0.0.1 | measure.js executing again | on the next page view |
Fifteen events. A screenshot of the banner captures event 8.
Before the banner rendered
The fixture server records the offset of every request from the moment it received the document request. Here is the first page view, untouched:
fixture receipt log, offsets in ms from the document request
+0.0 GET /
+2.3 GET /style.css
+2.9 GET /banner.js
+3.0 GET /vendor/measure.js
+4.6 GET /collect?e=pageview
banner.js response released at +150 by design; banner visible after that
Request order is not execution order
Read line three again. /banner.js was requested before /vendor/measure.js. The banner still lost.
The reason is in the script processing model. A classic script marked async is fetched in parallel with parsing and evaluated as soon as it is available, potentially before parsing completes. A script marked defer is fetched in parallel and evaluated only when parsing has finished. So the two scripts are not racing on a level track: one is permitted to run the moment it lands, the other has to wait for the document.
The fixture holds the banner script response for a documented 150 milliseconds, which models the round trip a banner served from another host would pay and a first-party tag would not. That delay is a fixture setting, not a discovery. What is not a fixture setting is the ordering rule underneath it: an async script is not sequenced against anything the banner does, at any latency. Remove the delay and the outcome becomes a coin toss decided by the network. That is worse, not better.
A network panel sorted by start time will show you the banner request first and tell you nothing about which script ran.
+3.0 ms GET /vendor/measure.js (async, in head)
+4.6 ms GET /collect?e=pageview (fired by measure.js)
+150 ms /banner.js response released
banner element becomes visible after thisEstablishesOn this fixture, in this browser, a collection request completed before the banner element was displayed.
Does not establishAnything about a real site, a real product, or what any particular implementation does. One fixture demonstrates that the ordering is possible and how to observe it. It does not measure how common it is.
The cookie nobody typed
Event 5 in the table is a cookie written by a response header, not by script. RFC 6265 describes the mechanism plainly: to store state, the origin server includes a Set-Cookie header in an HTTP response, and the user agent returns a Cookie header on subsequent requests. Origin servers may send Set-Cookie with any response, and user agents must process it on responses other than informational ones.
That means a script file can write a cookie by being served, before a single line of it has been evaluated. If you are reading the network panel's list of URLs and not opening the response headers, you will miss it. Grepping the site's JavaScript for document.cookie will also miss it, because this cookie is not in the JavaScript.
The write with no request at all
Cookies after load, before any choice: fx_mid=m-1
localStorage after load, before any choice: { "fx.visitor": "v-1785246048094" }
Network requests carrying either value: noneEstablishesAn identifier was created and persisted on the client before the banner appeared, and no request in the log carries it.
Does not establishWhat the identifier is for, whether it is ever transmitted, or whether it would be transmitted on a later visit. Storage shows what exists, not what it is used for.
Web Storage is a separate surface with separate rules. Each origin has its own storage area, sessionStorage is scoped to a single window session, and localStorage is designed for storage that spans multiple windows and outlives the current session. A setItem call touches no network at all. It will never appear in a request log, in any tool, at any verbosity, because there is no request.
While the banner was open
By the time a visitor can read the banner, the fixture has already written one cookie and one storage key and completed one collection request. The banner is asking a question whose subject has partly happened.
Notice what that does to the phrasing. "Trackers fire before consent" is a conclusion. "A collection request completed before the banner element was displayed, and here is the receipt log" is an observation. The second one survives an argument with the client's developer. The first one starts it.
After a choice was recorded
Clicking Accept produced four events in about 20 milliseconds: a storage write, a cookie written through document.cookie, a new script request, and a cookie set by that script's response header. Then the gated tag ran and made its own collection request.
That is the gate working. It is also the only moment it works, which the next section shows.
The evidence surfaces, and what each one settles
| Surface | Shows | Does not show | How you capture it |
|---|---|---|---|
| The rendered banner | What the visitor was asked | Anything the page already did | A screenshot |
| The network panel | Requests, order, headers, sizes | Storage writes, server-side collection | DevTools Network, preserve log on, cache disabled or not, stated either way |
| The cookie store | Names, values, attributes, expiry | Which response or script wrote each one | DevTools Application, cross-referenced with response headers |
| Web storage | Keys and values that persist | When they were written, or by what | DevTools Application, sampled at named moments |
| A beacon on unload | That a final request was queued | Whether it arrived, or any response | Preserve log across navigation, or a server receipt log |
| Server-side collection | Nothing | Everything | Not observable from the client at all |
On the next page view
The second page view of the same fixture produced this:
+0.0 GET /
+2.1 POST /beacon (from the page you just left)
+2.6 GET /style.css
+3.2 GET /vendor/measure.js
+3.4 GET /banner.js
+4.0 GET /collect?e=pageview
The beacon that lands in the next page's window
Event 14 arrives 2.1 milliseconds into page two. It belongs to page one.
The Beacon API exists to schedule asynchronous, non-blocking delivery of data, and the specification is explicit that beacon requests are guaranteed to be initiated before the page is unloaded. sendBeacon returns true when the user agent has queued the data for transfer, and the caller gets no response callback and no delivery confirmation. So the request is dispatched at the least convenient moment for anyone watching: after the observer has moved on, in a window that has closed.
If you capture with preserve log off, this request does not exist in your evidence. On the server receipt log it is unmissable, which is a good argument for capturing both.
The gate that only closes once
The stored choice made the banner stay hidden. It changed nothing about the ungated tag: /collect?e=pageview fired again, at 4.0 milliseconds, exactly as it had before any choice existed.
The gated tag did the opposite. It never loaded again. It was injected by the Accept click handler, and on a page view where the banner never appears there is no click to inject it. A naive gate runs the gated tag once, on the click, and never afterwards, while running the ungated tag forever. Both halves of that are wrong and neither is visible from the interface.
After a hard reload
A reload with the cache bypassed and storage untouched produced the same six lines: the ungated tag ran, the banner stayed hidden, the gated tag stayed absent.
Then a control. I cleared cookies and local storage and loaded the page again. The banner returned, and the cookie fx_mid was written before it did. Clearing a stored choice does not rewind anything the page does independently of that choice.
Two capture-hygiene notes fall out of this. A hard reload changes the log for cache reasons that have nothing to do with the banner, so two logs are comparable only when their cache state matches and you have written down which state that was. And storage persists across the reload, which means a second run is not a fresh visitor unless you make it one.
Here is what AgencyProof does in this family, in one sentence: it checks for a privacy-policy link and potentially revealing image filenames. AgencyProof inspects a bounded sample of reachable public pages. The report records the pages and methods used, plus important checks that were not performed. Analytics tag verification is one of the items in that not-performed list, for the reason the four-state model above spells out. The capture in this article is done by hand, in a browser, by a person, against a page they control.
Which is the boundary, and it is worth saying without hedging.
This method produces a record of what one browser requested, wrote and read, on one page, in one configuration, at one moment. A technical observation of what loads is not a legal conclusion about anything. It does not establish what any organisation is required to do, what any visitor was owed, or whether what happened was acceptable. Those are different questions with different experts, and the log is evidence they can use rather than an answer that spares them the work.
Provenance
- Written by
- Balaji PonnambalamFounder, AgencyProof
- Technical review
- Written and self-reviewed against the codebaseAgencyProof is a one-person engineering team; there is no second reviewer to claim.
- Published
- Evidence basis
- Examples in this article are synthetic fixtures built for the explanation.
Every product claim in an article is checked against the AgencyProof source tree before publication, and traced to the code path that implements it. Where the article describes a method rather than a shipped behaviour, it says which it is.
Related reading
Audit LabHow to report a Lighthouse score without overstating it
A Lighthouse score is one lab result under one device profile and network setting. Report it with those conditions.
Agency Field NotesHow to record a client's decision to launch with a known issue
Sometimes the authorised owner ships with a known issue. Record the evidence, the consequence and the check date.
Audit LabHow to tell a broken link from a protected endpoint
A response code is evidence, not a verdict. How to read 404, 403, 429, timeouts and bot challenges before calling a link broken.