Skip to content

Commit

Permalink
Add support for scope transformers (#99)
Browse files Browse the repository at this point in the history
* feat: add support for scope transformers

* style: fix linting issue in test

* docs: update README.md to provide information on transformers

* feat: add RavenTransformer decorator
  • Loading branch information
jeremylvln authored Aug 22, 2020
1 parent b7d34a0 commit 5808e76
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 35 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ to filter out good exceptions.
}
```

#### Transformers

It may be useful to add some extra data to the Sentry's context before sending
the payload. Adding some request-related properties for instance. To achieve
this we can add scope transformers on interceptor to injecte some data
dynamically.

> app.controller.ts
```ts
@UseInterceptors(new RavenInterceptor({
transformers: [
// Add an extra property to Sentry's scope
(scope: Scope) => { scope.addExtra('important key', 'useful value') }
],
}))
@Get('/some/route')
public async someRoute() {
...
}
```

#### Additional data

Interceptor automatically adds `req` and `req.user` (as user) to additional data.
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './raven.decorators';
export * from './raven.interceptor';
export * from './raven.interfaces';
export * from './raven.module';
8 changes: 8 additions & 0 deletions lib/raven.decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IRavenScopeTransformerFunction } from './raven.interfaces';
import { SetMetadata } from '@nestjs/common';

export const RAVEN_LOCAL_TRANSFORMERS_METADATA = 'raven-local-transformers';

export const RavenTransformer = (
...transformers: IRavenScopeTransformerFunction[]
) => SetMetadata(RAVEN_LOCAL_TRANSFORMERS_METADATA, transformers);
87 changes: 53 additions & 34 deletions lib/raven.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import {
Injectable,
NestInterceptor,
CallHandler,
Inject,
} from '@nestjs/common';
import { IRavenInterceptorOptions } from './raven.interfaces';
import {
IRavenInterceptorOptions,
IRavenScopeTransformerFunction,
} from './raven.interfaces';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import * as Sentry from '@sentry/minimal';
Expand All @@ -16,6 +20,8 @@ import {
} from '@nestjs/common/interfaces';
import { Handlers } from '@sentry/node';
import type { GraphQLArgumentsHost, GqlContextType } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { RAVEN_LOCAL_TRANSFORMERS_METADATA } from './raven.decorators';

let GqlArgumentsHost: any;
try {
Expand All @@ -24,54 +30,75 @@ try {

@Injectable()
export class RavenInterceptor implements NestInterceptor {
constructor(private readonly options: IRavenInterceptorOptions = {}) {}
constructor(
private readonly options: IRavenInterceptorOptions = {},
private readonly reflector: Reflector = new Reflector(),
) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const localTransformers = this.reflector.get<
IRavenScopeTransformerFunction[]
>(RAVEN_LOCAL_TRANSFORMERS_METADATA, context.getHandler());

// first param would be for events, second is for errors
return next.handle().pipe(
tap(null, (exception) => {
if (this.shouldReport(exception)) {
Sentry.withScope((scope) => {
switch (context.getType<GqlContextType>()) {
case 'http':
return this.captureHttpException(
this.addHttpExceptionMetadatas(scope, context.switchToHttp());
return this.captureException(
scope,
context.switchToHttp(),
exception,
localTransformers,
);
case 'ws':
return this.captureWsException(
this.addWsExceptionMetadatas(scope, context.switchToWs());
return this.captureException(
scope,
context.switchToWs(),
exception,
localTransformers,
);
case 'rpc':
return this.captureRpcException(
this.addRpcExceptionMetadatas(scope, context.switchToRpc());
return this.captureException(
scope,
context.switchToRpc(),
exception,
localTransformers,
);
case 'graphql':
if (!GqlArgumentsHost)
return this.captureException(scope, exception);
return this.captureGraphQLException(
return this.captureException(
scope,
exception,
localTransformers,
);
this.addGraphQLExceptionMetadatas(
scope,
GqlArgumentsHost.create(context),
);
return this.captureException(
scope,
exception,
localTransformers,
);
default:
return this.captureException(scope, exception);
return this.captureException(
scope,
exception,
localTransformers,
);
}
});
}
}),
);
}

private captureGraphQLException(
private addGraphQLExceptionMetadatas(
scope: Scope,
gqlHost: GraphQLArgumentsHost,
exception: any,
): void {
const context = gqlHost.getContext();
// Same as HttpException
Expand All @@ -89,14 +116,11 @@ export class RavenInterceptor implements NestInterceptor {
scope.setExtra('fieldName', info.fieldName);
const args = gqlHost.getArgs();
scope.setExtra('args', args);

this.captureException(scope, exception);
}

private captureHttpException(
private addHttpExceptionMetadatas(
scope: Scope,
http: HttpArgumentsHost,
exception: any,
): void {
const data = Handlers.parseRequest(
<any>{},
Expand All @@ -107,38 +131,33 @@ export class RavenInterceptor implements NestInterceptor {
scope.setExtra('req', data.request);
data.extra && scope.setExtras(data.extra);
if (data.user) scope.setUser(data.user);

this.captureException(scope, exception);
}

private captureRpcException(
scope: Scope,
rpc: RpcArgumentsHost,
exception: any,
): void {
private addRpcExceptionMetadatas(scope: Scope, rpc: RpcArgumentsHost): void {
scope.setExtra('rpc_data', rpc.getData());

this.captureException(scope, exception);
}

private captureWsException(
scope: Scope,
ws: WsArgumentsHost,
exception: any,
): void {
private addWsExceptionMetadatas(scope: Scope, ws: WsArgumentsHost): void {
scope.setExtra('ws_client', ws.getClient());
scope.setExtra('ws_data', ws.getData());

this.captureException(scope, exception);
}

private captureException(scope: Scope, exception: any): void {
private captureException(
scope: Scope,
exception: any,
localTransformers: IRavenScopeTransformerFunction[] | undefined,
): void {
if (this.options.level) scope.setLevel(this.options.level);
if (this.options.fingerprint)
scope.setFingerprint(this.options.fingerprint);
if (this.options.extra) scope.setExtras(this.options.extra);
if (this.options.tags) scope.setTags(this.options.tags);

if (this.options.transformers)
this.options.transformers.forEach((transformer) => transformer(scope));
if (localTransformers)
localTransformers.forEach((transformer) => transformer(scope));

Sentry.captureException(exception);
}

Expand Down
6 changes: 6 additions & 0 deletions lib/raven.interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Severity } from '@sentry/types';
import { Scope } from '@sentry/node';

export interface IRavenScopeTransformerFunction {
(scope: Scope): void;
}

export interface IRavenFilterFunction {
(exception: any): boolean;
Expand All @@ -11,6 +16,7 @@ export interface IRavenInterceptorOptionsFilter {

export interface IRavenInterceptorOptions {
filters?: IRavenInterceptorOptionsFilter[];
transformers?: IRavenScopeTransformerFunction[];
tags?: { [key: string]: string };
extra?: { [key: string]: any };
fingerprint?: string[];
Expand Down
42 changes: 41 additions & 1 deletion test/http/method.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
HttpStatus,
UseInterceptors,
} from '@nestjs/common';
import { RavenInterceptor } from '../../lib';
import { RavenInterceptor, RavenTransformer } from '../../lib';
import * as Sentry from '@sentry/types';

@Controller('')
Expand Down Expand Up @@ -42,6 +42,46 @@ export class MethodController {
);
}

@Get('transformer')
@UseInterceptors(
new RavenInterceptor({
transformers: [
(scope) => {
scope.setExtra('A', 'AAA');
},
],
}),
)
transformer() {
throw new Error('Something bad happened');
}

@Get('local-transformer')
@UseInterceptors(new RavenInterceptor())
@RavenTransformer((scope) => {
scope.setExtra('A', 'AAA');
})
localTransformer() {
throw new Error('Something bad happened');
}

@Get('combo-transformer')
@UseInterceptors(
new RavenInterceptor({
transformers: [
(scope) => {
scope.setExtra('A', 'AAA');
},
],
}),
)
@RavenTransformer((scope) => {
scope.setExtra('B', 'BBB');
})
comboTransformer() {
throw new Error('Something bad happened');
}

@Get('tags')
@UseInterceptors(
new RavenInterceptor({
Expand Down
40 changes: 40 additions & 0 deletions test/http/method.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ describe('Http:Method', () => {
expect(client.captureException.mock.calls).toEqual([]);
});

it(`/GET transformer`, async () => {
await request(app.getHttpServer()).get('/transformer').expect(500);

expect(client.captureException.mock.calls[0][0]).toMatchInlineSnapshot(
`[Error: Something bad happened]`,
);
expect(client.captureException.mock.calls[0][2]._extra).toHaveProperty(
'A',
'AAA',
);
});

it(`/GET local-transformer`, async () => {
await request(app.getHttpServer()).get('/local-transformer').expect(500);

expect(client.captureException.mock.calls[0][0]).toMatchInlineSnapshot(
`[Error: Something bad happened]`,
);
expect(client.captureException.mock.calls[0][2]._extra).toHaveProperty(
'A',
'AAA',
);
});

it(`/GET combo-transformer`, async () => {
await request(app.getHttpServer()).get('/combo-transformer').expect(500);

expect(client.captureException.mock.calls[0][0]).toMatchInlineSnapshot(
`[Error: Something bad happened]`,
);
expect(client.captureException.mock.calls[0][2]._extra).toHaveProperty(
'A',
'AAA',
);
expect(client.captureException.mock.calls[0][2]._extra).toHaveProperty(
'B',
'BBB',
);
});

it(`/GET tags`, async () => {
await request(app.getHttpServer()).get('/tags').expect(500);

Expand Down

0 comments on commit 5808e76

Please sign in to comment.