Skip to content

Commit 1b6c22e

Browse files
authored
feat(node): Allow to configure maxSpanWaitDuration (#12492)
This adds a new config option for `@sentry/node`, `maxSpanWaitDuration`. By configuring this, you can define how long the SDK waits for a finished spans parent span to be finished before we discard this. Today, this defaults to 5 min, so if a span is finished and its parent span is not finished within 5 minutes, the child span will be discarded. We do this to avoid memory leaks, as parent spans may be dropped/lost and thus child spans would be kept in memory forever. However, if you actually have very long running spans, they may be incorrectly dropped due to this. So now, you can configure your SDK to have a longer wait duration. Fixes #12491
1 parent 1bb86db commit 1b6c22e

File tree

5 files changed

+82
-5
lines changed

5 files changed

+82
-5
lines changed

packages/node/src/sdk/initOtel.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ export function setupOtel(client: NodeClient): BasicTracerProvider {
113113
}),
114114
forceFlushTimeoutMillis: 500,
115115
});
116-
provider.addSpanProcessor(new SentrySpanProcessor());
116+
provider.addSpanProcessor(
117+
new SentrySpanProcessor({
118+
timeout: client.getOptions().maxSpanWaitDuration,
119+
}),
120+
);
117121

118122
// Initialize the provider
119123
provider.register({

packages/node/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ export interface BaseNodeOptions {
7474
*/
7575
skipOpenTelemetrySetup?: boolean;
7676

77+
/**
78+
* The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span.
79+
* The SDK will automatically clean up spans that have no finished parent after this duration.
80+
* This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing.
81+
* However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early.
82+
* In this case, you can increase this duration to a value that fits your expected data.
83+
*
84+
* Defaults to 300 seconds (5 minutes).
85+
*/
86+
maxSpanWaitDuration?: number;
87+
7788
/** Callback that is executed when a fatal global error occurs. */
7889
onFatalError?(this: void, error: Error): void;
7990
}

packages/node/test/integration/transactions.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,4 +633,63 @@ describe('Integration | Transactions', () => {
633633
]),
634634
);
635635
});
636+
637+
it('allows to configure `maxSpanWaitDuration` to capture long running spans', async () => {
638+
const transactions: TransactionEvent[] = [];
639+
const beforeSendTransaction = jest.fn(event => {
640+
transactions.push(event);
641+
return null;
642+
});
643+
644+
const now = Date.now();
645+
jest.useFakeTimers();
646+
jest.setSystemTime(now);
647+
648+
const logs: unknown[] = [];
649+
jest.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg));
650+
651+
mockSdkInit({
652+
enableTracing: true,
653+
beforeSendTransaction,
654+
maxSpanWaitDuration: 100 * 60,
655+
});
656+
657+
Sentry.startSpanManual({ name: 'test name' }, rootSpan => {
658+
const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
659+
subSpan.end();
660+
661+
Sentry.startSpanManual({ name: 'inner span 2' }, innerSpan => {
662+
// Child span ends after 10 min
663+
setTimeout(
664+
() => {
665+
innerSpan.end();
666+
},
667+
10 * 60 * 1_000,
668+
);
669+
});
670+
671+
// root span ends after 99 min
672+
setTimeout(
673+
() => {
674+
rootSpan.end();
675+
},
676+
99 * 10 * 1_000,
677+
);
678+
});
679+
680+
// Now wait for 100 mins
681+
jest.advanceTimersByTime(100 * 60 * 1_000);
682+
683+
expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
684+
expect(transactions).toHaveLength(1);
685+
const transaction = transactions[0]!;
686+
687+
expect(transaction.transaction).toEqual('test name');
688+
const spans = transaction.spans || [];
689+
690+
expect(spans).toHaveLength(2);
691+
692+
expect(spans[0]!.description).toEqual('inner span 1');
693+
expect(spans[1]!.description).toEqual('inner span 2');
694+
});
636695
});

packages/opentelemetry/src/spanExporter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,19 @@ import { parseSpanDescription } from './utils/parseSpanDescription';
3333
type SpanNodeCompleted = SpanNode & { span: ReadableSpan };
3434

3535
const MAX_SPAN_COUNT = 1000;
36+
const DEFAULT_TIMEOUT = 300; // 5 min
3637

3738
/**
3839
* A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions.
3940
*/
4041
export class SentrySpanExporter {
4142
private _flushTimeout: ReturnType<typeof setTimeout> | undefined;
4243
private _finishedSpans: ReadableSpan[];
44+
private _timeout: number;
4345

44-
public constructor() {
46+
public constructor(options?: { timeout?: number }) {
4547
this._finishedSpans = [];
48+
this._timeout = options?.timeout || DEFAULT_TIMEOUT;
4649
}
4750

4851
/** Export a single span. */
@@ -103,7 +106,7 @@ export class SentrySpanExporter {
103106
*/
104107
private _cleanupOldSpans(spans = this._finishedSpans): void {
105108
this._finishedSpans = spans.filter(span => {
106-
const shouldDrop = shouldCleanupSpan(span, 5 * 60);
109+
const shouldDrop = shouldCleanupSpan(span, this._timeout);
107110
DEBUG_BUILD &&
108111
shouldDrop &&
109112
logger.log(

packages/opentelemetry/src/spanProcessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ function onSpanEnd(span: Span): void {
6565
export class SentrySpanProcessor implements SpanProcessorInterface {
6666
private _exporter: SentrySpanExporter;
6767

68-
public constructor() {
68+
public constructor(options?: { timeout?: number }) {
6969
setIsSetup('SentrySpanProcessor');
70-
this._exporter = new SentrySpanExporter();
70+
this._exporter = new SentrySpanExporter(options);
7171
}
7272

7373
/**

0 commit comments

Comments
 (0)