diff --git a/package.json b/package.json index 0f7a98a..760aec0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@threlte/core": "6.0.0-next.11", "@threlte/extras": "5.0.0-next.16", "@types/three": "^0.154.0", - "three": "^0.155.0" + "three": "^0.155.0", + "web-vitals": "^3.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 089d397..213b391 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: three: specifier: ^0.155.0 version: 0.155.0 + web-vitals: + specifier: ^3.4.0 + version: 3.4.0 devDependencies: '@skeletonlabs/skeleton': @@ -2340,6 +2343,10 @@ packages: vite: 4.4.4 dev: true + /web-vitals@3.4.0: + resolution: {integrity: sha512-n9fZ5/bG1oeDkyxLWyep0eahrNcPDF6bFqoyispt7xkW0xhDzpUBTgyDKqWDi1twT0MgH4HvvqzpUyh0ZxZV4A==} + dev: false + /webgl-sdf-generator@1.1.1: resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} dev: false diff --git a/src/lib/vitals.ts b/src/lib/vitals.ts new file mode 100644 index 0000000..b1fcab0 --- /dev/null +++ b/src/lib/vitals.ts @@ -0,0 +1,81 @@ +import type { Metric } from 'web-vitals'; +import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals'; + +const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'; + +// Improve type safety by defining the navigator.connection type +interface NavigatorWithConnection extends Navigator { + connection: { + effectiveType: string; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Params = Record; // Define a type for 'params' + +function getConnectionSpeed() { + return 'connection' in navigator && 'connection' && 'effectiveType' in (navigator as NavigatorWithConnection).connection + ? (navigator as NavigatorWithConnection).connection.effectiveType + : ''; +} + +function sendToAnalytics(metric: Metric, options: { + params: Params; + path: string; + analyticsId: string; + debug: boolean; +}) { + const page = Object.entries(options.params).reduce( + (acc, [key, value]) => acc.replace(value, `[${key}]`), + options.path + ); + + const body = { + dsn: options.analyticsId, + id: metric.id, + page, + href: location.href, + event_name: metric.name, + value: metric.value.toString(), + speed: getConnectionSpeed(), + }; + + if (options.debug) { + console.log('[Web Vitals]', metric.name, JSON.stringify(body, null, 2)); + } + + // Serialize body to a URLSearchParams object + const searchParams = new URLSearchParams(body); + + // The type 'Record' is compatible with 'URLSearchParams' + const blob = new Blob([searchParams.toString()], { + type: 'application/x-www-form-urlencoded', + }); + if (navigator.sendBeacon) { + navigator.sendBeacon(vitalsUrl, blob); + } else { + fetch(vitalsUrl, { + body: blob, + method: 'POST', + credentials: 'omit', + keepalive: true, + }); + } +} + +export function webVitals(options: { + params: Params; // Use the defined 'Params' type here + path: string; + analyticsId: string; + debug: boolean; +}) { + try { + getFID((metric) => sendToAnalytics(metric, options)); + getTTFB((metric) => sendToAnalytics(metric, options)); + getLCP((metric) => sendToAnalytics(metric, options)); + getCLS((metric) => sendToAnalytics(metric, options)); + getFCP((metric) => sendToAnalytics(metric, options)); + } catch (err) { + console.error('[Web Vitals]', err); + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fe95bbc..d7def7d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,6 +7,20 @@ import Navigation from '../lib/components/Navigation.svelte'; import Header from '$lib/components/Header.svelte'; + import { webVitals } from '$lib/vitals'; + import { browser } from '$app/env'; + import { page } from '$app/stores'; + + let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID; + + $: if (browser && analyticsId) { + webVitals({ + path: $page.url.pathname, + params: $page.params, + analyticsId + }) + } + let routes = [ { url: '/', label: 'Home' }, { url: '/projects', label: 'Projects' }, @@ -16,20 +30,17 @@ let progress = 0; - function handleScroll(event: Event) { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget as HTMLElement; - progress = (scrollTop / (scrollHeight - clientHeight)) * 100; - } + function handleScroll(event: Event) { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget as HTMLElement; + progress = (scrollTop / (scrollHeight - clientHeight)) * 100; + } - +
diff --git a/vite.config.ts b/vite.config.ts index cccd80e..0f83cd6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,5 +5,8 @@ export default defineConfig({ plugins: [sveltekit()], ssr: { noExternal: ['three'] + }, + define: { + 'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID) } });