diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..de3c787e --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# dependencies +/node_modules + +# IDE +/.idea +/.awcache +/.vscode + +# misc +npm-debug.log +.DS_Store + +# tests +/test +/coverage +/.nyc_output + +# dist +/dist \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0e705140 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Tine Jozelj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..eb3c2022 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +

+ Nest Logo + +

+ + +

Sentry Raven Module for Nest framework

+ +## Description + +This's a [Raven](https://github.com/getsentry/raven-node) module for [Nest](https://github.com/nestjs/nest). + +## Installation + +```bash +$ npm i --save nest-raven raven @types/raven +``` + +## Quick Start + +### Include Module +> app.module.ts + +```ts +@Module({ + imports: [ + ... + RavenModule.forRoot('https://your:sdn@sentry.io/290747'), + ] +}) +export class ApplicationModule implements NestModule { +} + +``` + +### Using Interceptor +> app.controller.ts + +```ts + @UseInterceptors(RavenInterceptor()) + @Get('/some/route') + public async someRoute() { + ... + } +``` + +With this setup, sentry will pick up all exceptions (even 400 types). + +##### Filters +Sometimes we don't want to catch all exceptions but only 500 or those +that we didn't handle properly. For that we can add filters on interceptor +to filter out good exceptions. + +> app.controller.ts + +```ts + @UseInterceptors(RavenInterceptor({ + filters: [ + // Filter exceptions of type HttpException. Ignore those that + // have status code of less than 500 + { type: HttpException, filter: (exception: HttpException) => 500 > exception.getStatus() } + ], + })) + @Get('/some/route') + public async someRoute() { + ... + } +``` + +##### Additional data +We can add additional data for each interceptor. + +Interceptor automatically adds `req` and `req.user` (as user) to additional data. + +Supported data: + * tags + * extra + * fingerprint + * level + +> app.controller.ts + +```ts + @UseInterceptors(RavenInterceptor({ + tags: { + type: 'fileUpload', + }, + level: 'warning', + })) + @Get('/some/route') + public async someRoute() + ... + } +``` + diff --git a/lib/raven.constants.ts b/lib/raven.constants.ts new file mode 100644 index 00000000..17f31273 --- /dev/null +++ b/lib/raven.constants.ts @@ -0,0 +1,2 @@ +export const RAVEN_SENTRY_PROVIDER = 'RAVEN_SENTRY_PROVIDER'; +export const RAVEN_SENTRY_CONFIG = 'RAVEN_SENTRY_CONFIG'; diff --git a/lib/raven.interceptor.abstract.ts b/lib/raven.interceptor.abstract.ts new file mode 100644 index 00000000..318be682 --- /dev/null +++ b/lib/raven.interceptor.abstract.ts @@ -0,0 +1,51 @@ +import { + ExecutionContext, Inject, Interceptor, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs/Observable'; +import { RAVEN_SENTRY_PROVIDER } from './raven.constants'; +import * as Raven from 'raven'; +import 'rxjs/add/operator/do'; +import { IRavenInterceptorOptions } from './raven.interfaces'; + +@Interceptor() +export abstract class AbstractRavenInterceptor implements NestInterceptor { + + protected abstract readonly options: IRavenInterceptorOptions = {}; + + constructor( + @Inject(RAVEN_SENTRY_PROVIDER) private ravenClient: Raven.Client, + ) { + } + + intercept( + dataOrRequest: any, + context: ExecutionContext, + stream$: Observable, + ): Observable | Promise> { + // first param would be for events, second is for errors + return stream$.do(null, (exception) => { + if (this.shouldReport(exception)) { + this.ravenClient.captureException( + exception as any, + { + req: dataOrRequest, + user: dataOrRequest.user, + tags: this.options.tags, + fingerprint: this.options.fingerprint, + level: this.options.level, + }); + } + }); + } + + private shouldReport(exception: any): boolean { + if (!this.options.filters) return true; + + // If all filters pass, then we do not report + return this.options.filters + .every(({ type, filter }) => { + return !(exception instanceof type && (!filter || filter(exception))); + }); + } +} diff --git a/lib/raven.interceptor.mixin.ts b/lib/raven.interceptor.mixin.ts new file mode 100644 index 00000000..cb9b244a --- /dev/null +++ b/lib/raven.interceptor.mixin.ts @@ -0,0 +1,12 @@ +import { mixin } from '@nestjs/common'; +import { AbstractRavenInterceptor } from './raven.interceptor.abstract'; +import { IRavenInterceptorOptions } from './raven.interfaces'; + +// tslint:disable-next-line:function-name +export function RavenInterceptor( + options?: IRavenInterceptorOptions, +) { + return mixin(class extends AbstractRavenInterceptor { + protected readonly options = options; + }); +} diff --git a/lib/raven.interfaces.ts b/lib/raven.interfaces.ts new file mode 100644 index 00000000..a2abc7a8 --- /dev/null +++ b/lib/raven.interfaces.ts @@ -0,0 +1,23 @@ +import * as Raven from 'raven'; + +export interface IRavenConfig { + dsn: string; + options: Raven.ConstructorOptions; +} + +export interface IRavenFilterFunction { + (exception: any): boolean; +} + +export interface IRavenInterceptorOptionsFilter { + type: any; + filter?: IRavenFilterFunction; +} + +export interface IRavenInterceptorOptions { + filters?: IRavenInterceptorOptionsFilter[]; + tags?: { [key: string]: string }; + extra?: { [key: string]: any }; + fingerprint?: string[]; + level?: string; +} diff --git a/lib/raven.module.ts b/lib/raven.module.ts new file mode 100644 index 00000000..3af6d84f --- /dev/null +++ b/lib/raven.module.ts @@ -0,0 +1,39 @@ +import { DynamicModule, Global, Module } from '@nestjs/common'; +import { ravenSentryProviders } from './raven.providers'; +import * as Raven from 'raven'; +import { RAVEN_SENTRY_CONFIG } from './raven.constants'; +import { RavenInterceptor } from './raven.interceptor.mixin'; +import { AbstractRavenInterceptor } from './raven.interceptor.abstract'; + +@Global() +@Module({ + components: [ + ...ravenSentryProviders, + AbstractRavenInterceptor, + RavenInterceptor, + ], + exports: [ + ...ravenSentryProviders, + AbstractRavenInterceptor, + RavenInterceptor, + ], +}) +export class RavenModule { + static forRoot(dsn?: string, options?: Raven.ConstructorOptions): DynamicModule { + return { + module: RavenModule, + components: [ + { + provide: RAVEN_SENTRY_CONFIG, + useValue: { dsn, options }, + }, + ], + exports: [ + { + provide: RAVEN_SENTRY_CONFIG, + useValue: { dsn, options }, + }, + ], + }; + } +} diff --git a/lib/raven.providers.ts b/lib/raven.providers.ts new file mode 100644 index 00000000..24c8ad88 --- /dev/null +++ b/lib/raven.providers.ts @@ -0,0 +1,13 @@ +import { RAVEN_SENTRY_CONFIG, RAVEN_SENTRY_PROVIDER } from './raven.constants'; +import * as Raven from 'raven'; +import { IRavenConfig } from './raven.interfaces'; + +export const ravenSentryProviders = [ + { + provide: RAVEN_SENTRY_PROVIDER, + useFactory: (config: IRavenConfig): Raven.Client => { + return Raven.config(config.dsn, config.options); + }, + inject: [RAVEN_SENTRY_CONFIG], + }, +]; diff --git a/package.json b/package.json new file mode 100644 index 00000000..442b76ea --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "nest-raven", + "version": "1.0.0", + "description": "Sentry Raven Module for Nest Framework", + "directories": { + "dist": "dist" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mentos1386/nest-raven.git" + }, + "keywords": [ + "nestjs", + "nest", + "raven", + "sentry", + "module" + ], + "author": "Tine Jozelj", + "license": "MIT", + "bugs": { + "url": "https://github.com/mentos1386/nest-raven/issues" + }, + "homepage": "https://github.com/mentos1386/nest-raven#readme", + "dependencies": { + "raven": "^2.4.1" + }, + "devDependencies": { + "@nestjs/common": "^4.5.0", + "rxjs": "^5.5.6", + "@types/raven": "^2.1.5", + "@types/node": "^7.0.41", + "ts-node": "^3.3.0", + "typescript": "^2.4.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..bc136152 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./lib", + "skipLibCheck": true + }, + "include": [ + "lib/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} \ No newline at end of file