Skip to content
ceaksan

Tracing the Source of GA4 and Ads Signals in Shopify

In Shopify, GA4 and Google Ads signals can come from different layers and with different configurations: the storefront, one or more app pixels, one or more custom pixels. Picking apart which signal came from which source, what it carries and what its consent state is in DevTools becomes an unnecessary burden. This post explains how to read that separation in a single panel with the Inspect Signals extension.

7 min read
TL;DR

In Shopify, GA4 and Google Ads signals can sit in different layers, such as the storefront, one or more app pixels and one or more custom pixels, and each can have its own configuration. Picking apart which signal came from which source, what it carries and what its consent state is in DevTools becomes an unnecessary burden. The source is read from the address of the frame that sent the hit; Inspect Signals collects that separation, the order cards and the warning rules in a single panel.

In Shopify, GA4 (Google Analytics 4) and Google Ads signals can come from different layers: the storefront where the theme code lives, one or more app pixels, one or more custom pixels. Picking apart which signal came from which source and what it carries one by one in DevTools, and reading each layer’s consent state separately, can become an unnecessary burden.

There are many tools for debugging, but the prominent ones are mostly aimed at technically skilled users. That makes it harder to summarize the situation in a screenshot or in a conversation with a client. What I needed was a way to hand the result to a client with limited technical knowledge in a form that is easy to follow. At some point, after answering similar questions again and again for the clients I consult for, I wrote a measurement debugging tool to make communication between the parties easier: Inspect Signals, a Chrome extension. This post covers what the tool reads and how it organizes the findings.

The examples below are based on my observations in client projects and on a test setup I recreated from them. They should not be read as general values that hold for every Shopify store.

Same Signal, Different Layers

I recreated a structure I had observed in client projects as a similar test setup, so as not to use client data directly and to be able to examine it in more detail. In this setup, the same order number reached three GA4 properties and two Ads accounts through different layers. The distribution was as follows:

LayerSent
Storefront and checkoutpurchase to two GA4 properties
Custom pixelpurchase to a third GA4 property, two conversion labels to one Ads account
App pixelOne conversion label to another Ads account

Remarketing and audience hits went out alongside these. The question “Did purchase fire once?” therefore has no single answer: it depends on which property, which account and which layer. The layers also report their own consent states, and those states can differ from each other (see Consent Mode Conflict below).

Finding the Source in DevTools

Shopify runs pixels inside either a Lax or a Strict sandbox, and app pixels are loaded in a strict sandbox.1 The initiator of requests sent from a sandbox iframe shows as null. A hit coming from the page’s own code and a hit coming from a pixel sandbox end up mixed in the same list. In the client setups I observed, finding the source in DevTools means opening the request’s Initiator tab, looking at the Request call stack area and searching for the shop_events_listener line.

The pixel’s source is read from the address of the frame that sent the request. Pixel sandbox addresses live under the store’s domain and follow this pattern:

  • App pixel: /web-pixels@<build>/app/web-pixel-<id>@<hash>/sandbox/modern/<page path>
  • Custom pixel: /web-pixels@<build>/custom/web-pixel-<id>@<version>/sandbox/modern/<page path>

Separating a hit’s source, its payload and its consent parameters (gcs, gcd) by hand becomes unsustainable when repeated across a shopping flow that, in my observation of client projects, can produce hundreds of hits.

The Same Information in the Panel: Layer and Order Card

Inspect Signals works only on stores that have been granted access; on first use it needs Grant access and then Reload page. The panel groups the tab’s hits page by page and states each hit’s source: Storefront, App pixel <id>, Custom pixel <id> or another iframe. This makes it easy to tell which layer a piece of data came from.

Signals that carry the same identifiers are collected into a single order card at the top of each page. The card shows side by side which signal went out from which layer, with which value. Google Ads states that a transaction ID must be unique for each transaction.2 When grouping, an Ads oid value that starts with GG_ is a per-hit identifier and does not count as an order key. For conversion hits that send no oid, approximate grouping is done on the same page within a short time window, and that card appears only when a rule flags the group.

Information read at row level:

ItemWhat it shows
G- and AW- chipsGA4 property or Ads account, each ID keeps the same color across the panel
BadgeConversion, Remarketing, Ecommerce
MarkerStar (conversion), person (hashed user data), curly braces (Ads data parameter)
gcs and gcd chipsThe consent state the hit reports
sid chipGA4 session ID, new label on a new session

Inspect Signals panel with the Ads filter on: Google Ads hits grouped by page and source. Each row shows the AW- account chip, a Remarketing or Conversion badge, gcs and gcd consent chips, the request type (image, xhr) and star, person and curly-brace markers

Clicking a row opens the Decoded, Params and Raw tabs. On Shopify stores, Decoded also shows the cart token, checkout token, order id and Shopify consent state read at that moment.

Which Inconsistencies Count as Warnings

Multiple signals from different layers in the same order group is not a warning on its own. Different labels can be intentional multiple conversions. The extension runs 13 rules, and some of them are:

RuleWhat it catches
duplicateThe same GA4 event, or the same Ads label, sent to the same ID more than once
value-mismatchDifferent value or currency within the same order group
items-totalGA4 purchase value does not match item price times quantity minus discount; info rather than warning when only a coupon code is sent
item-dataDifferent item ID, name or price across signals (info)
purchase-no-idpurchase sent without a transaction_id
purchase-missingA page with a completed Shopify checkout that sent no purchase (Shopify only)
consent-conflictOne signal sets a purpose to granted while another sets it to denied within the same order group

In a discounted order in one client project, I saw the items total come out as 598 and value as 0. Only one property sent the discount amount separately. The rule subtracts the discount when a discount amount is sent, and records an observation when only a coupon code is present.

Warnings do not diagnose the cause; they state the observed value. The Warnings filter in the panel lists only the hits that carry a warning.

The Decoded tab of a purchase row in the Inspect Signals panel, showing the order card, the source, transaction_id, value and the item table. The Warnings section states that the purchase hit was sent to the same property twice

Layers can report different consent states for the same visitor. In a client project, I saw gcd=13l3l3l3l3l1 on hits sent from the storefront. This value means none of the four consent signals was set. On the same page, hits sent from pixel sandboxes carried gcs=G111 and granted codes inside gcd. The extension’s Shopify consent rule flags a hit as a warning when it is granted or carries no consent mode information for a purpose Shopify has not allowed, and as info in the opposite case.

Two notes:

  • The gcd decoding is not a specification published by Google; it relies on a table compiled through reverse engineering.3 When a letter is not recognized, the answer is “unknown” instead of a guess.
  • In the setup I observed, the most reliable source for reading Shopify’s own consent state was the sandbox’s init data4 and its consent update messages. The Customer Privacy API also publishes the visitorConsentCollected document event whenever consent changes.5

Handing Findings to the Client: Flow and Export

There are two ways to show the findings to a client. The first is showing the panel in a screen recording: new rows are briefly highlighted and group headers stay fixed while scrolling. The second is the Flow page:

  • Journey: a Sankey of the pages in visit order; bands are colored by page, consent or session.
  • Breakdown: the distribution across page, source, GA4 property or Ads account, event and consent.
  • Timeline: one lane per property or account, sid segments and the moments consent changed.

The Inspect Signals Flow page in Journey view: the product, cart and thank-you pages shown as bands in visit order. Below, a timeline with one lane per GA4 property and Ads account and dots for each signal

Properties the store does not own can be removed from the filter; the choice is remembered per store. Export saves the recording as JSON, and Open JSON on the Flow page shows the same recording again later. That way a meeting with a client can look at the same recording without placing another order.

Limits of the Method (For Now)

  • Server-side GTM (Google Tag Manager) and first-party collection endpoints are not visible. The hit that was sent is visible; what the server did with it is not.
  • “Did Google accept this hit?” stays unanswered. Only what the browser sent and the HTTP result are known.
  • Pixels running inside workers cannot be observed. Reading customer events depends on the presence of sandbox iframes. It works on pages with at least one custom pixel or iframe-based app pixel.
  • Reading customer events depends on Shopify’s internal message format. I could not find it documented as a public interface; if it changes, the reading breaks.
  • Records are kept in session storage: they are deleted when the tab or the browser closes. Each tab keeps the newest 1500 hits; hits beyond the limit are left out of comparisons and warnings.
  • Observations belong to individual setups. Pixel distribution and consent flow may differ on another store.
Reading Signal Sources in a Single Panel

Project page of a Chrome extension that reads GA4 and Google Ads hits from the storefront, app pixels and custom pixels, along with their payload and consent state, in a single panel.

Go to Project Page
What's inside
  • Per-hit source: Storefront, App pixel, Custom pixel
  • Order cards and 13 warning rules
  • Flow view and JSON export

Footnotes

  1. Web Pixels API (Shopify). “For app developers integrating app web pixels, pixels are loaded in a strict sandbox.” The page also states that pixels run within “one of our Lax or Strict sandboxes”. ↩
  2. Use a transaction ID to minimize duplicate conversions (Google Ads Help). “The transaction ID must be unique for every single transaction and must be dynamically generated by your website’s backend or e-commerce platform for each purchase.” ↩
  3. Consent Mode Decoder by DWC. The source used for the position and letter decoding of the gcd value; it is not a specification published by Google but a reference compiled through reverse engineering. ↩
  4. init standard API (Shopify Web Pixels API). The customerPrivacy field is defined on this page. ↩
  5. Customer Privacy API (Shopify). “The Customer Privacy API publishes the document event visitorConsentCollected when consent changes.” ↩
Key Takeaways
  • 01 In Shopify, GA4 and Ads signals can come from different layers (the storefront, one or more app pixels, one or more custom pixels); which source a hit came from is not written on the hit, and its payload and consent state have to be read parameter by parameter.
  • 02 A sandbox frame address carries the pixel type and ID; source detection relies on the /web-pixels@.../app|custom/web-pixel-<id>@... pattern.
  • 03 Order signals are collected into a single card by transaction_id; an Ads oid value that starts with GG_ is a per-hit identifier and does not count as an order key.
  • 04 The warning rules answer 'what was sent'; 'what Google accepted' and sGTM stay outside this view.
  • 05 A recording can be exported as JSON and reopened on the Flow page; a screen recording or a file is enough for talking to a client.
Frequently Asked Questions (FAQ)
+ Why isn't the source of a Shopify custom pixel hit directly visible in DevTools?

The initiator of requests sent from a sandbox iframe shows as null, so the request cannot be tied directly to a page source. The source is read from the address of the frame that sent the request: the /web-pixels@.../custom/web-pixel-<id>@... pattern in the address gives the pixel type and ID.

+ Is it an error when different properties send different item IDs and names for the same order?

Not on its own. In stores that use Shopify Markets, or in properties tied to different data sources, item ID and item name values can differ. Inspect Signals flags this difference as information, not as an error.

+ Does this method also show hits sent through server-side GTM?

No. The method reads the requests the browser sends to Google addresses. Requests sent to a first-party collection endpoint, and what Google accepts on the server side, are outside this view.

+ Is the recorded data sent anywhere?

No. The extension has no server; records are kept in the browser's session storage and deleted when the tab or the browser closes. Export writes a file only when the Export button is pressed.

Feedback

Share your thoughts on the selected paragraphs. Email is required so I can reply.

Type