Skip to content

Commit 0d49557

Browse files
authored
fix(node-experimental): Ensure span.finish() works as expected (#8947)
For `Sentry.startSpan()` API, we need to ensure that when the Sentry Span is finished, we also finish the OTEL Span. Also adding some tests for these APIs.
1 parent 3487fa3 commit 0d49557

File tree

4 files changed

+210
-7
lines changed

4 files changed

+210
-7
lines changed

packages/node-experimental/src/sdk/trace.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function startActiveSpan<T>(context: TransactionContext, callback: (span:
2424
return callback(undefined);
2525
}
2626

27-
const name = context.description || context.op || '<unknown>';
27+
const name = context.name || context.description || context.op || '<unknown>';
2828

2929
return tracer.startActiveSpan(name, (span: OtelSpan): T => {
3030
const otelSpanId = span.spanContext().spanId;
@@ -82,18 +82,35 @@ export function startSpan(context: TransactionContext): Span | undefined {
8282
return undefined;
8383
}
8484

85-
const name = context.description || context.op || '<unknown>';
85+
const name = context.name || context.description || context.op || '<unknown>';
8686
const otelSpan = tracer.startSpan(name);
8787

8888
const otelSpanId = otelSpan.spanContext().spanId;
8989

9090
const sentrySpan = _INTERNAL_getSentrySpan(otelSpanId);
9191

92-
if (sentrySpan && isTransaction(sentrySpan) && context.metadata) {
92+
if (!sentrySpan) {
93+
return undefined;
94+
}
95+
96+
if (isTransaction(sentrySpan) && context.metadata) {
9397
sentrySpan.setMetadata(context.metadata);
9498
}
9599

96-
return sentrySpan;
100+
// Monkey-patch `finish()` to finish the OTEL span instead
101+
// This will also in turn finish the Sentry Span, so no need to call this ourselves
102+
const wrappedSentrySpan = new Proxy(sentrySpan, {
103+
get(target, prop, receiver) {
104+
if (prop === 'finish') {
105+
return () => {
106+
otelSpan.end();
107+
};
108+
}
109+
return Reflect.get(target, prop, receiver);
110+
},
111+
});
112+
113+
return wrappedSentrySpan;
97114
}
98115

99116
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { init } from '../../src/sdk/init';
2+
import type { NodeExperimentalClientOptions } from '../../src/types';
3+
4+
// eslint-disable-next-line no-var
5+
declare var global: any;
6+
7+
const PUBLIC_DSN = 'https://username@domain/123';
8+
9+
export function mockSdkInit(options?: Partial<NodeExperimentalClientOptions>) {
10+
global.__SENTRY__ = {};
11+
12+
init({ dsn: PUBLIC_DSN, defaultIntegrations: false, ...options });
13+
}

packages/node-experimental/test/sdk.test.ts renamed to packages/node-experimental/test/sdk/init.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Integration } from '@sentry/types';
22

3-
import * as auto from '../src/integrations/getAutoPerformanceIntegrations';
4-
import { init } from '../src/sdk/init';
5-
import * as sdk from '../src/sdk/init';
3+
import * as auto from '../../src/integrations/getAutoPerformanceIntegrations';
4+
import * as sdk from '../../src/sdk/init';
5+
import { init } from '../../src/sdk/init';
66

77
// eslint-disable-next-line no-var
88
declare var global: any;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Span, Transaction } from '@sentry/core';
2+
3+
import * as Sentry from '../../src';
4+
import { mockSdkInit } from '../helpers/mockSdkInit';
5+
6+
describe('trace', () => {
7+
beforeEach(() => {
8+
mockSdkInit({ enableTracing: true });
9+
});
10+
11+
describe('startActiveSpan', () => {
12+
it('works with a sync callback', () => {
13+
const spans: Span[] = [];
14+
15+
expect(Sentry.getActiveSpan()).toEqual(undefined);
16+
17+
Sentry.startActiveSpan({ name: 'outer' }, outerSpan => {
18+
expect(outerSpan).toBeDefined();
19+
spans.push(outerSpan!);
20+
21+
expect(outerSpan?.name).toEqual('outer');
22+
expect(outerSpan).toBeInstanceOf(Transaction);
23+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
24+
25+
Sentry.startActiveSpan({ name: 'inner' }, innerSpan => {
26+
expect(innerSpan).toBeDefined();
27+
spans.push(innerSpan!);
28+
29+
expect(innerSpan?.description).toEqual('inner');
30+
expect(innerSpan).toBeInstanceOf(Span);
31+
expect(innerSpan).not.toBeInstanceOf(Transaction);
32+
expect(Sentry.getActiveSpan()).toEqual(innerSpan);
33+
});
34+
});
35+
36+
expect(Sentry.getActiveSpan()).toEqual(undefined);
37+
expect(spans).toHaveLength(2);
38+
const [outerSpan, innerSpan] = spans;
39+
40+
expect((outerSpan as Transaction).name).toEqual('outer');
41+
expect(innerSpan.description).toEqual('inner');
42+
43+
expect(outerSpan.endTimestamp).toEqual(expect.any(Number));
44+
expect(innerSpan.endTimestamp).toEqual(expect.any(Number));
45+
});
46+
47+
it('works with an async callback', async () => {
48+
const spans: Span[] = [];
49+
50+
expect(Sentry.getActiveSpan()).toEqual(undefined);
51+
52+
await Sentry.startActiveSpan({ name: 'outer' }, async outerSpan => {
53+
expect(outerSpan).toBeDefined();
54+
spans.push(outerSpan!);
55+
56+
await new Promise(resolve => setTimeout(resolve, 10));
57+
58+
expect(outerSpan?.name).toEqual('outer');
59+
expect(outerSpan).toBeInstanceOf(Transaction);
60+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
61+
62+
await Sentry.startActiveSpan({ name: 'inner' }, async innerSpan => {
63+
expect(innerSpan).toBeDefined();
64+
spans.push(innerSpan!);
65+
66+
await new Promise(resolve => setTimeout(resolve, 10));
67+
68+
expect(innerSpan?.description).toEqual('inner');
69+
expect(innerSpan).toBeInstanceOf(Span);
70+
expect(innerSpan).not.toBeInstanceOf(Transaction);
71+
expect(Sentry.getActiveSpan()).toEqual(innerSpan);
72+
});
73+
});
74+
75+
expect(Sentry.getActiveSpan()).toEqual(undefined);
76+
expect(spans).toHaveLength(2);
77+
const [outerSpan, innerSpan] = spans;
78+
79+
expect((outerSpan as Transaction).name).toEqual('outer');
80+
expect(innerSpan.description).toEqual('inner');
81+
82+
expect(outerSpan.endTimestamp).toEqual(expect.any(Number));
83+
expect(innerSpan.endTimestamp).toEqual(expect.any(Number));
84+
});
85+
86+
it('works with multiple parallel calls', () => {
87+
const spans1: Span[] = [];
88+
const spans2: Span[] = [];
89+
90+
expect(Sentry.getActiveSpan()).toEqual(undefined);
91+
92+
Sentry.startActiveSpan({ name: 'outer' }, outerSpan => {
93+
expect(outerSpan).toBeDefined();
94+
spans1.push(outerSpan!);
95+
96+
expect(outerSpan?.name).toEqual('outer');
97+
expect(outerSpan).toBeInstanceOf(Transaction);
98+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
99+
100+
Sentry.startActiveSpan({ name: 'inner' }, innerSpan => {
101+
expect(innerSpan).toBeDefined();
102+
spans1.push(innerSpan!);
103+
104+
expect(innerSpan?.description).toEqual('inner');
105+
expect(innerSpan).toBeInstanceOf(Span);
106+
expect(innerSpan).not.toBeInstanceOf(Transaction);
107+
expect(Sentry.getActiveSpan()).toEqual(innerSpan);
108+
});
109+
});
110+
111+
Sentry.startActiveSpan({ name: 'outer2' }, outerSpan => {
112+
expect(outerSpan).toBeDefined();
113+
spans2.push(outerSpan!);
114+
115+
expect(outerSpan?.name).toEqual('outer2');
116+
expect(outerSpan).toBeInstanceOf(Transaction);
117+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
118+
119+
Sentry.startActiveSpan({ name: 'inner2' }, innerSpan => {
120+
expect(innerSpan).toBeDefined();
121+
spans2.push(innerSpan!);
122+
123+
expect(innerSpan?.description).toEqual('inner2');
124+
expect(innerSpan).toBeInstanceOf(Span);
125+
expect(innerSpan).not.toBeInstanceOf(Transaction);
126+
expect(Sentry.getActiveSpan()).toEqual(innerSpan);
127+
});
128+
});
129+
130+
expect(Sentry.getActiveSpan()).toEqual(undefined);
131+
expect(spans1).toHaveLength(2);
132+
expect(spans2).toHaveLength(2);
133+
});
134+
});
135+
136+
describe('startSpan', () => {
137+
it('works at the root', () => {
138+
const span = Sentry.startSpan({ name: 'test' });
139+
140+
expect(span).toBeDefined();
141+
expect(span).toBeInstanceOf(Transaction);
142+
expect(span?.name).toEqual('test');
143+
expect(span?.endTimestamp).toBeUndefined();
144+
expect(Sentry.getActiveSpan()).toBeUndefined();
145+
146+
span?.finish();
147+
148+
expect(span?.endTimestamp).toEqual(expect.any(Number));
149+
expect(Sentry.getActiveSpan()).toBeUndefined();
150+
});
151+
152+
it('works as a child span', () => {
153+
Sentry.startActiveSpan({ name: 'outer' }, outerSpan => {
154+
expect(outerSpan).toBeDefined();
155+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
156+
157+
const innerSpan = Sentry.startSpan({ name: 'test' });
158+
159+
expect(innerSpan).toBeDefined();
160+
expect(innerSpan).toBeInstanceOf(Span);
161+
expect(innerSpan).not.toBeInstanceOf(Transaction);
162+
expect(innerSpan?.description).toEqual('test');
163+
expect(innerSpan?.endTimestamp).toBeUndefined();
164+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
165+
166+
innerSpan?.finish();
167+
168+
expect(innerSpan?.endTimestamp).toEqual(expect.any(Number));
169+
expect(Sentry.getActiveSpan()).toEqual(outerSpan);
170+
});
171+
});
172+
});
173+
});

0 commit comments

Comments
 (0)