|
1 | | -import type { Handle } from '@sveltejs/kit'; |
| 1 | +import type { Handle, HandleServerError } from '@sveltejs/kit'; |
2 | 2 | import { runHealthChecks } from '$lib/server/monitor'; |
| 3 | + |
| 4 | +// Parse DSN into the store endpoint and auth header Sentry expects. |
| 5 | +// DSN format: https://<key>@<host>/<project_id> |
| 6 | +function parseDsn(dsn: string): { url: string; key: string } | null { |
| 7 | + try { |
| 8 | + const u = new URL(dsn); |
| 9 | + const key = u.username; |
| 10 | + const projectId = u.pathname.replace('/', ''); |
| 11 | + const host = u.host; |
| 12 | + return { url: `https://${host}/api/${projectId}/store/`, key }; |
| 13 | + } catch { |
| 14 | + return null; |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +async function captureToSentry( |
| 19 | + dsn: string, |
| 20 | + payload: { message?: string; exception?: unknown; level: string; request?: unknown } |
| 21 | +): Promise<void> { |
| 22 | + const parsed = parseDsn(dsn); |
| 23 | + if (!parsed) return; |
| 24 | + const event = { |
| 25 | + timestamp: new Date().toISOString(), |
| 26 | + platform: 'javascript', |
| 27 | + level: payload.level, |
| 28 | + ...(payload.message ? { message: payload.message } : {}), |
| 29 | + ...(payload.exception ? { exception: payload.exception } : {}), |
| 30 | + ...(payload.request ? { request: payload.request } : {}), |
| 31 | + }; |
| 32 | + await fetch(parsed.url, { |
| 33 | + method: 'POST', |
| 34 | + headers: { |
| 35 | + 'Content-Type': 'application/json', |
| 36 | + 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${parsed.key}, sentry_client=openboot/1.0`, |
| 37 | + }, |
| 38 | + body: JSON.stringify(event), |
| 39 | + signal: AbortSignal.timeout(5_000), |
| 40 | + }).catch(() => {}); // fire-and-forget, never block the response |
| 41 | +} |
3 | 42 | import { RESERVED_ALIASES } from '$lib/server/validation'; |
4 | 43 | import { getConfigForHookAlias, getConfigForInstall, getConfigForHookSlug } from '$lib/server/db'; |
5 | 44 | import { serveInstallByAlias, serveInstallBySlug } from '$lib/server/alias'; |
@@ -49,6 +88,19 @@ function isVersionOlderThan(version: string, minVersion: string): boolean { |
49 | 88 | return aMaj < bMaj || (aMaj === bMaj && (aMin < bMin || (aMin === bMin && aPat < bPat))); |
50 | 89 | } |
51 | 90 |
|
| 91 | +export const handleError: HandleServerError = async ({ error, event }) => { |
| 92 | + const dsn = event.platform?.env?.SENTRY_DSN; |
| 93 | + if (dsn) { |
| 94 | + await captureToSentry(dsn, { |
| 95 | + level: 'error', |
| 96 | + exception: { |
| 97 | + values: [{ type: 'Error', value: error instanceof Error ? error.message : String(error) }], |
| 98 | + }, |
| 99 | + request: { url: event.url.href, method: event.request.method }, |
| 100 | + }); |
| 101 | + } |
| 102 | +}; |
| 103 | + |
52 | 104 | export const handle: Handle = async ({ event, resolve }) => { |
53 | 105 | const path = event.url.pathname; |
54 | 106 |
|
@@ -106,6 +158,16 @@ export const handle: Handle = async ({ event, resolve }) => { |
106 | 158 | } |
107 | 159 |
|
108 | 160 | const response = await resolve(event); |
| 161 | + |
| 162 | + const dsn = event.platform?.env?.SENTRY_DSN; |
| 163 | + if (response.status >= 500 && dsn) { |
| 164 | + await captureToSentry(dsn, { |
| 165 | + level: 'error', |
| 166 | + message: `HTTP ${response.status} ${event.request.method} ${event.url.pathname}`, |
| 167 | + request: { url: event.url.href, method: event.request.method }, |
| 168 | + }); |
| 169 | + } |
| 170 | + |
109 | 171 | const securedResponse = withSecurityHeaders(response); |
110 | 172 |
|
111 | 173 | // Version negotiation: if CLI sends X-OpenBoot-Version, check compatibility. |
@@ -140,9 +202,21 @@ export const handle: Handle = async ({ event, resolve }) => { |
140 | 202 | // Cloudflare Workers Cron Trigger — runs on the schedule defined in wrangler.toml. |
141 | 203 | // Checks critical production signals and sends an alert to ALERT_WEBHOOK_URL if any fail. |
142 | 204 | export const scheduled: App.Scheduled = async ({ platform }) => { |
143 | | - await runHealthChecks({ |
144 | | - APP_URL: platform?.env?.APP_URL ?? 'https://openboot.dev', |
145 | | - ALERT_WEBHOOK_URL: platform?.env?.ALERT_WEBHOOK_URL, |
146 | | - }); |
| 205 | + const dsn = platform?.env?.SENTRY_DSN; |
| 206 | + try { |
| 207 | + await runHealthChecks({ |
| 208 | + APP_URL: platform?.env?.APP_URL ?? 'https://openboot.dev', |
| 209 | + }); |
| 210 | + } catch (err) { |
| 211 | + console.error('[monitor] cron failed:', err); |
| 212 | + if (dsn) { |
| 213 | + await captureToSentry(dsn, { |
| 214 | + level: 'error', |
| 215 | + exception: { |
| 216 | + values: [{ type: 'HealthCheckError', value: err instanceof Error ? err.message : String(err) }], |
| 217 | + }, |
| 218 | + }); |
| 219 | + } |
| 220 | + } |
147 | 221 | }; |
148 | 222 |
|
0 commit comments