Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 '{
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions simple-analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -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');
Expand Down
18 changes: 17 additions & 1 deletion src/Actions/AddInactiveComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<!-- Simple Analytics: Not logging requests from admins -->\n";
$reason = $this->triggeredRule !== '' ? $this->triggeredRule : 'Unknown Rule';

echo sprintf(
"<!-- Simple Analytics: Script not included because this visitor is excluded by tracking rule: %s -->\n",
\esc_html($reason)
);
}
}
7 changes: 5 additions & 2 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
16 changes: 13 additions & 3 deletions src/ScriptRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<!-- Simple Analytics - 100% privacy-first analytics (official WordPress plugin) -->\n" . $updatedTag;
}

return $updatedTag;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/TrackingRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
72 changes: 62 additions & 10 deletions src/UI/PageLayoutComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,7 +47,7 @@ public function __invoke(): void
<div class="flex items-center">
<!-- Logo -->
<a
href="https://dashboard.simpleanalytics.com/websites"
href="<?php echo esc_url(self::DASHBOARD_URL); ?>"
target="_blank"
class="text-base font-semibold leading-6 text-gray-900"
>
Expand All @@ -56,7 +59,7 @@ class="mr-2 inline-block h-10 w-auto text-primary"
</a>
<!-- "Open Dashboard" link -->
<a
href="https://dashboard.simpleanalytics.com/websites"
href="<?php echo esc_url(self::DASHBOARD_URL); ?>"
target="_blank"
class="inline-flex items-center rounded bg-white px-2 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
Expand All @@ -75,17 +78,22 @@ class="inline-flex items-center rounded bg-white px-2 py-1 text-xs font-semibold
<!-- Fields / Layout -->
<div class="mx-auto max-w-3xl bg-white px-4 py-6 sm:px-4 lg:px-0">
<div class="border-b border-gray-900/10 pb-7">
<?php if ($currentTab->getSlug() === 'general'): ?>
<?php $this->renderGeneralTabIntro(); ?>
<?php endif; ?>
<?php $currentTab->render(); ?>
</div>

<div class="mt-6 flex items-center justify-start gap-x-6">
<button
type="submit"
class="rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm bg-primary hover:bg-red-500 focus-visible:outline-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
>
Save Changes
</button>
</div>
<?php if ($currentTab->getSlug() !== 'general'): ?>
<div class="mt-6 flex items-center justify-start gap-x-6">
<button
type="submit"
class="rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm bg-primary hover:bg-red-500 focus-visible:outline-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
>
Save Changes
</button>
</div>
<?php endif; ?>
</div>
</form>
<script>
Expand Down Expand Up @@ -140,4 +148,48 @@ protected function findTabBySlug(array $tabs, string $slug): ?Tab

return null;
}

protected function renderGeneralTabIntro(): void
{
?>
<div class="mb-7" style="max-width: 64ch;">
<p class="text-sm text-gray-700">
Simple Analytics is now added to your WordPress site.
</p>
<p class="mt-4 text-sm text-gray-700">
The plugin collects pageviews in a privacy-first way, without cookies or personal data.
</p>
<p class="mt-4 text-sm text-gray-700">
Your stats will appear in
<a class="text-primary hover:underline" target="_blank" href="<?php echo esc_url(self::DASHBOARD_URL); ?>">
the Simple Analytics dashboard
</a>
within a few minutes.
</p>
<p class="mt-4 text-sm text-gray-700">
To avoid tracking your own visits, go to the "Ignore Rules" tab and ignore visits from logged-in admins.
You can also add your own IP address there.
</p>
<p class="mt-4 text-sm text-gray-700">
To automatically track downloads, outbound links, and email clicks, go to the "Events" tab and enable "Collect automated events".
</p>
<p class="mt-4 text-sm text-gray-700">
No account yet? Create one at
<a class="text-primary hover:underline" target="_blank" href="<?php echo esc_url(self::SIGNUP_URL); ?>">
simpleanalytics.com.
</a>
You can start with a free trial and choose a free or paid plan later.
</p>
<p class="mt-6">
<a
href="<?php echo esc_url(self::DASHBOARD_URL); ?>"
target="_blank"
class="inline-flex items-center rounded bg-primary px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500"
>
Open dashboard
</a>
</p>
</div>
<?php
}
}
63 changes: 55 additions & 8 deletions tests/Browser/pluginSettings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ 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 = '<!-- Simple Analytics: Not logging requests from admins -->';
const DASHBOARD_URL =
'https://dashboard.simpleanalytics.com/?utm_source=wordpress&utm_medium=plugin&utm_content=go_to_dashboard_button';
const SIGNUP_URL =
'https://www.simpleanalytics.com/signup?utm_source=wordpress&utm_medium=plugin&utm_content=signup_link';
const SCRIPT_PREFIX_COMMENT = '<!-- Simple Analytics - 100% privacy-first analytics (official WordPress plugin) -->';
const INACTIVE_COMMENT_PREFIX = '<!-- Simple Analytics: Script not included because this visitor is excluded by tracking rule:';
const INACTIVE_USER_ROLE_COMMENT = '<!-- Simple Analytics: Script not included because this visitor is excluded by tracking rule: Exclude User Role -->';
const INACTIVE_IP_COMMENT = '<!-- Simple Analytics: Script not included because this visitor is excluded by tracking rule: Exclude IP Address -->';

async function loginAs(page: Page, username: string, password: string) {
await page.goto('/wp-login.php');
Expand Down Expand Up @@ -36,15 +43,33 @@ async function visitAsGuest(browser: Browser, path = '/'): Promise<Page> {

test('adds a script by default', async ({ page, browser }) => {
await asAdmin(page);
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=general');
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=advanced');
await page.fill('[name="simpleanalytics_custom_domain"]', '');
await saveSettings(page);

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();
});

test('shows guidance on general tab and keeps custom domain in advanced tab', async ({ page }) => {
await asAdmin(page);
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=general');

await expect(page.getByText('Simple Analytics is now added to your WordPress site.')).toBeVisible();
await expect(page.getByText('without cookies or personal data')).toBeVisible();
await expect(page.getByRole('link', { name: 'the Simple Analytics dashboard' })).toHaveAttribute('href', DASHBOARD_URL);
await expect(page.getByRole('link', { name: 'simpleanalytics.com.' })).toHaveAttribute('href', SIGNUP_URL);
await expect(page.getByRole('link', { name: 'Open dashboard', exact: true })).toHaveAttribute('href', DASHBOARD_URL);
await expect(page.getByRole('link', { name: 'Open Dashboard', exact: true })).toHaveAttribute('href', DASHBOARD_URL);
await expect(page.getByRole('button', { name: 'Save Changes' })).toHaveCount(0);
await expect(page.locator('[name="simpleanalytics_custom_domain"]')).toHaveCount(0);

await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=advanced');
await expect(page.locator('[name="simpleanalytics_custom_domain"]')).toBeVisible();
});

test('adds inactive script for authenticated users by default', async ({ page }) => {
await asAdmin(page);
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=ignore-rules');
Expand All @@ -54,8 +79,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 }) => {
Expand Down Expand Up @@ -88,18 +118,35 @@ 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();
const editorPage = await editorCtx.newPage();
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');
Expand Down Expand Up @@ -244,7 +291,7 @@ test('adds automated events script with override global', async ({ page, browser

test('adds a script with a custom domain name', async ({ page, browser }) => {
await asAdmin(page);
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=general');
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=advanced');
await page.fill('[name="simpleanalytics_custom_domain"]', 'mydomain.com');
await saveSettings(page);
await expect(page.locator('[name="simpleanalytics_custom_domain"]')).toHaveValue('mydomain.com');
Expand All @@ -253,7 +300,7 @@ test('adds a script with a custom domain name', async ({ page, browser }) => {
await expect(guest.locator('script[src="https://mydomain.com/latest.js"]')).toBeAttached();
await guest.context().close();

await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=general');
await page.goto('/wp-admin/options-general.php?page=simpleanalytics&tab=advanced');
await page.fill('[name="simpleanalytics_custom_domain"]', '');
await saveSettings(page);
});