feat: batch analytics events via custom-table buffer and hourly flush - #17
Merged
Conversation
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
There was a problem hiding this comment.
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.
…buffer # Conflicts: # tests/wp-stubs.php
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.
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))
|
🎉 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/eventsendpoint's new batch support (JSON array body →BatchIngestEventResponse).Context
With
SUPERTAB_CONNECT_USE_WP_QUEUEenabled, 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(newAnalytics_Queue_Table): owns{$wpdb->prefix}supertab_connect_analytics_queue(id,payloadLONGTEXT,created_at).install()is dbDelta-based and version-gated viasupertab_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). Newflush()drains up to 10 batches × 500 events per run and POSTs each batch as a JSON array to/ingest/events(Bearer auth via the existingWP_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.supertab_connect_flush_analyticsaction —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.uninstall.phpdrops the table and version option.SUPERTAB_CONNECT_USE_WP_QUEUEunset/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..wp-env: activation creates the table; aGPTBotrequest buffers a row;wp cron event run supertab_connect_flush_analyticsdrains 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.