Skip to content

Commit

Permalink
Add Sentry module to simplify integration with Sentry (#2334)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Thomas Dax <[email protected]>
  • Loading branch information
chernylu and thomasdax98 authored Aug 12, 2024
1 parent 6be41b6 commit 19d53c4
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 1 deletion.
28 changes: 28 additions & 0 deletions .changeset/happy-shrimps-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@comet/cms-api": minor
---

Add Sentry module to simplify integration with Sentry.

### Usage:

```ts
// main.ts

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

```ts
// app.module.ts

SentryModule.forRootAsync({
dsn: "sentry_dsn_url",
environment: "dev",
shouldReportException: (exception) => {
// Custom logic to determine if the exception should be reported
return true;
},
}),
```
1 change: 1 addition & 0 deletions demo/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@opentelemetry/auto-instrumentations-node": "^0.36.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.1",
"@opentelemetry/sdk-node": "^0.35.1",
"@sentry/node": "^7.118.0",
"apollo-server-core": "^3.0.0",
"apollo-server-express": "^3.0.0",
"async": "^3.0.0",
Expand Down
2 changes: 2 additions & 0 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
KubernetesModule,
PageTreeModule,
RedirectsModule,
SentryModule,
UserPermissionsModule,
} from "@comet/cms-api";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
Expand Down Expand Up @@ -168,6 +169,7 @@ export class AppModule {
return true;
},
}),
...(config.sentry ? [SentryModule.forRootAsync(config.sentry)] : []),
],
};
}
Expand Down
7 changes: 7 additions & 0 deletions demo/api/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) {
cdn: {
originCheckSecret: envVars.CDN_ORIGIN_CHECK_SECRET,
},
sentry:
envVars.SENTRY_DSN && envVars.SENTRY_ENVIRONMENT
? {
dsn: envVars.SENTRY_DSN,
environment: envVars.SENTRY_ENVIRONMENT,
}
: undefined,
};
}

Expand Down
8 changes: 8 additions & 0 deletions demo/api/src/config/environment-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,12 @@ export class EnvironmentVariables {
@ValidateIf((v) => v.AZURE_OPEN_AI_CONTENT_GENERATION_API_URL || v.AZURE_OPEN_AI_CONTENT_GENERATION_API_KEY)
@IsString()
AZURE_OPEN_AI_CONTENT_GENERATION_DEPLOYMENT_ID?: string;

@IsOptional()
@IsUrl()
SENTRY_DSN?: string;

@ValidateIf((v) => v.SENTRY_DSN)
@IsString()
SENTRY_ENVIRONMENT?: string;
}
5 changes: 5 additions & 0 deletions demo/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CdnGuard, ExceptionInterceptor, ValidationExceptionFactory } from "@com
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import * as Sentry from "@sentry/node";
import { AppModule } from "@src/app.module";
import { useContainer } from "class-validator";
import compression from "compression";
Expand All @@ -21,6 +22,10 @@ async function bootstrap(): Promise<void> {
const appModule = AppModule.forRoot(config);
const app = await NestFactory.create<NestExpressApplication>(appModule);

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

// class-validator should use Nest for dependency injection.
// See https://github.com/nestjs/nest/issues/528,
// https://github.com/typestack/class-validator#using-service-container.
Expand Down
47 changes: 47 additions & 0 deletions docs/docs/sentry/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Sentry Module
sidebar_position: 16
---

SentryModule is a NestJS module that integrates Sentry for error tracking. To use the module, import it into your application's root module and call the `forRootAsync` method. For more detailed configurations and advanced usage please visit [Sentry](https://docs.sentry.io/platforms/javascript/guides/node/configuration/).

```ts
// app.module.ts
@Module({
imports: [
SentryModule.forRootAsync({
dsn: "sentry_dsn_url",
environment: "dev",
shouldReportException: (exception) => {
// Custom logic to determine if the exception should be reported
return true;
},
}),
],
})
export class AppModule {}
```

In your main file, add Sentry handlers to capture requests and errors:

```diff
// main.ts
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
+ import * as Sentry from "@sentry/node";
import { AppModule } from "@src/app.module";
import { createConfig } from "@src/config/config";

async function bootstrap() {
const config = createConfig(process.env);
const appModule = AppModule.forRoot(config);
const app = await NestFactory.create<NestExpressApplication>(appModule);

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

await app.listen(3000);
}
bootstrap();
```
7 changes: 7 additions & 0 deletions packages/api/cms-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"@nestjs/graphql": "^10.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@sentry/node": "^7.0.0",
"@types/express": "^4.0.0",
"@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9",
Expand Down Expand Up @@ -136,11 +137,17 @@
"@nestjs/core": "^9.0.0",
"@nestjs/graphql": "^10.0.0",
"@nestjs/platform-express": "^9.0.0",
"@sentry/node": "^7.0.0",
"express": "^4.0.0",
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
"nestjs-console": "^8.0.0",
"rxjs": "^7.0.0"
},
"peerDependenciesMeta": {
"@sentry/node": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
Expand Down
1 change: 1 addition & 0 deletions packages/api/cms-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export { RedirectsModule } from "./redirects/redirects.module";
export { createRedirectsResolver } from "./redirects/redirects.resolver";
export { RedirectsService } from "./redirects/redirects.service";
export { IsValidRedirectSource, IsValidRedirectSourceConstraint } from "./redirects/validators/isValidRedirectSource";
export { SentryModule } from "./sentry/sentry.module";
export { AzureAiTranslatorModule } from "./translation/azure-ai-translator.module";
export { AbstractAccessControlService } from "./user-permissions/access-control.service";
export { AffectedEntity, AffectedEntityMeta, AffectedEntityOptions } from "./user-permissions/decorators/affected-entity.decorator";
Expand Down
1 change: 1 addition & 0 deletions packages/api/cms-api/src/sentry/sentry.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SENTRY_CONFIG = "sentry-config";
63 changes: 63 additions & 0 deletions packages/api/cms-api/src/sentry/sentry.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor, Optional } from "@nestjs/common";
import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql";
import { Observable, tap } from "rxjs";

import { SENTRY_CONFIG } from "./sentry.constants";
import { SentryConfig } from "./sentry.module";

@Injectable()
export class SentryInterceptor implements NestInterceptor {
constructor(@Optional() @Inject(SENTRY_CONFIG) private readonly config?: SentryConfig) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
tap({
error: async (error) => {
const shouldReport = this.config?.shouldReportException?.(error) ?? true;

if (shouldReport) {
const Sentry = await import("@sentry/node");

await this.configureSentryScopeBasedOnContext(context);

Sentry.captureException(error);
}
},
}),
);
}

private async configureSentryScopeBasedOnContext(context: ExecutionContext) {
const Sentry = await import("@sentry/node");

const type = context.getType<GqlContextType>();
const scope = Sentry.getCurrentScope();

if (type === "http") {
const request = context.switchToHttp().getRequest();

if (request.user) {
scope.setExtra("user", request.user);
}

scope.setExtra("method", request.method);
scope.setExtra("url", request.url);
scope.setExtra("body", request.body);
scope.setExtra("query", request.query);
scope.setExtra("params", request.params);
scope.setExtra("headers", request.headers);
} else if (type === "graphql") {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;

if (request.user) {
scope.setExtra("user", request.user);
}

scope.setExtra("args", gqlContext.getArgs());
scope.setExtra("root", gqlContext.getRoot());
scope.setExtra("context", gqlContext.getContext());
scope.setExtra("info", gqlContext.getInfo());
}
}
}
48 changes: 48 additions & 0 deletions packages/api/cms-api/src/sentry/sentry.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DynamicModule, Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
import type { NodeOptions } from "@sentry/node";

import { SENTRY_CONFIG } from "./sentry.constants";
import { SentryInterceptor } from "./sentry.interceptor";

type SentryNodeOptions = Omit<NodeOptions, "dsn" | "environment"> & {
dsn: string;
environment: string;
};

type Options = SentryNodeOptions & SentryConfig;

export type SentryConfig = {
shouldReportException?: (exception: unknown) => boolean;
};

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: SentryInterceptor,
},
],
})
export class SentryModule {
static async forRootAsync({ shouldReportException, ...options }: Options): Promise<DynamicModule> {
const Sentry = await import("@sentry/node");

const integrations = options.integrations ?? [new Sentry.Integrations.Http({ tracing: true })];

Sentry.init({
...options,
integrations,
});

return {
module: SentryModule,
providers: [
{
provide: SENTRY_CONFIG,
useValue: { shouldReportException },
},
],
};
}
}
61 changes: 60 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 19d53c4

Please sign in to comment.