Skip to content

V3: Insights (consent-based usage tracking) + commercial V3 Integration - #29

Merged
shazzad merged 16 commits into
mainfrom
feature/v3-insights
Sep 26, 2026
Merged

shazzad merged 16 commits into
mainfrom
feature/v3-insights

Conversation

@shazzad

@shazzad shazzad commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

What V3 is

A new Shazzad\PluginUpdater\V3 namespace (src/V3/) that adds Insights: usage tracking on our own repo server, replacing what Appsero does for us today. Design: docs/superpowers/specs/2026-09-26-v3-insights-design.md, plan: docs/superpowers/plans/2026-09-26-v3-insights-plan.md.

It has two entry points:

  • Free plugins (wordpress.org): V3\Insights. An Appsero-style consent notice (Allow / No thanks, nonce'd, manage_options). Nothing is sent until an admin clicks Allow. After that it sends a daily track plus activate / deactivate / upgrade / optin events to wp-repo/v4/products/{key}/track, and opt_out() asks the server to delete the site's data. It loads no update or license code. Free plugins ship only src/V3/Insights.php + src/V3/Insights/. The README has a tested build strip recipe.
  • Commercial plugins: V3\Integration. This is the V2 entry point copied over, with the same config, option keys, transients, cron hook wprepo_sync_license_data_{name}, license page, notices and update-row message. A plugin moving V2 → V3 keeps every saved license. Tracking goes through the Insights parts in commercial mode: there is no consent step and no notice, and the license key is included. They are exposed as $insights_consent, $insights_collector, $insights_client and $insights_scheduler. Client::ping() is gone. The hourly sync now only checks the license, and activate / deactivate / upgrade send exactly one Insights track.
    • api_url is the plugin-repo wp-repo/v4 base, which serves updates, licensing and Insights (one URL, no derived Insights URL). An api_url still on wp-repo/v3 draws a _doing_it_wrong() notice and leaves tracking off; updates and licensing keep working.

Isolation guarantees

  • V1 and V2 are byte-identical to main and to the 2.1.1 tag. Plugins that stay on them see no change:
    git diff main -- src/*.php src/V2          # empty (shell-expanded glob)
    git diff 2.1.1 -- src/*.php src/V2         # empty
  • tests/V3/InsightsIsolationTest.php checks that the free files never reference Updater, License*, Admin\*, Integration, Tracker or V1/V2.
  • tests/V3/CommercialIsolationTest.php checks that the commercial V3 files never reference Insights\Notice or the free entry point, never ping, and never V1/V2.
  • A separate-process runtime test confirms that building a licensed V3\Integration and sending a track never loads Insights\Notice or V3\Insights. I made this test fail on purpose once, to confirm it can fail.

Server dependency

This needs shazzad/plugin-repo 2.8.0 (#79: wp-repo/v4 = all of v3 + track/optout) deployed on w4dev.com before any plugin ships on V3 — before that, v4 has no update, license or Insights routes there.

How to test

composer install
composer test                                                        # 415 tests, PHP 8.3
docker run --rm -v "$PWD":/app -w /app php:7.4-cli vendor/bin/phpunit # same 415 on PHP 7.4
composer lint   # V3 carries only the same pre-existing sniff classes as the V2 files it copies; PHPCompatibility 7.4- clean
  • 204 of the 415 tests are in tests/V3/. The 9 "risky" warnings (no assertions: Mockery-only migration tests) already existed in V1/V2; the V3 copies inherit the same 3.
  • End-to-end runs against the local stack (free + commercial test plugins) are covered by the overnight run's Task D. The manual QA sheet will be at docs/testing/v3-insights-manual-qa.md.

Not released

This is a draft. There is no version bump beyond the CHANGELOG 3.0.0 - Unreleased entry, no tag and no release. composer.json states no version.

🤖 Generated with Claude Code

Revised 2026-09-27: moved from the side namespace wp-repo-insights/v1 to wp-repo/v4; insights_api_url removed.

shazzad and others added 12 commits September 26, 2026 22:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V3\Insights wires Consent, Collector, Client, Scheduler and the opt-in
Notice for free plugins. Nothing is sent before an admin allows it:
Client::track() and Client::optout() refuse without consent, so every
call path shares one gate. The parts take plain constructor args so the
commercial V3\Integration can build them with consent pre-granted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers config validation, payload shape and the 200-plugin cap, meta and
license resolution, the consent state machine, notice gating, the Allow /
No thanks handler, the no-HTTP-before-consent rule on every path, opt-out,
and a static guard that the free files never reach update or license code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copies the V2 commercial entry point (Integration, Client, Updater,
Tracker, License/Store, Admin/*) into the V3 namespace with identical
storage keys, transients, cron hook and admin behaviour, so a plugin
moving V2 -> V3 keeps every saved license.

Differences from V2:
- Integration builds the Insights parts in commercial mode (consent
  implied, never asked, no Notice) and exposes them as
  insights_consent / insights_collector / insights_client /
  insights_scheduler. New optional insights_api_url config key; by
  default derived from api_url (/wp-repo/v3 -> /wp-repo-insights/v1).
  An underivable api_url fires _doing_it_wrong and leaves Insights off.
- The license key rides in the Insights payload when licensing is on.
- Client::ping() is gone; the hourly license sync only checks the
  license, activation/deactivation/upgrade only refresh caches and leave
  tracking to the Insights scheduler (no double send).
- admin_email/admin_name leave Integration (the collector reads them);
  meta/meta_callback and setMeta()/setMetaCallback() feed the collector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries the V2 Integration/Client/Updater/Tracker/Notices/UpdateMessage
tests over to V3 (ping cases dropped) on a new IntegrationTestCase, and
adds IntegrationInsightsTest (URL derivation, underivable api_url,
commercial consent, license in payload, hourly sync never pings, exactly
one activate/deactivate track, meta wiring, and a separate-process check
that the commercial path never loads Insights\Notice) plus
CommercialIsolationTest, a static guard over the commercial V3 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: Which namespace gains V3 (free V3\Insights + commercial
V3\Integration), quick starts for both, V3 config tables, the Insights
endpoints, and a "Shipping a free wp.org plugin" section with a tested
strip + dump-autoload recipe. CLAUDE.md: V3 is active, V2 stable
(additive fixes only), V3 architecture and isolation tests. CHANGELOG:
3.0.0 (unreleased).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A commercial plugin updated in place V2 -> V3 (WP-CLI, auto-update, MainWP)
on a site where nobody opens wp-admin never got the daily Insights cron:
only activation, admin_init and opt_in schedule it. No track was ever sent
and the server marked the install inactive after 7 days.

Tracker::sync_license_data() runs hourly from a cron Updater schedules on
every init, so it now self-heals the daily event and calls run_daily(),
whose 20h MIN_INTERVAL guard prevents double sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "What we collect" list under-disclosed what Collector::collect()
sends. It now covers every payload group: site name/URL/language plus the
multisite and local-site flags; WordPress version, memory limit and debug
mode; the active theme (name, version, parent); server versions and PHP
limits; user counts by role; active plugins' names and versions.

The admin line said "Your name and email address", but the payload carries
the site's admin_email option and the first administrator's display name,
not the clicking user: now "Your site's admin email address and
administrator name".

"Usage statistics specific to {name}" is added when meta or meta_callback
is configured, and a new notice.items config key appends extra lines
(non-empty strings; anything else is dropped with _doing_it_wrong()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Insights::opt_out() set consent to "no" and dropped the deletion request
even when Client::optout() failed (timeout, 5xx); the client then refused
to send it later because consent was no longer granted.

A failed optout is now stored in {slug}_insights_optout_pending (first
failure and latest attempt) and retried on admin_init (at most hourly, so
a down server cannot slow every admin page) and by the daily cron, which
is kept for that. While pending, Client::optout() may send even though
consent is "no" -- it only ever sends {site_url, token}; tracks stay
refused. The flag clears on success, on a missing token, on a new opt-in
(which supersedes the deletion) or 7 days after the first failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Insights::uninstall( $file ) deletes the {slug}_insights_consent, _token,
_last_send and _optout_pending options and clears the
wprepo_insights_track_{slug} cron -- on every site of a multisite network
(get_sites + switch_to_blog). It sends nothing. Plugin authors call it from
uninstall.php (WP_UNINSTALL_PLUGIN) or a register_uninstall_hook callback.

A network deactivation now clears the daily cron on every site (the
deactivate hook's $network_wide argument); only the current site sends the
deactivate track, so a large network does not stall on N requests.

Both live in the free-safe files (Insights.php, Insights/Scheduler.php).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: the V2 -> V3 migration note now lists what changed for callers --
Client::ping() and the public $admin_email / $admin_name are gone, and
assigning $integration->meta / ->meta_callback after construction does not
reach the payload (use setMeta() / setMetaCallback()). Also documents the
full disclosure list, notice.items, the _optout_pending option,
Insights::uninstall() with uninstall.php / register_uninstall_hook
examples, and the hourly backup of the daily track.

Spec: consent-notice section rewritten to the new list and wording; opt-out
retry, uninstall, network deactivation and the hourly backup recorded.
CHANGELOG: one line per review fix under 3.0.0. QA sheet: 443 tests, new
notice wording, uninstall and hourly-backup checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shazzad

shazzad commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Review fixes pushed (5 commits, 2815454..45451d2). Tests: 443 green on PHP 8.3 and 7.4 (was 415). V1/V2 diff against main is still empty.

  1. Commercial in-place updates never tracked. Tracker::sync_license_data() (hourly, scheduled on every init) now re-creates the daily Insights cron if it is missing and calls run_daily(). The 20h MIN_INTERVAL guard stops double sends. 2815454
  2. The notice under-disclosed. The list now covers every group in the payload: theme; plugin names and versions; users by role; WP memory limit and debug mode; multisite and local-site flags. "Usage statistics specific to {name}" is added automatically when meta/meta_callback is set. There is a new notice.items config key, validated like the other notice keys. A test ties the list to Collector::collect(). 950f0b8
  3. Wording. Now reads "Your site's admin email address and administrator name". The E2E assertion is updated in plugin-updater-test#1 (db31e10).
  4. Failed opt-out is no longer lost. It is stored in {slug}_insights_optout_pending and retried on admin_init (at most once an hour) and by the daily cron, which is kept for that. While pending, only {site_url, token} may be sent. The flag clears on success, on a missing token, on a new opt-in, or 7 days after the first failure. b23c447
  5. Insights::uninstall( $file ) removes all 4 options and the cron on every site of a network. A network deactivation now clears the cron on every site; only the current site sends deactivate. 2b7a76d
  6. Docs. The README migration note now covers the dropped $admin_email/$admin_name, the removed ping(), and the need to use setMeta()/setMetaCallback() after construction. The spec, CHANGELOG and QA sheet are updated. 45451d2

E2E: 22 passed, 2 failed, both on the server side. The shared local shazzad-plugin-repo checkout is on fix/lookup-hardening, which has another session's uncommitted work and no wp-repo-insights code. I left it alone. Re-run bin/insights-e2e once that checkout is back on feature/insights-api; expect 48 passed.

🤖 Generated with Claude Code

shazzad and others added 4 commits September 26, 2026 22:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server now serves the whole API plus Insights under wp-repo/v4, so
V3\Integration drops insights_api_url and the /wp-repo/v3 ->
/wp-repo-insights/v1 derivation and passes its api_url straight to the
Insights client. V3\Insights takes the same v4 base. An api_url still on
wp-repo/v3 draws a _doing_it_wrong() notice in both entry points, since
v3 has no Insights routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3 has no Insights routes, so tracking there would 404 every day. The
notice now says tracking is off; updates and licensing are unaffected.
Also fixes CLAUDE.md, which still described insights_api_url.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shazzad
shazzad marked this pull request as ready for review September 26, 2026 20:53
@shazzad
shazzad merged commit b1f0137 into main Sep 26, 2026
1 check passed
@shazzad
shazzad deleted the feature/v3-insights branch September 26, 2026 20:53
@shazzad
shazzad restored the feature/v3-insights branch September 26, 2026 20:54
@shazzad shazzad mentioned this pull request Sep 26, 2026
1 of 2 tasks
shazzad added a commit that referenced this pull request Sep 26, 2026
Revert V3 Insights merge (#29) for now
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant