Blog Product analytics and replay
Funnel drop-off: who left and what they did next
A funnel chart tells you how many people stopped between two steps. It does not tell you who they were, whether they still had time to finish, or what they did instead. Those are the questions you need answered before you can fix anything.
ZipLogger answers them with process reports, under Events → Processes. A process report is a saved, versioned funnel. Behind every drop-off number is the list of people it counts, with their session, replay, errors and the next thing they did.
How it differs from the dashboard funnel widget
The Funnel widget on a dashboard counts steps. A process report follows each person. The funnel analysis post covers the widget's presence and sequential modes. Here is what changes:
| Funnel widget | Process report | |
|---|---|---|
| Where it lives | A widget on a dashboard | Events → Processes, a saved report with its own page |
| Time allowed to finish | The dashboard's time range | A conversion window per person, from 1 second to 90 days |
| Who is one person | Sequential mode uses the userId and skips events without one | People (user ids and anonymous ids joined by identity links) or Sessions |
| What a step is | One event name | Up to 10 event names, and up to 5 conditions such as page is /pricing |
| People who still have time | Counted in the drop-off | Still has time, and kept out of the final rate |
| Changing it | Edits the widget in place | Every save is a new version. Older runs keep their numbers |
| The people behind a number | Not available | A list per step, with evidence and links |
Use the widget for a quick look next to your error panels. Use a process report when a drop-off needs explaining.
Track a checkout
A step is an event your app sends. This is a five-step checkout with the browser SDK, @ziplogger/browser:
import { ZipLoggerBrowser } from '@ziplogger/browser'
const ziplogger = new ZipLoggerBrowser({
endpoint: 'https://app.ziplogger.ai',
apiKey: 'YOUR_API_KEY',
release: 'web-1.9.0',
})
ziplogger.instrumentFetch()
// Product page. Properties you want to break down by go on this first event.
ziplogger.track('product_viewed', { sku: 'tent-2p', category: 'tents' })
// Cart
ziplogger.track('added_to_cart', { sku: 'tent-2p' })
// Checkout: say who it is as soon as you know
ziplogger.identify(customer.id)
ziplogger.track('checkout_started', { order_id: order.id, shipping: 'express' })
// Payment: track right after the request, so the event carries its trace id
await fetch(`/api/orders/${order.id}/pay`, { method: 'POST', body: JSON.stringify(payment) })
ziplogger.track('payment_submitted', { order_id: order.id, method: 'card' })
The last step happens on the server, where you know the payment went through. In .NET, with IEventTracker from ZipLogger.Extensions.Logging:
using ZipLogger.Client;
app.MapPost("/api/orders/{id}/pay", (string id, PayRequest req, IEventTracker analytics) =>
{
// ... charge the card ...
// AnonymousId is the browser's ziplogger.identity.anonymousId, for guest checkouts.
analytics.Track("purchase_completed", new { order_id = id },
userId: req.UserId, anonymousId: req.AnonymousId);
return Results.Ok();
});
record PayRequest(string? UserId, string? AnonymousId);
Why it is written this way:
- The names match the Checkout template. The builder looks for
product_viewed,added_to_cart,checkout_started,payment_submittedandpurchase_completedfirst, then common aliases such asadd_to_cartororder_completed. identify()joins the anonymous steps to the buyer. When the report counts People, the anonymousproduct_viewedandadded_to_cartcount for the same person as the signed-in steps after them. The widget's sequential mode skips them, because they have nouserId.- Breakdown properties are read from the first step's event. That is the event that makes someone enter. A property that only appears on a later step, like
shippinghere, is empty for everyone in a breakdown. payment_submittedcarries the request's trace id. WithinstrumentFetch()on, an event tracked within 5 seconds of an instrumentedfetchgets that trace id as itsrequestId. The drill-down uses it to look for an error logged on the same request.- The server event has no session id and no device. That is fine when counting People. Counting Sessions would leave it out.
Build the process
- Open Events and click Processes in the view switcher, then New process. Creating and editing needs the Editor role. Everyone in the workspace can open and run a report shared with the team.
- Pick the Checkout template. The builder checks it against the event names your app sent in the last 30 days and says either "All 5 steps matched events your app sends" or how many it could not find. A step it could not find is marked Not tracked yet, with the
track(...)call that would produce it. - Check each step. A step has a Step name, Events that count as this step (any one of them counts) and optional Conditions. Two steps can use the same event with different conditions, for example
page_viewwhere page is/pricing, thenpage_viewwhere page is/checkout. Conditions compare text exactly, so case matters. - Under Counting, set Count by to People and check the Conversion window. It is how long each person has, measured from their own entry, not from the date range. The Checkout template starts at 1 day.
- Leave Entity property empty here. It makes every step of one chain carry the same value, such as
order_id, so two orders are never stitched together. Every step's event must then have that property, andproduct_viewedhappens before an order exists. It suits a shorter process, such ascheckout_started → payment_submitted → purchase_completed. - Under Who counts, put device, browser, version and country filters under Only the first step. Later events, like a purchase recorded on the server, do not carry them. Use Every step for what all events have, such as environment is
production. A device filter under Every step gets a warning that it usually shows 0%. Audience limits entry to a saved cohort. - Add up to three Breakdown properties, and check the Time zone. It decides where a calendar day starts.
- Click Preview last 7 days. It runs the definition for real over the last seven whole days and saves nothing. Then click Create report.
Read the numbers
The report opens on Last 7 days of entries. The presets go up to Last 90 days, or you can type a range: From (included) and To (not included), in the report's time zone. Below it, the page states the cut-off: "Events are observed up to [time] (not including it)." Re-run now moves the cut-off to the current time.
Three rules decide every number:
- Entry. A person enters at their first
product_viewedinside the range. A later product view does not restart their clock. - Order. Steps must happen in order, but other events in between are fine. One event counts for one step at most.
- The window. Each person has their own window from their entry. A purchase after the end of the range still counts if it falls inside that person's window.
The cards across the top are Entered, Completed (final), Completed so far (provisional), Still pending, Did not complete (closed), Coverage unknown and Time to complete.
The final rate only counts people whose whole window has closed. Someone who paid two minutes after entering stays out of it until their day is over. Otherwise the fastest recent buyers would raise the rate while slower ones are still deciding. They are counted in the provisional figure instead.
A run on our demo data (a fictional shop, no real customers) had 245 people enter in the last seven days. Not all of them had a closed window yet, so the headline read:
20.6% of the 199 people whose 1 day window has closed completed the process (41 of 199).
Below the cards, the Funnel draws each step with a solid part (window closed, final) and a hatched part (still within their window, provisional). The Transitions table puts everyone who reached a step into exactly one of four states, and the four add up to Reached:
- Moved on: reached the next step before their window closed.
- Still has time: has not moved on, but the window is still open. Not a drop-off.
- Did not move on: the window closed, fully observed, without the next step.
- Coverage unknown: part of the window is older than the events the workspace keeps. Never counted as a drop-off.
Where people stop names two things that are often different steps: the Largest loss by count and the Highest loss rate. A step is only ranked by rate when at least 20 people there have a closed window, and fewer than 30 is marked as a small sample.
Open the people who dropped
Click Open people on a transition row, or the "did not move on" button under a step in the funnel. A panel opens next to the report, for example "People who reached Added to cart and did not reach Started checkout". Buttons at the top switch to the people who moved on, still have time, have unknown coverage, or everyone who reached the step. The panel has three tabs.
People lists them 25 at a time, with Load more. Each row shows:
- Who: a masked id, and whether it is a signed-in user or an anonymous visitor. Raw ids never go into a URL.
- Timing: when they entered and when their window closes.
- State and the last step they reached.
- Evidence around that last step: whether a replay exists, whether an error was logged on the same request, and what happened next.
- The events that matched each step, with their time and request id.
- Links: Open session around this step, Watch replay (only when a recording exists) and Investigate, which joins events, logs and traces around that event.
Export these people (CSV) downloads the same list.
What they did next
"Next" is the first event that belongs to that person after the last step they reached, up to the end of their window. It uses the same identity links the report ran with, so anonymous activity on a linked browser counts. It is one event, not a path. Next: product_viewed says they went back to browsing. It does not say why.
The chip has one of these labels:
| Label | What it means |
|---|---|
| Next: event name | The first event found, with its time |
| No further activity observed before the window closed | The whole window was searched and nothing was found |
| No further activity observed so far | Nothing yet, but the window is still open |
| Further activity not fully searched | The search stopped early, so absence is not claimed |
| Further activity unavailable | The lookup failed or ran out of time |
| Not observable (cutoff) | The report's cut-off falls before this step, or there is no id to look it up by |
The search stops early when one person has more than 25 linked devices, after 5 pages of 100 events, or when the window overlaps history that retention deleted. It says not fully searched rather than "nothing". Above the list, a note says which event store it read from and when. This evidence is read live, so an event that arrived after the report ran can show up here, while the counts stay as they were.
Segments and Evidence
Segments splits the people who reached the step by something their entry event had: Device type, Browser, Operating system, Release, Country, or one of your breakdown properties. Only people with a closed window are compared. Rows are ranked by how many did not move on, not by rate. To use a property the report does not capture, choose Break down by another property… and Capture and run again. It reruns with the same range and cut-off.
Evidence puts the people who moved on next to the people who did not, and compares what was recorded: With a session id, With a request id, Replay available (of those sampled) and Request had an error (of those checked). Look at the coverage rows first. If one group carries request ids far more often, a difference in errors says more about your logging than about your customers, and the page warns you when that happens.
Versions, shared links and "the events changed"
Saving a change to the definition, such as the steps, window, count unit or filters, creates a new version. The header says which one you are looking at, such as "v2 of 3", and an older version gets a banner telling you not to compare it with the current one as if nothing changed.
The page address holds the range, the version and the cut-off. A reload, or a teammate opening your link, asks the same question about the same stretch of time. Events that arrive late, with timestamps before the cut-off, can still change the answer.
A run, and the people lists behind it, is kept for 30 minutes. After that you get "This run of the report has expired" and run it again. Within those 30 minutes, the server sometimes has to compute the run again, for example after a restart. If the result is not exactly the same, it answers 409 execution_changed instead of showing people who no longer add up to the number you clicked. The page says "The events changed since this report ran", with a Run the report again button.
If the workspace's retention policy could not be read when the report ran, nothing is confirmed against it. There is no final rate and nobody is counted as not moving on. The page says "No final rate: the retention policy could not be read", and Advanced details shows Event retention: Not verified.
To keep a process on a dashboard, add the Process conversion widget and pick the report. Latest version follows your edits. Pinning a version number keeps the widget on that version.
What it does not do
- No retries. Each person enters once, at their first first-step event in the range. A second attempt is not a new entry.
- No strict sequences or excluded steps. Steps must be in order, but anything can happen between them, and you cannot say "without doing X in between".
- No accounts. It counts people or sessions, not companies.
- No paths. "Next" is one event. There is no tree of everything people did after they stopped.
- No causes. A segment, an error on the same request or a replay is evidence. The page says so in its own words.
- No alerts and nothing precomputed. Nothing notifies you when conversion drops. Each open of the report or its dashboard widget runs it again from the raw events.
- Storage-cap deletions are invisible. Coverage unknown covers events deleted by the retention period. If events were deleted because the workspace hit its storage cap, the report cannot tell, and people whose window touched those days can show as not moving on.
Limits
Every run reads the matching events, so there are budgets. These are the server defaults:
| Limit | Default |
|---|---|
| Steps | 2 to 8 |
| Event names per step | 10 |
| Conditions per step | 5 |
| Filters, both scopes together | 10 |
| Breakdown properties | 3 |
| Conversion window | 1 second to 90 days |
| Entry range | 92 days |
| Events read per run | 150,000 on OpenSearch, about 1,000,000 on ClickHouse |
| People or sessions per run | 250,000 |
| Anonymous ids looked up for identity | 200,000 |
| Time per run | 60 seconds |
| Runs at once per workspace | 2 |
| Audience size | 10,000 members |
| Rows per CSV export | 50,000 |
| Saved reports per workspace | 100 |
The report counts the candidate events before it scans. Over the budget it refuses with the real numbers ("This report is too large to run as asked") instead of sampling. Narrow the entry range, add a condition to a step, or count by session. Advanced details shows how close a run came: Events examined, out of the number allowed for one run, and which Event store that answered.
For the request id behind the error evidence, see the browser-to-backend tracing post. The SDK calls are in the product analytics docs. Or create a free workspace, send the five checkout events, and open Events → Processes.