Skip to content

Commit db1ea9f

Browse files
committed
feat(server): add Sentry support to the Koa instance returned by getApp
1 parent b8041fe commit db1ea9f

5 files changed

Lines changed: 98 additions & 6 deletions

File tree

packages/code-gen/src/processors/crud-validation.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,9 @@ function crudValidateRelation(generateContext, crud, relation) {
190190
model,
191191
)} via '${relation.fromParent?.field}' could not be resolved in the '${
192192
crud.group
193-
}' group. Make sure there is a relation with '${relation.fromParent
194-
?.field}' on ${stringFormatNameForError(model)}.`,
193+
}' group. Make sure there is a relation with '${
194+
relation.fromParent?.field
195+
}' on ${stringFormatNameForError(model)}.`,
195196
});
196197
}
197198

packages/server/src/app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { defaultHeaders } from "./middleware/headers.js";
55
import { healthHandler } from "./middleware/health.js";
66
import { logMiddleware } from "./middleware/log.js";
77
import { notFoundHandler } from "./middleware/notFound.js";
8+
import { sentry } from "./middleware/sentry.js";
89

910
/**
1011
* @typedef {ReturnType<getApp>} KoaApplication
@@ -78,6 +79,8 @@ export function getApp(opts = {}) {
7879
app.use(healthHandler());
7980
}
8081

82+
app.use(sentry());
83+
8184
app.use(logMiddleware(app, opts.logOptions ?? {}));
8285
app.use(errorHandler(opts.errorOptions ?? {}));
8386
app.use(notFoundHandler());

packages/server/src/middleware/error.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AppError, isProduction } from "@compas/stdlib";
1+
import { _compasSentryExport, AppError, isProduction } from "@compas/stdlib";
22

33
/**
44
* @type {NonNullable<import("../app.js").ErrorHandlerOptions["onError"]>}
@@ -30,6 +30,7 @@ export function errorHandler(opts) {
3030
return;
3131
}
3232

33+
const origErr = error;
3334
let err = error;
3435
let log = ctx.log.info;
3536

@@ -38,6 +39,20 @@ export function errorHandler(opts) {
3839
}
3940

4041
if (err.status >= 500) {
42+
if (_compasSentryExport) {
43+
if (err === origErr) {
44+
// An AppError.serverError was thrown.
45+
_compasSentryExport.captureException(
46+
new Error(err.key, {
47+
cause: err,
48+
}),
49+
);
50+
} else {
51+
// Something else was thrown.
52+
_compasSentryExport.captureException(origErr);
53+
}
54+
}
55+
4156
log = ctx.log.error;
4257
}
4358

packages/server/src/middleware/log.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Transform } from "node:stream";
22
import {
3+
_compasSentryExport,
34
AppError,
45
eventStart,
56
eventStop,
@@ -41,7 +42,7 @@ export function logMiddleware(app, options) {
4142
* @param {bigint} startTime
4243
* @param {number} length
4344
*/
44-
function logInfo(ctx, startTime, length) {
45+
function logInfoAndEndTrace(ctx, startTime, length) {
4546
const duration = Math.round(
4647
Number(process.hrtime.bigint() - startTime) / 1000000,
4748
);
@@ -79,8 +80,30 @@ export function logMiddleware(app, options) {
7980
// Skip eventStop if we don't have events enabled.
8081
// Skip eventStop for CORS requests, this gives a bit cleaner logs.
8182
if (options.disableRootEvent !== true && ctx.method !== "OPTIONS") {
83+
if (_compasSentryExport) {
84+
const span = _compasSentryExport.getActiveSpan();
85+
if (span) {
86+
span.description = ctx.event.name;
87+
span.updateName(ctx.event.name);
88+
}
89+
}
90+
8291
eventStop(ctx.event);
8392
}
93+
94+
if (_compasSentryExport) {
95+
const span = _compasSentryExport.getActiveSpan();
96+
if (span) {
97+
span.setStatus(
98+
_compasSentryExport.getSpanStatusFromHttpCode(ctx.status),
99+
);
100+
span.setAttributes({
101+
params: ctx.validatedParams,
102+
query: ctx.validatedQuery,
103+
});
104+
span.end();
105+
}
106+
}
84107
}
85108

86109
// Log stream errors after the headers are sent
@@ -96,6 +119,10 @@ export function logMiddleware(app, options) {
96119
syscall: error.syscall,
97120
error: AppError.format(error),
98121
});
122+
123+
if (_compasSentryExport) {
124+
_compasSentryExport.captureException(error);
125+
}
99126
});
100127

101128
return async (ctx, next) => {
@@ -124,8 +151,9 @@ export function logMiddleware(app, options) {
124151
} catch {
125152
// May throw on circular objects
126153
}
154+
127155
if (!isNil(responseLength)) {
128-
logInfo(ctx, startTime, responseLength);
156+
logInfoAndEndTrace(ctx, startTime, responseLength);
129157
return;
130158
} else if (ctx.body && ctx.body.readable) {
131159
const body = ctx.body;
@@ -134,7 +162,7 @@ export function logMiddleware(app, options) {
134162
await bodyCloseOrFinish(ctx);
135163
}
136164

137-
logInfo(ctx, startTime, isNil(counter) ? 0 : counter.length);
165+
logInfoAndEndTrace(ctx, startTime, isNil(counter) ? 0 : counter.length);
138166
};
139167
}
140168

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { _compasSentryExport } from "@compas/stdlib";
2+
3+
/**
4+
* Sentry support;
5+
* - Starts a new root span for each incoming request.
6+
* - Tries to name it based on the finalized name of `ctx.event`.
7+
* This is most likely in the format `router.foo.bar` for matched routes by the
8+
* generated router.
9+
* - Uses the sentry-trace header when provided.
10+
* Note that if a custom list of `allowHeaders` is provided in the CORS options,
11+
* 'sentry-trace' and 'baggage' should be allowed as well.
12+
* - If the error handler retrieves an unknown or AppError.serverError, it is reported as
13+
* an uncaught exception.
14+
*
15+
* @returns {import("koa").Middleware}
16+
*/
17+
export function sentry() {
18+
if (!_compasSentryExport) {
19+
return (ctx, next) => {
20+
return next();
21+
};
22+
}
23+
24+
return async (ctx, next) => {
25+
let traceParentData;
26+
if (ctx.request.get("sentry-trace")) {
27+
// @ts-expect-error
28+
traceParentData = _compasSentryExport.extractTraceparentData(
29+
ctx.request.get("sentry-trace"),
30+
);
31+
}
32+
33+
// @ts-expect-error
34+
return await _compasSentryExport.startSpanManual(
35+
{
36+
op: "http",
37+
name: "http",
38+
...traceParentData,
39+
},
40+
async () => {
41+
return await next();
42+
},
43+
);
44+
};
45+
}

0 commit comments

Comments
 (0)