From 2f6387df2cab25b6e7b0f700a3bdc65843daf443 Mon Sep 17 00:00:00 2001 From: Bart van der Braak Date: Tue, 16 Jan 2024 03:33:31 +0100 Subject: [PATCH] feat: implement @vercel/analytics and web-vitals dependencies --- package.json | 4 +- pnpm-lock.yaml | 20 ++++++++++ src/lib/vitals.ts | 82 +++++++++++++++++++++++++++++++++++++++ src/routes/+layout.svelte | 17 ++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/lib/vitals.ts diff --git a/package.json b/package.json index c5ac32c..07412ad 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "vite": "^5.0.11" }, "dependencies": { + "@vercel/analytics": "^1.1.1", "bits-ui": "^0.14.0", "clsx": "^2.1.0", "lucide-svelte": "^0.309.0", @@ -45,7 +46,8 @@ "radix-icons-svelte": "^1.2.1", "svelte-wrap-balancer": "^0.0.4", "tailwind-merge": "^2.2.0", - "tailwind-variants": "^0.1.20" + "tailwind-variants": "^0.1.20", + "web-vitals": "^3.5.1" }, "lint-staged": { "*.{js,ts,svelte,css,scss,postcss,md,json}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e93dcc..45cdd7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@vercel/analytics': + specifier: ^1.1.1 + version: 1.1.1 bits-ui: specifier: ^0.14.0 version: 0.14.0(svelte@4.2.8) @@ -29,6 +32,9 @@ dependencies: tailwind-variants: specifier: ^0.1.20 version: 0.1.20(tailwindcss@3.4.1) + web-vitals: + specifier: ^3.5.1 + version: 3.5.1 devDependencies: '@sveltejs/adapter-vercel': @@ -1076,6 +1082,12 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@vercel/analytics@1.1.1: + resolution: {integrity: sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA==} + dependencies: + server-only: 0.0.1 + dev: false + /@vercel/nft@0.26.2: resolution: {integrity: sha512-bxe2iShmKZi7476xYamyKvhhKwQ6JPEtQ2FSq1AjMUH2buMd8LQMkdoHinTqZYc+1sMTh3G0ARdjzNvV1FEisA==} engines: {node: '>=16'} @@ -2857,6 +2869,10 @@ packages: lru-cache: 6.0.0 dev: true + /server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -3401,6 +3417,10 @@ packages: vite: 5.0.11 dev: true + /web-vitals@3.5.1: + resolution: {integrity: sha512-xQ9lvIpfLxUj0eSmT79ZjRoU5wIRfIr7pNukL7ZE4EcWZSmfZQqOlhuAGfkVa3EFmzPHZhWhXfm2i5ys+THVPg==} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true diff --git a/src/lib/vitals.ts b/src/lib/vitals.ts new file mode 100644 index 0000000..31f1494 --- /dev/null +++ b/src/lib/vitals.ts @@ -0,0 +1,82 @@ +import type { Metric } from 'web-vitals'; +import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals'; + +const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'; + +interface NavigatorWithConnection extends Navigator { + connection: { + effectiveType: string; + }; +} + +type Params = Record; + +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) as [string, string][]).reduce( + (acc: string, [key, value]: [string, string]) => 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)); + } + + const searchParams = new URLSearchParams(body); + + 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; + path: string; + analyticsId: string; + debug: boolean; +}) { + try { + onFID((metric) => sendToAnalytics(metric, options)); + onTTFB((metric) => sendToAnalytics(metric, options)); + onLCP((metric) => sendToAnalytics(metric, options)); + onCLS((metric) => sendToAnalytics(metric, options)); + onFCP((metric) => sendToAnalytics(metric, options)); + } catch (err) { + console.error('[Web Vitals]', err); + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4eba7f4..789a3e0 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,23 @@ import '../styles/globals.css'; import { ModeWatcher } from 'mode-watcher'; import { fly } from 'svelte/transition'; + import { inject } from '@vercel/analytics'; + import { webVitals } from '$lib/vitals'; + import { browser } from '$app/environment'; + import { page } from '$app/stores'; + + inject({ mode: dev ? 'development' : 'production' }); + + let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID; + + $: if (browser && analyticsId) { + webVitals({ + path: $page.url.pathname, + params: $page.params, + analyticsId, + debug: false + }); + } export let data;