=== Vireo Analytics – Cookieless Google Analytics Alternative with Search Console ===
Contributors: RebelCode, jeangalea
Tags: google-analytics-alternative, privacy-friendly, marketing-analytics, cookieless-analytics, search-console
Requires at least: 6.9
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 0.9.0
License: GPLv3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html

Self-hosted WordPress analytics that imports your Google Analytics history, shows the searches behind each post, and filters out bots. No cookies.

== Description ==

Vireo Analytics is self-hosted analytics for WordPress. It brings your history with you when you leave Google Analytics, shows the Google searches behind each post, and tells you what AI assistants take from your site against the readers they send back. Visitor data stays in your own database, with no cookies and no consent banner.

= Bring your history with you =

Switching analytics tools usually means your charts start again on the day you switch. Vireo imports your daily traffic, pages, referrers, countries and devices from Google Analytics 4, so the years you already have stay in front of you.

It also imports from Koko Analytics, Jetpack Stats, Statify, WP Statistics, Burst Statistics, Independent Analytics, SlimStat and Plausible (from Plausible's CSV export). Imports run in batches you can watch, stop the day before Vireo's own data begins so no day is counted twice, and ask before importing a range you've already brought in.

= The searches behind each post =

Google stopped passing search terms to websites years ago, so most analytics plugins see a visit from Google and nothing more. Connect Search Console once and every post's stats screen lists the searches it appeared for, with clicks, impressions and average position. The Overview shows the landing page beside each search.

= What AI takes, and what it sends back =

ChatGPT, Claude, Gemini, Perplexity and Copilot are reported as their own traffic source. Next to the readers they send, Vireo counts the pages their crawlers fetched, so you can see the ratio for your own site. It works on the history you've already collected.

= Numbers you can check =

Over 800 known crawlers, AI trainers, SEO tools, uptime monitors and headless browsers are kept out of your numbers. Vireo reports how many it excluded and why, so a figure lower than your server logs makes sense. Automated browsers that claim to be Chrome are caught by the flags automation tools set on themselves, and prefetched pages nobody looked at aren't counted.

= Everything else =

* An Overview that opens with what changed since the previous period, and the pages and sources behind it.
* Top pages, referrers grouped by how people arrived, countries and devices, with filters for each.
* Core Web Vitals from your real visitors, graded at the 75th percentile the way Google grades them.
* Notes pinned to a date, so a spike still has its explanation weeks later.
* A weekly email digest, CSV export of any view, and a views column on the Posts screen.
* Custom events for clicks and form submissions, and 404 tracking to find broken links.
* Read-only dashboard access for editors or clients, without making them administrators.
* Multisite support, with one view across the network.
* A warning in Site Health if recording stops, so a broken setup doesn't look like a quiet week.
* Reports for WP RSS Aggregator and Spotlight sites: clicks through to aggregated articles per feed source, and what visitors do with Instagram feeds.
* Translated into German, Spanish, French, Italian, Japanese, Dutch, Portuguese (Portugal and Brazil) and Russian.

= Privacy =

Vireo sets no cookies and stores nothing on visitors' devices. Visitors are counted with a salted hash that changes every day, so nobody can be followed from one day to the next. Do Not Track and Global Privacy Control are honoured without a setting. Visitor data is never sent to RebelCode or anyone else; the only outside services are the ones you choose to connect, listed under External services below.

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/vireo-analytics/`, or install via Plugins → Add New and search for "Vireo Analytics".
2. Activate the plugin through the Plugins menu.
3. Visit **Vireo Analytics → Overview** to see your dashboard. First pageviews will appear within a minute.

To bring your history across from Google Analytics or another plugin, go to **Vireo Analytics → Settings → Import data**. If another supported analytics plugin is installed, the Overview offers to import from it.

== Frequently Asked Questions ==

= 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: 1` or `Sec-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, the `vireo_honor_opt_out` filter can return false.

= Why are my numbers lower than another plugin's? =

Most analytics plugins count some automated traffic as visitors: uptime monitors (StatusCake, UptimeRobot, Pingdom), SEO crawlers (Ahrefs, Semrush, MJ12), AI crawlers (GPTBot, ClaudeBot, PerplexityBot) and testing tools. Vireo filters those out and shows you what it removed, by category, so you can check the difference yourself. If you'd rather count some of them, the `vireo_bot_ua_pattern` filter changes the pattern.

= Can I migrate from Jetpack Stats? =

Yes. Settings, then Import data, pulls your daily pageviews, top posts and referrers from the WordPress.com Stats API for whatever date range you choose. You paste in your WordPress.com API key and the site address the stats are recorded under; Jetpack doesn't need to be active or connected.

= Can I migrate from Google Analytics? =

Yes. Settings, then Import data, brings your history across: daily pageviews and visitors, your pages, your referrers, countries and devices. It reads the Analytics Data API directly, so you need a Google service account key and your numeric property ID rather than the G- measurement ID. Sessions, events and conversions do not come across, because there is nowhere to put them.

= Can I migrate from Plausible Analytics? =

Yes. Export your site from Plausible (Site settings, then Imports & Exports), then upload the zip under Settings, then Import data. Daily pageviews and visitors, pages, referrers, countries and device types come across. No API key is needed, and the uploaded file is deleted as soon as it has been read. Browsers, operating systems, entry and exit pages, goals and custom properties are not imported.

= Does Vireo share anything with MainWP? =

Only if the site is connected to a MainWP dashboard through MainWP Child. Then the dashboard can read the site's visitor and pageview totals, top pages and top referrers, over MainWP's own signed connection. Nothing about individual visitors is shared, and the dashboard already has administrator access to the site. To switch it off, add `add_filter( 'vireo_mainwp_enabled', '__return_false' );` to the site.

= 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.

= My theme or landing page doesn't count visits. Why? =

The tracker loads in the footer through `wp_footer()`. Some themes, page builders and landing-page templates leave that call out, so the tracker never loads. Add `<?php do_action( 'vireo_print_tracking_script' ); ?>` just before `</body>` in that template. It's safe on pages that also call `wp_footer()`: the tracker only prints once.

= 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 by a standalone endpoint that boots WordPress without loading plugins or your theme. Hosts that block direct PHP inside wp-content fall back to admin-ajax automatically. On a well-hosted site you will not notice a difference.

== External services ==

This plugin includes an optional importer for historical Jetpack Stats data. When (and only when) a site owner runs that importer from **Vireo Analytics → Settings → Import data**, 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 WordPress.com API key the site owner pastes into the import screen.
* The site address whose stats are being imported.
* 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 API key the site owner entered.

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. A refresh fetches the last week straight away and then the rest of the requested history in the background, a fortnight at a time over a few minutes, so that one press can produce several calls in a row. 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 ==

1. The overview: what changed since the period before, the headline numbers, what the bot filter kept out, and daily traffic.
2. One post's stats, with the Search Console searches that brought people to it.
3. AI assistants: how many pages their crawlers took, against how many readers they sent back.
4. Top pages, referrers grouped by how people arrived, and where in the world they were.
5. Importing history from Google Analytics 4, Plausible, Koko Analytics, Jetpack Stats and other plugins.
6. Settings, including which roles can read the dashboard without being administrators.

== Changelog ==

= 0.9.0 =
* Added: import your history from Plausible Analytics. Upload the export zip from Plausible's site settings, and Vireo brings across your daily visitors and pageviews, pages, referrers, countries and device types.
* Added: groundwork for MainWP. A MainWP dashboard can now read a site's visitors, pageviews, top pages and referrers over MainWP's own signed connection, for an upcoming Vireo Analytics for MainWP add-on. Site owners can switch it off with the `vireo_mainwp_enabled` filter.
* Added: the weekly email opens with a one-line summary of what changed since the week before.
* Added: themes and landing pages that skip wp_footer() can load the tracker with do_action( 'vireo_print_tracking_script' ).
* Added: Support and Rate links on the Plugins screen, a support-forum link in Settings, and a one-time review request on the Overview after two weeks of use, which you can dismiss for good.
* Fixed: importing from another analytics plugin added its numbers on top of the days Vireo had already counted. Imports now stop the day before Vireo's own data begins, and the import screen says where. You can still include those days, with a warning that they'll be counted twice.
* Fixed: the Search and Social source filters showed no referrers, and any source filter left "What changed" without its sources because of a database error.
* Fixed: referrers now land in the same group on the dashboard card and in the source filters. Hacker News, Product Hunt, Slack, Discord and Mastodon instances count as Social, and AI assistants have their own group.
* Fixed: visits from the Google app on Android show as "Google app" under Search, and other common Android apps (Gmail, LinkedIn, Reddit, Slack, Telegram) show by name.
* Fixed: bots that name themselves (Amazon, Claude and DeepSeek search bots, xAI, Kimi, Mistral, Mojeek and more) are counted in their own category instead of "Other bots", about 40 bots that were counted as visitors are now filtered, and Internet Explorer 11 visitors on Windows 7 are no longer mistaken for search engines. Categories are assigned as stats are saved, so earlier history keeps its old ones.
* Fixed: counts of one read correctly across the plugin ("1 day", "1 view", "1 request").
* Changed: days with a note get a pin on the traffic chart, so a spike carries its explanation without hovering.
* Changed: the countries card shows the map with the top ten under it, and the map shades every country with visitors, not only the top ten.
* Changed: the front page is called "Home" in "What changed" and the weekly email, matching Top pages, and referrers shown by name (ChatGPT, Gemini) show their site address underneath.
* Fixed: the import screen and first-run banner no longer offer a plugin whose tables are empty.
* Fixed: the AI crawler ratio shows a whole number from 10 up, and several small layout issues in Top referrers, the bot note and the roles setting.
* Fixed: the group totals in Top referrers (Search, AI assistants, Social, Referral) count every site in the range, not only the ten listed, so they match the source filters and the Search & AI tab.
* Security: a Plausible upload is capped by its unpacked size, loading the import screen can no longer cancel a running import, each import run keeps its own progress so two tabs can't interfere, and long page paths or hosts in non-Latin scripts no longer make an import fail.

= 0.8.9 =
* Changed: Settings is split into tabs (General, Tracking, Email digest, Search Console & API, Import & export), and the dashboard into Overview, Search & AI, Page speed, and Issues & campaigns. The tab is kept in the URL, so reloads and shared links open on it.
* Changed: the import screen starts with the analytics plugins found on this site, each with its own button. Other sources sit below as secondary options.
* Fixed: after an import finished, the screen still offered "Start Import" next to a warning about running it twice, which read as if the first run had failed. It now says the import is done, with the date range and pageviews brought in, and running the same source again needs a confirmation. Jetpack gets the same overlap check as Google Analytics.
* Fixed: the import screen's record count was wrong (it showed 180 for 90 days).

= 0.8.8 =
* Fixed: on a busy day with thousands of distinct pages or referrers, the database could reject the single statement that saved them, and after five tries that stretch of traffic was set aside instead of counted. Rows are now saved in batches of 500, still inside one transaction.
* Fixed: a database error early in a save could go unnoticed if a later query in the same step succeeded, so the buffer was deleted with some of its rows missing. Every failed query now stops the save and keeps the buffer for another try.

= 0.8.7 =
* Fixed: sites on WP Engine recorded no visitors. WP Engine's network sends a `Cf-Verified-Bot: false` header with every request, and Vireo treated any value in that header as a verified bot. It now counts as a bot only when the value is `true`.

= 0.8.6 =
* Changed: Vireo Analytics is now developed and supported by RebelCode, the team behind WP RSS Aggregator and Spotlight.

= 0.8.5 =
* Added: a card for sites running WP RSS Aggregator. It counts the readers who click an aggregated item through to the original article, per feed source, and, for items imported as posts, the views each source's posts get on your site.
* Added: a card for sites running Spotlight. It counts how often visitors open an Instagram post from a feed, follow a link from the post, press Follow or Load more, and on which page.
* Both start counting when Vireo detects the plugin, and can be switched off with the `vireo_track_wpra` and `vireo_track_spotlight` filters.

= 0.8.4 =
* Fixed: a post ID that matches no post could list one page twice. The tracker sends the post ID from the visitor's browser, so a client can send any number; one that resolves to no post is now ignored and the view counts under the page's address.

= 0.8.3 =
* Fixed: one visitor could add unlimited pageviews in a day. A crawler that runs scripts viewed the same archive pages almost ninety times each and put several hundred pageviews into one day's report. Each visitor now counts for at most 5 views of a page and 50 views in total per day; anything past that is reported under filtered traffic as repeated views. Both limits can be changed with the `vireo_visitor_path_cap` and `vireo_visitor_daily_cap` filters.
* Fixed: the front page could appear twice in the page lists and in What changed, once by its title and once as "/", because some views were recorded with its post ID and some without. Pages are now grouped by address.
* Fixed: comparisons for a range that ends today weighed part of a day against a whole one, so every page looked like it was losing traffic in the morning. The previous period is now scaled to the share of today that has passed.

= 0.8.2 =
* Fixed: two long searches that began with the same 190 characters were stored as one, and the second overwrote the first's clicks and impressions. Searches are now told apart by their full text. Updating refetches the last 90 days of Search Console data once so the split applies to recent history.
* Changed: the Search Console button now reads "Fetch the last 90 days". It fetches the last week straight away and the rest in the background, which "now" no longer described.
* Changed: Japanese translations follow the Japanese WordPress style guide for spacing around English words, numbers, colons and brackets.

= 0.8.1 =
* Fixed: the "Connect Search Console" link on a post's stats screen opened Settings at the top of the page instead of at the Search Console section.

= 0.8.0 =
* Added: each post's screen now shows the searches it appeared for in Google, with the clicks and average position for each. Google has passed almost no organic search terms to websites since 2013, so no analytics plugin can read them from the visit itself. Search Console still has them, and this joins them to the post they reached.
* Added: the site-wide search list on the Overview names the page Google showed for each search.
* Fixed: Search Console history was being cut off. A pull stopped at 25,000 rows for the whole date range, and a 90-day pull on one modest site hit that exactly, so roughly nine rows in ten were dropped without any message. The top searches survived, because Google sorts by clicks, but the long tail and anything totalled from it did not. Pulls now read everything Google returns.
* Changed: refreshing Search Console fetches the last week immediately and the rest of the history in the background, a fortnight at a time. Reading the full history in one request took over a minute once nothing was being dropped, which is longer than an admin page is allowed to run on many hosts.
* Fixed: Search Console data was never removed by the data retention setting, so it was kept indefinitely whatever retention you chose.

= 0.7.9 =
* Fixed: the bundled translations never loaded. Nine locales have shipped inside the zip since 0.7.5 and none of them were ever read, so the plugin stayed in English whatever language the site ran in. The Text Domain and Domain Path lines in a plugin header are metadata; they do not make WordPress read a catalogue out of the plugin's own folder, and the call that does was missing. German, Spanish, French, Italian, Japanese, Dutch, Portuguese, Brazilian Portuguese and Russian now work.
* Changed: the Overview now opens with what actually moved. The sentence explaining the change was being generated already, but it sat inside a card roughly two thirds of the way down the page while a shorter version of the same thought ran as one grey line near the top. The fuller one leads, and the card no longer repeats it.
* Changed: the reorderable sections, including What changed and the traffic chart, now sit directly under the headline figures. AI assistants, Search Console, notes and page speed moved below them. Notes in particular read as a footnote to the chart, which now renders above them rather than below.
* Changed: gained and lost rows carry a bar, scaled across both columns rather than within each one, so a month where the losses are twice the gains looks like one.
* Changed: the headline figures use the plugin's own green. It was already on the chart but not on the numbers, which left the brand sitting on the supporting element.
* Fixed: other plugins' admin notices no longer print on Vireo's screens. Several analytics and licensing plugins register banners with no screen check, so an unrelated plugin's marketing could sit above your traffic. WordPress's own notices, including the update prompt, still show, and so do Vireo's own warnings.

= 0.7.8 =
* Fixed: five importers that shipped without a way to reach them. Statify, WP Statistics, Burst Statistics, Independent Analytics and SlimStat have had working importers since 0.7.0 and the readme has listed them since, but no screen was ever registered for any of them, so the only sources you could actually import from were Koko Analytics, Jetpack Stats and Google Analytics. All five now have an import screen, and the list under Settings is built from the importers that exist rather than a separate hand-written list, so the two cannot drift apart again.
* Changed: the first-run banner now looks for every source it can import from instead of only Koko Analytics and Jetpack Stats, and names the ones it found. A site running any of the other five previously got no prompt at all.
* Fixed: the Overview said the geo-location database was missing on sites whose host or CDN already supplies the visitor's country, while Settings correctly said countries were resolving. Both now check the same thing, and the admin notice and the "What changed" hint follow suit. The "Set it up" link now goes straight to the Geo-location section instead of the top of Settings.
* Fixed: clicking a section's move handle on the Overview, without dragging, left that section greyed out and pinned to the screen until the page was reloaded. A click now does nothing, and a drag always lets go of the section when you release it, even if the pointer leaves the window.
* Changed: the test email now says who it went to. When sending fails it shows the reason the mail system gave, and when it succeeds it explains that WordPress only hands the message to the server, so a missing email points at the host's mail delivery, not at the digest.
* Changed: the page speed card names what it measures (load time, response to clicks, layout shift), grades each figure in words as well as colour, and explains the 75th percentile in plain terms. With under 30 measured visits it says the figures will move.
* Changed: "Countries to treat as junk" no longer shows example country codes, which read as a recommendation.
* Changed: section headings on the Settings screen are larger, so the page scans as sections rather than one long form.
* Changed: the Overview and Settings screens carry the Vireo mark and use the brand green for charts, the map, buttons and selected controls, in place of the default admin blue.

= 0.7.7 =
* Added: a way to clear what an excluded country already recorded. Excluding a country has only ever stopped it being counted from that point on, which left the earlier data in your reports with no way to remove it. Settings, then Geo-location, now offers to clear the stored days for the countries you exclude. It clears the countries report and the lookup cache, and leaves your pageview and visitor totals alone, because nothing else records which country a visit came from and guessing after the fact would be worse than leaving it.

= 0.7.6 =
* Fixed: excluded countries were only honoured on the daily rollups. Everything else that asks whether a request should be counted (including the Pro live visitor feed) never checked the list, so traffic you had chosen to discard kept appearing live while the same traffic was correctly absent from your reports. The check now lives in the shared rule, so anything that consults it gets the setting for free. Reported by a user who noticed the two views disagreeing.
* Fixed: importing a history from another plugin wrote straight into the country report without checking your exclusions, putting back the countries you had already discarded. Imports now skip them.
* Note: neither fix removes rows already stored. Excluding a country stops it being counted from that point on; it does not erase what was counted before.

= 0.7.5 =
* Every bundled translation is complete again. The catalogues were last built in July and covered about two thirds of the plugin, so anything added since (the Google Analytics import, Search Console, the bot filter, AI assistant traffic, Site Health, annotations, Web Vitals) appeared in English no matter what language the site was in. German, French, Italian, Japanese, Dutch, Portuguese in both variants, Russian and Spanish now cover all of it.
* Fixed a batch of Spanish strings that stated the opposite of the English. The Jetpack importer told you to copy your API key into Jetpack rather than out of it, the retention note read as though whole tables were deleted, and the 404 table headed its request count with a word meaning successful accesses.

= 0.7.4 =
* Fixed: crawlers were only counted when they ran the tracking script, and almost none of them do. The filter saw the handful of agents that render JavaScript and missed every one that just fetches HTML, which skewed the categories against each other rather than only lowering them: one site reported AI crawlers at 22 times its search crawlers, where the server log for the same days had them at roughly 1.3 times. Crawlers are now counted when they ask WordPress for a page. Expect these numbers to step up sharply after you update, and expect the balance between categories to shift.
* Fixed: the bot panel described its percentage as "everything that asked for a page". It is what Vireo saw. A site with full-page caching answers many crawler requests before WordPress runs, and nothing inside WordPress can count those, so the panel now says that instead.

= 0.7.3 =
* Fixed: an import that failed said it had succeeded. The browser showed "Import complete" in green, the range was filed in the already-imported list, and the message explaining what to correct was never displayed. A failed import now stops with that message on screen and records nothing.
* Fixed: when Google refused a request because the Analytics Data API was not enabled on the project, the error said to check the service account's access on the property instead. Google's own message names the API and links the page that turns it on, and it is now shown as sent.

= 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_proxies` filter. 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.

Older versions are in [changelog.txt](https://plugins.trac.wordpress.org/browser/vireo-analytics/trunk/changelog.txt), which also ships with the plugin.

== Upgrade Notice ==

= 0.8.7 =
Fixes sites on WP Engine recording no visitors. Update if your site is hosted there.

= 0.2.2 =
Performance fix from the plugin review: the tracker's rate limiter no longer writes to the options table on sites without a persistent object cache.

= 0.2.1 =
Privacy and hardening fixes from the plugin review: no third-party requests, no generated PHP files, nonce-checked admin filters.

= 0.2.0 =
Initial public release.
