Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,912 changes: 2,825 additions & 1,087 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@prisma/adapter-pg": "^7.4.0",
"@prisma/client": "^7.4.0",
"@rekog/mcp-nest": "^1.9.3",
"@sentry/nestjs": "^10.51.0",
"@sentry/profiling-node": "^10.51.0",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.6.2",
Expand Down
79 changes: 79 additions & 0 deletions packages/backend/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Sentry instrumentation — must be imported BEFORE any other application
* code so the auto-instrumentation can wrap http / express / prisma.
*
* Opt-in: SENTRY_DSN unset → no-op. Self-hosted users see no behaviour
* change unless they explicitly want error reporting.
*
* Sensitive headers and DTO fields are scrubbed via Sentry's beforeSend.
*/
import * as Sentry from '@sentry/nestjs';

const dsn = process.env.SENTRY_DSN;

if (dsn) {
const sample = (raw: string | undefined, fallback: number) => {
const n = raw === undefined ? NaN : Number(raw);
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
};

Sentry.init({
dsn,
environment:
process.env.SENTRY_ENVIRONMENT ||
process.env.NODE_ENV ||
'development',
release: process.env.SENTRY_RELEASE || process.env.npm_package_version,

// Tracing is opt-in on top of error reporting because it adds overhead.
tracesSampleRate: sample(process.env.SENTRY_TRACES_SAMPLE_RATE, 0.0),
profilesSampleRate: sample(process.env.SENTRY_PROFILES_SAMPLE_RATE, 0.0),

sendDefaultPii: false,

beforeSend(event) {
// Strip auth headers and known credential fields. Pino already
// redacts these in logs; Sentry lives outside that pipeline.
const headers = event?.request?.headers;
if (headers && typeof headers === 'object') {
for (const k of Object.keys(headers)) {
const lower = k.toLowerCase();
if (
lower === 'authorization' ||
lower === 'cookie' ||
lower === 'x-api-key' ||
lower === 'set-cookie'
) {
(headers as Record<string, unknown>)[k] = '[Redacted]';
}
}
}
const data = event?.request?.data as unknown;
if (data && typeof data === 'object') {
scrub(data as Record<string, unknown>);
}
return event;
},
});
}

function scrub(obj: Record<string, unknown>): void {
for (const key of Object.keys(obj)) {
const lower = key.toLowerCase();
if (
lower.includes('password') ||
lower.includes('token') ||
lower.includes('secret') ||
lower.includes('apikey') ||
lower.includes('api_key') ||
lower.includes('credential')
) {
obj[key] = '[Redacted]';
continue;
}
const v = obj[key];
if (v && typeof v === 'object' && !Array.isArray(v)) {
scrub(v as Record<string, unknown>);
}
}
}
5 changes: 5 additions & 0 deletions packages/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ config({ path: join(__dirname, '..', '..', '..', '..', '.env') });
config({ path: join(__dirname, '..', '..', '..', '.env') });
config({ path: '.env' });

// Sentry must be imported before any other application code so the
// auto-instrumentation can wrap http/express/prisma. No-op when SENTRY_DSN
// is not set.
import './instrument';

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@sentry/nextjs": "^10.51.0",
"@tailwindcss/postcss": "^4.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
37 changes: 37 additions & 0 deletions packages/frontend/sentry.client.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Sentry client init. No-op when NEXT_PUBLIC_SENTRY_DSN is unset, so the
* default self-hosted experience ships nothing to Sentry.
*
* Sample rates default to 0 — operators who want tracing/replay must set
* NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE and NEXT_PUBLIC_SENTRY_REPLAYS_SAMPLE_RATE
* explicitly.
*/
import * as Sentry from '@sentry/nextjs';

const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;

if (dsn) {
const sample = (raw: string | undefined, fallback: number) => {
const n = raw === undefined ? NaN : Number(raw);
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
};

Sentry.init({
dsn,
environment:
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ||
process.env.NODE_ENV ||
'development',
release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,
tracesSampleRate: sample(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE, 0),
replaysSessionSampleRate: sample(
process.env.NEXT_PUBLIC_SENTRY_REPLAYS_SAMPLE_RATE,
0,
),
replaysOnErrorSampleRate: sample(
process.env.NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
0,
),
sendDefaultPii: false,
});
}
19 changes: 19 additions & 0 deletions packages/frontend/sentry.edge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Sentry edge runtime init (middleware / Next edge functions).
* No-op when SENTRY_DSN is unset.
*/
import * as Sentry from '@sentry/nextjs';

const dsn = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

if (dsn) {
Sentry.init({
dsn,
environment:
process.env.SENTRY_ENVIRONMENT ||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ||
process.env.NODE_ENV ||
'development',
tracesSampleRate: 0,
});
}
25 changes: 25 additions & 0 deletions packages/frontend/sentry.server.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Sentry server-side init for Next.js. No-op when SENTRY_DSN is unset.
*/
import * as Sentry from '@sentry/nextjs';

const dsn = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

if (dsn) {
const sample = (raw: string | undefined, fallback: number) => {
const n = raw === undefined ? NaN : Number(raw);
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
};

Sentry.init({
dsn,
environment:
process.env.SENTRY_ENVIRONMENT ||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ||
process.env.NODE_ENV ||
'development',
release: process.env.SENTRY_RELEASE,
tracesSampleRate: sample(process.env.SENTRY_TRACES_SAMPLE_RATE, 0),
sendDefaultPii: false,
});
}
35 changes: 35 additions & 0 deletions packages/frontend/src/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Next.js instrumentation entry point. Loaded once per server runtime and
* delegates to the appropriate Sentry config file based on which runtime
* Next has booted.
*
* No-op everywhere when SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN are unset.
*/

export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('../sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('../sentry.edge.config');
}
}

// Capture errors thrown from React Server Components / route handlers.
// Wrapper instead of re-exporting so a missing helper in older sentry
// versions doesn't break the build.
import * as SentryNext from '@sentry/nextjs';

type CaptureFn = (
err: unknown,
request: Request,
context: { routerKind: string; routePath: string; routeType: string },
) => void | Promise<void>;

export const onRequestError: CaptureFn = async (err, request, context) => {
const fn = (SentryNext as unknown as { captureRequestError?: CaptureFn })
.captureRequestError;
if (typeof fn === 'function') {
await fn(err, request, context);
}
};
Loading