diff --git a/api/package.json b/api/package.json index 5375f3b4f..a0fc3a4b2 100644 --- a/api/package.json +++ b/api/package.json @@ -32,6 +32,8 @@ "@koa/cors": "^3.3.0", "@koa/router": "^10.1.1", "@publicodes/api": "^1.0.0-beta.42", + "@sentry/node": "^7.1.1", + "@sentry/tracing": "^7.1.1", "koa": "^2.13.4", "koa-body": "^5.0.0", "koa-static": "^5.0.0", diff --git a/api/source/index.ts b/api/source/index.ts index 662c80f4e..61a5a3311 100644 --- a/api/source/index.ts +++ b/api/source/index.ts @@ -1,5 +1,5 @@ import cors from '@koa/cors' -import Router from '@koa/router' +import Router, { RouterContext } from '@koa/router' import { koaMiddleware as publicodesAPI } from '@publicodes/api' import Koa from 'koa' import rules from 'modele-social' @@ -7,6 +7,7 @@ import Engine from 'publicodes' import openapi from './openapi.json' assert { type: 'json' } import { docRoutes } from './route/doc.js' import { openapiRoutes } from './route/openapi.js' +import Sentry, { requestHandler, tracingMiddleWare } from './sentry.js' type State = Koa.DefaultState type Context = Koa.DefaultContext @@ -14,6 +15,18 @@ type Context = Koa.DefaultContext const app = new Koa() const router = new Router() +app.use(requestHandler) +app.use(tracingMiddleWare) + +app.on('error', (err, ctx: RouterContext) => { + Sentry.withScope((scope) => { + scope.addEventProcessor((event) => { + return Sentry.Handlers.parseRequest(event, ctx.request) + }) + Sentry.captureException(err) + }) +}) + app.use(cors()) const apiRoutes = publicodesAPI(new Engine(rules)) diff --git a/api/source/sentry.ts b/api/source/sentry.ts new file mode 100644 index 000000000..285edee20 --- /dev/null +++ b/api/source/sentry.ts @@ -0,0 +1,85 @@ +import { RouterContext } from '@koa/router' +import * as Sentry from '@sentry/node' +import * as Tracing from '@sentry/tracing' +// eslint-disable-next-line n/no-deprecated-api +import Domains from 'domain' +import { Context, Next } from 'koa' + +Sentry.init({ + dsn: 'https://21ddaba2424b46b4b14185dba51b1288@sentry.incubateur.net/42', + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + // We recommend adjusting this value in production + tracesSampleRate: 0.5, +}) + +export default Sentry + +// not mandatory, but adding domains does help a lot with breadcrumbs +export const requestHandler = (ctx: Context, next: Next) => { + return new Promise((resolve, reject) => { + const local = Domains.create() + local.add(ctx as never) + local.on('error', (err) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ctx.status = (err.status as number) || 500 + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ctx.body = err.message + ctx.app.emit('error', err, ctx) + }) + void local.run(async () => { + Sentry.getCurrentHub().configureScope((scope) => + scope.addEventProcessor((event) => + Sentry.Handlers.parseRequest(event, ctx.request, { user: false }) + ) + ) + await next() + resolve() + }) + }) +} + +// this tracing middleware creates a transaction per request +export const tracingMiddleWare = async (ctx: RouterContext, next: Next) => { + const reqMethod = (ctx.method || '').toUpperCase() + const reqUrl = ctx.url && Tracing.stripUrlQueryAndFragment(ctx.url) + + // connect to trace of upstream app + let traceparentData + if (ctx.request.get('sentry-trace')) { + traceparentData = Tracing.extractTraceparentData( + ctx.request.get('sentry-trace') + ) + } + + const transaction = Sentry.startTransaction({ + name: `${reqMethod} ${reqUrl}`, + op: 'http.server', + ...traceparentData, + }) + + ctx.__sentry_transaction = transaction + + // We put the transaction on the scope so users can attach children to it + Sentry.getCurrentHub().configureScope((scope) => { + scope.setSpan(transaction) + }) + + ctx.res.on('finish', () => { + // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction closes + setImmediate(() => { + // if using koa router, a nicer way to capture transaction using the matched route + if (ctx._matchedRoute) { + const mountPath = (ctx.mountPath as undefined | string) || '' + transaction.setName( + `${reqMethod} ${mountPath}${ctx._matchedRoute.toString()}` + ) + } + transaction.setHttpStatus(ctx.status) + transaction.finish() + }) + }) + + await next() +} diff --git a/yarn.lock b/yarn.lock index b4e845b7a..cda0e5ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4369,6 +4369,18 @@ __metadata: languageName: node linkType: hard +"@sentry/core@npm:7.1.1": + version: 7.1.1 + resolution: "@sentry/core@npm:7.1.1" + dependencies: + "@sentry/hub": 7.1.1 + "@sentry/types": 7.1.1 + "@sentry/utils": 7.1.1 + tslib: ^1.9.3 + checksum: 687d46e7a0960cf595ae7ce392aaad2157653513cd1f92bf2bbcbfe6663365fab874a53f63220fcf1ad1312abe27bc871bc1e84abe5b60cd661642e661be4b6c + languageName: node + linkType: hard + "@sentry/hub@npm:6.19.6": version: 6.19.6 resolution: "@sentry/hub@npm:6.19.6" @@ -4380,6 +4392,17 @@ __metadata: languageName: node linkType: hard +"@sentry/hub@npm:7.1.1": + version: 7.1.1 + resolution: "@sentry/hub@npm:7.1.1" + dependencies: + "@sentry/types": 7.1.1 + "@sentry/utils": 7.1.1 + tslib: ^1.9.3 + checksum: ddc61b31aa0f45ee9ea3f32173d6e49fa93fa8b83eb7d5ab0c53758f31731e1ae70a0730212ed3a0889ea47372e5919f511ad7c056dae2c18d31049caeb0c38c + languageName: node + linkType: hard + "@sentry/integrations@npm:^6.19.6": version: 6.19.6 resolution: "@sentry/integrations@npm:6.19.6" @@ -4403,6 +4426,22 @@ __metadata: languageName: node linkType: hard +"@sentry/node@npm:^7.1.1": + version: 7.1.1 + resolution: "@sentry/node@npm:7.1.1" + dependencies: + "@sentry/core": 7.1.1 + "@sentry/hub": 7.1.1 + "@sentry/types": 7.1.1 + "@sentry/utils": 7.1.1 + cookie: ^0.4.1 + https-proxy-agent: ^5.0.0 + lru_map: ^0.3.3 + tslib: ^1.9.3 + checksum: ecda1b11356da01fbfbf0909fa7eead19c442ff6ef7bce9f7a5fadcdc220399014a42ac08614d6fa03e555084de00c0ef8904fb45e0682010ef5a8e2920692f9 + languageName: node + linkType: hard + "@sentry/react@npm:^6.19.6": version: 6.19.6 resolution: "@sentry/react@npm:6.19.6" @@ -4432,6 +4471,18 @@ __metadata: languageName: node linkType: hard +"@sentry/tracing@npm:^7.1.1": + version: 7.1.1 + resolution: "@sentry/tracing@npm:7.1.1" + dependencies: + "@sentry/hub": 7.1.1 + "@sentry/types": 7.1.1 + "@sentry/utils": 7.1.1 + tslib: ^1.9.3 + checksum: 3b11cc36eaf99c716185ccd80a0ec2e5b042a38cf4419ce5a0eeda7f81c7d097134e94c4aa6dfa45b23c83d8563b20ac6ad9cd47bbd164f363039672b971b87d + languageName: node + linkType: hard + "@sentry/types@npm:6.19.6": version: 6.19.6 resolution: "@sentry/types@npm:6.19.6" @@ -4439,6 +4490,13 @@ __metadata: languageName: node linkType: hard +"@sentry/types@npm:7.1.1": + version: 7.1.1 + resolution: "@sentry/types@npm:7.1.1" + checksum: 3f1b43782ca93c6520589b3739d4833cd00afb0f3ff72f5afaa42d4224c63163b1ef6bd167143b7d72667d0711f09fc4a42d36418ee242898a9cbd979dbac65d + languageName: node + linkType: hard + "@sentry/utils@npm:6.19.6": version: 6.19.6 resolution: "@sentry/utils@npm:6.19.6" @@ -4449,6 +4507,16 @@ __metadata: languageName: node linkType: hard +"@sentry/utils@npm:7.1.1": + version: 7.1.1 + resolution: "@sentry/utils@npm:7.1.1" + dependencies: + "@sentry/types": 7.1.1 + tslib: ^1.9.3 + checksum: b36d0e03b007229cad81dc8db517c4bf58c8e7636aa80a575dc91fb605028c1a0b61e5f090704e04b5e9021fd8ec95b1a6f1465449f2a1a6c657b283295add71 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.3": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" @@ -7770,6 +7838,8 @@ __metadata: "@koa/cors": ^3.3.0 "@koa/router": ^10.1.1 "@publicodes/api": ^1.0.0-beta.42 + "@sentry/node": ^7.1.1 + "@sentry/tracing": ^7.1.1 "@types/koa": ^2.13.4 "@types/koa-static": ^4.0.2 "@types/koa__cors": ^3.3.0 @@ -9882,7 +9952,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2": +"cookie@npm:0.4.2, cookie@npm:^0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b @@ -15884,6 +15954,13 @@ __metadata: languageName: node linkType: hard +"lru_map@npm:^0.3.3": + version: 0.3.3 + resolution: "lru_map@npm:0.3.3" + checksum: ca9dd43c65ed7a4f117c548028101c5b6855e10923ea9d1f635af53ad20c5868ff428c364d454a7b57fe391b89c704982275410c3c5099cca5aeee00d76e169a + languageName: node + linkType: hard + "lz-string@npm:^1.4.4": version: 1.4.4 resolution: "lz-string@npm:1.4.4"