├── .circleci └── config.yml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── package.json ├── renovate.json ├── src └── index.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: 'circleci/node:latest' 6 | steps: 7 | - checkout 8 | - run: 9 | name: install 10 | command: npm install 11 | - run: 12 | name: test 13 | command: npm test 14 | - run: 15 | name: release 16 | command: npm run semantic-release || true 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | .DS_Store 5 | *.log* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Matic Zavadlal 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-middleware-sentry 2 | 3 | [![CircleCI](https://circleci.com/gh/BrunoScheufler/graphql-middleware-sentry.svg?style=shield)](https://circleci.com/gh/BrunoScheufler/graphql-middleware-sentry) 4 | [![npm version](https://badge.fury.io/js/graphql-middleware-sentry.svg)](https://badge.fury.io/js/graphql-middleware-sentry) 5 | 6 | > GraphQL Middleware plugin for Sentry. 7 | 8 | ## Usage 9 | 10 | > With GraphQL Yoga 11 | 12 | ```ts 13 | import { GraphQLServer } from 'graphql-yoga' 14 | import { sentry } from 'graphql-middleware-sentry' 15 | 16 | const typeDefs = ` 17 | type Query { 18 | hello: String! 19 | bug: String! 20 | } 21 | ` 22 | 23 | const resolvers = { 24 | Query: { 25 | hello: () => `Hey there!` 26 | bug: () => { 27 | throw new Error(`Many bugs!`) 28 | } 29 | } 30 | } 31 | 32 | const sentryMiddleware = sentry({ 33 | config: { 34 | dsn: process.env.SENTRY_DSN, 35 | environment: process.env.NODE_ENV 36 | }, 37 | withScope: (scope, error, context) => { 38 | scope.setUser({ 39 | id: context.authorization.userId, 40 | }); 41 | scope.setExtra('body', context.request.body) 42 | scope.setExtra('origin', context.request.headers.origin) 43 | scope.setExtra('user-agent', context.request.headers['user-agent']) 44 | }, 45 | }) 46 | 47 | const server = GraphQLServer({ 48 | typeDefs, 49 | resolvers, 50 | middlewares: [sentryMiddleware] 51 | }) 52 | 53 | server.start(() => `Server running on http://localhost:4000`) 54 | ``` 55 | 56 | ### Using a Sentry instance 57 | 58 | In cases where you want to use your own instance of Sentry to use it in other places in your application you can pass the `sentryInstance`. The `config` property should not be passed as an option. 59 | 60 | #### Example usage with a Sentry instance 61 | 62 | ```ts 63 | Sentry.init({ 64 | dsn: process.env.SENTRY_DSN, 65 | }) 66 | 67 | const sentryMiddleware = sentry({ 68 | sentryInstance: Sentry, 69 | withScope: (scope, error, context) => { 70 | scope.setExtra('origin', context.request.headers.origin) 71 | }, 72 | }) 73 | ``` 74 | 75 | ## API & Configuration 76 | 77 | ```ts 78 | export interface Options { 79 | sentryInstance?: Sentry 80 | config?: Sentry.NodeOptions 81 | withScope?: ExceptionScope 82 | captureReturnedErrors?: boolean 83 | forwardErrors?: boolean 84 | reportError?: (res: Error | any) => boolean 85 | } 86 | 87 | function sentry(options: Options): IMiddlewareFunction 88 | ``` 89 | 90 | ### Sentry context 91 | 92 | To enrich events sent to Sentry, you can modify the [context](https://docs.sentry.io/enriching-error-data/context/?platform=javascript). 93 | This can be done using the `withScope` configuration option. 94 | 95 | The `withScope` option is a function that is called with the current Sentry scope, the error, and the GraphQL Context. 96 | 97 | ```ts 98 | type ExceptionScope = ( 99 | scope: Sentry.Scope, 100 | error: Error, 101 | context: Context, 102 | ) => void 103 | ``` 104 | 105 | ### Filtering Out Custom Errors 106 | 107 | To filter out custom errors thrown by your server (such as "You Are Not Logged In"), use the `reportError` option and return a boolean for whether or not the error should be sent to sentry. 108 | 109 | ```ts 110 | class CustomError extends Error {} 111 | 112 | const sentryMiddleware = sentry({ 113 | reportError: (res) => { 114 | // you can check the error message strings 115 | if (res.message === 'You Are Not Logged In') { 116 | return false; 117 | } 118 | 119 | // or extend the error type and create a custom error 120 | if (res instanceof CustomError) { 121 | return false; 122 | } 123 | 124 | return true; 125 | } 126 | }) 127 | ``` 128 | 129 | 130 | 131 | ### Options 132 | 133 | | property | required | description | 134 | | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 135 | | `sentryInstance` | false | [Sentry's instance](https://docs.sentry.io/error-reporting/configuration/?platform=node) | 136 | | `config` | false | [Sentry's config object](https://docs.sentry.io/error-reporting/configuration/?platform=node) | 137 | | `withScope` | false | Function to modify the [Sentry context](https://docs.sentry.io/enriching-error-data/context/?platform=node) to send with the captured error. | 138 | | `captureReturnedErrors` | false | Capture errors returned from other middlewares, e.g., `graphql-shield` [returns errors](https://github.com/maticzav/graphql-shield#custom-errors) from rules and resolvers | 139 | | `forwardErrors` | false | Should middleware forward errors to the client or block them. | 140 | | `reportError` | false | Function that passes `res` as the parameter and accepts a boolean callback for whether or not the error should be captured 141 | 142 | #### Note 143 | 144 | If `sentryInstance` is not passed then `config.dsn` is required and vice-versa. 145 | 146 | ## License 147 | 148 | This project is licensed under the [MIT License](LICENSE.md). 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-middleware-sentry", 3 | "description": "Sentry plugin for GraphQL Middleware", 4 | "version": "0.0.0-semantic-release", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "typescript": { 11 | "definition": "dist/index.d.ts" 12 | }, 13 | "scripts": { 14 | "prepublish": "npm run test", 15 | "build": "rimraf dist && tsc -d", 16 | "lint": "tslint --project tsconfig.json {src}/**/*.ts && prettier-check --ignore-path .gitignore {src,.}/{*.ts,*.js}", 17 | "test": "npm run lint && npm run build", 18 | "semantic-release": "semantic-release" 19 | }, 20 | "contributors": [ 21 | { 22 | "name": "Matic Zavadlal", 23 | "email": "matic.zavadlal@gmail.com", 24 | "url": "https://github.com/maticzav" 25 | }, 26 | { 27 | "name": "Bruno Scheufler", 28 | "email": "bruno@brunoscheufler.com", 29 | "url": "https://github.com/BrunoScheufler" 30 | } 31 | ], 32 | "devDependencies": { 33 | "@sentry/node": "5.21.3", 34 | "@types/lodash": "4.14.159", 35 | "@types/node": "13.13.15", 36 | "graphql": "15.3.0", 37 | "graphql-middleware": "4.0.2", 38 | "prettier": "1.19.1", 39 | "prettier-check": "2.0.0", 40 | "rimraf": "3.0.2", 41 | "semantic-release": "17.1.1", 42 | "tslint": "5.20.1", 43 | "tslint-config-prettier": "1.18.0", 44 | "tslint-config-standard": "9.0.0", 45 | "typescript": "3.9.7" 46 | }, 47 | "license": "MIT", 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/BrunoScheufler/graphql-middleware-sentry.git" 51 | }, 52 | "peerDependencies": { 53 | "@sentry/node": "^5.10.2", 54 | "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0", 55 | "graphql-middleware": "^2.0.0 || ^3.0.0 || ^4.0.0" 56 | } 57 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "docker:disable"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | 3 | import { IMiddlewareFunction } from 'graphql-middleware' 4 | 5 | export type ExceptionScope = ( 6 | scope: Sentry.Scope, 7 | error: Error, 8 | context: Context, 9 | reportError?: (res: Error | any) => boolean, 10 | ) => void 11 | 12 | // Options for graphql-middleware-sentry 13 | export interface Options { 14 | sentryInstance?: any 15 | config?: Sentry.NodeOptions 16 | withScope?: ExceptionScope 17 | captureReturnedErrors?: boolean 18 | forwardErrors?: boolean 19 | reportError?: (res: Error | any) => boolean 20 | } 21 | 22 | export class SentryError extends Error {} 23 | 24 | export const sentry = ({ 25 | sentryInstance = null, 26 | config = {}, 27 | withScope = () => {}, 28 | captureReturnedErrors = false, 29 | forwardErrors = false, 30 | reportError, 31 | }: Options): IMiddlewareFunction => { 32 | // Check if either sentryInstance or config.dsn is present 33 | if (!sentryInstance && !config.dsn) { 34 | throw new SentryError( 35 | `Missing the sentryInstance or the dsn parameter in configuration.`, 36 | ) 37 | } 38 | 39 | if (!sentryInstance && config.dsn) { 40 | // Init Sentry 41 | sentryInstance = Sentry 42 | Sentry.init(config) 43 | } 44 | 45 | // Return middleware resolver 46 | return async (resolve, parent, args, ctx, info) => { 47 | try { 48 | const res = await resolve(parent, args, ctx, info) 49 | 50 | if (captureReturnedErrors && res instanceof Error) { 51 | captureException(sentryInstance, res, ctx, withScope, reportError) 52 | } 53 | 54 | return res 55 | } catch (error) { 56 | captureException(sentryInstance, error, ctx, withScope, reportError) 57 | 58 | // Forward error 59 | if (forwardErrors) { 60 | throw error 61 | } 62 | } 63 | } 64 | } 65 | 66 | function captureException( 67 | sentryInstance, 68 | error: Error, 69 | ctx: Context, 70 | withScope: ExceptionScope, 71 | reportError?: (res) => boolean, 72 | ) { 73 | if ((reportError && reportError(error)) || reportError === undefined) { 74 | sentryInstance.withScope(scope => { 75 | withScope(scope, error, ctx) 76 | sentryInstance.captureException(error) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "sourceMap": true, 9 | "lib": ["dom", "es2017", "esnext.asynciterable"], 10 | "experimentalDecorators": true 11 | }, 12 | "exclude": ["node_modules", "examples"] 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"], 3 | "rules": { 4 | "no-use-before-declare": false, 5 | "space-before-function-paren": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------