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
5 changes: 5 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ SEED_ADMIN_PASSWORD="your-admin-password"
# Database (use this for Docker setup)
DATABASE_URL=postgresql://postgres:password@localhost:5432/nestjs_app

# Sentry — get the DSN from sentry.io > Project Settings > Client Keys (DSN).
# Leave blank to disable Sentry (SDK is a no-op without a DSN).
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"

# Server
PORT=3001
NODE_ENV=development
Expand Down
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "^10.4.4",
"@repo/database": "*",
"@sentry/nestjs": "^10.63.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie-parser": "^1.4.7",
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Module } from "@nestjs/common";
import { SentryModule } from "@sentry/nestjs/setup";
import { ConfigModule } from "@nestjs/config";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
Expand All @@ -11,6 +12,7 @@ import { S3Module } from "./s3/s3.module";

@Module({
imports: [
SentryModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
}),
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/bun-error.filter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExceptionFilter, Catch, ArgumentsHost, Logger, HttpException } from '@nestjs/common';
import { captureException } from '@sentry/nestjs';

@Catch()
export class BunErrorFilter implements ExceptionFilter {
Expand All @@ -18,6 +19,10 @@ export class BunErrorFilter implements ExceptionFilter {
return;
}

// Expected HttpExceptions were handled above — anything past this point
// is an unexpected failure worth reporting to Sentry
captureException(exception);

// 🔍 UNWRAP BUN AGGREGATE ERRORS
if (exception instanceof AggregateError || exception.name === 'AggregateError') {
this.logger.error('💥 AGGREGATE ERROR DETECTED 💥');
Expand Down
14 changes: 14 additions & 0 deletions apps/backend/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Sentry from "@sentry/nestjs";

// Must be imported before any other module in main.ts so Sentry can
// instrument Node built-ins before NestJS loads them. Note: the backend
// runs under Bun, so OpenTelemetry auto-instrumentation is limited —
// error capture and manual spans work, automatic http/db spans may not.
Sentry.init({
debug: process.env.NODE_ENV === "development",
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
release: process.env.SENTRY_RELEASE,
// Tracing intentionally disabled for now — errors and logs only
enableLogs: true,
});
6 changes: 6 additions & 0 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Sentry must be initialised before NestJS or any other import
import "./instrument";

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { BunErrorFilter } from "./bun-error.filter";
Expand All @@ -7,6 +10,9 @@ import cookieParser from "cookie-parser";
async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Flush pending Sentry events on SIGTERM/SIGINT
app.enableShutdownHooks();

// 1. Enable CORS for local, Railway-default, and custom domains
app.enableCors({
origin: [
Expand Down
9 changes: 8 additions & 1 deletion apps/frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# Copy this to .env and update values for your environment (Railway, etc.)
VITE_API_BASE_URL="http://localhost:3001"
VITE_API_BASE_URL="http://localhost:3001"

# Sentry — get the DSN from sentry.io > Project Settings > Client Keys (DSN).
# The DSN is public-safe. VITE_ prefix exposes it to the browser bundle.
VITE_SENTRY_DSN=""
# Server-side (SSR) DSN — usually the same value
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
14 changes: 13 additions & 1 deletion apps/frontend/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import * as Sentry from "@sentry/react-router";
import { HydratedRouter } from "react-router/dom";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
// Tracing is intentionally disabled for now (no tracesSampleRate) —
// errors and replays only. Re-add reactRouterTracingIntegration() and
// trace meta-tag injection in entry.server.tsx when enabling it.
integrations: [Sentry.replayIntegration()],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>
);
});
18 changes: 17 additions & 1 deletion apps/frontend/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// Sentry must be initialised before anything else on the server
import "./instrument.server";

import * as Sentry from "@sentry/react-router";
import { EntryContext, ServerRouter } from "react-router";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";
import type { HandleErrorFunction } from "react-router";

export default async function handleRequest(
async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
Expand All @@ -14,6 +19,7 @@ export default async function handleRequest(
signal: request.signal,
onError(error: unknown) {
// Log streaming rendering errors from inside the shell
Sentry.captureException(error);
console.error(error);
responseStatusCode = 500;
},
Expand All @@ -30,3 +36,13 @@ export default async function handleRequest(
status: responseStatusCode,
});
}

export default Sentry.wrapSentryHandleRequest(handleRequest);
Comment thread
wfelliss marked this conversation as resolved.

export const handleError: HandleErrorFunction = (error, { request }) => {
// Aborted requests (client navigated away) are expected, not errors
if (!request.signal.aborted) {
Sentry.captureException(error);
console.error(error);
}
};
11 changes: 11 additions & 0 deletions apps/frontend/app/instrument.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from "@sentry/react-router";

// Server-side Sentry init — imported first in entry.server.tsx.
// (Direct import rather than NODE_OPTIONS --import so it works identically
// on Windows dev machines and the Railway deployment.)
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
// Tracing intentionally disabled for now — errors and logs only
enableLogs: true,
});
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@react-router/node": "^7.13.0",
"@react-router/serve": "^7.13.0",
"@repo/database": "*",
"@sentry/react-router": "^10.63.0",
"@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-query": "^5.90.16",
Expand Down
Loading
Loading