Activity Kit Stats: unique downloaders, download rate fix, last downloaded/viewed columns - #3654
Activity Kit Stats: unique downloaders, download rate fix, last downloaded/viewed columns#3654Piyopiyo-Kitsune wants to merge 2 commits into
Conversation
…oaded/viewed columns Jetpack Stats has no per-post unique-visitor data, and a prior PR (#3496) had a "record on every page view" endpoint rejected by WordPress.org infra for caching/scale reasons — so this doesn't add any new page-view tracking. Instead: - Adds unique-downloader tracking via a new custom table (wp_activity_kit_downloads), extending the already-shipped, low-frequency download-tracking endpoint (#3592) rather than repeating the rejected per-page-view pattern. Visitors are identified by a one-way hash salted with an HMAC-of-the-day secret, so the same visitor's hash can't be correlated across two different days and no raw IP is ever stored. - Redefines "Download Rate" as unique downloaders ÷ views × 100, fixing the original problem where repeat downloads by the same person could push the raw-download-based rate over 100%. - Adds "Last Downloaded" (postmeta timestamp) and "Last Viewed" (approximated from Jetpack's per-post view history, fetched only on dashboard load) columns. - Renames "Last Updated" to "Kit Last Updated" for clarity, and adds hover tooltips to all metric column headers explaining what each surfaces — including an explicit note that "Unique Daily Downloaders" dedupes per day, not per selected range. Schema creation is hooked to `init` rather than `admin_init` so the table exists before the first public, unauthenticated download request needs it. Verified locally in wp-env: table auto-creates on a fresh anonymous request, the real download REST endpoint records rows and updates the last-downloaded timestamp, unique-downloader counts dedupe correctly within a day, and the dashboard renders all 8 columns/tooltips/CSV export correctly. Pre-merge follow-ups (called out for reviewers, not blockers to this PR): verify the real Jetpack `weeks` response shape against a live connection, get a privacy-policy sign-off for the new visitor-hash tracking, and confirm with WordPress.org meta/infra whether production table creation needs manual provisioning (per the wporg_user_sessions precedent). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the Activity Kit Stats dashboard by introducing unique-downloader counting (to make “Download Rate” meaningful), adding “Last Downloaded” / “Last Viewed” dates, and clarifying column naming/tooltips for better interpretation of metrics.
Changes:
- Add a new custom table-backed unique-downloader tracking mechanism and integrate it into the existing download REST endpoint.
- Redefine “Download Rate” to use unique downloaders rather than raw download totals, and surface new last-downloaded/last-viewed columns.
- Update the Stats dashboard UI (table, sorting, CSV export, tooltips) to show the new metrics and renamed column labels.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| wp-content/plugins/wporg-learn/wporg-learn.php | Loads the new unique-downloads DB module. |
| wp-content/plugins/wporg-learn/js/activity-kit-stats/index.js | Updates stats UI rendering/sorting and CSV export for the new metrics/columns. |
| wp-content/plugins/wporg-learn/inc/activity-kit-stats-page.php | Adds new table headers, tooltips, and column label updates. |
| wp-content/plugins/wporg-learn/inc/activity-kit-rest.php | Records unique downloads + last-downloaded timestamp; adds last-viewed fetching and new fields to stats payload. |
| wp-content/plugins/wporg-learn/inc/activity-kit-downloads-db.php | Introduces the custom table schema management and unique-downloader counting queries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…arse - maybe_upgrade_schema() now only records DB_VERSION as current after confirming the table actually exists (SHOW TABLES), since dbDelta() doesn't reliably signal failure. Previously a failed CREATE would still mark the upgrade done, permanently skipping retries on that environment. - get_site_secret() now re-reads the persisted option when add_option() loses a concurrent race, instead of trusting the freshly generated (but unpersisted) value — avoids two requests hashing against different secrets on the first day the secret is created. - get_last_downloaded_date() now parses the stored GMT timestamp with an explicit +0000 offset, so strtotime() can't shift the resulting day near midnight on a server whose default timezone isn't UTC. Verified all three in wp-env via direct eval: simulated an add_option() race, confirmed the schema guard skips marking version-done when the table is absent and still succeeds on the real happy path, and confirmed the date-parse fix produces the correct day regardless of server timezone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for this, Destiny. I took it a step further in #3662, which keeps your columns, tooltips and the download-rate change but drops the custom table: uniques go into a second daily postmeta bucket with a transient dedupe, so nothing needs provisioning on wordpress.org. Three things were fixed on the way: the |
|
Thank you for the thorough review and revision @maciejpilarski . Happy to close this PR in favor of your new one #3662 . |
|
Thanks, Destiny. Your columns, tooltips and the download-rate change are all in #3662 as they were, and you are co-author on the commit. Lint is green there and it is waiting on @obenland. One ask, if you have time before you go: you verified the original in wp-env with a Jetpack connection, which I do not have here. The PR body has a seven-step checklist (double download, the five-a-day cap, a real Last Viewed date, Jetpack off and on, quick range switching, CSV in Firefox and Safari, the tooltips). If you can run through it and note the result on #3662, that would settle the one thing I could not test. |
What
The Activity Kit Stats dashboard (Activity Kits → Stats) currently shows a "Download Rate" computed as raw total downloads ÷ views. Since a visitor can download the same kit's ZIP more than once, total downloads can exceed views, producing a rate over 100% — not a meaningful "did visitors convert" signal. The dashboard also had no history of when a kit was last downloaded or last viewed, and the "Last Updated" column name was ambiguous.
This PR:
wp_activity_kit_downloads), extending the already-shipped, low-frequency download-tracking endpoint (Activity kits: server-side download tracking #3592) rather than adding new per-page-view tracking.unique downloaders ÷ views × 100, fixing the >100% issue.Why not track unique page visitors too?
Jetpack Stats has no per-post unique-visitor data anywhere in its public API — only site-wide aggregates. A prior PR on this repo (#3496) had a similar "record on every page view" tracking endpoint explicitly rejected by WordPress.org infra (dd32) for caching/scale reasons (writing to the DB on every page view defeats edge caching), not privacy. This PR deliberately doesn't repeat that pattern — no new page-view tracking is added. Downloads are a different story: they're a comparatively rare, already-server-side-tracked event, so extending that with one more low-frequency write (a dedup record per kit/visitor/day) is consistent with the precedent that already shipped, not the one that was rejected.
Privacy design
Visitors are identified by a one-way hash:
hash(daily_salt . ip . user_agent . kit_id), where the daily salt ishash_hmac('sha256', $day, $site_secret). No raw IP is ever stored — only the hash. Because the salt is derived per-day from a secret that never leaves the server, the same visitor produces an unrelated hash on a different day, so no persistent per-visitor identifier is ever stored or derivable. This is the same pattern privacy-focused analytics tools (Plausible, Fathom) use to avoid needing cookie consent.One consequence of this design, called out explicitly in the "Unique Daily Downloaders" column's tooltip: dedup resets daily, so the same person downloading on two different days within a multi-day range is counted twice, not once. This was verified directly in local testing.
Verified locally in wp-env
inithook on a fresh, anonymous, unauthenticated request (not gated behind an admin ever loading wp-admin first)./wp-json/activity-kits/v1/download/{id}REST endpoint records a unique-downloader row and updates_activity_last_downloaded.WordPressphpcs standard: clean on all touched files (one pre-existing false-positive$wpdb->prepare()placeholder-count warning already present identically in trunk'sget_download_counts(), not introduced here). JS lint clean. No stacked//comments./code-reviewpass; all 8 findings addressed (6 fixed, 2 explicitly left unchanged with reasoning documented in code comments — seeactivity-kit-downloads-db.phpandactivity-kit-rest.php).Pre-merge follow-ups (not blockers, flagging for reviewers)
weeksresponse shape against a live Jetpack connection (staging or Jurassic Tube) —extract_last_viewed_date()was written defensively but the exact day-level shape wasn't confirmed against live data.wporg_user_sessionsprecedent (that table's schema is documented as a comment only, not created viadbDelta()in production).🤖 Generated with Claude Code