Skip to content

Commit 9006287

Browse files
AbhiPrasadmydea
andauthored
ref(tracing): Add necessary helpers for using propagation context on outgoing headers (#8434)
This PR adds: 1. A getter for `PropagationContext` on the scope 2. `generateSentryTraceHeader`, which will be used to dynamically create `sentry-trace` headers, regardless of if there is a span or not 3. `getDynamicSamplingContextFromClient`, which is used to generate dynamic sampling context from a client directly (instead of having to go through a transaction) This PR also updates `extractTraceparentData` and `dynamicSamplingContextToSentryBaggageHeader` to be more liberal about the values it takes so we can better accommodate the new flows we are going to add. Co-authored-by: Francesco Novy <[email protected]>
1 parent 3625fb1 commit 9006287

File tree

18 files changed

+120
-63
lines changed

18 files changed

+120
-63
lines changed

packages/core/src/baseclient.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
addItemToEnvelope,
3131
checkOrSetAlreadyCaught,
3232
createAttachmentEnvelopeItem,
33-
dropUndefinedKeys,
3433
isPlainObject,
3534
isPrimitive,
3635
isThenable,
@@ -43,12 +42,12 @@ import {
4342
} from '@sentry/utils';
4443

4544
import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
46-
import { DEFAULT_ENVIRONMENT } from './constants';
4745
import { createEventEnvelope, createSessionEnvelope } from './envelope';
4846
import type { IntegrationIndex } from './integration';
4947
import { setupIntegration, setupIntegrations } from './integration';
5048
import type { Scope } from './scope';
5149
import { updateSession } from './session';
50+
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
5251
import { prepareEvent } from './utils/prepareEvent';
5352

5453
const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
@@ -531,20 +530,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
531530
...evt.contexts,
532531
};
533532

534-
const { publicKey: public_key } = this.getDsn() || {};
535-
const { segment: user_segment } = (scope && scope.getUser()) || {};
536-
537-
let dynamicSamplingContext = dsc;
538-
if (!dsc) {
539-
dynamicSamplingContext = dropUndefinedKeys({
540-
environment: options.environment || DEFAULT_ENVIRONMENT,
541-
release: options.release,
542-
user_segment,
543-
public_key,
544-
trace_id,
545-
});
546-
this.emit && this.emit('createDsc', dynamicSamplingContext);
547-
}
533+
const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope);
548534

549535
evt.sdkProcessingMetadata = {
550536
dynamicSamplingContext,

packages/core/src/scope.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,13 +533,20 @@ export class Scope implements ScopeInterface {
533533
}
534534

535535
/**
536-
* @inheritdoc
536+
* @inheritDoc
537537
*/
538538
public setPropagationContext(context: PropagationContext): this {
539539
this._propagationContext = context;
540540
return this;
541541
}
542542

543+
/**
544+
* @inheritDoc
545+
*/
546+
public getPropagationContext(): PropagationContext {
547+
return this._propagationContext;
548+
}
549+
543550
/**
544551
* This will be called after {@link applyToEvent} is finished.
545552
*/
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Client, DynamicSamplingContext, Scope } from '@sentry/types';
2+
import { dropUndefinedKeys } from '@sentry/utils';
3+
4+
import { DEFAULT_ENVIRONMENT } from '../constants';
5+
6+
/**
7+
* Creates a dynamic sampling context from a client.
8+
*
9+
* Dispatchs the `createDsc` lifecycle hook as a side effect.
10+
*/
11+
export function getDynamicSamplingContextFromClient(
12+
trace_id: string,
13+
client: Client,
14+
scope?: Scope,
15+
): DynamicSamplingContext {
16+
const options = client.getOptions();
17+
18+
const { publicKey: public_key } = client.getDsn() || {};
19+
const { segment: user_segment } = (scope && scope.getUser()) || {};
20+
21+
const dsc = dropUndefinedKeys({
22+
environment: options.environment || DEFAULT_ENVIRONMENT,
23+
release: options.release,
24+
user_segment,
25+
public_key,
26+
trace_id,
27+
}) as DynamicSamplingContext;
28+
29+
client.emit && client.emit('createDsc', dsc);
30+
31+
return dsc;
32+
}

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
77
export { SpanStatus } from './spanstatus';
88
export type { SpanStatusType } from './span';
99
export { trace } from './trace';
10+
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';

packages/core/src/tracing/span.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
TraceContext,
88
Transaction,
99
} from '@sentry/types';
10-
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
10+
import { dropUndefinedKeys, generateSentryTraceHeader, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1111

1212
/**
1313
* Keeps track of finished spans for a given transaction
@@ -265,11 +265,7 @@ export class Span implements SpanInterface {
265265
* @inheritDoc
266266
*/
267267
public toTraceparent(): string {
268-
let sampledString = '';
269-
if (this.sampled !== undefined) {
270-
sampledString = this.sampled ? '-1' : '-0';
271-
}
272-
return `${this.traceId}-${this.spanId}${sampledString}`;
268+
return generateSentryTraceHeader(this.traceId, this.spanId, this.sampled);
273269
}
274270

275271
/**

packages/core/src/tracing/transaction.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import type {
1111
} from '@sentry/types';
1212
import { dropUndefinedKeys, logger } from '@sentry/utils';
1313

14-
import { DEFAULT_ENVIRONMENT } from '../constants';
1514
import type { Hub } from '../hub';
1615
import { getCurrentHub } from '../hub';
16+
import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
1717
import { Span as SpanClass, SpanRecorder } from './span';
1818

1919
/** JSDoc */
@@ -245,33 +245,24 @@ export class Transaction extends SpanClass implements TransactionInterface {
245245
return this._frozenDynamicSamplingContext;
246246
}
247247

248-
const hub: Hub = this._hub || getCurrentHub();
249-
const client = hub && hub.getClient();
248+
const hub = this._hub || getCurrentHub();
249+
const client = hub.getClient();
250250

251251
if (!client) return {};
252252

253-
const { environment, release } = client.getOptions() || {};
254-
const { publicKey: public_key } = client.getDsn() || {};
253+
const scope = hub.getScope();
254+
const dsc = getDynamicSamplingContextFromClient(this.traceId, client, scope);
255255

256256
const maybeSampleRate = this.metadata.sampleRate;
257-
const sample_rate = maybeSampleRate !== undefined ? maybeSampleRate.toString() : undefined;
258-
259-
const { segment: user_segment } = hub.getScope().getUser() || {};
260-
261-
const source = this.metadata.source;
257+
if (maybeSampleRate !== undefined) {
258+
dsc.sample_rate = `${maybeSampleRate}`;
259+
}
262260

263261
// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
264-
const transaction = source && source !== 'url' ? this.name : undefined;
265-
266-
const dsc = dropUndefinedKeys({
267-
environment: environment || DEFAULT_ENVIRONMENT,
268-
release,
269-
transaction,
270-
user_segment,
271-
public_key,
272-
trace_id: this.traceId,
273-
sample_rate,
274-
});
262+
const source = this.metadata.source;
263+
if (source && source !== 'url') {
264+
dsc.transaction = this.name;
265+
}
275266

276267
// Uncomment if we want to make DSC immutable
277268
// this._frozenDynamicSamplingContext = dsc;

packages/hub/test/scope.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ describe('Scope', () => {
152152
expect((scope as any)._sdkProcessingMetadata.dogs).toEqual('are great!');
153153
});
154154

155+
test('set and get propagation context', () => {
156+
const scope = new Scope();
157+
const oldPropagationContext = scope.getPropagationContext();
158+
scope.setPropagationContext({
159+
traceId: '86f39e84263a4de99c326acab3bfe3bd',
160+
spanId: '6e0c63257de34c92',
161+
sampled: true,
162+
});
163+
expect(scope.getPropagationContext()).not.toEqual(oldPropagationContext);
164+
expect(scope.getPropagationContext()).toEqual({
165+
traceId: '86f39e84263a4de99c326acab3bfe3bd',
166+
spanId: '6e0c63257de34c92',
167+
sampled: true,
168+
});
169+
});
170+
155171
test('chaining', () => {
156172
const scope = new Scope();
157173
scope.setLevel('fatal').setUser({ id: '1' });

packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ test('Should populate and propagate sentry baggage if sentry-trace header does n
7676
test_data: {
7777
host: 'somewhere.not.sentry',
7878
// TraceId changes, hence we only expect that the string contains the traceid key
79-
baggage: expect.stringContaining(
80-
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-public_key=public,sentry-trace_id=',
79+
baggage: expect.stringMatching(
80+
/sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/,
8181
),
8282
},
8383
});
@@ -95,8 +95,8 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header
9595
test_data: {
9696
host: 'somewhere.not.sentry',
9797
// TraceId changes, hence we only expect that the string contains the traceid key
98-
baggage: expect.stringContaining(
99-
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-public_key=public,sentry-trace_id=',
98+
baggage: expect.stringMatching(
99+
/sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/,
100100
),
101101
},
102102
});

packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ test('should attach a `baggage` header to an outgoing request.', async () => {
1313
test_data: {
1414
host: 'somewhere.not.sentry',
1515
baggage:
16-
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-user_segment=SegmentA' +
17-
',sentry-public_key=public,sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1',
16+
'sentry-environment=prod,sentry-release=1.0,sentry-user_segment=SegmentA,sentry-public_key=public' +
17+
',sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress',
1818
},
1919
});
2020
});

packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an
3535
baggage: [
3636
'other=vendor,foo=bar,third=party,sentry-release=9.9.9,sentry-environment=staging,sentry-sample_rate=0.54,last=item',
3737
expect.stringMatching(
38-
/sentry-environment=prod,sentry-release=1\.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-public_key=public,sentry-trace_id=[0-9a-f]{32},sentry-sample_rate=1/,
38+
/sentry-environment=prod,sentry-release=1\.0,sentry-public_key=public,sentry-trace_id=[0-9a-f]{32},sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/,
3939
),
4040
],
4141
},

packages/node/test/integrations/http.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ describe('tracing', () => {
114114
const baggageHeader = request.getHeader('baggage') as string;
115115

116116
expect(baggageHeader).toEqual(
117-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
117+
'sentry-environment=production,sentry-release=1.0.0,' +
118118
'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
119-
'sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1',
119+
'sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark',
120120
);
121121
});
122122

@@ -130,7 +130,7 @@ describe('tracing', () => {
130130

131131
expect(baggageHeader).toEqual([
132132
'dog=great',
133-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1',
133+
'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark',
134134
]);
135135
});
136136

@@ -144,7 +144,7 @@ describe('tracing', () => {
144144

145145
expect(baggageHeader).toEqual([
146146
'dog=great',
147-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1',
147+
'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark',
148148
]);
149149
});
150150

packages/node/test/integrations/undici.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ conditionalTest({ min: 16 })('Undici integration', () => {
204204

205205
expect(requestHeaders['sentry-trace']).toEqual(span?.toTraceparent());
206206
expect(requestHeaders['baggage']).toEqual(
207-
`sentry-environment=production,sentry-transaction=test-transaction,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1`,
207+
`sentry-environment=production,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`,
208208
);
209209
});
210210

packages/opentelemetry-node/test/propagator.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('SentryPropagator', () => {
8585
spanId: '6e0c63257de34c92',
8686
sampled: true,
8787
},
88-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
88+
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction',
8989
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
9090
],
9191
[
@@ -101,7 +101,7 @@ describe('SentryPropagator', () => {
101101
spanId: '6e0c63257de34c92',
102102
sampled: false,
103103
},
104-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=not-sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
104+
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=not-sampled-transaction',
105105
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0',
106106
],
107107
[
@@ -161,7 +161,7 @@ describe('SentryPropagator', () => {
161161
const baggage = propagation.createBaggage({ foo: { value: 'bar' } });
162162
propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter);
163163
expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(
164-
'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
164+
'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction',
165165
);
166166
});
167167

@@ -232,7 +232,7 @@ describe('SentryPropagator', () => {
232232

233233
it('sets defined dynamic sampling context on context', () => {
234234
const baggage =
235-
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b';
235+
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction';
236236
carrier[SENTRY_BAGGAGE_HEADER] = baggage;
237237
const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
238238
expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual({

packages/tracing/test/utils.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ describe('extractTraceparentData', () => {
7777
});
7878

7979
test('invalid', () => {
80+
// undefined
81+
expect(extractTraceparentData(undefined)).toBeUndefined();
82+
8083
// empty string
8184
expect(extractTraceparentData('')).toBeUndefined();
8285

packages/types/src/scope.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,9 @@ export interface Scope {
192192
* Add propagation context to the scope, used for distributed tracing
193193
*/
194194
setPropagationContext(context: PropagationContext): this;
195+
196+
/**
197+
* Get propagation context from the scope, used for distributed tracing
198+
*/
199+
getPropagationContext(): PropagationContext;
195200
}

packages/utils/src/baggage.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ export function baggageHeaderToDynamicSamplingContext(
8383
*/
8484
export function dynamicSamplingContextToSentryBaggageHeader(
8585
// this also takes undefined for convenience and bundle size in other places
86-
dynamicSamplingContext: Partial<DynamicSamplingContext>,
86+
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
8787
): string | undefined {
88+
if (!dynamicSamplingContext) {
89+
return undefined;
90+
}
91+
8892
// Prefix all DSC keys with "sentry-" and put them into a new object
8993
const sentryPrefixedDSC = Object.entries(dynamicSamplingContext).reduce<Record<string, string>>(
9094
(acc, [dscKey, dscValue]) => {

packages/utils/src/tracing.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export function extractTraceparentData(traceparent?: string): TraceparentData |
4646
* Create tracing context from incoming headers.
4747
*/
4848
export function tracingContextFromHeaders(
49-
sentryTrace: Parameters<typeof extractTraceparentData>[0] = '',
50-
baggage: Parameters<typeof baggageHeaderToDynamicSamplingContext>[0] = '',
49+
sentryTrace: Parameters<typeof extractTraceparentData>[0],
50+
baggage: Parameters<typeof baggageHeaderToDynamicSamplingContext>[0],
5151
): {
5252
traceparentData: ReturnType<typeof extractTraceparentData>;
5353
dynamicSamplingContext: ReturnType<typeof baggageHeaderToDynamicSamplingContext>;
@@ -78,3 +78,18 @@ export function tracingContextFromHeaders(
7878
propagationContext,
7979
};
8080
}
81+
82+
/**
83+
* Create sentry-trace header from span context values.
84+
*/
85+
export function generateSentryTraceHeader(
86+
traceId: string = uuid4(),
87+
spanId: string = uuid4().substring(16),
88+
sampled?: boolean,
89+
): string {
90+
let sampledString = '';
91+
if (sampled !== undefined) {
92+
sampledString = sampled ? '-1' : '-0';
93+
}
94+
return `${traceId}-${spanId}${sampledString}`;
95+
}

packages/utils/test/baggage.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ test.each([
2828
});
2929

3030
test.each([
31+
[undefined, undefined],
3132
[{}, undefined],
3233
[{ release: 'abcdf' }, 'sentry-release=abcdf'],
3334
[{ release: 'abcdf', environment: '1234' }, 'sentry-release=abcdf,sentry-environment=1234'],

0 commit comments

Comments
 (0)