Skip to content

feat(track): gate TrackShopify on Shopify analytics consent - #654

Open
titouanmathis wants to merge 1 commit into
2.xfrom
feat/v2-track-shopify-consent
Open

titouanmathis wants to merge 1 commit into
2.xfrom
feat/v2-track-shopify-consent

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

What changed

TrackShopify.dispatch() evaluates window.Shopify?.customerPrivacy?.analyticsProcessingAllowed?.() before every publish and publishes only when it returns true. window.Shopify is read fresh on each dispatch, so a consent change applies to the next event without a remount, and a Customer Privacy API that appears after mount is picked up on the next event.

A dropped event is not queued and is never replayed once consent is granted. The declare global Window.Shopify type gains an optional customerPrivacy.analyticsProcessingAllowed.

Why the gate is needed

Shopify's Web Pixels Manager gates App Pixels at load time: it loads a pixel only when the visitor has granted every consent that pixel declares as required. It does not gate Custom Pixels, which receive every published event and are expected to implement their own consent logic inside the sandbox. Shopify's Analytics API best practices say to check whether analytics processing is allowed and publish only when it is true.

So the platform covers part of the case and the component covers the rest. Before this change, every storefront had to repeat the same check before each event, which made declarative HTML insufficient for consent-aware tracking.

Why a drop is diagnosed rather than silent

The Customer Privacy API is loaded asynchronously through loadFeatures(), so window.Shopify.customerPrivacy is legitimately undefined early in a page's life. An event that vanishes in that window reads as a bug rather than a privacy decision. Each drop now reports a keyed diagnostic, and the two reasons are distinguished:

Code Reported when
track.shopify-consent-denied analyticsProcessingAllowed() returned false
track.shopify-privacy-unavailable window.Shopify.customerPrivacy.analyticsProcessingAllowed is not a function

These follow the naming of the existing track.shopify-unavailable and track.missing-event-name keys.

Order of the checks

dispatch() checks the analytics provider, then the event name, then consent. The first two catch a broken environment and a broken declaration, which are wrong for every visitor. A consent drop follows one visitor's choice. Checking consent first would hide an authoring mistake behind a legitimate drop, so an author debugging a missing event sees the actionable diagnostic. The reasoning is in a code comment.

Track is unchanged

Track pushes to window.dataLayer, an array append that transmits nothing by itself. The tag manager or CMP reading that array decides what leaves the browser, so consent there belongs at the tag level. No opt-out option was added — that stays out until someone needs it.

Tests

The TrackShopify tests move out of Track.spec.ts into packages/tests/Track/TrackShopify.spec.ts, and cover every acceptance criterion through the component's public surface:

  • allowed consent publishes the event, with the resolved context payload;
  • denied consent does not publish, and reports track.shopify-consent-denied;
  • an absent Customer Privacy API does not publish, and reports track.shopify-privacy-unavailable;
  • a customerPrivacy object without analyticsProcessingAllowed behaves the same;
  • a consent change affects the next event without a remount, in both directions;
  • an API that appears after mount is picked up on the next event;
  • an event dropped before consent was granted is not replayed;
  • the published payload equals the declared layers exactly, so the component adds no customer identifier;
  • the existing unavailable-provider and missing-event-name behaviour still holds;
  • a missing event name is reported even while consent is denied, which pins the check order;
  • this stays bound to window.Shopify.analytics when publishing;
  • window.dataLayer is never touched.

Verified with the change reverted: six of the new tests fail without the gate.

npm run test (979 tests, 87 files), npm run lint and npm run manifest:check all pass.

Docs

reference/items/Track/index.md, js-api.md, anatomy.md and examples.md document the consent-safe default, what is checked and when, that a denied or absent API means no publish, and the diagnostic codes. A short paragraph explains how this relates to Shopify custom pixels and why Track is not gated the same way.

Closes #653

🤖 Generated with Claude Code

https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu

`dispatch()` now calls
`window.Shopify.customerPrivacy.analyticsProcessingAllowed()` before every
publish and publishes only when it returns `true`. `window.Shopify` is
read fresh on each dispatch, so a consent change applies to the next
event without a remount.

Shopify's Web Pixels Manager gates App Pixels at load time, but it hands
every published event to custom pixels, which are expected to apply their
own consent logic. That gate is now the component's default instead of
something each storefront repeats before each event.

A dropped event is not queued and is never replayed. Each drop reports a
diagnostic so a missing event is diagnosable: `track.shopify-consent-denied`
when processing is refused, `track.shopify-privacy-unavailable` when the
Customer Privacy API is absent — which is legitimate early in a page's
life, since Shopify loads it asynchronously through `loadFeatures()`.

The event-name check runs before the consent check. A declaration without
an `event` name is wrong for every visitor, so it stays visible instead of
being masked by a consent drop.

The TrackShopify tests move out of `Track.spec.ts` into their own spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
@titouanmathis
titouanmathis force-pushed the feat/v2-track-shopify-consent branch from e749ab5 to 0f4e148 Compare September 16, 2026 16:29
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.32%. Comparing base (2d5253b) to head (0f4e148).
⚠️ Report is 8 commits behind head on 2.x.

Additional details and impacted files
@@            Coverage Diff            @@
##                2.x     #654   +/-   ##
=========================================
  Coverage     86.32%   86.32%           
  Complexity      145      145           
=========================================
  Files            20       20           
  Lines           746      746           
  Branches         88       88           
=========================================
  Hits            644      644           
  Misses           95       95           
  Partials          7        7           
Flag Coverage Δ
unittests 86.32% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant