Read-only web analytics — AI traffic, revenue, goals, funnels — with the SQL behind every number.
One address for any MCP client. You sign in at app.vitrus.dev once, after adding it.

At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
| Access to | Read | Change |
|---|---|---|
| Analytics |
From the sign-in at app.vitrus.dev, as the endpoint publishes it. You approve it there.
Odel could not list its tools without signing in.
Open-source, cookie-free web analytics. Every number carries the SQL that produced it —
and an AI summary can't ship a number it can't prove. One process, on your own server.
Live demo · Use with ChatGPT / Claude · Website · Cloud · Documentation · Quickstart · Compare · Changelog · Discussions
You need Bun 1.3 or newer. That is the whole list.
git clone https://github.com/Vitrus-Dev/vitrus && cd vitrus
bun install && bun run build:tracker
alias vitrus="bun $PWD/packages/cli/src/cli.ts"
vitrus init # creates ./vitrus.db and a secret salt
vitrus site add "My site" example.com # prints your script tag
VITRUS_PASSWORD="a long secret" vitrus start # ingest + dashboard on http://localhost:3000
Then one line in your site's <head>:
<script defer data-site="SITE_ID" src="https://YOUR-HOST/v.js"></script>
No Docker Compose file, no database server, no migration step. Rather not run it? app.vitrus.dev is the same engine, hosted, with a free plan — and the live demo opens the full hosted dashboard on sample data, no account needed.
Click a figure and you get the SQL that ran, its parameters, the window and the result — not a description of the number, the number's origin. It is the same evidence the AI summary is checked against.
That matters more now that models write the reports. When an AI-written sentence contains a number that is not in the evidence, Vitrus drops the sentence before it reaches you:
Visitors are down 18% against the previous period. [e1]
The drop coincides with the last deploy. [e1, e7]
✗ dropped: "The new theme caused a 41.7% fall in conversion."
→ 41.7 is in no evidence record, and "caused" is a claim nobody can make from this data.
![]() Globe. Sessions at their city, on a canvas drawn from our own geometry — no map provider, no GeoIP database. | ![]() Real-time. Who is on the site now, and the latest events, streamed as they arrive. |
![]() AI traffic. Visitors ChatGPT and Claude sent you, kept apart from the crawlers that read your pages. | ![]() Funnels. Ordered — a step counts only after the one before it, so the conversion is not inflated. |
![]() Journeys. The paths people take, as a Sankey you can filter by start and end. | ![]() Revenue. Per currency, never converted, attributed to where the visit began. |
![]() Web Vitals. LCP, INP, CLS, FCP and TTFB at the percentile you choose, per page. | ![]() Retention. Cohorts of identified users — and an honest blank for days that have not happened yet. |
Screenshots: the hosted dashboard on a demo workspace; every figure is sample data — open it yourself. The self-hosted dashboard in this repository is simpler today — see Architecture for exactly which views it renders.
Vitrus speaks MCP. Your assistant reads your analytics — read-only — and every number it gets back comes
with the query that produced it, so it can cite instead of assert. Listed in the official
MCP Registry as dev.vitrus/analytics.
The plugin's skills, under plugins/vitrus/skills:
/vitrus:weekly-report — last week against the week before, channels, AI assistants, goals; every figure cited./vitrus:traffic-drop — finds the day a number broke and the segment it lives in, then checks errors and Web Vitals./vitrus:measurement-plan — reads your code and proposes the events, goals, funnels and revenue calls to add./vitrus:ai-search-audit — which assistants send people, which pages the AI crawlers read, and where the two disagree.15 tools, all read-only: query_stats (any metric by any of 20 dimensions or by day, with filters),
get_realtime, get_ai_traffic, get_revenue, goal_report, analyze_funnel, get_journeys,
get_retention, get_web_vitals, get_errors, get_digest and more — docs.
Connections are scoped to one workspace and can be disconnected any time under Account → Connected apps.
Vitrus tells you how many people visit your website, where they come from, what they do and where they leave. It sets no cookies, so you do not need a consent banner. Use the hosted version at vitrus.dev or run it yourself as a single program — there is no database server to install.
{ id, sql, params, window, value },
and the dashboard opens that exact SQL when you click the badge beside a figure.chatgpt.com sending a person and GPTBot reading a page are two
different events, classified separately and never summed — by a versioned rule table held at
100% precision and recall by its own eval.data-replay, and every text node and input value
masked by default. Password, card and one-time-code fields are always blocked. Recordings play in
a sandboxed player in the self-hosted dashboard.vitrus digest writes one in your terminal, every
line tagged with the evidence ids behind it. The Slack / email / webhook channels and the
never-sent-twice scheduler ship in the core.https://app.vitrus.dev/mcp, sign in, pick a workspace); Claude Code is one line:
claude mcp add vitrus --transport http https://app.vitrus.dev/mcp.Deliberately absent: ad-platform attribution (ROAS/CPA) and multi-touch attribution models, because a model's opinion is not something we can prove.
The Quickstart is the whole install. Set VITRUS_PASSWORD before the port is reachable
from outside: the dashboard and the read API then ask for it (HTTP Basic, any user name), while the
tracker and ingest stay public because your visitors' browsers call them. Without it, vitrus start
prints a warning and anyone who can reach the port can read your analytics.
Want data on screen straight away? vitrus demo <site-id> generates a realistic week of sample traffic.
vitrus site ls # list your sites
vitrus digest <site-id> --days 7 # deterministic summary, in your terminal
vitrus digest <site-id> --llm ollama:gemma3:4b # phrased by a local model; data never leaves the box
vitrus start --port 8080
The database path is VITRUS_DB (default ./vitrus.db). Put Cloudflare, Vercel, Fly, CloudFront or
Netlify in front and country (and, from Cloudflare, Vercel or CloudFront, region and city) is read from
the headers they already set — there is no GeoIP database
to download, and without a proxy the dashboard says the data is missing rather than guessing.
The open-source alternatives are good, and several do things we do not. This table lists only differences we can state without qualification. Checked 24 September 2026 against each product's own documentation — if a cell is out of date, please open an issue.
| GA4 | Plausible | Umami | Rybbit | Vitrus | |
|---|---|---|---|---|---|
| Open source | ✗ | ✓ AGPL-3.0 | ✓ MIT | ✓ AGPL-3.0 | ✓ Apache-2.0 |
| Cookie-free, no consent banner | ✗ | ✓ | ✓ | ✓ | ✓ |
| Services to install | — | Postgres + ClickHouse | Postgres or MySQL | Postgres + ClickHouse | none |
| Tracker size (gzipped) | ~28 KB | ~1 KB | ~2 KB | ~18 KB | 2.6 KB |
| AI assistants as a channel | ✗ | ✓ | ✓ | ✓ | ✓ |
| Crawler reads separated from the humans they send | ✗ | ✗ | ✗ | ✓ | ✓ |
| The query behind every number, in the UI | ✗ | ✗ | ✗ | ✗ | ✓ |
| AI sentences dropped when the number is unprovable | ✗ | ✗ | ✗ | ✗ | ✓ |
| Verified agent identity (Web Bot Auth) | ✗ | ✗ | ✗ | ✗ | ✓ |
| Bot detection beyond the user-agent, in the open-source build | ✗ | ✗ | ✗ | ✗ | ✓ |
| Funnels, journeys and goals | ✓ | ✓ (funnels on paid plans) | ✓ | ✓ | ✓ |
| Session replay | ✗ | ✗ | ✓ | ✓ | opt-in, masked by default |
| Public API with keys | ✓ | ✓ | ✓ | ✓ | ✓ read-only |
| City-level geography | ✓ | ✓ | ✓ | ✓ | from proxy headers |
| Hundreds of millions of events a month | ✓ | ✓ | ✓ | ✓ | ~1M/month per box |
One-page write-ups per tool: vitrus.dev/compare.
The visitor id is a one-way hash that cannot follow anyone across days:
sha256(secret_salt | day | site | ip_prefix | user_agent)
The salt rotates daily, the IP is never stored (IPv6 is reduced to its /64 first), and nothing is
written to the browser. That is also why the retention page tells you when it cannot compute a
cohort instead of showing zeros — call vitrus.identify(user.id) for signed-in users and retention
becomes real, with the raw id never touching disk.
packages/
core ingest · classifiers · MetricBundle · numeric guard · digest · agent verifier ·
explore queries (sessions, users, journeys, goals, geo) · replay ingest + player
tracker the browser script, 2.6 KB gzipped (+ an opt-in replay recorder, under 5 KB)
server one process: ingest + query API + tracker + evidence dashboard
cli init · site · start · digest · demo
mcp read-only MCP module (JSON-RPC over HTTP) for AI agents
Every package has zero runtime dependencies. The SQL string is the evidence — Store.select
takes raw SQL on purpose, because hiding it behind an ORM would remove the one thing this project is
for.
A few pieces ship in the core as libraries before the self-hosted server exposes them. Today the
server does not yet mount the MCP module, run the scheduled digest delivery, accept server-side
forwarded requests (which is how signed agents and non-rendering crawlers are seen — a browser script
never sees them), take row filters, or render the sessions, users, journeys, goals and globe views; the building blocks are here and wiring them into vitrus start is on the
roadmap.
bun install && bun run build:tracker
bun run gates
Type checks, the test suites, two golden-set evals and the build gates:
| Gate | What it prevents |
|---|---|
eval:referrer | the AI-source table going stale unnoticed — 100% required |
eval:insight | an unprovable number surviving into prose — zero leaks tolerated |
gate:core-purity | the metric layer ever importing the LLM layer |
gate:no-deps | a runtime dependency appearing without a deliberate decision |
gate:tracker-size | the tracker quietly growing past 3 KB |
gate:inline-js | a bad escape killing the dashboard's script while the page still renders |
app.vitrus.dev is the hosted version: teams and roles, quotas, EU
hosting and digest delivery without running your own mail provider, plus the sessions, users,
journeys, goals, real-time and globe views. The analysis engine is the same: every query those views
run lives in packages/core in this repository.