Skip to content

Commit 77139c4

Browse files
mydeaanonrig
authored andcommitted
feat(node-experimental): Allow to pass base span options to trace methods (#10006)
This was brought up in Slack, currently you cannot pass other span properties directly to otel `startSpan()`. This now adds `attributes`, `kind` and `startTime` to better align with OTEL. Note that I chose to omit `root` and `links` options, not sure if we want to support those in none-OTEL environments, and this API should (~~) be the same for node & non-node in v8.
1 parent db48635 commit 77139c4

File tree

6 files changed

+173
-7
lines changed

6 files changed

+173
-7
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const startActiveSpan = startSpan;
138138

139139
/**
140140
* Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span
141-
* after the function is done automatically.
141+
* after the function is done automatically. You'll have to call `span.end()` manually.
142142
*
143143
* The created span is the active span and will be used as parent by other spans created inside the function
144144
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.

packages/node-experimental/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanc
1313
export * as Handlers from './sdk/handlers';
1414
export type { Span } from './types';
1515

16-
export { startSpan, startInactiveSpan, getActiveSpan } from '@sentry/opentelemetry';
16+
export { startSpan, startSpanManual, startInactiveSpan, getActiveSpan } from '@sentry/opentelemetry';
1717
export {
1818
getClient,
1919
addBreadcrumb,

packages/opentelemetry/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export {
2929
export { isSentryRequestSpan } from './utils/isSentryRequest';
3030

3131
export { getActiveSpan, getRootSpan } from './utils/getActiveSpan';
32-
export { startSpan, startInactiveSpan } from './trace';
32+
export { startSpan, startSpanManual, startInactiveSpan } from './trace';
3333

3434
export { getCurrentHub, setupGlobalHub, getClient } from './custom/hub';
3535
export { OpenTelemetryScope } from './custom/scope';

packages/opentelemetry/src/trace.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function startSpan<T>(spanContext: OpenTelemetrySpanContext, callback: (s
2323

2424
const { name } = spanContext;
2525

26-
return tracer.startActiveSpan(name, span => {
26+
return tracer.startActiveSpan(name, spanContext, span => {
2727
function finishSpan(): void {
2828
span.end();
2929
}
@@ -57,6 +57,46 @@ export function startSpan<T>(spanContext: OpenTelemetrySpanContext, callback: (s
5757
});
5858
}
5959

60+
/**
61+
* Similar to `Sentry.startSpan`. Wraps a function with a span, but does not finish the span
62+
* after the function is done automatically. You'll have to call `span.end()` manually.
63+
*
64+
* The created span is the active span and will be used as parent by other spans created inside the function
65+
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
66+
*
67+
* Note that you'll always get a span passed to the callback, it may just be a NonRecordingSpan if the span is not sampled.
68+
*/
69+
export function startSpanManual<T>(spanContext: OpenTelemetrySpanContext, callback: (span: Span) => T): T {
70+
const tracer = getTracer();
71+
72+
const { name } = spanContext;
73+
74+
// @ts-expect-error - isThenable returns the wrong type
75+
return tracer.startActiveSpan(name, spanContext, span => {
76+
_applySentryAttributesToSpan(span, spanContext);
77+
78+
let maybePromiseResult: T;
79+
try {
80+
maybePromiseResult = callback(span);
81+
} catch (e) {
82+
span.setStatus({ code: SpanStatusCode.ERROR });
83+
throw e;
84+
}
85+
86+
if (isThenable(maybePromiseResult)) {
87+
return maybePromiseResult.then(
88+
res => res,
89+
e => {
90+
span.setStatus({ code: SpanStatusCode.ERROR });
91+
throw e;
92+
},
93+
);
94+
}
95+
96+
return maybePromiseResult;
97+
});
98+
}
99+
60100
/**
61101
* @deprecated Use {@link startSpan} instead.
62102
*/
@@ -77,7 +117,7 @@ export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span {
77117

78118
const { name } = spanContext;
79119

80-
const span = tracer.startSpan(name);
120+
const span = tracer.startSpan(name, spanContext);
81121

82122
_applySentryAttributesToSpan(span, spanContext);
83123

packages/opentelemetry/src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api';
1+
import type { Attributes, Span as WriteableSpan, SpanKind, TimeInput, Tracer } from '@opentelemetry/api';
22
import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base';
33
import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types';
44

@@ -13,6 +13,11 @@ export interface OpenTelemetrySpanContext {
1313
metadata?: Partial<TransactionMetadata>;
1414
origin?: SpanOrigin;
1515
source?: TransactionSource;
16+
17+
// Base SpanOptions we support
18+
attributes?: Attributes;
19+
kind?: SpanKind;
20+
startTime?: TimeInput;
1621
}
1722

1823
/**

packages/opentelemetry/test/trace.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { Span } from '@opentelemetry/api';
2+
import { SpanKind } from '@opentelemetry/api';
23
import { TraceFlags, context, trace } from '@opentelemetry/api';
34
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
45
import type { PropagationContext } from '@sentry/types';
56

67
import { getCurrentHub } from '../src/custom/hub';
78
import { InternalSentrySemanticAttributes } from '../src/semanticAttributes';
8-
import { startInactiveSpan, startSpan } from '../src/trace';
9+
import { startInactiveSpan, startSpan, startSpanManual } from '../src/trace';
910
import type { AbstractSpan } from '../src/types';
1011
import { setPropagationContextOnContext } from '../src/utils/contextData';
1112
import { getActiveSpan, getRootSpan } from '../src/utils/getActiveSpan';
13+
import { getSpanKind } from '../src/utils/getSpanKind';
1214
import { getSpanMetadata } from '../src/utils/spanData';
1315
import { spanHasAttributes, spanHasName } from '../src/utils/spanTypes';
1416
import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit';
@@ -231,6 +233,33 @@ describe('trace', () => {
231233
},
232234
);
233235
});
236+
237+
it('allows to pass base SpanOptions', () => {
238+
const date = Date.now() - 1000;
239+
240+
startSpan(
241+
{
242+
name: 'outer',
243+
kind: SpanKind.CLIENT,
244+
attributes: {
245+
test1: 'test 1',
246+
test2: 2,
247+
},
248+
249+
startTime: date,
250+
},
251+
span => {
252+
expect(span).toBeDefined();
253+
expect(getSpanName(span)).toEqual('outer');
254+
expect(getSpanAttributes(span)).toEqual({
255+
[InternalSentrySemanticAttributes.SAMPLE_RATE]: 1,
256+
test1: 'test 1',
257+
test2: 2,
258+
});
259+
expect(getSpanKind(span)).toEqual(SpanKind.CLIENT);
260+
},
261+
);
262+
});
234263
});
235264

236265
describe('startInactiveSpan', () => {
@@ -297,6 +326,98 @@ describe('trace', () => {
297326

298327
expect(getSpanMetadata(span2)).toEqual({ requestPath: 'test-path' });
299328
});
329+
330+
it('allows to pass base SpanOptions', () => {
331+
const date = Date.now() - 1000;
332+
333+
const span = startInactiveSpan({
334+
name: 'outer',
335+
kind: SpanKind.CLIENT,
336+
attributes: {
337+
test1: 'test 1',
338+
test2: 2,
339+
},
340+
startTime: date,
341+
});
342+
343+
expect(span).toBeDefined();
344+
expect(getSpanName(span)).toEqual('outer');
345+
expect(getSpanAttributes(span)).toEqual({
346+
[InternalSentrySemanticAttributes.SAMPLE_RATE]: 1,
347+
test1: 'test 1',
348+
test2: 2,
349+
});
350+
expect(getSpanKind(span)).toEqual(SpanKind.CLIENT);
351+
});
352+
});
353+
354+
describe('startSpanManual', () => {
355+
it('does not automatically finish the span', () => {
356+
expect(getActiveSpan()).toEqual(undefined);
357+
358+
let _outerSpan: Span | undefined;
359+
let _innerSpan: Span | undefined;
360+
361+
const res = startSpanManual({ name: 'outer' }, outerSpan => {
362+
expect(outerSpan).toBeDefined();
363+
_outerSpan = outerSpan;
364+
365+
expect(getSpanName(outerSpan)).toEqual('outer');
366+
expect(getActiveSpan()).toEqual(outerSpan);
367+
368+
startSpanManual({ name: 'inner' }, innerSpan => {
369+
expect(innerSpan).toBeDefined();
370+
_innerSpan = innerSpan;
371+
372+
expect(getSpanName(innerSpan)).toEqual('inner');
373+
expect(getActiveSpan()).toEqual(innerSpan);
374+
});
375+
376+
expect(getSpanEndTime(_innerSpan!)).toEqual([0, 0]);
377+
378+
_innerSpan!.end();
379+
380+
expect(getSpanEndTime(_innerSpan!)).not.toEqual([0, 0]);
381+
382+
return 'test value';
383+
});
384+
385+
expect(getSpanEndTime(_outerSpan!)).toEqual([0, 0]);
386+
387+
_outerSpan!.end();
388+
389+
expect(getSpanEndTime(_outerSpan!)).not.toEqual([0, 0]);
390+
391+
expect(res).toEqual('test value');
392+
393+
expect(getActiveSpan()).toEqual(undefined);
394+
});
395+
396+
it('allows to pass base SpanOptions', () => {
397+
const date = Date.now() - 1000;
398+
399+
startSpanManual(
400+
{
401+
name: 'outer',
402+
kind: SpanKind.CLIENT,
403+
attributes: {
404+
test1: 'test 1',
405+
test2: 2,
406+
},
407+
startTime: date,
408+
},
409+
span => {
410+
expect(span).toBeDefined();
411+
expect(getSpanName(span)).toEqual('outer');
412+
expect(getSpanAttributes(span)).toEqual({
413+
[InternalSentrySemanticAttributes.SAMPLE_RATE]: 1,
414+
test1: 'test 1',
415+
test2: 2,
416+
});
417+
expect(getSpanKind(span)).toEqual(SpanKind.CLIENT);
418+
},
419+
);
420+
});
300421
});
301422
});
302423

0 commit comments

Comments
 (0)