Adds error handling with third parties (#113)

This commit is contained in:
Chris Anderson 2023-04-05 09:14:42 -04:00 committed by GitHub
parent a035276986
commit dcb41b8cca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 313 additions and 5 deletions

View file

@ -13,12 +13,16 @@
"@aws-sdk/client-sns": "^3.121.0",
"@aws-sdk/client-sqs": "^3.171.0",
"@aws-sdk/lib-storage": "^3.171.0",
"@bugsnag/js": "^7.20.0",
"@bugsnag/plugin-koa": "^7.19.0",
"@koa/cors": "^3.3.0",
"@koa/router": "^11.0.1",
"@ladjs/country-language": "^1.0.3",
"@node-saml/node-saml": "github:node-saml/node-saml",
"@rxfork/sqs-consumer": "^6.0.0",
"@segment/analytics-node": "^1.0.0-beta.23",
"@sentry/node": "^7.46.0",
"@sentry/utils": "^7.46.0",
"ajv": "^8.11.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
@ -2143,6 +2147,69 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@bugsnag/browser": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@bugsnag/browser/-/browser-7.20.0.tgz",
"integrity": "sha512-LzZWI6q5cWYQSXvfJDcSl287d2xXESVn0L20lK+K5nwo/jXcK9IVZr9L+CYZ40HVXaC9jOmQbqZ18hsbO2QNIw==",
"dependencies": {
"@bugsnag/core": "^7.19.0"
}
},
"node_modules/@bugsnag/core": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@bugsnag/core/-/core-7.19.0.tgz",
"integrity": "sha512-2KGwdaLD9PhR7Wk7xPi3jGuGsKTatc/28U4TOZIDU3CgC2QhGjubwiXSECel5gwxhZ3jACKcMKSV2ovHhv1NrA==",
"dependencies": {
"@bugsnag/cuid": "^3.0.0",
"@bugsnag/safe-json-stringify": "^6.0.0",
"error-stack-parser": "^2.0.3",
"iserror": "0.0.2",
"stack-generator": "^2.0.3"
}
},
"node_modules/@bugsnag/cuid": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.0.2.tgz",
"integrity": "sha512-cIwzC93r3PQ/INeuwtZwkZIG2K8WWN0rRLZQhu+mr48Ay+i6sEki4GYfTsflse7hZ1BeDWrNb/Q9vgY3B31xHQ=="
},
"node_modules/@bugsnag/js": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@bugsnag/js/-/js-7.20.0.tgz",
"integrity": "sha512-lhUUSOveE8fP10RagAINqBmuH+eoOpyUOiTN1WRkjHUevWG0LZjRRUWEGN3AA+ZyTphmC6ljd2qE3/64qfOSGQ==",
"dependencies": {
"@bugsnag/browser": "^7.20.0",
"@bugsnag/node": "^7.19.0"
}
},
"node_modules/@bugsnag/node": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@bugsnag/node/-/node-7.19.0.tgz",
"integrity": "sha512-c4snyxx5d/fsMogmgehFBGc//daH6+4XCplia4zrEQYltjaQ+l8ud0dPx623DgJl/2j1+2zlRc7y7IHSd7Gm5w==",
"dependencies": {
"@bugsnag/core": "^7.19.0",
"byline": "^5.0.0",
"error-stack-parser": "^2.0.2",
"iserror": "^0.0.2",
"pump": "^3.0.0",
"stack-generator": "^2.0.3"
}
},
"node_modules/@bugsnag/plugin-koa": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@bugsnag/plugin-koa/-/plugin-koa-7.19.0.tgz",
"integrity": "sha512-zvzOMf1oXgO1hi0bQti+khFn4D32EfMv6W3Vc65+wcSGSFaMxuPJSE8sdOJqE6gFj2ANhCpHk8UvzlMAzRODSw==",
"dependencies": {
"iserror": "^0.0.2"
},
"peerDependencies": {
"@bugsnag/core": "^7.0.0"
}
},
"node_modules/@bugsnag/safe-json-stringify": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@bugsnag/safe-json-stringify/-/safe-json-stringify-6.0.0.tgz",
"integrity": "sha512-htzFO1Zc57S8kgdRK9mLcPVTW1BY2ijfH7Dk2CeZmspTWKdKqSo1iwmqrq2WtRjFlo8aRZYgLX0wFrDXF/9DLA=="
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -2993,6 +3060,91 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sentry-internal/tracing": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.46.0.tgz",
"integrity": "sha512-KYoppa7PPL8Er7bdPoxTNUfIY804JL7hhOEomQHYD22rLynwQ4AaLm3YEY75QWwcGb0B7ZDMV+tSumW7Rxuwuw==",
"dependencies": {
"@sentry/core": "7.46.0",
"@sentry/types": "7.46.0",
"@sentry/utils": "7.46.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/core": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.46.0.tgz",
"integrity": "sha512-BnNHGh/ZTztqQedFko7vb2u6yLs/kWesOQNivav32ZbsEpVCjcmG1gOJXh2YmGIvj3jXOC9a4xfIuh+lYFcA6A==",
"dependencies": {
"@sentry/types": "7.46.0",
"@sentry/utils": "7.46.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/node": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.46.0.tgz",
"integrity": "sha512-+GrgJMCye2WXGarRiU5IJHCK27xg7xbPc2XjGojBKbBoZfqxVAWbXEK4bnBQgRGP1pCmrU/M6ZhVgR3dP580xA==",
"dependencies": {
"@sentry-internal/tracing": "7.46.0",
"@sentry/core": "7.46.0",
"@sentry/types": "7.46.0",
"@sentry/utils": "7.46.0",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/types": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.46.0.tgz",
"integrity": "sha512-2FMEMgt2h6u7AoELhNhu9L54GAh67KKfK2pJ1kEXJHmWxM9FSCkizjLs/t+49xtY7jEXr8qYq8bV967VfDPQ9g==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.46.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.46.0.tgz",
"integrity": "sha512-elRezDAF84guMG0OVIIZEWm6wUpgbda4HGks98CFnPsrnMm3N1bdBI9XdlxYLtf+ir5KsGR5YlEIf/a0kRUwAQ==",
"dependencies": {
"@sentry/types": "7.46.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@ -4309,6 +4461,14 @@
"node": ">=10.16.0"
}
},
"node_modules/byline": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
"integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -4594,6 +4754,14 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookies": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
@ -4990,6 +5158,14 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/es-abstract": {
"version": "1.21.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz",
@ -7038,6 +7214,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/iserror": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/iserror/-/iserror-0.0.2.tgz",
"integrity": "sha512-oKGGrFVaWwETimP3SiWwjDeY27ovZoyZPHtxblC4hCq9fXxed/jasx+ATWFFjCVSRZng8VTMsN1nDnGo6zMBSw=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -8266,6 +8447,11 @@
"resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
"integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="
},
"node_modules/lru_map": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz",
"integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ=="
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -10035,6 +10221,14 @@
"node": ">=0.10.0"
}
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@ -10056,6 +10250,11 @@
"node": ">=8"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",

View file

@ -7,12 +7,16 @@
"@aws-sdk/client-sns": "^3.121.0",
"@aws-sdk/client-sqs": "^3.171.0",
"@aws-sdk/lib-storage": "^3.171.0",
"@bugsnag/js": "^7.20.0",
"@bugsnag/plugin-koa": "^7.19.0",
"@koa/cors": "^3.3.0",
"@koa/router": "^11.0.1",
"@ladjs/country-language": "^1.0.3",
"@node-saml/node-saml": "github:node-saml/node-saml",
"@rxfork/sqs-consumer": "^6.0.0",
"@segment/analytics-node": "^1.0.0-beta.23",
"@sentry/node": "^7.46.0",
"@sentry/utils": "^7.46.0",
"ajv": "^8.11.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",

View file

@ -13,12 +13,10 @@ export default class Api extends Koa {
this.proxy = process.env.NODE_ENV !== 'development'
app.error.attach(this)
this.use(koaBody())
.use(cors())
.use((ctx, next) => {
ctx.state.app = this.app
return next()
})
.use(serve('./public', {
hidden: true,
defer: true,

View file

@ -2,6 +2,7 @@ import loadDatabase, { Database } from './config/database'
import loadQueue from './config/queue'
import loadStorage from './config/storage'
import loadAuth from './config/auth'
import loadError, { logger } from './config/logger'
import type { Env } from './config/env'
import type Queue from './queue'
import Storage from './storage'
@ -9,7 +10,7 @@ import type Auth from './auth/Auth'
import { uuid } from './utilities'
import Api from './api'
import Worker from './worker'
import { logger } from './config/logger'
import ErrorHandler from './error/ErrorHandler'
export default class App {
private static $main: App
@ -21,6 +22,10 @@ export default class App {
}
static async init(env: Env): Promise<App> {
// Boot up error tracking
const error = await loadError(env.error)
// Load & migrate database
const database = await loadDatabase(env.db)
@ -39,6 +44,7 @@ export default class App {
queue,
auth,
storage,
error,
)
return App.$main
@ -56,6 +62,7 @@ export default class App {
public queue: Queue,
public auth: Auth,
public storage: Storage,
public error: ErrorHandler,
) {
this.#registered = {}
}

View file

@ -3,6 +3,7 @@ import type { StorageConfig } from '../storage/Storage'
import type { QueueConfig } from '../queue/Queue'
import type { DatabaseConfig } from './database'
import type { AuthConfig } from '../auth/Auth'
import type { ErrorConfig } from '../error/ErrorHandler'
export type Runner = 'api' | 'worker'
export interface Env {
@ -14,6 +15,7 @@ export interface Env {
port: number
secret: string
auth: AuthConfig
error: ErrorConfig
tracking: {
linkWrap: boolean,
deeplinkMirrorUrl: string | undefined,
@ -107,6 +109,14 @@ export default (type?: EnvType): Env => {
tokenLife: defaultTokenLife,
}),
}),
error: driver<ErrorConfig>(process.env.ERROR_DRIVER, {
bugsnag: () => ({
apiKey: process.env.ERROR_BUGSNAG_API_KEY,
}),
sentry: () => ({
dsn: process.env.ERROR_SENTRY_DSN,
}),
}),
tracking: {
linkWrap: (process.env.TRACKING_LINK_WRAP ?? 'true') === 'true',
deeplinkMirrorUrl: process.env.TRACKING_DEEPLINK_MIRROR_URL,

View file

@ -1,7 +1,12 @@
import pino from 'pino'
import pretty from 'pino-pretty'
import ErrorHandler, { ErrorConfig } from '../error/ErrorHandler'
// TODO: Check ENV and disable prettier for production
export const logger = pino({
level: process.env.LOG_LEVEL || 'warn',
}, pretty({ colorize: true }))
export default async (config: ErrorConfig) => {
return new ErrorHandler(config)
}

View file

@ -0,0 +1,28 @@
import { ErrorHandlerTypeConfig } from './ErrorHandler'
import ErrorHandlingProvider from './ErrorHandlerProvider'
import Koa from 'koa'
import Bugsnag from '@bugsnag/js'
import BugsnagPluginKoa from '@bugsnag/plugin-koa'
export interface BugSnagConfig extends ErrorHandlerTypeConfig {
driver: 'bugsnag'
apiKey: string
}
export default class BugSnagProvider implements ErrorHandlingProvider {
constructor(config: BugSnagConfig) {
Bugsnag.start({
apiKey: config.apiKey,
logger: null,
plugins: [BugsnagPluginKoa],
})
}
attach(api: Koa) {
const middleware = Bugsnag.getPlugin('koa')
if (middleware) {
api.use(middleware.requestHandler)
api.on('error', middleware.errorHandler)
}
}
}

View file

@ -0,0 +1,26 @@
import { DriverConfig } from '../config/env'
import Koa from 'koa'
import BugSnagProvider, { BugSnagConfig } from './BugSnagProvider'
import ErrorHandlerProvider, { ErrorHandlerProviderName } from './ErrorHandlerProvider'
import SentryProvider, { SentryConfig } from './SentryProvider'
export type ErrorConfig = BugSnagConfig | SentryConfig
export interface ErrorHandlerTypeConfig extends DriverConfig {
driver: ErrorHandlerProviderName
}
export default class ErrorHandler {
provider?: ErrorHandlerProvider
constructor(config: ErrorConfig) {
if (config?.driver === 'bugsnag') {
this.provider = new BugSnagProvider(config)
} else if (config?.driver === 'sentry') {
this.provider = new SentryProvider(config)
}
}
attach(api: Koa) {
this.provider?.attach(api)
}
}

View file

@ -0,0 +1,7 @@
import Koa from 'koa'
export type ErrorHandlerProviderName = 'bugsnag' | 'sentry'
export default interface ErrorHandlerProvider {
attach(api: Koa): void
}

View file

@ -0,0 +1,24 @@
import * as Sentry from '@sentry/node'
import Koa from 'koa'
import { ErrorHandlerTypeConfig } from './ErrorHandler'
import ErrorHandlingProvider from './ErrorHandlerProvider'
export interface SentryConfig extends ErrorHandlerTypeConfig {
driver: 'sentry'
dsn: string
}
export default class SentryProvider implements ErrorHandlingProvider {
constructor(config: SentryConfig) {
Sentry.init({ dsn: config.dsn })
}
attach(api: Koa) {
api.on('error', (err, ctx) => {
Sentry.withScope(scope => {
scope.setSDKProcessingMetadata({ request: ctx.request })
Sentry.captureException(err)
})
})
}
}