Skip to content

Commit f688034

Browse files
authored
feat(core): Ensure trace APIs always return a span (#10942)
When tracing is disabled or otherwise skipped, they return a non recording span instead of undefined. This aligns the `startSpan`, `startSpanManual` and `startInactiveSpan` APIs with opentelemetry, where you also always get a span back.
1 parent 02b0dad commit f688034

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+394
-274
lines changed

packages/bun/src/integrations/bunserver.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
9696
typeof serveOptions.fetch
9797
>);
9898
if (response && response.status) {
99-
if (span) {
100-
setHttpStatus(span, response.status);
101-
}
99+
setHttpStatus(span, response.status);
102100
if (span instanceof Transaction) {
103101
const scope = getCurrentScope();
104102
scope.setContext('response', {

packages/core/src/fetch.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
setHttpStatus,
1515
startInactiveSpan,
1616
} from './tracing';
17+
import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan';
1718
import { hasTracingEnabled } from './utils/hasTracingEnabled';
1819
import { spanToTraceHeader } from './utils/spanUtils';
1920

@@ -92,12 +93,10 @@ export function instrumentFetchRequest(
9293
},
9394
op: 'http.client',
9495
})
95-
: undefined;
96+
: new SentryNonRecordingSpan();
9697

97-
if (span) {
98-
handlerData.fetchData.__span = span.spanContext().spanId;
99-
spans[span.spanContext().spanId] = span;
100-
}
98+
handlerData.fetchData.__span = span.spanContext().spanId;
99+
spans[span.spanContext().spanId] = span;
101100

102101
if (shouldAttachHeaders(handlerData.fetchData.url) && client) {
103102
const request: string | Request = handlerData.args[0];

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
spanToJSON,
9191
spanIsSampled,
9292
spanToTraceContext,
93+
getSpanDescendants,
9394
} from './utils/spanUtils';
9495
export { getRootSpan } from './utils/getRootSpan';
9596
export { applySdkMetadata } from './utils/sdkMetadata';

packages/core/src/tracing/idleSpan.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { logger, timestampInSeconds } from '@sentry/utils';
33
import { getClient, getCurrentScope } from '../currentScopes';
44

55
import { DEBUG_BUILD } from '../debug-build';
6-
import { spanToJSON } from '../utils/spanUtils';
6+
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
7+
import { getSpanDescendants, removeChildSpanFromSpan, spanToJSON } from '../utils/spanUtils';
8+
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
79
import { SPAN_STATUS_ERROR } from './spanstatus';
810
import { startInactiveSpan } from './trace';
9-
import { getActiveSpan, getSpanDescendants, removeChildSpanFromSpan } from './utils';
11+
import { getActiveSpan } from './utils';
1012

1113
export const TRACING_DEFAULTS = {
1214
idleTimeout: 1_000,
@@ -71,10 +73,7 @@ interface IdleSpanOptions {
7173
* An idle span is a span that automatically finishes. It does this by tracking child spans as activities.
7274
* An idle span is always the active span.
7375
*/
74-
export function startIdleSpan(
75-
startSpanOptions: StartSpanOptions,
76-
options: Partial<IdleSpanOptions> = {},
77-
): Span | undefined {
76+
export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Partial<IdleSpanOptions> = {}): Span {
7877
// Activities store a list of active spans
7978
const activities = new Map<string, boolean>();
8079

@@ -101,21 +100,13 @@ export function startIdleSpan(
101100

102101
const client = getClient();
103102

104-
if (!client) {
105-
return;
103+
if (!client || !hasTracingEnabled()) {
104+
return new SentryNonRecordingSpan();
106105
}
107106

108107
const scope = getCurrentScope();
109108
const previousActiveSpan = getActiveSpan();
110-
const _span = _startIdleSpan(startSpanOptions);
111-
112-
// Span _should_ always be defined here, but TS does not know that...
113-
if (!_span) {
114-
return;
115-
}
116-
117-
// For TS, so that we know everything below here has a span
118-
const span = _span;
109+
const span = _startIdleSpan(startSpanOptions);
119110

120111
/**
121112
* Cancels the existing idle timeout, if there is one.
@@ -319,15 +310,13 @@ export function startIdleSpan(
319310
return span;
320311
}
321312

322-
function _startIdleSpan(options: StartSpanOptions): Span | undefined {
313+
function _startIdleSpan(options: StartSpanOptions): Span {
323314
const span = startInactiveSpan(options);
324315

325316
// eslint-disable-next-line deprecation/deprecation
326317
getCurrentScope().setSpan(span);
327318

328-
if (span) {
329-
DEBUG_BUILD && logger.log(`Setting idle span on scope. Span ID: ${span.spanContext().spanId}`);
330-
}
319+
DEBUG_BUILD && logger.log(`Setting idle span on scope. Span ID: ${span.spanContext().spanId}`);
331320

332321
return span;
333322
}

packages/core/src/tracing/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ export { startIdleTransaction, addTracingExtensions } from './hubextensions';
22
export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction';
33
export type { BeforeFinishCallback } from './idletransaction';
44
export { SentrySpan } from './sentrySpan';
5+
export { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
56
export { Transaction } from './transaction';
67
// eslint-disable-next-line deprecation/deprecation
7-
export { getActiveTransaction, getActiveSpan, getSpanDescendants } from './utils';
8+
export { getActiveTransaction, getActiveSpan } from './utils';
89
export {
910
setHttpStatus,
1011
getSpanStatusFromHttpCode,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type {
2+
Span,
3+
SpanAttributeValue,
4+
SpanAttributes,
5+
SpanContext,
6+
SpanContextData,
7+
SpanStatus,
8+
SpanTimeInput,
9+
} from '@sentry/types';
10+
import { uuid4 } from '@sentry/utils';
11+
import { TRACE_FLAG_NONE } from '../utils/spanUtils';
12+
13+
/**
14+
* A Sentry Span that is non-recording, meaning it will not be sent to Sentry.
15+
*/
16+
export class SentryNonRecordingSpan implements Span {
17+
private _traceId: string;
18+
private _spanId: string;
19+
20+
public constructor(spanContext: SpanContext = {}) {
21+
this._traceId = spanContext.traceId || uuid4();
22+
this._spanId = spanContext.spanId || uuid4().substring(16);
23+
}
24+
25+
/** @inheritdoc */
26+
public spanContext(): SpanContextData {
27+
return {
28+
spanId: this._spanId,
29+
traceId: this._traceId,
30+
traceFlags: TRACE_FLAG_NONE,
31+
};
32+
}
33+
34+
/** @inheritdoc */
35+
// eslint-disable-next-line @typescript-eslint/no-empty-function
36+
public end(_timestamp?: SpanTimeInput): void {}
37+
38+
/** @inheritdoc */
39+
public setAttribute(_key: string, _value: SpanAttributeValue | undefined): this {
40+
return this;
41+
}
42+
43+
/** @inheritdoc */
44+
public setAttributes(_values: SpanAttributes): this {
45+
return this;
46+
}
47+
48+
/** @inheritdoc */
49+
public setStatus(_status: SpanStatus): this {
50+
return this;
51+
}
52+
53+
/** @inheritdoc */
54+
public updateName(_name: string): this {
55+
return this;
56+
}
57+
58+
/** @inheritdoc */
59+
public isRecording(): boolean {
60+
return false;
61+
}
62+
}

packages/core/src/tracing/sentrySpan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import { getRootSpan } from '../utils/getRootSpan';
2222
import {
2323
TRACE_FLAG_NONE,
2424
TRACE_FLAG_SAMPLED,
25+
addChildSpanToSpan,
2526
getStatusMessage,
2627
spanTimeInputToSeconds,
2728
spanToJSON,
2829
spanToTraceContext,
2930
} from '../utils/spanUtils';
30-
import { addChildSpanToSpan } from './utils';
3131

3232
/**
3333
* Keeps track of finished spans for a given transaction

packages/core/src/tracing/trace.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { DEBUG_BUILD } from '../debug-build';
77
import { getCurrentHub } from '../hub';
88
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
99
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
10-
import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
10+
import { addChildSpanToSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
1111
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
12+
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
1213
import type { SentrySpan } from './sentrySpan';
1314
import { SPAN_STATUS_ERROR } from './spanstatus';
14-
import { addChildSpanToSpan, getActiveSpan, setCapturedScopesOnSpan } from './utils';
15+
import { getActiveSpan, setCapturedScopesOnSpan } from './utils';
1516

1617
/**
1718
* Wraps a function with a transaction/span and finishes the span after the function is done.
@@ -24,7 +25,7 @@ import { addChildSpanToSpan, getActiveSpan, setCapturedScopesOnSpan } from './ut
2425
* or you didn't set `tracesSampleRate`, this function will not generate spans
2526
* and the `span` returned from the callback will be undefined.
2627
*/
27-
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span | undefined) => T): T {
28+
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span) => T): T {
2829
const spanContext = normalizeContext(context);
2930

3031
return withScope(context.scope, scope => {
@@ -35,18 +36,16 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span |
3536

3637
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
3738
const activeSpan = shouldSkipSpan
38-
? undefined
39+
? new SentryNonRecordingSpan()
3940
: createChildSpanOrTransaction(hub, {
4041
parentSpan,
4142
spanContext,
4243
forceTransaction: context.forceTransaction,
4344
scope,
4445
});
4546

46-
if (activeSpan) {
47-
// eslint-disable-next-line deprecation/deprecation
48-
scope.setSpan(activeSpan);
49-
}
47+
// eslint-disable-next-line deprecation/deprecation
48+
scope.setSpan(activeSpan);
5049

5150
return handleCallbackErrors(
5251
() => callback(activeSpan),
@@ -75,10 +74,7 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span |
7574
* or you didn't set `tracesSampleRate`, this function will not generate spans
7675
* and the `span` returned from the callback will be undefined.
7776
*/
78-
export function startSpanManual<T>(
79-
context: StartSpanOptions,
80-
callback: (span: Span | undefined, finish: () => void) => T,
81-
): T {
77+
export function startSpanManual<T>(context: StartSpanOptions, callback: (span: Span, finish: () => void) => T): T {
8278
const spanContext = normalizeContext(context);
8379

8480
return withScope(context.scope, scope => {
@@ -89,18 +85,16 @@ export function startSpanManual<T>(
8985

9086
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
9187
const activeSpan = shouldSkipSpan
92-
? undefined
88+
? new SentryNonRecordingSpan()
9389
: createChildSpanOrTransaction(hub, {
9490
parentSpan,
9591
spanContext,
9692
forceTransaction: context.forceTransaction,
9793
scope,
9894
});
9995

100-
if (activeSpan) {
101-
// eslint-disable-next-line deprecation/deprecation
102-
scope.setSpan(activeSpan);
103-
}
96+
// eslint-disable-next-line deprecation/deprecation
97+
scope.setSpan(activeSpan);
10498

10599
function finishAndSetSpan(): void {
106100
activeSpan && activeSpan.end();
@@ -131,11 +125,7 @@ export function startSpanManual<T>(
131125
* or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans
132126
* and the `span` returned from the callback will be undefined.
133127
*/
134-
export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
135-
if (!hasTracingEnabled()) {
136-
return undefined;
137-
}
138-
128+
export function startInactiveSpan(context: StartSpanOptions): Span {
139129
const spanContext = normalizeContext(context);
140130
// eslint-disable-next-line deprecation/deprecation
141131
const hub = getCurrentHub();
@@ -147,7 +137,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
147137
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
148138

149139
if (shouldSkipSpan) {
150-
return undefined;
140+
return new SentryNonRecordingSpan();
151141
}
152142

153143
const scope = context.scope || getCurrentScope();
@@ -275,14 +265,14 @@ function createChildSpanOrTransaction(
275265
forceTransaction?: boolean;
276266
scope: Scope;
277267
},
278-
): Span | undefined {
268+
): Span {
279269
if (!hasTracingEnabled()) {
280-
return undefined;
270+
return new SentryNonRecordingSpan();
281271
}
282272

283273
const isolationScope = getIsolationScope();
284274

285-
let span: Span | undefined;
275+
let span: Span;
286276
if (parentSpan && !forceTransaction) {
287277
// eslint-disable-next-line deprecation/deprecation
288278
span = parentSpan.startChild(spanContext);

packages/core/src/tracing/transaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { DEBUG_BUILD } from '../debug-build';
1919
import { getCurrentHub } from '../hub';
2020
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
2121
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
22-
import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
22+
import { getSpanDescendants, spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
2323
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2424
import { SentrySpan, SpanRecorder } from './sentrySpan';
25-
import { getCapturedScopesOnSpan, getSpanDescendants } from './utils';
25+
import { getCapturedScopesOnSpan } from './utils';
2626

2727
/** JSDoc */
2828
export class Transaction extends SentrySpan implements TransactionInterface {

packages/core/src/tracing/utils.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -31,54 +31,6 @@ export function getActiveSpan(): Span | undefined {
3131
return getCurrentScope().getSpan();
3232
}
3333

34-
const CHILD_SPANS_FIELD = '_sentryChildSpans';
35-
36-
type SpanWithPotentialChildren = Span & {
37-
[CHILD_SPANS_FIELD]?: Set<Span>;
38-
};
39-
40-
/**
41-
* Adds an opaque child span reference to a span.
42-
*/
43-
export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: Span): void {
44-
if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) {
45-
span[CHILD_SPANS_FIELD].add(childSpan);
46-
} else {
47-
span[CHILD_SPANS_FIELD] = new Set([childSpan]);
48-
}
49-
}
50-
51-
/** This is only used internally by Idle Spans. */
52-
export function removeChildSpanFromSpan(span: SpanWithPotentialChildren, childSpan: Span): void {
53-
if (span[CHILD_SPANS_FIELD]) {
54-
span[CHILD_SPANS_FIELD].delete(childSpan);
55-
}
56-
}
57-
58-
/**
59-
* Returns an array of the given span and all of its descendants.
60-
*/
61-
export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] {
62-
const resultSet = new Set<Span>();
63-
64-
function addSpanChildren(span: SpanWithPotentialChildren): void {
65-
// This exit condition is required to not infinitely loop in case of a circular dependency.
66-
if (resultSet.has(span)) {
67-
return;
68-
} else {
69-
resultSet.add(span);
70-
const childSpans = span[CHILD_SPANS_FIELD] ? Array.from(span[CHILD_SPANS_FIELD]) : [];
71-
for (const childSpan of childSpans) {
72-
addSpanChildren(childSpan);
73-
}
74-
}
75-
}
76-
77-
addSpanChildren(span);
78-
79-
return Array.from(resultSet);
80-
}
81-
8234
const SCOPE_ON_START_SPAN_FIELD = '_sentryScope';
8335
const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
8436

0 commit comments

Comments
 (0)