diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 000000000..d56b7641f --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,26 @@ +import type { HandleClientError } from '@sveltejs/kit'; +import { pushError } from '$lib/utils/faro/faro-manager'; + +/** + * SvelteKit only invokes `handleError` for *unexpected* errors — anything thrown + * via `error(...)` (e.g. expected 404s) is handled by SvelteKit directly and + * never reaches here. So every error that arrives is a genuine client-side + * crash (a throw in a `load` function or during rendering) that surfaces to the + * user as a full-page 500. + * + * These errors are caught by SvelteKit and therefore never reach + * `window.onerror` / `unhandledrejection`, which is all Faro's default + * instrumentation observes — so without this hook they're invisible in + * monitoring. Forward them to Faro explicitly. + */ +export const handleError: HandleClientError = ({ error, event, status, message }) => { + const reported = error instanceof Error ? error : new Error(message); + + pushError(reported, { + route: event.route.id ?? 'unknown', + url: event.url.pathname, + status: String(status), + }); + + // Don't alter what the user sees — keep SvelteKit's default message. +}; diff --git a/src/lib/utils/faro/faro-manager.ts b/src/lib/utils/faro/faro-manager.ts index 2dd7d278b..826cd06a8 100644 --- a/src/lib/utils/faro/faro-manager.ts +++ b/src/lib/utils/faro/faro-manager.ts @@ -1,13 +1,15 @@ -import { getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk'; +import { getWebInstrumentations, initializeFaro, type Faro } from '@grafana/faro-web-sdk'; import { TracingInstrumentation } from '@grafana/faro-web-tracing'; import getOptionalEnvVar from '../get-optional-env-var/public'; +let faro: Faro | null = null; + export const init = () => { const FARO_ENABLED = getOptionalEnvVar('PUBLIC_FARO_ENABLED', false, null); const FARO_ENVIRONMENT = getOptionalEnvVar('PUBLIC_FARO_ENVIRONMENT', false, null); if (FARO_ENABLED === 'true' && FARO_ENVIRONMENT) { - initializeFaro({ + faro = initializeFaro({ url: 'https://faro-collector-prod-eu-west-2.grafana.net/collect/0a4519657ebc92ca47d9271be5503b63', app: { name: 'app', @@ -25,3 +27,17 @@ export const init = () => { }); } }; + +/** + * Forwards an error to Faro, if monitoring is active. No-ops when Faro isn't + * initialized (e.g. the user hasn't consented to monitoring, or + * `PUBLIC_FARO_ENABLED` is off), so it's always safe to call. + * + * Faro's default web instrumentation only captures errors that reach + * `window.onerror` / `unhandledrejection`. SvelteKit catches errors thrown in + * `load`/rendering itself, so those never surface to Faro unless we report them + * explicitly from the client `handleError` hook. + */ +export const pushError = (error: Error, context?: Record) => { + faro?.api.pushError(error, context ? { context } : undefined); +};