Basically you have concurrent requests in Node.js. And you probably want to enrich possible errors with data specific to each request. This request-specific data can be gathered in different parts of the app via
Sentry.configureScope(scope => scope.setSomeUsefulData(...))
Sentry.addBreadcrumb({ ... })
Later on somewhere in a deep nested asynchronous function call The Error gets thrown. How does Sentry know which of the data previously gathered is actually relevant to this particular error, considering requests are handled simultaneously and at the point where an error happens there's no access to some sentry "scope" to get data relevant to this particular request which resulted in the error.
Or do I have to pass sentry scope through all my function calls? Like
server.on('request', (requestContext) => {
// Create new Sentry scope
Sentry.configureScope(sentryScope => {
Products.getProductById(id, sentryScope); // And pass it on
});
});
// all the way down until...
function parseNumber(input, sentryScope) {
// ...
}
Or does sentry use some sort of magic to map specific data to relevant events? Or am I missing something?
Basically you have concurrent requests in Node.js. And you probably want to enrich possible errors with data specific to each request. This request-specific data can be gathered in different parts of the app via
Sentry.configureScope(scope => scope.setSomeUsefulData(...))
Sentry.addBreadcrumb({ ... })
Later on somewhere in a deep nested asynchronous function call The Error gets thrown. How does Sentry know which of the data previously gathered is actually relevant to this particular error, considering requests are handled simultaneously and at the point where an error happens there's no access to some sentry "scope" to get data relevant to this particular request which resulted in the error.
Or do I have to pass sentry scope through all my function calls? Like
server.on('request', (requestContext) => {
// Create new Sentry scope
Sentry.configureScope(sentryScope => {
Products.getProductById(id, sentryScope); // And pass it on
});
});
// all the way down until...
function parseNumber(input, sentryScope) {
// ...
}
Or does sentry use some sort of magic to map specific data to relevant events? Or am I missing something?
Share Improve this question edited Oct 25, 2019 at 4:16 disfated asked Oct 24, 2019 at 22:17 disfateddisfated 11k13 gold badges41 silver badges52 bronze badges1 Answer
Reset to default 10It appears they use Node.js Domains (https://nodejs/api/domain.html) to create a separate "context" for each request.
From the Sentry documentation, you need to use a requestHandler for this to work correctly.
E.g. for express (https://docs.sentry.io/platforms/node/express/):
const express = require('express');
const app = express();
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'https://[email protected]/1240240' });
// The request handler must be the first middleware on the app
app.use(Sentry.Handlers.requestHandler());
// All controllers should live here
app.get('/', function rootHandler(req, res) {
res.end('Hello world!');
});
The handler looks something like this:
(From: @sentry/node/dist/handlers.js)
function requestHandler(options) {
return function sentryRequestMiddleware(req, res, next) {
if (options && options.flushTimeout && options.flushTimeout > 0) {
// tslint:disable-next-line: no-unbound-method
var _end_1 = res.end;
res.end = function (chunk, encoding, cb) {
var _this = this;
sdk_1.flush(options.flushTimeout)
.then(function () {
_end_1.call(_this, chunk, encoding, cb);
})
.then(null, function (e) {
utils_1.logger.error(e);
});
};
}
var local = domain.create();
local.add(req);
local.add(res);
local.on('error', next);
local.run(function () {
core_1.getCurrentHub().configureScope(function (scope) {
return scope.addEventProcessor(function (event) { return parseRequest(event, req, options); });
});
next();
});
};
}
exports.requestHandler = requestHandler;
So, you can see the new Domain being created on new requests.
I found this info mostly by discovering this issue: https://github./getsentry/sentry-javascript/issues/1939
If you wanted to add "context" to other async parts of your Node backend (E.g. if you had workers / threads / something work not initiated from a HTTP request..), there are examples in the above issue that show how that can be done (by manually creating Hubs / Domains..).
If you're interested, keeping some sense of context in Node.js with various async functions / callbacks / setTimeouts can also be done now with async_hooks: https://nodejs/api/async_hooks.html
This is a good intro:
https://itnext.io/request-id-tracing-in-node-js-applications-c517c7dab62d?
And some libraries implementing this:
https://github./Jeff-Lewis/cls-hooked
https://github./vicanso/async-local-storage
For example, something like this should work:
const ALS = require("async-local-storage");
function withScope(fn, requestId) {
ALS.scope();
ALS.set("requestId", requestId, false);
return await fn();
}
async function work() {
try {
await part1();
await part2();
} catch(e) {
Sentry.withScope((scope) => {
scope.setTag("requestId", ALS.get("requestId"));
Sentry.captureException(new Error("..."))
})
}
}
withScope(work, "1234-5678");
You would have to keep track of the breadcrumbs yourself though, e.g. to add a breadcrumb:
ALS.set("breadcrumbs", [...ALS.get("breadcrumbs"), new_breadcrumb]);
And you would need to manually set these in the beforeSend() callback in Sentry:
beforeSend: function(data) {
data.breadcrumbs = ALS.get("breadcrumbs");
return data;
}