feat(sentry): add error monitoring, tracing, session replay, and structured logging

Integrates @sentry/sveltekit 10.49.0 via the modern SvelteKit path
(instrumentation.server.ts + experimental.instrumentation.server).

- instrumentation.server.ts: server-side init with tracing, logging,
  and console capture (log/warn/error forwarded to Sentry)
- hooks.client.ts: client-side init with browserTracingIntegration,
  replayIntegration (maskAllText/blockAllMedia), consoleLoggingIntegration,
  and handleErrorWithSentry
- hooks.server.ts: adds handleErrorWithSentry export, composes existing
  appHandle with sentryHandle() via sequence(), and sets isolation scope
  attributes (user_id, username, permission_level) per request for
  log/trace correlation
- svelte.config.js: enables experimental.instrumentation.server and
  experimental.tracing.server
- vite.config.ts: adds sentrySvelteKit() plugin for source map uploads
- .env.example: documents VITE_SENTRY_DSN, SENTRY_DSN, SENTRY_ORG,
  SENTRY_PROJECT, SENTRY_AUTH_TOKEN

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:10:36 +08:00
parent 4de2112a11
commit e55dfbe4ee
8 changed files with 1467 additions and 22 deletions

View File

@@ -3,3 +3,12 @@
# No API_BASE_URL override needed.
PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAACI5pu-lNWFc6Wu1
TURNSTILE_SECRET_KEY=
# Sentry — error monitoring, tracing, session replay, logging
# VITE_SENTRY_DSN is public (bundled into the browser build)
VITE_SENTRY_DSN=
SENTRY_DSN=
SENTRY_ORG=
SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN is secret; never commit it — use CI secrets or .env.local
SENTRY_AUTH_TOKEN=

View File

@@ -56,6 +56,7 @@
"dependencies": {
"@hey-api/client-fetch": "^0.13.1",
"@lucide/svelte": "^1.8.0",
"@sentry/sveltekit": "^10.49.0",
"@zxing/browser": "^0.1.5",
"bits-ui": "^2.17.3",
"daisyui": "^5.5.19",

1418
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

18
src/hooks.client.ts Normal file
View File

@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/sveltekit';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
sendDefaultPii: true,
tracesSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
Sentry.consoleLoggingIntegration({ levels: ['log', 'warn', 'error'] })
],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
enableLogs: true
});
export const handleError = Sentry.handleErrorWithSentry();

View File

@@ -1,8 +1,12 @@
import * as Sentry from '@sentry/sveltekit';
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { getUserInfo } from '$lib/api';
import { createApiClient } from '$lib/server/api';
export const handle: Handle = async ({ event, resolve }) => {
export const handleError = Sentry.handleErrorWithSentry();
const appHandle: Handle = async ({ event, resolve }) => {
event.locals.accessToken = event.cookies.get('access_token') ?? null;
event.locals.user = null;
@@ -18,6 +22,14 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}
if (event.locals.user) {
Sentry.getIsolationScope().setAttributes({
'user.id': event.locals.user.user_id,
'user.username': event.locals.user.username,
'user.permission_level': event.locals.user.permission_level
});
}
const theme = (event.cookies.get('theme') as 'dark' | 'light') ?? 'dark';
return resolve(event, {
transformPageChunk({ html }) {
@@ -25,3 +37,5 @@ export const handle: Handle = async ({ event, resolve }) => {
}
});
};
export const handle = sequence(Sentry.sentryHandle(), appHandle);

View File

@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/sveltekit';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV ?? 'production',
sendDefaultPii: true,
tracesSampleRate: 1.0,
enableLogs: true,
integrations: [Sentry.consoleLoggingIntegration({ levels: ['log', 'warn', 'error'] })]
});

View File

@@ -10,7 +10,11 @@ const config = {
},
kit: {
adapter: adapter(),
paths: { base: '/app' }
paths: { base: '/app' },
experimental: {
instrumentation: { server: true },
tracing: { server: true }
}
}
};

View File

@@ -1,12 +1,21 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { sentrySvelteKit } from '@sentry/sveltekit';
import { defineConfig } from 'vite';
const isTest = process.env.NODE_ENV === 'test';
const apiTarget = isTest ? 'http://localhost:4010' : 'http://10.0.0.10:8000';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
plugins: [
tailwindcss(),
sentrySvelteKit({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN
}),
sveltekit()
],
server: {
proxy: {
// In test mode the mock server is the "API root" — strip the