June 2024 update: You can now use GTM within the Customer Events sandbox. You can even get the GTM preview working if you’re using a first-party GTM server container. The method below still works, but might not be as useful as before.

I’m an analytics bum. It’s probably what I like to do the most! You can bet I was excited when Shopify announced Customer Events. Finally! I thought. No more DOM scraping and finicky template hacks that need to be tweaked for every theme. I’ll be able to create one script to rule them all, I thought.

Eagerly, I went on the Customer Events page, set up a GTM script on the “all_events” event just to test things out, plugged everything in excitedly and… nothing. Nothing was firing at all. I was pretty bummed, so I dived into the docs a bit further. Ah, I discovered, those cool events are sandboxed. No luck on getting an easy win there.

Not one to quit, I went deep in the rabbit hole, and tried a million ways to get those sweet sweet events out of their cage, each more hacky than the next. Then I stumbled on the holy grail: postMessage. It’s built specifically for communicating data between windows, sandboxed iframes being one of those use cases.

While this method is not officially supported by Shopify, it uses a well-documented browser function. The implementation below was tested in June 2024, but it still needs to validate the message origin and payload before using the data.

Add this to theme.liquid and checkout additional scripts. Replace the placeholder with the exact pixel origin observed in your environment. If the sandbox reports its origin as null, do not use this pattern as a trusted channel.

<script>
  const allowedPixelOrigins = new Set(["https://REPLACE-WITH-THE-EXACT-PIXEL-ORIGIN", ]);

  window.addEventListener("message", (event) => {
      if (!allowedPixelOrigins.has(event.origin)) return;
      const message = event.data;
      if (!message || typeof message !== "object" || message.message !== "shopify_pixel_event" || typeof message.event_name !== "string" || typeof message.json !== "string") {
          return;
      }
      let json_data;
      try {
          json_data = JSON.parse(message.json);
      } catch {
          return;
      }
      if (!json_data || typeof json_data !== "object" || Array.isArray(json_data)) {
          return;
      }

      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
          event: message.event_name,
          data: json_data,
      });
  });
</script>

And create a custom pixel with this:

analytics.subscribe("all_events", async (event) => {
    parent.postMessage({
        'message': 'shopify_pixel_event',
        'event_name': event.name,
        'json': JSON.stringify(event.data)
    }, event.context.document.location.origin)
});

What’s going to happen with this is that the complete event data for every customer event will be sent over as a JSON string, which will trigger a dataLayer.push event on the main window, that you can use however you’d like. You’ll be able to set up custom events using the Customer Event names, parse the data coming from Customer Events, etc.

This is the basic code that you should tweak for your needs. For example, you might need to structure the data according to GA4 schema. In that case, you could structure it within the Customer Events code block, to send a clean JSON object to catch on the main window. Or you could keep the “raw” object coming from Customer Events, and organize it on the main window using GTM. It’s your choice!