Package Name
dd-trace
Package Version(s)
7.0.0-pre
Describe the feature you'd like
Related to #1255 ("Source maps for node typescript + APM", open since 2021), which asks for this outcome. The request is a public span-finish hook (Option A below) so consumers can transform a span before export without reaching into private internals. An opt-in native remap (Option B) is a narrower alternative if you'd rather dd-trace own the specific source-map case. Both have a working proof-of-concept PR (linked at the very bottom).
The problem. TypeScript backends compiled to dist/*.js show transpiled frames in APM and Error Tracking instead of the original src/*.ts locations, because dd-trace does not symbolicate backend Node stacks server-side. Node's global --enable-source-maps fixes the display but taxes every Error.prepareStackTrace in the process, not just the errors actually reported to Datadog. We tried it and the CPU cost was too high for us in production, because our workload throws and catches enough errors on hot paths that paying source-map resolution on every one of them was not viable. So today the only bounded fix is to remap in-process, on the spans dd-trace is about to export, and there is no public API for that.
What consumers are forced to do today. Subscribe to the internal dd-trace:span:finish diagnostics channel and rewrite the stack in place on the live span before the processor formats it, reaching into private state: the error.stack string tag via span.context()._tags, the raw Error object stored in the error tag (rewriting its .stack), and span._events[].attributes.stacktrace for graphql resolver errors. The public Span interface can write (setTag, addTags) but cannot read tags, the error object, or events, so any consumer that wants to transform what an integration already recorded is pushed into private, version-coupled internals.
Why the channel already works. In opentracing/span.js, finish() calls finishCh.publish(this) synchronously and only then this._processor.process(this), so subscribers get the live, still-mutable span before it is serialized, and setTag has no post-finish guard. span_format.js's extractError reads error.stack off the error object at format time, so mutating that object's .stack in the finish handler lands in the exported meta. The pattern is already first-party: dd-trace's own openfeature/span-enrichment-hook.js and llmobs/index.js both subscribe to dd-trace:span:finish and mutate the span in the handler. The gap is only that reading the error carriers has no public API.
Option A (the request): promote the finish interception to a supported hook
PoC: shanempope#1
Formalize the extension point that already exists: a documented tracer-wide span-finish hook in init options, symmetric with the existing per-plugin hooks.request and hooks.execute, that runs after integrations tag the span and before export (throw-safe, so a hook error never breaks span export), plus stable read accessors on the public Span (getTag(key), getError(), getEvents()) so consumers never touch _tags, _events, or the raw error object. With that, the #1255 remap needs zero private access:
const tracer = require("dd-trace").init({
hooks: {
span: (span) => {
const error = span.getError();
if (error) {
error.stack = remapStack(error.stack);
span.setTag("error", error);
}
},
},
});
Option B (narrower alternative): remap APM error stacks natively, opt-in
PoC: shanempope#2
If you'd rather dd-trace own the specific source-map case rather than expose a general hook: dd-trace already owns source-map resolution in-tree (AppSec/IAST getOriginalPathAndLineFromSourceMap) and already ships an opt-in source-map flag for the profiler (DD_PROFILING_SOURCE_MAP). This wires the same capability into the APM error path in extractError, gated behind an opt-in flag mirroring the profiler's (experimental.sourceMap, env DD_TRACE_SOURCE_MAP, default off). Because it runs at format time on error spans only, it carries none of the global --enable-source-maps prepareStackTrace tax and is bounded to the frames dd-trace is about to export. The disabled path is byte-identical.
const tracer = require("dd-trace").init({
experimental: { sourceMap: true },
});
Is your feature request related to a problem?
No response
Describe alternatives you've considered
Our current workaround, is to subscribe to the internal dd-trace:span:finish diagnostics channel and mutate the live span in the handler, exactly as described under "What consumers are forced to do today" above. It works, but it depends on private, version-coupled internals (span.context()._tags, the raw Error object in the error tag, and span._events), so it can break on any dd-trace upgrade. That brittleness is why we are asking for a supported API.
Additional context
No response
Package Name
dd-trace
Package Version(s)
7.0.0-pre
Describe the feature you'd like
Related to #1255 ("Source maps for node typescript + APM", open since 2021), which asks for this outcome. The request is a public span-finish hook (Option A below) so consumers can transform a span before export without reaching into private internals. An opt-in native remap (Option B) is a narrower alternative if you'd rather dd-trace own the specific source-map case. Both have a working proof-of-concept PR (linked at the very bottom).
The problem. TypeScript backends compiled to
dist/*.jsshow transpiled frames in APM and Error Tracking instead of the originalsrc/*.tslocations, because dd-trace does not symbolicate backend Node stacks server-side. Node's global--enable-source-mapsfixes the display but taxes everyError.prepareStackTracein the process, not just the errors actually reported to Datadog. We tried it and the CPU cost was too high for us in production, because our workload throws and catches enough errors on hot paths that paying source-map resolution on every one of them was not viable. So today the only bounded fix is to remap in-process, on the spans dd-trace is about to export, and there is no public API for that.What consumers are forced to do today. Subscribe to the internal
dd-trace:span:finishdiagnostics channel and rewrite the stack in place on the live span before the processor formats it, reaching into private state: theerror.stackstring tag viaspan.context()._tags, the rawErrorobject stored in theerrortag (rewriting its.stack), andspan._events[].attributes.stacktracefor graphql resolver errors. The publicSpaninterface can write (setTag,addTags) but cannot read tags, the error object, or events, so any consumer that wants to transform what an integration already recorded is pushed into private, version-coupled internals.Why the channel already works. In
opentracing/span.js,finish()callsfinishCh.publish(this)synchronously and only thenthis._processor.process(this), so subscribers get the live, still-mutable span before it is serialized, andsetTaghas no post-finish guard.span_format.js'sextractErrorreadserror.stackoff the error object at format time, so mutating that object's.stackin the finish handler lands in the exportedmeta. The pattern is already first-party: dd-trace's ownopenfeature/span-enrichment-hook.jsandllmobs/index.jsboth subscribe todd-trace:span:finishand mutate the span in the handler. The gap is only that reading the error carriers has no public API.Option A (the request): promote the finish interception to a supported hook
PoC: shanempope#1
Formalize the extension point that already exists: a documented tracer-wide span-finish hook in init options, symmetric with the existing per-plugin
hooks.requestandhooks.execute, that runs after integrations tag the span and before export (throw-safe, so a hook error never breaks span export), plus stable read accessors on the publicSpan(getTag(key),getError(),getEvents()) so consumers never touch_tags,_events, or the raw error object. With that, the #1255 remap needs zero private access:Option B (narrower alternative): remap APM error stacks natively, opt-in
PoC: shanempope#2
If you'd rather dd-trace own the specific source-map case rather than expose a general hook: dd-trace already owns source-map resolution in-tree (AppSec/IAST
getOriginalPathAndLineFromSourceMap) and already ships an opt-in source-map flag for the profiler (DD_PROFILING_SOURCE_MAP). This wires the same capability into the APM error path inextractError, gated behind an opt-in flag mirroring the profiler's (experimental.sourceMap, envDD_TRACE_SOURCE_MAP, default off). Because it runs at format time on error spans only, it carries none of the global--enable-source-mapsprepareStackTracetax and is bounded to the frames dd-trace is about to export. The disabled path is byte-identical.Is your feature request related to a problem?
No response
Describe alternatives you've considered
Our current workaround, is to subscribe to the internal
dd-trace:span:finishdiagnostics channel and mutate the live span in the handler, exactly as described under "What consumers are forced to do today" above. It works, but it depends on private, version-coupled internals (span.context()._tags, the rawErrorobject in theerrortag, andspan._events), so it can break on any dd-trace upgrade. That brittleness is why we are asking for a supported API.Additional context
No response