diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 40a1fa135417..c7b506acb41e 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -11,6 +11,7 @@ import type { Extra, Extras, Primitive, + PropagationContext, RequestSession, Scope as ScopeInterface, ScopeContext, @@ -29,6 +30,7 @@ import { isThenable, logger, SyncPromise, + uuid4, } from '@sentry/utils'; import { updateSession } from './session'; @@ -70,6 +72,9 @@ export class Scope implements ScopeInterface { /** Attachments */ protected _attachments: Attachment[]; + /** Propagation Context */ + protected _propagationContext: PropagationContext; + /** * A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get * sent to Sentry @@ -108,6 +113,11 @@ export class Scope implements ScopeInterface { this._extra = {}; this._contexts = {}; this._sdkProcessingMetadata = {}; + this._propagationContext = { + traceId: uuid4(), + spanId: uuid4().substring(16), + sampled: false, + }; } /** @@ -131,6 +141,9 @@ export class Scope implements ScopeInterface { newScope._requestSession = scope._requestSession; newScope._attachments = [...scope._attachments]; newScope._sdkProcessingMetadata = { ...scope._sdkProcessingMetadata }; + if (scope._propagationContext) { + newScope._propagationContext = { ...scope._propagationContext }; + } } return newScope; } @@ -493,6 +506,16 @@ export class Scope implements ScopeInterface { event.tags = { transaction: transactionName, ...event.tags }; } } + } else { + const { traceId, spanId, parentSpanId, dsc } = this._propagationContext; + event.contexts = { + trace: { trace_id: traceId, span_id: spanId, parent_span_id: parentSpanId }, + ...event.contexts, + }; + if (dsc) { + event.sdkProcessingMetadata = { dynamicSamplingContext: dsc, ...event.sdkProcessingMetadata }; + } else { + } } this._applyFingerprint(event); @@ -514,6 +537,21 @@ export class Scope implements ScopeInterface { return this; } + /** + * @inheritdoc + */ + public setPropagationContext(ctx: PropagationContext): this { + this._propagationContext = { ...ctx }; + return this; + } + + /** + * @inheritdoc + **/ + public getPropagationContext(): PropagationContext { + return this._propagationContext; + } + /** * This will be called after {@link applyToEvent} is finished. */ diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index d62f4c3b2833..103dfbb9eb9b 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,6 +15,7 @@ import { DEFAULT_ENVIRONMENT } from '../constants'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { Span as SpanClass, SpanRecorder } from './span'; +import { dynamicSamplingContextFromHub } from '../utils/dynamicSampling'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { @@ -23,7 +24,7 @@ export class Transaction extends SpanClass implements TransactionInterface { /** * The reference to the current hub. */ - public _hub: Hub; + public hub: Hub; private _name: string; @@ -45,7 +46,7 @@ export class Transaction extends SpanClass implements TransactionInterface { public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); - this._hub = hub || getCurrentHub(); + this.hub = hub || getCurrentHub(); this._name = transactionContext.name || ''; @@ -141,7 +142,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // just sets the end timestamp super.finish(endTimestamp); - const client = this._hub.getClient(); + const client = this.hub.getClient(); if (client && client.emit) { client.emit('finishTransaction', this); } @@ -206,7 +207,7 @@ export class Transaction extends SpanClass implements TransactionInterface { __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); - return this._hub.captureEvent(transaction); + return this.hub.captureEvent(transaction); } /** @@ -245,31 +246,19 @@ export class Transaction extends SpanClass implements TransactionInterface { return this._frozenDynamicSamplingContext; } - const hub: Hub = this._hub || getCurrentHub(); - const client = hub && hub.getClient(); - - if (!client) return {}; - - const { environment, release } = client.getOptions() || {}; - const { publicKey: public_key } = client.getDsn() || {}; + const hub = this.hub || getCurrentHub(); + const client = hub.getClient(); 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; - // 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, + ...dynamicSamplingContextFromHub(this.traceId, hub), transaction, - user_segment, - public_key, - trace_id: this.traceId, sample_rate, }); @@ -288,6 +277,6 @@ export class Transaction extends SpanClass implements TransactionInterface { * @internal */ public setHub(hub: Hub): void { - this._hub = hub; + this.hub = hub; } } diff --git a/packages/core/src/utils/dynamicSampling.ts b/packages/core/src/utils/dynamicSampling.ts new file mode 100644 index 000000000000..dda41ee7dff8 --- /dev/null +++ b/packages/core/src/utils/dynamicSampling.ts @@ -0,0 +1,35 @@ +import type { DynamicSamplingContext, Hub } from '@sentry/types'; +import { dropUndefinedKeys } from '@sentry/utils'; + +import { DEFAULT_ENVIRONMENT } from '../constants'; +import { getCurrentHub } from '../hub'; + +/** + * Create a dynamic sampling context from a hub. + */ +export function dynamicSamplingContextFromHub( + trace_id: string, + hub: Hub = getCurrentHub(), +): Partial { + const client = hub.getClient(); + if (!client) { + return {}; + } + + const { environment = DEFAULT_ENVIRONMENT, release } = client.getOptions() || {}; + const { publicKey: public_key } = client.getDsn() || {}; + + const { segment: user_segment } = hub.getScope().getUser() || {}; + + const dsc = dropUndefinedKeys({ + environment, + release, + user_segment, + public_key, + trace_id, + }); + + client.emit && client.emit('createDsc', dsc); + + return dsc; +} diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 6571cd3122b4..8cea62413ab6 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -137,6 +137,23 @@ describe('Scope', () => { expect((scope as any)._sdkProcessingMetadata.dogs).toEqual('are great!'); }); + test('setPropagationContext', () => { + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12312012123120121231201212312012', + spanId: '1121201211212012', + sampled: false, + dsc: { trace_id: '12312012123120121231201212312012', public_key: 'public' }, + }); + + expect((scope as any)._propagationContext).toEqual({ + traceId: '12312012123120121231201212312012', + spanId: '1121201211212012', + sampled: false, + dsc: { trace_id: '12312012123120121231201212312012', public_key: 'public' }, + }); + }); + test('chaining', () => { const scope = new Scope(); scope.setLevel('fatal').setUser({ id: '1' }); @@ -315,7 +332,7 @@ describe('Scope', () => { }); }); - test('adds trace context', async () => { + test('adds trace context based on span on scope', async () => { expect.assertions(1); const scope = new Scope(); const span = { @@ -324,8 +341,20 @@ describe('Scope', () => { } as any; scope.setSpan(span); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); + const processedEvent = await scope.applyToEvent(event); + expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); + }); + + test('adds trace context based on propagation context on scope', async () => { + expect.assertions(1); + const scope = new Scope(); + const event: Event = {}; + const processedEvent = await scope.applyToEvent(event); + + const ctx = scope.getPropagationContext(); + expect((processedEvent!.contexts!.trace as any).a).toEqual({ + trace_id: ctx.traceId, + span_id: ctx.spanId, }); }); diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 35a3f9f4a82e..b7649cede039 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -70,6 +70,9 @@ export interface Hub { /** Returns the client of the top stack. */ getClient(): Client | undefined; + /** Returns the scope of the top stack */ + getScope(): Scope; + /** * Captures an exception event and sends it to Sentry. * diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ccabae59a995..b40bb9c1c1f1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -83,7 +83,7 @@ export type { Span, SpanContext } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; -export type { TracePropagationTargets } from './tracing'; +export type { TracePropagationTargets, PropagationContext } from './tracing'; export type { CustomSamplingContext, SamplingContext, diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 4ed11b287421..3e732d5e2f65 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -7,6 +7,7 @@ import type { Primitive } from './misc'; import type { RequestSession, Session } from './session'; import type { Severity, SeverityLevel } from './severity'; import type { Span } from './span'; +import type { PropagationContext } from './tracing'; import type { Transaction } from './transaction'; import type { User } from './user'; @@ -185,4 +186,19 @@ export interface Scope { * Add data which will be accessible during event processing but won't get sent to Sentry */ setSDKProcessingMetadata(newData: { [key: string]: unknown }): this; + + /** + * Set current propagation context for the current scope. + * + * Setting a new propagation context will overwrite all previously + * set values. + */ + setPropagationContext(context: PropagationContext): this; + + /** + * Get current propagation context for the current scope. + * + * There will always be a propagation context for the current scope. + */ + getPropagationContext(): PropagationContext; } diff --git a/packages/types/src/tracing.ts b/packages/types/src/tracing.ts index d11db382e2ed..4845a049a0f5 100644 --- a/packages/types/src/tracing.ts +++ b/packages/types/src/tracing.ts @@ -1 +1,16 @@ +import type { DynamicSamplingContext } from './envelope'; + export type TracePropagationTargets = (string | RegExp)[]; + +/** + * Context that contains details about distributed trace + * + * Generated from incoming `sentry-trace` and `baggage` headers. + */ +export interface PropagationContext { + traceId: string; + spanId: string; + parentSpanId?: string; + sampled: boolean; + dsc?: DynamicSamplingContext; +}