feat(observability): integrate Sentry error tracking for API and Web apps

- API: add @sentry/nestjs with instrument.ts, SentryModule, and SentryGlobalFilter
- Web: add @sentry/nextjs with client/server/edge configs, instrumentation hook
- Update next.config.js with withSentryConfig wrapper
- Replace TODO in error.tsx with Sentry.captureException
- Add SENTRY_DSN, SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT to .env.example

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 13:44:57 +07:00
parent 767afb56d5
commit 400a75845c
13 changed files with 1371 additions and 21 deletions

View File

@@ -111,6 +111,15 @@ SMTP_FROM=noreply@goodgo.vn
# -----------------------------------------------------------------------------
FIREBASE_SERVICE_ACCOUNT=
# -----------------------------------------------------------------------------
# Sentry Error Tracking
# -----------------------------------------------------------------------------
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------

View File

@@ -27,6 +27,8 @@
"@paralleldrive/cuid2": "^3.3.0",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@sentry/nestjs": "^10.47.0",
"@sentry/profiling-node": "^10.47.0",
"@willsoto/nestjs-prometheus": "^6.1.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",

View File

@@ -1,7 +1,8 @@
import { type MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { CqrsModule } from '@nestjs/cqrs';
import { ThrottlerModule } from '@nestjs/throttler';
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
import { AdminModule } from '@modules/admin';
import { AnalyticsModule } from '@modules/analytics';
import { AuthModule } from '@modules/auth';
@@ -20,6 +21,7 @@ import { AppController } from './app.controller';
@Module({
imports: [
SentryModule.forRoot(),
CqrsModule.forRoot(),
SharedModule,
AuthModule,
@@ -58,6 +60,10 @@ import { AppController } from './app.controller';
],
controllers: [AppController],
providers: [
{
provide: APP_FILTER,
useClass: SentryGlobalFilter,
},
{
provide: APP_GUARD,
useClass: ThrottlerBehindProxyGuard,

View File

@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/nestjs';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
Sentry.init({
dsn: process.env['SENTRY_DSN'],
environment: process.env['NODE_ENV'] ?? 'development',
integrations: [nodeProfilingIntegration()],
tracesSampleRate: process.env['NODE_ENV'] === 'production' ? 0.2 : 1.0,
profilesSampleRate: process.env['NODE_ENV'] === 'production' ? 0.2 : 1.0,
enabled: !!process.env['SENTRY_DSN'],
});

View File

@@ -1,3 +1,5 @@
import './instrument';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

View File

@@ -1,5 +1,6 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function GlobalError({
@@ -10,11 +11,8 @@ export default function GlobalError({
reset: () => void;
}) {
useEffect(() => {
// Report to error tracking service in production; log digest only
if (process.env.NODE_ENV === 'production') {
// TODO: integrate with Sentry/Datadog when available
// errorReporter.captureException(error);
} else {
Sentry.captureException(error);
if (process.env.NODE_ENV !== 'production') {
console.error('Unhandled error:', error);
}
}, [error]);

View File

@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/nextjs';
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');
}
}
export const onRequestError = Sentry.captureRequestError;

View File

@@ -1,3 +1,5 @@
const { withSentryConfig } = require('@sentry/nextjs');
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
@@ -42,4 +44,11 @@ const nextConfig = {
},
};
module.exports = nextConfig;
module.exports = withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
silent: !process.env.CI,
widenClientFileUpload: true,
disableLogger: true,
automaticVercelMonitors: true,
});

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@sentry/nextjs": "^10.47.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.7.0",

View File

@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env['NEXT_PUBLIC_SENTRY_DSN'],
environment: process.env['NODE_ENV'] ?? 'development',
tracesSampleRate: process.env['NODE_ENV'] === 'production' ? 0.2 : 1.0,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: process.env['NODE_ENV'] === 'production' ? 1.0 : 0,
enabled: !!process.env['NEXT_PUBLIC_SENTRY_DSN'],
});

View File

@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env['SENTRY_DSN'],
environment: process.env['NODE_ENV'] ?? 'development',
tracesSampleRate: process.env['NODE_ENV'] === 'production' ? 0.2 : 1.0,
enabled: !!process.env['SENTRY_DSN'],
});

View File

@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env['SENTRY_DSN'],
environment: process.env['NODE_ENV'] ?? 'development',
tracesSampleRate: process.env['NODE_ENV'] === 'production' ? 0.2 : 1.0,
enabled: !!process.env['SENTRY_DSN'],
});

1301
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff