The largest project of the period: rebuilding an entire ecommerce measurement stack so it
survives a platform migration without losing a day of conversion data.
Ecommerce tracking
Built Purchase and Add to Cart into a Shopify customer pixel with conversion labels matched
to the old Ecwid site so historical reporting stays comparable, and verified the value field
against what the old site recorded rather than assuming. Added GA4 into the pixel with a
switchable test mode so it couldn't pollute live data, anchored the GA4 client ID to Shopify's
Web Pixels API client ID, and excluded my own IP as internal traffic.
The product ID problem
The hardest single problem I solved this period. Dynamic remarketing and purchase reporting
both need product IDs that match Merchant Center — but Merchant Center holds legacy Ecwid
IDs while the Shopify storefront exposes Shopify IDs. I spent days on it: checked the theme
liquid, inspected the Add to Cart button markup, read the network requests, and found the
Ecwid IDs sitting in Shopify product metafields — by browsing Shopify myself rather than
waiting on a developer. I then established exactly where the developer must surface them, and
wrote that as a dev-facing instruction including how they can verify it from their side.
I demanded exact citations for the claims in that instruction and rejected two rounds of sources
that didn't actually support the point.
Along the way I found the reason product view and add-to-cart events appeared to fire nothing:
the store wasn't publicly live yet. I checked that with a senior tracking specialist, then
summarised her call for the client document.
GTM in theme, forms and chat
Worked out where GTM belongs in the theme liquid and got it installed and confirmed live.
Mapped every form on the site — Contact, Trade-in/Sell, Request More Information,
Notify Me When Available, Affiliate, Careers, Newsletter, Credit — and gave a
recommendation per form on whether to track it in Ads, GA4, or not at all, rather than tracking
everything by default. Set up Enhanced Conversions per form and tested submissions live in GTM
preview, catching that the UPD tag was firing after the conversion tag and correcting the
sequencing. Investigated LiveChat and established its native GA4 app already covers chat events,
so no duplicate GTM setup was needed.
Go-live
Wrote the cutover procedure — connect pixels and pause GTM at the same moment — so the
migration wouldn't double-count, and gave the dev the exact clicks. Verified all tracking after
they went live, then investigated a GA4 transaction arriving from something other than our
pixel.
Iteration: the client document went through six versions, with formatting
and wording reworked repeatedly for a non-technical reader, and the GTM container import rebuilt
after I found it contained duplicated and auto-paused tags.
Working with their web agency: I corresponded directly with the client's
developers throughout — confirming Ecwid IDs were available, flagging clearly that form
submission tracking was not ready while ecommerce was, and putting specific questions to
them about how each form behaves rather than guessing. On launch day, 2 August, I monitored
delivery across the switch to catch anything breaking in the first hours.
Recovering the browser-switch purchases: their agency sent a video and
script for draft order tracking. I watched it, decided a call wasn't needed and said so rather
than taking the meeting, and set the direction: configure the CTM export for contacts carrying a
gclid so buyers who complete payment in a different browser can still be matched. Recorded a Loom
and wrote up the extra Shopify access needed to automate the draft order exports; the client had
deliberately restricted that permission, so it was granted temporarily and will be reduced again
afterwards.
Offline conversion pipeline, 10 to 13 August
Turned the draft order direction into a working pipeline, and it took three separate
systems. Built the CallTrackingMetrics webhook into an Apps Script endpoint writing every
call with its click ID to a sheet, and wrote the click-by-click setup for the CTM webhook
screen rather than describing it. Cleared two false starts of my own: the Apps Script
editor's Run button doesn't exercise doPost at all, and CTM's Test button
sends the most recent call immediately, so no real call had to be waited on. Caught that
the account ID in the code still pointed at a different client after CTM was repointed to
Exquisite, and added a helper that prints the correct account ID rather than leaving it to
be hunted.
Shopify access the current way. Shopify has killed the old static Admin API token
flow, so I moved it to a client-credentials app in their Dev Dashboard - the script mints its
own token each run, so there is no credential to copy or expire. Specified the three read
scopes and the custom-distribution install, and worked out that email and phone stay null
until Level 2 protected customer data access is enabled and the install link is reopened.
Where the click ID actually lives. The client sent a CSV of matches they had
recovered from NetSuite, with real gclids and the campaign names to match. I first assumed
Shopify held the same values and was wrong, so I stopped guessing and wrote a probe that
dumps every place attribution could hide on an order - customer journey summary, all
metafields, note and custom attributes - and ran it across the completed orders. Extended
the journey scan from first and last visit to every visit, so a click ID sitting in a middle
visit is caught, and kept the query under Shopify's cost ceiling by paging smaller.
The result was a negative finding worth having: Shopify genuinely does not hold the
gclid - NetSuite does, and Shopify only carries the campaign and source of the same
visit. Shopify does expose NetSuite customer and sales order IDs on every order, so there
is a clean bridge. Wrote the client the ask with a concrete matched example - one order
where NetSuite has the gclid and Shopify demonstrably does not - rather than a general
request, and set out the two ways forward: NetSuite API access, or writing the gclid onto
the Shopify order as a metafield from their side.
Asked the strategist to confirm in the client email thread that we want 90 days of
GCLIDs, so the request goes to the client as a specific number rather than an open-ended
data ask.
27 August. Chased the outstanding question with the strategist: whether the client's
developer came back on importing GCLIDs from their CRM into Shopify, which is the piece the
offline side still depends on. Nothing back yet.
2 September
Draft order import ready to go into Google Ads. Asked Joey to decide whether the conversion
should be primary or secondary before turning it on.
Post-launch conversion audit, 14 September
Six weeks after the store went live, audited every conversion action against live data rather
than against the migration plan. Every "misconfigured" or "awaiting activity" flag in the account
traced back to three causes. Draft orders are 45% of orders and 67% of revenue, and because
they are typed in the admin rather than bought through the storefront they carry no feed id, so
only 31% of purchased items matched Merchant Center. One watch reached Google under twelve
identities in 30 days. The migration had also silently killed the old chat and phone conversions,
and Submit Notify me had never fired once - proved at code level, the label was
declared but no handler referenced it. Found the two lead actions are crossed: the one named for
the contact form actually receives trade-in submissions and vice versa.
Five pixel versions, two independent reviewers each
Put every version through a Claude agent and a Codex agent working blind to each other, and
they found real bugs I had shipped. Phone-click dedupe keyed the raw tel: href, so one
number written four ways counted four times. Setting user data only when it existed meant a
conversion with no email inherited the previous visitor's identity. Dropping items with no
id destroyed GA4 item revenue, so free-text lines are kept as name-only items instead.
Withdrew my own proposal rather than defend it. I had floated a Cloudflare Worker serving a
product-id map as one option and then started writing about it as agreed work; the client's rule
is that the only thing we change is the Shopify pixel. Before dropping it I measured what it
would have been worth across 1,248 draft-order line items: it fixes at most 23.4%, because 64.4%
are free text typed by staff and unreachable from any pixel. So the blank item id goes away but the
Merchant Center match rate cannot be materially improved from the pixel, and I said that plainly
rather than implying the audit had solved it. Also dropped a deposit-and-balance filter after
review, because across a deposit and its balance the total value is already correct and filtering
would have deleted real revenue to fix the smaller problem.
Shipped the final pixel comment-free on request, stripped with a JS-aware pass rather than a
naive one because a plain // strip corrupts the regular expressions in the file, then
re-checked and re-ran the tests after stripping.
GA4 changes and the chat event, 14 September
Made the GA4 changes myself rather than handing over instructions: registered
lead_type and form_id as custom dimensions so leads can finally be split
by form, and rebuilt chat_started as an event-create rule. Disproved the assumption we
were both working from - chat_started was never LiveChat's own integration, it was a custom tag on
the old site - and proved mechanically why three other events died: they are all rules keyed on a
/thanks page that does not exist on Shopify. Did not delete them, since nobody asked
and it is destructive.
Dynamic remarketing pixel, 15 September
Audited the separate remarketing pixel and found its add to cart handler has never fired
once since launch. It reads line-item properties, and Shopify's cart line object has no
properties field at all. Both reviewers caught it independently. Rebuilt it around a variant-to-feed-id
cache seeded from the theme's own product-view event, and the reviewer then caught the bug that
would have repeated the same failure: the cache was keyed on a bare numeric id but looked up with a
value Shopify may deliver in a prefixed form, which would have missed 100%, silently, forever, and
been undetectable from the account because there is no historical data to compare against.
Accepted a correction rather than keeping the tidier claim: I had written that omitting the
items key "satisfies both reviewers", and it does not - it is the right fail-safe, but it still does
not meet Google's rule that the event carry an item. Also fixed pricing to use the post-discount
line value instead of list price.
Verified live, 16 September
Checked two days later instead of calling it done. Notify me recorded its first ever
conversions. chat_started came back, 17 events on the first day against zero since 1 August,
and it needed no pixel paste at all - the GA4 rule did it alone, exactly as predicted. Phone
conversions went from a blog-only trickle to 8 a day. Trade-in submissions went to zero as
designed. Leads now break down by form for the first time.
Nearly drew the wrong conclusion and caught it: almost every conversion looked down on
the two days after the change. It was reporting lag, not breakage, and I proved it three ways
before reporting anything - GA4 sessions for those days were less than half normal and still
processing, the last complete day was dead normal, and Google-hosted actions our pixel cannot
touch fell too.
Offline conversion import errors, 18 to 21 September
The imported offline conversion was showing errors, mostly "event timestamp precedes the click".
Worked out the share of matched orders that were recorded against those rejected, and corrected my
own figures twice against what the Google Ads interface showed before reporting any of it. The main
cause is gclids older than 90 days. Since Shopify has only been live about 50 days, those old clicks
cannot come from the storefront, so they are most likely pulled from the CRM. Told Joey the conversion
is in the account and working as it should on the current setup, then sent their developer a short
message asking how the gclid is picked before an order is pushed to Shopify, without claiming anything
was wrong.