Skip to content

Commit e249f36

Browse files
authored
feat(core): Allow custom tracing implementations (#11003)
And do this for opentelemetry, instead of relying on extra exports. Also needed to move things around a bit to avoid circular dependency build issues. Missing is `getRootSpan` which is still different, I will update this in a follow up to actually use the same non-enumerable property under the hood so we can use the same implementation there! ## Reasoning Until now, we had a core implementation of tracing APIs in `@sentry/core`. In `@sentry/opentelemetry`, we had some alternative implementations, which where instead re-exported in `@sentry/node` - so if you do e.g. this: ```js import { startSpan } from '@sentry/node'; ``` You'll get the correct, OTEL-powered performance API - great! However, if any code would internally do e.g. this: ```js import { getActiveSpan } from '@sentry/core'; // import from core, not node ``` It would not work, because it would use the non-OTEL-powered API. This PR addresses this by ensuring we use the same API everywhere when `@sentry/node` is being used.
1 parent 6d4af1c commit e249f36

22 files changed

+461
-162
lines changed

packages/core/src/asyncContext.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Hub, Integration } from '@sentry/types';
22
import type { Scope } from '@sentry/types';
33
import { GLOBAL_OBJ } from '@sentry/utils';
4+
import type { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './tracing/trace';
5+
import type { getActiveSpan } from './utils/spanUtils';
46

57
/**
68
* @private Private API with no semver guarantees!
@@ -42,6 +44,24 @@ export interface AsyncContextStrategy {
4244
* Get the currently active isolation scope.
4345
*/
4446
getIsolationScope: () => Scope;
47+
48+
// OPTIONAL: Custom tracing methods
49+
// These are used so that we can provide OTEL-based implementations
50+
51+
/** Start an active span. */
52+
startSpan?: typeof startSpan;
53+
54+
/** Start an inactive span. */
55+
startInactiveSpan?: typeof startInactiveSpan;
56+
57+
/** Start an active manual span. */
58+
startSpanManual?: typeof startSpanManual;
59+
60+
/** Get the currently active span. */
61+
getActiveSpan?: typeof getActiveSpan;
62+
63+
/** Make a span the active span in the context of the callback. */
64+
withActiveSpan?: typeof withActiveSpan;
4565
}
4666

4767
/**

packages/core/src/exports.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,16 @@ import type {
1010
FinishedCheckIn,
1111
MonitorConfig,
1212
Primitive,
13-
Scope as ScopeInterface,
1413
Session,
1514
SessionContext,
1615
SeverityLevel,
17-
Span,
1816
TransactionContext,
1917
User,
2018
} from '@sentry/types';
2119
import { GLOBAL_OBJ, isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
2220

2321
import { DEFAULT_ENVIRONMENT } from './constants';
24-
import { getClient, getCurrentScope, getIsolationScope, withScope } from './currentScopes';
22+
import { getClient, getCurrentScope, getIsolationScope } from './currentScopes';
2523
import { DEBUG_BUILD } from './debug-build';
2624
import type { Hub } from './hub';
2725
import { getCurrentHub } from './hub';
@@ -126,23 +124,6 @@ export function setUser(user: User | null): ReturnType<Hub['setUser']> {
126124
getIsolationScope().setUser(user);
127125
}
128126

129-
/**
130-
* Forks the current scope and sets the provided span as active span in the context of the provided callback. Can be
131-
* passed `null` to start an entirely new span tree.
132-
*
133-
* @param span Spans started in the context of the provided callback will be children of this span. If `null` is passed,
134-
* spans started within the callback will not be attached to a parent span.
135-
* @param callback Execution context in which the provided span will be active. Is passed the newly forked scope.
136-
* @returns the value returned from the provided callback function.
137-
*/
138-
export function withActiveSpan<T>(span: Span | null, callback: (scope: ScopeInterface) => T): T {
139-
return withScope(scope => {
140-
// eslint-disable-next-line deprecation/deprecation
141-
scope.setSpan(span || undefined);
142-
return callback(scope);
143-
});
144-
}
145-
146127
/**
147128
* Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation.
148129
*

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export {
2929
startSession,
3030
endSession,
3131
captureSession,
32-
withActiveSpan,
3332
addEventProcessor,
3433
} from './exports';
3534
export {
@@ -93,6 +92,7 @@ export {
9392
getSpanDescendants,
9493
getStatusMessage,
9594
getRootSpan,
95+
getActiveSpan,
9696
} from './utils/spanUtils';
9797
export { applySdkMetadata } from './utils/sdkMetadata';
9898
export { DEFAULT_ENVIRONMENT } from './constants';

packages/core/src/metrics/aggregator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorBase, Primitive } from '@sentry/types';
22
import { timestampInSeconds } from '@sentry/utils';
3+
import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils';
34
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
45
import { captureAggregateMetrics } from './envelope';
56
import { METRIC_MAP } from './instance';
6-
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
77
import type { MetricBucket, MetricType } from './types';
88
import { getBucketKey, sanitizeTags } from './utils';
99

packages/core/src/metrics/browser-aggregator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Client, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';
22
import { timestampInSeconds } from '@sentry/utils';
3+
import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils';
34
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
45
import { captureAggregateMetrics } from './envelope';
56
import { METRIC_MAP } from './instance';
6-
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
77
import type { MetricBucket, MetricType } from './types';
88
import { getBucketKey, sanitizeTags } from './utils';
99

packages/core/src/metrics/metric-summary.ts

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { MeasurementUnit, Span } from '@sentry/types';
22
import type { MetricSummary } from '@sentry/types';
33
import type { Primitive } from '@sentry/types';
44
import { dropUndefinedKeys } from '@sentry/utils';
5-
import { getActiveSpan } from '../tracing/utils';
65
import type { MetricType } from './types';
76

87
/**
@@ -40,52 +39,50 @@ export function getMetricSummaryJsonForSpan(span: Span): Record<string, Array<Me
4039
}
4140

4241
/**
43-
* Updates the metric summary on the currently active span
42+
* Updates the metric summary on a span.
4443
*/
45-
export function updateMetricSummaryOnActiveSpan(
44+
export function updateMetricSummaryOnSpan(
45+
span: Span,
4646
metricType: MetricType,
4747
sanitizedName: string,
4848
value: number,
4949
unit: MeasurementUnit,
5050
tags: Record<string, Primitive>,
5151
bucketKey: string,
5252
): void {
53-
const span = getActiveSpan();
54-
if (span) {
55-
const storage = getMetricStorageForSpan(span) || new Map<string, [string, MetricSummary]>();
53+
const storage = getMetricStorageForSpan(span) || new Map<string, [string, MetricSummary]>();
5654

57-
const exportKey = `${metricType}:${sanitizedName}@${unit}`;
58-
const bucketItem = storage.get(bucketKey);
55+
const exportKey = `${metricType}:${sanitizedName}@${unit}`;
56+
const bucketItem = storage.get(bucketKey);
5957

60-
if (bucketItem) {
61-
const [, summary] = bucketItem;
62-
storage.set(bucketKey, [
63-
exportKey,
64-
{
65-
min: Math.min(summary.min, value),
66-
max: Math.max(summary.max, value),
67-
count: (summary.count += 1),
68-
sum: (summary.sum += value),
69-
tags: summary.tags,
70-
},
71-
]);
72-
} else {
73-
storage.set(bucketKey, [
74-
exportKey,
75-
{
76-
min: value,
77-
max: value,
78-
count: 1,
79-
sum: value,
80-
tags,
81-
},
82-
]);
83-
}
84-
85-
if (!SPAN_METRIC_SUMMARY) {
86-
SPAN_METRIC_SUMMARY = new WeakMap();
87-
}
58+
if (bucketItem) {
59+
const [, summary] = bucketItem;
60+
storage.set(bucketKey, [
61+
exportKey,
62+
{
63+
min: Math.min(summary.min, value),
64+
max: Math.max(summary.max, value),
65+
count: (summary.count += 1),
66+
sum: (summary.sum += value),
67+
tags: summary.tags,
68+
},
69+
]);
70+
} else {
71+
storage.set(bucketKey, [
72+
exportKey,
73+
{
74+
min: value,
75+
max: value,
76+
count: 1,
77+
sum: value,
78+
tags,
79+
},
80+
]);
81+
}
8882

89-
SPAN_METRIC_SUMMARY.set(span, storage);
83+
if (!SPAN_METRIC_SUMMARY) {
84+
SPAN_METRIC_SUMMARY = new WeakMap();
9085
}
86+
87+
SPAN_METRIC_SUMMARY.set(span, storage);
9188
}

packages/core/src/tracing/errors.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import {
55
} from '@sentry/utils';
66

77
import { DEBUG_BUILD } from '../debug-build';
8-
import { getRootSpan } from '../utils/spanUtils';
8+
import { getActiveSpan, getRootSpan } from '../utils/spanUtils';
99
import { SPAN_STATUS_ERROR } from './spanstatus';
10-
import { getActiveSpan } from './utils';
1110

1211
let errorsInstrumented = false;
1312

packages/core/src/tracing/idleSpan.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { getClient, getCurrentScope } from '../currentScopes';
55
import { DEBUG_BUILD } from '../debug-build';
66
import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes';
77
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
8-
import { getSpanDescendants, removeChildSpanFromSpan, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
8+
import {
9+
getActiveSpan,
10+
getSpanDescendants,
11+
removeChildSpanFromSpan,
12+
spanTimeInputToSeconds,
13+
spanToJSON,
14+
} from '../utils/spanUtils';
915
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
1016
import { SPAN_STATUS_ERROR } from './spanstatus';
1117
import { startInactiveSpan } from './trace';
12-
import { getActiveSpan } from './utils';
1318

1419
export const TRACING_DEFAULTS = {
1520
idleTimeout: 1_000,

packages/core/src/tracing/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export { SentrySpan } from './sentrySpan';
44
export { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
55
export { Transaction } from './transaction';
66
// eslint-disable-next-line deprecation/deprecation
7-
export { getActiveTransaction, getActiveSpan } from './utils';
7+
export { getActiveTransaction } from './utils';
88
export {
99
setHttpStatus,
1010
getSpanStatusFromHttpCode,
@@ -15,6 +15,7 @@ export {
1515
startInactiveSpan,
1616
startSpanManual,
1717
continueTrace,
18+
withActiveSpan,
1819
} from './trace';
1920
export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2021
export { setMeasurement } from './measurement';

packages/core/src/tracing/measurement.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { MeasurementUnit, Span, Transaction } from '@sentry/types';
2-
import { getRootSpan } from '../utils/spanUtils';
3-
4-
import { getActiveSpan } from './utils';
2+
import { getActiveSpan, getRootSpan } from '../utils/spanUtils';
53

64
/**
75
* Adds a measurement to the current active transaction.

packages/core/src/tracing/trace.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import type { Hub, Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types';
22

33
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
4+
import type { AsyncContextStrategy } from '../asyncContext';
5+
import { getMainCarrier } from '../asyncContext';
46
import { getCurrentScope, getIsolationScope, withScope } from '../currentScopes';
57

68
import { DEBUG_BUILD } from '../debug-build';
7-
import { getCurrentHub } from '../hub';
9+
import { getAsyncContextStrategy, getCurrentHub } from '../hub';
810
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
911
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
10-
import { addChildSpanToSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
12+
import {
13+
addChildSpanToSpan,
14+
getActiveSpan,
15+
spanIsSampled,
16+
spanTimeInputToSeconds,
17+
spanToJSON,
18+
} from '../utils/spanUtils';
1119
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
1220
import { SentryNonRecordingSpan } from './sentryNonRecordingSpan';
1321
import type { SentrySpan } from './sentrySpan';
1422
import { SPAN_STATUS_ERROR } from './spanstatus';
15-
import { getActiveSpan, setCapturedScopesOnSpan } from './utils';
23+
import { setCapturedScopesOnSpan } from './utils';
1624

1725
/**
1826
* Wraps a function with a transaction/span and finishes the span after the function is done.
@@ -26,6 +34,11 @@ import { getActiveSpan, setCapturedScopesOnSpan } from './utils';
2634
* and the `span` returned from the callback will be undefined.
2735
*/
2836
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span) => T): T {
37+
const acs = getAcs();
38+
if (acs.startSpan) {
39+
return acs.startSpan(context, callback);
40+
}
41+
2942
const spanContext = normalizeContext(context);
3043

3144
return withScope(context.scope, scope => {
@@ -73,6 +86,11 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span) =
7386
* and the `span` returned from the callback will be undefined.
7487
*/
7588
export function startSpanManual<T>(context: StartSpanOptions, callback: (span: Span, finish: () => void) => T): T {
89+
const acs = getAcs();
90+
if (acs.startSpanManual) {
91+
return acs.startSpanManual(context, callback);
92+
}
93+
7694
const spanContext = normalizeContext(context);
7795

7896
return withScope(context.scope, scope => {
@@ -122,6 +140,11 @@ export function startSpanManual<T>(context: StartSpanOptions, callback: (span: S
122140
* and the `span` returned from the callback will be undefined.
123141
*/
124142
export function startInactiveSpan(context: StartSpanOptions): Span {
143+
const acs = getAcs();
144+
if (acs.startInactiveSpan) {
145+
return acs.startInactiveSpan(context);
146+
}
147+
125148
const spanContext = normalizeContext(context);
126149
// eslint-disable-next-line deprecation/deprecation
127150
const hub = getCurrentHub();
@@ -248,6 +271,28 @@ export const continueTrace: ContinueTrace = <V>(
248271
});
249272
};
250273

274+
/**
275+
* Forks the current scope and sets the provided span as active span in the context of the provided callback. Can be
276+
* passed `null` to start an entirely new span tree.
277+
*
278+
* @param span Spans started in the context of the provided callback will be children of this span. If `null` is passed,
279+
* spans started within the callback will not be attached to a parent span.
280+
* @param callback Execution context in which the provided span will be active. Is passed the newly forked scope.
281+
* @returns the value returned from the provided callback function.
282+
*/
283+
export function withActiveSpan<T>(span: Span | null, callback: (scope: Scope) => T): T {
284+
const acs = getAcs();
285+
if (acs.withActiveSpan) {
286+
return acs.withActiveSpan(span, callback);
287+
}
288+
289+
return withScope(scope => {
290+
// eslint-disable-next-line deprecation/deprecation
291+
scope.setSpan(span || undefined);
292+
return callback(scope);
293+
});
294+
}
295+
251296
function createChildSpanOrTransaction(
252297
hub: Hub,
253298
{
@@ -340,3 +385,8 @@ function normalizeContext(context: StartSpanOptions): TransactionContext {
340385

341386
return context;
342387
}
388+
389+
function getAcs(): AsyncContextStrategy {
390+
const carrier = getMainCarrier();
391+
return getAsyncContextStrategy(carrier);
392+
}

packages/core/src/tracing/utils.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Span, Transaction } from '@sentry/types';
22
import type { Scope } from '@sentry/types';
33
import { addNonEnumerableProperty } from '@sentry/utils';
4-
import { getCurrentScope } from '../currentScopes';
54

65
import type { Hub } from '../hub';
76
import { getCurrentHub } from '../hub';
@@ -23,14 +22,6 @@ export function getActiveTransaction<T extends Transaction>(maybeHub?: Hub): T |
2322
// so it can be used in manual instrumentation without necessitating a hard dependency on @sentry/utils
2423
export { stripUrlQueryAndFragment } from '@sentry/utils';
2524

26-
/**
27-
* Returns the currently active span.
28-
*/
29-
export function getActiveSpan(): Span | undefined {
30-
// eslint-disable-next-line deprecation/deprecation
31-
return getCurrentScope().getSpan();
32-
}
33-
3425
const SCOPE_ON_START_SPAN_FIELD = '_sentryScope';
3526
const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
3627

0 commit comments

Comments
 (0)