Articles on: Integrations
This article is also available in:

Google Consent Mode in a Shopify custom pixel (gtag.js)

Overview


Some stores load Google Analytics 4 (G-) or Google Ads (AW-) directly with gtag.js from a Shopify custom pixel (Settings > Customer events). This article gives you ready-to-paste pixel code that applies Google Consent Mode Advanced to those properties, using the consent your visitors give on the Pandectes banner.


If your Google properties are loaded by a Google Tag Manager container, use Google Consent Mode in a Shopify custom pixel (Google Tag Manager) instead.


This is an implementation guide. For background on default and update commands and on Advanced mode, see Google's Consent Mode with gtag.js.


Before you start



This pixel is one half of a two-part setup. In Shopify Admin, go to Apps > Pandectes GDPR > Integrations, click Configure on the Google Consent Mode card, and turn the integration on. See Using Google Consent Mode Integration. Google Consent Mode works on the Plus plan and above.


A Shopify custom pixel runs in a sandboxed iframe with its own window, dataLayer and copy of gtag.js. It is separate from the storefront page, and consent set in one is invisible to the other. So each needs its own setup:


Where

What runs there

What sets consent mode

Storefront page

Google tags in theme.liquid, your GTM container, tags from other apps

The Pandectes Google Consent Mode integration

Pixel sandbox

The gtag.js you load in the pixel and its properties

The code in this article


The pixel does not replace the integration. Nothing in the pixel can reach the storefront page, so without the integration your storefront has no consent mode at all.


Do you use Google Tag Manager?


If your store has a GTM container, check whether your Google properties are already loaded there. They usually are. Adding a pixel that loads the same GA4 or Google Ads property again duplicates your page views, sessions, purchases and conversions, and creates two competing consent states.


Your setup

Follow

GA4 or Google Ads loaded by a GTM container

Google Consent Mode in a Shopify custom pixel (Google Tag Manager)

No GTM, properties loaded with gtag.js in a custom pixel

This article


What this code covers


The code sets the consent state and nothing else. It sends Google the store's default policy, applies the visitor's choice, keeps it in sync with the banner, and loads the Google tag so the consent signals have somewhere to go. It does not send analytics or conversion data by itself.


Pandectes provides the consent layer only. Which events you send, what data they carry, how they map to your GA4 property or Google Ads conversions, and whether your reporting is accurate are your responsibility. Pandectes does not support or verify the tracking built on top of this code.


Your event subscriptions go in the same pixel


Sending Shopify's customer events to Google is the real purpose of the pixel, and those subscriptions must be in the same pixel, below the consent code. The examples in each snippet are commented out on purpose. Uncomment them or replace them with your own.


Every pixel that loads a Google property needs its own copy


You cannot put the consent code in one custom pixel and your events in another. Each pixel is a separate sandbox. A pixel that loads GA4 or Google Ads without the consent code fires everything under Google's defaults, as if no consent mode existed, and Shopify does not warn you.


  • One pixel loads your properties: one copy of the code, with that pixel's events below it.
  • Three pixels each load a Google property: three copies, one in each pixel.
  • If several pixels load the same property, fix that duplication first.


Advanced mode and the pixel permission


This code gives you Google Consent Mode Advanced: the Google tag loads on every page view, sends cookieless pings while consent is denied, and switches to full measurement once consent is granted. That is what lets Google model conversions for visitors who do not consent.


For that, the pixel must be allowed to run before consent. In Shopify Admin, go to Settings > Customer events, open your custom pixel, and under Customer privacy set Permission to Not required. If you set it to Required, Shopify holds the pixel back until the visitor accepts, so the Google tag never loads before consent and no cookieless pings are sent.


If you want Basic mode (nothing loads until consent is given), you do not need this code. Set Permission to Required, select the categories you need, and let Shopify hold the pixel back.


How the code works


Shopify gives the pixel the visitor's consent in init.customerPrivacy, but it does not say whether that is the store default or a choice the visitor already made. So the code always runs these steps, in this order:


  1. consent default: the store policy, written into the code.
  2. consent update: whatever init.customerPrivacy contains. If it was only the default, nothing changes. If it was a saved choice, it applies before the first hit.
  3. Load gtag.js and configure your Google properties.
  4. Listen for visitorConsentCollected and send an update every time the visitor changes their choice.


In a custom pixel, analytics, browser and init are available directly, but customerPrivacy is reached through api. Calling customerPrivacy.subscribe(...) without api. throws a ReferenceError.


Shopify's consent values map to Google's consent types like this:


Shopify

Google

analyticsProcessingAllowed

analytics_storage

marketingAllowed

ad_storage, ad_user_data, ad_personalization

preferencesProcessingAllowed

functionality_storage, personalization_storage

saleOfDataAllowed

No Consent Mode equivalent. It is a US state sale/share signal (see restricted_data_processing).

(always)

security_storage is always granted


Which snippet do I need?


Your store's policy

Use

Same policy worldwide: nothing runs until the visitor consents

Snippet 1 (one default command)

Consent collected only in some regions, allowed elsewhere

Snippet 2 (default commands per region)


Both snippets support GA4, Google Ads, or both.


  1. In Shopify Admin, go to Settings > Customer events and click Add custom pixel.
  2. Give the pixel a name and paste one of the snippets.
  3. Fill in your IDs between START_MODIFY_FOR_YOUR_STORE and END_MODIFY_FOR_YOUR_STORE.
  4. Add your own event subscriptions at the bottom.
  5. Set the permission as described above, click Save, then Connect.


Snippet 1: one default command


One default command with no region, so it applies to every visitor. Everything is denied except security_storage.


/* START_MODIFY_FOR_YOUR_STORE */
const GA4_ID = ""; // e.g. "G-XXXXXXXXXX", leave empty if not used
const AW_ID = ""; // e.g. "AW-XXXXXXXXXX", leave empty if not used
const DEBUG = false; // true sends GA4 hits to DebugView while testing
/* END_MODIFY_FOR_YOUR_STORE */

const GRANTED = "granted";
const DENIED = "denied";
const GOOGLE_URL = "https://www.googletagmanager.com/gtag/js?id=";
const DEVELOPER_ID = "dMTZkMj";

/* Shopify customerPrivacy -> Google consent state */
const toConsentState = (cp) => ({
ad_storage: cp.marketingAllowed ? GRANTED : DENIED,
ad_user_data: cp.marketingAllowed ? GRANTED : DENIED,
ad_personalization: cp.marketingAllowed ? GRANTED : DENIED,
analytics_storage: cp.analyticsProcessingAllowed ? GRANTED : DENIED,
functionality_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
personalization_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
security_storage: GRANTED,
});

/* The pixel sandbox has its own dataLayer. */
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}

/* Pandectes developer ID. Identifies the implementation to Google,
* has no effect on your data. Leave it as it is. */
gtag("set", `developer_id.${DEVELOPER_ID}`, true);

/* STEP 1: store policy, before gtag.js loads. Applies to everyone. */
gtag("consent", "default", {
ad_storage: DENIED,
ad_user_data: DENIED,
ad_personalization: DENIED,
analytics_storage: DENIED,
functionality_storage: DENIED,
personalization_storage: DENIED,
security_storage: GRANTED,
});

/* STEP 2: visitor state, always sent. If it is only the default,
* nothing changes. If it is a saved choice, it lands before the first hit. */
gtag("consent", "update", toConsentState(init.customerPrivacy));

/* STEP 3: load the Google tag once and configure each property. */
const TAG_IDS = [GA4_ID, AW_ID].filter(Boolean);

if (TAG_IDS.length) {
gtag("js", new Date());

if (GA4_ID) {
gtag("config", GA4_ID, {
send_page_view: false,
...(DEBUG ? { debug_mode: true } : {}),
});
}

if (AW_ID) {
gtag("config", AW_ID);
}

const script = document.createElement("script");
script.async = true;
script.src = GOOGLE_URL + TAG_IDS[0];
document.head.appendChild(script);
}

/* STEP 4: follow the visitor's choices. Note the api. prefix. */
api.customerPrivacy.subscribe("visitorConsentCollected", (event) => {
gtag("consent", "update", toConsentState(event.customerPrivacy));
});

/* YOUR EVENT SUBSCRIPTIONS GO HERE, IN THIS SAME PIXEL.
* The examples are commented out on purpose. What you send is yours
* to own and verify. */

/* Page view: only if the storefront does not already send page_view
* for the same property, or it is counted twice. */
// analytics.subscribe("page_viewed", (event) => {
// gtag("event", "page_view", {
// page_path: event.context.document.location.pathname,
// page_title: event.context.document.title,
// page_location: event.context.window.location.href,
// });
// });

/* Purchase: if GA4 and Google Ads are linked, import the GA4 purchase
* as a conversion in Google Ads and delete the conversion block below,
* or every order is counted twice. */
// analytics.subscribe("checkout_completed", (event) => {
// const checkout = event.data.checkout;
// const items = checkout.lineItems.map((line) => ({
// item_id: "shopify_" + line.variant.product.id + "_" + line.variant.id,
// item_name: line.title,
// item_variant: line.variant.title,
// price: line.variant.price.amount,
// quantity: line.quantity,
// }));
//
// gtag("event", "purchase", {
// transaction_id: checkout.token,
// currency: checkout.currencyCode,
// value: checkout.totalPrice.amount,
// tax: checkout.totalTax.amount,
// shipping: checkout.shippingLine ? checkout.shippingLine.price.amount : 0,
// items: items,
// });
//
// // Google Ads conversion, ONLY when GA4 and Google Ads are NOT linked.
// // Use the conversion label from your Google Ads conversion action.
// gtag("event", "conversion", {
// send_to: "AW-XXXXXXXXXX/AbC-D_efGhIjKlMnOp",
// transaction_id: checkout.token,
// currency: checkout.currencyCode,
// value: checkout.totalPrice.amount,
// });
// });


Snippet 2: default commands per region


For stores that collect consent only where it is legally required. There are two default commands: a global one that grants everything, and one for the listed regions that denies everything except security_storage. When both match a visitor, the more specific region wins.


The rest is identical to Snippet 1.


/* START_MODIFY_FOR_YOUR_STORE */
const GA4_ID = ""; // e.g. "G-XXXXXXXXXX", leave empty if not used
const AW_ID = ""; // e.g. "AW-XXXXXXXXXX", leave empty if not used

/* ISO 3166-2 codes where the store collects consent: EEA + UK below.
* Add US states ("US-CA", "US-CO", ...) if you treat them as opt-in.
* This list MUST match the regions where your banner collects consent. */
const RESTRICTED_REGIONS = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "LI", "NO", "IS",
];

const DEBUG = false; // true sends GA4 hits to DebugView while testing
/* END_MODIFY_FOR_YOUR_STORE */

const GRANTED = "granted";
const DENIED = "denied";
const GOOGLE_URL = "https://www.googletagmanager.com/gtag/js?id=";
const DEVELOPER_ID = "dMTZkMj";

/* Shopify customerPrivacy -> Google consent state */
const toConsentState = (cp) => ({
ad_storage: cp.marketingAllowed ? GRANTED : DENIED,
ad_user_data: cp.marketingAllowed ? GRANTED : DENIED,
ad_personalization: cp.marketingAllowed ? GRANTED : DENIED,
analytics_storage: cp.analyticsProcessingAllowed ? GRANTED : DENIED,
functionality_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
personalization_storage: cp.preferencesProcessingAllowed ? GRANTED : DENIED,
security_storage: GRANTED,
});

/* The pixel sandbox has its own dataLayer. */
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}

/* Pandectes developer ID. Identifies the implementation to Google,
* has no effect on your data. Leave it as it is. */
gtag("set", `developer_id.${DEVELOPER_ID}`, true);

/* STEP 1a: policy for everyone outside the listed regions. */
gtag("consent", "default", {
ad_storage: GRANTED,
ad_user_data: GRANTED,
ad_personalization: GRANTED,
analytics_storage: GRANTED,
functionality_storage: GRANTED,
personalization_storage: GRANTED,
security_storage: GRANTED,
});

/* STEP 1b: policy for the listed regions. Google resolves the region
* from the IP address, so test it with a VPN. */
gtag("consent", "default", {
ad_storage: DENIED,
ad_user_data: DENIED,
ad_personalization: DENIED,
analytics_storage: DENIED,
functionality_storage: DENIED,
personalization_storage: DENIED,
security_storage: GRANTED,
region: RESTRICTED_REGIONS,
});

/* STEP 2: visitor state, always sent. Shopify has already applied your
* regional settings to init.customerPrivacy, so this matches 1a or 1b. */
gtag("consent", "update", toConsentState(init.customerPrivacy));

/* STEP 3: load the Google tag once and configure each property. */
const TAG_IDS = [GA4_ID, AW_ID].filter(Boolean);

if (TAG_IDS.length) {
gtag("js", new Date());

if (GA4_ID) {
gtag("config", GA4_ID, {
send_page_view: false,
...(DEBUG ? { debug_mode: true } : {}),
});
}

if (AW_ID) {
gtag("config", AW_ID);
}

const script = document.createElement("script");
script.async = true;
script.src = GOOGLE_URL + TAG_IDS[0];
document.head.appendChild(script);
}

/* STEP 4: follow the visitor's choices. Note the api. prefix. */
api.customerPrivacy.subscribe("visitorConsentCollected", (event) => {
gtag("consent", "update", toConsentState(event.customerPrivacy));
});

/* YOUR EVENT SUBSCRIPTIONS GO HERE, IN THIS SAME PIXEL.
* Use the same page view and purchase examples as in Snippet 1. */


Check that it works


Tag Assistant cannot see the pixel


Google Tag Assistant inspects the storefront page only. The pixel's gtag, dataLayer and properties live in a sandboxed iframe that it does not reach, so it reports nothing (or only the storefront's own tags) even when the pixel works correctly. This is expected and is not an error.


Check the dataLayer in the pixel's frame


  1. Open Chrome DevTools and go to the Console tab.
  2. In the console toolbar, open the frame dropdown (it normally shows top) and select the pixel's sandbox frame (its name starts with web-pixel). Shopify creates one frame per custom pixel, so pick the right one if you have several.
  3. Run the checks below.


In the top frame, dataLayer is the storefront's data layer, which tells you nothing about the pixel.


  • Do the defaults come first? In dataLayer, the consent entries must come before the config entries.
  • Does consent reach the tag? Set DEBUG = true, uncomment an event so something is sent, and open GA4 DebugView. With analytics_storage denied you see a cookieless ping instead of a normal hit.
  • Does the pixel load before consent? It must, for Advanced mode. If the Google tag only appears after the visitor accepts, the pixel permission is set to Required and you are in Basic mode.
  • Does the region logic work? Test with a VPN inside and outside a listed country.
  • Does the subscription fire? Add a temporary console.log inside the visitorConsentCollected callback and change your choice on the banner.


Common mistakes


Mistake

What happens

Consent code in one pixel, events in another

The events pixel has no gtag and no consent state. Its events go nowhere.

Several pixels load a Google property, only one has the consent code

The others fire with no consent mode, with no warning.

Pixel Permission set to Required

The tag never loads before consent: no cookieless pings and no modelling. That is Basic mode.

Ads conversion event sent while the GA4 purchase is also imported as a conversion

Every order is counted twice in Google Ads.

customerPrivacy.subscribe(...) without api.

ReferenceError. Consent never updates after the banner.

Skipping the update because init.customerPrivacy looks like the default

Returning visitors with saved consent get denied tags on their first page view.

default sent after config

Consent Mode is not applied to the first hits.

Google Consent Mode integration not turned on in Pandectes

Only the pixel's properties get consent mode. Google tags on the storefront run without it.

Pixel built while GTM already loads the same property

Duplicate page views, sessions, purchases and conversions, plus two consent states.

Testing with Tag Assistant or reading dataLayer in the top frame

Neither looks inside the pixel sandbox.

security_storage denied

Breaks fraud prevention signals with no compliance benefit.

RESTRICTED_REGIONS does not match the banner's regions

The default and the update disagree. The update wins and the region policy is ignored.


Settings this article does not cover


The snippets leave out Google's account-level settings, so Google's defaults apply:


Setting

What it controls

url_passthrough

Whether ad click IDs such as GCLID are passed in the URL between pages when cookies cannot be set

ads_data_redaction

Whether ad click IDs are removed from cookieless pings while ad_storage is denied

allow_enhanced_conversions

Whether hashed customer data may be sent with conversions

restricted_data_processing

The US state sale/share restriction, related to Shopify's saleOfDataAllowed

wait_for_update

How long tags wait for a consent update before firing


Pandectes cannot advise on these. The right values depend on your Google accounts, the modelling you rely on, and your business's legal position. Discuss them with whoever manages your Google setup, or with your legal advisor where there is a privacy question.


If you decide to set one, add it as a gtag("set", ...) call before the consent default commands:


// gtag("set", "url_passthrough", true);
// gtag("set", "ads_data_redaction", true);


wait_for_update is the exception: it goes inside a default command. These snippets do not need it, because init.customerPrivacy is available immediately and the update is sent straight after the defaults.

Updated on: 30/10/2026

Was this article helpful?

Share your feedback

Cancel

Thank you!