---
title: "The Funnel You Can't See: Why Your Analytics Skip the Intermediate Steps"
canonical: https://dxdev.com/blog/2026-08-22_the-unseen-funnel/
datePublished: 2026-07-11
---
`sign_up`, `begin_checkout`, and `purchase` were the only 3 events in a customer journey with 8 decisions that mattered.

The analytics dashboard looked finished at first glance. It showed traffic, final sales, and revenue. What it could not show was the part we needed to understand: what happened after a customer created an account and before they decided to pay.

That missing stretch was not a reporting problem. It was an instrumentation problem. We had never emitted the actions in the middle of the journey, so no funnel view could recover them later. A chart can arrange events you collect. It cannot reconstruct an event that never existed.

## The dashboard was answering the wrong question

The immediate question was simple: where do customers stop moving? We could see an account get created, and we could see a completed transaction. Everything in between was a blank interval.

That blank interval made every conversation speculative. If a customer did not become paid, did they fail to configure the site? Did they never add a roster? Did they stall before creating a schedule? Did they reach checkout and quit? The dashboard flattened all of those paths into the same nonanswer, and every business decision about where onboarding needed work had been resting on that nonanswer for as long as this dashboard existed. We were not short on data. We were making real calls on which part of the funnel to fix with three data points covering an eight-step journey.

I started by inspecting what actually fired on a clean page load and comparing it with the analytics configuration. That uncovered two active GA4 properties, an old Universal Analytics tag that was still loading, and two tag containers. At a glance, it looked like a data cleanliness problem. It was not.

Both GA4 properties had real traffic, so traffic volume was useless as a decision rule. The canonical property was the one linked to the ads account and already receiving purchase conversions. A new onboarding event belongs beside the purchase it is meant to explain. Wiring it into the other property would have created a prettier shadow dashboard and split the funnel one more time.

Before the fix, `sign_up` told us that an account had been created. `begin_checkout` told us that a customer reached the payment handoff. `purchase` told us they became paid. Nothing told us whether they had done useful setup work between those points.

The missing section was the product. We had instrumented the bookends.

## Mapping the eight decisions before touching a tag

I wrote the 8 node map before changing the implementation. It started with account creation, followed the setup checkpoints a customer actually encounters, then carried through the commercial handoff and paid conversion. The point was not to manufacture an ideal marketing funnel. It was to name the state changes that already existed in the product.

Four setup transitions were the immediate holes: `site_configured`, `roster_added`, `schedule_added`, and `website_edited`. Those are not page views. They happen when a customer saves durable work. A customer can visit the roster page, leave without entering anyone, and still generate a page view. Calling that progress would poison the funnel from day one.

The final implementation used one GA4 event, `onboarding_step_complete`, with an event scoped `onboarding_step` value. The event could represent each real checkpoint without turning the analytics schema into a pile of nearly identical tags.

```js
dataLayer.push({
  event: "onboarding_step_complete",
  onboarding_step: "roster_added"
});
```

The important choice was where that push originated. We put it at the server side save point, then passed it through the existing server to browser data layer bridge and the primary tag container into the canonical GA4 property. The event comes from the action that persists the work, not from the page that happens to display the form.

```text
successful save
  -> server emits onboarding step
  -> browser data layer receives it
  -> tag container matches the event
  -> canonical GA4 property records it
```

I considered three easier routes and rejected all of them. First, we could have built a report from the existing GA4 data. That lost because the intermediate events were absent. A better query cannot retrieve missing observations.

Second, we could have reused page view triggers for the setup pages. That would have been fast, but it would measure intent to look at a task, not completion of the task. It also would have made a refresh look like progress.

Third, we could have hardcoded direct analytics calls in the display code. That would have bypassed the existing tag management boundary and duplicated configuration in pages that were already old and inconsistent. The server save hook and data layer bridge were already the supported path. We extended that path rather than creating a second one.

We also left the existing `sign_up`, `begin_checkout`, and `purchase` events alone. Replacing working bookends while adding the middle would have multiplied the variables in the same release. The duplicate GA4 property, the dead Universal Analytics tag, and the stray container became a separate cleanup ticket. They were real debt, but they were not the reason the funnel was invisible.

## The first test still recorded nothing

The initial local test made it look as if the new events were broken. I could see the data layer push, I could save a real setup action, and the analytics property showed nothing. That narrowed the problem sharply. The save point was working. The data was reaching the browser. The failure lived between the container and GA4.

The tag had been configured to fire too early. The server generated the setup event before the GA4 configuration tag had finished initializing, and the event tag was tied to the base page load timing. There was no visible error. The event was simply dropped.

We changed the event tag trigger to **Window Loaded**. That preserved the server generated event in the data layer while allowing the GA4 configuration to come up first. On the next local run, the event flowed from a real saved action through the container and into the canonical property.

That was the verification path I wanted. Not a green configuration screen, and not a tag preview alone. We tested the actual state change on a local site, published the corrected tag setup, then confirmed a real event landing in the production analytics property. The code shipped in release 3.358.

The funnel did not become useful because we built a dashboard. It became useful because release 3.358 gave the middle of the journey four real checkpoints instead of zero. Now, when a customer fails to become paid, we can tell whether they never configured anything or whether they built a roster and schedule, reached checkout, and stopped there. Before this release, both of those customers were the exact same blank interval.
