Skip to content

Commit a2be0b2

Browse files
authored
fix: close any existing active span when extracting the tracecontext (#688)
* close any existing active span when extracting the tracecontext
1 parent 125f375 commit a2be0b2

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

src/trace/trace-context-service.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("TraceContextService", () => {
1414
mockXRayShouldThrow = false;
1515
const traceWrapper = {
1616
traceContext: () => spanContextWrapper,
17+
closeScope: jest.fn(),
1718
};
1819
traceContextService = new TraceContextService(traceWrapper as any, {} as any);
1920
});
@@ -126,4 +127,54 @@ describe("TraceContextService", () => {
126127
expect(result?.toTraceId()).toBe("newTraceId");
127128
expect(traceContextService.traceSource).toBe("event");
128129
});
130+
131+
it("should not leak dd-trace context from previous invocation when extract is called", async () => {
132+
// Simulate dd-trace having a stale active span from a previous invocation (warm start scenario)
133+
const staleDdTraceContext = {
134+
toTraceId: () => "staleTraceId_999",
135+
toSpanId: () => "staleSpanId_888",
136+
sampleMode: () => 1,
137+
source: TraceSource.DdTrace,
138+
spanContext: {},
139+
};
140+
141+
// Mock tracerWrapper that returns stale context initially, then null after closeScope is called
142+
let traceContextValue: any = staleDdTraceContext;
143+
const mockCloseScopeFn = jest.fn(() => {
144+
// After closeScope is called, traceContext should return null
145+
traceContextValue = null;
146+
});
147+
148+
const mockTracerWrapper = {
149+
traceContext: jest.fn(() => traceContextValue),
150+
closeScope: mockCloseScopeFn,
151+
};
152+
153+
const service = new TraceContextService(mockTracerWrapper as any, {} as any);
154+
155+
// Mock the extractor to return a NEW context for the current invocation
156+
const newEventContext = {
157+
toTraceId: () => "newTraceId_123",
158+
toSpanId: () => "newSpanId_456",
159+
sampleMode: () => 2,
160+
source: TraceSource.Event,
161+
spanContext: {},
162+
};
163+
const mockExtract = jest.fn().mockResolvedValue(newEventContext);
164+
service["traceExtractor"] = { extract: mockExtract } as any;
165+
166+
// Call extract for the new invocation
167+
await service.extract({}, {} as any);
168+
169+
// Verify that closeScope was called to clear the stale context
170+
expect(mockCloseScopeFn).toHaveBeenCalled();
171+
172+
// After the fix: currentTraceHeaders should return the NEW context from the event
173+
// not the stale dd-trace context from the previous invocation
174+
const headers = service.currentTraceHeaders;
175+
176+
expect(headers["x-datadog-trace-id"]).toBe("newTraceId_123");
177+
expect(headers["x-datadog-parent-id"]).toBe("newSpanId_456");
178+
expect(headers["x-datadog-sampling-priority"]).toBe("2");
179+
});
129180
});

src/trace/trace-context-service.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ export class TraceContextService {
5050
}
5151

5252
async extract(event: any, context: Context): Promise<SpanContextWrapper | null> {
53-
// Reset trace context from previous invocation to prevent caching
53+
// Reset trace context and close dd-trace scope to prevent stale context from previous invocation due to unfinished spans
5454
this.rootTraceContext = null;
55+
this.tracerWrapper.closeScope();
5556

5657
this.rootTraceContext = await this.traceExtractor?.extract(event, context);
57-
58-
return this.currentTraceContext;
58+
// Return the extracted context, not the current context which may not be related to the event or context
59+
return this.rootTraceContext;
5960
}
6061

6162
get currentTraceHeaders(): Partial<DatadogTraceHeaders> {
@@ -70,8 +71,6 @@ export class TraceContextService {
7071
}
7172

7273
get currentTraceContext(): SpanContextWrapper | null {
73-
if (this.rootTraceContext === null) return null;
74-
7574
const traceContext = this.rootTraceContext;
7675
const currentDatadogContext = this.tracerWrapper.traceContext();
7776
if (currentDatadogContext) {

src/trace/tracer-wrapper.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,35 @@ describe("TracerWrapper", () => {
127127

128128
expect(mockDataStreamsCheckpointer.setConsumeCheckpoint).toHaveBeenCalledWith(eventType, arn, contextJson, false);
129129
});
130+
131+
it("should finish active span when closing scope to prevent context leakage", () => {
132+
const mockFinishFn = jest.fn();
133+
mockSpan = {
134+
context: () => ({
135+
toSpanId: () => "1234",
136+
toTraceId: () => "45678",
137+
_sampling: {
138+
priority: "2",
139+
},
140+
}),
141+
finish: mockFinishFn,
142+
};
143+
144+
const wrapper = new TracerWrapper();
145+
wrapper.closeScope();
146+
147+
expect(mockFinishFn).toHaveBeenCalled();
148+
});
149+
150+
it("should not error when closing scope with no active span", () => {
151+
mockSpan = null;
152+
const wrapper = new TracerWrapper();
153+
expect(() => wrapper.closeScope()).not.toThrow();
154+
});
155+
156+
it("should not error when closing scope with tracer unavailable", () => {
157+
mockNoTracer = true;
158+
const wrapper = new TracerWrapper();
159+
expect(() => wrapper.closeScope()).not.toThrow();
160+
});
130161
});

src/trace/tracer-wrapper.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { logDebug } from "../utils";
1+
import { logDebug, logWarning } from "../utils";
22
import { SpanContextWrapper } from "./span-context-wrapper";
33
import { TraceSource } from "./trace-context-service";
44

@@ -94,6 +94,20 @@ export class TracerWrapper {
9494
return new SpanContextWrapper(span.context(), TraceSource.DdTrace);
9595
}
9696

97+
public closeScope(): void {
98+
try {
99+
const activeSpan = this.currentSpan;
100+
if (activeSpan && typeof activeSpan.finish === "function") {
101+
logDebug("Detected stale span from previous invocation, finishing it to prevent trace context leakage");
102+
activeSpan.finish();
103+
}
104+
} catch (err) {
105+
if (err instanceof Object || err instanceof Error) {
106+
logDebug("Failed to close dd-trace scope", err);
107+
}
108+
}
109+
}
110+
97111
public injectSpan(span: SpanContext): any {
98112
const dest = {};
99113
this.tracer.inject(span, "text_map", dest);

0 commit comments

Comments
 (0)