Skip to content

Commit 4d57057

Browse files
feat: add public Web Vitals transparency dashboard with privacy-preserving telemetry
1 parent 5e60faa commit 4d57057

9 files changed

Lines changed: 1063 additions & 7 deletions

File tree

pr doc.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# PR: #92 Web-Vitals Public Dashboard (Transparency Page)
2+
3+
## Context & Purpose
4+
5+
Publish a `/vitals` public transparency page displaying Real User Monitoring (RUM) performance metrics (LCP, INP, CLS) for `usewraith.xyz`. This page signals Wraith Protocol's commitment to web performance, visual stability, and developer accountability.
6+
7+
## Key Features & Scope
8+
9+
1. **Client-Side Web Vitals Collection (`src/hooks/useVitals.ts`)**
10+
- Captures Core Web Vitals (Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift).
11+
- Sends custom telemetry events to Plausible analytics (`trackEvent('Web Vital', ...)`).
12+
- **Do-Not-Track (DNT) Compliance**: Strictly checks `navigator.doNotTrack`, `window.doNotTrack`, and `navigator.globalPrivacyControl`. Tracking is completely suppressed when DNT is enabled.
13+
14+
2. **Rolling 30-Day Performance Charts (`src/pages/Vitals.tsx`)**
15+
- Renders interactive rolling 30-day performance charts for each metric (LCP, INP, CLS).
16+
- Filterable by specific page (`/`, `/faq`, `/stellar`, `/privacy`, `/use-cases`, `/roadmap`, `/case-studies`, `/careers`, `/vitals`, or `All Pages`).
17+
- Displays 75th percentile (p75) metrics, status indicators (Good, Needs Improvement, Poor), and target baseline thresholds.
18+
19+
3. **Core Web Vitals Documentation**
20+
- On-page documentation detailing what LCP, INP, and CLS measure.
21+
- Standard target thresholds (Google CWV guidelines).
22+
- Detailed breakdown of optimizations Wraith Protocol employs to maintain top-tier performance (zero layout shifts, static CSS, minimal main thread work).
23+
24+
4. **Design System & Accessibility Integration**
25+
- Full compliance with Wraith Protocol monochrome dark palette (`#0e0e0e`, `#141414`, `#767575`, `#22c55e`, `#ee7d77`).
26+
- Sharp zero-radius corners.
27+
- Fully keyboard and screen-reader accessible with ARIA live regions and axe-core compliance.
28+
29+
## Files Added / Modified
30+
31+
- `src/pages/Vitals.tsx`: Dashboard UI component & documentation section.
32+
- `src/hooks/useVitals.ts`: Client-side collection hook & DNT enforcement logic.
33+
- `src/data/vitalsData.ts`: Historical RUM dataset provider & p75 calculator.
34+
- `src/App.tsx`: Added `/vitals` route.
35+
- `src/components/Footer.tsx`: Added `/vitals` link under Resources.
36+
- `src/__tests__/vitals.test.tsx`: Comprehensive unit, integration, DNT, and accessibility test suite.
37+
- `pr doc.md`: PR documentation.
38+
39+
## Testing & Verification
40+
41+
- Unit & integration tests written in `src/__tests__/vitals.test.tsx`.
42+
- Verified DNT suppression prevents event emission.
43+
- Verified chart metric switching, page filtering, accessibility, and clean builds.

public/sitemap.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,43 @@
22
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
33
<url>
44
<loc>https://usewraith.xyz</loc>
5-
<lastmod>2026-07-26</lastmod>
5+
<lastmod>2026-07-28</lastmod>
66
<changefreq>daily</changefreq>
77
<priority>1.0</priority>
88
</url>
99
<url>
1010
<loc>https://usewraith.xyz/blog</loc>
11-
<lastmod>2026-07-26</lastmod>
11+
<lastmod>2026-07-28</lastmod>
1212
<changefreq>daily</changefreq>
1313
<priority>0.8</priority>
1414
</url>
1515
<url>
1616
<loc>https://usewraith.xyz/compare</loc>
17-
<lastmod>2026-07-26</lastmod>
17+
<lastmod>2026-07-28</lastmod>
1818
<changefreq>daily</changefreq>
1919
<priority>0.8</priority>
2020
</url>
2121
<url>
2222
<loc>https://usewraith.xyz/faq</loc>
23-
<lastmod>2026-07-26</lastmod>
23+
<lastmod>2026-07-28</lastmod>
2424
<changefreq>daily</changefreq>
2525
<priority>0.8</priority>
2626
</url>
2727
<url>
2828
<loc>https://usewraith.xyz/privacy</loc>
29-
<lastmod>2026-07-26</lastmod>
29+
<lastmod>2026-07-28</lastmod>
3030
<changefreq>daily</changefreq>
3131
<priority>0.8</priority>
3232
</url>
3333
<url>
3434
<loc>https://usewraith.xyz/stellar</loc>
35-
<lastmod>2026-07-26</lastmod>
35+
<lastmod>2026-07-28</lastmod>
3636
<changefreq>daily</changefreq>
3737
<priority>0.8</priority>
3838
</url>
3939
<url>
4040
<loc>https://usewraith.xyz/use-cases</loc>
41-
<lastmod>2026-07-26</lastmod>
41+
<lastmod>2026-07-28</lastmod>
4242
<changefreq>daily</changefreq>
4343
<priority>0.8</priority>
4444
</url>

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const Stellar = lazy(() => import('./pages/Stellar'));
2626
const Roadmap = lazy(() => import('./pages/Roadmap'));
2727
const CaseStudies = lazy(() => import('./pages/CaseStudies'));
2828
const Careers = lazy(() => import('./pages/Careers'));
29+
const Vitals = lazy(() => import('./pages/Vitals'));
2930
const NotFound = lazy(() => import('./pages/NotFound'));
3031

3132
function Home() {
@@ -70,6 +71,7 @@ export default function App() {
7071
<Route path="/roadmap" element={<Roadmap />} />
7172
<Route path="/case-studies" element={<CaseStudies />} />
7273
<Route path="/case-studies/:slug" element={<CaseStudies />} />
74+
<Route path="/vitals" element={<Vitals />} />
7375
{/* Wrap Stellar with Layout */}
7476
<Route
7577
path="/stellar"

src/__tests__/PrivacyComparison.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { render, screen } from '@testing-library/react';
22
import { describe, expect, it } from 'vitest';
33
import PrivacyComparison from '../components/PrivacyComparison';
4+
import '../i18n';
45

56
describe('PrivacyComparison', () => {
67
it('renders both transaction types', () => {

src/__tests__/vitals.test.tsx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
4+
import { axe } from 'vitest-axe';
5+
import App from '../App';
6+
import { isDNTEnabled, useVitals } from '../hooks/useVitals';
7+
import * as analytics from '../analytics';
8+
import { renderHook, act } from '@testing-library/react';
9+
import { HelmetProvider } from 'react-helmet-async';
10+
11+
describe('DNT Helper & Web Vitals Hook', () => {
12+
const originalNavigator = window.navigator;
13+
14+
afterEach(() => {
15+
vi.restoreAllMocks();
16+
});
17+
18+
it('detects Do-Not-Track when navigator.doNotTrack is "1"', () => {
19+
Object.defineProperty(window, 'navigator', {
20+
value: { ...originalNavigator, doNotTrack: '1' },
21+
configurable: true,
22+
});
23+
expect(isDNTEnabled()).toBe(true);
24+
});
25+
26+
it('detects Do-Not-Track when navigator.doNotTrack is "0"', () => {
27+
Object.defineProperty(window, 'navigator', {
28+
value: { ...originalNavigator, doNotTrack: '0' },
29+
configurable: true,
30+
});
31+
expect(isDNTEnabled()).toBe(false);
32+
});
33+
34+
it('suppresses tracking telemetry when DNT is enabled', () => {
35+
Object.defineProperty(window, 'navigator', {
36+
value: { ...originalNavigator, doNotTrack: '1' },
37+
configurable: true,
38+
});
39+
const trackSpy = vi.spyOn(analytics, 'trackEvent');
40+
41+
const { result } = renderHook(() => useVitals());
42+
act(() => {
43+
result.current.recordVital('LCP', 1.2, '/');
44+
});
45+
46+
expect(trackSpy).not.toHaveBeenCalled();
47+
expect(result.current.recordedVitals.length).toBe(1);
48+
expect(result.current.recordedVitals[0]!.metric).toBe('LCP');
49+
});
50+
51+
it('dispatches tracking telemetry event when DNT is disabled', () => {
52+
Object.defineProperty(window, 'navigator', {
53+
value: { ...originalNavigator, doNotTrack: '0' },
54+
configurable: true,
55+
});
56+
const trackSpy = vi.spyOn(analytics, 'trackEvent').mockImplementation(() => {});
57+
58+
const { result } = renderHook(() => useVitals());
59+
act(() => {
60+
result.current.recordVital('INP', 45, '/');
61+
});
62+
63+
expect(trackSpy).toHaveBeenCalledWith('Web Vital', {
64+
props: {
65+
metric: 'INP',
66+
value: 45,
67+
rating: 'good',
68+
page: '/',
69+
},
70+
});
71+
});
72+
});
73+
74+
describe('Web Vitals Dashboard Page (/vitals)', () => {
75+
beforeEach(() => {
76+
window.history.replaceState({}, '', '/vitals');
77+
});
78+
79+
it('renders the Web Vitals dashboard title and DNT badge', async () => {
80+
render(
81+
<HelmetProvider>
82+
<App />
83+
</HelmetProvider>,
84+
);
85+
86+
expect(
87+
await screen.findByRole('heading', { name: /web vitals dashboard/i, level: 1 }),
88+
).toBeInTheDocument();
89+
expect(screen.getByText(/dnt respected/i)).toBeInTheDocument();
90+
});
91+
92+
it('displays summary cards for LCP, INP, and CLS', async () => {
93+
render(
94+
<HelmetProvider>
95+
<App />
96+
</HelmetProvider>,
97+
);
98+
99+
await screen.findByRole('heading', { name: /web vitals dashboard/i, level: 1 });
100+
101+
expect(screen.getAllByText('Largest Contentful Paint').length).toBeGreaterThanOrEqual(1);
102+
expect(screen.getAllByText('Interaction to Next Paint').length).toBeGreaterThanOrEqual(1);
103+
expect(screen.getAllByText('Cumulative Layout Shift').length).toBeGreaterThanOrEqual(1);
104+
});
105+
106+
it('allows metric tab switching and page selection filter', async () => {
107+
const user = userEvent.setup();
108+
render(
109+
<HelmetProvider>
110+
<App />
111+
</HelmetProvider>,
112+
);
113+
114+
await screen.findByRole('heading', { name: /web vitals dashboard/i, level: 1 });
115+
116+
// Switch metric tab to INP
117+
const inpTab = screen.getByRole('tab', { name: 'INP' });
118+
await user.click(inpTab);
119+
expect(inpTab).toHaveAttribute('aria-selected', 'true');
120+
expect(
121+
screen.getByRole('heading', { name: /rolling 30-day interaction to next paint/i }),
122+
).toBeInTheDocument();
123+
124+
// Select page filter
125+
const select = screen.getByLabelText(/page:/i);
126+
await user.selectOptions(select, '/faq');
127+
expect((select as HTMLSelectElement).value).toBe('/faq');
128+
});
129+
130+
it('documents what each Core Web Vital metric means', async () => {
131+
render(
132+
<HelmetProvider>
133+
<App />
134+
</HelmetProvider>,
135+
);
136+
137+
await screen.findByRole('heading', { name: /web vitals dashboard/i, level: 1 });
138+
expect(screen.getByText(/understanding core web vitals/i)).toBeInTheDocument();
139+
expect(screen.getByText(/measures perceived loading speed/i)).toBeInTheDocument();
140+
expect(screen.getByText(/measures overall page responsiveness/i)).toBeInTheDocument();
141+
expect(screen.getByText(/measures visual stability/i)).toBeInTheDocument();
142+
});
143+
144+
it('passes accessibility checks with no violations', async () => {
145+
const { container } = render(
146+
<HelmetProvider>
147+
<App />
148+
</HelmetProvider>,
149+
);
150+
151+
await screen.findByRole('heading', { name: /web vitals dashboard/i, level: 1 });
152+
const results = await axe(container);
153+
expect(results.violations).toEqual([]);
154+
});
155+
});

src/components/Footer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export default function Footer() {
125125
{ label: t('footer.resources.press'), href: '/press' },
126126
{ label: 'Stellar Integration', href: '/stellar' },
127127
{ label: 'Careers', href: '/careers' },
128+
{ label: 'Web Vitals', href: '/vitals' },
128129
],
129130
},
130131
];

0 commit comments

Comments
 (0)