Skip to content

Commit 6faec42

Browse files
authored
feat(core): Deprecate Transaction.getDynamicSamplingContext in favour of getDynamicSamplingContextFromSpan (#10094)
Deprecate `Transaction.getDynamicSamplingContext` and introduce its direct replacement, a top-level utility function. Note that this only is an intermediate step we should take to rework how we generate and handle the DSC creation. More details in #10095
1 parent eb5bb3d commit 6faec42

File tree

24 files changed

+276
-75
lines changed

24 files changed

+276
-75
lines changed

MIGRATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ In v8, the Span class is heavily reworked. The following properties & methods ar
9898
* `span.traceId`: Use `span.spanContext().traceId` instead.
9999
* `span.name`: Use `spanToJSON(span).description` instead.
100100
* `span.description`: Use `spanToJSON(span).description` instead.
101+
* `span.getDynamicSamplingContext`: Use `getDynamicSamplingContextFromSpan` utility function instead.
101102
* `transaction.setMetadata()`: Use attributes instead, or set data on the scope.
102103
* `transaction.metadata`: Use attributes instead, or set data on the scope.
103104
* `span.tags`: Set tags on the surrounding scope instead, or use attributes.

dev-packages/rollup-utils/plugins/bundlePlugins.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export function makeTerserPlugin() {
135135
// These are used by instrument.ts in utils for identifying HTML elements & events
136136
'_sentryCaptured',
137137
'_sentryId',
138+
// For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext
139+
// TODO (v8): Remove this reserved word
140+
'_frozenDynamicSamplingContext',
138141
],
139142
},
140143
},

packages/astro/src/server/meta.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { getDynamicSamplingContextFromClient, spanToTraceHeader } from '@sentry/core';
1+
import {
2+
getDynamicSamplingContextFromClient,
3+
getDynamicSamplingContextFromSpan,
4+
spanToTraceHeader,
5+
} from '@sentry/core';
26
import type { Client, Scope, Span } from '@sentry/types';
37
import {
48
TRACEPARENT_REGEXP,
@@ -33,7 +37,7 @@ export function getTracingMetaTags(
3337
const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);
3438

3539
const dynamicSamplingContext = transaction
36-
? transaction.getDynamicSamplingContext()
40+
? getDynamicSamplingContextFromSpan(transaction)
3741
: dsc
3842
? dsc
3943
: client

packages/astro/test/server/meta.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const mockedScope = {
3232
describe('getTracingMetaTags', () => {
3333
it('returns the tracing tags from the span, if it is provided', () => {
3434
{
35+
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({
36+
environment: 'production',
37+
});
38+
3539
const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient);
3640

3741
expect(tags).toEqual({

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { getClient } from './exports';
2121
import { MetricsAggregator } from './metrics/aggregator';
2222
import type { Scope } from './scope';
2323
import { SessionFlusher } from './sessionflusher';
24-
import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing';
24+
import {
25+
addTracingExtensions,
26+
getDynamicSamplingContextFromClient,
27+
getDynamicSamplingContextFromSpan,
28+
} from './tracing';
2529
import { spanToTraceContext } from './utils/spanUtils';
2630

2731
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
@@ -258,7 +262,7 @@ export class ServerRuntimeClient<
258262
// eslint-disable-next-line deprecation/deprecation
259263
const span = scope.getSpan();
260264
if (span) {
261-
const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined;
265+
const samplingContext = span.transaction ? getDynamicSamplingContextFromSpan(span) : undefined;
262266
return [samplingContext, spanToTraceContext(span)];
263267
}
264268

packages/core/src/tracing/dynamicSamplingContext.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { Client, DynamicSamplingContext, Scope } from '@sentry/types';
1+
import type { Client, DynamicSamplingContext, Scope, Span, Transaction } from '@sentry/types';
22
import { dropUndefinedKeys } from '@sentry/utils';
33

44
import { DEFAULT_ENVIRONMENT } from '../constants';
5+
import { getClient, getCurrentScope } from '../exports';
6+
import { spanIsSampled, spanToJSON } from '../utils/spanUtils';
57

68
/**
79
* Creates a dynamic sampling context from a client.
810
*
9-
* Dispatchs the `createDsc` lifecycle hook as a side effect.
11+
* Dispatches the `createDsc` lifecycle hook as a side effect.
1012
*/
1113
export function getDynamicSamplingContextFromClient(
1214
trace_id: string,
@@ -30,3 +32,62 @@ export function getDynamicSamplingContextFromClient(
3032

3133
return dsc;
3234
}
35+
36+
/**
37+
* A Span with a frozen dynamic sampling context.
38+
*/
39+
type TransactionWithV7FrozenDsc = Transaction & { _frozenDynamicSamplingContext?: DynamicSamplingContext };
40+
41+
/**
42+
* Creates a dynamic sampling context from a span (and client and scope)
43+
*
44+
* @param span the span from which a few values like the root span name and sample rate are extracted.
45+
*
46+
* @returns a dynamic sampling context
47+
*/
48+
export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<DynamicSamplingContext>> {
49+
const client = getClient();
50+
if (!client) {
51+
return {};
52+
}
53+
54+
// passing emit=false here to only emit later once the DSC is actually populated
55+
const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client, getCurrentScope());
56+
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;
60+
if (!txn) {
61+
return dsc;
62+
}
63+
64+
// TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext
65+
// For now we need to avoid breaking users who directly created a txn with a DSC, where this field is still set.
66+
// @see Transaction class constructor
67+
const v7FrozenDsc = txn && txn._frozenDynamicSamplingContext;
68+
if (v7FrozenDsc) {
69+
return v7FrozenDsc;
70+
}
71+
72+
// TODO (v8): Replace txn.metadata with txn.attributes[]
73+
// We can't do this yet because attributes aren't always set yet.
74+
// eslint-disable-next-line deprecation/deprecation
75+
const { sampleRate: maybeSampleRate, source } = txn.metadata;
76+
if (maybeSampleRate != null) {
77+
dsc.sample_rate = `${maybeSampleRate}`;
78+
}
79+
80+
// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
81+
const jsonSpan = spanToJSON(txn);
82+
83+
// after JSON conversion, txn.name becomes jsonSpan.description
84+
if (source && source !== 'url') {
85+
dsc.transaction = jsonSpan.description;
86+
}
87+
88+
dsc.sampled = String(spanIsSampled(txn));
89+
90+
client.emit && client.emit('createDsc', dsc);
91+
92+
return dsc;
93+
}

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ export {
1919
startSpanManual,
2020
continueTrace,
2121
} from './trace';
22-
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
22+
export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2323
export { setMeasurement } from './measurement';

packages/core/src/tracing/transaction.ts

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { Hub } from '../hub';
1717
import { getCurrentHub } from '../hub';
1818
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
1919
import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils';
20-
import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
20+
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2121
import { Span as SpanClass, SpanRecorder } from './span';
2222

2323
/** JSDoc */
@@ -35,6 +35,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
3535

3636
private _trimEnd?: boolean;
3737

38+
// DO NOT yet remove this property, it is used in a hack for v7 backwards compatibility.
3839
private _frozenDynamicSamplingContext: Readonly<Partial<DynamicSamplingContext>> | undefined;
3940

4041
private _metadata: Partial<TransactionMetadata>;
@@ -230,43 +231,11 @@ export class Transaction extends SpanClass implements TransactionInterface {
230231
* @inheritdoc
231232
*
232233
* @experimental
234+
*
235+
* @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead.
233236
*/
234237
public getDynamicSamplingContext(): Readonly<Partial<DynamicSamplingContext>> {
235-
if (this._frozenDynamicSamplingContext) {
236-
return this._frozenDynamicSamplingContext;
237-
}
238-
239-
const hub = this._hub || getCurrentHub();
240-
const client = hub.getClient();
241-
242-
if (!client) return {};
243-
244-
const { _traceId: traceId, _sampled: sampled } = this;
245-
246-
const scope = hub.getScope();
247-
const dsc = getDynamicSamplingContextFromClient(traceId, client, scope);
248-
249-
// eslint-disable-next-line deprecation/deprecation
250-
const maybeSampleRate = this.metadata.sampleRate;
251-
if (maybeSampleRate !== undefined) {
252-
dsc.sample_rate = `${maybeSampleRate}`;
253-
}
254-
255-
// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
256-
// eslint-disable-next-line deprecation/deprecation
257-
const source = this.metadata.source;
258-
if (source && source !== 'url') {
259-
dsc.transaction = this._name;
260-
}
261-
262-
if (sampled !== undefined) {
263-
dsc.sampled = String(sampled);
264-
}
265-
266-
// Uncomment if we want to make DSC immutable
267-
// this._frozenDynamicSamplingContext = dsc;
268-
269-
return dsc;
238+
return getDynamicSamplingContextFromSpan(this);
270239
}
271240

272241
/**
@@ -344,7 +313,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
344313
type: 'transaction',
345314
sdkProcessingMetadata: {
346315
...metadata,
347-
dynamicSamplingContext: this.getDynamicSamplingContext(),
316+
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
348317
},
349318
...(source && {
350319
transaction_info: {

packages/core/src/utils/applyScopeDataToEvent.ts

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

56
/**
@@ -176,7 +177,7 @@ function applySpanToEvent(event: Event, span: Span): void {
176177
const transaction = span.transaction;
177178
if (transaction) {
178179
event.sdkProcessingMetadata = {
179-
dynamicSamplingContext: transaction.getDynamicSamplingContext(),
180+
dynamicSamplingContext: getDynamicSamplingContextFromSpan(span),
180181
...event.sdkProcessingMetadata,
181182
};
182183
const transactionName = spanToJSON(transaction).description;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { TransactionSource } from '@sentry/types';
2+
import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, makeMain } from '../../../src';
3+
import { Transaction, getDynamicSamplingContextFromSpan, startInactiveSpan } from '../../../src/tracing';
4+
import { addTracingExtensions } from '../../../src/tracing';
5+
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
6+
7+
describe('getDynamicSamplingContextFromSpan', () => {
8+
let hub: Hub;
9+
beforeEach(() => {
10+
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0, release: '1.0.1' });
11+
const client = new TestClient(options);
12+
hub = new Hub(client);
13+
hub.bindClient(client);
14+
makeMain(hub);
15+
addTracingExtensions();
16+
});
17+
18+
afterEach(() => {
19+
jest.resetAllMocks();
20+
});
21+
22+
test('returns the DSC provided during transaction creation', () => {
23+
const transaction = new Transaction({
24+
name: 'tx',
25+
metadata: { dynamicSamplingContext: { environment: 'myEnv' } },
26+
});
27+
28+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction);
29+
30+
expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' });
31+
});
32+
33+
test('returns a new DSC, if no DSC was provided during transaction creation (via attributes)', () => {
34+
const transaction = startInactiveSpan({ name: 'tx' });
35+
36+
// Setting the attribute should overwrite the computed values
37+
transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56);
38+
transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
39+
40+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!);
41+
42+
expect(dynamicSamplingContext).toStrictEqual({
43+
release: '1.0.1',
44+
environment: 'production',
45+
sampled: 'true',
46+
sample_rate: '0.56',
47+
trace_id: expect.any(String),
48+
transaction: 'tx',
49+
});
50+
});
51+
52+
test('returns a new DSC, if no DSC was provided during transaction creation (via deprecated metadata)', () => {
53+
const transaction = startInactiveSpan({
54+
name: 'tx',
55+
});
56+
57+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!);
58+
59+
expect(dynamicSamplingContext).toStrictEqual({
60+
release: '1.0.1',
61+
environment: 'production',
62+
sampled: 'true',
63+
sample_rate: '1',
64+
trace_id: expect.any(String),
65+
transaction: 'tx',
66+
});
67+
});
68+
69+
test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => {
70+
const transaction = new Transaction({
71+
name: 'tx',
72+
metadata: {
73+
sampleRate: 0.56,
74+
source: 'route',
75+
},
76+
sampled: true,
77+
});
78+
79+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!);
80+
81+
expect(dynamicSamplingContext).toStrictEqual({
82+
release: '1.0.1',
83+
environment: 'production',
84+
sampled: 'true',
85+
sample_rate: '0.56',
86+
trace_id: expect.any(String),
87+
transaction: 'tx',
88+
});
89+
});
90+
91+
describe('Including transaction name in DSC', () => {
92+
test('is not included if transaction source is url', () => {
93+
const transaction = new Transaction({
94+
name: 'tx',
95+
metadata: {
96+
source: 'url',
97+
sampleRate: 0.56,
98+
},
99+
});
100+
101+
const dsc = getDynamicSamplingContextFromSpan(transaction);
102+
expect(dsc.transaction).toBeUndefined();
103+
});
104+
105+
test.each([
106+
['is included if transaction source is parameterized route/url', 'route'],
107+
['is included if transaction source is a custom name', 'custom'],
108+
])('%s', (_: string, source) => {
109+
const transaction = new Transaction({
110+
name: 'tx',
111+
metadata: {
112+
...(source && { source: source as TransactionSource }),
113+
},
114+
});
115+
116+
// Only setting the attribute manually because we're directly calling new Transaction()
117+
transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
118+
119+
const dsc = getDynamicSamplingContextFromSpan(transaction);
120+
121+
expect(dsc.transaction).toEqual('tx');
122+
});
123+
});
124+
});

packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core';
1+
import {
2+
addTracingExtensions,
3+
getClient,
4+
getCurrentScope,
5+
getDynamicSamplingContextFromSpan,
6+
spanToTraceHeader,
7+
} from '@sentry/core';
28
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
39
import type App from 'next/app';
410

@@ -66,7 +72,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
6672
if (requestTransaction) {
6773
appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestTransaction);
6874

69-
const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext();
75+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction);
7076
appGetInitialProps.pageProps._sentryBaggage =
7177
dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
7278
}

0 commit comments

Comments
 (0)