# How ProductLense counts

The exact definitions behind every number ProductLense shows: visitors, sessions, channels, attribution, revenue, fees, profit, and where counts approximate.

This page defines every number in the dashboard and how it is computed. When a figure is approximate, the page says so and gives the error bound.

## Visitors

A visitor is one browser on one site on one UTC day:

```text
visitor_id = first 16 hex characters of sha256(site_id | ip | user_agent | daily_salt)
```

The salt is 32 random bytes generated for each UTC day. The IP address is used for the hash and the country lookup, then dropped; it is never stored. The same browser on the next day gets a different, unlinkable id, so a person who visits on three days counts as three visitors until your code calls [`identify`](/docs/track/identify-users). See [Cookieless visitor counting](/docs/privacy/cookieless-counting) for the privacy side.

## Sessions and bounce rate

A session is a run of events from one visitor with no gap longer than 30 minutes. Sessions are assigned in memory when events arrive.

```text
bounce rate      = bounced sessions / sessions × 100
average duration = mean of (last event time − first event time) per session
```

Bounce rate, duration, and entry and exit pages are pre-aggregated per day once a day is complete (90 minutes after UTC midnight). Days not yet rolled up are read live, so totals never miss sessions.

## Signups

Signups are the visitors who called `identify` in the range. With a channel, source, medium, campaign or referrer filter, signups use the visitor's first touch, the same attribution as the channel table.

## Channels

Every event is classified when it arrives. The first rule that matches wins:

| Order | Rule | Channel |
|---|---|---|
| 1 | URL has a `gclid`, `fbclid` or `msclkid` value | `paid` |
| 2 | `utm_medium` is `cpc`, `ppc`, `paid`, `paidsocial` or `display` | `paid` |
| 3 | `utm_medium` is `email` | `email` |
| 4 | Referrer is an AI assistant: chatgpt.com, chat.openai.com, perplexity.ai, claude.ai, gemini.google.com, copilot.microsoft.com, you.com, poe.com, meta.ai, grok.com, x.ai, mistral.ai (and their subdomains), or DuckDuckGo with `ia=chat` | `ai_assistant` |
| 5 | Referrer is a social network: twitter.com, x.com, t.co, facebook.com, instagram.com, linkedin.com, reddit.com, youtube.com, tiktok.com, threads.net, bsky.app, news.ycombinator.com, producthunt.com (and subdomains) | `social` |
| 6 | Referrer domain contains a search engine label: google, bing, duckduckgo, yahoo, baidu, yandex, ecosia, brave | `organic_search` |
| 7 | No referrer, but a source (`utm_source`, or `ref`, `via` or `source` when there is no `utm_source`) that names an AI assistant, social network or search engine by domain or by name, such as `chatgpt.com`, `producthunt`, `twitter`, `hn` or `google` | `ai_assistant`, `social` or `organic_search`, by the lists in rules 4 to 6 |
| 8 | No referrer and no source, `utm_medium` or `utm_campaign` | `direct` |
| 9 | Anything else | `referral` |

A referrer on your own hostname is treated as no referrer, so internal navigation never becomes `referral`. Rule 7 matters because browsers and apps often strip the referrer: ChatGPT, for example, adds `utm_source=chatgpt.com` to its links instead. UTM values for source and medium are lower-cased; campaign, term and content keep their case.

## First touch

The first pageview of each visitor fixes its first touch: channel, `utm_source`, `utm_medium`, `utm_campaign`, referrer domain, landing path and country. It is kept for the life of the site, after raw events expire, so a payment months later can still be attributed.

## Attribution

ProductLense uses first-touch attribution. When a payment is written, ProductLense looks for the paying customer's visitors in this order and stops at the first source whose visitors have a first touch:

1. The user id on the customer (from metadata keys `pl_user_id`, `user_id`, `userId` or `uid`, or the provider's own field, such as Polar's external id) matched to your `identify` calls.
2. The customer email, matched to the `email` trait sent with `identify`, or to a user id that is that email (case-insensitive).
3. A ProductLense visitor id in the customer metadata (`pl_visitor_id` or `visitor_id`).

When one user has several visitors, the earliest first touch wins. The result is stored on the payment row. Payments that could not be matched are tried again every night for 90 days, so an `identify` that arrives after the payment still credits the right channel. Details: [How payments are attributed](/docs/revenue/attribution).

## Money metrics

Every money number in ProductLense uses one definition, the same in the dashboard, the API, Ask and these docs. All amounts are USD at the payment date's ECB rate (original currency stored too). Only payments with status `succeeded` count. A channel's numbers are attributed by first touch.

| Name | Definition |
|---|---|
| Revenue | Charges − refunds − chargebacks, before processor fees. |
| Fees | Processor and merchant-of-record fees on those charges, minus fees returned on refunds. Estimated for providers that do not report fees (`fee_estimated_share` says how much). |
| Net revenue | Revenue − Fees. |
| Ad spend | Ad spend rows for the range, prorated by day. |
| Profit | Net revenue − Ad spend, i.e. Revenue − Fees − Ad spend. |
| Customers | Distinct customers with a first succeeded charge in the range (new customers). |
| CAC | Ad spend ÷ customers, for the same channel and range. Null when customers = 0. |
| ROAS | Revenue ÷ Ad spend. Null when spend = 0. |
| Payback | CAC ÷ average first-month net revenue per new customer. |
| MRR | Sum over active, non-trial subscriptions of the period price normalised to a month and divided by `interval_count`. |
| LTV | Average net revenue per customer over the customer's lifetime to date. |
| ARPU | Net revenue in range ÷ customers with a succeeded payment in range. |

Where Revenue and Profit appear side by side, the difference is always Fees + Ad spend; where Fees are shown, Net revenue = Revenue − Fees. For example:

```text
$2,407 revenue − $152 fees − $1,518 ad spend = $737 profit
```

### Fees per payment

Every payment keeps its original amount and currency, plus a USD amount converted at the European Central Bank reference rate for its day. When a day has no rate, the nearest earlier day within 10 days is used. The fee is what the processor or merchant of record kept, as reported by the provider. Providers that do not report fees get an estimate you set on the connection (a percentage plus a fixed amount), marked as estimated. Each provider page states which case applies; see [Refunds, fees and currencies](/docs/revenue/refunds-fees-currencies).

### Profit by channel

Ad spend joins revenue on channel, source, medium and campaign, and spend entered for a date range is spread evenly over its days. Rows without spend show no CAC, ROAS or payback. See [Profit per channel](/docs/profit/profit-per-channel) and [CAC, ROAS and payback](/docs/profit/cac-roas-payback).

## Conversion rate

```text
conversion rate = customers / visitors × 100
```

Per channel, both sides use the channel of the visitor's first touch.

## Where counts are approximate

Unique visitors and sessions are stored as fixed-size HyperLogLog states so that a 12-month range loads as fast as a day. The counts are exact for small sets and approximate for large ones:

| Report | Typical error |
|---|---|
| Site totals (Overview tiles and chart), countries, devices, browsers, operating systems | about 0.8% |
| Pages, sources, channels, campaigns, referrers, entry and exit pages | about 1.6% |

Below a few hundred to a few thousand uniques per row the count is exact. Pageviews, events, revenue, fees, spend and profit are always exact sums.

In breakdowns, ProductLense first picks the page of rows by pageviews and then counts unique visitors for those rows. A list ordered by visitors can therefore differ slightly at the cut-off from a list ordered purely by visitors over all keys; within the page, rows are sorted by visitors.
