Blog Product analytics and replay
Funnel analysis: sequential vs presence funnels
A funnel answers one question: of the people who started, how many got to the end, and where did the rest stop? The numbers only mean something if you know how each step was counted. Two tools can show different conversion for the same events because one checks the order of the steps and the other does not.
This post builds a funnel in ZipLogger from tracked events, shows the two counting modes side by side on the same data, and covers what to do when a step loses people.
Track the steps
A funnel step is an event name. You send events with track() from the browser SDK or Track() from .NET. A checkout flow in the browser:
import { ZipLoggerBrowser } from '@ziplogger/browser'
const ziplogger = new ZipLoggerBrowser({
endpoint: 'https://app.ziplogger.ai',
apiKey: 'YOUR_API_KEY',
release: '2026.09.29',
})
ziplogger.instrumentFetch()
ziplogger.track('cart_viewed', { items: 3 })
ziplogger.track('checkout_started', { total: 49 })
The last step usually happens on the server, where you know the payment went through. With ZipLogger.Extensions.Logging, inject IEventTracker:
using ZipLogger.Client;
app.MapPost("/orders/{id}/pay", (string id, PayRequest req, IEventTracker analytics) =>
{
// ... charge the card ...
analytics.Track("payment_captured", new { userId = req.UserId, orderId = id, amount = req.Amount });
return Results.Ok();
});
record PayRequest(string UserId, decimal Amount);
IEventTracker is registered by builder.Logging.AddZipLogger(...). Track never blocks and never throws.
Three details matter for funnels:
- Names are normalized. The server lower-cases a name and turns spaces and dashes into underscores, so
Checkout Startedarrives ascheckout_started. Step names you type into a funnel are normalized the same way. - Sequential funnels need a
userIdon the event. In the browser it comes fromidentify(). In .NET, pass it as theuserIdargument or as auserIdkey in the object. It is lifted out of the properties into the event's identity field. requestIdlinks the event to its request. The .NET SDK takes it fromActivity.Current. The browser SDK attaches the trace id of the last instrumentedfetchif it started less than 5 seconds earlier (requestCorrelationTtlMs). You need this later to get from a drop-off to an error.
Build the funnel widget
The funnel widget lives on a dashboard. A saved funnel with its own page, where you can open the people behind each drop-off, is a process report: see Funnel drop-off: who left and what they did next. For the widget, open Dashboards, click + Widget, and pick Funnel under Events. Then open the widget's ⋯ menu, choose Configure, and set:
- Steps, in order: two to eight event names, comma-separated, for example
cart_viewed, checkout_started, payment_captured. - Step order: Sequential (each step after the last, per user), which new widgets start with, or Presence in window (each step counted on its own). A widget saved before this option existed keeps presence.
- Count: sessions, users or events. This applies to presence mode only. Sequential always counts users.
The funnel covers the dashboard's time range. In sequential mode the table shows each step's count, a Conversion column, Drop-off from the previous step with the number of people lost, and a Median gap: the median time from the previous step. Presence mode shows only the count seen at each step and % of step 1's count, with no drop-off column, because its steps are counted independently. Each step name links to that event's own page.
Presence vs sequential, on the same data
Take four users and a three-step signup funnel: pricing_viewed → signup_started → signup_completed.
| User | What they did, in order |
|---|---|
| u1 | pricing_viewed, signup_started (2 min later), signup_completed (4 min later) |
| u2 | signup_started, pricing_viewed, signup_completed |
| u3 | pricing_viewed |
| u4 | pricing_viewed, signup_started (10 min later) |
We ran these events through the sequential chain ZipLogger uses (FunnelChain in the event store) and counted distinct users per step for presence. This is the output:
PRESENCE (distinct users per step, order ignored)
pricing_viewed 4
signup_started 3
signup_completed 2
SEQUENTIAL (FunnelChain)
pricing_viewed users=4 medianGapSec=null
signup_started users=2 medianGapSec=360
signup_completed users=1 medianGapSec=240
entered=4 ignoredWithoutIdentity=0
Presence asks "did this step happen at all in the window?" Every step is counted on its own. u2 counts at every step, even though they started signing up before they saw pricing. So presence reports 2 of 4 completed.
Sequential asks "did the steps happen in this order?" Each person holds a pointer at the step they are waiting for, and only that exact event moves it forward. u2's signup_started came before their pricing_viewed, so it does not count, and nothing after that can count either. Only u1 finished. The median gap for signup_started is 360 seconds, the middle of u1's 2 minutes and u4's 10.
Some consequences that follow from this:
- In presence mode a later step can be bigger than step 1. That is why the column is called % of step 1's count and not conversion, and why presence mode has no drop-off column.
- Presence user and session counts come from an approximate distinct count. Sequential counts are exact.
- Sequential mode can use the same event twice (
product_viewed, cart_viewed, product_viewed). Presence mode needs different events and says so on the widget. - Two steps in the same millisecond still advance, so batched server-side tracking is not penalized.
Use presence for a quick "are people reaching each page" view. Use sequential when you want to report conversion.
Conversion windows
There is no per-step timeout such as "completed within 24 hours of starting". The only time limit is the funnel's time range. To ask "converted within 7 days", set the dashboard to Last 7 days, the widest range the dashboard picker offers. The API itself accepts up to 31 days. For a deadline per person, use a process report: its conversion window runs from each person's own entry, from 1 second to 90 days.
The range cuts both ways. Someone who did step 1 before the range started and step 2 inside it never enters the funnel, because the query cannot see their step 1. A funnel over a short range therefore undercounts long journeys.
The login boundary
Most signup funnels start before the user has an account, and that is where sequential funnels catch people out. Here is what the browser SDK actually sent in a test run for pricing_viewed, signup_started, then identify() and signup_completed:
{"type":"track","name":"pricing_viewed","anonymousId":"anon_9ccaa9d026e116090001","sessionId":"sess_fc9f96790ed480714cb3", ...}
{"type":"track","name":"signup_started","anonymousId":"anon_9ccaa9d026e116090001","sessionId":"sess_fc9f96790ed480714cb3", ...}
{"type":"identify","userId":"user_42","anonymousId":"anon_9ccaa9d026e116090001","sessionId":"sess_fc9f96790ed480714cb3", ...}
{"type":"track","name":"signup_completed","userId":"user_42","anonymousId":"anon_9ccaa9d026e116090001","sessionId":"sess_fc9f96790ed480714cb3", ...}
The first two events have no userId. Sequential funnels group by userId and skip events without one. The widget says how many it skipped: "events had no user id and could not be attributed to anyone". The identity link that identify() creates is not applied to funnel counts, so this person never enters the funnel at step 1. Process reports that count People do apply the link, so the anonymous steps before identify() count for the same person.
Two ways around it:
- For steps before sign-in, use presence mode with Count set to sessions. The session id stays the same across
identify(), as the output shows. - Start the sequential funnel at the first step where the user is signed in.
Breakdowns
The funnel widget has no "break down by property" option. To compare segments, use Duplicate in the widget menu and give each copy its own widget filters in Configure: Country, Version, Service, Environment, or a Property filter such as plan is business. When the dashboard and the widget both set the same dimension, the widget's value wins. Two funnels side by side, one per release, is the usual way to see whether a deploy changed conversion. A process report has a Segments tab that splits the people at one step by device, browser, release, country or a property of the entry event.
The funnel API also takes a saved cohort (?cohort=<id>), but the dashboard has no cohort picker yet. A process report's Audience setting takes a saved cohort of up to 10,000 members. The retention and cohorts post covers cohorts.
From a drop-off to the errors behind it
A drop-off tells you where people stopped. The next question is whether something broke there. ZipLogger keeps events and logs in one place, joined on the trace id, so there are three ways in:
- Click the step name. The event's page shows Requests sampled, Requests failed and Failure rate: of a sample of the requests this event happened in (up to 200), how many also logged an error. Under Errors on these requests it lists the error patterns.
- Add an Event → errors widget next to the funnel, set to the step that loses people. It shows the same failure rate and top errors, and it follows the dashboard's filters.
- Start from the error. In Search, open an error pattern and click What were users doing?. It finds the events on the failing requests and shows the most common path of events into the error, how many users were affected, and links to example session journeys.
All three depend on requestId. Browser events get it only when instrumentFetch() is on and the event is tracked within 5 seconds of the request. Server events get it from the current trace. Logs need a traceId field, which the .NET logging provider and the Serilog sink add from the current Activity. The browser-to-backend tracing post covers that setup.
Limits
- Sequential funnels have a scan limit. They read every step event in the range. Past the server's limit the widget shows the real count and what to do: "Narrow the time range, add a filter, or use presence mode."
- Nothing is precomputed. Every funnel is answered from raw events when the widget loads.
- Exclusions apply. A dashboard filter that excludes one of the funnel's own step names makes that step zero.
- Anonymous-only traffic is excluded from sequential funnels. It is not attributed by session or anonymous id.
To see what a single person did between steps, the user journeys post covers profiles and session timelines. The SDK calls are in the product analytics docs. Or create a free workspace, send a few events, and add a Funnel widget to a dashboard.