WP Manifestindependent plugin directory
manifest / analytics / wp-visits-stats

WP Visits Stats

Privacy-friendly, self-hosted analytics: which posts get the most visits, how long readers stay, and where readers exit quickly. No cookies, no PII, data never leaves your site.

by Aristath · github.com/aristath/wp-visits-stats · website

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/aristath/wp-visits-stats/archive/refs/heads/main.zip

Privacy-friendly, self-hosted post-level analytics for WordPress.

Three questions, answered per post:

  • How much — which posts get the most (and least) visits.
  • How deep — how long readers stay, as a per-post dwell-time distribution.
  • Where it's lost — quick-exit rates, and the posts nobody reads.

No cookies. No user-specific data. No PII. Two tiny same-origin requests per page view. All data lives in your own database and never leaves your site.

What it is and is not

This is not a tracking suite. There are no visitor profiles, no geolocation, no referrer or campaign tracking, and no "who is online right now". It answers the three questions above with aggregate numbers only, and it says so plainly: the per-install secret shipped in the page source is a junk filter, not authentication — it stops forged noise, it is not a security boundary.

How it works

A hand-written ~2.5 KB frontend script (assets/beacon.js, no build step) sends two beacons per page view, both via sendBeacon (with a fetch keep-alive fallback):

  1. View — on page load.
  2. Dwell — when the reader leaves (pagehide), reporting how long the tab was visible, mapped to one of eight fixed buckets.

Dwell is stored as a bucket, not an exact second count:

Bucket Time on page
b0 under 5 s
b1 5–15 s
b2 15–30 s
b3 30–60 s
b4 1–2 min
b5 2–5 min
b6 5–15 min
b7 15 min or more

Each beacon hits beacon.php, which validates the payload, checks the per-install secret (current or previous), and writes a single aggregated row. Nothing is stored that identifies or could be connected to a person.

Storage

Two custom tables per site, prefixed by the site's table prefix:

  • …_wvs_hourly — (hour, post_id, views, b0 … b7)
  • …_wvs_daily — (day, post_id, views, b0 … b7)

There is no raw event log. Complete hourly days are rolled up into the daily table by the nightly cron, and data older than the retention window is pruned.

The read path merges the two tables on the "last rolled day" cursor, so a late cron run can only lag the numbers — never drop them.

Requirements

  • WordPress 6.0+
  • PHP 8.0+
  • A MySQL or MariaDB database, or the SQLite integration drop-in. The data layer uses only $wpdb, so no second code path is needed.

Installation

  1. Upload the wp-visits-stats folder to /wp-content/plugins/.
  2. Activate the plugin (network-activation works on multisite).
  3. Open Visits Stats in the admin sidebar.

Settings

  • Pause collection — temporarily stop recording; existing data is kept.
  • Exclude logged-in users — don't count visits from logged-in accounts (e.g. your own previewing).
  • Quick-exit threshold — the bucket under which a stay counts as a quick exit (under 5 s / 15 s / 30 s).
  • Data retention — keep data for 90 / 365 / 730 days, or forever.
  • Delete all data on uninstall — when the plugin is deleted, drop the statistics tables and settings (otherwise the data is left in place).

Dashboard

Period tabs (7 / 30 / 90 days, all-time, custom range), a KPI strip (views, quick-exit %, read-past-1-minute %, dwell events), a site dwell-time distribution bar, a top-100 posts table with per-post distribution bars, a "Nobody read these" list of zero-view published posts (the 100 most recent), a "Fastest exits" list, and CSV export. The view polls every 30 seconds and pauses when the tab is hidden.

Privacy model

  • A "visit" is a page load. Without cookies we cannot distinguish two loads, so refreshing counts again. Counts are honest but not deduplicated to unique humans — that is the deliberate privacy trade-off.
  • No PII. No IPs, no user agents, no referrers, no timestamps tied to a person. The hour of an event is derived server-side (the client never sends a timestamp).
  • Self-contained. Beacons go only to your own beacon.php; nothing is sent to any third party.
  • Accepted, documented limitations. A reader whose tab crashes loses their final dwell beacon; ad-blockers may suppress the beacons (an undercount); a page cached longer than the secret-rotation window may briefly 404 on the beacon and self-heal on the next rotation; the "Nobody read these" list only scans the most recent published posts (bounded, so it stays fast on large sites) and surfaces the 100 most recent of those.

Development

npm install
npm run build     # builds dist/admin/ (committed to the repo)
npm run i18n      # regenerates languages/wp-visits-stats.pot

The admin bundle externalizes React, ReactDOM, and all @wordpress/* packages against the globals wp-admin already ships — the plugin never bundles a second React. The frontend beacon script is hand-written vanilla JS with no build step.

The plugin is validated by a standalone PHP test suite that runs the data, beacon, REST, and lifecycle layers against both a MySQL scratch database and the real SQLite drop-in:

php tests/harness-mysql.php      # data layer, MySQL
php tests/harness-sqlite.php     # data layer, SQLite drop-in
php tests/harness-beacon.php     # write path
php tests/harness-rest.php       # read path
php tests/harness-lifecycle.php  # activation / cron / uninstall

File layout

wp-visits-stats.php          Main plugin file (hooks, admin, asset enqueues)
beacon.php                   Thin beacon endpoint
uninstall.php                Uninstall handler
includes/
  class-wvs-data.php         All SQL ($wpdb only): DDL, upsert, reads, rollup, prune
  class-wvs-settings.php     One options array + secret rotation
  class-wvs-beacon.php       Beacon validation/record decision path
  class-wvs-rest.php         Dashboard / export / settings REST routes
  class-wvs-lifecycle.php    Activation, upgrades, cron, uninstall, multisite
assets/beacon.js             Hand-written frontend beacon (no build)
src/admin/                   React dashboard + settings source
dist/admin/                  Built admin bundle (committed)
tests/                       Standalone validation harnesses
tools/generate-pot.js        .pot generator

See CHANGELOG.md for release notes and readme.txt for the wp.org listing.