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/components/gltf/Github3d.svelte b/src/lib/components/gltf/Github3d.svelte index 5e2c117..f0e1d97 100644 --- a/src/lib/components/gltf/Github3d.svelte +++ b/src/lib/components/gltf/Github3d.svelte @@ -1,29 +1,29 @@ + + - {#await gltf} - - {:then gltf} - - {:catch error} - - {/await} + {#await gltf} + + {:then gltf} + + {:catch error} + + {/await} - + 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..fbedb34 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,11 +2,26 @@ import '../theme.postcss'; import '@skeletonlabs/skeleton/styles/skeleton.css'; import '../app.postcss'; - import { AppShell, Drawer, ProgressBar, drawerStore } from '@skeletonlabs/skeleton'; + import { AppShell, Drawer } from '@skeletonlabs/skeleton'; import Footer from '../lib/components/Footer.svelte'; import Navigation from '../lib/components/Navigation.svelte'; import Header from '$lib/components/Header.svelte'; + import { webVitals } from '$lib/vitals'; + import { browser } from '$app/environment'; + 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, + debug: false + }); + } + let routes = [ { url: '/', label: 'Home' }, { url: '/projects', label: 'Projects' }, @@ -16,20 +31,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/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index da3e9b0..21076dd 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -1,7 +1,7 @@ + diff --git a/src/routes/tools/Scene.svelte b/src/routes/tools/Scene.svelte index bb197c7..1fd418d 100644 --- a/src/routes/tools/Scene.svelte +++ b/src/routes/tools/Scene.svelte @@ -1,3 +1,4 @@ +