=== AnGo Scroll Analytics ===
Contributors: andrewgolubev
Tags: scroll depth, reading analytics, engagement, statistics, content
Requires at least: 6.8
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.5.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Scroll depth statistics for posts and pages — see how far visitors actually read, by day, month and article.

== Description ==

AnGo Scroll Analytics records how far visitors scroll through your posts and pages and shows the result on a "Readability" screen in wp-admin. It answers the question a pageview count cannot: did anyone actually read past the first screen?

The tracking script is loaded only on single posts and pages (is_singular for the post and page types) and measures how far the page was scrolled during a visit. That is depth through the scrollable height of the document, not the share of the article visible on screen: a page that fits on one screen records 100%.

A visit is registered as soon as the page loads — a visitor who scrolls nowhere still gets a row, with 0%. After that, data is sent to the server only when reading progress actually advances (checked every 5 seconds), and always when the visitor leaves the page or switches tabs (via navigator.sendBeacon, or fetch with keepalive where it is unavailable). A failed send is retried at the next check.

One visit is one row in the table, updated as the reader gets further down. A new tab or a new session is a new visit.

= What the dashboard shows =

* The period, chosen by hand (the last 30 days by default), with an "Apply" button and grouping by day or by month.
* A summary for the selected period: number of visits, average scroll %, median.
* Percentiles 50/75/90/100, phrased as "X% of visitors scrolled at least to Y% of the article".
* An "Exclude load-and-die visits" checkbox (on by default): visits whose only beacon fired at page load — on a live site mostly bots and link previews — are left out of the statistics, and the Visits card shows how many were excluded. Uncheck it to see the raw population.
* A trend chart: average scroll as a line, visits as bars.
* A table per item: title, type (post/page), visits, average scroll, median, 90th percentile, sorted by visit count.

== Installation ==

1. In wp-admin go to Plugins → Add New → Upload Plugin.
2. Choose the downloaded archive (ango-scroll-analytics-1.5.0.zip), click "Install Now", then "Activate".
3. On activation the plugin creates its table in the database, {prefix}_ango_visits (wp_ango_visits with the default prefix). If the schema changes in a later version, the table is brought up to date the next time wp-admin is opened (dbDelta).
4. A "Readability" entry appears in the left-hand menu — the dashboard lives there.

== Frequently Asked Questions ==

= Do visits with no scrolling really count? =

Yes. Every arrival is a row, including the ones with 0% scroll: the percentiles describe all arrivals, not only those who read to the end. That is a deliberate decision, not lost data.

The dashboard does distinguish two kinds of zero-scroll visits. A visit that sent nothing after the initial page-load beacon (max_scroll 0 and time on page 0) is "load-and-die" — on a live site those are usually bots, crawlers and link previews, not people. They are excluded from the statistics by default (the Visits card shows how many were left out), while a person who loaded the page and stayed without scrolling — time on page above zero — always counts. The checkbox in the filter bar turns the exclusion off.

= Is the tracking endpoint public? =

Yes — it has to be, because visitors are not logged in. POST /wp-json/ango/v1/depth is limited to 60 requests per minute per IP; the limit is changed through the ango_rate_limit filter, and a value below 1 disables the check. The IP itself is not stored, only a hash of it, for the duration of the window. Behind a proxy (Cloudflare and similar) REMOTE_ADDR is the proxy address, so the limit is shared by all visitors — raise it in that setup.

= Why do the numbers differ from my analytics tool? =

This plugin measures depth through the scrollable height of the page, not the share of the article that was on screen: a short page that fits on one screen records 100%. And it counts visits, not unique readers — a new tab or a new session is a new visit.

= What timezone are the numbers in? =

Timestamps in the table (created_at) are stored in UTC, while the dashboard computes day boundaries in the site timezone. Keep that in mind when writing manual SQL against {prefix}_ango_visits.

= Can I track custom post types? =

The tracking script is loaded only on single posts and pages (is_singular( [ 'post', 'page' ] )). To track custom post types as well, add them to that list in ango-scroll-analytics.php, in ango_enqueue_tracker().

= Does the admin screen load anything from outside my site? =

No. The chart is drawn by Chart.js 4.5.1, bundled with the plugin (assets/vendor/chart.umd.min.js, MIT license), so wp-admin makes no external request and the chart works offline and behind a filtering proxy.

= Why does the bundled Chart.js ship minified? =

Chart.js 4.5.1 (MIT) ships here as the minified UMD build that upstream publishes — assets/vendor/chart.umd.min.js, licence banner intact. Upstream has no unminified UMD to use instead: dist/chart.umd.js and dist/chart.umd.min.js in its package are both minified. The readable code is published, just in another build: the unminified ESM bundle is dist/chart.js (https://unpkg.com/chart.js@4.5.1/dist/chart.js), and the sources it is built from are at https://github.com/chartjs/Chart.js/tree/v4.5.1/src.

= Why are the file names so plain? =

They are deliberately neutral: the tracking script is assets/js/scroll-depth.js and the public endpoint is /wp-json/ango/v1/depth, with no tracker/track in either. Those words are a common target for filter lists, and a blocked script or beacon loses data silently. Neither EasyList/EasyPrivacy nor the uBlock Origin and AdGuard filters matched these URLs (checked 2026-09-16 under the plugin's previous name; the endpoint paths did not change in the rename), but the lists update daily — so do not rename them back "for clarity".

= How do I update the Russian translation? =

English is the source language; Russian is a translation (languages/ango-scroll-analytics-ru_RU.po). For the WordPress.org-hosted plugin the catalogue is served by translate.wordpress.org and no translation files ship in the package — the .po lives in the development repository for contributors. A site that installs the plugin from the WordPress.org directory receives community translations automatically; WordPress reads the compiled .mo, so after editing the .po you must rebuild it:

msgfmt -o languages/ango-scroll-analytics-ru_RU.mo languages/ango-scroll-analytics-ru_RU.po

If gettext is not installed, a minimal Python MO writer does the same job. The test suite checks that the .po and the .mo stay in agreement: python3 tests/run.py

= What happens to my data when I uninstall the plugin? =

Deleting the plugin removes everything it stored: the {prefix}_ango_visits table, the ango_db_version option and the throttle transients. Deactivating does not — the data survives a deactivate and reactivate cycle, so an upgrade or a temporary switch-off costs nothing. Clicking Delete on the Plugins screen shows a confirmation naming the statistics that will be lost. If you need the numbers after the plugin is gone, export them before deleting, or hold the cleanup back from a must-use plugin with add_filter( 'ango_remove_data_on_uninstall', '__return_false' ).

== Changelog ==

= 1.5.0 =
* New: logged-in visitors are not tracked — the site owner's own views no longer pollute the percentiles. The tracking script is not loaded for logged-in users, and the endpoint refuses their beacons. Multi-author sites that want member visits counted can turn the exclusion off with the ango_track_logged_in filter.

= 1.4.0 =
* Fix: the tracker no longer records 100% depth when the page layout is still unsettled at load time (scrollable height not yet known) — such loads were miscounted as full-depth reads.

= 1.3.0 =
* New "Exclude load-and-die visits" filter (on by default): page loads whose only beacon fired at page load — mostly bots and link previews — are excluded from all dashboard statistics. The Visits card shows how many were excluded, and a checkbox in the filter bar turns it off.

= 1.2.1 =
* Fix: the author is shown as Andrew Golubev, the Details link points to the WordPress.org page, and "Details" is translated in Russian.

= 1.2.0 =
* Renamed to AnGo Scroll Analytics per the WordPress.org review; all code symbols, options and the REST namespace moved to the ango_ prefix.
* Translation files no longer ship in the package — translations come from translate.wordpress.org.

= 1.1.8 =
* Fix: cleared all Plugin Check errors (inline prepare() calls, %i identifier placeholder, sanitized REMOTE_ADDR).

= 1.1.7 =
* Readme discloses the bundled Chart.js 4.5.1 build (MIT).

= 1.1.6 =
* Uninstall cleanup: deleting the plugin removes its table, options and transients, with a warning on the Plugins screen naming what is lost.
* Added the WordPress.org readme metadata block and plugin header fields.

= 1.1.5 =
* Fix: rebuilt the Russian translation file with a parseable header (the previous build shipped a malformed one).

= 1.1.4 =
* Fix: removed a doubled percent sign in the percentile lines (50%% -> 50%).

= 1.1.3 =
* Plugin attributed to Adviko Inc.

= 1.1.2 =
* Neutral asset and endpoint names (scroll-depth.js, /wp-json/.../depth) so ad-blocker filter lists don't silently break tracking.

= 1.1.1 =
* Fix: the Apply button sits on the controls' line in the filter row.

= 1.1.0 =
* The admin interface is translatable (English source, Russian catalogue).

= 1.0.1 =
* Fix: Chart.js is bundled with the plugin instead of loaded from a CDN URL that returned 404.

= 1.0.0 =
* Initial release: scroll-depth tracking on posts and pages, the Readability dashboard (visits, averages, percentiles, trend chart, per-item table), UTC storage with site-timezone day grouping.

== Other Notes ==

= Performance at scale =

The plugin computes the statistics on the fly with database queries. At very large volumes (hundreds of thousands of visits) caching (transients) or cron-based aggregation is worth adding. A typical blog does not need it.

= Rate limiting and the database =

Without a persistent object cache, each beacon writes the throttle counter to the options table, so the limit costs two queries per beacon. That is cheap at the scale this plugin targets — it is a spam brake, not a firewall.
