---
title: "Release 3.358 and the Missing Middle of Our Funnel"
canonical: https://dxdev.com/blog/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 dashboard looked finished at first glance. It showed traffic, final sales, and revenue. What it could not show was the stretch we needed: what happened after a customer created an account and before they decided to pay.

That 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.

## A blank interval under every decision

The 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 blank.

So every conversation about it was a guess. If a customer did not become paid, did they fail to configure the site? Did they never reach the `members_added` step? 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 we had been making real calls about which part of onboarding to fix on three data points covering an eight-step journey.

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

Both GA4 properties had real traffic, so traffic volume was useless as a way to pick one. 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 given us a prettier shadow dashboard and split the funnel one more time.

Before the fix, `sign_up` told us an account had been created. `begin_checkout` told us 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. We had instrumented the bookends and skipped the product.

## Eight nodes on paper first

We 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 to name state changes that already existed in the product.

Four setup transitions were the immediate holes: `site_configured`, `members_added`, `schedule_added`, and `website_edited`. Each one happens when a customer saves durable work. A customer can open the page for the `members_added` step, leave without entering anyone, and still generate a page view. Calling that progress would poison the funnel from day one.

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

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

The choice that mattered was where the push originates. We put it at the server side save point and 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, and the page that happens to display the form has no part in it.

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

We left `sign_up`, `begin_checkout`, and `purchase` alone. Replacing working bookends while adding the middle would have multiplied the variables in one 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 why the funnel was invisible.

## The first test recorded nothing

The initial local test made the new events look broken. We could see the data layer push, we could save a real setup action, and the analytics property showed nothing. That narrowed it sharply. The save point worked. The data reached the browser. The failure lived between the container and GA4.

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

We changed the event tag trigger to **Window Loaded**. That kept the server generated event in the data layer while letting the GA4 configuration 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 we wanted. 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.

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 reached `members_added` and `schedule_added`, reached checkout, and stopped there. Before this release, both of those customers were the same blank interval.

If your analytics record only the two ends between signup and payment, a chart will never recover the steps between them. Emit each checkpoint from the action that saves the work, and confirm a real event lands in the production property.
