Skip to content

Microsoft Clarity, UET and Meta Pixel fire before Cookiebot consent #167

Description

@spizeck

Confirmed production defect (discovered during #161 testing)

On a fresh browser profile at https://www.seasaba.com, with no interaction with the Cookiebot banner (Cookiebot state: hasResponse: false, statistics/marketing false):

Observed pre-consent Evidence
clarity.ms/tag/xq4re63wsc?ref=gtm + scripts.clarity.ms/0.8.70/clarity.js download Clarity tag fires unconditionally
c.clarity.ms/c.gif, {b,g,o,u}.clarity.ms/collect Clarity telemetry sent
_clck, _clsk on .seasaba.com with real values Clarity first-party cookies
bat.bing.com/bat.js, bat.bing.com/action/0?...evt=pageLoad UET base tag fires + pageLoad beacon
_uetsid, _uetvid on .seasaba.com with real GUIDs (e.g. db220b30b84d11f1…), plus localStorage._uetsid/_uetvid(+_exp) UET advertising identifiers written
MUID, CLID, ANONCHK, SM, MR on .clarity.ms/.bing.com Microsoft third-party device-ID cookies
connect.facebook.net/fbevents.js, facebook.com/tr PageView, fr cookie Meta Pixel fires unconditionally
www.clarity.ms/tag/uet/187264257?conversions=1&insights=1 Clarity additionally loads its own UET integration (linked to UET tag 187264257)

Reproduced on Chromium 153 and WebKit (Playwright, fresh contexts). On WebKit ITP blocked the third-party cookies but all first-party cookies/localStorage and all telemetry requests still occurred.

Decline is too late: clicking Decline makes Cookiebot remove the first-party tracker cookies, but third-party cookies persist, already-loaded Clarity keeps calling *.clarity.ms/collect after decline, and the telemetry has already been sent. Consent must gate the tag load, not clean up after it.

Root cause

Verified by decoding the compiled live container GTM-5PFMJFN (gtm.js):

  • No tag in the container has an Additional Consent Check — zero consent metadata on all 26 compiled tags.
  • gtm.init_consent (Consent Initialization) fires only the Cookiebot CMP tag (__cvt_57SF4, CBID 25085b0a-…, consentModeEnabled on) — correct, and Consent Mode defaults ARE established before other tags (gcs=G100 denied defaults on the first GA4 collect; pscdl=denied).
  • gtm.init fires GA4 config + the Clarity tag (__cvt_MQDKZ, project xq4re63wsc) — Clarity has no native Consent Mode support, so it loads regardless of the denied default.
  • gtm.js (All Pages) fires the UET base tag (__baut, tag 187264257) and a custom HTML tag = Meta Pixel (fbq init 23854267411660080, PageView) — both ungated.
  • Ten __baut UET event tags (tag_id 52–61) mirror the GA4 custom events and the purchase tag (tag_id 67) is bound to booking-page DOM events — all ungated.

Why _uetsid/_uetvid got real identifiers instead of null|… placeholders: Microsoft's sanctioned denied-mode writes only placeholder values. We observed real GUIDs and no evt=consent/asc=D signal to bat.bing.com pre-consent — the denied state never reaches UET. The __baut template can push consent default {source:'gtm_default'}, but that source reads dataLayer consent entries; the Cookiebot template sets defaults via the sandbox setDefaultConsentState API (no dataLayer entry — confirmed: zero consent pushes observed before a user choice). So bat.js initializes unrestricted.

Isolation test (all clarity.ms blocked): _uetsid/_uetvid still written → GTM's own __baut tag is independently defective, not just Clarity's UET loader.

What works correctly: GA4/Google Ads honor Consent Mode natively — denied pings only (gcs=G100), no _ga/_gcl_au until consent granted. Cookiebot defaults precede all tag execution — no race.

Remediation (GTM dashboard — no repo code change required)

The repo only loads gtm.js (components/analytics-loader.tsx); the consent boundary belongs inside GTM. docs/COOKIEBOT_CONSENT_SETUP.md §2.4–2.7 already prescribes this configuration — it was documented but never applied/published.

  1. Clarity tag (__cvt_MQDKZ, project xq4re63wsc): add Additional Consent Check analytics_storage (Statistics, per documented mapping). Blocks clarity.js, _clck/_clsk, collect calls, and Clarity's tag/uet loader.
  2. UET consent default — create custom HTML tag on Consent Initialization – All Pages: window.uetq = window.uetq || []; window.uetq.push('consent','default',{ad_storage:'denied'}); This queues denied before bat.js init → placeholder-only cookies + cookieless modeling (fixes the real-identifier writes while keeping Microsoft's Advanced Consent Mode / avoiding "Need attention"). Alternative, stricter: also add ad_storage consent check to the UET base tag — no bat.js at all pre-consent, but Microsoft loses the denied signal. Choose deliberately.
  3. UET base tag (187264257): verify the template's "Inherit initial consent"/gtm_default option is enabled; confirm with UET Tag Helper that asc=D appears pre-consent.
  4. UET event tags (tag_ids 52–61) + purchase tag (67): add Additional Consent Check ad_storage (Marketing).
  5. Meta Pixel custom HTML tag (62): add Additional Consent Check ad_storage (blocks fbevents.js, PageView, fr).
  6. Publish a new GTM version after verifying in Preview/Tag Assistant.

Verification (deterministic)

scripts/consent-audit.mjs (added in the companion docs/tooling PR) replays the three states against production and prints cookies/storage/requests:

  • No interaction / Declined: no clarity.ms or bat.bing.com requests, no _clck/_clsk/_uetsid/_uetvid, no MUID/CLID/fr, empty _uet* localStorage. UET pageLoad shows asc=D only if the base tag is intentionally left ungated under option 2 (placeholders null| acceptable per Microsoft docs — decide and record).
  • Granted: _clck, _ga, _uetsid, fr appear; evt=consent…asc=G fires to bat.bing.com; Clarity collects normally.
  • Regression: GA4 denied-pings-only pre-consent (gcs=G100, no _ga); Respond.io widget unaffected; Checkfront booking ungated.

Acceptance criteria

  • Fresh visitor before consent: no Clarity script/telemetry/cookies
  • Declined: same, and no already-loaded tracker keeps collecting
  • Granted (statistics): Clarity initializes; granted (all): UET/Meta initialize
  • UET receives ad_storage denied before bat.js init (no real _uetsid/_uetvid/MUID values pre-consent)
  • Consent Mode defaults precede optional tag execution (already true — keep it)
  • GA4/Google Ads behavior unchanged; Respond.io, Checkfront, Sentry unaffected
  • Docs updated to match actual production config

Refs: discovered in #161 (independent of the harmless Clarity ICU exception); consent architecture per docs/COOKIEBOT_CONSENT_SETUP.md.

Generated with Devin

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions