A consent banner can look correct while the Meta Pixel sends events before advertising consent is granted. This example compares a scripted sequence of requests with a written consent rule, then explains how to investigate the same problem in Chrome DevTools.
1. The plan: a CSV anyone can read
A tracking plan can be a plain CSV file. This example records event names, required parameters, value rules and consent conditions:
# consent_mode: advanced
platform,event,required,rules,count,consent,note
ga4,purchase,transaction_id;currency;value;items,currency is iso4217; value > 0; each item has item_id|item_name,once,analytics_storage,Order confirmation
meta,ViewContent,content_ids;value;currency,currency is iso4217; value >= 0,at_least_once,ad_storage,Product page
meta,AddToCart,content_ids;value;currency,currency is iso4217; value >= 0,at_least_once,ad_storage,Add to cart
meta,PageView,,,at_least_once,ad_storage,Every page
The consent column says which consent each event needs. For the Meta rows it is ad_storage: the Pixel should
not send those events while the visitor has not accepted advertising cookies. A plan written like this is useful
even without any tool: it is the checklist you hand to whoever implements the tags.
2. The bug
Our automated test opens journey.html?consent=denied&metarevoke=0: this sets Google's consent to denied and deliberately skips Meta's consent revocation. The page uses real tracking libraries; the harness intercepts collection requests locally. The page's default is:
gtag('consent', 'default', {ad_storage: 'denied', analytics_storage: 'denied',
ad_user_data: 'denied', ad_personalization: 'denied'});
In advanced Consent Mode, Google tags can send cookieless pings while storage consent is denied. Google's consent commands do not automatically control the Meta Pixel.
The page calls PageView on load. The harness then calls ViewContent, updates Google's consent signals to granted to simulate acceptance, and calls AddToCart. The first two requests occur while ad_storage is denied; AddToCart occurs after it is granted.
For a manual check on a site with an interactive banner, inspect facebook.com/tr requests in DevTools and compare their timing with the page's consent updates. This demo has no interactive banner: reloading calls PageView, while the harness supplies ViewContent, the consent update and AddToCart. Reloading alone does not reproduce the full sequence.
3. Checking the journey against the plan
For this example, the plan requires the Meta Pixel events to occur only after ad_storage is granted. This is a rule of the site's implementation; Google's consent commands do not automatically enforce it for Meta.
Compare the sequence described above with that rule:
| Event | Consent state when sent | Consent check |
|---|---|---|
PageView |
ad_storage denied |
Fails |
ViewContent |
ad_storage denied |
Fails |
AddToCart |
ad_storage granted |
Meets this consent condition |
To investigate a site manually, open DevTools before the journey, enable Preserve log and filter Network by facebook.com/tr. Identify the events and establish their order relative to the site's consent changes. A snapshot of the final consent state is not enough to determine the state when an earlier request was sent.
If the order cannot be established, record the result as inconclusive. An expected event that never appears is not observed; its absence does not demonstrate a successful consent check. Other requirements, such as parameters and event counts, need separate checks.
4. The fix
Make the Meta Pixel wait for the same decision as the Google tag. Two common ways:
- With the Pixel code: call
fbq('consent', 'revoke')beforefbq('init', ...)when consent is denied, andfbq('consent', 'grant')when the visitor accepts. - With Google Tag Manager: require
ad_storagein the Meta tag's additional consent checks and provide a trigger that fires after consent is granted. Updating consent alone does not automatically replay a previously blocked tag.
Repeat the check after the fix. Under this example's rule, no Meta event request should occur while ad_storage is denied. After consent is granted, perform the relevant actions and confirm that the expected events appear with the required parameters. Missing events still need investigation.
Scope
This checks what the browser sends during the journey you record. It does not verify processing inside Google
Analytics or Meta, server-to-server integrations (such as the Conversions API) or legal compliance.
Google Analytics and Google Tag Manager are trademarks of Google LLC. Meta is a trademark of Meta Platforms, Inc.
Tracklint is not affiliated with, endorsed or sponsored by Google or Meta.
This article was written with AI assistance (#ABotWroteThis).
Top comments (5)
the metarevoke=0 bit is what makes this work. most QA writeups only show the passing case, so the test never proves it catches anything.
the fix snippet deserves more attention too. fbq('consent', 'revoke') before init should be in every implementation guide, and somehow it never is.
Thanks! That was the point of showing the failing run first: a check that has only ever passed doesn't prove it would catch anything. And agreed on the fix: with
fbq('consent', 'revoke')beforefbq('init'), the pixel script still loads but doesn't send events untilfbq('consent', 'grant')is called, so the banner's accept handler needs that grant call too.good to have that confirmed explicitly. the script still loading is the part that confuses people. they see fbevents.js load and assume data is leaving, or they see no network calls after the fix and assume it's broken. loading doesn't equal sending, and that deserves a line of its own.
the accept handler bit is the real gotcha though. every banner updates gtag consent, almost nobody remembers the fbq grant in the same handler. two consent systems, one banner.
Agreed, "loading isn't sending" deserves its own line. It's easy to see in DevTools: fbevents.js is only the library, and the events are the requests to facebook.com/tr, with the event name in the
evparameter. After revoke you should see the script load and no /tr requests until the grant. And putting the gtag consent update andfbq('consent', 'grant')in the same accept callback keeps the two systems from drifting apart.that's the real gotcha for me. everyone updates gtag consent in the accept handler, almost nobody remembers the fbq grant call sitting right next to it. the /tr requests check gives qa something concrete to assert on instead of eyeballing the network tab. loading vs sending deserves its own line everywhere, it confuses people constantly.
Some comments have been hidden by the post's author - find out more