├── .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 | [](https://npmjs.org/package/@travelerdev/nestjs-sentry 'View this project on npm')
2 | [](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 | [](https://npmjs.org/package/@travelerdev/nestjs-sentry 'View this project on npm')
2 | [](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 |
--------------------------------------------------------------------------------