← All case studies

Build

Greenfield Tracking Build

A SaaS product with no analytics at all. I built the measurement layer from empty, alongside a colleague's existing pipeline, without changing any of it.

ProjectInternal SaaS product
Starting pointZero analytics
StackGA4, web + server GTM, first-party subdomain
ScopeWeb container, server container, conversion tags
StatusShipped, verified with a live curl
01 The brief

A SaaS product with zero analytics: no GA4 property, no containers, nothing in place. The job was to build the full measurement layer, meaning the web container, the server-side container, first-party routing, and the conversion tags. Start from an empty slate and end with a pipeline that actually reports.

02 The architecture decision

Analytics and automation fail for different reasons.

A colleague already had work in the server container: a third-party automation lane that fanned events out over webhooks. I could have extended that and pushed GA4 through the same mechanism. Instead I built a GTM-native GA4 lane alongside it and left his work untouched.

There were a few reasons for that. Analytics and marketing automation tend to fail for different reasons, so it's better not to have them share a single path. I also didn't want GA4 reporting to depend on a third-party automation tool staying online. And his lane was working production code that I had no particular reason to modify, so modifying it would have added risk without adding value.

Two-lane server container: a new GA4 lane alongside the colleague's untouched automation lane Browser dataLayer WEB CONTAINER GTM-XXXXXXX 21 DLVs, 3 triggers First-party server subdomain SERVER CONTAINER GTM-XXXXXXX GA4 LANE (ADDED) forwarding tag + 4 conversion tags purchase, begin_checkout, trial, sign_up AUTOMATION LANE (UNTOUCHED) webhook fan-out to third-party colleague's prior work, left intact

Two lanes in one server container. I added the GA4-native lane and left the automation lane exactly as it was.

03 The build

Starting from empty, I set up a GA4 property and data stream, then built a web container with 21 Data Layer Variables and 3 Custom Event triggers. A Google tag routed first-party traffic through the server subdomain. From there I extended the server container with a GA4 forwarding tag and four conversion tags, covering purchase, begin_checkout, trial, and sign_up.

04 The catch worth writing about

An undefined Google tag can be Firebase, not a bug.

There are two things here worth being honest about, plus one finding that isn't documented anywhere.

The first is that I built a set of triggers in the wrong container. I caught it, deleted them, and rebuilt them in the right one. It's a small mistake, but catching it there cost a few minutes, whereas not catching it would have cost a confusing debugging session later on.

The second was a scare in Tag Assistant: three Google tags showing, one of them undefined. It looked at first like a duplicate-source problem, but it turned out to be an internal config push originating from Firebase. I confirmed that with a few console checks rather than assuming, and once confirmed it was clearly expected behavior.

# Tag Assistant: 3 Google tags, 1 undefined > config records present: 3 > the undefined one -> origin: Firebase auto-config verdict: expected, not a duplicate source

The finding worth recording: server-injected events don't show up in GA4 DebugView. The server-side tag template doesn't translate debug_mode into the wire flag that DebugView reads, so the events are firing correctly but stay invisible to that particular view. Server-lane verification has to be done through the outgoing-request view instead, which is not something the official docs mention.

# server event, checking GA4 DebugView DebugView -> (no events shown) outgoing request -> event present, 200 # template never sets the debug wire flag; use the request view
05 Verification

Rather than settling for a setup that looked right, I confirmed it end to end. I hit the full live path with a raw curl, no preview header, and got a 200 back with the data client's response body. That confirmed the whole chain in production: browser, web container, first-party subdomain, server container, and GA4.

$ curl -s -i 'https://server.example.com/g/collect?v=2&...' HTTP/2 200 # data client response body returned, no preview header
06 What this taught me

Neither lane can take the other down.

Building alongside someone else's work was a deliberate design choice rather than just a courtesy to a colleague. Keeping the two lanes independent means the GA4 lane and the automation lane each stand on their own, and neither one can take the other down if it fails. The details most worth writing down afterward were the ones no documentation would have told me: that server events skip DebugView, and that an undefined Google tag can be a Firebase config push rather than an actual bug.

← All case studies