Skip to content

Commit

Permalink
feat(server): add Sentry support to the Koa instance returned by `get…
Browse files Browse the repository at this point in the history
…App`
  • Loading branch information
dirkdev98 committed Apr 14, 2024
1 parent b8041fe commit db1ea9f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 6 deletions.
5 changes: 3 additions & 2 deletions packages/code-gen/src/processors/crud-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,9 @@ function crudValidateRelation(generateContext, crud, relation) {
model,
)} via '${relation.fromParent?.field}' could not be resolved in the '${
crud.group
}' group. Make sure there is a relation with '${relation.fromParent
?.field}' on ${stringFormatNameForError(model)}.`,
}' group. Make sure there is a relation with '${
relation.fromParent?.field
}' on ${stringFormatNameForError(model)}.`,
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defaultHeaders } from "./middleware/headers.js";
import { healthHandler } from "./middleware/health.js";
import { logMiddleware } from "./middleware/log.js";
import { notFoundHandler } from "./middleware/notFound.js";
import { sentry } from "./middleware/sentry.js";

/**
* @typedef {ReturnType<getApp>} KoaApplication
Expand Down Expand Up @@ -78,6 +79,8 @@ export function getApp(opts = {}) {
app.use(healthHandler());
}

app.use(sentry());

app.use(logMiddleware(app, opts.logOptions ?? {}));
app.use(errorHandler(opts.errorOptions ?? {}));
app.use(notFoundHandler());
Expand Down
17 changes: 16 additions & 1 deletion packages/server/src/middleware/error.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppError, isProduction } from "@compas/stdlib";
import { _compasSentryExport, AppError, isProduction } from "@compas/stdlib";

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

const origErr = error;
let err = error;
let log = ctx.log.info;

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

if (err.status >= 500) {
if (_compasSentryExport) {
if (err === origErr) {
// An AppError.serverError was thrown.
_compasSentryExport.captureException(
new Error(err.key, {
cause: err,
}),
);
} else {
// Something else was thrown.
_compasSentryExport.captureException(origErr);
}
}

log = ctx.log.error;
}

Expand Down
34 changes: 31 additions & 3 deletions packages/server/src/middleware/log.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Transform } from "node:stream";
import {
_compasSentryExport,
AppError,
eventStart,
eventStop,
Expand Down Expand Up @@ -41,7 +42,7 @@ export function logMiddleware(app, options) {
* @param {bigint} startTime
* @param {number} length
*/
function logInfo(ctx, startTime, length) {
function logInfoAndEndTrace(ctx, startTime, length) {
const duration = Math.round(
Number(process.hrtime.bigint() - startTime) / 1000000,
);
Expand Down Expand Up @@ -79,8 +80,30 @@ export function logMiddleware(app, options) {
// Skip eventStop if we don't have events enabled.
// Skip eventStop for CORS requests, this gives a bit cleaner logs.
if (options.disableRootEvent !== true && ctx.method !== "OPTIONS") {
if (_compasSentryExport) {
const span = _compasSentryExport.getActiveSpan();
if (span) {
span.description = ctx.event.name;
span.updateName(ctx.event.name);
}
}

eventStop(ctx.event);
}

if (_compasSentryExport) {
const span = _compasSentryExport.getActiveSpan();
if (span) {
span.setStatus(
_compasSentryExport.getSpanStatusFromHttpCode(ctx.status),
);
span.setAttributes({
params: ctx.validatedParams,
query: ctx.validatedQuery,
});
span.end();
}
}
}

// Log stream errors after the headers are sent
Expand All @@ -96,6 +119,10 @@ export function logMiddleware(app, options) {
syscall: error.syscall,
error: AppError.format(error),
});

if (_compasSentryExport) {
_compasSentryExport.captureException(error);
}
});

return async (ctx, next) => {
Expand Down Expand Up @@ -124,8 +151,9 @@ export function logMiddleware(app, options) {
} catch {
// May throw on circular objects
}

if (!isNil(responseLength)) {
logInfo(ctx, startTime, responseLength);
logInfoAndEndTrace(ctx, startTime, responseLength);
return;
} else if (ctx.body && ctx.body.readable) {
const body = ctx.body;
Expand All @@ -134,7 +162,7 @@ export function logMiddleware(app, options) {
await bodyCloseOrFinish(ctx);
}

logInfo(ctx, startTime, isNil(counter) ? 0 : counter.length);
logInfoAndEndTrace(ctx, startTime, isNil(counter) ? 0 : counter.length);
};
}

Expand Down
45 changes: 45 additions & 0 deletions packages/server/src/middleware/sentry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { _compasSentryExport } from "@compas/stdlib";

/**
* Sentry support;
* - Starts a new root span for each incoming request.
* - Tries to name it based on the finalized name of `ctx.event`.
* This is most likely in the format `router.foo.bar` for matched routes by the
* generated router.
* - Uses the sentry-trace header when provided.
* Note that if a custom list of `allowHeaders` is provided in the CORS options,
* 'sentry-trace' and 'baggage' should be allowed as well.
* - If the error handler retrieves an unknown or AppError.serverError, it is reported as
* an uncaught exception.
*
* @returns {import("koa").Middleware}
*/
export function sentry() {
if (!_compasSentryExport) {
return (ctx, next) => {
return next();
};
}

return async (ctx, next) => {
let traceParentData;
if (ctx.request.get("sentry-trace")) {
// @ts-expect-error
traceParentData = _compasSentryExport.extractTraceparentData(
ctx.request.get("sentry-trace"),
);
}

// @ts-expect-error
return await _compasSentryExport.startSpanManual(
{
op: "http",
name: "http",
...traceParentData,
},
async () => {
return await next();
},
);
};
}

0 comments on commit db1ea9f

Please sign in to comment.