Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(tracing): Add necessary helpers for using propagation context on outgoing headers #8434

Merged
merged 9 commits into from
Jul 1, 2023
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,
});

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.toString();
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
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
29 changes: 23 additions & 6 deletions packages/utils/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export const TRACEPARENT_REGEXP = new RegExp(
*
* @returns Object containing data from the header, or undefined if traceparent string is malformed
*/
export function extractTraceparentData(traceparent: string): TraceparentData | undefined {
const matches = traceparent.match(TRACEPARENT_REGEXP);
export function extractTraceparentData(traceparent?: string): TraceparentData | undefined {
if (!traceparent) {
return undefined;
}

if (!traceparent || !matches) {
// empty string or no matches is invalid traceparent data
const matches = traceparent.match(TRACEPARENT_REGEXP);
if (!matches) {
return undefined;
}

Expand All @@ -44,8 +46,8 @@ export function extractTraceparentData(traceparent: string): TraceparentData | u
* 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 @@ -76,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