Skip to main content

OneTrust

OneTrust is our consent management platform. It renders the cookie banner and the preference center, decides which cookie categories a visitor has accepted, and broadcasts that decision to everything that needs it.

Two systems consume that decision independently: GTM gates individual tags on it, and the storefront forwards it to Shopify's Customer Privacy API. Neither knows about the other. Most confusion about consent on this site comes from assuming they are one path.


How it is wiredโ€‹

The banner is not ours. OneTrust owns that surface entirely โ€” Shopify's built-in privacy banner is explicitly disabled where the storefront configures its analytics provider (withPrivacyBanner: false), and the storefront never renders consent UI of its own.

What the storefront does own is a translation layer, mounted client-side inside Hydrogen's analytics provider:

  • A consent helper (getOneTrustConsent) reads the accepted categories off the window and returns four booleans.
  • A client-only component listens for OneTrust's OneTrustGroupsUpdated event, calls that helper, and pushes the result into Shopify's Customer Privacy API via setTrackingConsent. It registers itself with Hydrogen's analytics provider and signals readiness once wired, so Hydrogen does not emit analytics events before consent has been applied.
  • Two footer buttons โ€” "Cookie Settings" and "Do not sell my personal information" โ€” reopen the preference center by calling the SDK directly.

Everything else is configuration rather than code: the CSP allowlist, brand styling overrides for the vendor's dialogs, and window type declarations for the SDK's globals.


Where the SDK comes fromโ€‹

There is no OneTrust script tag in the storefront, and none in the server-rendered HTML. The loader is injected at runtime by the GTM container (GTM-PF5QHP2) from cdn.cookielaw.org, carrying the OneTrust domain script id as data-domain-script. That stub then pulls the banner SDK from the same host.

Two consequences:

  • Banner changes are GTM or OneTrust changes, not code changes. Version upgrades, geolocation rules, and banner copy all live outside the repo.
  • cdn.cookielaw.org is deliberately absent from the CSP scriptSrc list. The stub is injected by an already-trusted script, so 'strict-dynamic' covers it. Only the directives strict-dynamic does not cover need explicit entries.

Because the SDK is client-injected, the consent globals are undefined for the first moments of a page's life. Every consumer has to tolerate that.


OneTrust exposes accepted categories as a comma-delimited string, window.OnetrustActiveGroups, for example:

,C0001,C0003,C0002,C0005,C0004,

The category constants in the consent helper include a trailing comma ('C0002,') so a match cannot collide with a longer id that starts with the same characters.

Each category feeds both consumers. The storefront translates a subset into Shopify consent keys; GTM maps categories onto Google Consent Mode types, which is what actually gates tags:

OneTrust groupShopify consent keyGoogle Consent Mode type
C0001 Strictly Necessarynot read โ€” always onsecurity_storage
C0002 Performanceanalyticsanalytics_storage
C0003 Functionalpreferencesfunctionality_storage
C0004 Targetingmarketingad_storage, ad_user_data, ad_personalization
C0005 Social Medianot readpersonalization_storage
SSPD_BG Sale of datasale_of_data, California onlynot a Consent Mode type

All seven Consent Mode types are live on the container, and OptanonWrapper โ€” OneTrust's GTM hook โ€” is defined on the page. The category-to-type pairing itself is configured in OneTrust's GTM template rather than in code, so treat the right-hand column as the intended mapping and confirm it in GTM's Consent Overview before relying on it for a specific tag.

Note the asymmetry: C0005 gates tags in GTM but is invisible to the storefront, and sale_of_data travels only through the storefront's sync because Consent Mode has no equivalent.


Tag gating is container configuration, not application code. Every tag in GTM declares its consent requirements โ€” either explicitly requiring none, or naming the consent types it needs โ€” so two tags on the same page can behave differently under identical consent. A tag collecting advertising data stays dark when targeting consent is denied, while an analytics tag whose data does not breach that category still fires. Google tags generally ship with built-in consent handling; non-Google tags need theirs set by hand.

GTM's Consent Overview menu lists every tag alongside its consent configuration. It is the fastest way to audit what fires under a given consent state, and the only way to find a tag that is not gated at all.

This split is why the page reload below exists: nothing in the storefront can gate a tag that was never configured for consent in the container.


California and sale of dataโ€‹

sale_of_data consent defaults to true and is only narrowed for California visitors. The consent helper asks Shopify's Customer Privacy API for the visitor's region and, when it comes back as USCA, reads the SSPD_BG category; everywhere else the default stands.

getRegion() is not in Hydrogen's published types, so the helper widens the type locally. The whole region check is wrapped in try/catch โ€” if the API is not ready, the failure is logged to Sentry under a consent tag and consent stays at its permissive default.

SSPD_BG is not part of the standard category list. It appears only in the rule set served to California visitors, which is why it is absent from the group list elsewhere.


When a visitor changes their selection, the storefront reloads the page rather than re-gating pixels in place. This is "Option 1" from OneTrust's single-page application guidance, and it exists because pixels kept firing after consent was revoked during client-side navigation. Option 2 โ€” consent actioning without a reload โ€” was tried and did not hold, likely because some GTM tags are not gated behind consent checks and fire regardless. Auditing those tags in Consent Overview is the prerequisite for revisiting this.

The reload is guarded so it only follows a real user change: the previous category string must be defined and different from the new one. On local and staging the categories initialize as undefined before defaulting, and they do not persist across reloads, so without that guard the initial settling would trigger a reload loop.


Content Security Policyโ€‹

DirectiveHosts
imgSrchttps://cdn.cookielaw.org
connectSrchttps://cdn.cookielaw.org, https://geolocation.onetrust.com, https://*.onetrust.com
scriptSrcnone โ€” covered by 'strict-dynamic'

geolocation.onetrust.com is what the SDK calls to decide which rule set, and therefore which banner, a visitor sees.


Stylingโ€‹

The banner and preference center are the vendor's own DOM, restyled by id selectors โ€” #onetrust-consent-sdk for the banner, #onetrust-pc-sdk for the preference center. Buttons, toggles, accordions, and focus outlines are overridden to brand colors, several with !important to win against the SDK's inline styles. An SDK version bump can change these selectors, so check both surfaces after one.


Verifying in the browserโ€‹

On any page, in the console:

window.OnetrustActiveGroups;                    // ",C0001,C0003,C0002,C0005,C0004,"
window.Shopify.customerPrivacy.getRegion(); // "USNY"
window.Shopify.customerPrivacy.currentVisitorConsent();
// { marketing: 'yes', analytics: 'yes', preferences: 'yes', sale_of_data: 'yes' }
window.OneTrust.GetDomainData().Groups.map((g) => `${g.CustomGroupId}:${g.GroupName}`);
window.google_tag_data.ics.entries; // Consent Mode state per type

Consent defaults are opt-out: on a first production visit every category comes back accepted. If currentVisitorConsent() disagrees with OnetrustActiveGroups, the storefront's sync is where to look. If a tag fires when it should not, the disagreement is in GTM instead.


Known gapsโ€‹

  • The sync effect leaks a listener. It subscribes to OneTrustGroupsUpdated without cleanup and re-subscribes whenever the Customer Privacy client changes identity, so a session can accumulate duplicate listeners and redundant setTrackingConsent calls.
  • Consent values can be undefined. The category check returns undefined before the SDK loads, and that value is passed straight to setTrackingConsent.
  • No test coverage. Neither the sync component nor the consent helper has a spec; changes are verified in the browser.