Description
Problem Statement
Our current Otel-node-specific implementation of continueTrace
creates a remote span which holds the trace information from the sentry-trace
and baggage
data passed to continueTrace
(as described in #11092).
The presence of the remote span as the currently active span forces startSpan
to create a new root span for the first startSpan
call within the continueTrace
callback. This causes potentially unexpected behavior, whenever nested continueTrace
calls occur.
I came across this problem in our SvelteKit SDK where we run into a situation where we indeed call continueTrace
in a nested form. In certain situations, an initial server request (creating a http.server
root span in a continueTrace
callback) can make a "sub-request" which would trigger the same instrumentation to make another continueTrace
call and start another http.server
span. While this span should be a child span within the outer http.server
root span, it becomes its own root span at the moment.
For SvelteKit I opted to only call continueTrace
once and avoid calling it if we already have an active span. TODO: Link to PR once it's up.
In a more general example, this behaviour would lead to two root spans/transactions:
function handle(callback) {
return continueTrace({sentry-trace, baggage}, () => {
return Sentry.startSpan({name: 'http.server'}, () => {
return callback();
})
})
}
function getUser() {
return handle(() => {
return {id: 1};
})
}
function getUserDetails() {
return handle(() => {
const user = getUser();
return {...user, name: 'foo'};
})
}
Now imagine this handle
function being a middleware or something that's automatically invoked whenever a get
function is called. In situations with a sub request like in Kit or user-created nested continueTrace
calls, we'd end up with multiple http.server
root spans instead of a nested structure.
Solution Brainstorm
Before thinking about a concrete solution, we need to answer a more fundamental question: Is this actually a problem or intended behaviour?
This can be argued both ways:
- [intended] If I'm in an active root span but I call
continueTrace
- why should my new spans within that call even be part of the active root span? Especially if the trace data differs, there's no reason to connect these anymore. In this case we want separate root spans. - [problematic] In our previous none-otel based Node SDK, nested
continueTrace
calls did nothing(?) or even changed the entire transaction's trace parent to the trace data from the nested call (assuming the trace data wasn't identical). Changing this behaviour could confuse users who currently rely on this behavior (just like we did in SvelteKit).
After thinking about it for a bit I'm currently tentatively leaning towards intended behaviour (1). If the team agrees, we can leave things as is but we need to point this out clearly in our documentation (i.e. "don't make nested continueTrace
calls")
In case we want to change the behaviour (i.e. go with (2)), we need a solution how to handle such situations. Some ideas:
- Simply ignore the nested
continueTrace
call (maybe log a warning) and therefore avoid creating a remote span -> no new root span - Wait until we can stream spans because by then the trace context has to be passed on per span (or segment?). This implicitly also means multiple root spans but maybe at that point it's fine 🤔