Correctly Tracking Marketing Consent State Changes

Hey all!

We all love following the data trail with our web pixels, we know this. In a custom web pixel, how are we supposed to track that a customer had consented to email or sms marketing within that checkout session?

I want this to trigger on checkout_completed, and I can see there are fields event?.data?.checkout?.buyerAcceptsSmsMarketing and event?.data?.checkout?.buyerAcceptsEmailMarketing, these fields are persistent across checkout sessions since they relate to the customer. My goal here is to only dispatch these events on the checkout where the customer had checked off marketing consent and completed checkout.

I do not want these event to fire off every checkout_completed for customers that consented unknown checkouts ago, but rather fire off events when we can determine to our best ability that the customer had 0 consent at the start, but given consent by the end.

I have tried a few ways, one was listening to input_changed and checking when contact_marketing_opt_in or contact_sms_marketing_opt_in have been changed and they are checked, storing something to browser.sessionStorage and then checking for the key and value on checkout_completed and dispatch my dataLayer event from there, but it doesn’t seem to be working, nor do I think it’s a great solution if the DOM identifiers ever change. This method does seem to work for email but not sms

Another way I have tried is storing the value of event?.data?.checkout?.buyerAcceptsEmailMarketing to a global variable on checkout_started and comparing it to the end value of event?.data?.checkout?.buyerAcceptsEmailMarketing on checkout_completed but this never resolves a detection in consent for email or sms. I have noticed that any code outside of an analytics.subscribe() doesn’t seem to execute first. I had defined a function that immediately logs a message, but my checkout_started log seemed to run well before that, even though it was called well before. Maybe run order is a problem?

It seems shocking to me that Shopify doesn’t have a formal event for marketing_consent_granted which would make our lives a lot easier.

Some app pixels allegedly seem to have no problem detecting sign up consent, so what am I getting wrong here?

Here is some example pixel code from the latest:


// debug log flags
const SHOW_CONSOLE_LOGS = true

// proxy function for console log if we have the debug flag on
const debug_log = (...args) => SHOW_CONSOLE_LOGS && console.log(...args);

debug_log(`custom pixel loaded`);

// Store the GTM ID as a variable so we only have 1 source of ID
const GTM_ID = 'ID';

// initialize the flags as false, set them in `checkout_started` and compare values in `checkout_completed`
let buyerAcceptsEmailMarketingOnCheckoutStarted = false;
let buyerAcceptsSmsMarketingOnCheckoutStarted = false;

// Push data to dataLayer and log it
const pushToDataLayer = (eventData) => {
  debug_log(`pushToDataLayer():`, eventData);
  // no event data for some reason, early exit.
  if(!eventData) return;
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push(eventData);
};

// When window.dataLayer is defined push up event data, if applicable.
// Otherwise, load the GTM script, and then push the sent event data, if applicable.
const initializeGTM = (eventData) => {
  debug_log(`initializeGTM()`, eventData);
  if(window.dataLayer) {
    debug_log('GTM already loaded');
    eventData && pushToDataLayer(eventData);
    return;
  }
  debug_log(`window.dataLayer does not exist, creating script tag with onload callback.`)
  window.dataLayer = [];
  // establish our GTM script
  const script = document.createElement('script');
  script.async = true;
  script.src = `https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`;
  script.onload = () => {
    debug_log('GTM loaded');
    pushToDataLayer({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
    eventData && pushToDataLayer(eventData);
  };
  // insert the GTM script into the web pixel head using the old faithful
  const firstScript = document.getElementsByTagName('script')[0];
  firstScript.parentNode.insertBefore(script, firstScript);
};

// Subscribe to checkout_started and grab the current consent values
// The consent values on checkout_started will be used to determine if the customer was already signed up for marketing or not.
analytics.subscribe("checkout_started", (event) => {
  // checkout started, initialize GTM if it is not already initialized.
  // we have attempted to call this function outside of an analytics subscribe event, but it seems these events run well before anything outside.
  initializeGTM();
  // set our marketing consent flags as the checkout session is started.
  buyerAcceptsEmailMarketingOnCheckoutStarted = event?.data?.checkout?.buyerAcceptsEmailMarketing;
  buyerAcceptsSmsMarketingOnCheckoutStarted = event?.data?.checkout?.buyerAcceptsSmsMarketing;
  debug_log(`checkout_started, buyerAcceptsSmsMarketing: ${buyerAcceptsSmsMarketingOnCheckoutStarted}, buyerAcceptsEmailMarketing: ${buyerAcceptsEmailMarketingOnCheckoutStarted}`);
});

// Subscribe to the checkout_completed event to check for consent, since consent can be toggled on and off during the contact info stage.
analytics.subscribe("checkout_completed", (event) => {
  debug_log(`checkout_completed`);
  // if the customer consented to email marketing
  // If the buyer did not have email consent at the start of checkout, but does at the end, we will assume they just consented for email marketing in this checkout
  if(buyerAcceptsEmailMarketingOnCheckoutStarted == false && event?.data?.checkout?.buyerAcceptsEmailMarketing == true){
    debug_log(`Customer consented to email marketing in this checkout.`);
    initializeGTM({
      event: "gtmEvent",
      event_name: "signup",
      contact_type: "email",
      signup_location: "checkout",
      contact_data: event?.data?.checkout?.email
    });
  }
  // if the customer consented to sms marketing
  // If the buyer did not have sms consent at the start of checkout, but does at the end, we will assume they just consented for sms marketing in this checkout
  if(buyerAcceptsSmsMarketingOnCheckoutStarted == false && event?.data?.checkout?.buyerAcceptsSmsMarketing == true){
    debug_log(`Customer consented to sms marketing in this checkout.`);
    initializeGTM({
      event: "gtmEvent",
      event_name: "signup",
      contact_type: "sms",
      signup_location: "checkout",
      contact_data: event?.data?.checkout?.smsMarketingPhone
    });
  }
});

This may be helpful here:

Or better yet utilizing both with this for the snapshot at time of page render:

So you could get the state at the start and then know if someone submits consent during checkout similar to your other code.

Frankly, my guess is that it’s not entirely fool-proof because I am guessing someone could use a separate tab to accept marketing after a checkout has started…

That said, it does not seem to provide whether it was email versus sms that was consented to based on the documentation.

customerPrivacy I had not considered for this use case. I have only utilized customerPrivacy for the cookie consent bar and dealing with third party script management as a theme level usually attempting to translate Shopify consent status to third party consent management platform statuses like DataGrail, not with checkout data.

So getting initial consent state via init on page_viewed and storing that to a global pixel variable may be more helpful than only setting it at checkout_started.

Completing checkout in another session / tab is a concern, but seems highly unlikely to consider at the moment.

The part that seems to fall short the most is checkout_completed, like the consent state isn’t completely set at the point or something.

Well, the next thing to consider given the context is whether or not the shop has upgraded to Checkout Extensibility, which from the way the Shopify Help Center reads it suggests the shop needs to be on the Shopify Plus plan.

https://help.shopify.com/en/manual/checkout-settings/customize-checkout-configurations/checkout-extensibility#eligibility

My guess is that it means that the two properties you were listening for is only available to Shopify Plus stores.

If that’s the case and you are referring to a Shopify Plus store then there’s likely a bug causing it to not display the data properly at that time.

The stores I work on are Shopify Plus stores, so that part is covered.

@Matthew_Crigger this was fun! Sorry this is so verbose but I wanted to to be clear

1.) Checkbox Changed:

// INPUT CHANGED EVENT
analytics.subscribe('input_changed', (event) => {
    // Access the changed input element and its properties
    const element = event.data.element;
    const elementId = element.id;
    const elementValue = element.value;

    // Check if the marketing opt-in checkbox was changed to true
    if (elementId === "contact_marketing_opt_in" && elementValue === "true") {
        
        (async () => {
            await browser.sessionStorage.setItem('checkout_email_opt', true);
            const emailOptStatus = await browser.sessionStorage.getItem('checkout_email_opt');
            console.log("email_opt_set: ",emailOptStatus);
        })();
    }
});

2.) On checkout_start take a look at current opt-in state and store it in session

analytics.subscribe("checkout_started", (event) => {

    // Store the initial email marketing opt-in status at the start of checkout
    const currentStatus = event.data.checkout.buyerAcceptsEmailMarketing || false;
    browser.sessionStorage.setItem('checkout_start_email_opt', currentStatus);

    (async () => {
        const currentStatus = event.data.checkout.buyerAcceptsEmailMarketing || false;
        await browser.sessionStorage.setItem('checkout_start_email_opt', currentStatus);

        const emailOptStatus = await browser.sessionStorage.getItem('checkout_start_email_opt');
        console.log("email_opt_status: ", emailOptStatus);
    })();
});

3.) when on checkout_complete see what the session storage was before, if it is now updated then they opted in from checkout

// CHECKOUT COMPLETED EVENT
analytics.subscribe("checkout_completed", (event) => {
    console.log("checkout_completed: ", event);

    // Check if the email opt-in status changed during checkout
    if (browser.sessionStorage.getItem('checkout_start_email_opt')) {
 
        (async () => {
            const previousEmailStatus = await browser.sessionStorage.getItem('checkout_start_email_opt');
            const currentEmailStatus = event.data.checkout.buyerAcceptsEmailMarketing.toString();

            if (previousEmailStatus != currentEmailStatus) {
                console.log("email_previous_state: ", previousEmailStatus);
                console.log("email_current_state: ", currentEmailStatus);

                // Now you know they opted-in from checkout
                browser.sessionStorage.clear();
            }
        })();
    }
});
2 Likes

I’ll give this a shot later tonight and add in support for the SMS marketing input as well, thanks for taking the interest. I had another pixel iteration using session storage, but I didn’t utilize this way and maybe that was the problem.

I’ll do some test orders and see!