Skip to content

Commit

Permalink
refactor(back): rework sentry integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tsa96 committed Jul 7, 2024
1 parent 91aefeb commit 363e5bf
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 193 deletions.
37 changes: 6 additions & 31 deletions apps/backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import * as Sentry from '@sentry/node';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FastifyMulterModule } from '@nest-lab/fastify-multer';
import { ExceptionHandlerFilter } from './filters/exception-handler.filter';
import { ConfigFactory, Environment, validate } from './config';
import { SentryModule } from './modules/sentry/sentry.module';
import { AuthModule } from './modules/auth/auth.module';
import { ActivitiesModule } from './modules/activities/activities.module';
import { AdminModule } from './modules/admin/admin.module';
Expand All @@ -22,6 +20,7 @@ import { MapReviewModule } from './modules/map-review/map-review.module';
import { DbModule } from './modules/database/db.module';
import { KillswitchModule } from './modules/killswitch/killswitch.module';
import { HealthcheckModule } from './modules/healthcheck/healthcheck.module';
import { setupNestInterceptor } from '../instrumentation';

@Module({
imports: [
Expand All @@ -31,34 +30,6 @@ import { HealthcheckModule } from './modules/healthcheck/healthcheck.module';
isGlobal: true,
validate
}),
// We use Sentry in production for error logging and performance tracing.
// This is a small wrapper module around @sentry/node that only inits in
// production if a valid DSN is set.
SentryModule.forRootAsync({
useFactory: async (config: ConfigService) => {
// Whether to enable SentryInterceptor. If enabled, we run a transaction
// for the lifetime of tracesSampleRate * all HTTP requests. This
// provides more detailed error
const enableTracing = config.getOrThrow('sentry.enableTracing');
return {
environment: config.getOrThrow('env'),
enableTracing,
sentryOpts: {
// If this isn't set in prod we won't init Sentry.
dsn: config.getOrThrow('sentry.dsn'),
enableTracing,
environment: config.getOrThrow('sentry.env'),
tracesSampleRate: config.getOrThrow('sentry.tracesSampleRate'),
integrations: config.getOrThrow('sentry.tracePrisma')
? [Sentry.prismaIntegration()]
: undefined,
debug: false
}
};
},
imports: [DbModule.forRoot()],
inject: [ConfigService]
}),
// Pino is a highly performant logger that outputs logs as JSON, which we
// then export to Grafana Loki. This module sets up `pino-http` which logs
// all HTTP requests (so no need for a Nest interceptor).
Expand Down Expand Up @@ -100,6 +71,10 @@ import { HealthcheckModule } from './modules/healthcheck/healthcheck.module';
{
provide: APP_FILTER,
useClass: ExceptionHandlerFilter
},
{
provide: APP_INTERCEPTOR,
useFactory: setupNestInterceptor
}
]
})
Expand Down
7 changes: 0 additions & 7 deletions apps/backend/src/app/config/config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ export interface ConfigInterface {
accessKeyID: string;
secretAccessKey: string;
};
sentry: {
dsn: string;
enableTracing: boolean;
tracesSampleRate: number;
tracePrisma: boolean;
env: 'production' | 'staging';
};
limits: {
dailyReports: number;
mapImageUploads: number;
Expand Down
7 changes: 0 additions & 7 deletions apps/backend/src/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ export const ConfigFactory = (): ConfigInterface => {
gameExpTime: JWT_GAME_EXPIRY_TIME,
refreshExpTime: JWT_REFRESH_EXPIRY_TIME
},
sentry: {
dsn: process.env['SENTRY_DSN'] || '',
enableTracing: process.env['SENTRY_ENABLE_TRACING'] === 'true' || false,
tracesSampleRate: +process.env['SENTRY_TRACE_SAMPLE_RATE'] || 0,
tracePrisma: process.env['SENTRY_TRACE_PRISMA'] === 'true' || false,
env: (process.env['SENTRY_ENV'] || '') as 'production' | 'staging'
},
sessionSecret: process.env['SESSION_SECRET'] || '',
steam: {
webAPIKey: process.env['STEAM_WEB_API_KEY'] || "This won't work!!",
Expand Down
22 changes: 0 additions & 22 deletions apps/backend/src/app/config/config.validation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
IsBoolean,
IsEnum,
IsIn,
IsInt,
IsNumber,
IsOptional,
IsString,
IsUrl,
Expand Down Expand Up @@ -66,26 +64,6 @@ export class ConfigValidation {
@MinLength(20)
readonly JWT_SECRET?: string;

@IsUrl()
@IsOptionalWithEmptyString()
readonly SENTRY_DSN?: string;

@IsOptional()
@IsBoolean()
readonly SENTRY_ENABLE_TRACING?: boolean;

@IsOptional()
@IsNumber()
readonly SENTRY_TRACE_SAMPLE_RATE?: number;

@IsOptional()
@IsBoolean()
readonly SENTRY_TRACE_PRISMA?: boolean;

@IsOptional()
@IsIn(['production', 'staging'])
readonly SENTRY_ENV?: 'production' | 'staging';

@IsUrl({ require_tld: false, protocols: ['postgresql'] })
readonly DATABASE_URL?: string;

Expand Down
15 changes: 6 additions & 9 deletions apps/backend/src/app/filters/exception-handler.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,21 @@ import {
ExceptionFilter,
HttpException,
HttpStatus,
Inject,
Logger
Logger,
ServiceUnavailableException
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { ConfigService } from '@nestjs/config';
import '@sentry/tracing'; // Required according to https://github.com/getsentry/sentry-javascript/issues/4731#issuecomment-1075410543
import * as Sentry from '@sentry/node';
import { SENTRY_INIT_STATE } from '../modules/sentry/sentry.const';
import { SentryInitState } from '../modules/sentry/sentry.interface';
import { Environment } from '../config';

@Catch()
export class ExceptionHandlerFilter implements ExceptionFilter {
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
private readonly configService: ConfigService,
@Inject(SENTRY_INIT_STATE) private readonly sentryEnabled: SentryInitState
private readonly configService: ConfigService
) {}

private readonly logger = new Logger('Exception Filter');
Expand Down Expand Up @@ -69,9 +66,9 @@ export class ExceptionHandlerFilter implements ExceptionFilter {
message: exception.message
};

// In production, send to Sentry so long as it's enabled (if the DSN is
// invalid/empty it'll be disabled).
if (this.sentryEnabled) {
// In production, send to Sentry so long as it's enabled
// TODO: maybe still want to inject sentry init state token
if (Sentry.isInitialized()) {
eventID = Sentry.captureException(exception);
msg.eventID = eventID;
}
Expand Down
48 changes: 22 additions & 26 deletions apps/backend/src/app/interceptors/sentry.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
import * as Sentry from '@sentry/node';
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { Observable } from 'rxjs';
import { getIsolationScope } from '@sentry/node';
import { getDefaultIsolationScope } from '@sentry/core';

@Injectable()
export class SentryInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
if (getIsolationScope() === getDefaultIsolationScope()) {
Logger.warn(
'Isolation scope is still the default isolation scope, skipping setting transactionName.'
);
return next.handle();
}

// Based on https://github.com/ericjeker/nestjs-sentry-example/blob/main/src/sentry/sentry.interceptor.ts,
// but updated for Sentry 7, which majorly changed how transactions/spans are handled.
return Sentry.startSpan(
{
op: 'http.server',
name: `${method} ${url}`
},
(rootSpan: Sentry.Span) =>
Sentry.startSpan(
{
op: 'http.handler',
name: `${context.getClass().name}.${context.getHandler().name}`
},
(span: Sentry.Span) =>
next.handle().pipe(
tap(() => {
span.end();
rootSpan.end();
})
)
)
);
if (context.getType() !== 'http') {
return next.handle();
}

const req = context.switchToHttp().getRequest();
if (req.route) {
getIsolationScope().setTransactionName(
`${req.method?.toUpperCase() ?? 'UNKNOWN'} ${req.route.path}`
);
}

return next.handle();
}
}
2 changes: 0 additions & 2 deletions apps/backend/src/app/modules/sentry/sentry.const.ts

This file was deleted.

17 changes: 0 additions & 17 deletions apps/backend/src/app/modules/sentry/sentry.interface.ts

This file was deleted.

72 changes: 0 additions & 72 deletions apps/backend/src/app/modules/sentry/sentry.module.ts

This file was deleted.

Loading

0 comments on commit 363e5bf

Please sign in to comment.