Skip to content
Merged
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
193 changes: 193 additions & 0 deletions e2e/analytics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* Acceptance tests for the first-party analytics instrumentation.
*
* The real Plausible CDN script is blocked and replaced with a deterministic
* local probe so event requests can be asserted at the network boundary.
*/

import { test, expect, type Page, type Route } from '@playwright/test';

const ANALYTICS_ENDPOINT = '**/api/event**';
const PLAUSIBLE_SCRIPT = '**/plausible.io/js/**';

type PlausibleEvent = { name: string; props?: Record<string, unknown> };

async function setupAnalyticsProbe(page: Page): Promise<PlausibleEvent[]> {
const events: PlausibleEvent[] = [];

await page.route(PLAUSIBLE_SCRIPT, (route: Route) => route.abort());
await page.route(ANALYTICS_ENDPOINT, (route: Route) => {
const body = route.request().postData();
if (body) {
try {
events.push(JSON.parse(body) as PlausibleEvent);
} catch {
// Ignore non-JSON payloads from unrelated requests.
}
}
return route.fulfill({ status: 204 });
});

await page.addInitScript(() => {
(window as unknown as { plausible: unknown }).plausible = function (
name: string,
opts?: { props?: Record<string, unknown> },
) {
void fetch('https://plausible.io/api/event', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: JSON.stringify({
name,
domain: location.hostname,
props: opts?.props ?? {},
}),
keepalive: true,
});
};
});

return events;
}

async function enableDNT(page: Page): Promise<void> {
await page.addInitScript(() => {
Object.defineProperty(navigator, 'doNotTrack', {
configurable: true,
get: () => '1',
});
});
}

test.describe('first-party analytics', () => {
test('CTA click emits exactly one cta_click', async ({ page }) => {
const events = await setupAnalyticsProbe(page);
await page.route('**/console.usewraith.xyz/**', (route: Route) => route.abort());
await page.goto('/');

const ctaStrip = page.locator('section', { hasText: 'Start shipping private payments' });
const cta = ctaStrip.getByRole('link', { name: /get api keys/i }).first();
await cta.waitFor();
await cta.click();
await expect.poll(() => events.filter((event) => event.name === 'cta_click').length).toBe(1);

const ctaEvents = events.filter((event) => event.name === 'cta_click');
expect(ctaEvents[0]?.props?.source).toBe('ctastrip-console');
});

test('newsletter success emits exactly one newsletter_submit and no confirm', async ({
page,
}) => {
const events = await setupAnalyticsProbe(page);
await page.route('**/api/subscribe', (route: Route) =>
route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ ok: true }),
}),
);
await page.goto('/newsletter');

await page.locator('#newsletter-email').fill('reader@example.com');
const submit = page.getByRole('main').getByRole('button', { name: /subscribe/i });
await submit.click();
await expect
.poll(() => events.filter((event) => event.name === 'newsletter_submit').length)
.toBe(1);

await expect(submit).toBeHidden();
await page.waitForTimeout(100);

expect(events.filter((event) => event.name === 'newsletter_submit')).toHaveLength(1);
expect(events.filter((event) => event.name === 'newsletter_confirm')).toHaveLength(0);
});

test('blog_post_read fires once at/after 80% scroll', async ({ page }) => {
const events = await setupAnalyticsProbe(page);
await page.goto('/blog/wave-7-kickoff');
await page.getByRole('heading', { level: 1 }).first().waitFor();

await page.evaluate(() =>
window.scrollTo({ top: document.documentElement.scrollHeight * 0.4, behavior: 'instant' }),
);
await page.waitForTimeout(100);
expect(events.filter((event) => event.name === 'blog_post_read')).toHaveLength(0);

await page.evaluate(() =>
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }),
);
await expect
.poll(() => events.filter((event) => event.name === 'blog_post_read').length)
.toBe(1);

await page.evaluate(() =>
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }),
);
await page.waitForTimeout(100);
expect(events.filter((event) => event.name === 'blog_post_read')).toHaveLength(1);
});

test('calculator share emits exactly one calculator_share after a successful copy', async ({
page,
}) => {
const events = await setupAnalyticsProbe(page);
await page.goto('/use-cases/calculator');

await page.getByRole('button', { name: /copy scenario link/i }).click();
await expect(page.getByText(/scenario link copied/i)).toBeVisible();
await expect
.poll(() => events.filter((event) => event.name === 'calculator_share').length)
.toBe(1);

const shares = events.filter((event) => event.name === 'calculator_share');
expect(shares[0]?.props?.source).toBe('cost-calculator');
});

test('outbound click emits exactly one outbound_click with category', async ({ page }) => {
const events = await setupAnalyticsProbe(page);
await page.goto('/');

const github = page.getByRole('link', { name: /github/i }).first();
await github.click();
await expect
.poll(() => events.filter((event) => event.name === 'outbound_click').length)
.toBe(1);

const outbound = events.filter((event) => event.name === 'outbound_click');
expect(outbound[0]?.props?.category).toBe('github');
});

test('DNT enabled => zero Plausible script and analytics requests', async ({ page }) => {
await enableDNT(page);
const analyticsRequests: string[] = [];
const scriptRequests: string[] = [];

page.on('request', (request) => {
const url = request.url();
if (url.includes('plausible.io/js/')) scriptRequests.push(url);
if (url.includes('/api/event')) analyticsRequests.push(url);
});

await page.goto('/');
await page
.getByRole('link', { name: /github/i })
.first()
.click()
.catch(() => {});
await page.goto('/newsletter');
await page.locator('#newsletter-email').fill('reader@example.com');
await page
.getByRole('main')
.getByRole('button', { name: /subscribe/i })
.click()
.catch(() => {});
await page.goto('/use-cases/calculator');
await page
.getByRole('button', { name: /copy scenario link/i })
.click()
.catch(() => {});
await page.waitForTimeout(200);

expect(scriptRequests).toHaveLength(0);
expect(analyticsRequests).toHaveLength(0);
});
});
37 changes: 22 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,30 @@

<!--
Plausible Analytics — cookieless, GDPR-compliant, no consent banner required.
• No cookies are set; only aggregate page/event counts are stored.
• script.scroll.js extension tracks scroll depth at every percent automatically.
• script.tagged-events.js lets us fire goals from JS via window.plausible().
• Integrity hash pins the exact script bytes served by plausible.io CDN.
• Replace the integrity value if you upgrade to a newer Plausible release.
The loader is gated by DNT/GPC so privacy signals suppress both the analytics
script request and all subsequent page/event requests.
-->
<script
defer
data-domain="usewraith.xyz"
src="https://plausible.io/js/script.scroll.tagged-events.js"
></script>
<script>
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
(function () {
const nav = window.navigator || {};
const dnt = nav.doNotTrack || window.doNotTrack;
const gpc = nav.globalPrivacyControl === true;
const privacyOptOut = dnt === '1' || dnt === 'yes' || gpc;

if (privacyOptOut) return;

window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};

const script = document.createElement('script');
script.defer = true;
script.dataset.domain = 'usewraith.xyz';
script.src = 'https://plausible.io/js/script.scroll.tagged-events.js';
document.head.appendChild(script);
})();
</script>

<link
Expand Down
22 changes: 11 additions & 11 deletions public/feed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,32 @@
<link>https://usewraith.xyz/blog</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Tue, 25 Aug 2026 22:23:09 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed.xml" rel="self" type="application/rss+xml" />

<item>
<title>How Stealth Addresses Keep Payments Private</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
<description>A look at the cryptography behind stealth addresses and why every Wraith payment lands on a fresh one-time address that cannot be linked to the recipient.</description>
<author>Lena Vogt</author>
</item>
<item>
<title>Wave 7 Kick-off + What We Shipped in Wave 6</title>
<link>https://usewraith.xyz/blog/wave-7-kickoff</link>
<guid>https://usewraith.xyz/blog/wave-7-kickoff</guid>
<pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
<description>Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.</description>
<author>Wraith Protocol Team</author>
</item>
<item>
<title>Stealth addresses explained</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 22 Jul 2026 12:00:00 GMT</pubDate>
<description>A straightforward introduction to stealth addresses and why they matter.</description>
<author>Wraith Team</author>
</item>
<item>
<title>Privacy by default</title>
<link>https://usewraith.xyz/blog/privacy-by-default</link>
<guid>https://usewraith.xyz/blog/privacy-by-default</guid>
<pubDate>Mon, 20 Jul 2026 12:00:00 GMT</pubDate>
<pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate>
<description>How Wraith makes private payments practical for everyday apps.</description>
<author>Wraith Team</author>
<author>Wraith Protocol Team</author>
</item>
</channel>
</rss>
4 changes: 2 additions & 2 deletions public/feed/tag/announcements.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link>https://usewraith.xyz/blog/tag/announcements</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Tue, 25 Aug 2026 05:06:07 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed/tag/announcements.xml" rel="self" type="application/rss+xml" />

<item>
Expand All @@ -14,7 +14,7 @@
<guid>https://usewraith.xyz/blog/wave-7-kickoff</guid>
<pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
<description>Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.</description>
<author>Wraith Protocol Team</author>
<author>Wraith Team</author>
</item>
</channel>
</rss>
2 changes: 1 addition & 1 deletion public/feed/tag/cryptography.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link>https://usewraith.xyz/blog/tag/cryptography</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Thu, 27 Aug 2026 12:50:19 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed/tag/cryptography.xml" rel="self" type="application/rss+xml" />

<item>
Expand Down
10 changes: 5 additions & 5 deletions public/feed/tag/privacy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
<link>https://usewraith.xyz/blog/tag/privacy</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Tue, 25 Aug 2026 05:06:07 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed/tag/privacy.xml" rel="self" type="application/rss+xml" />

<item>
<title>Stealth addresses explained</title>
<title>How Stealth Addresses Keep Payments Private</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
<description>A straightforward introduction to stealth addresses and why they matter for private payments.</description>
<author>Wraith Protocol Team</author>
<pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
<description>A look at the cryptography behind stealth addresses and why every Wraith payment lands on a fresh one-time address that cannot be linked to the recipient.</description>
<author>Lena Vogt</author>
</item>
<item>
<title>Privacy by default</title>
Expand Down
12 changes: 2 additions & 10 deletions public/feed/tag/sdk.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link>https://usewraith.xyz/blog/tag/sdk</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Tue, 25 Aug 2026 05:06:07 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed/tag/sdk.xml" rel="self" type="application/rss+xml" />

<item>
Expand All @@ -14,15 +14,7 @@
<guid>https://usewraith.xyz/blog/wave-7-kickoff</guid>
<pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
<description>Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.</description>
<author>Wraith Protocol Team</author>
</item>
<item>
<title>Stealth addresses explained</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
<description>A straightforward introduction to stealth addresses and why they matter for private payments.</description>
<author>Wraith Protocol Team</author>
<author>Wraith Team</author>
</item>
</channel>
</rss>
20 changes: 10 additions & 10 deletions public/feed/tag/stealth-payments.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@
<link>https://usewraith.xyz/blog/tag/stealth-payments</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Tue, 25 Aug 2026 05:06:07 GMT</lastBuildDate>
<lastBuildDate>Sat, 29 Aug 2026 13:32:10 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed/tag/stealth-payments.xml" rel="self" type="application/rss+xml" />

<item>
<title>How Stealth Addresses Keep Payments Private</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
<description>A look at the cryptography behind stealth addresses and why every Wraith payment lands on a fresh one-time address that cannot be linked to the recipient.</description>
<author>Lena Vogt</author>
</item>
<item>
<title>Wave 7 Kick-off + What We Shipped in Wave 6</title>
<link>https://usewraith.xyz/blog/wave-7-kickoff</link>
<guid>https://usewraith.xyz/blog/wave-7-kickoff</guid>
<pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
<description>Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.</description>
<author>Wraith Protocol Team</author>
</item>
<item>
<title>Stealth addresses explained</title>
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
<pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate>
<description>A straightforward introduction to stealth addresses and why they matter for private payments.</description>
<author>Wraith Protocol Team</author>
<author>Wraith Team</author>
</item>
<item>
<title>Privacy by default</title>
Expand Down
Loading
Loading