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:
@@ -111,6 +111,15 @@ SMTP_FROM=noreply@goodgo.vn
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
FIREBASE_SERVICE_ACCOUNT=
|
FIREBASE_SERVICE_ACCOUNT=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Sentry Error Tracking
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
SENTRY_DSN=
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
|
SENTRY_ORG=
|
||||||
|
SENTRY_PROJECT=
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"@paralleldrive/cuid2": "^3.3.0",
|
"@paralleldrive/cuid2": "^3.3.0",
|
||||||
"@prisma/adapter-pg": "^7.7.0",
|
"@prisma/adapter-pg": "^7.7.0",
|
||||||
"@prisma/client": "^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",
|
"@willsoto/nestjs-prometheus": "^6.1.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
|
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 { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
|
||||||
import { AdminModule } from '@modules/admin';
|
import { AdminModule } from '@modules/admin';
|
||||||
import { AnalyticsModule } from '@modules/analytics';
|
import { AnalyticsModule } from '@modules/analytics';
|
||||||
import { AuthModule } from '@modules/auth';
|
import { AuthModule } from '@modules/auth';
|
||||||
@@ -20,6 +21,7 @@ import { AppController } from './app.controller';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
SentryModule.forRoot(),
|
||||||
CqrsModule.forRoot(),
|
CqrsModule.forRoot(),
|
||||||
SharedModule,
|
SharedModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
@@ -58,6 +60,10 @@ import { AppController } from './app.controller';
|
|||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: SentryGlobalFilter,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerBehindProxyGuard,
|
useClass: ThrottlerBehindProxyGuard,
|
||||||
|
|||||||
11
apps/api/src/instrument.ts
Normal file
11
apps/api/src/instrument.ts
Normal 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'],
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './instrument';
|
||||||
|
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export default function GlobalError({
|
export default function GlobalError({
|
||||||
@@ -10,11 +11,8 @@ export default function GlobalError({
|
|||||||
reset: () => void;
|
reset: () => void;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Report to error tracking service in production; log digest only
|
Sentry.captureException(error);
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// TODO: integrate with Sentry/Datadog when available
|
|
||||||
// errorReporter.captureException(error);
|
|
||||||
} else {
|
|
||||||
console.error('Unhandled error:', error);
|
console.error('Unhandled error:', error);
|
||||||
}
|
}
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|||||||
13
apps/web/instrumentation.ts
Normal file
13
apps/web/instrumentation.ts
Normal 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;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const { withSentryConfig } = require('@sentry/nextjs');
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@sentry/nextjs": "^10.47.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
|
|||||||
10
apps/web/sentry.client.config.ts
Normal file
10
apps/web/sentry.client.config.ts
Normal 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'],
|
||||||
|
});
|
||||||
8
apps/web/sentry.edge.config.ts
Normal file
8
apps/web/sentry.edge.config.ts
Normal 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'],
|
||||||
|
});
|
||||||
8
apps/web/sentry.server.config.ts
Normal file
8
apps/web/sentry.server.config.ts
Normal 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
1301
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user