Skip to content

Commit

Permalink
ref(tracing): Add necessary helpers for using propagation context on …
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
AbhiPrasad and mydea authored Jul 1, 2023
1 parent 3625fb1 commit 9006287
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 63 deletions.
18 changes: 2 additions & 16 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
addItemToEnvelope,
checkOrSetAlreadyCaught,
createAttachmentEnvelopeItem,
dropUndefinedKeys,
isPlainObject,
isPrimitive,
isThenable,
Expand All @@ -43,12 +42,12 @@ import {
} from '@sentry/utils';

import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
import { DEFAULT_ENVIRONMENT } from './constants';
import { createEventEnvelope, createSessionEnvelope } from './envelope';
import type { IntegrationIndex } from './integration';
import { setupIntegration, setupIntegrations } from './integration';
import type { Scope } from './scope';
import { updateSession } from './session';
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
import { prepareEvent } from './utils/prepareEvent';

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

const { publicKey: public_key } = this.getDsn() || {};
const { segment: user_segment } = (scope && scope.getUser()) || {};

let dynamicSamplingContext = dsc;
if (!dsc) {
dynamicSamplingContext = dropUndefinedKeys({
environment: options.environment || DEFAULT_ENVIRONMENT,
release: options.release,
user_segment,
public_key,
trace_id,
});
this.emit && this.emit('createDsc', dynamicSamplingContext);
}
const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope);

evt.sdkProcessingMetadata = {
dynamicSamplingContext,
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,13 +533,20 @@ export class Scope implements ScopeInterface {
}

/**
* @inheritdoc
* @inheritDoc
*/
public setPropagationContext(context: PropagationContext): this {
this._propagationContext = context;
return this;
}

/**
* @inheritDoc
*/
public getPropagationContext(): PropagationContext {
return this._propagationContext;
}

/**
* This will be called after {@link applyToEvent} is finished.
*/
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/tracing/dynamicSamplingContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Client, DynamicSamplingContext, Scope } from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';

import { DEFAULT_ENVIRONMENT } from '../constants';

/**
* Creates a dynamic sampling context from a client.
*
* Dispatchs the `createDsc` lifecycle hook as a side effect.
*/
export function getDynamicSamplingContextFromClient(
trace_id: string,
client: Client,
scope?: Scope,
): DynamicSamplingContext {
const options = client.getOptions();

const { publicKey: public_key } = client.getDsn() || {};
const { segment: user_segment } = (scope && scope.getUser()) || {};

const dsc = dropUndefinedKeys({
environment: options.environment || DEFAULT_ENVIRONMENT,
release: options.release,
user_segment,
public_key,
trace_id,
}) as DynamicSamplingContext;

client.emit && client.emit('createDsc', dsc);

return dsc;
}
1 change: 1 addition & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
export { SpanStatus } from './spanstatus';
export type { SpanStatusType } from './span';
export { trace } from './trace';
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
8 changes: 2 additions & 6 deletions packages/core/src/tracing/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
TraceContext,
Transaction,
} from '@sentry/types';
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
import { dropUndefinedKeys, generateSentryTraceHeader, logger, timestampInSeconds, uuid4 } from '@sentry/utils';

/**
* Keeps track of finished spans for a given transaction
Expand Down Expand Up @@ -265,11 +265,7 @@ export class Span implements SpanInterface {
* @inheritDoc
*/
public toTraceparent(): string {
let sampledString = '';
if (this.sampled !== undefined) {
sampledString = this.sampled ? '-1' : '-0';
}
return `${this.traceId}-${this.spanId}${sampledString}`;
return generateSentryTraceHeader(this.traceId, this.spanId, this.sampled);
}

/**
Expand Down
33 changes: 12 additions & 21 deletions packages/core/src/tracing/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import type {
} from '@sentry/types';
import { dropUndefinedKeys, logger } from '@sentry/utils';

import { DEFAULT_ENVIRONMENT } from '../constants';
import type { Hub } from '../hub';
import { getCurrentHub } from '../hub';
import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
import { Span as SpanClass, SpanRecorder } from './span';

/** JSDoc */
Expand Down Expand Up @@ -245,33 +245,24 @@ export class Transaction extends SpanClass implements TransactionInterface {
return this._frozenDynamicSamplingContext;
}

const hub: Hub = this._hub || getCurrentHub();
const client = hub && hub.getClient();
const hub = this._hub || getCurrentHub();
const client = hub.getClient();

if (!client) return {};

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

const maybeSampleRate = this.metadata.sampleRate;
const sample_rate = maybeSampleRate !== undefined ? maybeSampleRate.toString() : undefined;

const { segment: user_segment } = hub.getScope().getUser() || {};

const source = this.metadata.source;
if (maybeSampleRate !== undefined) {
dsc.sample_rate = `${maybeSampleRate}`;
}

// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
const transaction = source && source !== 'url' ? this.name : undefined;

const dsc = dropUndefinedKeys({
environment: environment || DEFAULT_ENVIRONMENT,
release,
transaction,
user_segment,
public_key,
trace_id: this.traceId,
sample_rate,
});
const source = this.metadata.source;
if (source && source !== 'url') {
dsc.transaction = this.name;
}

// Uncomment if we want to make DSC immutable
// this._frozenDynamicSamplingContext = dsc;
Expand Down
16 changes: 16 additions & 0 deletions packages/hub/test/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ describe('Scope', () => {
expect((scope as any)._sdkProcessingMetadata.dogs).toEqual('are great!');
});

test('set and get propagation context', () => {
const scope = new Scope();
const oldPropagationContext = scope.getPropagationContext();
scope.setPropagationContext({
traceId: '86f39e84263a4de99c326acab3bfe3bd',
spanId: '6e0c63257de34c92',
sampled: true,
});
expect(scope.getPropagationContext()).not.toEqual(oldPropagationContext);
expect(scope.getPropagationContext()).toEqual({
traceId: '86f39e84263a4de99c326acab3bfe3bd',
spanId: '6e0c63257de34c92',
sampled: true,
});
});

test('chaining', () => {
const scope = new Scope();
scope.setLevel('fatal').setUser({ id: '1' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ test('Should populate and propagate sentry baggage if sentry-trace header does n
test_data: {
host: 'somewhere.not.sentry',
// TraceId changes, hence we only expect that the string contains the traceid key
baggage: expect.stringContaining(
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-public_key=public,sentry-trace_id=',
baggage: expect.stringMatching(
/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/,
),
},
});
Expand All @@ -95,8 +95,8 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header
test_data: {
host: 'somewhere.not.sentry',
// TraceId changes, hence we only expect that the string contains the traceid key
baggage: expect.stringContaining(
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-public_key=public,sentry-trace_id=',
baggage: expect.stringMatching(
/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/,
),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ test('should attach a `baggage` header to an outgoing request.', async () => {
test_data: {
host: 'somewhere.not.sentry',
baggage:
'sentry-environment=prod,sentry-release=1.0,sentry-transaction=GET%20%2Ftest%2Fexpress,sentry-user_segment=SegmentA' +
',sentry-public_key=public,sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1',
'sentry-environment=prod,sentry-release=1.0,sentry-user_segment=SegmentA,sentry-public_key=public' +
',sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress',
},
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an
baggage: [
'other=vendor,foo=bar,third=party,sentry-release=9.9.9,sentry-environment=staging,sentry-sample_rate=0.54,last=item',
expect.stringMatching(
/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/,
/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/,
),
],
},
Expand Down
8 changes: 4 additions & 4 deletions packages/node/test/integrations/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ describe('tracing', () => {
const baggageHeader = request.getHeader('baggage') as string;

expect(baggageHeader).toEqual(
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
'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-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark',
);
});

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

expect(baggageHeader).toEqual([
'dog=great',
'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',
'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',
]);
});

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

expect(baggageHeader).toEqual([
'dog=great',
'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',
'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',
]);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/node/test/integrations/undici.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ conditionalTest({ min: 16 })('Undici integration', () => {

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

Expand Down
8 changes: 4 additions & 4 deletions packages/opentelemetry-node/test/propagator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('SentryPropagator', () => {
spanId: '6e0c63257de34c92',
sampled: true,
},
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction',
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
],
[
Expand All @@ -101,7 +101,7 @@ describe('SentryPropagator', () => {
spanId: '6e0c63257de34c92',
sampled: false,
},
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=not-sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=not-sampled-transaction',
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0',
],
[
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('SentryPropagator', () => {
const baggage = propagation.createBaggage({ foo: { value: 'bar' } });
propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter);
expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(
'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction',
);
});

Expand Down Expand Up @@ -232,7 +232,7 @@ describe('SentryPropagator', () => {

it('sets defined dynamic sampling context on context', () => {
const baggage =
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b';
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction';
carrier[SENTRY_BAGGAGE_HEADER] = baggage;
const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual({
Expand Down
3 changes: 3 additions & 0 deletions packages/tracing/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ describe('extractTraceparentData', () => {
});

test('invalid', () => {
// undefined
expect(extractTraceparentData(undefined)).toBeUndefined();

// empty string
expect(extractTraceparentData('')).toBeUndefined();

Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,9 @@ export interface Scope {
* Add propagation context to the scope, used for distributed tracing
*/
setPropagationContext(context: PropagationContext): this;

/**
* Get propagation context from the scope, used for distributed tracing
*/
getPropagationContext(): PropagationContext;
}
6 changes: 5 additions & 1 deletion packages/utils/src/baggage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ export function baggageHeaderToDynamicSamplingContext(
*/
export function dynamicSamplingContextToSentryBaggageHeader(
// this also takes undefined for convenience and bundle size in other places
dynamicSamplingContext: Partial<DynamicSamplingContext>,
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
): string | undefined {
if (!dynamicSamplingContext) {
return undefined;
}

// Prefix all DSC keys with "sentry-" and put them into a new object
const sentryPrefixedDSC = Object.entries(dynamicSamplingContext).reduce<Record<string, string>>(
(acc, [dscKey, dscValue]) => {
Expand Down
19 changes: 17 additions & 2 deletions packages/utils/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export function extractTraceparentData(traceparent?: string): TraceparentData |
* Create tracing context from incoming headers.
*/
export function tracingContextFromHeaders(
sentryTrace: Parameters<typeof extractTraceparentData>[0] = '',
baggage: Parameters<typeof baggageHeaderToDynamicSamplingContext>[0] = '',
sentryTrace: Parameters<typeof extractTraceparentData>[0],
baggage: Parameters<typeof baggageHeaderToDynamicSamplingContext>[0],
): {
traceparentData: ReturnType<typeof extractTraceparentData>;
dynamicSamplingContext: ReturnType<typeof baggageHeaderToDynamicSamplingContext>;
Expand Down Expand Up @@ -78,3 +78,18 @@ export function tracingContextFromHeaders(
propagationContext,
};
}

/**
* Create sentry-trace header from span context values.
*/
export function generateSentryTraceHeader(
traceId: string = uuid4(),
spanId: string = uuid4().substring(16),
sampled?: boolean,
): string {
let sampledString = '';
if (sampled !== undefined) {
sampledString = sampled ? '-1' : '-0';
}
return `${traceId}-${spanId}${sampledString}`;
}
1 change: 1 addition & 0 deletions packages/utils/test/baggage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ test.each([
});

test.each([
[undefined, undefined],
[{}, undefined],
[{ release: 'abcdf' }, 'sentry-release=abcdf'],
[{ release: 'abcdf', environment: '1234' }, 'sentry-release=abcdf,sentry-environment=1234'],
Expand Down

0 comments on commit 9006287

Please sign in to comment.