# Cohorts

> Group sessions by any attribute your telemetry carries and rank the values by how much worse they are than everyone else. Nothing to register in advance.

A platform-wide error rate hides per-customer disasters. In a real thirty-minute window on our reference app, one merchant failed **45% of checkouts** while the overall failure rate read **7.8%** — a three-and-a-half point move nobody would page on.

Cohorts answers the question that number cannot: *who is this broken for?*

## What you need

One thing: spans carrying an attribute worth grouping by, and a session id to group them into.

- **Backend only** works. Any attribute on your server spans — `tenant.id`, `plan`, `deployment.region`, `feature.flag` — is a dimension.
- **Frontend included** works better, because failures that never reach your server (a bundle that 404s, a fetch that dies at the CDN, a redirect that never returns) have no server-side record at all. See [Browser instrumentation](/instrumentation/browser).

Nothing is registered in advance. Add an attribute today and it is a dimension on the next query.

## Reading the view

Pick a dimension. Every value becomes a row, ranked worst first, with the population rate marked on each bar so you can see the gap at a glance.

| Column | Meaning |
| --- | --- |
| **sessions** | How many sessions carried this value |
| **failed** | Sessions where any span errored or returned ≥ 400 |
| **rate** | failed ÷ sessions |
| **vs population** | This cohort's bar against the population rate |
| **lift** | Share of all failures ÷ share of all traffic |

**Lift is the number to read.** Above 1 means the cohort carries more damage than its size explains. A cohort at 8.4% of traffic and 48.6% of failures has a lift of 5.8 — that single figure is the finding.

## Drill down

Click a row to get the sessions in that cohort, worst first. Click a session to get **every span it touched**, across every service, ordered by time — the browser's page load, the fetch from the form, your API's server span, the database call, and the downstream service that actually failed.

> **Note:** Session drill-down scopes by trace id, not by the `session.id` attribute. Baggage rides on `fetch` and XHR, so page navigations and static assets never carry it — scoping by trace closes that gap, and works even when your frontend propagates nothing at all.

## How ranking works

Two choices that make the list survive contact with real data.

**Sessions, not spans.** "What fraction of this customer's spans errored" overweights whoever retried the most. The question teams actually ask is what fraction of their *checkouts* — or logins, or uploads — failed.

**A Wilson lower bound, not the raw rate.** A cohort with one failure in three sessions has a 33% failure rate and would top a naive list forever. Its 95% lower bound is about 5%, so it sinks on its own — no arbitrary "minimum 50 sessions" threshold anyone has to defend.

A session counts as failed if any span in it has an error status **or** an HTTP status ≥ 400. Both, because a 402 is often recorded with span status `Unset` — status alone would miss every decline.

## Cardinality

Dimensions are discovered by sampling attribute keys, then filtered:

- A key with one value is not a dimension.
- A key with roughly as many distinct values as sessions is an identifier, not a dimension.

Both are hidden automatically. Tens of thousands of distinct values per dimension is fine. Put unbounded identifiers — order ids, email addresses, full URLs — on spans if you want them for debugging, but do not expect to group by them.

> **Warning:** Rank on attributes your **server** stamps. Anything the browser writes is attacker-controlled: a user with devtools can reassign their session to another tenant. Client attributes are the right place for build version, experiment arm, or viewport — not for anything that decides attribution, billing, or an SLA.
