Changelog

What shipped, and when

Only work that is in production is listed here — nothing is dated into the future and nothing is marked “coming soon”. Where a feature has a known limit, the limit is in the same entry as the feature. A changelog that only lists wins is marketing copy with dates on it.

Security

Security review: fixes across the cloud and the self-hosted server

  • Workspace roles. Adding a member now checks the role it is given. Before, a workspace admin could change the owner's role or grant themselves owner through the invite form. An owner's role can no longer be changed there, and only an owner can add another owner.
  • Self-hosted dashboard password. Set VITRUS_PASSWORD and the self-hosted dashboard, its read API and the replay settings ask for it; the tracker and ingest stay public. Without it, vitrus start warns that anyone who can reach the port can read the analytics — which was true, silently, until now.
  • Password guessing is limited per address and per account, and an account that signs in only with Google or GitHub now answers a password attempt in the same time as any other.
  • Security headers on every response: pages cannot be framed by another site, content types are not sniffed, and HSTS is sent behind TLS.
  • Ingest bodies are capped at 64 KB, on the cloud and in the self-hosted server.
New

Prices in dollars, a Business plan, checkout in the dashboard

  • Plans in US dollars: Starter $12, Pro $29 and a new Business plan at $99 for 10 million events a month. Paying yearly costs ten months. Pricing.
  • Upgrade from Billing. Pick a plan and pay in Paddle's checkout without leaving the dashboard; the plan changes when Paddle confirms the payment. Paddle's script loads only when you press the button.
  • Email. Invitations are now emailed and accepted from the link — by the invited address only — and "Forgot your password?" sends a one-hour, single-use reset link.

Known limits of what shipped

  • Pro now includes 50 sites, 10 members and 20,000 recordings a month (it was 100, 25 and 50,000); workspaces already over these keep what they have and cannot add more.
New

Google Search Console

  • Search Console. Connect it under Traffic → Search Console to see the Google queries that brought people to your site, the pages Google showed, and clicks, impressions, CTR and average position — by query, page, country and device, and by day.
  • Read-only, and revocable. Vitrus asks Google only to view search data and the connected account's email address. Disconnecting deletes the stored access and revokes it at Google.
  • Google's numbers, labelled as Google's. We cannot re-run them, so their evidence is the exact request we sent to the Search Console API and the rows it answered with. They stay on their own page and are never mixed into Vitrus's own metrics.

Known limits of what shipped

  • Search Console data arrives two to three days late; the page says where Google's data for the range ends.
  • The property is picked from your site's domain; if none or several could be meant, you choose.
New

Revenue, API keys, and your Umami history

  • Revenue. Send revenue and currency with any event and the new Revenue page shows totals, orders, average order value, revenue per visitor and conversion rate — by channel, source, campaign, event, page, country and device, and over time. Channel, source and campaign are where the visit started, not the checkout page. How it is counted.
  • No currency conversion. Each currency is its own tab. A total that folded euros into dollars would rest on an exchange rate from somewhere else, and could not be checked against your data.
  • A bad amount never loses the event. An amount that is not a number, negative or missing its currency is removed and its reason kept; the page says how many and why.
  • Import from Umami. Upload an Umami export under Site settings → Import history — the .csv.gz Umami Cloud hands out, or a CSV from one query against a self-hosted Umami database. Rows from when Vitrus was already collecting are skipped, a repeated file adds nothing, and every import can be removed. How it works; Plausible exports only daily totals, so it is not supported yet.
  • API keys. Workspace admins create read-only keys under Workspace → API keys. A key reads every site in its workspace and can change nothing, goes in the Authorization header only, and is shown once. Reference.

Known limits of what shipped

  • Purchases in sessions driven by a verified AI agent are shown on their own line, not in the totals.
  • API keys do not reach live data or session replays.
Fixed

One visit, one visitor — and a path ad blockers do not match

  • Visits were split behind Cloudflare. The client IP was read from x-forwarded-for, which our reverse proxy rewrites to the Cloudflare edge — a different address per request. One landing visit became up to three visitors (pageview, outbound click, Web Vitals), each a zero-length session: sessions roughly doubled, bounce rate read 100%, and funnel steps after the first page stayed empty. The IP now comes from cf-connecting-ip.
  • The tracker posts to /api/d. Blocklists match /api/collect as a path on any domain, first-party included, so a visitor running one went uncounted even on a self-hosted install. /api/collect still works; it is the same endpoint.
  • Proxies name the visitor. Behind a proxy on your own domain, every request reaches us from your server. The proxy guide now sets X-Vitrus-Client-IP in every example, and has a Next.js route handler and a Cloudflare Worker that do.

Known limits of what shipped

  • Events already stored cannot be re-joined: visitor ids are one-way hashes. Numbers are right from this release on, and sessions before it are overcounted.
New

Analytics that shows its work: globe, real-time, sessions, journeys, goals and opt-in replay

  • A new motto, because the product earned it. “Analytics that shows its work” — every number, list and chart below opens the SQL that produced it, and the AI digest still drops any sentence whose number is not in that evidence.
  • Globe, 3D and 2D — rewritten on a canvas: sessions placed at their city on a timeline you can scrub and play, glowing coordinate cells, and choropleths by country and by state or province. City, region and 0.1° coordinates come from your proxy's headers (Cloudflare's visitor-location transform, Vercel, CloudFront); the geometry is served from this origin, with no map provider and no token.
  • Real-time — a live count in the top bar of every page, and a Realtime page with views per minute and the latest events labelled person, crawler or agent. Streamed over server-sent events with a polling fallback, and filtered exactly like the page you are on.
  • Sessions, users and events — a sessions list with a timeline for each visit; user profiles with traits for people you identify() (the raw id is hashed before it is stored); events with a property breakdown, a trend and a log; errors grouped by message, file and line.
  • Journeys and goals — a hand-drawn Sankey of the paths sessions take, goals on page wildcards, custom events or event properties, and saved ordered funnels where each step links to the sessions that reached it.
  • A Rybbit-style main page — six KPIs with change and sparklines, a chart with a dashed previous-period line and a granularity picker, tabbed breakdown cards with bar-backed rows, and a weekday × hour heatmap. Filters now take is / is not / contains / starts with / regex on 24 dimensions and any event property, live in the URL, and can be saved as segments.
  • Session replay, opt-in. Off by default, loaded only on pages that carry data-replay, and every text node and input value masked unless you unmask an element on purpose. It is a separate script under 5 KB gzipped, so pages without it keep the small tracker.
  • Site settings — IP, CIDR, country, path, hostname and user-agent exclusions applied before your quota, a data-retention setting, read-only share links and CSV export.
  • A quieter dashboard. Smaller, muted page titles, a single KPI strip, pill tabs, tabular numerals throughout, visible keyboard focus, reduced-motion support, and a layout that holds down to phone width. It also fixes a bug that flattened every bar on the trend chart into a 4-pixel dash.

Known limits of what shipped

  • Cities need your proxy to send them. Cloudflare needs its visitor-location managed transform switched on; Netlify and Fly send a country only. Without those headers the globe names the missing header instead of drawing an empty map, and country-only sessions are counted but never pinned to a country's centre.
  • Journeys, goals and the Users page join days together only for people you identify. An anonymous visitor's id rotates daily by design, so a journey that continues tomorrow is two journeys.
  • Replay masks by default, but a site that unmasks elements or turns off media blocking is choosing what it records. Password, card and one-time-code fields are always blocked, whatever the page asks.
  • The tracker grew by about 200 bytes gzipped for file downloads, hash routing and user traits. The 3 KB build gate still holds.
  • The self-hosted dashboard (vitrus start) renders every metric and plays replays; the sessions, users, journeys, goals and globe pages are in the hosted dashboard today. Their queries are in the open-source core.
Fixed

The globe draws an actual world, and stops freezing the tab

  • The globe locked up the browser. Every redraw bound another mousemove listener to the window, and the move handler triggered a redraw — one gesture became two listeners, then four, then eight. Dragging froze the tab within a second; the rotate button leaked one every 60 ms. Nothing is bound to the redrawn element any more, and redraws are coalesced to one per animation frame.
  • Country outlines. The globe was an ocean disc, a grid and one dot per country, which with a handful of countries read as an empty circle. It now draws real land from Natural Earth, shades every country you have traffic from, and filters the page when you click one.
  • The geometry is served from this origin, not a tile provider: 23 KB, cached for a week, no account with anyone and no third party learning which dashboards your team opens. Rybbit's globe takes a Mapbox token; this is the same picture without one.
  • A country too small for the map — most island states — still gets a dot rather than being counted in the table beside the globe and left off the globe itself.

Known limits of what shipped

  • This page previously argued the globe should not draw countries, because shading one implies knowing where inside it a visit came from. That was wrong: shading a whole country claims country precision, which is exactly what a proxy header gives. A dot at the centroid was the worse of the two — it points at a spot.
  • Still no GeoIP database, so still no region and no city. Country comes from your proxy's header or it is not measured at all.
  • Far-side outlines are pushed to the horizon rather than clipped exactly. It is accurate where the two meet and hidden everywhere else, but it is an approximation.
New

Who the traffic really was: verified agents, agent sessions, and a second detection layer

  • Agent identity — when a request is signed (Web Bot Auth, over RFC 9421 HTTP Message Signatures) the signature is checked against the Ed25519 key its operator publishes, and the signer is named. Verification only ever promotes a label: a missing signature never makes a visitor suspicious, because almost nothing signs yet.
  • Agent sessions are now their own class. An agentic browser is a real Chrome session driven by an agent; Umami and Plausible discard it, Rybbit blocks it, GA4 counts it as a person. Here it is excluded from your visitor numbers, reported separately, and has its own funnel — an agent that completes a checkout is revenue.
  • Traffic quality — a second detection layer checks whether the rest of a request agrees with the browser its user-agent claims to be: five header rules, each weighted, threshold 5 so no single rule can accuse anyone. It is in the Apache-2.0 core, not held back for the paid tier.
  • Suspected traffic is recorded, not removed. It stays in every number until you flip a switch, the switch is not remembered between visits, a banner stays on screen while it is on, and the evidence panel shows the predicate doing the excluding.
  • Globe — visitors by country on an orthographic projection drawn from the country table, with no tile service and no third-party request.
  • Filters — click any row to narrow every number on the page. Filters are compiled into the SQL on the server, never applied to rows in the browser, so the query you can read is the query that produced the number.
  • llms.txt, llms-full.txt and openapi.json are generated from the same arrays the marketing pages render from, so they cannot drift from what the site says.

Known limits of what shipped

  • The agent-session count is a floor, never a total. An agent that browses without signing is indistinguishable from a person, and the automation signals below are deliberately kept out of that number — a signed agent is a fact and a suspicious one is an opinion.
  • The bot-detection documentation previously said header heuristics were “not a trade available to us”. That was too strong and has been corrected in place: they are computed, they are shown, and they change no number unless asked.
  • A headless browser with stealth patches sends plausible headers and scores zero, as does a residential-proxy botnet running real Chrome. The rule table is versioned and will grow; it will not reach certainty.
  • Signatures travel on the request for your page, which our browser script never sees. Verified agents and non-rendering crawlers both require server-side ingest.
New

Operator console, support desk, and the two pages the dashboard was missing

  • Performance — Core Web Vitals at p75 with the good / needs-improvement / poor thresholds drawn, plus the slowest pages. The measurements were already being collected; there was no screen showing them.
  • Errors — uncaught JavaScript errors grouped by message and page, and by browser and OS. Sessions affected is reported next to raw occurrences, because one person reloading a broken page twenty times is not twenty broken sessions.
  • Support desk — open a ticket from inside the dashboard. A ticket belongs to a workspace rather than to the person who typed it, so a colleague can pick up the thread. Messages cannot be edited or deleted by either side.
  • Light theme — the dashboard follows your system setting and remembers an explicit choice.
  • Operator console (platform administrators only) — users, workspaces, sites, the support queue and an append-only audit log. Every KPI in it carries the query that produced it, exactly like a customer-facing number.

Known limits of what shipped

  • Web Vitals need a browser that implements PerformanceObserver. Safari and Firefox report a subset, so a low-traffic window can legitimately show nothing at all — the page says so rather than showing dashes.
  • Slowest-pages only lists paths with at least three samples. A page measured twice is noise, and publishing it as a ranking would send you optimising it.
Fixed

One canonical home, and a sign-in bug that produced no error at all

  • Every marketing page now declares a canonical URL. One process answers on both the marketing domain and the dashboard domain, so all of them existed at two addresses — duplicate content that splits ranking signals between the two.
  • Sign-in with Google and GitHub was failing silently. The server was joining two Set-Cookie headers with a comma, which is invalid HTTP: browsers do not split on commas because a comma is legal inside Expires. The session cookie was therefore never stored, and the dashboard showed the login screen again with no error and no log line.
  • The dashboard and the OAuth callback now live on a single host. Starting a login on the marketing domain wrote the state cookie to the wrong host, and the callback could never see it.
  • Fifteen API error strings that were still in Turkish are now in English, and a test pins them.

Known limits of what shipped

  • The test that should have caught the cookie bug was passing: it read the header with headers.get(), which folds multiple headers into one comma-joined string — so “two cookies” and “one broken cookie” looked identical. It now reads the array.
New

The open-source core is public

  • github.com/Vitrus-Dev/vitrus — the ingest pipeline, the classifiers, the metric bundle, the numeric guard, the tracker, the single-process server, the CLI and the MCP server, all Apache-2.0.
  • Fifteen feature pages, a standalone pricing page and a security page, each with a section showing the query behind the claim.
  • Seven new documentation pages: framework integrations, traffic filtering, bot detection, how each metric is counted, serving the tracker from your own domain, troubleshooting, and self-host versus cloud.
  • Tracker controls — exclude your own visits with ?vitrus_ignore=1, skip pages entirely, or mask an identifying path segment in the browser before anything is sent.
New

Funnels, retention and proactive delivery

  • Ordered funnels — 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.
  • Retention cohorts — with vitrus.identify(), a real cohort matrix and an average curve.
  • Digest delivery — weekly or daily, to Slack, email or a plain webhook. The same period is never sent twice, a failed send is retried rather than recorded as delivered, and a week with no traffic stays silent.
  • Google and GitHub sign-in.

Known limits of what shipped

  • Retention is not computed for anonymous traffic and the page explains why instead of showing an empty table: the visitor id is salted per day, so cross-day tracking of an anonymous visitor is arithmetically impossible, not merely disallowed.
  • A cohort cell that reaches into the future is drawn as ·, never as “0% returned”.
New

First release

  • Cookie-free ingest, the AI referral and AI crawler classifiers, the metric bundle where every entry is { id, sql, params, window, value }, the numeric guard, and the deterministic digest.
  • The evidence panel: click any number and you get the query that ran, its parameters and the raw rows.
  • A 41-case golden set for the AI classifier, required to score 100% on every CI run, and a 20-case set for the guard where a single fabricated number failing to be dropped fails the build.

The open-source core's full commit history is on GitHub. Planned work is on the FAQ rather than here, for the reason above.