Skip to content

feat: batch analytics events via custom-table buffer and hourly flush - #17

Merged
tomasstark merged 11 commits into
betafrom
feat/analytics-batch-buffer
Jul 15, 2026
Merged

feat: batch analytics events via custom-table buffer and hourly flush#17
tomasstark merged 11 commits into
betafrom
feat/analytics-batch-buffer

Conversation

@tomasstark

@tomasstark tomasstark commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

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 test92/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.

Add $wp_test_http_response global to wp-stubs.php for response override,
enabling tests of Analytics_Dispatcher::post_batch() branches:
- non-2xx status handling (no retry, events dropped)
- 2xx with rejected_count > 0 (partial rejection tolerated)

All 83 tests pass.
… coverage

- Wrap table install in register() with try-catch to prevent DB errors from
  fataling cron runs
- Add test verifying scheduling proceeds after install failure
- Track autoload parameter in update_option stub for option assertions
- Add assertion that schema version option has autoload=false
- Add boundary test for exact 500-item batch claim behavior
- Extend wp_remote_get stub with same response override as wp_remote_post
- Update HTTP stubs comment to document response override capability

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes the WordPress-queue-backed analytics delivery mechanism to buffer events in a custom database table and flush them hourly in batches (up to 500) using the API’s batch ingest support, reducing per-event scheduling and HTTP overhead on busy sites.

Changes:

  • Added a custom analytics queue table with schema versioning, insert, count, and batch-claim operations.
  • Reworked analytics dispatching to buffer events into the table and flush them hourly in bounded batches, while keeping the legacy per-event hook for backward compatibility.
  • Updated activation/uninstall lifecycle and expanded unit-test stubs and coverage for the new buffering + scheduling behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
uninstall.php Drops the new analytics queue table and removes the schema version option on uninstall.
tests/wp-stubs.php Extends WP stubs (wpdb spy, cron/AS stubs, json encode) to support new buffering/scheduling tests.
tests/phpstan-bootstrap.php Adds PHPStan stubs for Action Scheduler recurring APIs and defines ABSPATH for analysis context.
tests/AnalyticsQueueTableTest.php New tests covering table naming, dbDelta install/versioning, insert/count, and claim/delete behavior.
tests/AnalyticsDispatcherTest.php Updates tests to validate buffering, flush batching semantics, scheduling behavior, and legacy hook clearing.
supertab-connect.php Installs the analytics queue table on plugin activation.
src/class-plugin.php Wires the dispatcher to use the new Analytics_Queue_Table.
src/class-analytics-queue-table.php New custom table implementation for buffering and batch-claiming analytics payloads.
src/class-analytics-dispatcher.php Implements hourly batch flush + buffer enqueueing, scheduling migration, and legacy dispatch compatibility.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/class-analytics-queue-table.php Outdated
Comment thread src/class-analytics-dispatcher.php Outdated
Overlapping flush runners (WP-Cron and Action Scheduler firing together
during a backend migration) could both SELECT the same rows before either
DELETE ran, double-delivering a batch. Row locks make the second claimer
wait and see the rows already gone.
enqueue() ran SELECT COUNT(*) on every buffered event — an index scan of
up to 10k rows per classified bot request. Rows are inserted with
ascending ids and only ever deleted oldest-first, so MAX(id) - MIN(id) + 1
bounds the live row count with two O(1) index lookups. Auto-increment
gaps can only trip the cap early, which is the fail-open direction.
@tomasstark
tomasstark merged commit d125194 into beta Jul 15, 2026
5 checks passed
@tomasstark
tomasstark deleted the feat/analytics-batch-buffer branch July 15, 2026 10:02
github-actions Bot pushed a commit that referenced this pull request Jul 15, 2026
# [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))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.3.0-beta.10 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

tomasstark added a commit that referenced this pull request Jul 16, 2026
#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants