Description
Vireo Analytics is a self-hosted, privacy-first analytics plugin for WordPress. It gives you the numbers that actually matter: pageviews, visitors, top content, referrers, countries, devices, without cookies, without a consent banner, and without sending a single byte to a third party. All data stays in your own database.
The numbers you see are the numbers that matter. Most analytics tools count uptime monitors (StatusCake, UptimeRobot, Pingdom), SEO crawlers (Ahrefs, Semrush), AI bots (GPTBot, ClaudeBot, PerplexityBot), and headless browsers as “visitors”, inflating your stats by 20 to 40%. Vireo Analytics filters them out at the tracker level against a database of over eight hundred known crawlers, sorted into what each one is doing, plus Cloudflare signals and automation detection. What you see is real humans reading your content.
It’s designed to be a drop-in replacement for Jetpack Stats or Google Analytics for site owners who want clarity over dashboards full of noise. If you’re already running Koko Analytics, Vireo Analytics can import your existing history in one click. If you’re moving off Jetpack Stats, the built-in importer pulls up to 14 months of historical data from the Jetpack API.
Features
- Clean, fast dashboard with pageviews, unique visitors, and trend charts.
- Top content, top referrers, countries, and device breakdowns.
- Cookie-free tracking, so no consent banner is needed for basic analytics.
- Daily-rotating salted hashes for visitor identity, GDPR-compatible out of the box.
- Counts pageviews with or without JavaScript. A no-script pixel catches readers whose browser never runs the tracker, and both paths keep working behind a full-page cache.
- Admin-bar sparkline showing today’s traffic at a glance.
- Per-post analytics column on the Posts list screen.
- Custom events API for tracking button clicks, form submissions, and other interactions.
- 404 tracking so you can spot broken inbound links.
- Weekly email digest delivered to your inbox.
- CSV export of any dashboard view.
- One-click import of your history from Koko Analytics, Jetpack Stats, Statify, WP Statistics, Burst Statistics, Independent Analytics and SlimStat, so switching does not mean starting at zero.
- See what the AI assistants take against what they send back. ChatGPT, Claude, Gemini, Perplexity, Copilot and others are reported as their own traffic source, alongside how many pages their crawlers fetched and the ratio between the two. Works on history you already collected.
- Bot filtering you can actually see. Crawlers, AI trainers, SEO tools, uptime monitors, link previews and headless browsers are sorted into categories and reported back to you, so you can check what was excluded instead of taking it on trust.
- Catches automated browsers that lie about who they are. A headless browser can send any user agent it likes, so the tracker checks the flags the automation tools set on themselves.
- Skips prefetched and prerendered pages, which the reader may never actually look at.
- Honours Do Not Track and Global Privacy Control: visitors who ask not to be tracked are not tracked, with no setting to turn on.
- Core Web Vitals measured from the people actually visiting your site, where a lab tool measures one run on one machine. Loading, responsiveness and visual stability, reported as the 75th percentile Google grades on, because an average hides the slowest quarter of your readers.
- The searches that brought people in, pulled from Search Console. Google strips the query before your site ever sees it, so this is the only place it exists.
- One-click import of your history out of Google Analytics 4, alongside the seven WordPress plugins already supported.
- Notes pinned to a date, so a spike six weeks ago still has an explanation attached to it.
- Let an editor or a client read the dashboard without making them an administrator.
- Works across a multisite network, with a single view of every site for whoever runs them.
- Tells you when it stops recording. Analytics fails silently by nature, and a plugin that has stopped counting looks exactly like a quiet week, so this one raises it in Site Health and in the admin rather than leaving you to notice a flat chart weeks later.
- Per-site “exclude logged-in admins” setting.
External services
This plugin includes an optional one-click importer for historical Jetpack Stats data. When (and only when) a site owner runs that importer from Vireo Analytics Tools Import, the plugin calls the WordPress.com Stats API at https://stats.wordpress.com/csv.php to retrieve the site’s existing pageview, referrer, and top-post history.
What is sent:
- The site’s existing Jetpack
blog_id(read from the connected Jetpack install). - The Jetpack API key (read from the connected Jetpack install).
- A requested date range and table name (e.g.
views,referrers,topposts).
What is not sent: no visitor data, no user data, no site content, no credentials beyond the Jetpack API key the site already holds.
When it is sent: only while the import is actively running, in response to a site owner clicking “Run Import”. No background or recurring calls are made.
The Stats API is provided by Automattic Inc. (WordPress.com).
- Terms of Service: https://wordpress.com/tos/
- Privacy Policy: https://automattic.com/privacy/
The importer is entirely optional.
Google Analytics (optional, importer)
When a site owner runs the Google Analytics importer, the plugin signs a request with the service-account key they pasted and calls https://oauth2.googleapis.com/token for an access token, then https://analyticsdata.googleapis.com to read that property’s history.
What is sent: the service account’s own credentials and a date range. No visitor data, no site content, nothing about anyone reading the site.
When it is sent: only while an import is running, in response to a site owner starting one. The key can be removed from the same screen afterwards.
Google Search Console (optional)
When a site owner connects Search Console, the plugin authenticates the same way and calls https://searchconsole.googleapis.com to read the searches that led people to the site.
What is sent: the service account’s own credentials, the property address the owner entered, and a date range.
When it is sent: once a day on a schedule, and whenever the owner presses the refresh control. This is the one recurring outbound call the plugin makes, and it only happens on sites that have connected Search Console. Disconnecting stops it and removes the schedule.
Both Google APIs are provided by Google LLC.
- Terms of Service: https://policies.google.com/terms
- Privacy Policy: https://policies.google.com/privacy
DB-IP (optional, geolocation)
Country-level geolocation needs a local database, and Pro’s city-level detail needs a second one. When a site owner presses the download button, the plugin fetches the current monthly file from https://download.db-ip.com.
What is sent: nothing but the request for the file. No site data, no visitor data, no identifiers. Lookups afterwards happen entirely on your own server against the downloaded file; no address is ever sent anywhere.
When it is sent: only when the owner presses the button.
DB-IP Lite data is provided by DB-IP under CC BY 4.0.
- Website: https://db-ip.com/
- Privacy Policy: https://db-ip.com/legal/privacy
A site that imports nothing, connects nothing and downloads no geolocation database contacts none of these services.
It does make one recurring request to itself: once an hour the plugin posts to its own tracking endpoint to check the fast path is still reachable, because a host rule or a CDN change can break it silently. That request never leaves your server and carries no data about anyone.
None of these paths send visitor data anywhere. Every one of them pulls data in.
Screenshots





Installation
- Upload the plugin files to
/wp-content/plugins/vireo-analytics/, or install via Plugins Add New and search for “Vireo Analytics”. - Activate the plugin through the Plugins menu.
- Visit Vireo Analytics Overview to see your dashboard. First pageviews will appear within a minute.
If you already have analytics data from Koko Analytics or Jetpack Stats, you’ll see a one-click import banner on the Overview page. Imports run in the background and can be paused or resumed at any time.
FAQ
-
Is this GDPR-compliant?
-
Yes. Vireo Analytics doesn’t set cookies, doesn’t use localStorage for tracking, and hashes visitor identifiers with a salt that rotates every 24 hours. No consent banner is required for basic analytics. Country-level geolocation is derived from the IP on the server; the raw IP is never written to the analytics tables, and the short-lived buffer file holding it is deleted as soon as the aggregator processes it, usually within a minute. You should still mention the plugin in your privacy policy.
-
What exactly does the “Visitors” number mean?
-
Within a single day it’s a true unique count: one person is counted once no matter how many pages they read. Over a longer range it’s the sum of the daily counts, so someone who visited on three days in the month counts three times.
That isn’t laziness, it’s the privacy model. A visitor is identified by a hash of their IP and browser, salted with a secret that is thrown away and regenerated every night. Tomorrow the same person produces a completely different hash, so there is nothing that can link them back to today. That’s what makes cookie-free, consent-free tracking legitimate rather than a technicality. The cost is that “unique people this month” is a question the data genuinely cannot answer, and we’d rather say so than print a number that looks precise and isn’t.
Pageviews are exact over any range.
-
Does it honour Do Not Track and Global Privacy Control?
-
Yes, and it’s on by default. If a visitor’s browser sends
DNT: 1orSec-GPC: 1, the tracker refuses to fire for them, and the collection endpoint refuses the pageview even if something sends one anyway. Those visits are not recorded anywhere. This costs you some traffic in the dashboard, which is the point: an opt-out you can switch off isn’t an opt-out. If you have a specific reason to override it, thevireo_honor_opt_outfilter can return false. -
How does it compare to Koko Analytics?
-
Vireo Analytics started with deep respect for Koko’s approach and covers the same ground: cookie-free, self-hosted, clean dashboard. Where they differ: Vireo Analytics includes per-post analytics columns on the Posts list screen, 404 tracking with referrer attribution, an admin-bar sparkline, and a more aggressive bot filter. Both are excellent; pick whichever fits how you work.
-
Why are my numbers lower than [other plugin]?
-
You’re probably used to inflated numbers. Most other analytics plugins count uptime monitors (StatusCake, UptimeRobot, Pingdom), SEO crawlers (Ahrefs, Semrush, MJ12), AI indexing bots (GPTBot, ClaudeBot, PerplexityBot), and various testing tools as “visitors”. Vireo Analytics filters all of those out, so the numbers you see are real humans reading your content. On most sites this strips 20-40% of what other tools report. We think that’s the right tradeoff; if you disagree there’s a filter (
va_bot_ua_pattern) to relax the pattern. -
Can I migrate from Jetpack Stats?
-
Yes. The built-in importer pulls up to 14 months of historical data from the Jetpack Stats API (requires an active Jetpack connection). The Overview page will show a one-click banner the first time Jetpack Stats is detected on the site.
-
Can I migrate from Google Analytics?
-
Not directly. Google Analytics doesn’t expose a backfill API that’s compatible with our schema. You can run both plugins side by side for a week to confirm the numbers match, then remove GA.
-
Does it work with caching plugins?
-
Yes. Tracking runs on every page load via a lightweight POST beacon, including cached pages. When you first activate the plugin, you may need to wait for your cache to regenerate before every page starts tracking. Once that happens, tracking is universal.
-
How does it compare to Google Analytics?
-
Google Analytics shows you more (segmentation, audiences, attribution modelling) but requires a cookie banner, sends your visitors’ data to Google, and has become increasingly complex. Vireo Analytics answers “how many people visited what, from where, on what device”, which is what most WordPress site owners actually look at. For site owners who don’t need enterprise-grade attribution modelling, the tradeoff is usually worth it.
-
Does it track logged-in admins?
-
By default, no. There’s a setting at Vireo Analytics Settings to enable admin tracking if you want to see your own visits (useful for testing).
-
How long is data kept?
-
Raw visitor hashes and referrer rows are aggregated into daily rollups after 24 hours. Daily rollups are kept indefinitely unless you set a retention limit in Settings Retention. The default retention is 730 days (two years).
-
Will it slow down my site?
-
The tracking beacon is fired asynchronously after the page is interactive, using the browser’s
navigator.sendBeacon()API where available. The pageview is recorded server-side via a lightweight admin-ajax handler. On a well-hosted site you will not notice a difference.
Reviews
Contributors & Developers
“Vireo Analytics – Privacy-First Website Analytics” is open source software. The following people have contributed to this plugin.
Contributors“Vireo Analytics – Privacy-First Website Analytics” has been translated into 2 locales. Thank you to the translators for their contributions.
Translate “Vireo Analytics – Privacy-First Website Analytics” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
0.7.2
- Added: countries you can treat as junk traffic. Settings, then Geo-location, takes a list of two-letter codes, and anything from those countries is discarded instead of counted, the same as an excluded IP. Bots that announce themselves were already filtered; this is for the traffic that does not. Requested by a user who found the bot filter was not catching what he was seeing.
0.7.1
Hardening and import safety, from an independent review of the 0.7.0 code.
- Fixed: the tracking endpoint believed a forwarded IP header from anyone who sent one, once proxy trust was switched on. A visitor could claim any address, which sidesteps the per-address rate limit and the excluded-IP rules and invents a new visitor on demand. Headers are now read only when the connection itself came from a known proxy, and the list of those can be set with the
vireo_trusted_proxiesfilter. Cloudflare’s ranges and the private ranges are trusted by default, so a site behind either keeps working unchanged. - Fixed: around the day boundary two visitors could be issued different daily salts, because rotation read, generated and wrote without claiming anything first. Only one salt survived, so visitors hashed under the others were counted a second time. A day’s salt is now claimed once and shared. Salts older than yesterday are still deleted, so old hashes stay unrecoverable.
- Fixed: the buffer’s size limit applied to each file rather than to the directory. A burst arriving while no buffer existed could create several, each with its own allowance. There is now a ceiling across the directory, and the aggregator drains every buffer it finds in a tick rather than the first one.
- Added: imports record what they brought in. Asking for dates already imported now says which run covers them and asks you to confirm, rather than adding the numbers on top in silence. Importing still cannot be undone, so the confirmation is the protection.
- Added: setup instructions for the Google Analytics import and for Search Console now name each screen, both Google products involved, and which property ID is the right one.
- Fixed: the import progress bar reports itself to screen readers, and results and errors are announced instead of appearing silently.
0.7.0
This release fixes a fault that could stop a site recording, and answers several questions the dashboard had been raising without being able to settle.
- Fixed: on a site that had already been running, a new table could be missing while the code that writes to it was live. Because the daily rollups are written together, that one failure rolled back everything written beside it, including pageviews, and the site stopped recording. Schema changes no longer depend on anyone remembering to bump a version, and a missing table now costs its own rows and nothing else.
- Fixed: deleting the plugin left a Google service-account key, a set of access tokens, four tables and every uploaded database behind. On a network it cleaned only the site you happened to be on. Uninstall now removes all of it, on every site.
- Fixed: a saved Google key could not be removed once pasted. There is now a control for it, and removing the key drops its cached tokens too.
- Fixed: Search Console could lose months of history if a sync timed out partway. It now works a day at a time and loses at most the day it was in the middle of.
- Fixed: speed measurements ignored the excluded-IP list and stored page paths in a different form from everything else, so they never lined up with the pages they described.
- Fixed: a crafted link could put arbitrary text into an admin notice on the settings screen.
- Fixed: on a network, activating the plugin set up only one site, and sites created afterwards were never set up at all.
- Fixed: on sites running Redis or Memcached, the per-address limit on the tracking endpoint counted by reading a value and writing it back, so requests arriving together could all read the same number and all pass. It now counts atomically, and treats an IPv6 allocation as one address rather than as the billions it contains.
- Fixed: importing from Google Analytics could only be reached by knowing the address of its screen, because nothing linked to it.
- Added: Core Web Vitals from real visitors, reported as the 75th percentile.
- Added: Search Console, bringing in the searches people ran before they arrived.
- Added: import from Google Analytics 4.
- Added: notes pinned to a date on the chart.
- Added: choose which roles can read the dashboard, without handing out administrator.
- Added: a network-wide view on multisite.
- Added: works with consent plugins that use the WP Consent API, and can be told to wait for consent on sites whose policy asks for it.
- Added: a Site Health check that says whether the site is recording right now.
0.6.2
This release fixes a fault that could stop a site recording altogether, with nothing to show for it but a dashboard that stopped moving.
- Fixed: one visit the database refused would fail the whole batch, and the retry left a file behind that the plugin then mistook for the buffer it was writing to. Visits kept being saved to it and never reached your reports. Any buffer left stranded this way is picked up and written when you update, so those visits come back.
- Fixed: search terms and other text containing emoji could be cut in half mid-character, which produces text the database rejects. That is what set the above off. Text is now trimmed on character boundaries.
- Fixed: when a batch failed partway, whatever had already been written stayed written, so retrying it counted some visits twice.
- Fixed: the job that writes visits to the database was scheduled once, when the plugin was activated, and never checked again. If it went missing the site stopped recording permanently. It is now put back automatically.
- Added: when the plugin cannot record, it says so in the admin and in the error log, rather than failing silently.
0.6.1
- Fixed: on sites also running Vireo Analytics Pro, 0.6.0 created a table that Pro already owns, with a different structure. Left in place it would have written malformed rows into Pro’s entry and exit page data. The feature has been withdrawn from the free plugin, where it duplicated Pro to begin with. Sites without Pro were unaffected; no data is lost either way.
0.6.0
- Fixed: activating the plugin could fail with a fatal error when the daily salt rotation had not yet been scheduled, which included every fresh install and any site that was deactivated and reactivated. Introduced in 0.5.0.
- New: see what the AI assistants take against what they send back. ChatGPT, Claude, Gemini, Perplexity, Copilot and others are reported as their own traffic source, next to how many pages their crawlers fetched, and the ratio between the two. Works on history you already collected.
- New: import your history from Statify, WP Statistics, Burst Statistics, Independent Analytics and SlimStat, joining the existing Koko Analytics and Jetpack Stats importers.
- Improved: bot coverage goes from around thirty user-agent patterns to over eight hundred named crawlers, sorted into the same categories the dashboard already reports. AI crawler coverage roughly triples.
0.5.0
- New: pageviews are counted even when JavaScript never runs. A no-script pixel catches readers whose browser blocks or strips the tracker, and it keeps working behind a full-page cache.
- New: the bot filter reports what it caught. Crawlers, AI trainers, SEO tools, uptime monitors, link previews and headless browsers are sorted into categories and shown above your totals, so you can see what was excluded instead of taking it on trust.
- New: automated browsers are no longer counted as readers. A headless browser can send any user agent it likes, so the tracker now checks the flags the automation tools set on themselves.
- New: prefetched and prerendered pages are skipped. The browser fetched them speculatively and the reader may never look at them.
- Improved: wider crawler coverage, including Amazonbot, OAI-SearchBot, ChatGPT-User, Meta’s crawler and CommonCrawl.
- Improved: the tracker posts to its own endpoint instead of admin-ajax, which skips loading every other plugin and your theme on each pageview. The more plugins a site runs, the bigger the saving. It never affected how fast a page loaded for the reader, but it did tie up a PHP worker and a database connection each time, and those run out before bandwidth does. Hosts that block direct PHP in wp-content keep using the old route automatically.
- Fixed: page paths containing accented or non-Latin characters were recorded with those characters missing. A Japanese or Cyrillic slug could be reduced to little more than slashes, so those pages were effectively invisible in your reports. They are now recorded as written.
- Fixed: the per-IP request limit counted each IPv6 address separately. A single IPv6 allocation covers billions of addresses, so one visitor could rotate past the limit indefinitely; IPv6 is now grouped by network block.
- Fixed: the feature list claimed server-side tracking counted every pageview. That was never true of a JavaScript beacon; with the no-script pixel added, the claim now matches what the plugin does.
0.4.4
- Fixed: automated scanners could get themselves listed as broken links on your site by faking the page they came from. A referrer is now only believed when it could plausibly be a real page: it cannot be the missing page itself, and it cannot be another address that is also missing.
0.4.3
- Changed: the 404 report is now two reports. Pages on your site that link somewhere missing are listed as broken links, with the page doing the linking so you know where to go and fix it. Everything else, mostly automated scanners looking for software you don’t run, is listed separately. Previously they were mixed, and on a typical site the handful of real broken links were buried under thousands of probes.
0.4.2
- New: goal completions now fire an action carrying which visitor completed which goal and when, so add-ons can measure things the daily totals can’t answer, such as whether the same person completed one goal and then another, which is what a funnel is.
0.4.1
- Fixed: a flood of requests for pages that don’t exist could grow the 404 report without limit. Those hits are now rate-limited the same way the tracker is, and there’s a cap on how many distinct new paths get recorded per day (filterable, and you can turn it off). Broken links you already know about keep counting as before.
- Fixed: the analytics abilities used by AI agents and other Abilities API clients were declared read-only in the wrong place, so WordPress treated them as if they changed your site. Asking for them over GET returned an error, and agents were told a request for your traffic numbers might modify something. They are now correctly marked read-only.
0.4.0
- New: you can rearrange the dashboard. Grab the handle above any card and drag it where you want it, or tab to the handle and use the arrow keys. Your layout is saved to your own account, so it doesn’t change anyone else’s, and a Reset layout link puts it back.
- Changed: Top Pages, Top Referrers and Top Countries now sit above the traffic chart. The chart mostly restates the totals you’ve already read at the top of the page, and it was pushing the tables you actually came for below the fold.
0.3.4
- New: the dashboard now opens with a plain-language line telling you what matters, before the chart and tables. On a quiet or brand-new site it says so honestly instead of leaving you to read a wall of numbers.
- New: a short note under the pageviews explains why Vireo’s totals sit below what server-log or pixel tools report: it counts real browsers only, so bots that never run JavaScript are never counted. Dismissable, and it stays dismissed.
0.3.3
- New: the plugin now ships in nine languages: Spanish, German, French, Italian, Dutch, Portuguese (Portugal and Brazil), Russian and Japanese. Every screen is covered, including the analytics dashboard.
- Fixed: the dashboard’s JavaScript never loaded translations, so the chart tooltips and screen-reader labels, the world-map tooltip and the settings-page messages stayed in English whatever the site language. Those strings now translate like everything else (loaded via wp_set_script_translations).
0.3.2
- Fixed: the “What changed” panel mixed pages and traffic sources in one list, which double-counted the same change and read as a jumble. Pages and sources are now shown as two separate labelled groups, so you can tell “which of my content moved” from “which sources moved”. Your own domain no longer shows up as a referrer.
0.3.1
- New: countries now set themselves up. Most sites are behind a host or CDN (Cloudflare, Kinsta, CloudFront) that already knows each visitor’s country, and the plugin now uses that automatically, with nothing to install. The settings page tells you when this is the case instead of asking you to go and fetch a database.
- New: for sites without that, one button installs a free country database (DB-IP, no account, no license key). This replaces the old four-step MaxMind process, which is still available as an advanced option for anyone who prefers it.
- Fixed: the date-range tabs and the other toggles rendered as plain grey text with no button shape until selected. They now sit on a track with the selected option as a raised pill, so they read as buttons.
0.3.0
- New: a “What changed” panel that tells you why your traffic moved, instead of leaving you to work it out from five leaderboards. It names the pages and referrers that gained and lost the most, and says which one is actually responsible for the change. It won’t guess: if the drop is spread thinly across the whole site, it says so rather than blaming whichever page happens to be top of a list.
- New: the dashboard now warns you when the data itself is wrong. If tracking stops recording, or every visitor’s country comes back unknown, or 404s spike, it says so. A tracker that has stopped working looks exactly like a site with no visitors, and nothing used to tell you which one you were looking at.
- Changed: browsers, operating systems and device types are now one Technology card with tabs, instead of three near-identical cards taking a full row.
- Changed: 404s, campaigns and site search moved into a collapsed section at the bottom. They’re useful when you want them and noise when you don’t.
- Changed: comparison is no longer a Pro feature. It was already showing everyone the numbers while pretending the toggle was locked.
- Improved: changing the date range or a filter no longer reloads the whole admin page. It swaps the cards in place, so switching from Last 30 to Last 90 is instant.
- Improved: every card now expresses change the same way. Percentages on lists, percentage points on shares, so “Chrome -1,568” next to a 58.2% bar no longer reads as a 1,568-point collapse in share.
- Fixed: Direct traffic was always reported as zero. Direct visits have no referrer, so nothing could ever land in that bucket, even though direct is usually the biggest source on a site.
- Fixed: the traffic chart can now be read with a keyboard. Arrow keys step through the days and each one is announced, where the values were previously only reachable with a mouse.
- Fixed: the chart’s vertical axis now follows whichever metric you’re looking at. Switching to Visitors used to squash the bars against a ceiling that belonged to Pageviews.
- Fixed: the prior period is now drawn as a thin reference line rather than a solid bar. On any day traffic fell, the old bar stood taller than the real one and dominated the chart.
- Fixed: a stray PHP notice on every front-end page load, which on sites with debugging switched on could break the login screen.
0.2.10
- Fixed: the traffic chart was stretched sideways. It was drawn at a fixed width and then scaled up to fill the card, so on a wide screen everything in it, including the date labels, came out squashed horizontally. It’s now drawn at the size it’s actually displayed at, and redraws when the window resizes.
- Fixed: the “Start fresh” link on the welcome banner rendered as a bordered box with underlined link text inside it. It was using two WordPress button classes that undo each other.
0.2.9
- Fixed: on sites with full-page caching, traffic could silently stop being counted. Whether a visitor was a bot, or had asked not to be tracked, was decided when the page was built, and that page then got cached and served to everyone else. If a crawler happened to be the request that filled the cache, the tracker was left out of the cached page and every real visitor served it afterwards went uncounted. The page now always carries the tracker and those decisions are made at the point the visit is recorded, which can’t be cached. Do Not Track and bot filtering work exactly as before.
0.2.8
- Fixed: the dashboard could render as raw, unstyled HTML. Its stylesheet was served from a copy the plugin wrote into your uploads folder, so anything that made that copy unreachable, a host rule, a CDN rewrite, tightened permissions, left the page with no styling at all. The stylesheet now loads from the plugin folder, alongside its JavaScript, where it can’t go missing. Old copies in uploads are cleaned up on update.
- Countries now work without a MaxMind database on most sites. If your host resolves the visitor’s country for you (Cloudflare, CloudFront, and hosts running the nginx GeoIP module all do), Vireo uses that and you don’t need to download anything. The MaxMind file is now only a fallback for sites whose host doesn’t provide one.
0.2.7
- No functional changes. The directory listing now says what the plugin actually does, so people searching for privacy-friendly analytics can find it.
0.2.6
- Security: the tracking endpoint is now rate limited on every site. The limiter only worked when a persistent object cache (Redis, Memcached) was installed and did nothing otherwise, which is most sites, leaving the public endpoint open to being flooded. It now falls back to APCu, then to lightweight counter files. It still writes nothing to the database on public requests.
- Fixed: retrying an import chunk could count it twice. Imports are applied additively and the browser retries a chunk when a response is lost, which can happen after the data has already been written. The server now records which chunks it has applied and refuses to apply one twice.
- Security: the WordPress.com API key used by the Jetpack importer no longer lingers. It was kept in the options table indefinitely if an import failed or was abandoned, it survived uninstalling the plugin, and it was written back into the import form’s HTML. It now expires on its own, is cleared when an import fails, is removed on uninstall, and is never rendered into the page.
0.2.5
- Fixed: traffic was filed under the wrong day on any site not set to UTC. Days were bucketed in UTC while the dashboard’s date ranges were built in your site’s timezone, so evening visits could land on tomorrow and “Today” would be missing hours it should have had. The admin bar and the dashboard could also disagree about what “today” was. Everything now uses your site’s day, and the visitor salt rotates on the same boundary.
- Note: the changeover creates a one-off seam. Traffic recorded before this update stays bucketed as it was, so the day either side of the upgrade may look slightly off. It settles immediately after.
- Changed: the Visitors figure no longer describes itself as “unique this period” for multi-day ranges, because it isn’t. Within a day it’s a true unique count; across a range it’s the sum of the daily counts. See the FAQ for why the privacy model makes a true multi-day unique count impossible.
- Fixed: a failed database write could silently throw traffic away. The pageview buffer was deleted as soon as it had been read, before the rollups were written, so a deadlock or a dropped connection during the write lost that minute of traffic for good. The buffer is now kept until the database has confirmed the writes, and anything a crashed run left behind is replayed on the next one.
0.2.4
- Fixed: unique visitor counts were inflated. The aggregator processes pageviews in batches, and it only recognised a repeat visitor within a single batch, so someone reading several pages was often counted as several visitors. Visitors are now tracked across the whole day, so the count is a real unique count. This affects every visitor figure: site, per-post, per-referrer, country, device, and hourly.
- Note: your visitor numbers will drop after this update, on some sites by a lot. Nothing has been lost. The old numbers were counting the same people more than once. Pageviews are unchanged and were always correct.
- Fixed: the per-post referrer table was never covered by the retention setting and grew without limit.
0.2.3
- Fixed: the tracker is no longer held back by delay-JavaScript optimisers. Perfmatters, WP Rocket, LiteSpeed Cache, and FlyingPress delay scripts until the visitor interacts with the page, so visits that ended without a click or scroll were never counted. Vireo now excludes its own scripts from those optimisers automatically, no configuration needed. If you run one of them, expect your numbers to rise: that is traffic you always had and were not seeing.
- Fixed: the dashboard rendered unstyled on Apache. The hashed dashboard stylesheet was written inside the plugin’s data directory, which carries a deny-all rule that Apache also applies to everything beneath it, so the stylesheet was blocked. It now lives in its own directory outside the protected one.
- Fixed: the dashboard overflowed horizontally at 1280px wide, clipping the countries card.
0.2.2
- Beacon rate limiter now keeps its per-IP state in a persistent object cache only, so public tracker requests never write to the options table.
0.2.1
- Removed third-party favicon requests from the dashboard, no remote calls remain.
- Tracker reads its runtime config from the database instead of generated PHP files.
- Added nonce verification to the dashboard view filters.
- Prefixed all internal identifiers with vireo_.
0.2.0
- Initial public release.
- Self-hosted, cookie-free analytics dashboard.
- Importers for Koko Analytics and Jetpack Stats.
- Weekly email digest.
- Custom events API.
- 404 tracking.
- CSV export from any dashboard view.
- Admin-bar sparkline + per-post analytics columns.
