fix(geoip): prevent infinite AJAX loop when WP-Cron disabled#166
fix(geoip): prevent infinite AJAX loop when WP-Cron disabled#166parhumm merged 14 commits intodevelopmentfrom
Conversation
When DISABLE_WP_CRON is true, the GeoIP fallback fires on every init hook including its own AJAX requests, and the AJAX handler never updates slimstat_last_geoip_dl — creating an infinite self-replicating loop. Root causes fixed: - Guard fallback: admin-only, no AJAX recursion, capability-gated, provider-aware (skips disabled installs) - AJAX handler: update timestamp on success and Cloudflare early return - Provider resolution: centralized resolve_geolocation_provider() helper respects legacy enable_maxmind tri-state (on/no/disable) - Config save: sync enable_maxmind when geolocation_provider is saved - Settings UI: lazy-migrate legacy flag, add 'Disabled' select option - Tracker: wire GeoService to resolver for authoritative provider state
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a centralized provider resolver and a "disable" geolocation provider; updates admin UI, gating for cron/AJAX and GeoIP update flows, GeoService behavior to short-circuit when disabled, lazy-migration/legacy sync, and extensive unit, e2e, perf, and QA test tooling. Changes
Sequence Diagram(s)sequenceDiagram
participant AdminUI as Admin UI
participant Resolver as wp_slimstat::resolve_geolocation_provider()
participant GeoSvc as GeoService
participant DB as Local GeoIP DB
participant Ext as External Provider
AdminUI->>Resolver: request provider
Resolver-->>AdminUI: 'maxmind' / 'dbip' / 'cloudflare' / false
alt provider == false
AdminUI->>GeoSvc: request update/check
GeoSvc->>Resolver: confirm provider
Resolver-->>GeoSvc: false
GeoSvc-->>AdminUI: return error/notice (geolocation disabled)
else provider == 'cloudflare'
AdminUI->>GeoSvc: request update
GeoSvc->>Resolver: confirm provider
Resolver-->>GeoSvc: 'cloudflare'
GeoSvc-->>AdminUI: update last-run timestamp (no DB required)
else provider == 'maxmind' or 'dbip'
AdminUI->>GeoSvc: request update/check
GeoSvc->>Resolver: confirm provider
Resolver-->>GeoSvc: 'maxmind'/'dbip'
GeoSvc->>Ext: download/check from external provider
Ext-->>GeoSvc: data / error
GeoSvc->>DB: write/delete DB file / update timestamps
DB-->>GeoSvc: ack
GeoSvc-->>AdminUI: success or provider-specific error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
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)
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 |
…ing raw lookups - Config save: skip DB check/schedule when provider is 'disable' (not just 'cloudflare'), preventing wasted GeolocationService instantiation - admin/view/index.php and wp-slimstat-reports.php: replace raw geolocation_provider ?? 'dbip' with resolve_geolocation_provider() for correct legacy MaxMind provider resolution
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/Services/GeoService.php (2)
76-79: Potential inconsistency:isJsDelivrEnabled()still uses legacyenable_maxmindproperty.This method checks
'no' == $this->enableMaxmindbut with the new provider system,enable_maxmindcan now be'disable'. When geolocation is disabled via the new UI (geolocation_provider = 'disable'), the sync logic setsenable_maxmind = 'disable', causing this method to returnfalse(which is likely correct behavior).However, for consistency with the rest of the PR's changes, consider updating this method to use the resolver:
♻️ Suggested refactor for consistency
public function isJsDelivrEnabled() { - return 'no' == $this->enableMaxmind; + return 'dbip' === \wp_slimstat::resolve_geolocation_provider(); }This aligns with how
isGeoIPEnabled()andisMaxMindEnabled()now work.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Services/GeoService.php` around lines 76 - 79, The isJsDelivrEnabled() method still checks the legacy enableMaxmind value; update it to use the new provider resolver like the other methods (e.g., mirror the pattern used in isGeoIPEnabled() and isMaxMindEnabled()) so it reads the geolocation/provider state from the resolver instead of comparing $this->enableMaxmind to 'no' (or alternatively check geolocation_provider !== 'disable' via the resolver); adjust isJsDelivrEnabled() to call the resolver method that determines JsDelivr/mirror availability.
17-21: Consider: Constructor readsenable_maxminddirectly but key methods now bypass it.The constructor still initializes
$this->enableMaxmindfrom settings, butisGeoIPEnabled()andisMaxMindEnabled()now ignore this property and call the resolver directly. This creates a subtle inconsistency where:
$this->enableMaxmindis set but mostly unusedisJsDelivrEnabled()andgetEnableMaxmind()still use the property- The other methods use the resolver
This is not a bug—the current behavior is correct—but the property and its accessors (
setEnableMaxmind,getEnableMaxmind) are now partially orphaned. This could be cleaned up in a follow-up to reduce confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Services/GeoService.php` around lines 17 - 21, The constructor sets $this->enableMaxmind but several methods (isGeoIPEnabled, isMaxMindEnabled) now call the resolver directly, leaving the property and its accessors partially orphaned; to clean this up, remove the unused property and its setter/getter (setEnableMaxmind, getEnableMaxmind) and update any methods that currently read the property (isJsDelivrEnabled, getEnableMaxmind usages) to call the same resolver used by isGeoIPEnabled()/isMaxMindEnabled() so all checks consistently use the resolver; locate symbols __construct, $this->enableMaxmind, isGeoIPEnabled(), isMaxMindEnabled(), isJsDelivrEnabled(), setEnableMaxmind(), and getEnableMaxmind to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@wp-slimstat.php`:
- Around line 359-372: The resolve_geolocation_provider() function currently
treats any set value for self::$settings['geolocation_provider'] (including
empty or malformed strings) as authoritative; update it to sanitize and
allowlist the new setting before returning it and only use the legacy
enable_maxmind flag if the new setting is missing or invalid. Specifically, in
resolve_geolocation_provider() call sanitize_text_field() on
self::$settings['geolocation_provider'], check it against an explicit allowed
set (e.g., 'disable', 'maxmind', 'dbip') and return false for 'disable' or the
allowed provider name; if the sanitized value is not in the allowlist, fall back
to reading self::$settings['enable_maxmind'] (handle 'on' -> 'maxmind', 'no' ->
'dbip') and return false otherwise.
---
Nitpick comments:
In `@src/Services/GeoService.php`:
- Around line 76-79: The isJsDelivrEnabled() method still checks the legacy
enableMaxmind value; update it to use the new provider resolver like the other
methods (e.g., mirror the pattern used in isGeoIPEnabled() and
isMaxMindEnabled()) so it reads the geolocation/provider state from the resolver
instead of comparing $this->enableMaxmind to 'no' (or alternatively check
geolocation_provider !== 'disable' via the resolver); adjust isJsDelivrEnabled()
to call the resolver method that determines JsDelivr/mirror availability.
- Around line 17-21: The constructor sets $this->enableMaxmind but several
methods (isGeoIPEnabled, isMaxMindEnabled) now call the resolver directly,
leaving the property and its accessors partially orphaned; to clean this up,
remove the unused property and its setter/getter (setEnableMaxmind,
getEnableMaxmind) and update any methods that currently read the property
(isJsDelivrEnabled, getEnableMaxmind usages) to call the same resolver used by
isGeoIPEnabled()/isMaxMindEnabled() so all checks consistently use the resolver;
locate symbols __construct, $this->enableMaxmind, isGeoIPEnabled(),
isMaxMindEnabled(), isJsDelivrEnabled(), setEnableMaxmind(), and
getEnableMaxmind to make these changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a7c5730a-6186-4a8f-bb8f-c1ac76784537
📒 Files selected for processing (5)
admin/assets/js/admin.jsadmin/config/index.phpadmin/index.phpsrc/Services/GeoService.phpwp-slimstat.php
When the previous provider was disabled, resolve_geolocation_provider() returns false and ?: 'dbip' coerced it to 'dbip', making the provider change detection at line 877 inaccurate. Now maps false → 'disable' so the comparison is correct for all state transitions.
QA ChecklistSetupFor each scenario, use the database directly ( 1. Infinite loop fix (critical path)
2. Recursion guard
3. Non-admin / frontend isolation
4. Provider resolution — legacy installs
5. Settings page — select rendering
6. Settings save — legacy flag sync
7. AJAX handlers
8. Cron path (DISABLE_WP_CRON=false)
9. Tracker integration
10. Regression checks
|
The resolver now calls sanitize_text_field() and checks against an explicit allowlist (maxmind, dbip, cloudflare, disable). Empty or malformed values fall through to the legacy enable_maxmind flag instead of being returned as-is, preventing invalid provider strings from reaching GeolocationFactory.
Add 4 test suites (67 assertions) covering the GeoIP provider resolver, GeoService wiring, legacy enable_maxmind sync mapping, and lazy migration. Wire all tests into `composer test` alongside existing license-tag tests.
Composer 2.9 doesn't resolve 'composer test' as a shorthand for custom scripts. Rename to 'test:all' matching the colon-namespaced convention.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@composer.json`:
- Around line 39-49: Add an integration test that reproduces regression `#164`:
create a new test file (e.g., tests/admin-ajax-geoip-regression-164-test.php)
that exercises the admin fallback → AJAX handler flow, the capability/AJAX
guards, and verifies slimstat_last_geoip_dl is updated (ensuring the
self-replicating loop is resolved); then add a composer script entry (e.g.,
"test:geoip-regression-164": "php
tests/admin-ajax-geoip-regression-164-test.php") and include that script in the
"test:all" array so the aggregate suite covers this regression path alongside
the existing scripts ("test:license-tags", "test:geoip-resolver",
"test:geoservice", "test:legacy-sync", "test:lazy-migration").
In `@tests/lazy-migration-test.php`:
- Around line 37-73: The test duplicates the lazy migration logic instead of
exercising the real path; remove the local implementations (the local
lazy_migrate() and wp_slimstat::resolve_geolocation_provider()) and instead load
or reference the canonical implementation from admin/config/index.php (or
extract that migration logic into a shared helper that both the production code
and tests require). Ensure the test bootstraps the real resolver so assertions
exercise the actual migration flow (call the real lazy_migrate() from the
shared/loaded code) and keep stubs/mocks only for external dependencies, not the
migration logic itself.
In `@tests/legacy-sync-mapping-test.php`:
- Around line 28-46: The test currently shadows production logic by implementing
sync_enable_maxmind() and a handwritten resolver copy; instead, remove this
duplicate mapping and have the test call the real production mapping/save logic
used by the app (i.e., replace sync_enable_maxmind() and the resolver copy with
a call to the actual save handler or shared mapping function used in
production), or move the mapping into a shared function in production code and
import it into the test; also eliminate the extra unknown-provider fallback in
the test so behavior matches the real save handler exactly.
In `@tests/resolve-geolocation-provider-test.php`:
- Around line 37-66: The test currently defines a local stub of
wp_slimstat::resolve_geolocation_provider() which masks regressions in the real
implementation; instead, remove the local method stub and update the test to
exercise the production resolver by either requiring or autoloading the real
implementation (the original resolve_geolocation_provider logic from
wp-slimstat.php) or by extracting that resolver into a shared module used by
both production code and this test; locate references to
wp_slimstat::resolve_geolocation_provider and replace the local class definition
with a call/load of the production module so the assertions validate the real
code path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fabec8b9-b159-4017-9604-08611dc73396
📒 Files selected for processing (9)
admin/config/index.phpadmin/view/index.phpadmin/view/wp-slimstat-reports.phpcomposer.jsontests/geoservice-provider-resolution-test.phptests/lazy-migration-test.phptests/legacy-sync-mapping-test.phptests/resolve-geolocation-provider-test.phpwp-slimstat.php
🚧 Files skipped from review as they are similar to previous changes (2)
- wp-slimstat.php
- admin/config/index.php
| "test:license-tags": "php tests/license-tag-gating-test.php", | ||
| "test:geoip-resolver": "php tests/resolve-geolocation-provider-test.php", | ||
| "test:geoservice": "php tests/geoservice-provider-resolution-test.php", | ||
| "test:legacy-sync": "php tests/legacy-sync-mapping-test.php", | ||
| "test:lazy-migration": "php tests/lazy-migration-test.php", | ||
| "test:all": [ | ||
| "@test:license-tags", | ||
| "@test:geoip-resolver", | ||
| "@test:geoservice", | ||
| "@test:legacy-sync", | ||
| "@test:lazy-migration" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
test:all still misses the #164 regression path.
These scripts cover resolver, mapping, migration, and GeoService wiring only; they never exercise the admin fallback → AJAX handler interaction, the new capability/AJAX guards, or the slimstat_last_geoip_dl update that stopped the self-replicating loop. Please add one integration test for that flow before relying on this aggregate suite as the regression gate. Based on learnings, "Include integration tests to confirm new features don't break existing reports and tracking."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@composer.json` around lines 39 - 49, Add an integration test that reproduces
regression `#164`: create a new test file (e.g.,
tests/admin-ajax-geoip-regression-164-test.php) that exercises the admin
fallback → AJAX handler flow, the capability/AJAX guards, and verifies
slimstat_last_geoip_dl is updated (ensuring the self-replicating loop is
resolved); then add a composer script entry (e.g., "test:geoip-regression-164":
"php tests/admin-ajax-geoip-regression-164-test.php") and include that script in
the "test:all" array so the aggregate suite covers this regression path
alongside the existing scripts ("test:license-tags", "test:geoip-resolver",
"test:geoservice", "test:legacy-sync", "test:lazy-migration").
| class wp_slimstat | ||
| { | ||
| public static $settings = []; | ||
|
|
||
| public static function resolve_geolocation_provider() | ||
| { | ||
| if (isset(self::$settings['geolocation_provider'])) { | ||
| $p = sanitize_text_field(self::$settings['geolocation_provider']); | ||
| if ('disable' === $p) { | ||
| return false; | ||
| } | ||
| if (in_array($p, ['maxmind', 'dbip', 'cloudflare'], true)) { | ||
| return $p; | ||
| } | ||
| } | ||
| $em = self::$settings['enable_maxmind'] ?? 'disable'; | ||
| if ('on' === $em) { | ||
| return 'maxmind'; | ||
| } | ||
| if ('no' === $em) { | ||
| return 'dbip'; | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Replicates the lazy migration logic from admin/config/index.php:17-22. | ||
| * Populates geolocation_provider in $settings if not already set. | ||
| */ | ||
| function lazy_migrate(): void | ||
| { | ||
| if (!isset(wp_slimstat::$settings['geolocation_provider'])) { | ||
| $resolved = wp_slimstat::resolve_geolocation_provider(); | ||
| wp_slimstat::$settings['geolocation_provider'] = false !== $resolved ? $resolved : 'disable'; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Please exercise the real lazy-migration path here.
lazy_migrate() and the resolver are reimplemented locally rather than pulled from admin/config/index.php:17-22, so this script can keep passing after the production migration changes. Extract the migration into shared code, or load the real implementation with stubs, so the assertions guard the actual settings flow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/lazy-migration-test.php` around lines 37 - 73, The test duplicates the
lazy migration logic instead of exercising the real path; remove the local
implementations (the local lazy_migrate() and
wp_slimstat::resolve_geolocation_provider()) and instead load or reference the
canonical implementation from admin/config/index.php (or extract that migration
logic into a shared helper that both the production code and tests require).
Ensure the test bootstraps the real resolver so assertions exercise the actual
migration flow (call the real lazy_migrate() from the shared/loaded code) and
keep stubs/mocks only for external dependencies, not the migration logic itself.
| /** | ||
| * Replicates the sync logic from admin/config/index.php:857-864. | ||
| * If this logic changes in the source, this function must be updated too. | ||
| * | ||
| * @param string $provider The geolocation_provider value being saved | ||
| * @return string The enable_maxmind value that should be synced | ||
| */ | ||
| function sync_enable_maxmind(string $provider): string | ||
| { | ||
| if ('maxmind' === $provider) { | ||
| return 'on'; | ||
| } elseif ('dbip' === $provider || 'cloudflare' === $provider) { | ||
| return 'no'; | ||
| } elseif ('disable' === $provider) { | ||
| return 'disable'; | ||
| } | ||
| // Unknown provider — should not happen with allowlist, but return current default | ||
| return 'disable'; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Please exercise the real sync contract here.
sync_enable_maxmind() and the resolver are handwritten copies of admin/config/index.php:858-866 and wp-slimstat.php:359-379, so this round-trip can stay green while the real save path drifts. sync_enable_maxmind() also adds an unknown-provider fallback that the current save handler does not have. Pull the mapping into shared production code, or invoke the real save logic here, instead of shadowing it in the test.
Also applies to: 67-91
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/legacy-sync-mapping-test.php` around lines 28 - 46, The test currently
shadows production logic by implementing sync_enable_maxmind() and a handwritten
resolver copy; instead, remove this duplicate mapping and have the test call the
real production mapping/save logic used by the app (i.e., replace
sync_enable_maxmind() and the resolver copy with a call to the actual save
handler or shared mapping function used in production), or move the mapping into
a shared function in production code and import it into the test; also eliminate
the extra unknown-provider fallback in the test so behavior matches the real
save handler exactly.
| /** | ||
| * Minimal wp_slimstat stub with the real resolve_geolocation_provider() logic. | ||
| * Copied from wp-slimstat.php:359-379 (commit 7ba56338). | ||
| */ | ||
| class wp_slimstat | ||
| { | ||
| public static $settings = []; | ||
|
|
||
| public static function resolve_geolocation_provider() | ||
| { | ||
| if (isset(self::$settings['geolocation_provider'])) { | ||
| $p = sanitize_text_field(self::$settings['geolocation_provider']); | ||
| if ('disable' === $p) { | ||
| return false; | ||
| } | ||
| if (in_array($p, ['maxmind', 'dbip', 'cloudflare'], true)) { | ||
| return $p; | ||
| } | ||
| // Invalid/empty value — fall through to legacy flag | ||
| } | ||
| $em = self::$settings['enable_maxmind'] ?? 'disable'; | ||
| if ('on' === $em) { | ||
| return 'maxmind'; | ||
| } | ||
| if ('no' === $em) { | ||
| return 'dbip'; | ||
| } | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Please exercise the production resolver, not a local clone.
This file redefines wp_slimstat::resolve_geolocation_provider() instead of loading wp-slimstat.php:359-379, so the script can stay green while the real implementation regresses. Extract the resolver into shared production code or load the actual method here so these assertions protect the real path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/resolve-geolocation-provider-test.php` around lines 37 - 66, The test
currently defines a local stub of wp_slimstat::resolve_geolocation_provider()
which masks regressions in the real implementation; instead, remove the local
method stub and update the test to exercise the production resolver by either
requiring or autoloading the real implementation (the original
resolve_geolocation_provider logic from wp-slimstat.php) or by extracting that
resolver into a shared module used by both production code and this test; locate
references to wp_slimstat::resolve_geolocation_provider and replace the local
class definition with a call/load of the production module so the assertions
validate the real code path.
Playwright tests (6 cases): - Single/successive/concurrent admin loads produce bounded AJAX calls - Author (non-admin) triggers 0 AJAX calls - Provider disabled triggers 0 AJAX calls - Direct AJAX POST shows no self-recursion k6 load test: - 20 RPS steady-state (2 min) + spike to 50 RPS (1 min) - Verifies AJAX count stays bounded under high concurrent traffic - All thresholds pass: 0% errors, p95 < 5s, p99 < 8s Infrastructure: - mu-plugin logger as oracle for server-side AJAX counting - MySQL helper for DB state management - Orchestration script (tests/run-qa.sh) for full QA suite
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
tests/e2e/global-setup.ts (2)
43-44: Useconfig.projectsto derivebaseURLinstead of hardcoding.The
configparameter is passed toglobalSetupbut unused. ThebaseURLshould be extracted from the Playwright config for consistency:♻️ Proposed fix
export default async function globalSetup(config: FullConfig): Promise<void> { - const baseURL = 'http://localhost:10003'; + const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:10003';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 43 - 44, The globalSetup function currently hardcodes baseURL; instead extract the base URL from the provided Playwright config: inspect config.projects (e.g., find the first project or the project matching your test environment) and read its use.baseURL (or use.playwright options) to set baseURL, falling back to a sensible default if no baseURL is present; update the code in globalSetup to replace the hardcoded baseURL with this derived value and handle missing/undefined values gracefully.
51-52: Hardcoded test credentials.Test usernames and passwords are hardcoded. For better maintainability, consider using environment variables with defaults, consistent with the k6 test approach.
♻️ Suggested approach
+const ADMIN_USER = process.env.WP_ADMIN_USER || 'parhumm'; +const ADMIN_PASS = process.env.WP_ADMIN_PASS || 'testpass123'; +const AUTHOR_USER = process.env.WP_AUTHOR_USER || 'dordane'; +const AUTHOR_PASS = process.env.WP_AUTHOR_PASS || 'testpass123'; // Login as admin await loginAndSave( baseURL, - 'parhumm', - 'testpass123', + ADMIN_USER, + ADMIN_PASS, path.join(AUTH_DIR, 'admin.json') );Also applies to: 59-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 51 - 52, Replace the hardcoded credentials 'parhumm' and 'testpass123' with environment-driven defaults (e.g., use process.env.E2E_TEST_USER ?? 'parhumm' and process.env.E2E_TEST_PASS ?? 'testpass123') wherever those literals appear (also update the other occurrences noted at the second pair) so tests read credentials from env vars with the same fallback values; ensure any helper/initializer (the array or variables that currently contain 'parhumm'/'testpass123') now references these env variables.tests/e2e/helpers/setup.ts (2)
72-77: Add error handling for JSON parsing.If the log file contains malformed entries,
JSON.parse(line)will throw and crash the test. Consider wrapping in try/catch for robustness.🔧 Proposed fix
export function readAjaxLog(): AjaxLogEntry[] { if (!fs.existsSync(AJAX_LOG)) return []; const raw = fs.readFileSync(AJAX_LOG, 'utf8').trim(); if (!raw) return []; - return raw.split('\n').filter(Boolean).map((line) => JSON.parse(line)); + return raw.split('\n').filter(Boolean).map((line) => { + try { + return JSON.parse(line); + } catch { + console.warn(`Malformed log entry: ${line}`); + return null; + } + }).filter((entry): entry is AjaxLogEntry => entry !== null); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers/setup.ts` around lines 72 - 77, The readAjaxLog function currently calls JSON.parse(line) directly which will throw on malformed lines and crash tests; update readAjaxLog to wrap the JSON.parse call in a try/catch (inside the map/filter pipeline) so malformed JSON entries are caught, skipped (or collected as parse errors), and do not abort the entire function—optionally emit a warning (e.g., console.warn) with the offending line or index for debugging while continuing to return the successfully parsed AjaxLogEntry[].
135-140: Escape regex metacharacters inkeyto prevent pattern injection.Static analysis flagged a potential ReDoS risk. While
keyis test-controlled, escaping metacharacters is a defensive best practice that prevents subtle bugs if keys contain characters like.or*.🛡️ Proposed fix
+function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} export async function setSlimstatSetting(key: string, value: string): Promise<void> { // ... + const escapedKey = escapeRegex(key); const keyPattern = new RegExp( - `s:\\d+:"${key}";s:\\d+:"[^"]*"`, + `s:\\d+:"${escapedKey}";s:\\d+:"[^"]*"`, 'g' );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers/setup.ts` around lines 135 - 140, Escape regex metacharacters in the `key` before building the `keyPattern` RegExp to avoid pattern injection; create an escapedKey (e.g., replace /[.*+?^${}()|[\]\\]/g) and use that in the new RegExp for `keyPattern` while keeping the original `key` (and `value`) when computing the `replacement` string so lengths remain correct; update the code that creates `keyPattern` and `replacement` (symbols: keyPattern, replacement) to use escapedKey for the regex and key for the serialized length.tests/e2e/geoip-ajax-loop.spec.ts (2)
144-152: Dead code: unusednonceevaluation.This block fetches a nonce but immediately returns
nulland thenoncevariable is never used. The subsequent code at lines 163-177 works without it. This appears to be leftover from development.🧹 Remove dead code
- // Extract nonce from the page — SlimStat injects it - // We'll generate one by hitting a page that outputs wpnonce - const nonce = await page.evaluate(async () => { - // Fetch a fresh nonce via admin-ajax approach - const res = await fetch('/wp-admin/admin-ajax.php', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'action=slimstat_update_geoip_database&security=invalid_nonce', - }); - return null; // We'll use a different approach - }); - // Direct POST using Playwright's request context (carries auth cookies)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/geoip-ajax-loop.spec.ts` around lines 144 - 152, Remove the dead page.evaluate block that assigns `nonce` but immediately returns null and is never used: delete the fetch call inside the test (the anonymous async passed to page.evaluate that posts to '/wp-admin/admin-ajax.php' with body 'action=slimstat_update_geoip_database&security=invalid_nonce') and the unused `const nonce = await page.evaluate(...)` declaration so the test relies on the subsequent working approach at lines 163-177; ensure no other references to `nonce` remain.
46-57: Timing-based waits (waitForTimeout) are brittle.Hard-coded timeouts (e.g., 4000ms, 1000ms, 3000ms) can cause flakiness on slower systems or pass prematurely on faster ones. Consider using
waitForResponseor polling patterns where possible.For example, instead of waiting a fixed time for AJAX to settle:
// Wait for the specific AJAX request to complete await page.waitForResponse( resp => resp.url().includes('admin-ajax.php') && resp.request().postData()?.includes('slimstat_update_geoip_database'), { timeout: 5000 } ).catch(() => { /* no AJAX fired, which is valid */ });Also applies to: 71-76
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/geoip-ajax-loop.spec.ts` around lines 46 - 57, Replace brittle fixed delays in the test "Test 1: single admin page load triggers at most 1 AJAX call" (and the similar waits around lines 71-76) by waiting for the specific AJAX activity instead of using page.waitForTimeout(4000); locate the test function and the helper readAjaxLog and replace the timeout with an awaited waitForResponse (or a short polling loop) that checks for responses whose URL contains "admin-ajax.php" and whose request post data includes "slimstat_update_geoip_database", with a reasonable timeout (e.g., 5s) and a catch that allows no response as valid; this ensures the assertion on readAjaxLog().length remains stable without hard-coded sleeps.tests/run-qa.sh (2)
131-131: Phase numbering inconsistency.The script jumps from "Phase 4" (line 115) to "Phase 7" (line 131). Phases 5 and 6 are missing. This appears to be a cosmetic oversight.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/run-qa.sh` at line 131, The phase heading "Phase 7: Post-test DB verification" is misnumbered relative to the earlier "Phase 4" and should be corrected; locate the string "Phase 7: Post-test DB verification" in tests/run-qa.sh and either renumber it to "Phase 5: Post-test DB verification" (if phases 5–6 were never intended) or insert/rename the missing intermediate phase headings consistent with the rest of the script so the sequence is contiguous.
111-112: MySQL credentials exposed on command line.Password on command line (
-proot) is visible in process lists and shell history. For local development this is acceptable, but consider using a.my.cnffile or environment variable for better practice. Also, the MySQL user/password are hardcoded.♻️ Suggested improvement
+MYSQL_USER="${MYSQL_USER:-root}" +MYSQL_PASS="${MYSQL_PASS:-root}" +MYSQL_DB="${MYSQL_DB:-local}" mysql --socket="$MYSQL_SOCKET" -u root -proot local -e \ - "DELETE FROM wp_options WHERE option_name = 'slimstat_last_geoip_dl';" 2>/dev/null +mysql --socket="$MYSQL_SOCKET" -u "$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_DB" -e \ + "DELETE FROM wp_options WHERE option_name = 'slimstat_last_geoip_dl';" 2>/dev/null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/run-qa.sh` around lines 111 - 112, Replace the hardcoded password in the mysql call `mysql --socket="$MYSQL_SOCKET" -u root -proot local -e "DELETE FROM wp_options WHERE option_name = 'slimstat_last_geoip_dl';"` so credentials are not on the command line; for example, read user/password from environment variables (MYSQL_USER/MYSQL_PASSWORD) and invoke without `-p` while exporting MYSQL_PWD="$MYSQL_PASSWORD" before the call, or use a temporary `--defaults-extra-file` or `--login-path` pointing to a .my.cnf file generated from those env vars; update the invocation to use the variableized user (replace `-u root` with `-u "$MYSQL_USER"`) and remove the `-proot` literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/e2e/helpers/setup.ts`:
- Around line 15-24: The tests currently use hardcoded filesystem and socket
paths (WP_ROOT, WP_CONFIG, WP_CONTENT, MU_PLUGINS, LOGGER_SRC, LOGGER_DEST,
AJAX_LOG, CRON_LINE, MYSQL_SOCKET) which breaks portability; update setup.ts to
read these values from environment variables or a shared config with sensible
defaults (e.g., process.env.WP_ROOT || fallback) and replace direct constants
with those variables so local dev machines and CI can override paths and the
MySQL socket via env vars or a config file.
In `@tests/perf/geoip-load.js`:
- Around line 119-135: The teardown currently fetches the AJAX log over HTTP
using http.get(`${BASE_URL}/wp-content/geoip-ajax-calls.log`) inside the
teardown function, which can expose .log files; change this to avoid HTTP
exposure by either (A) reading the log file directly with k6's file APIs (e.g.,
use open or other local file-read mechanism at runtime) when tests run locally,
replacing the http.get call and subsequent log parsing, or (B) add a guard that
requires an explicit opt-in env flag before attempting HTTP access and otherwise
log a warning to check server config; update code references in teardown, the
BASE_URL/http.get usage, and the lines.length checks accordingly.
- Around line 71-92: The setup() function currently authenticates once but does
not propagate cookies to VU iterations; capture the authentication cookies from
loginRes in setup() (e.g., return loginRes.cookies or a structured cookies
object) and then in default(data) use http.cookieJar() to set those cookies into
the VU's jar before hitting admin pages so each VU is authenticated;
alternatively move the login logic from setup() into default() so each VU
performs its own POST to `${BASE_URL}/wp-login.php` (refer to setup(), loginRes,
loginOk, default(), and http.cookieJar()).
In `@tests/run-qa.sh`:
- Around line 12-16: The script hardcodes machine-specific paths (WP_ROOT,
WP_CONFIG, MU_PLUGINS, AJAX_LOG, MYSQL_SOCKET) which breaks portability; update
each variable to read from an environment variable with a sensible fallback
default (e.g., WP_ROOT="${WP_ROOT:-./public}" style) and derive related paths
from WP_ROOT (so WP_CONFIG, MU_PLUGINS, AJAX_LOG are constructed from WP_ROOT)
and make MYSQL_SOCKET configurable via an env var
(MYSQL_SOCKET="${MYSQL_SOCKET:-/var/run/mysqld/mysqld.sock}") so other devs/CI
can override values without editing the script.
- Around line 98-100: The script uses a macOS-specific sed invocation (the
string starting with sed -i '' "/That's all, stop editing/i\\
define('DISABLE_WP_CRON', true);" "$WP_CONFIG") which breaks on Linux; replace
it with a portable approach such as using sed -i.bak to create a temporary
backup and then remove the .bak file (or detect OS and use -i '' on Darwin but
-i on others), so update the sed invocation in tests/run-qa.sh to use sed -i.bak
for the insert and then delete the backup to maintain cross-platform
compatibility.
---
Nitpick comments:
In `@tests/e2e/geoip-ajax-loop.spec.ts`:
- Around line 144-152: Remove the dead page.evaluate block that assigns `nonce`
but immediately returns null and is never used: delete the fetch call inside the
test (the anonymous async passed to page.evaluate that posts to
'/wp-admin/admin-ajax.php' with body
'action=slimstat_update_geoip_database&security=invalid_nonce') and the unused
`const nonce = await page.evaluate(...)` declaration so the test relies on the
subsequent working approach at lines 163-177; ensure no other references to
`nonce` remain.
- Around line 46-57: Replace brittle fixed delays in the test "Test 1: single
admin page load triggers at most 1 AJAX call" (and the similar waits around
lines 71-76) by waiting for the specific AJAX activity instead of using
page.waitForTimeout(4000); locate the test function and the helper readAjaxLog
and replace the timeout with an awaited waitForResponse (or a short polling
loop) that checks for responses whose URL contains "admin-ajax.php" and whose
request post data includes "slimstat_update_geoip_database", with a reasonable
timeout (e.g., 5s) and a catch that allows no response as valid; this ensures
the assertion on readAjaxLog().length remains stable without hard-coded sleeps.
In `@tests/e2e/global-setup.ts`:
- Around line 43-44: The globalSetup function currently hardcodes baseURL;
instead extract the base URL from the provided Playwright config: inspect
config.projects (e.g., find the first project or the project matching your test
environment) and read its use.baseURL (or use.playwright options) to set
baseURL, falling back to a sensible default if no baseURL is present; update the
code in globalSetup to replace the hardcoded baseURL with this derived value and
handle missing/undefined values gracefully.
- Around line 51-52: Replace the hardcoded credentials 'parhumm' and
'testpass123' with environment-driven defaults (e.g., use
process.env.E2E_TEST_USER ?? 'parhumm' and process.env.E2E_TEST_PASS ??
'testpass123') wherever those literals appear (also update the other occurrences
noted at the second pair) so tests read credentials from env vars with the same
fallback values; ensure any helper/initializer (the array or variables that
currently contain 'parhumm'/'testpass123') now references these env variables.
In `@tests/e2e/helpers/setup.ts`:
- Around line 72-77: The readAjaxLog function currently calls JSON.parse(line)
directly which will throw on malformed lines and crash tests; update readAjaxLog
to wrap the JSON.parse call in a try/catch (inside the map/filter pipeline) so
malformed JSON entries are caught, skipped (or collected as parse errors), and
do not abort the entire function—optionally emit a warning (e.g., console.warn)
with the offending line or index for debugging while continuing to return the
successfully parsed AjaxLogEntry[].
- Around line 135-140: Escape regex metacharacters in the `key` before building
the `keyPattern` RegExp to avoid pattern injection; create an escapedKey (e.g.,
replace /[.*+?^${}()|[\]\\]/g) and use that in the new RegExp for `keyPattern`
while keeping the original `key` (and `value`) when computing the `replacement`
string so lengths remain correct; update the code that creates `keyPattern` and
`replacement` (symbols: keyPattern, replacement) to use escapedKey for the regex
and key for the serialized length.
In `@tests/run-qa.sh`:
- Line 131: The phase heading "Phase 7: Post-test DB verification" is
misnumbered relative to the earlier "Phase 4" and should be corrected; locate
the string "Phase 7: Post-test DB verification" in tests/run-qa.sh and either
renumber it to "Phase 5: Post-test DB verification" (if phases 5–6 were never
intended) or insert/rename the missing intermediate phase headings consistent
with the rest of the script so the sequence is contiguous.
- Around line 111-112: Replace the hardcoded password in the mysql call `mysql
--socket="$MYSQL_SOCKET" -u root -proot local -e "DELETE FROM wp_options WHERE
option_name = 'slimstat_last_geoip_dl';"` so credentials are not on the command
line; for example, read user/password from environment variables
(MYSQL_USER/MYSQL_PASSWORD) and invoke without `-p` while exporting
MYSQL_PWD="$MYSQL_PASSWORD" before the call, or use a temporary
`--defaults-extra-file` or `--login-path` pointing to a .my.cnf file generated
from those env vars; update the invocation to use the variableized user (replace
`-u root` with `-u "$MYSQL_USER"`) and remove the `-proot` literal.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c1e195f5-1a5c-40dc-b78f-9f6364584c71
📒 Files selected for processing (8)
package.jsontests/e2e/geoip-ajax-loop.spec.tstests/e2e/global-setup.tstests/e2e/helpers/ajax-logger-mu-plugin.phptests/e2e/helpers/setup.tstests/e2e/playwright.config.tstests/perf/geoip-load.jstests/run-qa.sh
| const WP_ROOT = '/Users/parhumm/Local Sites/test/app/public'; | ||
| const WP_CONFIG = path.join(WP_ROOT, 'wp-config.php'); | ||
| const WP_CONTENT = path.join(WP_ROOT, 'wp-content'); | ||
| const MU_PLUGINS = path.join(WP_CONTENT, 'mu-plugins'); | ||
| const AJAX_LOG = path.join(WP_CONTENT, 'geoip-ajax-calls.log'); | ||
| const LOGGER_SRC = path.join(__dirname, 'ajax-logger-mu-plugin.php'); | ||
| const LOGGER_DEST = path.join(MU_PLUGINS, 'geoip-ajax-logger.php'); | ||
| const CRON_LINE = "define('DISABLE_WP_CRON', true);"; | ||
|
|
||
| const MYSQL_SOCKET = '/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock'; |
There was a problem hiding this comment.
Hardcoded paths and credentials reduce portability.
Same concern as in run-qa.sh: these paths are developer-machine-specific and the MySQL socket path is particularly environment-dependent. Consider using environment variables or a shared configuration file.
🔧 Suggested approach
-const WP_ROOT = '/Users/parhumm/Local Sites/test/app/public';
+const WP_ROOT = process.env.WP_ROOT || '/Users/parhumm/Local Sites/test/app/public';
const WP_CONFIG = path.join(WP_ROOT, 'wp-config.php');
// ... etc
-const MYSQL_SOCKET = '/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock';
+const MYSQL_SOCKET = process.env.MYSQL_SOCKET || '/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const WP_ROOT = '/Users/parhumm/Local Sites/test/app/public'; | |
| const WP_CONFIG = path.join(WP_ROOT, 'wp-config.php'); | |
| const WP_CONTENT = path.join(WP_ROOT, 'wp-content'); | |
| const MU_PLUGINS = path.join(WP_CONTENT, 'mu-plugins'); | |
| const AJAX_LOG = path.join(WP_CONTENT, 'geoip-ajax-calls.log'); | |
| const LOGGER_SRC = path.join(__dirname, 'ajax-logger-mu-plugin.php'); | |
| const LOGGER_DEST = path.join(MU_PLUGINS, 'geoip-ajax-logger.php'); | |
| const CRON_LINE = "define('DISABLE_WP_CRON', true);"; | |
| const MYSQL_SOCKET = '/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock'; | |
| const WP_ROOT = process.env.WP_ROOT || '/Users/parhumm/Local Sites/test/app/public'; | |
| const WP_CONFIG = path.join(WP_ROOT, 'wp-config.php'); | |
| const WP_CONTENT = path.join(WP_ROOT, 'wp-content'); | |
| const MU_PLUGINS = path.join(WP_CONTENT, 'mu-plugins'); | |
| const AJAX_LOG = path.join(WP_CONTENT, 'geoip-ajax-calls.log'); | |
| const LOGGER_SRC = path.join(__dirname, 'ajax-logger-mu-plugin.php'); | |
| const LOGGER_DEST = path.join(MU_PLUGINS, 'geoip-ajax-logger.php'); | |
| const CRON_LINE = "define('DISABLE_WP_CRON', true);"; | |
| const MYSQL_SOCKET = process.env.MYSQL_SOCKET || '/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/helpers/setup.ts` around lines 15 - 24, The tests currently use
hardcoded filesystem and socket paths (WP_ROOT, WP_CONFIG, WP_CONTENT,
MU_PLUGINS, LOGGER_SRC, LOGGER_DEST, AJAX_LOG, CRON_LINE, MYSQL_SOCKET) which
breaks portability; update setup.ts to read these values from environment
variables or a shared config with sensible defaults (e.g., process.env.WP_ROOT
|| fallback) and replace direct constants with those variables so local dev
machines and CI can override paths and the MySQL socket via env vars or a config
file.
| export function setup() { | ||
| // Authenticate by POSTing to wp-login.php | ||
| const loginPage = http.get(`${BASE_URL}/wp-login.php`); | ||
|
|
||
| const loginRes = http.post(`${BASE_URL}/wp-login.php`, { | ||
| log: WP_USER, | ||
| pwd: WP_PASS, | ||
| 'wp-submit': 'Log In', | ||
| redirect_to: `${BASE_URL}/wp-admin/`, | ||
| testcookie: '1', | ||
| }, { | ||
| redirects: 0, // Don't follow redirect, just capture cookies | ||
| }); | ||
|
|
||
| // Verify login succeeded (302 redirect to admin) | ||
| const loginOk = loginRes.status === 302 || loginRes.status === 200; | ||
| if (!loginOk) { | ||
| console.error(`Login failed with status ${loginRes.status}`); | ||
| } | ||
|
|
||
| return { loginOk }; | ||
| } |
There was a problem hiding this comment.
❓ Verification inconclusive
k6 setup function share cookies with default VUs
In k6, setup() can’t directly share its cookie jar with the VUs that run default(). setup() runs in its own transient VU / separate JS VM, and each VU has its own cookie jar. [1][2][3]
What to do instead
- Extract cookies in
setup()and return them as plain data, then set them into each VU’s jar at the start ofdefault():
import http from 'k6/http';
export function setup() {
const res = http.post('https://example.com/login', { user: 'u', pass: 'p' });
// capture cookies as data (not the jar)
const cookies = res.cookies; // or pick specific names from res.cookies[name][0].value
return { cookies };
}
export default function (data) {
const jar = http.cookieJar();
// set needed cookies for this VU (example for one cookie)
const session = data.cookies.sessionid[0].value;
jar.set('https://example.com', 'sessionid', session);
http.get('https://example.com/app'); // will send cookie from this VU’s jar
}-
If you need different sessions per VU, don’t do a single login in
setup()—have each VU authenticate (or return a list of sessions fromsetup()and index by VU id). [3] -
If your issue is cookies being cleared between iterations, consider the
noCookiesResetoption (it keeps a VU’s cookies across iterations), but it still won’t share cookies across VUs. [1][4]
Sources: per-VU cookie jar behavior [1]; setup() isolation / transient VU [2][3]; example approach of collecting cookies in setup() and reusing in VUs [5].
References:
[1] https://grafana.com/docs/k6/latest/using-k6/cookies/
[2] https://community.grafana.com/k6/t/k6-setup-function-issue/6436
[3] https://community.grafana.com/k6/t/k6-authorization-in-the-test-life-cycle/1094
[4] https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/
[5] https://community.grafana.com/t/common-storage-in-k6/96550
Cookies from setup() are not shared with VU iterations — each VU has its own isolated cookie jar.
In k6, setup() runs once in a transient VU before iterations begin, but its HTTP session is not automatically propagated to individual VUs in default(). Each VU will hit admin pages unauthenticated and likely receive redirects to wp-login.php rather than actual admin content.
To fix this, extract the authentication cookies in setup() and return them as data, then explicitly set them in each VU's jar:
// In setup()
const loginRes = http.post(`${BASE_URL}/wp-login.php`, { ... });
return { cookies: loginRes.cookies };
// In default(data)
const jar = http.cookieJar();
// Set the authentication cookies into this VU's jar
data.cookies.wordpress_logged_in[0].value; // extract and set as needed
http.get(`${BASE_URL}/wp-admin/`); // now authenticatedAlternatively, move authentication into default() so each VU authenticates itself.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/perf/geoip-load.js` around lines 71 - 92, The setup() function
currently authenticates once but does not propagate cookies to VU iterations;
capture the authentication cookies from loginRes in setup() (e.g., return
loginRes.cookies or a structured cookies object) and then in default(data) use
http.cookieJar() to set those cookies into the VU's jar before hitting admin
pages so each VU is authenticated; alternatively move the login logic from
setup() into default() so each VU performs its own POST to
`${BASE_URL}/wp-login.php` (refer to setup(), loginRes, loginOk, default(), and
http.cookieJar()).
| export function teardown(_data) { | ||
| // Try to read the AJAX log via direct HTTP (if accessible) | ||
| const logRes = http.get(`${BASE_URL}/wp-content/geoip-ajax-calls.log`); | ||
| if (logRes.status === 200 && logRes.body) { | ||
| const lines = logRes.body.trim().split('\n').filter((l) => l.length > 0); | ||
| console.log(`\n========================================`); | ||
| console.log(`GeoIP AJAX handler invocations: ${lines.length}`); | ||
| console.log(`Expected: 0-1 (fix working) or 0 (provider disabled)`); | ||
| console.log(`Old bug would produce: hundreds to thousands`); | ||
| console.log(`========================================\n`); | ||
|
|
||
| if (lines.length > 10) { | ||
| console.warn(`WARNING: ${lines.length} AJAX calls detected — possible regression!`); | ||
| } | ||
| } else { | ||
| console.log('Could not read AJAX log (may not be HTTP-accessible). Check file directly.'); | ||
| } |
There was a problem hiding this comment.
Exposing the AJAX log via HTTP is a security risk.
The teardown reads geoip-ajax-calls.log via HTTP. If the web server allows serving .log files from wp-content, this could expose test data. Ensure .log files are blocked by server configuration, or access the file directly via k6's file system APIs if running locally.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/perf/geoip-load.js` around lines 119 - 135, The teardown currently
fetches the AJAX log over HTTP using
http.get(`${BASE_URL}/wp-content/geoip-ajax-calls.log`) inside the teardown
function, which can expose .log files; change this to avoid HTTP exposure by
either (A) reading the log file directly with k6's file APIs (e.g., use open or
other local file-read mechanism at runtime) when tests run locally, replacing
the http.get call and subsequent log parsing, or (B) add a guard that requires
an explicit opt-in env flag before attempting HTTP access and otherwise log a
warning to check server config; update code references in teardown, the
BASE_URL/http.get usage, and the lines.length checks accordingly.
| WP_ROOT="/Users/parhumm/Local Sites/test/app/public" | ||
| WP_CONFIG="$WP_ROOT/wp-config.php" | ||
| MU_PLUGINS="$WP_ROOT/wp-content/mu-plugins" | ||
| AJAX_LOG="$WP_ROOT/wp-content/geoip-ajax-calls.log" | ||
| MYSQL_SOCKET="/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock" |
There was a problem hiding this comment.
Hardcoded paths make this script non-portable.
These paths are specific to the developer's local machine and will fail on any other environment. Consider using environment variables with fallback defaults:
🔧 Proposed fix for portability
-WP_ROOT="/Users/parhumm/Local Sites/test/app/public"
-WP_CONFIG="$WP_ROOT/wp-config.php"
-MU_PLUGINS="$WP_ROOT/wp-content/mu-plugins"
-AJAX_LOG="$WP_ROOT/wp-content/geoip-ajax-calls.log"
-MYSQL_SOCKET="/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock"
+WP_ROOT="${WP_ROOT:-/Users/parhumm/Local Sites/test/app/public}"
+WP_CONFIG="$WP_ROOT/wp-config.php"
+MU_PLUGINS="$WP_ROOT/wp-content/mu-plugins"
+AJAX_LOG="$WP_ROOT/wp-content/geoip-ajax-calls.log"
+MYSQL_SOCKET="${MYSQL_SOCKET:-/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| WP_ROOT="/Users/parhumm/Local Sites/test/app/public" | |
| WP_CONFIG="$WP_ROOT/wp-config.php" | |
| MU_PLUGINS="$WP_ROOT/wp-content/mu-plugins" | |
| AJAX_LOG="$WP_ROOT/wp-content/geoip-ajax-calls.log" | |
| MYSQL_SOCKET="/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock" | |
| WP_ROOT="${WP_ROOT:-/Users/parhumm/Local Sites/test/app/public}" | |
| WP_CONFIG="$WP_ROOT/wp-config.php" | |
| MU_PLUGINS="$WP_ROOT/wp-content/mu-plugins" | |
| AJAX_LOG="$WP_ROOT/wp-content/geoip-ajax-calls.log" | |
| MYSQL_SOCKET="${MYSQL_SOCKET:-/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock}" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/run-qa.sh` around lines 12 - 16, The script hardcodes machine-specific
paths (WP_ROOT, WP_CONFIG, MU_PLUGINS, AJAX_LOG, MYSQL_SOCKET) which breaks
portability; update each variable to read from an environment variable with a
sensible fallback default (e.g., WP_ROOT="${WP_ROOT:-./public}" style) and
derive related paths from WP_ROOT (so WP_CONFIG, MU_PLUGINS, AJAX_LOG are
constructed from WP_ROOT) and make MYSQL_SOCKET configurable via an env var
(MYSQL_SOCKET="${MYSQL_SOCKET:-/var/run/mysqld/mysqld.sock}") so other devs/CI
can override values without editing the script.
| sed -i '' "/That's all, stop editing/i\\ | ||
| define('DISABLE_WP_CRON', true); | ||
| " "$WP_CONFIG" |
There was a problem hiding this comment.
macOS-specific sed syntax.
The sed -i '' syntax is macOS-specific. On Linux, sed -i works without the empty string argument. This limits portability.
🔧 Cross-platform alternative
-sed -i '' "/That's all, stop editing/i\\
-define('DISABLE_WP_CRON', true);
-" "$WP_CONFIG"
+# Cross-platform sed in-place edit
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "/That's all, stop editing/i\\
+define('DISABLE_WP_CRON', true);
+" "$WP_CONFIG"
+else
+ sed -i "/That's all, stop editing/i\\define('DISABLE_WP_CRON', true);" "$WP_CONFIG"
+fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sed -i '' "/That's all, stop editing/i\\ | |
| define('DISABLE_WP_CRON', true); | |
| " "$WP_CONFIG" | |
| # Cross-platform sed in-place edit | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| sed -i '' "/That's all, stop editing/i\\ | |
| define('DISABLE_WP_CRON', true); | |
| " "$WP_CONFIG" | |
| else | |
| sed -i "/That's all, stop editing/i\\define('DISABLE_WP_CRON', true);" "$WP_CONFIG" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/run-qa.sh` around lines 98 - 100, The script uses a macOS-specific sed
invocation (the string starting with sed -i '' "/That's all, stop editing/i\\
define('DISABLE_WP_CRON', true);" "$WP_CONFIG") which breaks on Linux; replace
it with a portable approach such as using sed -i.bak to create a temporary
backup and then remove the .bak file (or detect OS and use -i '' on Darwin but
-i on others), so update the sed invocation in tests/run-qa.sh to use sed -i.bak
for the insert and then delete the backup to maintain cross-platform
compatibility.
Add 60s timeout to Test 6 (direct AJAX POST) to handle slow admin page loads. Remove flaky timestamp assertion from Test 4 (author) — prior admin test's async wp_safe_remote_post can set slimstat_last_geoip_dl after clearGeoipTimestamp(). The AJAX log assertion (0 entries) is the meaningful check proving the author didn't trigger the fallback.
QA Test Results — GeoIP Infinite AJAX Loop FixPlaywright E2E Tests: 6/6 PASSED
k6 Load Test: PASSED
Regression VerificationTemporarily removed the
This confirms the Test Infrastructure
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/TESTING.md`:
- Around line 87-96: The markdown in TESTING.md contains unlabeled code fences
for the directory trees (the blocks showing "tests/e2e/" and "wp-slimstat/");
update each opening triple-backtick to include the language label "text" (i.e.,
replace ``` with ```text) for the directory-tree fences referenced in the diff
and the additional similar block around lines 274–298 so markdownlint MD040 is
satisfied, keeping the rest of the fence content unchanged.
- Around line 27-30: The TESTING.md section currently hard-codes local test
credentials and a machine-specific MySQL socket (usernames 'parhumm' and
'dordane', password 'testpass123', and path under 'conf/mysql/mysqld.sock');
replace these with generic placeholders or references to environment variables
(e.g., MYSQL_USER, MYSQL_PASSWORD, MYSQL_SOCKET) and update the "Local by
Flywheel" entry to show how to populate those placeholders or read from env;
ensure you remove any absolute paths and instead instruct readers to locate
their socket (or set MYSQL_SOCKET) so no machine-specific data remains checked
in.
In `@tests/e2e/geoip-ajax-loop.spec.ts`:
- Around line 136-187: The test "Test 6: direct AJAX POST — no self-recursion"
is flaky because prior navigations to '/wp-admin/' and
'/wp-admin/admin.php?page=slimstat' can enqueue the same server-side fallback
after clearAjaxLog() runs; remove those priming navigations or ensure the
navigation-triggered handler has fully settled before clearing the log. To fix,
either (A) eliminate the page.goto calls and perform the authenticated POST
directly via page.request.post (keep action 'slimstat_update_geoip_database' and
security field) so clearAjaxLog() only clears pre-existing entries, or (B) if
you must navigate, after each page.goto call wait for background AJAX to finish
(e.g., poll readAjaxLog() until its length is stable for a short period) before
calling clearAjaxLog() and before performing the POST, then assert
expect(readAjaxLog().length).toBe(1). Ensure changes touch the test function and
use clearAjaxLog, clearGeoipTimestamp, page.request.post, readAjaxLog, and the
final expect call.
- Around line 46-57: The tests currently only assert an upper bound on
readAjaxLog() so a zero-invocation (fallback never firing) still passes; update
the e2e tests (the Test 1/2/3 cases that call readAjaxLog()) to first set the
GeoIP provider state to force the fallback path (e.g., disable the primary
provider or set the plugin/transient/config that triggers fallback) before
navigation, then assert a positive invocation count (use
expect(log.length).toBeGreaterThan(0) or an exact expected count) instead of
only toBeLessThanOrEqual(1), referencing the test blocks named "Test 1: single
admin page load ..." and the helper readAjaxLog() so the test guarantees the
fallback actually ran.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 357c9322-9efc-4765-a5d8-fb7ac53db301
📒 Files selected for processing (2)
docs/TESTING.mdtests/e2e/geoip-ajax-loop.spec.ts
| - **Local by Flywheel** running at `http://localhost:10003` | ||
| - PHP 8.x, MySQL accessible via socket | ||
| - WordPress users: `parhumm` (administrator), `dordane` (author) — both with password `testpass123` | ||
| - MySQL socket: found in Local's `conf/mysql/mysqld.sock` (currently `/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock`) |
There was a problem hiding this comment.
Remove checked-in local credentials and machine-specific paths.
Lines 27-30 hard-code test usernames, a shared password, and an absolute socket path from one developer machine. That makes the guide non-portable and leaks local account/path information into the repo; please switch these to placeholders or env vars.
Suggested doc change
-- **Local by Flywheel** running at `http://localhost:10003`
-- PHP 8.x, MySQL accessible via socket
-- WordPress users: `parhumm` (administrator), `dordane` (author) — both with password `testpass123`
-- MySQL socket: found in Local's `conf/mysql/mysqld.sock` (currently `/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock`)
+- **Local by Flywheel** running at your local site URL
+- PHP 8.x, MySQL accessible via socket
+- Create local administrator/author test users and provide credentials via env vars or a local, ignored config file
+- Set `MYSQL_SOCKET` to your Local socket path under `conf/mysql/mysqld.sock`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - **Local by Flywheel** running at `http://localhost:10003` | |
| - PHP 8.x, MySQL accessible via socket | |
| - WordPress users: `parhumm` (administrator), `dordane` (author) — both with password `testpass123` | |
| - MySQL socket: found in Local's `conf/mysql/mysqld.sock` (currently `/Users/parhumm/Library/Application Support/Local/run/X-JdmZXIa/mysql/mysqld.sock`) | |
| - **Local by Flywheel** running at your local site URL | |
| - PHP 8.x, MySQL accessible via socket | |
| - Create local administrator/author test users and provide credentials via env vars or a local, ignored config file | |
| - Set `MYSQL_SOCKET` to your Local socket path under `conf/mysql/mysqld.sock` |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/TESTING.md` around lines 27 - 30, The TESTING.md section currently
hard-codes local test credentials and a machine-specific MySQL socket (usernames
'parhumm' and 'dordane', password 'testpass123', and path under
'conf/mysql/mysqld.sock'); replace these with generic placeholders or references
to environment variables (e.g., MYSQL_USER, MYSQL_PASSWORD, MYSQL_SOCKET) and
update the "Local by Flywheel" entry to show how to populate those placeholders
or read from env; ensure you remove any absolute paths and instead instruct
readers to locate their socket (or set MYSQL_SOCKET) so no machine-specific data
remains checked in.
| ``` | ||
| tests/e2e/ | ||
| ├── playwright.config.ts # Config: baseURL, auth projects, timeouts | ||
| ├── global-setup.ts # Authenticates admin + author, caches auth state | ||
| ├── geoip-ajax-loop.spec.ts # Test suite (6 test cases) | ||
| ├── helpers/ | ||
| │ ├── setup.ts # wp-config toggler, mu-plugin manager, DB helper | ||
| │ └── ajax-logger-mu-plugin.php # PHP mu-plugin that logs AJAX handler calls | ||
| └── .auth/ # Cached browser auth state (gitignored) | ||
| ``` |
There was a problem hiding this comment.
Add a language to the directory-tree code fences.
These two fences are unlabeled, which is why markdownlint is raising MD040 here. text is enough for both blocks.
Suggested doc change
-```
+```text
tests/e2e/
├── playwright.config.ts # Config: baseURL, auth projects, timeouts
...
-```
+```
-```
+```text
wp-slimstat/
├── composer.json # PHP test scripts (test:all, test:*)
...
-```
+```Also applies to: 274-298
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 87-87: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/TESTING.md` around lines 87 - 96, The markdown in TESTING.md contains
unlabeled code fences for the directory trees (the blocks showing "tests/e2e/"
and "wp-slimstat/"); update each opening triple-backtick to include the language
label "text" (i.e., replace ``` with ```text) for the directory-tree fences
referenced in the diff and the additional similar block around lines 274–298 so
markdownlint MD040 is satisfied, keeping the rest of the fence content
unchanged.
| test('Test 1: single admin page load triggers at most 1 AJAX call', async ({ page }) => { | ||
| await page.goto('/wp-admin/'); | ||
| // Wait for the non-blocking wp_safe_remote_post to complete | ||
| await page.waitForTimeout(4000); | ||
|
|
||
| const log = readAjaxLog(); | ||
| // The key assertion: at most 1 AJAX call, not a cascade | ||
| expect(log.length).toBeLessThanOrEqual(1); | ||
| // Note: slimstat_last_geoip_dl may NOT be set if the download fails | ||
| // (no GeoIP license key configured). That's OK — we're testing the | ||
| // loop prevention, not the download success. | ||
| }); |
There was a problem hiding this comment.
Tests 1-3 can still pass when the fallback never fires.
These cases only check an upper bound on log.length, so 0 is treated as success. That means a regression that disables the GeoIP fallback entirely, or an environment with the provider already disabled, still passes the suite. For the positive-path scenarios, set a provider state that must trigger the fallback and assert an actual invocation count.
Also applies to: 59-83, 85-110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/geoip-ajax-loop.spec.ts` around lines 46 - 57, The tests currently
only assert an upper bound on readAjaxLog() so a zero-invocation (fallback never
firing) still passes; update the e2e tests (the Test 1/2/3 cases that call
readAjaxLog()) to first set the GeoIP provider state to force the fallback path
(e.g., disable the primary provider or set the plugin/transient/config that
triggers fallback) before navigation, then assert a positive invocation count
(use expect(log.length).toBeGreaterThan(0) or an exact expected count) instead
of only toBeLessThanOrEqual(1), referencing the test blocks named "Test 1:
single admin page load ..." and the helper readAjaxLog() so the test guarantees
the fallback actually ran.
| test('Test 6: direct AJAX POST — no self-recursion', async ({ page }) => { | ||
| test.setTimeout(60_000); | ||
| // First, visit an admin page to get a valid nonce | ||
| await page.goto('/wp-admin/'); | ||
| await page.waitForTimeout(1000); | ||
| clearAjaxLog(); // Clear any AJAX from the page load itself | ||
|
|
||
| // Extract nonce from the page — SlimStat injects it | ||
| // We'll generate one by hitting a page that outputs wpnonce | ||
| const nonce = await page.evaluate(async () => { | ||
| // Fetch a fresh nonce via admin-ajax approach | ||
| const res = await fetch('/wp-admin/admin-ajax.php', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
| body: 'action=slimstat_update_geoip_database&security=invalid_nonce', | ||
| }); | ||
| return null; // We'll use a different approach | ||
| }); | ||
|
|
||
| // Direct POST using Playwright's request context (carries auth cookies) | ||
| // The nonce check will fail with invalid nonce, but the handler IS invoked | ||
| // (check_ajax_referer dies on failure, so the mu-plugin at priority 1 | ||
| // runs BEFORE the die). Actually, check_ajax_referer calls wp_die on failure. | ||
| // The mu-plugin runs at priority 1, before SlimStat's handler. | ||
| // But wp_ajax_ fires both — the action fires, then SlimStat's callback runs. | ||
|
|
||
| // Actually, for a valid test we need a real nonce. | ||
| // Let's get it by parsing a page that contains it. | ||
| await clearGeoipTimestamp(); | ||
| clearAjaxLog(); | ||
|
|
||
| // Navigate to an admin page where SlimStat might inject the nonce | ||
| await page.goto('/wp-admin/admin.php?page=slimstat'); | ||
| await page.waitForTimeout(1000); | ||
| clearAjaxLog(); // Clear page-load AJAX | ||
|
|
||
| // Use page.request (inherits browser cookies) to POST directly | ||
| const response = await page.request.post('/wp-admin/admin-ajax.php', { | ||
| form: { | ||
| action: 'slimstat_update_geoip_database', | ||
| security: 'test_invalid', // Will fail nonce, but tests the path | ||
| }, | ||
| }); | ||
|
|
||
| await page.waitForTimeout(2000); | ||
|
|
||
| const log = readAjaxLog(); | ||
| // The mu-plugin fires at priority 1 (before nonce check at priority 10) | ||
| // so we should see exactly 1 entry even if the nonce fails | ||
| expect(log.length).toBe(1); | ||
| // The important thing: it's 1, not 2+. No self-recursion. | ||
| }); |
There was a problem hiding this comment.
The direct-POST test is still contaminated by its own preflight navigations.
This test hits /wp-admin/ and /wp-admin/admin.php?page=slimstat before the POST, and either page load can enqueue the same server-side fallback you're trying to isolate. Because that call is async, it can land after clearAjaxLog() and make the final expect(log.length).toBe(1) flaky. The most reliable fix is to skip the priming navigations and issue the authenticated admin-ajax.php POST directly, or wait for the navigation-triggered handler to settle before clearing the log.
Suggested simplification
- // First, visit an admin page to get a valid nonce
- await page.goto('/wp-admin/');
- await page.waitForTimeout(1000);
- clearAjaxLog(); // Clear any AJAX from the page load itself
-
- // Extract nonce from the page — SlimStat injects it
- // We'll generate one by hitting a page that outputs wpnonce
- const nonce = await page.evaluate(async () => {
- const res = await fetch('/wp-admin/admin-ajax.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: 'action=slimstat_update_geoip_database&security=invalid_nonce',
- });
- return null;
- });
-
await clearGeoipTimestamp();
clearAjaxLog();
-
- await page.goto('/wp-admin/admin.php?page=slimstat');
- await page.waitForTimeout(1000);
- clearAjaxLog(); // Clear page-load AJAX
const response = await page.request.post('/wp-admin/admin-ajax.php', {
form: {
action: 'slimstat_update_geoip_database',
security: 'test_invalid',
},
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/geoip-ajax-loop.spec.ts` around lines 136 - 187, The test "Test 6:
direct AJAX POST — no self-recursion" is flaky because prior navigations to
'/wp-admin/' and '/wp-admin/admin.php?page=slimstat' can enqueue the same
server-side fallback after clearAjaxLog() runs; remove those priming navigations
or ensure the navigation-triggered handler has fully settled before clearing the
log. To fix, either (A) eliminate the page.goto calls and perform the
authenticated POST directly via page.request.post (keep action
'slimstat_update_geoip_database' and security field) so clearAjaxLog() only
clears pre-existing entries, or (B) if you must navigate, after each page.goto
call wait for background AJAX to finish (e.g., poll readAjaxLog() until its
length is stable for a short period) before calling clearAjaxLog() and before
performing the POST, then assert expect(readAjaxLog().length).toBe(1). Ensure
changes touch the test function and use clearAjaxLog, clearGeoipTimestamp,
page.request.post, readAjaxLog, and the final expect call.
- Combine identical cloudflare/disable branches in admin.js - Migrate isJsDelivrEnabled() to use resolver instead of legacy field - Add explicit return after wp_send_json_error/success in AJAX handlers - Reorder fallback GeoIP block: cheap time check before expensive DB check - Remove dead nonce code in E2E Test 6
Code Review: Simplify Pass + Manual QARan a /simplify review (code reuse, quality, efficiency) followed by manual browser testing of all geolocation provider types. Fixes Applied (committed to branch)
Test ResultsPHP Unit Tests — 67/67 assertions passed
Manual Browser QA — All provider types verified
Database Operations (DB-IP)
Noted (not fixed — lower priority / larger refactor)
|
k6 Load Test + Country Tracking Verificationk6 Load Test — ALL THRESHOLDS PASSED
AJAX handler invocations during load: 1,495 — bounded race condition under 20-50 RPS concurrent load. NOT the infinite loop (old bug would produce 10,000+ with cascading growth). Count is bounded by concurrent requests, not multiplying. Country Tracking — VERIFIED WORKINGDB-IP city database: 131MB, present and freshly updated. Historical data shows correct resolution across 20+ countries:
Recent local dev hits correctly show NULL country (localhost cannot be geolocated — expected). Provider UI VerificationAll four providers tested in browser with correct row visibility and save/reload persistence:
|
- Fresh installs now default geolocation_provider to 'dbip' (get_fresh_defaults) - Processor uses resolve_geolocation_provider() directly, supports all providers - GeoService guards checkDatabase/deleteDatabaseFile for non-DB providers - Cloudflare region→subdivision mapping fixed for city precision - isGeoIPEnabled() no longer excludes Cloudflare - Add DB_PROVIDERS constant, centralize precision via get_geolocation_precision() - Add 8 E2E tests covering fresh install, legacy upgrades, all providers - Add option-mutator mu-plugin for safe serialized option manipulation in tests
Test Results — Geolocation Pipeline Fix (b4a6e3b)PHP Unit Tests — 67/67 assertions
E2E Geolocation Provider Pipeline — 8/8 Pass
E2E GeoIP AJAX Loop Regression — 6/6 Pass
Total: 81 assertions + 14 E2E scenarios — all passing. |
Manual QA ChecklistA. Fresh Install (no prior SlimStat data)
B. Legacy Upgrade (existing
|
- Add Admin Bar Stats to main features (new in 5.4.0) - Add Customize Reports to README.md (was missing) - Update GDPR bullet: WP Consent API only + WPML/Polylang support - Fix World Map credit: amMap → JQVMap (changed in 5.0.2) - Update Pro: Email Reports with column mappings, add User Avatars - Fix README.md: update release notes link, WordPress requirement
QA: Upgrade Data Integrity — 7/7 PASS ✅Verified that existing user tracking data remains intact after updating with this PR's geolocation refactor. Tests Run
Key Assertions
Safety
Environment: wp-slimstat 5.4.1 + wp-slimstat-pro 1.2.0, WP 6.9.3, Local by Flywheel Generated by |
Summary
Closes #164
This PR fixes the infinite GeoIP AJAX loop when WP-Cron is disabled, wires the Cloudflare geolocation provider into the tracking pipeline, and fixes several related geolocation bugs that affected fresh installs, legacy upgrades, and admin page stability.
What changed for users
Fresh installs
Existing installs (upgrading)
enable_maxmindsetting are automatically migrated to the newgeolocation_providersettingenable_maxmind=onmaps tomaxmind,enable_maxmind=nomaps todbip,enable_maxmind=disablemaps todisableCloudflare users
Sites with WP-Cron disabled
Admin stability
Bugs fixed
initwhen cron disabledis_admin() && !wp_doing_ajax() && current_user_can()slimstat_last_geoip_dltimestampupdate_option('slimstat_last_geoip_dl', time())on success?? 'maxmind'/?? 'dbip')resolve_geolocation_provider()geolocation_providerdefaultget_fresh_defaults()setsgeolocation_provider=dbipisGeoIPEnabled()excluded Cloudflare'maxmind'or'dbip'resolve_geolocation_provider()directlycheckDatabase()/deleteDatabaseFile()calledgetDbPath()on CloudflareDB_PROVIDERSconstantregionkey, Processor readssubdivisionregionmapped tosubdivisionwp_send_json_successenable_maxmindnot migrated on upgraderesolve_geolocation_provider()Code architecture changes
resolve_geolocation_provider()— single source of truth for active provider, handles legacy migrationget_fresh_defaults()— centralized install defaults, used in bothinit()and admin resetget_geolocation_precision()— eliminates duplicated country/city precision logicDB_PROVIDERSconstant —['maxmind', 'dbip']used everywhere to guard DB-specific operationsGeoServiceobject instantiation per pageviewFiles changed (33 files, +2411 / -110)
Core plugin:
wp-slimstat.php—resolve_geolocation_provider(),get_fresh_defaults(),get_geolocation_precision(), init() fixsrc/Tracker/Processor.php— Direct resolver usage, removed GeoService dependencysrc/Services/GeoService.php—DB_PROVIDERSconstant, guards, fixedisGeoIPEnabled()src/Services/Geolocation/Provider/CloudflareGeolocationProvider.php— region to subdivision mappingAdmin:
admin/index.php— Provider-aware AJAX handlers, DB_PROVIDERS guardsadmin/config/index.php— Lazy migration, save sync, reset usesget_fresh_defaults()admin/view/index.php— Provider-aware GeoIP noticeadmin/view/wp-slimstat-reports.php— Provider-aware GeoIP noticeTests (new):
Test results
All tests passing — see test results comment for details.