feat(api): add server-side tracking API for programmatic contexts (#171)#184
feat(api): add server-side tracking API for programmatic contexts (#171)#184parhumm merged 7 commits intodevelopmentfrom
Conversation
Add wp_slimstat::slimtrack() backward-compatible wrapper and wp_slimstat::slimtrack_server() for server-side tracking contexts (cron, CLI, redirect handlers) that bypasses CMP consent checks. CMP consent is a browser-side concept. In server-side contexts, there is no browser session and CMP consent has no meaningful role. The following settings remain enforced: - DNT (Do Not Track) headers - IP anonymization and hashing settings - Tracker cookie configuration - All exclusion rules
📝 WalkthroughWalkthroughAdds a programmatic/server-side tracking mode, a new filter context builder, and reorders consent decision flow to evaluate DNT and programmatic bypass before CMP integrations; exposes server-side tracking API and a flag to control programmatic tracking context; adds tests for filter-context behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client/Request
participant SlimStat as SlimStat (wp-slimstat.php)
participant Consent as Consent (src/Utils/Consent.php)
participant CMP as CMP / External Filters
rect rgba(120,200,100,0.5)
Note over Client,SlimStat: Browser/client-side tracking
Client->>SlimStat: track call
SlimStat->>Consent: canTrack()
Consent->>CMP: apply `slimstat_can_track` with context {source: 'browser', programmatic: false}
CMP-->>Consent: consent decision
Consent-->>SlimStat: allow/deny
SlimStat-->>Client: result
end
rect rgba(100,150,220,0.5)
Note over Client,SlimStat: Server-side / programmatic tracking
Client->>SlimStat: slimtrack_server()
SlimStat->>SlimStat: set is_programmatic_tracking = true
SlimStat->>Consent: canTrack()
Consent->>Consent: DNT & anonymous checks (respect DNT)
Consent->>CMP: apply `slimstat_can_track` with context {source: 'server', programmatic: true}
CMP-->>Consent: consent decision (may be bypassed for programmatic)
Consent-->>SlimStat: allow/deny
SlimStat->>SlimStat: restore is_programmatic_tracking = false
SlimStat-->>Client: result
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Utils/Consent.php`:
- Around line 391-397: The early unconditional "return true" when
\wp_slimstat::$is_programmatic_tracking is set bypasses anonymous-mode PII
protections; change the branch so programmatic flows only bypass CMP checks but
still enforce anonymous/PII restrictions — e.g., within the block that checks
\wp_slimstat::$is_programmatic_tracking, return true only if anonymous mode is
not enabled (check the anonymous-mode flag or helper such as a
Consent::is_anonymous_mode() or \wp_slimstat::$anonymous_mode), otherwise skip
returning and allow the existing anonymous/PII handling to run; keep the
existing DNT handling intact.
In `@wp-slimstat.php`:
- Around line 203-209: In slimtrack_server (the block setting
self::$is_programmatic_tracking around \SlimStat\Tracker\Tracker::slimtrack()),
save the current value of self::$is_programmatic_tracking to a local variable
before setting it to true and then in the finally block restore that saved value
instead of unconditionally assigning false; this preserves nested/re-entrant
programmatic tracking state and ensures the outer call's flag is not clobbered
by inner calls.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 15e44025-f5e6-4302-8351-7a17becb085c
📒 Files selected for processing (2)
src/Utils/Consent.phpwp-slimstat.php
- Restore prior flag state in slimtrack_server() for nested calls - Keep anonymous mode PII restrictions intact in programmatic mode
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Utils/Consent.php`:
- Around line 175-181: Update the PHPDoc step lists in the Consent class to
match actual runtime order: for canTrack() and piiAllowed() reorder the numbered
steps so they reflect the code path (DNT header check first, then Anonymous
Tracking handling, then programmatic tracking flag that bypasses CMP, then
determine if config collects PII (cookies OR full IPs), then if PII collected
check CMP consent (server-side verifiable CMPs or conservative blocking), then
apply the 'slimstat_can_track' filter, and finally return the decision); adjust
both docblocks that describe the decision tree so the step numbers and
descriptions match the control flow implemented by the canTrack() and
piiAllowed() methods.
- Around line 233-246: The programmatic-mode branch currently returns
apply_filters('slimstat_can_track', $default) which allows a filter to re-enable
tracking even when $default is false due to DNT; change it so DNT wins: call
apply_filters('slimstat_can_track', $default) into a local $can_track, cast to
bool, but if $default is false (DNT set) always return false; otherwise return
$can_track. Reference wp_slimstat::$is_programmatic_tracking and
apply_filters('slimstat_can_track', $default).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 29bd3781-44e7-4975-a3d9-d62e0ce46195
📒 Files selected for processing (2)
src/Utils/Consent.phpwp-slimstat.php
🚧 Files skipped from review as they are similar to previous changes (1)
- wp-slimstat.php
| * 1. Check programmatic tracking flag (bypasses CMP consent checks) | ||
| * 2. Check DNT header (if enabled in settings) | ||
| * 3. Check Anonymous Tracking mode (allows tracking without consent) | ||
| * 4. Determine if configuration collects PII (cookies OR full IPs) | ||
| * 5. If collects PII: Check CMP consent (for server-side verifiable CMPs or conservative blocking) | ||
| * 6. Apply 'slimstat_can_track' filter for external override | ||
| * 7. Return final decision |
There was a problem hiding this comment.
Decision-tree PHPDoc order does not match actual execution order.
Both method docs list programmatic checks before DNT/anonymous checks, but runtime flow does DNT first (and in piiAllowed(), anonymous-mode handling happens after the programmatic branch). Please align the step numbering with code to prevent maintenance mistakes.
Also applies to: 330-346
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Utils/Consent.php` around lines 175 - 181, Update the PHPDoc step lists
in the Consent class to match actual runtime order: for canTrack() and
piiAllowed() reorder the numbered steps so they reflect the code path (DNT
header check first, then Anonymous Tracking handling, then programmatic tracking
flag that bypasses CMP, then determine if config collects PII (cookies OR full
IPs), then if PII collected check CMP consent (server-side verifiable CMPs or
conservative blocking), then apply the 'slimstat_can_track' filter, and finally
return the decision); adjust both docblocks that describe the decision tree so
the step numbers and descriptions match the control flow implemented by the
canTrack() and piiAllowed() methods.
| // Programmatic tracking mode - bypass CMP consent checks | ||
| // Used by slimtrack_server() for server-side contexts (cron, CLI, redirect handlers) | ||
| // where no browser session exists and CMP consent has no meaningful role. | ||
| // DNT headers are still respected above. | ||
| if (\wp_slimstat::$is_programmatic_tracking) { | ||
| /** | ||
| * Filter: slimstat_can_track | ||
| * | ||
| * Allows third parties to override tracking decision in programmatic mode. | ||
| * | ||
| * @param bool $default Default decision (true in programmatic mode, respecting DNT) | ||
| */ | ||
| return (bool) apply_filters('slimstat_can_track', $default); | ||
| } |
There was a problem hiding this comment.
DNT is not hard-enforced in programmatic mode because the filter can re-enable tracking.
When HTTP_DNT=1, $default is set to false, but Line 245 still passes that through apply_filters('slimstat_can_track', $default). A callback can flip it back to true, which contradicts the “DNT headers are still respected above” guarantee.
🔧 Proposed fix
// Respect Do Not Track if enabled in settings
$respectDnt = ('on' === ($settings['do_not_track'] ?? 'off'));
+ $dntBlocked = false;
if ($respectDnt) {
$dntHeader = isset($_SERVER['HTTP_DNT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_DNT'])) : '';
if ('1' === $dntHeader) {
- $default = false;
+ $dntBlocked = true;
}
}
+
+ // DNT should be authoritative: do not allow overrides.
+ if ($dntBlocked) {
+ return false;
+ }As per coding guidelines: "Ensure no regressions in GDPR compliance; new code must maintain or improve privacy posture."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Utils/Consent.php` around lines 233 - 246, The programmatic-mode branch
currently returns apply_filters('slimstat_can_track', $default) which allows a
filter to re-enable tracking even when $default is false due to DNT; change it
so DNT wins: call apply_filters('slimstat_can_track', $default) into a local
$can_track, cast to bool, but if $default is false (DNT set) always return
false; otherwise return $can_track. Reference
wp_slimstat::$is_programmatic_tracking and apply_filters('slimstat_can_track',
$default).
src/Utils/Consent.php
Outdated
| * | ||
| * @param bool $default Default decision (true in programmatic mode, respecting DNT) | ||
| */ | ||
| return (bool) apply_filters('slimstat_can_track', $default); |
There was a problem hiding this comment.
One compatibility gap still looks open here: slimtrack_server() is described as bypassing CMP consent checks, but this branch still routes through apply_filters( slimstat_can_track, $default ). Consent.php documents that filter as the external override point for plugins, so any consent integration implemented there will continue to veto programmatic calls unless it is updated to inspect wp_slimstat::$is_programmatic_tracking.
That means #171 is only fully fixed for SlimStats built-in consent branches; filter-based integrations still have no explicit way to distinguish a normal browser hit from a server-side/programmatic one. The lowest-risk fix would be to pass a second context argument to the filter (for example [programmatic => \wp_slimstat::$is_programmatic_tracking, source => server]) and add a regression test that registers a deny-by-default slimstat_can_track callback, then proves slimtrack_server() can opt into the server-side path without changing normal browser behavior.
…tion Add second $context argument to all slimstat_can_track filter calls, allowing filter-based consent integrations to distinguish between normal browser hits and server-side/programmatic calls from slimtrack_server(). Context array contains: - programmatic: bool flag indicating server-side call - source: 'server' or 'browser' identifier This closes the compatibility gap where filter-based integrations would veto programmatic calls because they had no way to detect the server-side context. Adds regression test proving: - Deny-by-default filters can allow programmatic tracking via context - Normal browser behavior remains unchanged - Backward compatible with single-arg filter callbacks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/Utils/Consent.php (2)
191-198:⚠️ Potential issue | 🟡 MinorSync the decision-tree PHPDoc with the current control flow.
canTrack()still documents programmatic before DNT, andpiiAllowed()still says "No CMP: allow" even though Lines 668-669 now deny PII in that case. The step lists are out of date in both order and outcome.Also applies to: 362-382, 654-669
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Utils/Consent.php` around lines 191 - 198, Update the PHPDoc decision-tree comments to match the actual control flow and outcomes: revise the canTrack() doc to reflect that DNT is evaluated before/over programmatic flags (or whatever the current code enforces), and update piiAllowed() wording to state that absence of a CMP results in denying PII (not allowing it); ensure the numbered steps and outcomes match the real sequence used in the functions canTrack() and piiAllowed() and also correct the duplicated/outdated decision-tree blocks present near the other comment blocks to avoid contradictory descriptions.
247-276:⚠️ Potential issue | 🟠 MajorMake the DNT decision terminal.
HTTP_DNT=1only sets$default = false; the programmatic return here, the later Real Cookie Banner branch, and the final filter can still turn tracking back on. That means an opted-out request can still passcanTrack().🔒 One possible fix
$respectDnt = ('on' === ($settings['do_not_track'] ?? 'off')); if ($respectDnt) { $dntHeader = isset($_SERVER['HTTP_DNT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_DNT'])) : ''; if ('1' === $dntHeader) { - $default = false; + return false; } }As per coding guidelines: "Ensure no regressions in GDPR compliance; new code must maintain or improve privacy posture".
Also applies to: 341-346
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Utils/Consent.php` around lines 247 - 276, When HTTP_DNT is '1' we must make the opt-out terminal: after the DNT header check in Consent::canTrack (or the method containing $dntHeader and $default), immediately return false so programmatic mode (\wp_slimstat::$is_programmatic_tracking), the Real Cookie Banner branch, and the final apply_filters('slimstat_can_track', $default, ...) cannot re-enable tracking; locate the block that reads $_SERVER['HTTP_DNT'], sets $default = false, and change flow to short-circuit (return false) at that point.
🧹 Nitpick comments (2)
tests/consent-filter-context-test.php (2)
16-118: Prefer aWP_UnitTestCasehere over a local hook harness.This file reimplements
add_filter()/apply_filters()and uses a standalone assert/exit flow. For a regression about WordPress filter compatibility, that can drift from the real dispatcher and the repo's normal test bootstrap.As per coding guidelines: "
tests/**/*.php: Write unit tests usingWP_UnitTestCase".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/consent-filter-context-test.php` around lines 16 - 118, Replace this custom test harness with a proper WP_UnitTestCase-based test: create a test class extending WP_UnitTestCase (instead of top-level functions and custom asserts), move reset_state() logic into setUp()/tearDown() to reset wp_slimstat::$settings and the global $_filter_callbacks, remove the local add_filter/apply_filters/remove_filter/sanitize_text_field/wp_unslash stubs so the test uses WordPress's real dispatcher, and convert the procedural asserts/exit flow to PHPUnit assertions (e.g. $this->assertTrue, $this->assertFalse, $this->assertSame) while still requiring the Consent class (use SlimStat\Utils\Consent) in the test file.
110-118: Add one GDPR-enabled scenario.
reset_state()pins'gdpr_enabled' => 'off', so every assertion exits throughsrc/Utils/Consent.php:210-223. None of these tests execute the new DNT/programmatic/CMP path, so the mainslimtrack_server()flow is still untested.As per coding guidelines: "
tests/**/*.php: Include integration tests to confirm new features don't break existing reports and tracking".Also applies to: 124-245
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/consent-filter-context-test.php` around lines 110 - 118, The tests always force GDPR off in reset_state() which prevents exercising the GDPR/DNT/programmatic/CMP branches and thus never hits the main slimtrack_server() path; update the tests to include at least one GDPR-enabled scenario by either changing reset_state() to accept a parameter (e.g., $gdpr_enabled) or adding a new helper (e.g., reset_state_gdpr_enabled) that sets wp_slimstat::$settings['gdpr_enabled'] = 'on' and use that helper in the relevant tests (around the blocks you're running 124-245) so Consent.php's DNT/programmatic/CMP logic and the slimtrack_server() flow are executed. Ensure references to global $_filter_callbacks and wp_slimstat::$is_programmatic_tracking remain reset as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/Utils/Consent.php`:
- Around line 191-198: Update the PHPDoc decision-tree comments to match the
actual control flow and outcomes: revise the canTrack() doc to reflect that DNT
is evaluated before/over programmatic flags (or whatever the current code
enforces), and update piiAllowed() wording to state that absence of a CMP
results in denying PII (not allowing it); ensure the numbered steps and outcomes
match the real sequence used in the functions canTrack() and piiAllowed() and
also correct the duplicated/outdated decision-tree blocks present near the other
comment blocks to avoid contradictory descriptions.
- Around line 247-276: When HTTP_DNT is '1' we must make the opt-out terminal:
after the DNT header check in Consent::canTrack (or the method containing
$dntHeader and $default), immediately return false so programmatic mode
(\wp_slimstat::$is_programmatic_tracking), the Real Cookie Banner branch, and
the final apply_filters('slimstat_can_track', $default, ...) cannot re-enable
tracking; locate the block that reads $_SERVER['HTTP_DNT'], sets $default =
false, and change flow to short-circuit (return false) at that point.
---
Nitpick comments:
In `@tests/consent-filter-context-test.php`:
- Around line 16-118: Replace this custom test harness with a proper
WP_UnitTestCase-based test: create a test class extending WP_UnitTestCase
(instead of top-level functions and custom asserts), move reset_state() logic
into setUp()/tearDown() to reset wp_slimstat::$settings and the global
$_filter_callbacks, remove the local
add_filter/apply_filters/remove_filter/sanitize_text_field/wp_unslash stubs so
the test uses WordPress's real dispatcher, and convert the procedural
asserts/exit flow to PHPUnit assertions (e.g. $this->assertTrue,
$this->assertFalse, $this->assertSame) while still requiring the Consent class
(use SlimStat\Utils\Consent) in the test file.
- Around line 110-118: The tests always force GDPR off in reset_state() which
prevents exercising the GDPR/DNT/programmatic/CMP branches and thus never hits
the main slimtrack_server() path; update the tests to include at least one
GDPR-enabled scenario by either changing reset_state() to accept a parameter
(e.g., $gdpr_enabled) or adding a new helper (e.g., reset_state_gdpr_enabled)
that sets wp_slimstat::$settings['gdpr_enabled'] = 'on' and use that helper in
the relevant tests (around the blocks you're running 124-245) so Consent.php's
DNT/programmatic/CMP logic and the slimtrack_server() flow are executed. Ensure
references to global $_filter_callbacks and
wp_slimstat::$is_programmatic_tracking remain reset as before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dd49a51b-45f5-41f5-9e6f-34e34185050a
📒 Files selected for processing (2)
src/Utils/Consent.phptests/consent-filter-context-test.php
…-side-tracking-api
…eated filter calls
Replaces 4 identical apply_filters('slimstat_can_track', ...) call sites
(each with ~8 lines of duplicated PHPDoc) with a single private helper.
Ensures the context parameter is never accidentally omitted at new call sites.
8 tests validating slimtrack_server() behavior: - Programmatic tracking enters pipeline bypassing CMP consent - Flag state restoration after calls (including error cases) - Sequential calls don't leak state - GDPR enabled/disabled modes - Backward-compatible slimtrack() wrapper - DNT header handling in programmatic context
QA Test Results — PR #184 (Issue #171)Unit Tests: 14/14 passedCovers: E2E Tests: 8/8 passedCode Review (/simplify): CleanExtracted Blocking Issue FoundDuring E2E testing, a pre-existing Browscap/Flysystem namespace scoping bug was discovered that blocks the tracking pipeline from completing DB inserts. Filed as #187. This is unrelated to PR #184 — it affects all tracking calls on the current codebase. Environment
|
Context-aware filter callbacks could override DNT=1 block in the programmatic branch by returning true. Add $dntBlocked flag and short-circuit return false before the filter when DNT is active. Add regression tests (TEST 7-8) verifying DNT cannot be overridden by permissive filters in programmatic mode.
P1 Fix: DNT bypass in programmatic tracking pathCommit: 77958b7 Issue: Context-aware Fix: Added if (\wp_slimstat::$is_programmatic_tracking) {
if ($dntBlocked) {
return false;
}
return self::applyCanTrackFilter($default);
}Tests: Added 2 regression assertions (TEST 7-8) verifying:
All 16 unit tests pass. |
Add wp_slimstat::slimtrack() backward-compatible wrapper and wp_slimstat::slimtrack_server() for server-side tracking contexts (cron, CLI, redirect handlers) that bypasses CMP consent checks.
CMP consent is a browser-side concept. In server-side contexts, there is no browser session and CMP consent has no meaningful role.
The following settings remain enforced:
Closes #171
Related: #187 (pre-existing Browscap/Flysystem scoping bug discovered during E2E testing — unrelated to this PR)
Describe your changes
...
Submission Review Guidelines:
CHANGELOG.md.Type of change
Summary by CodeRabbit