It appears they use Node.js Domains (https://nodejs.org/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://963bad1904fe48b18c1866f10e69eff8@sentry.io/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.com/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.org/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.com/Jeff-Lewis/cls-hooked
https://github.com/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;
}