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:

NumberMeans
Newactive now, first ever seen inside this range
Retainedactive now and in the previous period
Came backactive now, not in the previous period, seen before it
Lostactive in the previous period, not now
Netnew + came back − lost
Quick ratio(new + came back) ÷ lost; empty when nobody was lost — not infinity, not zero
One-and-doneof the people first seen at least 7 days before the range ends, the share active on one day only
Became payingmoved 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:

ColumnMeans
Statusreturning — active on two or more days; one-time — one day, at least one action; signed up only — one day, no action
Actionscustom events your app sends (vitrus("invoice_sent")), not the automatic ones (page views, scrolls, sign-ins, errors)
Active days · VisitsUTC days with activity · sessions (30 minutes of inactivity ends one)
Joined · Last activefirst 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.

The totals add apps together. The same person signed in to two of your apps is counted twice. That is the privacy model working: ids are hashed per site, so no one — us included — can tell they are the same person.

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.