Skip to content

Commit 8b8febb

Browse files
authored
feat(node): Allow to configure maxSpanWaitDuration (#12610)
1 parent f905c98 commit 8b8febb

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)