Skip to content

v1.3.0 - #24

Merged
tomasstark merged 32 commits into
mainfrom
beta
Aug 27, 2026
Merged

tomasstark merged 32 commits into
mainfrom
beta

Conversation

@tomasstark

Copy link
Copy Markdown
Contributor

This PR promotes the beta line to the stable 1.3.0 release.

Context

The 1.3.0 beta series has been running through fourteen prereleases and is ready to ship. Main is a strict ancestor of beta, so this promotion is a clean fast-forward with no divergent history. PR #23 (SDK 2.0.1 bump plus the legacy enforcement value mapping) should be merged into beta before this PR; once it lands, this PR picks it up automatically.

What 1.3.0 brings over 1.2.2

  • Bot classification analytics, opt-in via a new settings checkbox (off by default). Events are batched through a dedicated queue table with an Action Scheduler / WP-Cron dispatcher, and delivery targets the dedicated ingest service (ingest-connect.supertab.co, sbx variant for non-prod).
  • Status self-report endpoint at /.well-known/supertab/status for remote diagnostics.
  • Supertab Connect PHP SDK updated to the stable v2.0.1.
  • Legacy enforcement mode values from 1.2.x configs (soft/strict) are mapped to the renamed SDK values (observe/enforce), so existing wp-config.php settings keep their behavior after upgrading.
  • Hardened response header sanitization and various fixes from the beta series (deferred analytics flush scheduling, Authorization header fallback on Apache, settings page spacing and copy).

Versioning

No commit in the beta history carries a breaking-change marker, so semantic-release computes 1.3.0 on merge. Please use a merge commit rather than a squash for this promotion, so semantic-release sees the individual conventional commits on main.

tomasstark and others added 30 commits June 15, 2026 16:19
Pulls in the 1.4.0 beta of the Connect PHP SDK so testers can verify
the new SDK against the WordPress plugin. Shipped via the beta
prerelease channel only; not published to WordPress.org.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# [1.3.0-beta.1](v1.2.2...v1.3.0-beta.1) (2026-06-15)

### Features

* bump connect-sdk-php to 1.4.0-beta.1 for testing ([719cb58](719cb58))
## Summary

Replicates the last beta-release flow (a `feat:` commit on `beta` →
semantic-release auto-cuts the next prerelease) for enabling analytics.

- Passes `analyticsEnabled: true` to the `SupertabConnect` constructor
in `Plugin::init_bot_protection()`, so the plugin emits one analytics
event per bot request.
- The beta SDK (`getsupertab/connect-sdk-php` `1.4.0-beta.1`) is
**already pinned on this branch** from the previous beta, so no
dependency change is needed here — this is a one-line enablement.

## Release impact

`beta` is currently at `v1.3.0-beta.1`. Merging this `feat:` commit
triggers `release.yml` → semantic-release, which will cut
**`v1.3.0-beta.2`** automatically. (WordPress.org deploy only runs for
`main`, not `beta`.)

## Verification

- `composer lint` ✅ — no syntax errors
- `composer phpcs` ✅ — no violations (also enforced by pre-commit hook)
- `composer phpstan` ✅ — no errors
- `composer test` ✅ — 58/58 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# [1.3.0-beta.2](v1.3.0-beta.1...v1.3.0-beta.2) (2026-06-17)

### Features

* enable bot traffic analytics by default ([#11](#11)) ([2aaa352](2aaa352))
## Summary

Updates `getsupertab/connect-sdk-php` from `1.4.0-beta.1` to
`1.4.0-beta.3`. Pre-release only — targets the `beta` branch.

## Changes

- **`composer.json` / `composer.lock`** — bump the SDK pin to
`1.4.0-beta.3`.
- **`src/class-plugin.php`** — the SDK renamed the `EnforcementMode`
enum cases (`SOFT`/`STRICT` → `OBSERVE`/`ENFORCE`). Updated the default
in `Plugin::get_enforcement_mode()` from `EnforcementMode::SOFT` to
`EnforcementMode::OBSERVE` (the equivalent monitor-but-don't-block mode,
and the SDK's own constructor default).

## Compatibility note

The wire values backing the enum also changed (`soft`/`strict` →
`observe`/`enforce`). Any site overriding the mode via the
`SUPERTAB_CONNECT_ENFORCEMENT_MODE` constant must use the new values
(`observe`/`enforce`); old values now fall through to the default.
Acceptable for a pre-release.

## Verification

- `composer lint` — no syntax errors
- `composer phpcs` — no violations
- `composer phpstan` — no errors
- `composer test` — 58 tests, 65 assertions pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# [1.3.0-beta.3](v1.3.0-beta.2...v1.3.0-beta.3) (2026-06-30)

### Features

* updated PHP SDK ([390cb54](390cb54))
This PR updates PHP SDK to 1.4.0-beta.5 with async analytics transport
layer.
# [1.3.0-beta.4](v1.3.0-beta.3...v1.3.0-beta.4) (2026-06-30)

### Features

* update getsupertab/connect-sdk-php to 1.4.0-beta.5 ([#13](#13)) ([ca12083](ca12083))
## Problem

Analytics events emitted by the plugin reached the relay identifying as
`WordPress/<ver>; <site_url>` rather than the SDK.

`WP_Http_Client` (the plugin's `HttpClientInterface` adapter) never set
a `User-Agent`, so `wp_remote_get()` / `wp_remote_post()` fell back to
WordPress's default UA on **every** outbound SDK call — analytics
(`/ingest/events`), JWKS, billing events, and license-token requests.
The SDK's native curl client sets `supertab-connect-sdk-php/<version>`
via `CURLOPT_USERAGENT`, but the plugin injects this WP wrapper instead,
so that identity was silently dropped.

## Fix

Set `'user-agent' => HttpClient::resolveUserAgent()` on both `get()` and
`post()`. WordPress honors the `user-agent` arg (overriding its default)
and it passes through `vip_safe_wp_remote_get()` on VIP. Outbound
requests now identify as `supertab-connect-sdk-php/<version>`, matching
the native client.

Note: this is the *transport* User-Agent the relay observes. The
visitor/bot UA recorded in the analytics event payload (`userAgent`) is
unaffected.

## Tests

- New `tests/WPHttpClientTest.php` (written test-first — verified it
failed on the missing `user-agent` key before the fix).
- Added WP HTTP API stubs
(`wp_remote_post`/`wp_remote_get`/`is_wp_error`/`wp_remote_retrieve_*`)
that capture outbound args.
- `composer test`: 61/61 pass · `lint`, `phpcs`, `phpstan`: clean.
# [1.3.0-beta.5](v1.3.0-beta.4...v1.3.0-beta.5) (2026-06-30)

### Bug Fixes

* send SDK user-agent on plugin HTTP requests ([#14](#14)) ([9891e03](9891e03))
# [1.3.0-beta.6](v1.3.0-beta.5...v1.3.0-beta.6) (2026-07-01)

### Features

* update PHP SDK to v1.4.0-beta.6 ([b01f004](b01f004))
This PR adds an opt-in WordPress job queue for analytics delivery —
gated behind the `SUPERTAB_CONNECT_USE_WP_QUEUE` constant — so the
analytics POST runs off the visitor request when enabled, dispatched by
Action Scheduler or WP-Cron.

## Context

Bot protection emits one analytics event per classified request. The
plugin passes `analyticsEnabled: true`, so the SDK builds its default
`DeferredAnalyticsTransport`, which only takes the POST off the request
on FastCGI SAPIs. On mod_php/apache2handler (including `.wp-env`),
`fastcgi_finish_request()` is absent, so the POST to `/ingest/events`
runs **synchronously on the visitor request**, adding latency to every
classified page view.

## Key Changes

- **Opt-in via `SUPERTAB_CONNECT_USE_WP_QUEUE`**
(`src/class-plugin.php`): define it truthy in `wp-config.php` to route
analytics through the queue. When unset or falsy, the plugin injects a
`null` transport and the SDK falls back to its default — **behavior is
identical to before this PR**.
- **`src/class-analytics-dispatcher.php`** (new `Analytics_Dispatcher`):
`enqueue()` selects the best available backend — **Action Scheduler**
(`as_enqueue_async_action`, near-real-time async loopback) → **WP-Cron**
(`wp_schedule_single_event`) → **inline** best-effort last resort.
`dispatch()` (the registered job handler) rehydrates via
`AnalyticsEvent::fromArray()` and POSTs through the SDK's
`HttpAnalyticsTransport`. Fail-open throughout.
- **`src/class-plugin.php`**: when the queue is enabled, the job handler
is registered in **all** request contexts (admin-ajax for AS async,
`wp-cron.php` for the WP-Cron/AS queue runner, front-end) so the runner
can deliver wherever it executes; an injected
`CallbackAnalyticsTransport` replaces the default transport.
Bot-protection interception stays front-end-only and skips REST + cron.
- **`supertab-connect.php`**: deactivation hook clears pending work
args-agnostically (`wp_unschedule_hook()` + hook-only
`as_unschedule_all_actions()`).
- The WP-Cron fallback is best-effort — without Action Scheduler each
event serializes into the autoloaded `cron` option, so AS (bundled with
WooCommerce / available on VIP) is the intended production path and is
auto-selected when present. No delivery retries (deliver-once,
fail-open), matching current SDK semantics.

## Verification

- `composer test` — **67/67 unit tests passing** (100 assertions);
`lint`, `phpcs` (WordPress-VIP-Go), `phpstan` (level 5) clean.
- End-to-end in a running WordPress (with
`SUPERTAB_CONNECT_USE_WP_QUEUE` enabled: bot request → enqueue → `cron
event run` → dispatch) not yet exercised live.
# [1.3.0-beta.7](v1.3.0-beta.6...v1.3.0-beta.7) (2026-07-03)

### Features

* queue analytics dispatch via Action Scheduler / WP-Cron ([#15](#15)) ([5397d46](5397d46))
This PR exposes `GET /.well-known/supertab/status` so the Supertab
Connect API can probe a merchant site and learn what's running there —
plugin version, bundled PHP SDK version, effective enforcement mode, and
whether event reporting is active. It's the WordPress counterpart of
getsupertab/connect-sdk-typescript#40 and pairs with the backend's
per-component version resolver, which maps `component.kind:
"wordpress-plugin"` to the wordpress.org update registry.

## Context

The backend's live-health check probes sites with a backend-minted ES256
challenge JWT (`Authorization: Bearer`, purpose `status-probe`, `aud` =
the site origin, verifiable against the platform JWKS). A valid
challenge gets the live config; anything else gets a minimal `404
{"supertab": true}` decoy, so the endpoint discloses nothing to anyone
without a challenge. Without a `component` identity, the backend's
legacy shim would resolve a WordPress site's version against npm — wrong
registry, wrong label.

The PHP SDK's own `handleRequest()` also serves this path, but in this
plugin it only runs when bot protection is enabled and the path matches
active patterns — and it reports the SDK's identity, not the plugin's.
The plugin needs to own its component identity and answer probes
regardless of bot-protection state.

## Key Changes

- **`Status_Handler`** (new, mirrors `RSL_License_Handler`): hooks
`parse_request` at priority 5 — before `Bot_Protection`'s 9, so probes
never reach bot detection or analytics — exact-matches
`.well-known/supertab/status`, verifies the challenge via the SDK's
`StatusChallengeVerifier` (JWKS cached in WP transients,
refresh-and-retry on key rotation), and responds:
- invalid/missing challenge → `404` `{"supertab": true}` (fail-closed:
every verification failure, including JWKS fetch errors, resolves to the
decoy)
- valid challenge → `200` with `{runtime: null, sdkVersion, component:
{kind: "wordpress-plugin", version}, enforcement, eventReporting}`
- both with `Content-Type: application/json` + `Cache-Control: no-store`
- **Registered unconditionally** in `Plugin::init()` (like the RSL
handler), so the backend can learn the plugin version even when bot
protection is off; `enforcement`/`eventReporting` honestly report
`disabled`/`false` in that case.
- **SDK bump `1.4.0-beta.6` → `1.4.0-beta.8`** for
`StatusChallengeVerifier`/`JwksProvider`.
- **`Bot_Protection` now serves the SDK's `RespondResult`** (new in
beta.8) instead of leaking its headers onto the next rendered page —
relevant on plain-permalink sites, where the SDK's URL-based status
check fires instead of the plugin's handler.
- **Tests**: 14 new unit tests (decoy paths, fail-closed behavior incl.
throwing verifier, payload shape, hook priority, header filtering) plus
an integration test proving WP's rewrite pipeline delivers the
dot-prefixed path to `parse_request`. Verified end-to-end in wp-env:
unauthenticated and garbage-bearer probes return the exact decoy with
correct headers.

Reachability was verified live against a WP Engine production site
(theleek.demo.supertab.co): the path passes through Cloudflare and WP
Engine's proxy into WordPress's front controller.

## Notes

Both degradation modes fail closed and look like "plugin not installed"
to the backend (which degrades to "show version, no nudge"):

- **Plain permalinks**: `$wp->request` isn't populated without rewrite
rules, so the plugin handler can't match (same constraint as
`license.xml`); with bot protection on, the SDK's own status response
answers instead (without `component`).
# [1.3.0-beta.8](v1.3.0-beta.7...v1.3.0-beta.8) (2026-07-14)

### Features

* serve /.well-known/supertab/status self-report endpoint ([#18](#18)) ([d44a344](d44a344)), closes [getsupertab/connect-sdk-typescript#40](getsupertab/connect-sdk-typescript#40)
…Status_Handler (#19)

This PR fixes the self-report status endpoint silently serving the 404
decoy to the backend's challenge probes on hosts where Apache withholds
the `Authorization` header from `$_SERVER` — which is why the live
self-report check fails for sites on WP Engine despite running
1.3.0-beta.8.

## Context

The backend's health check probes `/.well-known/supertab/status` with a
challenge JWT in the `Authorization` header.
`Status_Handler::get_authorization_header()` read only
`$_SERVER['HTTP_AUTHORIZATION']` / `REDIRECT_HTTP_AUTHORIZATION` — but
Apache withholds `Authorization` from the CGI-style `$_SERVER` variables
(a CGI-era behavior), while `getallheaders()` still exposes it. Since
every verification failure fails closed to the decoy, the endpoint
looked healthy from outside while every valid probe was rejected.

Stock WordPress masks this: since WP 5.6 the core `.htaccess` rewrite
block includes `RewriteRule .* -
[E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]`, which re-exports the
header. Hosts that generate their own server config without that rule
(WP Engine among them) hit the bug. That's also why the original wp-env
verification couldn't catch it — wp-env's stock `.htaccess` carries the
compensating rule.

This is the same defect fixed in the SDK by
getsupertab/connect-sdk-php#24, found live on the vanilla-PHP
self-report demo; the plugin duplicated the pre-fix header reading
instead of going through the SDK.

## Key Changes

- **SDK bump `1.4.0-beta.9` → `1.4.0-beta.10`**, which ships
`RequestContext::resolveAuthorizationHeader()` (and fixes the same bug
inside the SDK's own `handleRequest()` path used by `Bot_Protection` for
license-token checks).
- **`Status_Handler::get_authorization_header()`** now delegates to the
SDK resolver: `$_SERVER` wins when populated, otherwise the raw request
headers are searched case-insensitively. New protected
`get_raw_request_headers()` seam wraps `getallheaders()` (guarded by
`function_exists`; unavailable under the CLI SAPI) so tests can inject
headers.
- **3 regression tests**: fallback to raw headers when `$_SERVER` lacks
the header, `$_SERVER` precedence, and empty result when absent
everywhere.

Verified end-to-end in wp-env (Apache + mod_php) via A/B: with the core
`.htaccess` auth rule removed to mirror affected hosts, the old code
never delivered the bearer to the verifier (no JWKS activity, decoy
served); with this fix the same probe reaches `StatusChallengeVerifier`,
fetches the platform JWKS, and fails only on the (deliberately)
unverifiable test token — decoy behavior for invalid/missing challenges
is unchanged.
# [1.3.0-beta.9](v1.3.0-beta.8...v1.3.0-beta.9) (2026-07-14)

### Bug Fixes

* resolve Authorization via the SDK's getallheaders() fallback in Status_Handler ([#19](#19)) ([d515452](d515452)), closes [getsupertab/connect-sdk-php#24](getsupertab/connect-sdk-php#24)
…#17)

This PR replaces the per-event analytics queue jobs with a custom-table
buffer drained hourly in batches of up to 500 events, using the
`/ingest/events` endpoint's new batch support (JSON array body →
`BatchIngestEventResponse`).

## Context

With `SUPERTAB_CONNECT_USE_WP_QUEUE` enabled, every classified bot
request scheduled its own queue job (Action Scheduler async → WP-Cron
fallback), and each job POSTed a single event to `/ingest/events`. On
busy sites that means one AS table write plus one loopback request plus
one outbound POST per event. The API now accepts up to 500 events per
request, so events can be accumulated and delivered in hourly batches
instead.

## Key Changes

- **`src/class-analytics-queue-table.php`** (new
`Analytics_Queue_Table`): owns
`{$wpdb->prefix}supertab_connect_analytics_queue` (`id`, `payload`
LONGTEXT, `created_at`). `install()` is dbDelta-based and version-gated
via `supertab_connect_db_version` (autoload=no); `insert()` is one
atomic row write; `claim_batch( $limit )` SELECTs oldest-first and
DELETEs the claimed rows in the same call — **delete-before-send**, so
delivery is deliver-once with no double-send window.
- **`src/class-analytics-dispatcher.php`** (rewritten): `enqueue()` now
INSERTs a JSON row instead of scheduling a job (row cap 10,000 → drop;
insert failure → inline single-event fallback). New `flush()` drains up
to 10 batches × 500 events per run and POSTs each batch as a JSON array
to `/ingest/events` (Bearer auth via the existing `WP_Http_Client`).
Non-2xx, partial rejections (`rejected_count`), timeouts: debug-logged
and dropped — fail-open, no retries, matching SDK semantics. The legacy
per-event hook stays registered so jobs queued by a previous version
drain gracefully.
- **Scheduling**: one hourly recurring
`supertab_connect_flush_analytics` action —
`as_schedule_recurring_action()` when Action Scheduler is present,
`wp_schedule_event()` fallback — ensured idempotently on admin/cron
requests (front-end requests do no schema or schedule work), with
automatic migration of a stale WP-Cron recurrence when AS appears.
Deactivation unschedules both hooks in both backends.
- **Lifecycle**: activation provisions the table; already-active
installs self-heal on the first admin/cron request after update;
`uninstall.php` drops the table and version option.
- Behavior with `SUPERTAB_CONNECT_USE_WP_QUEUE` unset/falsy is unchanged
(SDK default transport path untouched).

## Verification

- `composer test` — **92/92 unit tests passing** (165 assertions);
`lint`, `phpcs` (WordPress-VIP-Go), `phpstan` (level 5) clean.
- End-to-end in `.wp-env`: activation creates the table; a `GPTBot`
request buffers a row; `wp cron event run
supertab_connect_flush_analytics` drains the buffer to zero with a
single batch POST; deactivation removes the cron entry. The POST was
exercised against the live sandbox API with an invalid key — real HTTP
401, event dropped fail-open as designed.
- Not yet verified live: a 2xx batch delivery with a valid sandbox
merchant key.
# [1.3.0-beta.10](v1.3.0-beta.9...v1.3.0-beta.10) (2026-07-15)

### Features

* batch analytics events via custom-table buffer and hourly flush ([#17](#17)) ([d125194](d125194))
#20)

This PR adds real-database integration coverage for
`Analytics_Queue_Table` — the SQL semantics the unit suite's in-memory
`wpdb` spy records but cannot execute.

**Stacked on #17** (base: `feat/analytics-batch-buffer`): review only
the test diff here; the stack lands on `beta` as one work package once
#17 merges.

## Context

#17 gained two SQL-level behaviors late in review — the `FOR UPDATE`
claim transaction and the O(1) id-span capacity probe — that nothing
executes against a real database. The existing integration suite (WP
tests lib + MySQL 8.0 in CI, WP 6.4/7.0 × PHP 8.1/8.4) is the right home
for that proof.

## Key Changes

`tests/integration/AnalyticsQueueTableTest.php` (new, 6 tests):

- **Schema** — `install()` materializes the exact dbDelta schema
(`DESCRIBE`-verified) and records the version option; a version-current
re-run provably leaves existing rows untouched.
- **Round-trip** — payloads survive byte-for-byte (multibyte UTF-8
included); `created_at` is current UTC.
- **Drain** — `claim_batch()` returns oldest-first, deletes what it
returns, and reports a drained buffer as an empty claim.
- **Capacity probe** — `is_full()` boundary at the cap, plus the
documented id-span early-trip: a carved id gap trips the cap with only 2
rows present (the fail-open direction, pinned as intended).
- **`FOR UPDATE` claim** — a second mysqli session holds locks on the
oldest rows while the main connection claims: the contending claim must
return nothing (never the locked rows, `innodb_lock_wait_timeout = 1` to
fail fast); after the holder deletes-and-commits, a follow-up claim
returns exactly the survivors. Asserts claims are disjoint and
exhaustive — no double delivery, no loss.

Test-framework notes (documented in the file header): the suite's
`CREATE TEMPORARY TABLE` rewrite is disabled (temporary tables are
invisible to the second connection) and cleanup is explicit, since
`claim_batch()`'s own `COMMIT` ends the per-test rollback wrapper.

## Verification

- `composer lint`, `phpcs` (WordPress-VIP-Go), `phpstan` (level 5)
clean.
- The WP tests lib + MySQL harness is CI-only (same as #18's
`StatusRoutingTest`) — the Integration matrix on this PR is the gate.
This PR makes analytics opt-in: a new "Enable analytics" checkbox on the
plugin settings screen, off by default. Only when checked does the
plugin pass `analyticsEnabled: true` to the SDK or register the
batch-buffer delivery machinery.

## Context

Analytics was implicitly always on: whenever CAP ran, `Plugin::init()`
passed a hardcoded `analyticsEnabled: true` to the SDK, and (with the WP
queue enabled) buffered and delivered events. Merchants had no control.
This PR gives them the switch and defaults it to off.

## Key Changes

- **`Settings`** — new option `supertab_connect_analytics_enabled` with
`is_analytics_enabled()` / `set_analytics_enabled()`, mirroring the CAP
pair; cleared by `Settings::delete()` and on uninstall.
- **Settings screen** — "Enable analytics" checkbox inside the CAP
section (hidden with it when CAP is off). Saved on the same form branch
as CAP; reset to off when the API key is cleared.
- **`Plugin::init()`** — the misnamed `$analytics_enabled` local (it
actually gated bot protection) is renamed `$bot_protection_active`; a
real `$analytics_enabled = $bot_protection_active &&
is_analytics_enabled()` now gates the batch-buffer dispatcher, and the
SDK receives the derived flag instead of hardcoded `true`. With
analytics off there is no queue registration, no flush schedule, and the
SDK builds its noop transport. **CAP behavior is unchanged by the
toggle.**
- **Status self-report** — `eventReporting` in
`/.well-known/supertab/status` now also requires the opt-in, so the
backend prober sees the truth on analytics-off sites (found in review;
`enforcement` reporting unchanged).

## Verification

- `composer test` — 116/116 passing (213 assertions); lint, phpcs
(WordPress-VIP-Go), phpstan (level 5) clean.
- wp-env smoke: default (option absent) → GPTBot request buffers **no**
analytics row; option on → row buffered as before; Playwright round-trip
of the settings screen (checkbox renders in the CAP section, saves both
ways, hides with the CAP toggle); status endpoint reports
`eventReporting: false` until opted in.
# [1.3.0-beta.11](v1.3.0-beta.10...v1.3.0-beta.11) (2026-07-16)

### Features

* make analytics opt-in via a settings checkbox ([#21](#21)) ([9df74f0](9df74f0))
This PR fixes the hourly analytics flush never being scheduled on sites
where Action Scheduler is installed, which left buffered events stranded
in the queue table and nothing ever reaching the relay or merchant
dashboard.

## Context

Action Scheduler's data store only initializes on `init` priority 1, but
the plugin boots on `plugins_loaded` and
`Analytics_Dispatcher::register()` called `ensure_scheduled()`
synchronously right there. Called that early,
`as_has_scheduled_action()` returns `false` and
`as_schedule_recurring_action()` returns `0` without scheduling anything
(both guarded by `ActionScheduler::is_initialized()`, with a
`_doing_it_wrong` notice). Because Action Scheduler's functions *exist*
at that point, `action_scheduler_available()` was true and the WP-Cron
fallback was never reached either — the flush job ended up scheduled in
neither backend.

Reproduced on a live install (1.3.0-beta.11, queue constant on,
analytics opted in): events buffer into the queue table while neither
Scheduled Actions nor `wp cron event list` contains
`supertab_connect_flush_analytics`. wp-env never caught it because
Action Scheduler isn't installed there, so the code fell back to
`wp_schedule_event()`, which works fine at `plugins_loaded`.

## Solution

`register()` now hooks `ensure_scheduled()` to `init` instead of calling
it inline. The plugin boots at `plugins_loaded`, which always precedes
`init` (including on cron requests), so the deferred check runs once
Action Scheduler is ready; the WP-Cron fallback path is unaffected.
`ensure_scheduled()` becomes public so WordPress can invoke it as a hook
callback.

Verified end-to-end in wp-env with the `action-scheduler` plugin
installed and `SUPERTAB_CONNECT_USE_WP_QUEUE` on: after the fix an admin
request schedules the pending recurring action (no `_doing_it_wrong`
notices), and firing the hook drains the buffer and POSTs the batch to
`/ingest/events`. Events already buffered on affected installs will be
delivered by the first flush after deploy; no migration needed.
# [1.3.0-beta.12](v1.3.0-beta.11...v1.3.0-beta.12) (2026-07-16)

### Bug Fixes

* defer the analytics flush schedule check to init ([#22](#22)) ([8322a84](8322a84))
# [1.3.0-beta.13](v1.3.0-beta.12...v1.3.0-beta.13) (2026-07-17)

### Bug Fixes

* add vertical spacing between the CAP and analytics checkboxes ([a12bf0c](a12bf0c))
# [1.3.0-beta.14](v1.3.0-beta.13...v1.3.0-beta.14) (2026-07-17)

### Bug Fixes

* copy for enabling agent & bot classification ([817fd05](817fd05))
tomasstark and others added 2 commits August 27, 2026 11:23
…ngest service (#23)

* feat: update PHP SDK to v2.0.1 and route analytics to the dedicated ingest service

* fix: accept legacy soft/strict enforcement mode values from 1.2.x configs
# [1.3.0-beta.15](v1.3.0-beta.14...v1.3.0-beta.15) (2026-08-27)

### Features

* update PHP SDK to v2.0.1 and route analytics to the dedicated ingest service ([#23](#23)) ([90b6c69](90b6c69))
@tomasstark
tomasstark merged commit c3bb342 into main Aug 27, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.3.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants