feat: connect sentry to dash and api

This commit is contained in:
Rotimi Best
2026-05-21 17:18:08 +01:00
parent 25a4b1fd19
commit 0657c32c97
14 changed files with 1134 additions and 131 deletions
+6 -1
View File
@@ -40,7 +40,12 @@ CLOUDFLARE_ACCESS_KEY=""
CLOUDFLARE_ACCOUNT_ID=""
CLOUDFLARE_BUCKET_DOMAIN=""
CLOUDFLARE_SECRET_ACCESS_KEY=""
SENTRY_DNS=""
# Sentry (server-side error tracking). Leave SENTRY_DSN unset to disable.
# Skipped automatically in dev (NODE_ENV !== 'production') and self-hosted (PUBLIC_IS_SELFHOSTED=true).
SENTRY_DSN=""
SENTRY_ENVIRONMENT="production"
SENTRY_TRACES_SAMPLE_RATE="0"
# BullMQ dashboard (/admin/queues) ───────────────────────────────────────────
# In dev the dashboard is open. In production it's hidden unless ONE of the
+1 -1
View File
@@ -54,7 +54,7 @@
"@hono/standard-validator": "^0.1.5",
"@hono/zod-validator": "^0.7.6",
"@scalar/hono-api-reference": "^0.9.18",
"@sentry/profiling-node": "^7.110.1",
"@sentry/node": "^10.53.1",
"ai": "^6.0.158",
"axios": "^1.4.0",
"css-tree": "^3.2.1",
+9
View File
@@ -1,5 +1,6 @@
import 'dotenv/config';
import * as Sentry from '@sentry/node';
import { consumeHandoffPayload, storeHandoffPayload } from '@api/utils/redis/oauth-handoff';
import { isPublicCorsPath, sessionCors } from '@api/middlewares/cors';
import { randomBytes } from 'crypto';
@@ -78,6 +79,13 @@ export const app = new Hono()
c.set('user', session.user);
c.set('session', session.session);
c.set('orgRoles', (session as { orgRoles?: Record<string, number> }).orgRoles ?? {});
Sentry.setUser({
id: session.user.id,
...(session.user.email && { email: session.user.email }),
...(session.user.name && { username: session.user.name })
});
await next();
})
@@ -206,6 +214,7 @@ export const app = new Hono()
// Error handling
.onError((err, c) => {
Sentry.captureException(err);
console.error('Error:', err);
return c.json({ error: 'Internal Server Error' }, 500);
});
+4
View File
@@ -78,6 +78,10 @@ const envSchema = z.object({
return Number.isFinite(parsed) && parsed >= 1 ? Math.trunc(parsed) : 15;
}),
/** Sentry error tracking (server-side). Leave SENTRY_DSN unset to disable. */
SENTRY_DSN: z.string().optional(),
SENTRY_ENVIRONMENT: z.string().optional(),
SENTRY_TRACES_SAMPLE_RATE: z.string().optional(),
/** Max fetch_documentation_url calls per org per UTC day (default 500). */
AGENT_MAX_FETCHES_PER_ORG_PER_DAY: z
.string()
+1
View File
@@ -1,4 +1,5 @@
import 'dotenv/config';
import './instrument';
import { API_PORT } from '@api/constants';
import { app } from '@api/app';
+20
View File
@@ -0,0 +1,20 @@
/**
* Must be imported as the FIRST thing in `apps/api/src/index.ts`. The Sentry
* Node SDK relies on auto-instrumentation that hooks into Node's `require`
* resolution and module loaders; any code that runs before this file initializes
* Sentry will not be traced.
*/
import * as Sentry from '@sentry/node';
const dsn = process.env.SENTRY_DSN?.trim();
const isProd = process.env.NODE_ENV === 'production';
const isSelfHosted = process.env.PUBLIC_IS_SELFHOSTED === 'true';
if (dsn && isProd && !isSelfHosted) {
Sentry.init({
dsn,
environment: process.env.SENTRY_ENVIRONMENT?.trim() || 'production',
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0),
sendDefaultPii: false
});
}
+7
View File
@@ -45,3 +45,10 @@ PRIVATE_APP_SUBDOMAINS=app
POLAR_ACCESS_TOKEN=
POLAR_WEBHOOK_SECRET=
POLAR_TOKEN_PACK_PRODUCT_ID=
# Sentry (client-side error tracking). Leave PUBLIC_SENTRY_DSN unset to disable.
PUBLIC_SENTRY_DSN=
PUBLIC_SENTRY_ENVIRONMENT=production
PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0
PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0
PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1
+1
View File
@@ -32,6 +32,7 @@
"@lucide/svelte": "^0.545.0",
"@polar-sh/sdk": "^0.47.1",
"@polar-sh/sveltekit": "^0.7.5",
"@sentry/sveltekit": "^10.53.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/adapter-vercel": "^4.0.0",
+20
View File
@@ -0,0 +1,20 @@
import * as Sentry from '@sentry/sveltekit';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/public';
import { handleErrorWithSentry } from '@sentry/sveltekit';
const dsn = env.PUBLIC_SENTRY_DSN?.trim();
const isSelfHosted = env.PUBLIC_IS_SELFHOSTED === 'true';
if (dsn && !dev && !isSelfHosted) {
Sentry.init({
dsn,
environment: env.PUBLIC_SENTRY_ENVIRONMENT?.trim() || 'production',
tracesSampleRate: Number(env.PUBLIC_SENTRY_TRACES_SAMPLE_RATE ?? 0),
replaysSessionSampleRate: Number(env.PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? 0),
replaysOnErrorSampleRate: Number(env.PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? 1),
integrations: [Sentry.replayIntegration()]
});
}
export const handleError = handleErrorWithSentry();
@@ -1,6 +1,5 @@
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import { initPosthog, type PosthogBootstrapUser } from '$lib/utils/services/posthog';
import { initSentry } from '$lib/utils/services/sentry';
import { initUmami } from '$lib/utils/services/umami';
import { licenseApi } from '$features/license/api/license.svelte';
@@ -10,7 +9,6 @@ export function setupAnalytics(user?: PosthogBootstrapUser) {
if (isInitialized) return;
isInitialized = true;
initSentry();
initPosthog(user);
initUmami();
}
@@ -1,6 +1,7 @@
import { appInitApi } from '$features/app/init.svelte';
import { authClient } from '$lib/utils/services/auth/client';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { clearSentryUser } from '$lib/utils/services/sentry';
import { goto } from '$app/navigation';
import posthog from 'posthog-js';
import { resolve } from '$app/paths';
@@ -16,6 +17,7 @@ export async function logout(redirect = true) {
capturePosthogEvent('user_logged_out');
posthog.reset();
clearSentryUser();
if (redirect) {
goto(resolve('/login', {}));
@@ -289,8 +289,6 @@ export function getApiHeaders(
): { headers: { cookie: string; 'cio-org-id'?: string } } {
const cioCookies = getCioCookieString(cookies);
console.log('cioCookies', cioCookies);
console.log('cookies', cookies.getAll());
const headers: { cookie: string; 'cio-org-id'?: string } = {
cookie: cioCookies || ''
};
@@ -1,26 +1,30 @@
import * as Sentry from '@sentry/sveltekit';
import { dev } from '$app/environment';
// import * as Sentry from '@sentry/browser';
// import { CaptureConsole } from '@sentry/integrations';
import { env } from '$env/dynamic/public';
export const initSentry = (): void => {
if (dev) return;
const isEnabled = () =>
Boolean(env.PUBLIC_SENTRY_DSN?.trim()) && !dev && env.PUBLIC_IS_SELFHOSTED !== 'true';
// Sentry.init({
// dsn: 'https://c966f7e8cb1d4306be20b26bb4f0cc96@o476906.ingest.sentry.io/5999999',
// integrations: [
// new CaptureConsole({
// levels: ['error']
// }),
// new Sentry.Replay()
// ],
// environment: 'production',
// replaysSessionSampleRate: 0.5,
// replaysOnErrorSampleRate: 1.0
// });
export type SentryUser = {
id: string;
email?: string | null;
username?: string | null;
fullname?: string | null;
};
export const setSentryUser = (user: Record<string, unknown>): void => {
if (dev) return;
export const setSentryUser = (user: SentryUser): void => {
if (!isEnabled()) return;
// Sentry.setUser(user);
Sentry.setUser({
id: user.id,
...(user.email && { email: user.email }),
...(user.username && { username: user.username }),
...(user.fullname && { fullname: user.fullname })
});
};
export const clearSentryUser = (): void => {
if (!isEnabled()) return;
Sentry.setUser(null);
};
+1040 -106
View File
File diff suppressed because it is too large Load Diff