Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update sentry error handling #27

Merged
merged 1 commit into from
Mar 20, 2024
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 changes: 1 addition & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
CORS=*
SENTRY_DSN=
KEYCLOAK_URL=https://keycloak.aam-digital.net
KEYCLOAK_ADMIN=admin
KEYCLOAK_PASSWORD=PASSWORD
KEYCLOAK_PASSWORD=PASSWORD
5 changes: 4 additions & 1 deletion nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger"],
"assets": ["assets/*"]
"assets": [
"assets/*",
{ "include": "./config/*.yaml", "outDir": "./dist", "watchAssets": true }
]
}
}
3,651 changes: 1,507 additions & 2,144 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 3 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.0.5",
"@ntegral/nestjs-sentry": "^3.0.7",
"@sentry/node": "^7.11.1",
"@sentry/node": "^7.102.1",
"@sentry/tracing": "^7.102.1",
"js-yaml": "4.1.0",
"jwt-decode": "^3.1.2",
"passport-http-bearer": "^1.0.1",
"reflect-metadata": "^0.1.13",
Expand Down Expand Up @@ -60,13 +61,6 @@
"tsconfig-paths": "4.0.0",
"typescript": "^4.3.5"
},
"overrides": {
"@ntegral/nestjs-sentry": {
"@nestjs/common": "$@nestjs/common",
"@nestjs/core": "$@nestjs/core",
"@sentry/node": "$@sentry/node"
}
},
"jest": {
"moduleFileExtensions": [
"js",
Expand Down
45 changes: 6 additions & 39 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,17 @@
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { AccountModule } from './account/account.module';
import { SeverityLevel } from '@sentry/node';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SentryInterceptor, SentryModule } from '@ntegral/nestjs-sentry';
import { APP_INTERCEPTOR } from '@nestjs/core';

const lowSeverityLevels: SeverityLevel[] = ['log', 'info'];
import { ConfigModule } from '@nestjs/config';
import { AppConfiguration } from './config/configuration';

@Module({
providers: [
{ provide: APP_INTERCEPTOR, useFactory: () => new SentryInterceptor() },
],
imports: [
AuthModule,
AccountModule,
ConfigModule.forRoot({ isGlobal: true }),
SentryModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
if (!configService.get('SENTRY_DSN')) {
return;
}

return {
dsn: configService.get('SENTRY_DSN'),
debug: true,
environment: 'prod',
release: 'accounts@' + process.env.npm_package_version,
whitelistUrls: [/https?:\/\/(.*)\.?aam-digital\.com/],
initialScope: {
tags: {
// ID of the docker container in which this is run
hostname: process.env.HOSTNAME || 'unknown',
},
},
beforeSend: (event) => {
if (lowSeverityLevels.includes(event.level)) {
return null;
} else {
return event;
}
},
};
},
ConfigModule.forRoot({
isGlobal: true,
ignoreEnvFile: false,
load: [AppConfiguration],
}),
],
})
Expand Down
8 changes: 8 additions & 0 deletions src/config/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Logger Configuration
# values can be overwritten in .env file

SENTRY:
ENABLED: false
INSTANCE_NAME: local-development # can be personalised in .env -> local-development-<your-name>
ENVIRONMENT: local # local | development | production
DSN: ''
38 changes: 38 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';

const CONFIG_FILENAME = 'app.yaml';

/**
* loads local CONFIG_FILENAME file and provides them in NestJs Config Service
* See: /src/config/app.yaml
*/
export function AppConfiguration(): Record<string, string> {
return flatten(
yaml.load(readFileSync(join(__dirname, CONFIG_FILENAME), 'utf8')) as Record<
string,
string
>,
);
}

/**
* Recursively create a flat key-value object where keys contain nested keys as prefixes
*/
function flatten(
obj: any,
prefix = '',
delimiter = '_',
): Record<string, string> {
return Object.keys(obj).reduce((acc: any, k: string) => {
const pre = prefix.length ? prefix + delimiter : '';

if (typeof obj[k] === 'object')
Object.assign(acc, flatten(obj[k], pre + k));
else {
acc[pre + k] = obj[k];
}
return acc;
}, {});
}
11 changes: 8 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { SentryService } from '@ntegral/nestjs-sentry';
import { ConfigService } from '@nestjs/config';
import { AppConfiguration } from './config/configuration';
import { configureSentry } from './sentry.configuration';

async function bootstrap() {
// load ConfigService instance to access .env and app.yaml values
const configService = new ConfigService(AppConfiguration());

const app = await NestFactory.create(AppModule);

app.enableCors({ origin: process.env.CORS });
Expand All @@ -20,8 +25,8 @@ async function bootstrap() {
swaggerOptions: { persistAuthorization: true },
});

app.useLogger(SentryService.SentryServiceInstance());
configureSentry(app, configService);

await app.listen(3000);
await app.listen(process.env.PORT || 3000);
}
bootstrap();
68 changes: 68 additions & 0 deletions src/sentry.configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as Sentry from '@sentry/node';
import { ConfigService } from '@nestjs/config';
import { ArgumentsHost, INestApplication } from '@nestjs/common';
import { BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core';

export class SentryConfiguration {
ENABLED = false;
DSN = '';
INSTANCE_NAME = '';
ENVIRONMENT = '';
}

function loadSentryConfiguration(
configService: ConfigService,
): SentryConfiguration {
return {
ENABLED: configService.getOrThrow('SENTRY_ENABLED'),
DSN: configService.getOrThrow('SENTRY_DSN'),
INSTANCE_NAME: configService.getOrThrow('SENTRY_INSTANCE_NAME'),
ENVIRONMENT: configService.getOrThrow('SENTRY_ENVIRONMENT'),
};
}

export function configureSentry(
app: INestApplication,
configService: ConfigService,
): void {
const sentryConfiguration = loadSentryConfiguration(configService);
if (sentryConfiguration.ENABLED) {
configureLoggingSentry(app, sentryConfiguration);
}
}

export class SentryFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
Sentry.captureException(exception);
super.catch(exception, host);
}
}

function configureLoggingSentry(
app: INestApplication,
sentryConfiguration: SentryConfiguration,
): void {
Sentry.init({
debug: true,
serverName: sentryConfiguration.INSTANCE_NAME,
environment: sentryConfiguration.ENVIRONMENT,
dsn: sentryConfiguration.DSN,
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Console(),
new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.Express(),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
});

app.use(Sentry.Handlers.errorHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(Sentry.Handlers.requestHandler());

const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new SentryFilter(httpAdapter));
}
Loading