Skip to content

Commit 324e5bf

Browse files
authored
feat(core): Deprecate Span.transaction in favor of getRootSpan (#10134)
Deprecate the `transaction` field on the `Span` interface and class. Instead, we'll store the span hierarchy in a different format external to the span class. In node-experimental, we use a WeakMap referencing the parent span as a data structure which we might need to generally do in v8. This, however, is a breaking change, so for now we deprecate the field but use it in the replacement utility function for v7.
1 parent 98979d8 commit 324e5bf

File tree

19 files changed

+100
-27
lines changed

19 files changed

+100
-27
lines changed

MIGRATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ In v8, the Span class is heavily reworked. The following properties & methods ar
152152
- `span.setTag()`: Use `span.setAttribute()` instead or set tags on the surrounding scope.
153153
- `span.setData()`: Use `span.setAttribute()` instead.
154154
- `span.instrumenter` This field was removed and will be replaced internally.
155+
- `span.transaction`: Use `getRootSpan` utility function instead.
155156
- `transaction.setContext()`: Set context on the surrounding scope instead.
156157

157158
## Deprecate `pushScope` & `popScope` in favor of `withScope`

packages/astro/src/server/meta.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
getDynamicSamplingContextFromClient,
33
getDynamicSamplingContextFromSpan,
4+
getRootSpan,
45
spanToTraceHeader,
56
} from '@sentry/core';
67
import type { Client, Scope, Span } from '@sentry/types';
@@ -32,12 +33,12 @@ export function getTracingMetaTags(
3233
client: Client | undefined,
3334
): { sentryTrace: string; baggage?: string } {
3435
const { dsc, sampled, traceId } = scope.getPropagationContext();
35-
const transaction = span?.transaction;
36+
const rootSpan = span && getRootSpan(span);
3637

3738
const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);
3839

39-
const dynamicSamplingContext = transaction
40-
? getDynamicSamplingContextFromSpan(transaction)
40+
const dynamicSamplingContext = rootSpan
41+
? getDynamicSamplingContextFromSpan(rootSpan)
4142
: dsc
4243
? dsc
4344
: client

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export {
8181
spanToJSON,
8282
spanIsSampled,
8383
} from './utils/spanUtils';
84+
export { getRootSpan } from './utils/getRootSpan';
8485
export { DEFAULT_ENVIRONMENT } from './constants';
8586
export { ModuleMetadata } from './integrations/metadata';
8687
export { RequestData } from './integrations/requestdata';

packages/core/src/scope.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ export class Scope implements ScopeInterface {
334334
// Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will
335335
// have a pointer to the currently-active transaction.
336336
const span = this._span;
337+
// Cannot replace with getRootSpan because getRootSpan returns a span, not a transaction
338+
// Also, this method will be removed anyway.
339+
// eslint-disable-next-line deprecation/deprecation
337340
return span && span.transaction;
338341
}
339342

packages/core/src/server-runtime-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getDynamicSamplingContextFromClient,
2727
getDynamicSamplingContextFromSpan,
2828
} from './tracing';
29+
import { getRootSpan } from './utils/getRootSpan';
2930
import { spanToTraceContext } from './utils/spanUtils';
3031

3132
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
@@ -262,7 +263,7 @@ export class ServerRuntimeClient<
262263
// eslint-disable-next-line deprecation/deprecation
263264
const span = scope.getSpan();
264265
if (span) {
265-
const samplingContext = span.transaction ? getDynamicSamplingContextFromSpan(span) : undefined;
266+
const samplingContext = getRootSpan(span) ? getDynamicSamplingContextFromSpan(span) : undefined;
266267
return [samplingContext, spanToTraceContext(span)];
267268
}
268269

packages/core/src/tracing/dynamicSamplingContext.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { dropUndefinedKeys } from '@sentry/utils';
33

44
import { DEFAULT_ENVIRONMENT } from '../constants';
55
import { getClient, getCurrentScope } from '../exports';
6+
import { getRootSpan } from '../utils/getRootSpan';
67
import { spanIsSampled, spanToJSON } from '../utils/spanUtils';
78

89
/**
@@ -54,9 +55,8 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<
5455
// passing emit=false here to only emit later once the DSC is actually populated
5556
const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client, getCurrentScope());
5657

57-
// As long as we use `Transaction`s internally, this should be fine.
58-
// TODO: We need to replace this with a `getRootSpan(span)` function though
59-
const txn = span.transaction as TransactionWithV7FrozenDsc | undefined;
58+
// TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext
59+
const txn = getRootSpan(span) as TransactionWithV7FrozenDsc | undefined;
6060
if (!txn) {
6161
return dsc;
6262
}

packages/core/src/tracing/span.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1717

1818
import { DEBUG_BUILD } from '../debug-build';
19+
import { getRootSpan } from '../utils/getRootSpan';
1920
import {
2021
TRACE_FLAG_NONE,
2122
TRACE_FLAG_SAMPLED,
@@ -105,6 +106,7 @@ export class Span implements SpanInterface {
105106

106107
/**
107108
* @inheritDoc
109+
* @deprecated Use top level `Sentry.getRootSpan()` instead
108110
*/
109111
public transaction?: Transaction;
110112

@@ -304,12 +306,16 @@ export class Span implements SpanInterface {
304306
childSpan.spanRecorder.add(childSpan);
305307
}
306308

307-
childSpan.transaction = this.transaction;
309+
const rootSpan = getRootSpan(this);
310+
// TODO: still set span.transaction here until we have a more permanent solution
311+
// Probably similarly to the weakmap we hold in node-experimental
312+
// eslint-disable-next-line deprecation/deprecation
313+
childSpan.transaction = rootSpan as Transaction;
308314

309-
if (DEBUG_BUILD && childSpan.transaction) {
315+
if (DEBUG_BUILD && rootSpan) {
310316
const opStr = (spanContext && spanContext.op) || '< unknown op >';
311317
const nameStr = spanToJSON(childSpan).description || '< unknown name >';
312-
const idStr = childSpan.transaction.spanContext().spanId;
318+
const idStr = rootSpan.spanContext().spanId;
313319

314320
const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`;
315321
logger.log(logMessage);
@@ -416,11 +422,12 @@ export class Span implements SpanInterface {
416422

417423
/** @inheritdoc */
418424
public end(endTimestamp?: SpanTimeInput): void {
425+
const rootSpan = getRootSpan(this);
419426
if (
420427
DEBUG_BUILD &&
421428
// Don't call this for transactions
422-
this.transaction &&
423-
this.transaction.spanContext().spanId !== this._spanId
429+
rootSpan &&
430+
rootSpan.spanContext().spanId !== this._spanId
424431
) {
425432
const logMessage = this._logMessage;
426433
if (logMessage) {

packages/core/src/tracing/transaction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export class Transaction extends SpanClass implements TransactionInterface {
6666
this._trimEnd = transactionContext.trimEnd;
6767

6868
// this is because transactions are also spans, and spans have a transaction pointer
69+
// TODO (v8): Replace this with another way to set the root span
70+
// eslint-disable-next-line deprecation/deprecation
6971
this.transaction = this;
7072

7173
// If Dynamic Sampling Context is provided during the creation of the transaction, we freeze it as it usually means

packages/core/src/utils/applyScopeDataToEvent.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types';
22
import { arrayify } from '@sentry/utils';
33
import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext';
4+
import { getRootSpan } from './getRootSpan';
45
import { spanToJSON, spanToTraceContext } from './spanUtils';
56

67
/**
@@ -174,13 +175,13 @@ function applySdkMetadataToEvent(
174175

175176
function applySpanToEvent(event: Event, span: Span): void {
176177
event.contexts = { trace: spanToTraceContext(span), ...event.contexts };
177-
const transaction = span.transaction;
178-
if (transaction) {
178+
const rootSpan = getRootSpan(span);
179+
if (rootSpan) {
179180
event.sdkProcessingMetadata = {
180181
dynamicSamplingContext: getDynamicSamplingContextFromSpan(span),
181182
...event.sdkProcessingMetadata,
182183
};
183-
const transactionName = spanToJSON(transaction).description;
184+
const transactionName = spanToJSON(rootSpan).description;
184185
if (transactionName) {
185186
event.tags = { transaction: transactionName, ...event.tags };
186187
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Span } from '@sentry/types';
2+
3+
/**
4+
* Returns the root span of a given span.
5+
*
6+
* As long as we use `Transaction`s internally, the returned root span
7+
* will be a `Transaction` but be aware that this might change in the future.
8+
*
9+
* If the given span has no root span or transaction, `undefined` is returned.
10+
*/
11+
export function getRootSpan(span: Span): Span | undefined {
12+
// TODO (v8): Remove this check and just return span
13+
// eslint-disable-next-line deprecation/deprecation
14+
return span.transaction;
15+
}

packages/core/test/lib/tracing/dynamicSamplingContext.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('getDynamicSamplingContextFromSpan', () => {
2020
});
2121

2222
test('returns the DSC provided during transaction creation', () => {
23-
// eslint-disable-next-line deprecation/deprecation
23+
// eslint-disable-next-line deprecation/deprecation -- using old API on purpose
2424
const transaction = new Transaction({
2525
name: 'tx',
2626
metadata: { dynamicSamplingContext: { environment: 'myEnv' } },
@@ -68,7 +68,7 @@ describe('getDynamicSamplingContextFromSpan', () => {
6868
});
6969

7070
test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => {
71-
// eslint-disable-next-line deprecation/deprecation
71+
// eslint-disable-next-line deprecation/deprecation -- using old API on purpose
7272
const transaction = new Transaction({
7373
name: 'tx',
7474
metadata: {
@@ -92,7 +92,7 @@ describe('getDynamicSamplingContextFromSpan', () => {
9292

9393
describe('Including transaction name in DSC', () => {
9494
test('is not included if transaction source is url', () => {
95-
// eslint-disable-next-line deprecation/deprecation
95+
// eslint-disable-next-line deprecation/deprecation -- using old API on purpose
9696
const transaction = new Transaction({
9797
name: 'tx',
9898
metadata: {
@@ -109,8 +109,7 @@ describe('getDynamicSamplingContextFromSpan', () => {
109109
['is included if transaction source is parameterized route/url', 'route'],
110110
['is included if transaction source is a custom name', 'custom'],
111111
])('%s', (_: string, source) => {
112-
// eslint-disable-next-line deprecation/deprecation
113-
const transaction = new Transaction({
112+
const transaction = startInactiveSpan({
114113
name: 'tx',
115114
metadata: {
116115
...(source && { source: source as TransactionSource }),
@@ -120,7 +119,7 @@ describe('getDynamicSamplingContextFromSpan', () => {
120119
// Only setting the attribute manually because we're directly calling new Transaction()
121120
transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
122121

123-
const dsc = getDynamicSamplingContextFromSpan(transaction);
122+
const dsc = getDynamicSamplingContextFromSpan(transaction!);
124123

125124
expect(dsc.transaction).toEqual('tx');
126125
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Span, Transaction, getRootSpan } from '../../../src';
2+
3+
describe('getRootSpan', () => {
4+
it('returns the root span of a span (Span)', () => {
5+
const root = new Span({ name: 'test' });
6+
// @ts-expect-error this is highly illegal and shouldn't happen IRL
7+
// eslint-disable-next-line deprecation/deprecation
8+
root.transaction = root;
9+
10+
// eslint-disable-next-line deprecation/deprecation
11+
const childSpan = root.startChild({ name: 'child' });
12+
expect(getRootSpan(childSpan)).toBe(root);
13+
});
14+
15+
it('returns the root span of a span (Transaction)', () => {
16+
// eslint-disable-next-line deprecation/deprecation
17+
const root = new Transaction({ name: 'test' });
18+
19+
// eslint-disable-next-line deprecation/deprecation
20+
const childSpan = root.startChild({ name: 'child' });
21+
expect(getRootSpan(childSpan)).toBe(root);
22+
});
23+
24+
it('returns the span itself if it is a root span', () => {
25+
// eslint-disable-next-line deprecation/deprecation
26+
const span = new Transaction({ name: 'test' });
27+
28+
expect(getRootSpan(span)).toBe(span);
29+
});
30+
31+
it('returns undefined if span has no root span', () => {
32+
const span = new Span({ name: 'test' });
33+
34+
expect(getRootSpan(span)).toBe(undefined);
35+
});
36+
});

packages/opentelemetry-node/src/propagator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Baggage, Context, TextMapGetter, TextMapSetter } from '@opentelemetry/api';
22
import { TraceFlags, isSpanContextValid, propagation, trace } from '@opentelemetry/api';
33
import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core';
4-
import { getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core';
4+
import { getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
55
import {
66
SENTRY_BAGGAGE_KEY_PREFIX,
77
baggageHeaderToDynamicSamplingContext,
@@ -35,7 +35,7 @@ export class SentryPropagator extends W3CBaggagePropagator {
3535
if (span) {
3636
setter.set(carrier, SENTRY_TRACE_HEADER, spanToTraceHeader(span));
3737

38-
if (span.transaction) {
38+
if (getRootSpan(span)) {
3939
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span);
4040
baggage = Object.entries(dynamicSamplingContext).reduce<Baggage>((b, [dscKey, dscValue]) => {
4141
if (dscValue) {

packages/opentelemetry-node/src/utils/spanMap.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getRootSpan } from '@sentry/core';
12
import type { Span as SentrySpan } from '@sentry/types';
23

34
interface SpanMapEntry {
@@ -31,7 +32,7 @@ export function getSentrySpan(spanId: string): SentrySpan | undefined {
3132
export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void {
3233
let ref: SpanRefType = SPAN_REF_ROOT;
3334

34-
const rootSpanId = sentrySpan.transaction?.spanContext().spanId;
35+
const rootSpanId = getRootSpan(sentrySpan)?.spanContext().spanId;
3536

3637
if (rootSpanId && rootSpanId !== spanId) {
3738
const root = SPAN_MAP.get(rootSpanId);

packages/svelte/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"dependencies": {
3232
"@sentry/browser": "7.93.0",
33+
"@sentry/core": "7.93.0",
3334
"@sentry/types": "7.93.0",
3435
"@sentry/utils": "7.93.0",
3536
"magic-string": "^0.30.0"

packages/svelte/src/performance.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Span, Transaction } from '@sentry/types';
33
import { afterUpdate, beforeUpdate, onMount } from 'svelte';
44
import { current_component } from 'svelte/internal';
55

6+
import { getRootSpan } from '@sentry/core';
67
import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants';
78
import type { TrackComponentOptions } from './types';
89

@@ -74,7 +75,7 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void {
7475
// If we are initializing the component when the update span is started, we start it as child
7576
// of the init span. Else, we start it as a child of the transaction.
7677
const parentSpan =
77-
initSpan && !initSpan.endTimestamp && initSpan.transaction === transaction ? initSpan : transaction;
78+
initSpan && !initSpan.endTimestamp && getRootSpan(initSpan) === transaction ? initSpan : transaction;
7879

7980
// eslint-disable-next-line deprecation/deprecation
8081
updateSpan = parentSpan.startChild({

packages/tracing-internal/src/browser/request.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getCurrentScope,
66
getDynamicSamplingContextFromClient,
77
getDynamicSamplingContextFromSpan,
8+
getRootSpan,
89
hasTracingEnabled,
910
spanToJSON,
1011
spanToTraceHeader,
@@ -298,7 +299,7 @@ export function xhrCallback(
298299

299300
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
300301
if (span) {
301-
const transaction = span && span.transaction;
302+
const transaction = span && getRootSpan(span);
302303
const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction);
303304
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
304305
setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader);

packages/tracing-internal/src/common/fetch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getCurrentScope,
55
getDynamicSamplingContextFromClient,
66
getDynamicSamplingContextFromSpan,
7+
getRootSpan,
78
hasTracingEnabled,
89
spanToTraceHeader,
910
} from '@sentry/core';
@@ -134,7 +135,7 @@ export function addTracingHeadersToFetchRequest(
134135
// eslint-disable-next-line deprecation/deprecation
135136
const span = requestSpan || scope.getSpan();
136137

137-
const transaction = span && span.transaction;
138+
const transaction = span && getRootSpan(span);
138139

139140
const { traceId, sampled, dsc } = scope.getPropagationContext();
140141

packages/types/src/span.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export interface Span extends SpanContext {
216216

217217
/**
218218
* The transaction containing this span
219+
* @deprecated Use top level `Sentry.getRootSpan()` instead
219220
*/
220221
transaction?: Transaction;
221222

0 commit comments

Comments
 (0)