Skip to content

Commit

Permalink
feat: Add webvitals to react
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielelpidio committed Feb 3, 2025
1 parent dd991ac commit a437852
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 1 deletion.
42 changes: 42 additions & 0 deletions packages/react/src/web-vitals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';
import { Logger } from '@axiomhq/logging';
import * as React from 'react';
import { onLCP, onFID, onCLS, onINP, onFCP, onTTFB } from 'web-vitals';
import type { Metric } from 'web-vitals';

export function useReportWebVitals(reportWebVitalsFn: (metric: Metric) => void) {
const ref = React.useRef(reportWebVitalsFn);

ref.current = reportWebVitalsFn;

React.useEffect(() => {
onCLS(ref.current);
onFID(ref.current);
onLCP(ref.current);
onINP(ref.current);
onFCP(ref.current);
onTTFB(ref.current);
}, []);
}

const transformWebVitalsMetric = (metric: Metric): Record<string, any> => {
return {
webVital: metric,
_time: new Date().getTime(),
source: 'web-vital',
path: window.location.pathname,
};
};

export const createWebVitalsComponent = (logger: Logger) => {
const reportWebVitals = (metric: Metric) => {
logger.raw(transformWebVitalsMetric(metric));
logger.flush();
};

return () => {
useReportWebVitals(reportWebVitals);

return <></>;
};
};
121 changes: 121 additions & 0 deletions packages/react/test/unit/web-vitals.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from 'react';
import { renderHook, render } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useReportWebVitals, createWebVitalsComponent } from '../../src/web-vitals';
import { Logger } from '@axiomhq/logging';
import * as webVitals from 'web-vitals';

// Mock all web-vitals functions
vi.mock('web-vitals', () => ({
onCLS: vi.fn(),
onFID: vi.fn(),
onLCP: vi.fn(),
onINP: vi.fn(),
onFCP: vi.fn(),
onTTFB: vi.fn(),
}));

describe('Web Vitals', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset window location
Object.defineProperty(window, 'location', {
value: { pathname: '/test-path' },
writable: true,
});
});

describe('useReportWebVitals', () => {
it('should register all web vitals metrics', () => {
const reportFn = vi.fn();
renderHook(() => useReportWebVitals(reportFn));

expect(webVitals.onCLS).toHaveBeenCalled();
expect(webVitals.onFID).toHaveBeenCalled();
expect(webVitals.onLCP).toHaveBeenCalled();
expect(webVitals.onINP).toHaveBeenCalled();
expect(webVitals.onFCP).toHaveBeenCalled();
expect(webVitals.onTTFB).toHaveBeenCalled();
});

it('should pass the report function to all web vitals', () => {
const reportFn = vi.fn();
renderHook(() => useReportWebVitals(reportFn));

const calls = [
webVitals.onCLS,
webVitals.onFID,
webVitals.onLCP,
webVitals.onINP,
webVitals.onFCP,
webVitals.onTTFB,
];

calls.forEach((call) => {
expect(call).toHaveBeenCalledWith(expect.any(Function));
});
});
});

describe('createWebVitalsComponent', () => {
it('should create a component that uses web vitals reporting', () => {
const mockLogger = {
raw: vi.fn(),
flush: vi.fn(),
} as unknown as Logger;

const WebVitals = createWebVitalsComponent(mockLogger);
render(<WebVitals />);

// Verify that all web vitals are registered
expect(webVitals.onCLS).toHaveBeenCalled();
expect(webVitals.onFID).toHaveBeenCalled();
expect(webVitals.onLCP).toHaveBeenCalled();
expect(webVitals.onINP).toHaveBeenCalled();
expect(webVitals.onFCP).toHaveBeenCalled();
expect(webVitals.onTTFB).toHaveBeenCalled();
});

it('should log and flush metrics when reported', () => {
const mockLogger = {
raw: vi.fn(),
flush: vi.fn(),
} as unknown as Logger;

const WebVitals = createWebVitalsComponent(mockLogger);
render(<WebVitals />);

// Simulate a web vital metric being reported
const mockMetric = {
name: 'CLS',
value: 0.1,
id: 'test',
};

// Get the callback passed to onCLS and call it
const onCLSCallback = vi.mocked(webVitals.onCLS).mock.calls[0][0] as Function;
onCLSCallback(mockMetric);

// Verify the metric was logged and flushed
expect(mockLogger.raw).toHaveBeenCalledWith({
webVital: mockMetric,
_time: expect.any(Number),
source: 'web-vital',
path: '/test-path',
});
expect(mockLogger.flush).toHaveBeenCalled();
});

it('should render an empty fragment', () => {
const mockLogger = {
raw: vi.fn(),
flush: vi.fn(),
} as unknown as Logger;

const WebVitals = createWebVitalsComponent(mockLogger);
const { container } = render(<WebVitals />);

expect(container.firstChild).toBeNull();
});
});
});
3 changes: 2 additions & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
"baseUrl": ".",
"rootDir": "./src",
"outDir": "dist/esm",
"jsx": "react",
"declarationDir": "dist/esm/types",
"resolveJsonModule": true,
"declarationMap": true,
"emitDeclarationOnly": true
},
"include": ["src/**/*"]
"include": ["src/**/*", "test/**/*"]
}

0 comments on commit a437852

Please sign in to comment.