Skip to content

V4: Insights + v4 plugins API client (4.0.0) - #32

Merged
shazzad merged 12 commits into
mainfrom
feature/v4-client
Sep 27, 2026
Merged

shazzad merged 12 commits into
mainfrom
feature/v4-client

Conversation

@shazzad

@shazzad shazzad commented Sep 27, 2026

Copy link
Copy Markdown
Owner

What

Adds the Shazzad\PluginUpdater\V4 namespace: updates, licensing and usage tracking ("Insights") against plugin-repo's wp-repo/v4 API (plugin-repo 2.9.0+, contract in shazzad-plugin-repo/docs/v4-api.md). Release: 4.0.0.

This is the reverted V3 (#29 → #30), re-landed and renamed V3 → V4 so the updater namespace number matches the API version it calls (V4 ↔ wp-repo/v4; V2 ↔ wp-repo/v3 stays the one legacy exception). There will be no 3.x.

Changes since #29

  • Calls {api_url}/plugins/{prod_uid}/{updates,details,download,changelog,check_license,track,optout}; track sends plugin_version / plugin_status.
  • product_uid is required (format ^prod_[a-z0-9]+). Without it, or on a wp-repo/v3 / empty api_url, it logs _doing_it_wrong and makes no HTTP calls: no updates, no licensing, no Insights, no consent notice. Config keys are unchanged (product_uid, product_id); product_id only keeps the legacy V1 license-storage keys.
  • A refused track backs off. Any 4xx waits the 20h minimum instead of retrying hourly. A 403 wprepo_insights_tracking_disabled pauses tracking until the plugin version changes or 7 days pass (opt-out still always goes out).
  • Removing a license key sends license: "" once, so the server unbinds the install and frees the seat on its next recount. This only happens after an explicit removal, never when a key is merely missing.
  • Trailing slash on api_url trimmed; license key URL-encoded.
  • V1 (src/*.php) and V2 (src/V2) are byte-identical to main.

Verification

  • composer test (PHP 8.3) and PHP 7.4 docker: 497 tests, 1489 assertions (9 risky, pre-existing).
  • Insights e2e (shazzad-plugin-updater-test bin/insights-e2e, branch feature/v4-namespace) against local plugin-repo 2.9.0: 49/49.
  • Two independent review rounds; all findings fixed.

Before merging

Don't merge or tag 4.0.0 until plugin-repo 2.9.0 is released and deployed to w4dev.com. Live w4dev.com runs 2.6.0, where v4 has no plugins/ routes, so a V4 plugin's update and license checks would 404.

🤖 Generated with Claude Code

shazzad and others added 12 commits September 27, 2026 06:05
…ntract

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V3 never shipped, so nothing depends on the old name. The library release
becomes 4.0.0; @SInCE, @Version and _doing_it_wrong versions follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v4 server addresses a plugin as {api_url}/plugins/{uid}/… and accepts
only the prod_… uid; a numeric id is 404 rest_no_route. So:

- Client and Insights\Client build /plugins/{uid}/{route}.
- The track payload sends plugin_version / plugin_status (the server no
  longer reads the product_* names). Config keys and the product_*
  properties are unchanged.
- product_uid is required by V4\Integration and V4\Insights: without it a
  _doing_it_wrong() notice fires and no update, license or Insights call
  is made. get_api_product_key() / get_product_key() no longer fall back
  to product_id, which stays for the legacy id-keyed license storage.
- Integration skips the Insights parts without a uid; setProductUid()
  builds them later (and keeps the Insights URL in step with the uid).
- The free Insights entry point draws no consent notice without a uid,
  and a refused opt-out for a missing uid is not queued for retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README, CLAUDE.md, CHANGELOG: namespace V3 -> V4, release 3.0.0 -> 4.0.0
  (the Unreleased section is now 4.0.0; there is no 3.0.0), API URLs
  {api_url}/plugins/{uid}/..., track keys plugin_version / plugin_status,
  product_uid required, plugin-repo 2.9.0+. References to the frozen
  wp-repo/v3 API are unchanged.
- README + CLAUDE.md: the namespace number matches the API version it
  calls (V4 <-> wp-repo/v4); V1/V2 <-> wp-repo/v3 is the legacy exception.
- Rename the Insights spec, plan and manual QA sheet to v4 names and fix
  the links; the spec and plan get a dated revision note, the QA sheet
  points at the current branches and checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing can work against wp-repo/v3 (no plugins/{uid} routes) or with a
uid the v4 route pattern rejects, so both entry points now refuse up
front instead of sending requests that can only 404:

- Integration::get_api_error() (empty api_url, v3 api_url, missing or
  malformed uid) gates the update Client — check_license / updates /
  details return wprepo_no_api_url / wprepo_v3_api_url /
  wprepo_no_product_uid and never an invalid_license verdict — and the
  Insights parts. The v3 notice now says nothing works on v3.
- Insights\Client::get_config_error() is the same gate for the free
  entry point: no consent notice is drawn and nothing is sent.
  Insights::opt_in() returns that error without storing consent, token
  or cron. Config errors are not retried as failed opt-outs.
- product_uid must match the server's route pattern ^prod_[a-z0-9]+\z;
  anything else draws a _doing_it_wrong() (4.0.0) and counts as missing.
- api_url is right-trimmed of '/' once in the Integration constructor,
  so ".../v4/" no longer builds "v4//plugins/...".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
last_send was only written on success and the hourly license sync calls
run_daily(), so a track the server refused was re-POSTed (up to 64 KB)
every hour. Now:

- Any 4xx stores {slug}_insights_last_attempt; run_daily() waits
  MIN_INTERVAL (20 h) from the later of the last success and the last
  refused try. A success clears it.
- 403 wprepo_insights_tracking_disabled also stores the installed plugin
  version in {slug}_insights_disabled_version; until the installed
  version differs, nothing but `deactivate` is sent (opt-out is a
  separate route and is never blocked). The contract: stop sending until
  the plugin updates. Other 403s (a proxy/WAF) only get the 20 h wait.
- Network errors and 5xx store nothing and keep retrying as before.
- Insights::uninstall() removes both new options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v4 track contract unbinds the install (license_id 0, seat released)
when `license` is an empty string, and leaves the binding alone when the
key is absent. With licensing on and no code stored (never entered, or
deleted on the license page) the payload now carries "license": "" so
the seat frees up. Licensing off still sends no license key, and the free
entry point (no license callback) never does.

Integration::get_insights_license() returns null for "don't send";
Collector sends any string, including ''.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add_query_arg() does not encode values, so a key holding +, &, # or a
space reached the server altered (or truncated at the #). The update
Client now rawurlencode()s its query args. V1/V2 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README / CLAUDE.md: V4 needs a wp-repo/v4 api_url; on v3 no update,
  license or Insights call is made. uid shape (prod_ + [a-z0-9]),
  trailing-slash trim, the two new Insights options and the refusal
  backoff, license "" on licensed-but-empty installs, query encoding.
- README: setProductUid() after construction still leaves the
  "missing product_uid" notice in debug logs; pass the uid in config.
- CHANGELOG 4.0.0 (Unreleased): the new behaviour and fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23c3a97 sent `license: ""` whenever licensing was on and no code could be
read, which unbinds the install on the server. A key the site merely
cannot read — a V1-shipped plugin configured with a uid but no
product_id (the legacy-key migration misses the customer's key), or a
failed option read — would have unbound a paying install.

Now "" is sent only when a stored code was explicitly removed:
Store::delete_license_code() sets {slug}_insights_license_removed when a
code was stored and licensing is on; update_license_code() (used by the
license page) clears it, and so does the first 2xx track that carried ""
(the collector reports it through the new license_sent_callback). No
code and no flag: no `license` key, as with licensing off.
Insights::uninstall() removes the option.

Docs also say the seat is freed on the server's next install/activation
recount, not immediately.

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

- The 403 wprepo_insights_tracking_disabled pause is stored as
  [version, since] and lifts when the installed plugin version changes
  or after Scheduler::PAUSE_MAX (7 days), so an install resumes if
  tracking is switched back on server-side without a release. A repeat
  403 after expiry starts a new pause.
- `deactivate` is no longer exempt: the server's 403 check runs before it
  would record anything, so sending it only cost a request. Opt-out is a
  separate call (Client::optout()) and still always goes out.
- A paused send returns before any write: one option read per attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README mixed V1, V2 and V4 detail in one 650-line page. It is now an
overview: features, install, "Which namespace to use" (with a namespace ->
API -> guide table), a minimal V4 example, and what every version shares
(server responses, request parameters, metadata merge rules, update hooks,
license page, security, errors). Version detail moves to docs/v4.md,
docs/v2.md and docs/v1-legacy.md.

Corrections made while moving:
- V2 file tree said "active"; V2 is stable
- "V2 only" hooks and notices also apply to V4 Integration
- the product_uid-without-product_id notice fires only with license on,
  and V4 Integration fires it too (now documented in the V4 guide)

The guides now ship in the package (only docs/superpowers and docs/testing
stay export-ignored) so the README's links resolve from vendor/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shazzad
shazzad marked this pull request as ready for review September 27, 2026 01:37
@shazzad
shazzad merged commit 504b5dd into main Sep 27, 2026
1 check passed
@shazzad
shazzad deleted the feature/v4-client branch September 27, 2026 01:37
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