Funnels & Cohorts

Funnels and cohorts are the two "second-order" analyses that turn the raw event stream into business-readable numbers. Both run against BigQuery directly (no rollup tables — the queries are fast on the partitioned + clustered events table). Both share the same identity-stitching layer so they can be run in either visitor mode (one row per anonymous browser) or user mode (one row per signed-in person across devices).

Funnels

A funnel measures conversion across an ordered series of events.

Defining a Funnel

Open Web Analytics > Funnels > + New. The builder collects:

Field Notes
name Display name.
steps Ordered list of event names. Use Event Definition names for stable references; raw auto-events are fine too.
stepFilters Optional per-step filter — same field/op/value shape as Event Definitions.
lookbackDays Max time between step N and step N+1 for the visitor to be counted as having progressed. Default 30.
groupBy 'visitor' (default) or 'user'. See Visitor vs User Mode below.
segmentId Optional pre-filter by Segment.

How the Query Runs

funnelQueryService.computeFunnel() issues a single BigQuery query that:

  1. Selects every row matching any of the step event names within the lookback + date range.
  2. For each visitor/user, sorts events chronologically.
  3. Walks the steps in order, requiring each subsequent event to occur within lookbackDays of the prior one.
  4. Aggregates by step into counts: step_1_visitors, step_2_visitors, ..., and per-step time-to-progress percentiles (p50, p90).

The result is what you see in the funnel chart — absolute count per step, plus the implicit drop-off between each.

Visitor vs User Mode

This is where identity stitching matters.

User mode requires the visitor to have called karma('identify', ...) at some point in their history. If they never did, user mode falls back to visitor_id for them — so user mode is a superset of visitor mode, not a different population.

User mode SQL adds a join roughly like:

LEFT JOIN identity_map idm
  ON events.tenant_id = idm.tenant_id
  AND events.site_id  = idm.site_id
  AND events.visitor_id = idm.visitor_id
GROUP BY COALESCE(idm.user_id, events.visitor_id)

Funnel Output Shape

{
  "funnelId": "...",
  "groupBy": "user",
  "computedAt": "2026-05-22T...",
  "steps": [
    { "name": "page_view",      "count": 12480, "filter": null },
    { "name": "add_to_cart",    "count":  1872, "filter": null,
      "timeToHere": { "p50_sec": 12, "p90_sec": 38 } },
    { "name": "checkout_start", "count":   942, "filter": null,
      "timeToHere": { "p50_sec":  9, "p90_sec": 22 } },
    { "name": "purchase",       "count":   311, "filter": null,
      "timeToHere": { "p50_sec": 14, "p90_sec": 47 } }
  ],
  "totalConverted": 311,
  "topConversionRate": 0.0249
}

The UI renders this as the familiar shrinking-bar funnel with drop-off percentages between each step.

Cohorts

A cohort measures retention — what percentage of users from a starting cohort return on day N, week N, or month N.

Defining a Cohort

Web Analytics > Cohorts > + New collects:

Field Notes
name Display name.
cohortEvent The event that defines membership (e.g. page_view for "anyone who visited", signup for "new users").
returnEvent The event that counts as "returning" (e.g. page_view, or a more specific conversion).
period 'day' | 'week' | 'month'.
numPeriods How many periods of retention to compute (default 12).
groupBy 'visitor' or 'user', same semantics as funnels.

Output Shape

A cohort table — rows are cohorts (e.g. "users who signed up week of 2026-05-01"), columns are periods (week 0, week 1, week 2, ...). Each cell is the percentage of cohort members who fired returnEvent at least once in that period.

Cohort week        | Size | W0    | W1    | W2    | W3    | ...
2026-05-01         | 1247 | 100%  | 42%   | 31%   | 27%   | ...
2026-05-08         | 1418 | 100%  | 45%   | 33%   |  —    | ...

Period 0 is always 100% by definition (everyone in the cohort fired the cohort event in period 0). Period N% is the retention number.

How It's Computed

cohortService.computeCohort() runs one BQ query that:

  1. Selects everyone who fired cohortEvent in the time range, grouping by their cohort period.
  2. Selects everyone who fired returnEvent in any period.
  3. JOINs the two on (visitor_id | user_id) and counts unique IDs per (cohort period, return period).
  4. Divides by cohort size to produce the percentage.

User mode applies the same identity_map LEFT JOIN as funnels.

Segments

A Segment is a saved filter you can attach to funnels or cohorts to scope the analysis. Examples:

Segments are stored in segmentModel per tenant and reference one or more conditions on the standard event fields + UTM + device + geo. They compose with the funnel/cohort groupBy — a user-mode cohort of mobile users counts unique signed-in mobile humans, not unique signed-in browsers that happened to be mobile.

When to Use Which

Performance Notes

What These Analyses Don't Do (Yet)