Skip to content
AgencyProofby Intelliblitz

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.

Balaji Ponnambalam9 min read
A barrier arm stands raised, its signal lamp dark and its lever untouched, while a small bird at the console watches two parcels already past it. A separate lane waits under blank dockets.

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.

OrderRequest or writeHostTriggered byRelative to the banner
1GET /127.0.0.1Navigationbefore it rendered
2GET /style.css127.0.0.1Parser and preload scannerbefore it rendered
3GET /banner.js127.0.0.1defer script at the end of the bodybefore it rendered
4GET /vendor/measure.js127.0.0.1async script in the headbefore it rendered
5Cookie fx_mid written127.0.0.1Set-Cookie on the measure.js responsebefore it rendered
6localStorage key fx.visitor writtenNone, same originmeasure.js executingbefore it rendered
7GET /collect?e=pageview127.0.0.1measure.js executingbefore it rendered
8Banner element becomes visibleNonebanner.js executing after its delayed responsewhile it was open
9localStorage key fx.consent writtenNone, same originAccept clickafter a choice was recorded
10Cookie fx_consent writtenNone, same originAccept click, via document.cookieafter a choice was recorded
11GET /vendor/remarketing.js127.0.0.1The Accept click handlerafter a choice was recorded
12Cookie fx_aud written127.0.0.1Set-Cookie on the remarketing.js responseafter a choice was recorded
13GET /collect?e=audience127.0.0.1remarketing.js executingafter a choice was recorded
14POST /beacon127.0.0.1sendBeacon on visibility changeon the next page view
15GET /collect?e=pageview127.0.0.1measure.js executing againon the next page view
Scroll for more →

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.

The hit that preceded the interface
+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 this

EstablishesOn 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

Storage the network panel cannot show you
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: none

EstablishesAn 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

SurfaceShowsDoes not showHow you capture it
The rendered bannerWhat the visitor was askedAnything the page already didA screenshot
The network panelRequests, order, headers, sizesStorage writes, server-side collectionDevTools Network, preserve log on, cache disabled or not, stated either way
The cookie storeNames, values, attributes, expiryWhich response or script wrote each oneDevTools Application, cross-referenced with response headers
Web storageKeys and values that persistWhen they were written, or by whatDevTools Application, sampled at named moments
A beacon on unloadThat a final request was queuedWhether it arrived, or any responsePreserve log across navigation, or a server receipt log
Server-side collectionNothingEverythingNot observable from the client at all
Scroll for more →

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.