Analysis
Users, lifecycle & portfolio
Two pages built on vitrus.identify(user.id). Neither lists a
single person: every result is a count, with the query that produced it.
Setup
// required — wherever your app knows the user is signed in vitrus.identify(user.id); // optional — sharper numbers vitrus("signup"); vitrus("login"); vitrus("plan", { plan: "pro" }); // free · pro · cancelled — your own labels
No auth provider to connect and no admin keys to hand over: it works the same whether your app signs people in with Supabase, Clerk, Firebase, Better Auth or your own code.
Lifecycle: one app
A person is active in a range when at least one of their events falls in it. The range is compared with the period of the same length directly before it:
| Number | Means |
|---|---|
| New | active now, first ever seen inside this range |
| Retained | active now and in the previous period |
| Came back | active now, not in the previous period, seen before it |
| Lost | active in the previous period, not now |
| Net | new + came back − lost |
| Quick ratio | (new + came back) ÷ lost; empty when nobody was lost — not infinity, not zero |
| One-and-done | of the people first seen at least 7 days before the range ends, the share active on one day only |
| Became paying | moved to a plan that is not free, trial, none, cancelled, expired or inactive |
Active now = retained + new + came back, and previous = retained + lost: the bridge on the page always adds up, and a test pins it.
Users: one row per person, no person stored
Each signed-in user is a row under a stable made-up name (“Amber Falcon · 3f9a2c”), with:
| Column | Means |
|---|---|
| Status | returning — active on two or more days; one-time — one day, at least one action; signed up only — one day, no action |
| Actions | custom events your app sends (vitrus("invoice_sent")), not the automatic ones (page views, scrolls, sign-ins, errors) |
| Active days · Visits | UTC days with activity · sessions (30 minutes of inactivity ends one) |
| Joined · Last active | first and latest event on record |
Open a row for actions by day and the latest visits. Filter by status or plan, sort by any column. Counted all-time, from the first signed-in event on record — the page says that date: anything before Vitrus was added is not known and is not guessed. Members only: a public share link never shows a row per person.
Portfolio: every app
All sites of a workspace in one table — visitors against the previous period, active, new and lost users, net, signups, became paying, paying now and revenue per currency — plus a total row. Click an app to open it.
Limits, stated
- History bounds "new" and "came back". A person first seen before the oldest event on record (for example, past your retention period) counts as new when they return, and "came back" needs history older than the previous period. The page says how far back it can see.
- Days are UTC days for one-and-done: a visit from 23:50 to 00:10 is two active days.
- Filters do not apply. "Was this person active" has no sensible answer for "only visits from Germany".
- Strict privacy mode ignores
identify(), so these columns show — rather than 0.
API and agents
GET /api/people?site=SITE_ID&segment=returning&sort=last_active GET /api/people/IDENTITY_HASH?site=SITE_ID // actions by day, recent visits GET /api/lifecycle?site=SITE_ID&days=30 GET /api/orgs/ORG_ID/portfolio?days=7 // signed in GET /api/portfolio?days=7 // Authorization: Bearer vk_…
Over MCP the same numbers are get_lifecycle and get_portfolio, each
answer carrying its queries. Self-hosted: /api/lifecycle and /api/portfolio
on your own server, behind the dashboard password.