One panel you can check.
Traffic, users and revenue for your websites, iOS and Android apps — side by side, without storing who your users are.
$ <script defer data-site="SITE_ID" src="https://app.vitrus.dev/v.js"></script>
Every number opens the query that counted it — for you, or for ChatGPT and Claude over MCP.
Free plan, no card · 3.0 KB script · Apache-2.0 core · the demo needs no account
This is the product, not a drawing of it. Click a figure on any page and the SQL, its parameters, the window and the result are right there — the same evidence our AI summary is checked against.
Screenshots are of the shipped dashboard running on a demo workspace, and say so on the image itself — the figures are sample data, not a customer's. Open that workspace yourself →
Built five apps with AI this year? Stop opening five admin pages to see who signed up. Add one line where your app knows the user is signed in, and Vitrus counts who is new, who came back, who you lost and who started paying — for every app, side by side, web and mobile.
vitrus.identify(user.id) // hashed in the browser, never sent raw
Open any app and see each signed-in user: returning, one-time or signed up only, their actions, active days and visits — as “Amber Falcon”, because there is no email or name on our side to show.
The script sends sha256(site + id); the server hashes that again with a secret.
Your users' ids — often emails — never reach our servers.
The same person in two of your apps is two unrelated hashes, so nobody — us included — can join them. Email, phone and name fields are dropped in the browser and again on the server.
Plus a Lifecycle page per app — new, came back, lost, net, one-and-done, became paying — and every
number on all three pages opens its query. Ask ChatGPT or Claude over MCP and they answer from the same numbers.
Two optional calls make it sharper: vitrus("signup") and
vitrus("plan", { plan: "pro" }). No auth provider to connect, no admin keys handed over.
Open the portfolio in the demo → · How it is counted
Real-time, a 3D globe, sessions, funnels, journeys, goals, revenue, retention and Web Vitals — all in one process and one SQLite file. Click a number, read the SQL that produced it.
Off until you switch it on, loaded only on pages that ask for it, and every text node and input value masked by default.
Agents that sign their requests (Web Bot Auth) are checked against their operator's published key and named. A user-agent is a sentence; a signature is proof.
Your iOS, Android and macOS apps in the same panel as your site — ids hashed on device, no device id, an offline queue, a privacy manifest.
Where people click and how far they scroll, per device, over the page they saw — drawn from your recordings, nothing extra collected.
Every visit with its timeline, profiles for the users you identify (the raw id is never stored), and events down to each occurrence.
No ClickHouse to babysit, no Postgres, no Redis, no Kafka. Ingest, the API, the dashboard and the MCP server are one Bun process over one SQLite file.
VACUUM INTO is consistent while it runs.What changed
The audience stopped being only human, and the reporting stopped being only written by people. Most analytics tools were designed before either happened.
ChatGPT, Perplexity and Claude send real visitors — and roughly 70% of them land in GA4's “direct” bucket. Meanwhile GPTBot and ClaudeBot read your pages every day and most tools either ignore them or count them as people. Worse, those crawlers do not run JavaScript, so a browser script cannot see them at all — ours included, until you forward them from your server.
Every analytics product is bolting an AI on top that writes your summaries. Those sentences look authoritative and cannot be checked. When a model writes the report — and increasingly when an agent reads it — “can I verify this number?” stops being pedantic and becomes the whole question.
Vitrus is built for both halves: it separates AI referrals from AI crawlers, and it refuses to publish a number it cannot prove — to you, or to your agent.
Platform
Most teams bolt a traffic tool, a funnel tool and an AI plugin together, then try to reconcile the numbers. Vitrus brings acquisition, behavior and conversion into one place — and ties every one of them to evidence.
Where traffic actually comes from. Channels, referrers, campaigns — and AI assistants as a channel of their own.
What people do once they land. CTAs, scroll depth, field-level form abandonment, rage clicks.
Where you lose them. Ordered funnels show the drop at every step; retention shows who comes back.
Only here
Every number carries the query that produced it, and you can click it. Unprovable sentences never ship.
The whole surface
If you are moving from Google Analytics, Plausible or Umami, this is the checklist. Every item is in every plan, including the free one; how many sites and teammates grows with the plan.
Deliberately absent: ad-platform attribution. Session replay exists, but it is off by default and masked by default. Here is why.
Proof
Below is a digest in the shape the product actually produces. Click an
e# badge: what opens is not an explanation — it is the query
that produced the number. The queries are the real ones; the figures are an example, and
saying so is the least a product with this claim can do.
SELECT COUNT(DISTINCT visitor_id) AS value FROM events WHERE site_id = ? AND ts >= ? AND ts < ? AND bot_kind = ''
["demo", 1789003421000, 1789608221000]
40 (previous period: 44)
SELECT source, COUNT(DISTINCT session_id) AS sessions FROM events WHERE site_id = ? AND ts >= ? AND ts < ? AND bot_kind = '' AND channel = 'ai' GROUP BY source ORDER BY sessions DESC
[{"source":"perplexity","sessions":9},{"source":"chatgpt","sessions":7}]
SELECT json_extract(props,'$.field') AS field, COUNT(*) AS abandons FROM events WHERE site_id = ? AND ts >= ? AND ts < ? AND bot_kind = '' AND name = 'form_abandon' GROUP BY field ORDER BY abandons DESC
[{"field":"phone","abandons":6},{"field":"company","abandons":2}]
Fixed queries run first and produce an evidence bundle. The model only sees that bundle, and it is forbidden from doing arithmetic.
Every number in the output is matched against the evidence set. No match, no sentence — it is discarded, not corrected.
If the model is unreachable or invents something, we fall back to the deterministic text. An LLM can enrich this product; it cannot break it.
“Conversion rate jumped 41.7% to an all-time high.”
↳ 41.7 is not in the evidence set — sentence dropped, digest marked incomplete.
Acquisition
Roughly 70% of AI referrals land in GA4's “direct” bucket:
ChatGPT appends utm_source to outbound links, but Gemini and
Perplexity mostly do not. On top of that, most tools lump the bot that reads you
together with the human it sends.
chatgpt.com referrer is a real visitor| Channel | Sessions |
|---|---|
| AI assistant ● | 16 |
| Search | 54 |
| Social | 31 |
| Direct | 22 |
Separately: GPTBot made 148 requests — not counted as visitors.
One line of script. Pageviews, sources and channels, UTM campaigns, entry and exit pages, country, device, browser, language. None of it needs extra code.
data-vitrus-event-*, no code requiredBehavior & conversion
Steps are counted in order: a step must happen after the one before it. Unordered counting treats someone who signs up and then reads the pricing page as a conversion, and inflates the funnel. You define the steps; your first funnel works with no configuration at all.
| Home | |
| Clicked CTA | |
| Signed up |
Biggest loss at Clicked CTA: 57 sessions.
Recording someone's screen can mean recording the ID number they typed into your form, so replay is off until you switch it on, every text node and input value is masked in the browser, and password and card fields are never captured. Most of the time the cheaper, privacy-preserving signals answer the question first.
| Form field | Abandons |
|---|---|
| phone | 61 |
| company | 18 |
| 4 |
Our visitor id rotates daily (we use no cookies), so cross-day tracking of anonymous traffic is mathematically impossible. Most tools solve this with a persistent identifier or quietly count it wrong. We took a third route: we say we can't, and we open the route where we can.
vitrus.identify() and the cohort matrix is computed for real| Cohort | Day 0 | +1 | +2 | +3 |
|---|---|---|---|---|
| Sep 08 | 100% | 42% | 31% | 28% |
| Sep 09 | 100% | 38% | 29% | · |
| Sep 10 | 100% | 45% | · | · |
· hasn't happened yet — an unmeasured day is never rendered as zero.
Proactive
Slack, email, or a plain webhook. Weekly or daily. Three rules hold it together.
Getting the same weekly summary twice is worse than not getting it. One ledger entry per period.
Network errors are not written to the ledger, so the next tick retries. You don't lose that week.
No “0 visitors” email for a week with no traffic.
Connect ChatGPT, Claude, Cursor or Claude Code in a minute. Your assistant reads your analytics — read-only — and every number it gets back comes with the SQL behind it, so it can cite instead of assert, and you can check what it told you.
https://app.vitrus.dev/mcp as a connector — or one click in Cursor.claude plugin marketplace add Vitrus-Dev/vitrus{ "by": "source", "days": 30, "filters": [{ "field": "channel", "value": "ai" }] }ChatGPT sent the most: 188 visitors, then Claude 97, Perplexity 92 and Gemini 43.
Source: SELECT source, COUNT(DISTINCT visitor_id) … WHERE channel = 'ai' AND bot_kind = '' …
AI crawlers (GPTBot, ClaudeBot) are not in these numbers — they read pages but bring nobody.
Verified identity
Every analytics tool in this category identifies a bot by reading its user-agent — a sentence the client wrote about itself. We report what a request proved separately from what it claimed, because that distinction is the whole product applied to bots.
Web Bot Auth — an IETF draft built on RFC 9421 HTTP Message Signatures — lets an agent sign its requests. Anthropic, OpenAI, Perplexity and Common Crawl have begun. When a signed request arrives we check it against the Ed25519 key its operator publishes, and we can name the signer.
What this deliberately does not say: an unsigned request is not evidence of anything. Almost nothing signs yet, and calling unsigned traffic suspicious would replace one unprovable claim with another.
| Label | Requests |
|---|---|
| verified ● | 1,284 |
| claimed | 9,610 |
Example figures. The verified row is the one nobody else can show you — how it works.
Privacy
The visitor id is a one-way hash that changes every day:
hash(secret_salt + day + site + ip_prefix + browser).
The IP is never stored — it is only an input to the hash.
No persistent identifier, no retroactive identity stitching. Most sites can drop the banner entirely.
On by default. A privacy stance that isn't a marketing line should show up in the defaults.
Run the open-source core on your own infrastructure, or use the cloud. Either way it is never sold or shared.
Optional
One switch per site drops country, screen size, referrer query strings and cross-day identity — enforced on the server, where the browser script cannot bypass it.
Same for the AI layer: no model is enabled by default. If you turn one on, it's either Ollama on your own server or your own API key — and what reaches the model is an aggregated evidence bundle, never raw visitor data.
Compare
Most individual features exist somewhere. The bold rows exist nowhere else — and where a rival is ahead of us, we say so below the table.
Checked 24 September 2026 against each product's own documentation (mobile SDKs: 18 October 2026). These products ship quickly; a comparison table with no date on it is a claim about the present that quietly rots. If a cell is wrong, tell us and we will fix it.
| Capability | Umami | Plausible | Rybbit | Databuddy | Vitrus |
|---|---|---|---|---|---|
| Cookie-free, no consent banner | ✓ | ✓ | ✓ | ✓ | ✓ |
| Self-hostable | ✓ | ✓ | ✓ | ✓ | ✓ |
| Services to install | Postgres | PG + ClickHouse | PG + ClickHouse | PG + CH + Redis | none · one SQLite file |
| Daily salt on the visitor id | ✓ | ✓ | optional, off by default | — | always on |
| Funnels, journeys and goals | ✓ | ✓ (funnels on paid plans) | ✓ | ✓ | ✓ |
| Proactive digest (arrives unasked) | — | alerts | ✓ | ✓ | ✓ |
| AI usage metered separately | no | no | no | yes, credits | never |
| Local LLM / bring your own key | — | — | — | — | ✓ |
| Session replay | ✓ | — | ✓ | — | opt-in, masked by default |
| Official Swift (iOS) and Flutter SDKs | community only | Events API | React Native only | HTTP API | ✓ |
| All your apps in one panel: users, plans, revenue | — | — | — | — | ✓ |
| User ids hashed in the browser, never stored raw | — | no user ids | salting off by default | — | ✓ |
| Clickable query behind every number | — | — | — | — | ✓ |
| Unprovable AI sentences auto-dropped | — | — | — | — | ✓ |
Where others are ahead of us today. Umami has attribution reports and custom boards; Plausible imports Google Analytics history, and Rybbit imports Plausible exports (we import from Umami only); and both run on databases built for far larger volumes. If one of them is what you need, pick that tool.
Pricing
Pageviews and custom events count the same. AI messages are not metered — the digest is deterministic anyway, so rationing it would be artificial scarcity.
Going over the limit does not cut you off: data keeps flowing inside a 20% grace band with a warning in the dashboard. Analytics you failed to collect never comes back.
FAQ
Every metric is defined as a fixed SQL query. When a digest is produced those queries run and their results are stored as evidence records. Click a number in the dashboard and you see the query that ran, its parameters and the raw rows. An LLM may rephrase that bundle but cannot produce a new number — the moment it does, the output gate drops the sentence.
No — and not because of a setting: the data never reaches us. When your app calls
vitrus.identify(user.id), the script hashes the id in your user's browser before
anything is sent; the server hashes it again with a secret and stores only that. Email, phone, name, address and
similar fields are dropped in the browser and again on the server. The Users page shows each person under a
made-up name such as “Amber Falcon”, so you can follow someone's activity without anyone learning
who they are — and the same person in two of your apps cannot be linked, by us or by anyone.
Yes. Connect ChatGPT, Claude, Cursor or Claude Code over MCP and ask “which of my apps lost the most users
this week?”. get_portfolio and get_lifecycle answer with
counts only — no list of people — and the query behind each number, so the assistant cites instead of guessing.
Not unless you want it to. No model is enabled by default; the digest is generated deterministically. If you turn one on you have two options: Ollama on your own server (data never leaves the box) or your own API key. In both cases what reaches the model is an aggregated evidence bundle, not raw visitor data.
A one-way hash built with a secret salt that rotates daily:
hash(salt + day + site + ip_prefix + browser). The IP is never stored, it is
only an input. The next day the same person hashes to a different value; there is no persistent identifier.
Yes, and it ships the other way round from most tools. Recording a visitor's screen can mean recording the ID number they typed into your form — if masking misses one field, you created your customer's compliance problem. So replay is off by default, a page has to opt in, every text node and input value is masked unless you unmask an element on purpose, password and card fields are never captured, and Do Not Track always stops it. Field-level form abandonment, rage clicks and scroll depth still answer most of the same questions without recording anyone.
Yes — the core is Apache-2.0. One process, no second service to install (the database is embedded). The cloud version takes on team management, quotas and hosting; the engine is identical and no feature is held hostage.
For most sites, yes. Not for ad-platform attribution (Google/Meta Ads ROAS) — we deliberately don't do that. The question Vitrus answers is “where is my traffic coming from, where do people drop off, and is this number correct?”
One line to install. First digest a week later. Or look around first — the demo needs no account.