Skip to content

Commit

Permalink
Use the script instead of web-vitals directly (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiaslins authored Sep 21, 2023
1 parent 67d7c14 commit 1e058c9
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 202 deletions.
1 change: 1 addition & 0 deletions apps/nextjs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

.env
# local env files
.env*.local

Expand Down
3 changes: 2 additions & 1 deletion apps/nextjs/app/blog/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<nav>
<Link href="/blog/some-slug">Some post</Link>
<Link href="/blog/another-slug">Another post</Link>
<Link href="/blog/test">Testin</Link>
<Link href="/blog/test">Testing article</Link>
<Link href="/blog">Blog Home</Link>
</nav>
{children}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/nextjs/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function Blog() {
return <div>BLOG Testing speed insights</div>;
return <div>My blog page</div>;
}
9 changes: 8 additions & 1 deletion apps/nextjs/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import Link from 'next/link';

export default function Blog() {
return <div>BLOG Testing speed insights</div>;
return (
<div>
<h1>Welcome to the Blog</h1>
<Link href="/blog/test">First blog entry</Link>
</div>
);
}
5 changes: 1 addition & 4 deletions packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vercel/speed-insights",
"version": "0.0.1-beta.2",
"version": "0.0.1-beta.3",
"description": "Speed Insights is a tool for measuring web performance and providing suggestions for improvement.",
"keywords": [
"speed-insights",
Expand Down Expand Up @@ -49,9 +49,6 @@
"test": "jest",
"type-check": "tsc --noEmit"
},
"dependencies": {
"web-vitals": "^3.4.0"
},
"devDependencies": {
"@swc/core": "^1.3.82",
"@swc/jest": "^0.2.29",
Expand Down
80 changes: 80 additions & 0 deletions packages/web/src/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { name as packageName, version } from '../package.json';
import { initQueue } from './queue';
import type { SpeedInsightsProps } from './types';
import { isBrowser, isDevelopment } from './utils';

const SCRIPT_URL = `/v1/speed-insights`;
const SCRIPT_PROD_NAME = 'script.js';
const SCRIPT_DEBUG_NAME = 'script.debug.js';

/**
* Injects the Vercel Speed Insights script into the page head and starts tracking page views. Read more in our [documentation](https://vercel.com/docs/speed-insights).
* @param [props] - Speed Insights options.
* @param [props.debug] - Whether to enable debug logging in development. Defaults to `true`.
* @param [props.beforeSend] - A middleware function to modify events before they are sent. Should return the event object or `null` to cancel the event.
*/
function inject(props: SpeedInsightsProps): {
setDynamicPath: (path: string) => void;
} | null {
if (!isBrowser()) return null;

initQueue();

if (props.beforeSend) {
window.si?.('beforeSend', props.beforeSend);
}
const src =
props.scriptSrc ||
`${SCRIPT_URL}/${isDevelopment() ? SCRIPT_DEBUG_NAME : SCRIPT_PROD_NAME}`;

if (document.head.querySelector(`script[src*="${src}"]`)) return null;

const script = document.createElement('script');
script.src = src;
script.defer = true;
script.setAttribute('data-sdkn', packageName);
script.setAttribute('data-sdkv', version);

if (props.sampleRate) {
script.setAttribute('data-sample-rate', props.sampleRate.toString());
}
if (props.dynamicPath) {
script.setAttribute('data-dynamic-path', props.dynamicPath);
}
if (props.endpoint) {
script.setAttribute('data-endpoint', props.endpoint);
}
if (props.token) {
script.setAttribute('data-token', props.token);
}
if (isDevelopment() && props.debug === false) {
script.setAttribute('data-debug', 'false');
}

script.onerror = (): void => {
const errorMessage = isDevelopment()
? 'Please check if any ad blockers are enabled and try again.'
: 'Be sure to enable Speed Insights for your project and deploy again. See https://vercel.com/docs/speed-insights for more information.';

// eslint-disable-next-line no-console -- Logging is okay here
console.log(
`[Vercel Speed Insights] Failed to load script from ${src}. ${errorMessage}`,
);
};

document.head.appendChild(script);

return {
setDynamicPath: (path: string): void => {
script.dataset.dynamicPath = path;
},
};
}

export { inject };
export type { SpeedInsightsProps };

// eslint-disable-next-line import/no-default-export -- Allow default export
export default {
inject,
};
47 changes: 0 additions & 47 deletions packages/web/src/generic/index.ts

This file was deleted.

54 changes: 8 additions & 46 deletions packages/web/src/nextjs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,14 @@
import { useCallback, useEffect, useRef } from 'react';
import type { Metric, MetricWithAttribution } from 'web-vitals';
import { sendVitals, watchMetrics } from '../generic';
import type { CollectedMetric } from '../types';
import React from 'react';
import { SpeedInsights as SpeedInsightsScript } from '../react';
import type { SpeedInsightsProps } from '../types';
import { useDynamicPath } from './utils';

interface SpeedInsightsProps {
token: string;
sampleRate?: number; // Only send a percentage of events to the server to reduce costs
}

export function SpeedInsights({ token, sampleRate }: SpeedInsightsProps): null {
export function SpeedInsights(
props: Omit<SpeedInsightsProps, 'dynamicPath'>,
): JSX.Element {
const dynamicPath = useDynamicPath();
const vitals = useRef<CollectedMetric[]>([]);

const flush = useCallback(() => {
if (vitals.current.length > 0) {
if (sampleRate && Math.random() > sampleRate) {
return;
}
const body = vitals.current;

// eslint-disable-next-line no-console -- ok for now
console.log('flushing', body);
sendVitals(body, token);

vitals.current = [];
}
}, [sampleRate, vitals.current]);

useEffect(() => {
addEventListener('visibilitychange', flush);
addEventListener('pagehide', flush);
return () => {
removeEventListener('visibilitychange', flush);
removeEventListener('pagehide', flush);
};
}, [flush]);

const reportVital = useCallback(
(metric: MetricWithAttribution): void => {
vitals.current.push({ ...metric, dynamicPath });
},
[dynamicPath],
return (
<SpeedInsightsScript {...(dynamicPath && { dynamicPath })} {...props} />
);

useEffect(() => {
watchMetrics(reportVital as (metric: Metric) => void); // TODO: fix typing -- caused by not properly typed library
}, []);

return null;
}
8 changes: 8 additions & 0 deletions packages/web/src/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const initQueue = (): void => {
// initialize va until script is loaded
if (window.si) return;

window.si = function a(...params): void {
(window.siq = window.siq || []).push(params);
};
};
21 changes: 21 additions & 0 deletions packages/web/src/react/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';
import { useEffect, useRef } from 'react';
import type { SpeedInsightsProps } from '../types';
import { inject } from '../generic';

export function SpeedInsights(props: SpeedInsightsProps): JSX.Element | null {
const scriptDynamicPath = useRef<((path: string) => void) | null>(null);
useEffect(() => {
const script = inject(props);

scriptDynamicPath.current = script?.setDynamicPath || null;
}, []);

useEffect(() => {
if (props.dynamicPath && scriptDynamicPath.current) {
scriptDynamicPath.current(props.dynamicPath);
}
}, [props.dynamicPath]);

return null;
}
62 changes: 41 additions & 21 deletions packages/web/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import type { MetricWithAttribution } from 'web-vitals';

// Internal typings
export type CollectedMetric = MetricWithAttribution & {
dynamicPath: string | null;
};

// V2
export interface SpeedInsightsPayload {
dsn: string;
speed: string;
metrics: SpeedInsightsMetric[];
export interface SpeedInsightsProps {
token?: string;
sampleRate?: number; // Only send a percentage of events to the server to reduce costs
beforeSend?: BeforeSendMiddleware;
debug?: boolean;
dynamicPath?: string;

scriptSrc?: string;
endpoint?: string;
}

export type EventTypes = 'vital';

export interface Event {
type: EventTypes;
url: string;
}

export type BeforeSendMiddleware = (
data: Event,
// Should we be more strict here? Compiler won't help a lot if it's that loose
) => Event | null | undefined | false;

export interface Functions {
beforeSend?: BeforeSendMiddleware;
}

export interface SpeedInsightsMetric {
id: string;
type: string;
value: string | number;
dynamicPath: string | null;
href: string;
attribution: {
target?: string;
};
export interface SpeedInsights<T extends keyof Functions = keyof Functions> {
queue: [T, Functions[T]][];
addAction: (action: T, data: Functions[T]) => void;
}

declare global {
interface Window {
// Base interface
/** Base interface to track events */
si?: SpeedInsights['addAction'];
/** Queue for speed insights datapoints, before the library is loaded */
siq?: SpeedInsights['queue'];

sil?: boolean;
// vam?: Mode;
}
}
Loading

0 comments on commit 1e058c9

Please sign in to comment.