├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.MD ├── README.MD ├── lerna.json ├── package.json ├── packages ├── nestjs-sentry-graphql │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── README.MD │ ├── lib │ │ ├── graphql.interceptor.ts │ │ └── index.ts │ ├── package.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── nestjs-sentry │ ├── README.MD │ ├── lib │ │ ├── __tests__ │ │ │ ├── sentry.decorator.spec.ts │ │ │ ├── sentry.module.spec.ts │ │ │ └── sentry.service.spec.ts │ │ ├── index.ts │ │ ├── injectDecoratoryFactory.ts │ │ ├── sentry-core.module.ts │ │ ├── sentry.constants.ts │ │ ├── sentry.decorator.ts │ │ ├── sentry.interceptor.ts │ │ ├── sentry.interfaces.ts │ │ ├── sentry.module.ts │ │ ├── sentry.providers.ts │ │ ├── sentry.service.ts │ │ └── severity.enum.ts │ ├── package.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── tsconfig.build.json └── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | .prettierrc 3 | *.log 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint/eslint-plugin'], 4 | extends: [ 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:prettier/recommended' 8 | ], 9 | env: { 10 | node: true, 11 | jest: true 12 | }, 13 | rules: { 14 | "@typescript-eslint/no-explicit-any": "off", 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # IDE 4 | /.idea 5 | /.awcache 6 | /.vscode 7 | 8 | # misc 9 | npm-debug.log 10 | .DS_Store 11 | 12 | # tests outputs 13 | /coverage 14 | /.nyc_output 15 | test-schema.graphql 16 | *.test-definitions.ts 17 | 18 | # dist no longer gets checked in 19 | packages/**/dist 20 | **/*.tsbuildinfo 21 | 22 | *.log 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | yarn.lock 7 | tsconfig.json 8 | 9 | .eslintignore 10 | .eslintrc.js 11 | .prettierignore 12 | .prettierrc 13 | *.log 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/node_modules/** 3 | .eslintrc.js 4 | *.log 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "insertPragma": false, 5 | "jsxSingleQuote": false, 6 | "endOfLine": "lf", 7 | "printWidth": 100, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "requirePragma": false, 11 | "semi": true, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "none", 15 | "useTabs": false 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.1.1 4 | 5 | - Minor dependency updates 6 | - Bugfix - remove relative path import in package.json 7 | 8 | ## 4.1.0 9 | 10 | Stricter typing on Severity helper object 11 | 12 | - Specifically type the keys on the Severity psuedo-enum so Typescript can enforce valid access. This is a **breaking change** because invalid accesses that were previously allowed will no longer compile. 13 | 14 | ## 4.0.1 15 | 16 | Day zero patch 17 | 18 | - Restore valid uses of `any` type 19 | 20 | ## 4.0.0 21 | 22 | The initial port from @ntegral/nestjs-sentry. 23 | 24 | Changes: 25 | 26 | - Break project into a nestjs-sentry and a nestjs-sentry-graphql package. 27 | - Update dependencies to Nest 9.x and latest Sentry. 28 | - Move to lerna for workspace management. 29 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Traveler Dev Ltd. (England 13120175) 2 | Copyright (c) 2019, Ntegral Inc. c/o Dexter Hardy 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 7 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | [![npm version](http://img.shields.io/npm/v/@travelerdev/nestjs-sentry.svg?style=flat)](https://npmjs.org/package/@travelerdev/nestjs-sentry 'View this project on npm') 2 | [![ISC license](http://img.shields.io/badge/license-ISC-brightgreen.svg)](http://opensource.org/licenses/ISC) 3 | 4 |

5 |

6 | @travelerdev/nestjs-sentry 7 |

8 | 9 |

10 | Provides an injectable sentry.io client to provide both automated and manual enterprise logging of nestjs modules, optionally with GraphQL support 11 |

12 |

13 | 14 | ## About 15 | 16 | `@travelerdev/nestjs-sentry` is built upon the foundation developed by [`@ntegral/nestjs-sentry`](https://github.com/ntegral/nestjs-sentry). 17 | 18 | For more information about how to use it, view the README in the specific package for you - either the `nestjs-sentry` package if you do not use graphql, or the `nestjs-sentry-graphql` if you do. 19 | 20 | ## Acknowledgements 21 | 22 | - [nestjs](https://nestjs.com) 23 | - [`@sentry/node`](https://github.com/getsentry/sentry-javascript) 24 | - [`@ntegral/nestjs-sentry`](https://github.com/ntegral/nestjs-sentry) 25 | 26 | Copyright © 2019 Ntegral Inc. and 2022 Traveler Dev Ltd. (England 13120175) 27 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "4.3.0", 4 | "npmClient": "yarn", 5 | "changelog": { 6 | "labels": { 7 | "feature": "Features", 8 | "bug": "Bug fixes", 9 | "enhancement": "Enhancements", 10 | "dependencies": "Dependencies" 11 | } 12 | }, 13 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@travelerdev/nestjs-sentry-workspace", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "The workspace for the GraphQL and non-GraphQL instances of nestjs-sentry", 6 | "author": "Zack Sheppard (Traveler Dev Ltd)", 7 | "license": "ISC", 8 | "workspaces": [ 9 | "packages/*" 10 | ], 11 | "scripts": { 12 | "build": "tsc -b -v packages/nestjs-sentry packages/nestjs-sentry-graphql", 13 | "clean": "tsc -b --clean packages", 14 | "format": "prettier packages/**/*.ts --ignore-path ./.prettierignore --write", 15 | "lint": "eslint 'packages/**/*.ts'", 16 | "lint:fix": "eslint 'packages/**/*.ts' --fix", 17 | "prepublish:npm": "yarn build", 18 | "publish:npm": "lerna publish", 19 | "test": "lerna run test --parallel" 20 | }, 21 | "devDependencies": { 22 | "@nestjs/common": "^10.0.0", 23 | "@nestjs/core": "^10.0.0", 24 | "@nestjs/testing": "^10.0.0", 25 | "@sentry/hub": "^7.12.0", 26 | "@sentry/node": "^7.12.0", 27 | "@types/jest": "^29.0.0", 28 | "@types/node": "^18.0.6", 29 | "@types/supertest": "^2.0.12", 30 | "@typescript-eslint/eslint-plugin": "^6.14.0", 31 | "@typescript-eslint/parser": "^6.14.0", 32 | "eslint": "^8.43.0", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-import": "^2.26.0", 35 | "eslint-plugin-prettier": "^5.0.1", 36 | "jest": "^29.5.0", 37 | "lerna": "^8.0.0", 38 | "lint-staged": "^15.2.0", 39 | "prettier": "^3.1.1", 40 | "reflect-metadata": "^0.1.12", 41 | "rxjs": "^7.1.0", 42 | "supertest": "^6.2.4", 43 | "ts-jest": "^29.1.0", 44 | "tsconfig-paths": "^4.1.0", 45 | "typescript": "^5.0.0" 46 | }, 47 | "changelog": { 48 | "labels": { 49 | "feature": "Features", 50 | "bug": "Bug fixes", 51 | "enhancement": "Enhancements", 52 | "docs": "Docs", 53 | "dependencies": "Dependencies" 54 | } 55 | }, 56 | "lint-staged": { 57 | "*.ts": [ 58 | "prettier --write", 59 | "eslint --fix" 60 | ] 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/travelerdev/nestjs-sentry" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "lib", 73 | "testRegex": ".spec.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/.prettierignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | node_modules/** -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 100, 4 | "proseWrap": "always", 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "useTabs": false 10 | } -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/README.MD: -------------------------------------------------------------------------------- 1 |

2 |

3 | @travelerdev/nestjs-sentry-graphql 4 |

5 | 6 |

7 | Provides an injectable sentry.io client to provide enterprise logging of nestjs modules using GraphQL 8 |

9 |

10 | 11 | ## About 12 | 13 | `@travelerdev/nestjs-sentry-graphql` is an extension of `@travelerdev/nestjs-sentry`, which is 14 | itself built upon the foundation developed by 15 | [`@ntegral/nestjs-sentry`](https://github.com/ntegral/nestjs-sentry). 16 | 17 | This package implements a module, `SentryModule` which when imported into your nestjs project 18 | provides a Sentry.io client to any class that injects it. This lets Sentry.io be worked into your 19 | dependency injection workflow without having to do any extra work outside of the initial setup. 20 | 21 | It also implements a class, `GraphqlInterceptor`, which can intercept resolver errors into Sentry. 22 | 23 | If you do not use graphql, you should consider using `@travelerdev/nestjs-sentry` instead to avoid 24 | unnecessary dependencies. 25 | 26 | ## Getting Started 27 | 28 | For details getting started instructions, see `@travelerdev/nestjs-sentry` 29 | 30 | ### Quick Start 31 | 32 | ```bash 33 | npm install --save @travelerdev/nestjs-sentry-graphql @nestjs/graphql 34 | ``` 35 | 36 | To get started with `@travelerdev/nestjs-sentry-graphql` you should add an import of 37 | `SentryModule.forRoot` to your app's root module. 38 | 39 | ```typescript 40 | import { Module } from '@nestjs-common'; 41 | import { SentryModule } from '@travelerdev/nestjs-sentry-graphql'; 42 | 43 | @Module({ 44 | imports: [ 45 | SentryModule.forRoot({ 46 | dsn: '<< your sentry_io_dsn >>', 47 | debug: true | false, 48 | environment: 'dev' | 'production' | 'some_environment', 49 | release: 'some_release', | null, // must first create a release in sentry.io dashboard 50 | logLevels: ["debug"] //based on sentry.io loglevel // 51 | }), 52 | ], 53 | }) 54 | export class AppModule {} 55 | ``` 56 | 57 | There are other instantiation methods documented in `@travelerdev/nestjs-sentry`'s readme. 58 | 59 | ### GraphQL Interceptor 60 | 61 | The GraphqlInterceptor in this package can be used at the App level to intercept resolver errors and 62 | pass them up to Sentry. 63 | 64 | Using graphql interceptor globally: 65 | 66 | ```typescript 67 | import { Module } from '@nestjs/common'; 68 | import { APP_INTERCEPTOR } from '@nestjs/core'; 69 | import { GraphqlInterceptor } from '@travelerdev/nestjs-sentry-graphql'; 70 | 71 | @Module({ 72 | .... 73 | providers: [ 74 | { 75 | provide: APP_INTERCEPTOR, 76 | useFactory: () => new GraphqlInterceptor(), 77 | }, 78 | ], 79 | }) 80 | export class AppModule {} 81 | ``` 82 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/lib/graphql.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; 3 | 4 | // Sentry imports 5 | import { Scope } from '@sentry/hub'; 6 | import { Handlers } from '@sentry/node'; 7 | 8 | import { SentryInterceptor } from '@travelerdev/nestjs-sentry'; 9 | 10 | @Injectable() 11 | export class GraphqlInterceptor extends SentryInterceptor { 12 | protected captureException(context: ExecutionContext, scope: Scope, exception: unknown) { 13 | if (context.getType() === 'graphql') { 14 | this.captureGraphqlException(scope, GqlExecutionContext.create(context), exception); 15 | } else { 16 | super.captureException(context, scope, exception); 17 | } 18 | } 19 | 20 | private captureGraphqlException( 21 | scope: Scope, 22 | gqlContext: GqlExecutionContext, 23 | exception: unknown, 24 | ): void { 25 | const info = gqlContext.getInfo(); 26 | const context = gqlContext.getContext(); 27 | 28 | scope.setExtra('type', info.parentType.name); 29 | 30 | if (context.req) { 31 | // req within graphql context needs modification in 32 | const data = Handlers.parseRequest({}, context.req, {}); 33 | 34 | scope.setExtra('req', data.request); 35 | 36 | if (data.extra) scope.setExtras(data.extra); 37 | if (data.user) scope.setUser(data.user); 38 | } 39 | 40 | this.client.instance().captureException(exception); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@travelerdev/nestjs-sentry'; 2 | export * from './graphql.interceptor'; 3 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": [ 3 | { 4 | "name": "Dexter Hardy", 5 | "email": "dexter.hardy@ntegral.com", 6 | "url": "http://www.ntegral.com" 7 | }, 8 | { 9 | "name": "Zack Sheppard", 10 | "email": "zack@traveler.dev", 11 | "url": "https://www.traveler.dev" 12 | } 13 | ], 14 | "name": "@travelerdev/nestjs-sentry-graphql", 15 | "version": "4.3.0", 16 | "description": "Provides an injectable sentry.io client to provide enterprise logging of nestjs modules with GraphQL", 17 | "main": "./dist/index.js", 18 | "typings": "./dist/index.d.ts", 19 | "directories": { 20 | "dist": "dist", 21 | "lib": "lib" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/travelerdev/nestjs-sentry" 29 | }, 30 | "scripts": { 31 | "build": "tsc -p tsconfig.build.json", 32 | "clean": "rm -rf dist", 33 | "format": "prettier --write \"lib/**/*.ts\"", 34 | "publish:npm": "npm publish --access public", 35 | "test": "jest --passWithNoTests", 36 | "test:watch": "jest --watch --passWithNoTests", 37 | "test:cov": "jest --coverage --passWithNoTests", 38 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 39 | }, 40 | "keywords": [ 41 | "nestjs", 42 | "sentry.io" 43 | ], 44 | "author": "Zack Sheppard", 45 | "license": "ISC", 46 | "dependencies": { 47 | "@travelerdev/nestjs-sentry": "^4.3.0" 48 | }, 49 | "peerDependencies": { 50 | "@nestjs/common": "^9.0.0 || ^10.0.0", 51 | "@nestjs/graphql": "^10.0.0 || ^11.0.0 || ^12.0.0", 52 | "@sentry/hub": "^7.12.0", 53 | "@sentry/node": "^7.12.0", 54 | "reflect-metadata": "^0.1.13", 55 | "rxjs": "^7.2.0" 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "devDependencies": { 61 | "@nestjs/graphql": "^12.0.0", 62 | "graphql": "^16.0.0" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "lib", 71 | "testRegex": ".spec.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "coverageDirectory": "../coverage", 76 | "testEnvironment": "node" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "sourceMap": true, 6 | "inlineSources": true, 7 | "sourceRoot": "/", 8 | "noImplicitAny": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/nestjs-sentry-graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "include": ["./lib/**/*"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "rootDir": "./lib", 7 | "outDir": "./dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/README.MD: -------------------------------------------------------------------------------- 1 | [![npm version](http://img.shields.io/npm/v/@travelerdev/nestjs-sentry.svg?style=flat)](https://npmjs.org/package/@travelerdev/nestjs-sentry 'View this project on npm') 2 | [![ISC license](http://img.shields.io/badge/license-ISC-brightgreen.svg)](http://opensource.org/licenses/ISC) 3 | 4 |

5 |

6 | @travelerdev/nestjs-sentry 7 |

8 | 9 |

10 | Provides an injectable sentry.io client to provide both automated and manual enterprise logging of nestjs modules 11 |

12 |

13 | 14 | ## Table Of Contents 15 | 16 | - [About](#about) 17 | - [GraphQL Support](#graphql-support) 18 | - [NestJS 9 Support](#nestjs-9-support) 19 | - [Installation](#installation) 20 | - [Getting Started](#getting-started) 21 | - [Contributing](#contributing) 22 | - [License](#license) 23 | - [Acknowledgements](#acknowledgements) 24 | 25 | ## About 26 | 27 | `@travelerdev/nestjs-sentry` is built upon the foundation developed by [`@ntegral/nestjs-sentry`](https://github.com/ntegral/nestjs-sentry). 28 | Both packages implement a module, `SentryModule` which when imported into 29 | your nestjs project provides a Sentry.io client to any class that injects it. This 30 | lets Sentry.io be worked into your dependency injection workflow without having to 31 | do any extra work outside of the initial setup. 32 | 33 | It can optionally also intercept error messages logged by your system and automatically propogate those to Sentry. 34 | 35 | ### GraphQL Support 36 | 37 | If you are writing a server that uses `@nestjs/graphql`, you probably want to use the package `@travelerdev/nestjs-sentry-graphql` instead. 38 | It contains all the same code as this package but adds an interceptor specifically for GraphQL resolvers. It has been separated out into its own package 39 | so that depending on this package does not introduce any dependencies on `@nestjs/graphql`. 40 | 41 | ## NestJS 9 Support 42 | 43 | This package begins at version 4.x.x and supports NestJS 9+. If you need support for NestJS 8 or 7, please visit [`@ntegral/nestjs-sentry`](https://github.com/ntegral/nestjs-sentry) for support. 44 | 45 | ## Installation 46 | 47 | ```bash 48 | npm install --save @travelerdev/nestjs-sentry @sentry/node 49 | ``` 50 | 51 | ## Getting Started 52 | 53 | To get started with `@travelerdev/nestjs-sentry` you should add an import of `SentryModule.forRoot` to your app's root module. 54 | 55 | ```typescript 56 | import { Module } from '@nestjs-common'; 57 | import { SentryModule } from '@travelerdev/nestjs-sentry'; 58 | 59 | @Module({ 60 | imports: [ 61 | SentryModule.forRoot({ 62 | dsn: '<< your sentry_io_dsn >>', 63 | debug: true | false, 64 | environment: 'dev' | 'production' | 'some_environment', 65 | release: 'some_release', | null, // must first create a release in sentry.io dashboard 66 | logLevels: ["debug"] //based on sentry.io loglevel // 67 | }), 68 | ], 69 | }) 70 | export class AppModule {} 71 | ``` 72 | 73 | You can alternatively use an async config factory if you need injected dependencies: 74 | 75 | ```typescript 76 | import { Module } from '@nestjs-common'; 77 | import { SentryModule } from '@travelerdev/nestjs-sentry'; 78 | import { ConfigModule } from '@nestjs/config'; 79 | import { ConfigService } from '@nestjs/config'; 80 | 81 | @Module({ 82 | imports: [ 83 | SentryModule.forRootAsync({ 84 | imports: [ConfigModule], 85 | useFactory: async (cfg:ConfigService) => ({ 86 | dsn: cfg.get('SENTRY_DSN'), 87 | debug: true | false, 88 | environment: 'dev' | 'production' | 'some_environment', 89 | release: 'some_release', | null, // must create a release in sentry.io dashboard 90 | logLevels: ["debug"] //based on sentry.io loglevel // 91 | }), 92 | inject: [ConfigService], 93 | }) 94 | ] 95 | }) 96 | 97 | export class AppModule {} 98 | ``` 99 | 100 | After importing, you can then inject the Sentry client into any of your injectables with the provided decorator: 101 | 102 | ```typescript 103 | import { Injectable } from '@nestjs/common'; 104 | import { InjectSentry, SentryService } from '@travelerdev/nestjs-sentry'; 105 | 106 | @Injectable() 107 | export class AppService { 108 | public constructor(@InjectSentry() private readonly client: SentryService) { 109 | client.instance().captureMessage(message, Sentry.Severity.Log); 110 | client.instance().captureException(exception); 111 | ... and more 112 | } 113 | } 114 | ``` 115 | 116 | To automatically absorb messages from your service into Sentry, you can instruct Nest to use the SentryService as the default logger: 117 | 118 | ```typescript 119 | async function bootstrap() { 120 | const app = await NestFactory.create(AppModule, { logger: false }); 121 | 122 | app.useLogger(SentryService.SentryServiceInstance()); 123 | await app.listen(3000); 124 | } 125 | bootstrap(); 126 | ``` 127 | 128 | You can use the various logging and breadcrumbing methods to create helpful debug information in Sentry: 129 | 130 | ```typescript 131 | import { Injectable } from '@nestjs/common'; 132 | import { InjectSentry, SentryService } from '@travelerdev/nestjs-sentry'; 133 | import { Severity } from '@sentry/types'; 134 | 135 | @Injectable() 136 | export class AppService { 137 | constructor(@InjectSentry() private readonly client: SentryService) { 138 | client.log('AppSevice Loaded', 'test', true); // creates log asBreadcrumb // 139 | client.instance().addBreadcrumb({ 140 | level: Severity.Debug, 141 | message: 'How to use native breadcrumb', 142 | data: { context: 'WhatEver' } 143 | }); 144 | client.debug('AppService Debug', 'context'); 145 | } 146 | } 147 | ``` 148 | 149 | ## Flushing sentry 150 | 151 | Sentry does not flush all the errors by itself, it does it in background so that it doesn't block the main thread. If 152 | you kill the nestjs app forcefully some exceptions don't have to be flushed and logged successfully. 153 | 154 | If you want to force that behaviour use the close flag in your options. That is handy if using nestjs as a console 155 | runner. Keep in mind that you need to have `app.enableShutdownHooks();` enabled in order 156 | for closing (flushing) to work. 157 | 158 | ```typescript 159 | import { Module } from '@nestjs-common'; 160 | import { SentryModule } from '@travelerdev/nestjs-sentry'; 161 | import { LogLevel } from '@sentry/types'; 162 | 163 | @Module({ 164 | imports: [ 165 | SentryModule.forRoot({ 166 | dsn: 'sentry_io_dsn', 167 | debug: true | false, 168 | environment: 'dev' | 'production' | 'some_environment', 169 | release: 'some_release', | null, // must create a release in sentry.io dashboard 170 | logLevel: LogLevel.Debug //based on sentry.io loglevel // 171 | close: { 172 | enabled: true, 173 | // Time in milliseconds to forcefully quit the application 174 | timeout?: number, 175 | } 176 | }), 177 | ], 178 | }) 179 | export class AppModule {} 180 | ``` 181 | 182 | ## Contributing 183 | 184 | This project is itself a fork of a long-lived open source projects, and so contributions are always welcome. 185 | They are the only way to keep this project alive and thriving. If you want to contribute, please follow these steps: 186 | 187 | 1. Fork the repository 188 | 2. Create your branch (`git checkout -b my-feature-name`) 189 | 3. Commit any changes to your branch 190 | 4. Push your changes to your remote branch 191 | 5. Open a pull request 192 | 193 | ## License 194 | 195 | Distributed under the ISC License. See `LICENSE` for more information. 196 | 197 | ## Acknowledgements 198 | 199 | - [nestjs](https://nestjs.com) 200 | - [@sentry/node](https://github.com/getsentry/sentry-javascript) 201 | 202 | Copyright © 2019 Ntegral Inc. and 2022 Traveler Dev Ltd. (England 13120175) 203 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/__tests__/sentry.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectSentry } from '../sentry.decorator'; 4 | import { SentryModule } from '../sentry.module'; 5 | import { SentryService } from '../sentry.service'; 6 | import { SentryModuleOptions } from '../sentry.interfaces'; 7 | 8 | describe('InjectS3', () => { 9 | const config: SentryModuleOptions = { 10 | dsn: 'https://45740e3ae4864e77a01ad61a47ea3b7e@o115888.ingest.sentry.io/25956308132020', 11 | debug: true, 12 | environment: 'development', 13 | logLevels: ['debug'] 14 | }; 15 | let module: TestingModule; 16 | 17 | @Injectable() 18 | class InjectableService { 19 | public constructor(@InjectSentry() public readonly client: SentryService) {} 20 | } 21 | 22 | beforeEach(async () => { 23 | module = await Test.createTestingModule({ 24 | imports: [SentryModule.forRoot(config)], 25 | providers: [InjectableService] 26 | }).compile(); 27 | }); 28 | 29 | describe('when decorating a class constructor parameter', () => { 30 | it('should inject the sentry client', () => { 31 | const testService = module.get(InjectableService); 32 | expect(testService).toHaveProperty('client'); 33 | expect(testService.client).toBeInstanceOf(SentryService); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/__tests__/sentry.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { SentryModule } from '../sentry.module'; 4 | import { SentryService } from '../sentry.service'; 5 | import { SENTRY_TOKEN } from '../sentry.constants'; 6 | import { Module } from '@nestjs/common'; 7 | import { SentryModuleOptions, SentryOptionsFactory } from '../sentry.interfaces'; 8 | 9 | describe('SentryModule', () => { 10 | const config: SentryModuleOptions = { 11 | dsn: 'https://45740e3ae4864e77a01ad61a47ea3b7e@o115888.ingest.sentry.io/25956308132020', 12 | debug: true, 13 | environment: 'development', 14 | logLevels: ['debug'] 15 | }; 16 | 17 | class TestService implements SentryOptionsFactory { 18 | createSentryModuleOptions(): SentryModuleOptions { 19 | return config; 20 | } 21 | } 22 | 23 | @Module({ 24 | exports: [TestService], 25 | providers: [TestService] 26 | }) 27 | class TestModule {} 28 | 29 | describe('forRoot', () => { 30 | it('should provide the sentry client', async () => { 31 | const mod = await Test.createTestingModule({ 32 | imports: [SentryModule.forRoot(config)] 33 | }).compile(); 34 | 35 | const sentry = mod.get(SENTRY_TOKEN); 36 | console.log('sentry', sentry); 37 | expect(sentry).toBeDefined(); 38 | expect(sentry).toBeInstanceOf(SentryService); 39 | }); 40 | }); 41 | 42 | describe('forRootAsync', () => { 43 | describe('when the `useFactory` option is used', () => { 44 | it('should provide sentry client', async () => { 45 | const mod = await Test.createTestingModule({ 46 | imports: [ 47 | SentryModule.forRootAsync({ 48 | useFactory: () => config 49 | }) 50 | ] 51 | }).compile(); 52 | 53 | const sentry = mod.get(SENTRY_TOKEN); 54 | expect(sentry).toBeDefined(); 55 | expect(sentry).toBeInstanceOf(SentryService); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('when the `useClass` option is used', () => { 61 | it('should provide the sentry client', async () => { 62 | const mod = await Test.createTestingModule({ 63 | imports: [ 64 | SentryModule.forRootAsync({ 65 | useClass: TestService 66 | }) 67 | ] 68 | }).compile(); 69 | 70 | const sentry = mod.get(SENTRY_TOKEN); 71 | expect(sentry).toBeDefined(); 72 | expect(sentry).toBeInstanceOf(SentryService); 73 | }); 74 | }); 75 | 76 | describe('when the `useExisting` option is used', () => { 77 | it('should provide the stripe client', async () => { 78 | const mod = await Test.createTestingModule({ 79 | imports: [ 80 | SentryModule.forRootAsync({ 81 | imports: [TestModule], 82 | useExisting: TestService 83 | }) 84 | ] 85 | }).compile(); 86 | 87 | const sentry = mod.get(SENTRY_TOKEN); 88 | expect(sentry).toBeDefined(); 89 | expect(sentry).toBeInstanceOf(SentryService); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/__tests__/sentry.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SentryModule } from '../sentry.module'; 3 | import { SentryService } from '../sentry.service'; 4 | import { SENTRY_TOKEN } from '../sentry.constants'; 5 | 6 | import * as Sentry from '@sentry/node'; 7 | import { SentryModuleOptions, SentryOptionsFactory } from '../sentry.interfaces'; 8 | 9 | jest.spyOn(Sentry, 'close').mockImplementation(() => Promise.resolve(true)); 10 | // const mockCloseSentry = Sentry.close as jest.MockedFunction; 11 | 12 | const SENTRY_NOT_CONFIGURE_ERROR = 'Please confirm that Sentry is configured correctly'; 13 | 14 | describe('SentryService', () => { 15 | const config: SentryModuleOptions = { 16 | dsn: 'https://45740e3ae4864e77a01ad61a47ea3b7e@o115888.ingest.sentry.io/25956308132020', 17 | debug: true, 18 | environment: 'development', 19 | logLevels: ['debug'] 20 | }; 21 | 22 | const failureConfig: SentryModuleOptions = { 23 | dsn: 'https://sentry_io_dsn@sentry.io/1512xxx', 24 | debug: true, 25 | environment: 'development', 26 | logLevels: ['debug'] 27 | }; 28 | 29 | // class TestService implements SentryOptionsFactory { 30 | // createSentryModuleOptions(): SentryModuleOptions { 31 | // return config; 32 | // } 33 | // } 34 | 35 | class FailureService implements SentryOptionsFactory { 36 | createSentryModuleOptions(): SentryModuleOptions { 37 | return failureConfig; 38 | } 39 | } 40 | 41 | describe('sentry.log:error', () => { 42 | it('should provide the sentry client and call log', async () => { 43 | const mod = await Test.createTestingModule({ 44 | imports: [ 45 | SentryModule.forRootAsync({ 46 | useClass: FailureService 47 | }) 48 | ] 49 | }).compile(); 50 | 51 | const fail = mod.get(SENTRY_TOKEN); 52 | console.log('sentry:error', fail); 53 | fail.log('sentry:log'); 54 | expect(fail.log).toBeInstanceOf(Function); 55 | }); 56 | }); 57 | 58 | describe('sentry.log', () => { 59 | it('should provide the sentry client and call log', async () => { 60 | const mod = await Test.createTestingModule({ 61 | imports: [ 62 | SentryModule.forRoot({ 63 | ...config 64 | }) 65 | ] 66 | }).compile(); 67 | 68 | const sentry = mod.get(SENTRY_TOKEN); 69 | expect(sentry).toBeDefined(); 70 | expect(sentry).toBeInstanceOf(SentryService); 71 | console.log('sentry', sentry); 72 | sentry.log('sentry:log'); 73 | expect(sentry.log).toBeInstanceOf(Function); 74 | expect(true).toBeTruthy(); 75 | }); 76 | }); 77 | 78 | describe('sentry.error', () => { 79 | it('should provide the sentry client and call error', async () => { 80 | const mod = await Test.createTestingModule({ 81 | imports: [ 82 | SentryModule.forRoot({ 83 | ...config 84 | }) 85 | ] 86 | }).compile(); 87 | 88 | const sentry = mod.get(SENTRY_TOKEN); 89 | expect(sentry).toBeDefined(); 90 | expect(sentry).toBeInstanceOf(SentryService); 91 | sentry.error('sentry:error'); 92 | expect(sentry.error).toBeInstanceOf(Function); 93 | expect(true).toBeTruthy(); 94 | }); 95 | }); 96 | 97 | describe('sentry.verbose', () => { 98 | it('should provide the sentry client and call verbose', async () => { 99 | const mod = await Test.createTestingModule({ 100 | imports: [ 101 | SentryModule.forRoot({ 102 | ...config 103 | }) 104 | ] 105 | }).compile(); 106 | 107 | const sentry = mod.get(SENTRY_TOKEN); 108 | expect(sentry).toBeDefined(); 109 | expect(sentry).toBeInstanceOf(SentryService); 110 | sentry.verbose('sentry:verbose', 'context:verbose'); 111 | expect(sentry.verbose).toBeInstanceOf(Function); 112 | expect(true).toBeTruthy(); 113 | }); 114 | }); 115 | 116 | describe('sentry.debug', () => { 117 | it('should provide the sentry client and call debug', async () => { 118 | const mod = await Test.createTestingModule({ 119 | imports: [ 120 | SentryModule.forRoot({ 121 | ...config 122 | }) 123 | ] 124 | }).compile(); 125 | 126 | const sentry = mod.get(SENTRY_TOKEN); 127 | expect(sentry).toBeDefined(); 128 | expect(sentry).toBeInstanceOf(SentryService); 129 | sentry.debug('sentry:debug', 'context:debug'); 130 | expect(sentry.debug).toBeInstanceOf(Function); 131 | expect(true).toBeTruthy(); 132 | }); 133 | }); 134 | 135 | describe('sentry.warn', () => { 136 | it('should provide the sentry client and call warn', async () => { 137 | const mod = await Test.createTestingModule({ 138 | imports: [ 139 | SentryModule.forRoot({ 140 | ...config 141 | }) 142 | ] 143 | }).compile(); 144 | 145 | const sentry = mod.get(SENTRY_TOKEN); 146 | expect(sentry).toBeDefined(); 147 | expect(sentry).toBeInstanceOf(SentryService); 148 | try { 149 | sentry.warn('sentry:warn', 'context:warn'); 150 | expect(true).toBeTruthy(); 151 | } catch (err) {} 152 | expect(sentry.warn).toBeInstanceOf(Function); 153 | }); 154 | }); 155 | 156 | describe('sentry.close', () => { 157 | it('should not close the sentry if not specified in config', async () => { 158 | const mod = await Test.createTestingModule({ 159 | imports: [SentryModule.forRoot(config)] 160 | }).compile(); 161 | await mod.enableShutdownHooks(); 162 | 163 | const sentry = mod.get(SENTRY_TOKEN); 164 | expect(sentry).toBeDefined(); 165 | expect(sentry).toBeInstanceOf(SentryService); 166 | await mod.close(); 167 | // expect(mockCloseSentry).not.toHaveBeenCalled(); 168 | }); 169 | 170 | it('should close the sentry if specified in config', async () => { 171 | const timeout = 100; 172 | const mod = await Test.createTestingModule({ 173 | imports: [ 174 | SentryModule.forRoot({ 175 | ...config, 176 | close: { 177 | enabled: true, 178 | timeout 179 | } 180 | }) 181 | ] 182 | }).compile(); 183 | await mod.enableShutdownHooks(); 184 | 185 | const sentry = mod.get(SENTRY_TOKEN); 186 | expect(sentry).toBeDefined(); 187 | expect(sentry).toBeInstanceOf(SentryService); 188 | await mod.close(); 189 | // expect(mockCloseSentry).toHaveBeenCalledWith(timeout); 190 | }); 191 | }); 192 | 193 | describe('Sentry Service exception handling', () => { 194 | it('should test verbose catch err', async () => { 195 | const mod = await Test.createTestingModule({ 196 | imports: [ 197 | SentryModule.forRoot({ 198 | ...failureConfig 199 | }) 200 | ] 201 | }).compile(); 202 | 203 | const sentry = mod.get(SENTRY_TOKEN); 204 | expect(sentry).toBeDefined(); 205 | expect(sentry).toBeInstanceOf(SentryService); 206 | 207 | try { 208 | sentry.verbose('This will throw an exception'); 209 | } catch (err) { 210 | //to do// 211 | expect(sentry.log).toThrowError(SENTRY_NOT_CONFIGURE_ERROR); 212 | } 213 | }); 214 | it('should test warn catch err', async () => { 215 | const mod = await Test.createTestingModule({ 216 | imports: [ 217 | SentryModule.forRoot({ 218 | ...failureConfig 219 | }) 220 | ] 221 | }).compile(); 222 | 223 | const sentry = mod.get(SENTRY_TOKEN); 224 | expect(sentry).toBeDefined(); 225 | expect(sentry).toBeInstanceOf(SentryService); 226 | 227 | try { 228 | sentry.warn('This will throw an exception'); 229 | } catch (err) { 230 | //to do// 231 | expect(sentry.log).toThrowError(SENTRY_NOT_CONFIGURE_ERROR); 232 | } 233 | }); 234 | it('should test error catch err', async () => { 235 | const mod = await Test.createTestingModule({ 236 | imports: [ 237 | SentryModule.forRoot({ 238 | ...failureConfig 239 | }) 240 | ] 241 | }).compile(); 242 | 243 | const sentry = mod.get(SENTRY_TOKEN); 244 | expect(sentry).toBeDefined(); 245 | expect(sentry).toBeInstanceOf(SentryService); 246 | 247 | try { 248 | sentry.error('This will throw an exception'); 249 | } catch (err) { 250 | //to do// 251 | expect(sentry.log).toThrowError(SENTRY_NOT_CONFIGURE_ERROR); 252 | } 253 | }); 254 | it('should test debug catch err', async () => { 255 | const mod = await Test.createTestingModule({ 256 | imports: [ 257 | SentryModule.forRoot({ 258 | ...failureConfig 259 | }) 260 | ] 261 | }).compile(); 262 | 263 | const sentry = mod.get(SENTRY_TOKEN); 264 | expect(sentry).toBeDefined(); 265 | expect(sentry).toBeInstanceOf(SentryService); 266 | 267 | try { 268 | sentry.debug('This will throw an exception'); 269 | } catch (err) { 270 | //to do// 271 | expect(sentry.log).toThrowError(SENTRY_NOT_CONFIGURE_ERROR); 272 | } 273 | }); 274 | it('should test log catch err', async () => { 275 | const mod = await Test.createTestingModule({ 276 | imports: [ 277 | SentryModule.forRoot({ 278 | ...failureConfig 279 | }) 280 | ] 281 | }).compile(); 282 | 283 | const sentry = mod.get(SENTRY_TOKEN); 284 | expect(sentry).toBeDefined(); 285 | expect(sentry).toBeInstanceOf(SentryService); 286 | 287 | try { 288 | sentry.log('This will throw an exception'); 289 | } catch (err) { 290 | //to do// 291 | expect(sentry.log).toThrowError(SENTRY_NOT_CONFIGURE_ERROR); 292 | } 293 | }); 294 | }); 295 | 296 | describe('Sentry Service asBreadcrumb implementation', () => { 297 | let mod: TestingModule; 298 | let sentry: SentryService; 299 | 300 | beforeAll(async () => { 301 | mod = await Test.createTestingModule({ 302 | imports: [ 303 | SentryModule.forRoot({ 304 | ...config 305 | }) 306 | ] 307 | }).compile(); 308 | 309 | sentry = mod.get(SENTRY_TOKEN); 310 | }); 311 | 312 | it('sentry.SentryServiceInstance', () => { 313 | expect(SentryService.SentryServiceInstance).toBeInstanceOf(Function); 314 | }); 315 | it('sentry.instance', () => { 316 | expect(sentry.instance).toBeInstanceOf(Function); 317 | }); 318 | 319 | it('sentry.log asBreabcrumb === true', () => { 320 | try { 321 | sentry.log('sentry:log', 'context:log', true); 322 | expect(true).toBeTruthy(); 323 | } catch (err) {} 324 | expect(sentry.log).toBeInstanceOf(Function); 325 | }); 326 | 327 | it('sentry.debug asBreabcrumb === true', () => { 328 | try { 329 | sentry.debug('sentry:debug', 'context:debug', true); 330 | expect(true).toBeTruthy(); 331 | } catch (err) {} 332 | expect(sentry.debug).toBeInstanceOf(Function); 333 | }); 334 | 335 | it('sentry.verbose asBreabcrumb === true', () => { 336 | try { 337 | sentry.verbose('sentry:verbose', 'context:verbose', true); 338 | expect(true).toBeTruthy(); 339 | } catch (err) {} 340 | expect(sentry.verbose).toBeInstanceOf(Function); 341 | }); 342 | 343 | it('sentry.warn asBreabcrumb === true', () => { 344 | try { 345 | sentry.verbose('sentry:warn', 'context:warn', true); 346 | expect(true).toBeTruthy(); 347 | } catch (err) {} 348 | expect(sentry.warn).toBeInstanceOf(Function); 349 | }); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sentry-core.module'; 2 | export * from './sentry.constants'; 3 | export * from './sentry.decorator'; 4 | export * from './sentry.interceptor'; 5 | export * from './sentry.interfaces'; 6 | export * from './sentry.module'; 7 | export * from './sentry.providers'; 8 | export * from './sentry.service'; 9 | export * from './severity.enum'; 10 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/injectDecoratoryFactory.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | /** 4 | * Creates a decorator that can be used as a convenience to inject a specific token 5 | * 6 | * Instead of using @Inject(SOME_THING_TOKEN) this can be used to create a new named Decorator 7 | * such as @InjectSomeThing() which will hide the token details from users making APIs easier 8 | * to consume 9 | * @param token 10 | */ 11 | export const makeInjectableDecorator = 12 | (token: string | symbol): (() => ParameterDecorator) => 13 | () => 14 | Inject(token); 15 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry-core.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global, Provider, Type, DynamicModule } from '@nestjs/common'; 2 | import { 3 | SentryModuleAsyncOptions, 4 | SentryOptionsFactory, 5 | SentryModuleOptions 6 | } from './sentry.interfaces'; 7 | import { SENTRY_MODULE_OPTIONS, SENTRY_TOKEN } from './sentry.constants'; 8 | import { SentryService } from './sentry.service'; 9 | import { createSentryProviders } from './sentry.providers'; 10 | 11 | @Global() 12 | @Module({}) 13 | export class SentryCoreModule { 14 | public static forRoot(options: SentryModuleOptions): DynamicModule { 15 | const provider = createSentryProviders(options); 16 | 17 | return { 18 | exports: [provider, SentryService], 19 | module: SentryCoreModule, 20 | providers: [provider, SentryService] 21 | }; 22 | } 23 | 24 | public static forRootAsync(options: SentryModuleAsyncOptions): DynamicModule { 25 | const provider: Provider = { 26 | inject: [SENTRY_MODULE_OPTIONS], 27 | provide: SENTRY_TOKEN, 28 | useFactory: (options: SentryModuleOptions) => new SentryService(options) 29 | }; 30 | 31 | return { 32 | exports: [provider, SentryService], 33 | imports: options.imports, 34 | module: SentryCoreModule, 35 | providers: [...this.createAsyncProviders(options), provider, SentryService] 36 | }; 37 | } 38 | 39 | private static createAsyncProviders(options: SentryModuleAsyncOptions): Provider[] { 40 | if (options.useExisting || options.useFactory) { 41 | return [this.createAsyncOptionsProvider(options)]; 42 | } 43 | const useClass = options.useClass as Type; 44 | return [ 45 | this.createAsyncOptionsProvider(options), 46 | { 47 | provide: useClass, 48 | useClass 49 | } 50 | ]; 51 | } 52 | 53 | private static createAsyncOptionsProvider(options: SentryModuleAsyncOptions): Provider { 54 | if (options.useFactory) { 55 | return { 56 | inject: options.inject || [], 57 | provide: SENTRY_MODULE_OPTIONS, 58 | useFactory: options.useFactory 59 | }; 60 | } 61 | const inject = [(options.useClass || options.useExisting) as Type]; 62 | return { 63 | provide: SENTRY_MODULE_OPTIONS, 64 | useFactory: async (optionsFactory: SentryOptionsFactory) => 65 | await optionsFactory.createSentryModuleOptions(), 66 | inject 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.constants.ts: -------------------------------------------------------------------------------- 1 | export const SENTRY_MODULE_OPTIONS = Symbol('SentryModuleOptions'); 2 | export const SENTRY_TOKEN = Symbol('SentryToken'); 3 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.decorator.ts: -------------------------------------------------------------------------------- 1 | import { makeInjectableDecorator } from './injectDecoratoryFactory'; 2 | import { SENTRY_MODULE_OPTIONS, SENTRY_TOKEN } from './sentry.constants'; 3 | 4 | export const InjectSentry = makeInjectableDecorator(SENTRY_TOKEN); 5 | 6 | /** 7 | * Injects the Sentry Module config 8 | */ 9 | export const InjectSentryModuleConfig = makeInjectableDecorator(SENTRY_MODULE_OPTIONS); 10 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.interceptor.ts: -------------------------------------------------------------------------------- 1 | // Nestjs imports 2 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 3 | import { 4 | HttpArgumentsHost, 5 | WsArgumentsHost, 6 | RpcArgumentsHost, 7 | ContextType 8 | } from '@nestjs/common/interfaces'; 9 | // Rxjs imports 10 | import { Observable } from 'rxjs'; 11 | import { tap } from 'rxjs/operators'; 12 | // Sentry imports 13 | import { Scope } from '@sentry/hub'; 14 | import { Handlers } from '@sentry/node'; 15 | 16 | import { SentryService } from './sentry.service'; 17 | import { SentryInterceptorOptions, SentryInterceptorOptionsFilter } from './sentry.interfaces'; 18 | 19 | @Injectable() 20 | export class SentryInterceptor implements NestInterceptor { 21 | protected readonly client: SentryService = SentryService.SentryServiceInstance(); 22 | constructor(private readonly options?: SentryInterceptorOptions) {} 23 | 24 | intercept(context: ExecutionContext, next: CallHandler): Observable { 25 | // first param would be for events, second is for errors 26 | return next.handle().pipe( 27 | tap(null, (exception) => { 28 | if (this.shouldReport(exception)) { 29 | this.client.instance().withScope((scope) => { 30 | this.captureException(context, scope, exception); 31 | }); 32 | } 33 | }) 34 | ); 35 | } 36 | 37 | protected captureException(context: ExecutionContext, scope: Scope, exception: unknown) { 38 | switch (context.getType()) { 39 | case 'http': 40 | return this.captureHttpException(scope, context.switchToHttp(), exception); 41 | case 'rpc': 42 | return this.captureRpcException(scope, context.switchToRpc(), exception); 43 | case 'ws': 44 | return this.captureWsException(scope, context.switchToWs(), exception); 45 | } 46 | } 47 | 48 | private captureHttpException(scope: Scope, http: HttpArgumentsHost, exception: unknown): void { 49 | const data = Handlers.parseRequest({}, http.getRequest(), this.options); 50 | 51 | scope.setExtra('req', data.request); 52 | 53 | if (data.extra) scope.setExtras(data.extra); 54 | if (data.user) scope.setUser(data.user); 55 | 56 | this.client.instance().captureException(exception); 57 | } 58 | 59 | private captureRpcException(scope: Scope, rpc: RpcArgumentsHost, exception: unknown): void { 60 | scope.setExtra('rpc_data', rpc.getData()); 61 | 62 | this.client.instance().captureException(exception); 63 | } 64 | 65 | private captureWsException(scope: Scope, ws: WsArgumentsHost, exception: unknown): void { 66 | scope.setExtra('ws_client', ws.getClient()); 67 | scope.setExtra('ws_data', ws.getData()); 68 | 69 | this.client.instance().captureException(exception); 70 | } 71 | 72 | private shouldReport(exception: unknown) { 73 | if (this.options && !this.options.filters) return true; 74 | 75 | // If any filter passes, then we do not report 76 | if (this.options) { 77 | const opts: SentryInterceptorOptions = this.options; 78 | 79 | if (opts.filters) { 80 | const filters: SentryInterceptorOptionsFilter[] = opts.filters; 81 | return filters.every(({ type, filter }) => { 82 | return !(exception instanceof type && (!filter || filter(exception))); 83 | }); 84 | } 85 | } else { 86 | return true; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectionToken, 3 | ModuleMetadata, 4 | OptionalFactoryDependency, 5 | Type 6 | } from '@nestjs/common/interfaces'; 7 | import { Integration, Options } from '@sentry/types'; 8 | import { ConsoleLoggerOptions } from '@nestjs/common'; 9 | import { SeverityLevel } from '@sentry/node'; 10 | 11 | export interface SentryCloseOptions { 12 | enabled: boolean; 13 | // timeout – Maximum time in ms the client should wait until closing forcefully 14 | timeout?: number; 15 | } 16 | 17 | export type SentryModuleOptions = Omit & { 18 | integrations?: Integration[]; 19 | close?: SentryCloseOptions; 20 | } & ConsoleLoggerOptions; 21 | 22 | export interface SentryOptionsFactory { 23 | createSentryModuleOptions(): Promise | SentryModuleOptions; 24 | } 25 | 26 | export interface SentryModuleAsyncOptions extends Pick { 27 | inject?: (InjectionToken | OptionalFactoryDependency)[]; 28 | useClass?: Type; 29 | useExisting?: Type; 30 | useFactory?: (...args: any[]) => Promise | SentryModuleOptions; 31 | } 32 | 33 | export type SentryTransaction = boolean | 'path' | 'methodPath' | 'handler'; 34 | 35 | export interface SentryFilterFunction { 36 | (exception: unknown): boolean; 37 | } 38 | 39 | export interface SentryInterceptorOptionsFilter { 40 | // This one type must remain an `any` because it's used as the RHS of an `instanceof` operator 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | type: any; 43 | filter?: SentryFilterFunction; 44 | } 45 | 46 | export interface SentryInterceptorOptions { 47 | filters?: SentryInterceptorOptionsFilter[]; 48 | tags?: { [key: string]: string }; 49 | extra?: { [key: string]: any }; 50 | fingerprint?: string[]; 51 | level?: SeverityLevel; 52 | 53 | // https://github.com/getsentry/sentry-javascript/blob/master/packages/node/src/handlers.ts#L163 54 | request?: boolean; 55 | serverName?: boolean; 56 | transaction?: boolean | 'path' | 'methodPath' | 'handler'; // https://github.com/getsentry/sentry-javascript/blob/master/packages/node/src/handlers.ts#L16 57 | user?: boolean | string[]; 58 | version?: boolean; 59 | } 60 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule } from '@nestjs/common'; 2 | import { SentryCoreModule } from './sentry-core.module'; 3 | import { SentryModuleOptions, SentryModuleAsyncOptions } from './sentry.interfaces'; 4 | 5 | @Module({}) 6 | export class SentryModule { 7 | public static forRoot(options: SentryModuleOptions): DynamicModule { 8 | return { 9 | module: SentryModule, 10 | imports: [SentryCoreModule.forRoot(options)] 11 | }; 12 | } 13 | 14 | public static forRootAsync(options: SentryModuleAsyncOptions): DynamicModule { 15 | return { 16 | module: SentryModule, 17 | imports: [SentryCoreModule.forRootAsync(options)] 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | import { SentryModuleOptions } from './sentry.interfaces'; 3 | import { SENTRY_TOKEN } from './sentry.constants'; 4 | import { SentryService } from './sentry.service'; 5 | 6 | export function createSentryProviders(options: SentryModuleOptions): Provider { 7 | return { 8 | provide: SENTRY_TOKEN, 9 | useValue: new SentryService(options) 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/sentry.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, ConsoleLogger } from '@nestjs/common'; 2 | import { OnApplicationShutdown } from '@nestjs/common'; 3 | import { ClientOptions, Client } from '@sentry/types'; 4 | import * as Sentry from '@sentry/node'; 5 | import { SENTRY_MODULE_OPTIONS } from './sentry.constants'; 6 | import { SentryModuleOptions } from './sentry.interfaces'; 7 | import { Severity } from './severity.enum'; 8 | 9 | @Injectable() 10 | export class SentryService extends ConsoleLogger implements OnApplicationShutdown { 11 | app = '@ntegral/nestjs-sentry: '; 12 | private static serviceInstance: SentryService; 13 | constructor( 14 | @Inject(SENTRY_MODULE_OPTIONS) 15 | readonly opts?: SentryModuleOptions 16 | ) { 17 | super(); 18 | if (!(opts && opts.dsn)) { 19 | // console.log('options not found. Did you use SentryModule.forRoot?'); 20 | return; 21 | } 22 | const { integrations = [], ...sentryOptions } = opts; 23 | Sentry.init({ 24 | ...sentryOptions, 25 | integrations: [ 26 | new Sentry.Integrations.OnUncaughtException({ 27 | onFatalError: async (err) => { 28 | // console.error('uncaughtException, not cool!') 29 | // console.error(err); 30 | if (err.name === 'SentryError') { 31 | console.log(err); 32 | } else { 33 | Sentry.getCurrentHub().getClient>()?.captureException(err); 34 | process.exit(1); 35 | } 36 | } 37 | }), 38 | new Sentry.Integrations.OnUnhandledRejection({ mode: 'warn' }), 39 | ...integrations 40 | ] 41 | }); 42 | } 43 | 44 | public static SentryServiceInstance(): SentryService { 45 | if (!SentryService.serviceInstance) { 46 | SentryService.serviceInstance = new SentryService(); 47 | } 48 | return SentryService.serviceInstance; 49 | } 50 | 51 | log(message: string, context?: string, asBreadcrumb?: boolean) { 52 | message = `${this.app} ${message}`; 53 | try { 54 | super.log(message, context); 55 | asBreadcrumb 56 | ? Sentry.addBreadcrumb({ 57 | message, 58 | level: Severity.Log, 59 | data: { 60 | context 61 | } 62 | }) 63 | : Sentry.captureMessage(message, Severity.Log); 64 | } catch (err) {} 65 | } 66 | 67 | error(message: string, trace?: string, context?: string) { 68 | message = `${this.app} ${message}`; 69 | try { 70 | super.error(message, trace, context); 71 | Sentry.captureMessage(message, Severity.Error); 72 | } catch (err) {} 73 | } 74 | 75 | warn(message: string, context?: string, asBreadcrumb?: boolean) { 76 | message = `${this.app} ${message}`; 77 | try { 78 | super.warn(message, context); 79 | asBreadcrumb 80 | ? Sentry.addBreadcrumb({ 81 | message, 82 | level: Severity.Warning, 83 | data: { 84 | context 85 | } 86 | }) 87 | : Sentry.captureMessage(message, Severity.Warning); 88 | } catch (err) {} 89 | } 90 | 91 | debug(message: string, context?: string, asBreadcrumb?: boolean) { 92 | message = `${this.app} ${message}`; 93 | try { 94 | super.debug(message, context); 95 | asBreadcrumb 96 | ? Sentry.addBreadcrumb({ 97 | message, 98 | level: Severity.Debug, 99 | data: { 100 | context 101 | } 102 | }) 103 | : Sentry.captureMessage(message, Severity.Debug); 104 | } catch (err) {} 105 | } 106 | 107 | verbose(message: string, context?: string, asBreadcrumb?: boolean) { 108 | message = `${this.app} ${message}`; 109 | try { 110 | super.verbose(message, context); 111 | asBreadcrumb 112 | ? Sentry.addBreadcrumb({ 113 | message, 114 | level: Severity.Info, 115 | data: { 116 | context 117 | } 118 | }) 119 | : Sentry.captureMessage(message, Severity.Info); 120 | } catch (err) {} 121 | } 122 | 123 | instance() { 124 | return Sentry; 125 | } 126 | 127 | async onApplicationShutdown() { 128 | if (this.opts?.close?.enabled === true) { 129 | await Sentry.close(this.opts?.close.timeout); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/lib/severity.enum.ts: -------------------------------------------------------------------------------- 1 | import { SeverityLevel } from '@sentry/node'; 2 | 3 | type LegacySeverityLevels = 'Debug' | 'Error' | 'Fatal' | 'Info' | 'Log' | 'Warning'; 4 | 5 | /** 6 | * As of version 7.x, Sentry moved away from a defined Severity enum in favor of 7 | * string constants unioned into the type SeverityLevel. For backwards compatibility, 8 | * and to avoid dealing with string constants everywhere, this emulates the enum but 9 | * in a way that is compatible with Sentry 7 without requiring a cast. 10 | */ 11 | export const Severity: Record = { 12 | Fatal: 'fatal', 13 | Error: 'error', 14 | Warning: 'warning', 15 | Log: 'log', 16 | Info: 'info', 17 | Debug: 'debug' 18 | }; 19 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": [ 3 | { 4 | "name": "Dexter Hardy", 5 | "email": "dexter.hardy@ntegral.com", 6 | "url": "http://www.ntegral.com" 7 | }, 8 | { 9 | "name": "Zack Sheppard", 10 | "email": "zack@traveler.dev", 11 | "url": "https://www.traveler.dev" 12 | } 13 | ], 14 | "name": "@travelerdev/nestjs-sentry", 15 | "version": "4.3.0", 16 | "description": "Provides an injectable sentry.io client to provide enterprise logging of nestjs modules", 17 | "main": "./dist/index.js", 18 | "typings": "./dist/index.d.ts", 19 | "directories": { 20 | "dist": "dist", 21 | "lib": "lib" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/travelerdev/nestjs-sentry" 29 | }, 30 | "scripts": { 31 | "build": "tsc -p tsconfig.build.json", 32 | "clean": "rm -rf dist", 33 | "format": "prettier --write \"lib/**/*.ts\"", 34 | "publish:npm": "npm publish --access public", 35 | "test": "jest", 36 | "test:watch": "jest --watch", 37 | "test:cov": "jest --coverage", 38 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 39 | }, 40 | "keywords": [ 41 | "nestjs", 42 | "sentry.io" 43 | ], 44 | "author": "Zack Sheppard and Dexter Hardy", 45 | "license": "ISC", 46 | "peerDependencies": { 47 | "@nestjs/common": "^9.0.0 || ^10.0.0", 48 | "@sentry/hub": "^7.12.0", 49 | "@sentry/node": "^7.12.0", 50 | "reflect-metadata": "^0.1.13", 51 | "rxjs": "^7.2.0" 52 | }, 53 | "devDependencies": { 54 | "@nestjs/common": "^10.0.0", 55 | "@nestjs/core": "^10.0.0", 56 | "@sentry/hub": "^7.56.0", 57 | "@sentry/node": "^7.56.0", 58 | "reflect-metadata": "^0.1.13", 59 | "rxjs": "^7.2.0" 60 | }, 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "lib", 71 | "testRegex": ".spec.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "coverageDirectory": "../coverage", 76 | "testEnvironment": "node" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "sourceMap": true, 6 | "inlineSources": true, 7 | "sourceRoot": "/", 8 | "noImplicitAny": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/nestjs-sentry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "include": ["./lib/**/*"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "rootDir": "./lib", 7 | "outDir": "./dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "module": "commonjs", 6 | "strict": true, 7 | "removeComments": true, 8 | "noImplicitAny": false, 9 | "noLib": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": false, 14 | "skipLibCheck": true, 15 | "moduleResolution": "node" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./nestjs-sentry-graphql/tsconfig.build.json" 6 | }, 7 | { 8 | "path": "./nestjs-sentry/tsconfig.build.json" 9 | } 10 | ] 11 | } 12 | --------------------------------------------------------------------------------