diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 9a1312a131ac..01253b203f74 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -14,12 +14,15 @@ import type { EventHint, Integration, IntegrationClass, + Options, Outcome, + PropagationContext, SdkMetadata, Session, SessionAggregates, Severity, SeverityLevel, + TraceContext, Transaction, TransactionEvent, Transport, @@ -585,6 +588,8 @@ export abstract class BaseClient implements Client { throw new SentryError('An event processor returned `null`, will not send event.', 'log'); } + addPropagationContextToEvent(prepared, options, this.getDsn()); + const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true; if (isInternalException) { return prepared; @@ -755,3 +760,36 @@ function isErrorEvent(event: Event): event is ErrorEvent { function isTransactionEvent(event: Event): event is TransactionEvent { return event.type === 'transaction'; } + +function addPropagationContextToEvent(event: Event, options: Options, dsn: DsnComponents | undefined): void { + const { propagationContext } = event.sdkProcessingMetadata || {}; + if (!propagationContext) { + return; + } + + const { + traceId: trace_id, + spanId: span_id, + parentSpanId: parent_span_id, + dsc, + } = propagationContext as PropagationContext; + + const dynamicSamplingContext = dsc ? dsc : { trace_id }; + + const trace = event.contexts && event.contexts.trace; + if (!trace) { + event.contexts = { + trace: { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...event.contexts, + }; + + event.sdkProcessingMetadata = { + ...event.sdkProcessingMetadata, + dynamicSamplingContext: dsc as DynamicSamplingContext, + }; + } +} diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 40a1fa135417..5efb7e91bbd8 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 for distributed tracing */ + 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,7 @@ export class Scope implements ScopeInterface { newScope._requestSession = scope._requestSession; newScope._attachments = [...scope._attachments]; newScope._sdkProcessingMetadata = { ...scope._sdkProcessingMetadata }; + newScope._propagationContext = { ...scope._propagationContext }; } return newScope; } @@ -347,6 +358,9 @@ export class Scope implements ScopeInterface { if (captureContext._requestSession) { this._requestSession = captureContext._requestSession; } + if (captureContext._propagationContext) { + this._propagationContext = captureContext._propagationContext; + } } else if (isPlainObject(captureContext)) { // eslint-disable-next-line no-param-reassign captureContext = captureContext as ScopeContext; @@ -365,6 +379,9 @@ export class Scope implements ScopeInterface { if (captureContext.requestSession) { this._requestSession = captureContext.requestSession; } + if (captureContext.propagationContext) { + this._propagationContext = captureContext.propagationContext; + } } return this; @@ -480,9 +497,10 @@ export class Scope implements ScopeInterface { // We want to set the trace context for normal events only if there isn't already // a trace context on the event. There is a product feature in place where we link // errors with transaction and it relies on that. - if (this._span) { - event.contexts = { trace: this._span.getTraceContext(), ...event.contexts }; - const transaction = this._span.transaction; + const span = this._span; + if (span) { + event.contexts = { trace: span.getTraceContext(), ...event.contexts }; + const transaction = span.transaction; if (transaction) { event.sdkProcessingMetadata = { dynamicSamplingContext: transaction.getDynamicSamplingContext(), @@ -500,7 +518,11 @@ export class Scope implements ScopeInterface { event.breadcrumbs = [...(event.breadcrumbs || []), ...this._breadcrumbs]; event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined; - event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, ...this._sdkProcessingMetadata }; + event.sdkProcessingMetadata = { + ...event.sdkProcessingMetadata, + ...this._sdkProcessingMetadata, + propagationContext: this._propagationContext, + }; return this._notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index d62f4c3b2833..9b8c2a146152 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -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 { getDynamicSamplingContextFromHub } from '../utils/dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; /** JSDoc */ @@ -245,38 +245,31 @@ 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 partialDsc = getDynamicSamplingContextFromHub(hub); 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, + ...partialDsc, transaction, - user_segment, - public_key, trace_id: this.traceId, sample_rate, - }); + }) as DynamicSamplingContext; // Uncomment if we want to make DSC immutable // this._frozenDynamicSamplingContext = dsc; - client.emit && client.emit('createDsc', dsc); + const client = hub.getClient(); + if (client && client.emit) { + client.emit('createDsc', dsc); + } return dsc; } diff --git a/packages/core/src/utils/dynamicSamplingContext.ts b/packages/core/src/utils/dynamicSamplingContext.ts new file mode 100644 index 000000000000..84a19f0e1274 --- /dev/null +++ b/packages/core/src/utils/dynamicSamplingContext.ts @@ -0,0 +1,26 @@ +import type { DynamicSamplingContext } from '@sentry/types'; + +import { DEFAULT_ENVIRONMENT } from '../constants'; +import { getCurrentHub } from '../hub'; + +type PartialDsc = Partial>; + +/** */ +export function getDynamicSamplingContextFromHub(hub = getCurrentHub()): PartialDsc { + 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() || {}; + + return { + environment, + release, + user_segment, + public_key, + }; +} diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 6571cd3122b4..d5d96ae68128 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -12,6 +12,21 @@ describe('Scope', () => { GLOBAL_OBJ.__SENTRY__.globalEventProcessors = undefined; }); + describe('init', () => { + test('it creates a propagation context', () => { + const scope = new Scope(); + + // @ts-ignore asserting on private properties + expect(scope._propagationContext).toEqual({ + traceId: expect.any(String), + spanId: expect.any(String), + sampled: false, + dsc: undefined, + parentSpanId: undefined, + }); + }); + }); + describe('attributes modification', () => { test('setFingerprint', () => { const scope = new Scope(); @@ -193,6 +208,14 @@ describe('Scope', () => { expect(parentScope.getRequestSession()).toEqual({ status: 'ok' }); expect(scope.getRequestSession()).toEqual({ status: 'ok' }); }); + + test('should clone propagation context', () => { + const parentScope = new Scope(); + const scope = Scope.clone(parentScope); + + // @ts-ignore accessing private property for test + expect(scope._propagationContext).toEqual(parentScope._propagationContext); + }); }); describe('applyToEvent', () => { @@ -393,6 +416,12 @@ describe('Scope', () => { scope.clear(); expect((scope as any)._extra).toEqual({}); expect((scope as any)._requestSession).toEqual(undefined); + // @ts-ignore acessing private property for test + expect(scope._propagationContext).toEqual({ + traceId: expect.any(String), + spanId: expect.any(String), + sampled: false, + }); }); test('clearBreadcrumbs', () => { @@ -486,6 +515,8 @@ describe('Scope', () => { expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); expect(updatedScope._requestSession.status).toEqual('ok'); + // @ts-ignore accessing private property for test + expect(updatedScope._propagationContext).toEqual(localScope._propagationContext); }); test('given an empty instance of Scope, it should preserve all the original scope data', () => { @@ -518,7 +549,13 @@ describe('Scope', () => { tags: { bar: '3', baz: '4' }, user: { id: '42' }, requestSession: { status: 'errored' as RequestSessionStatus }, + propagationContext: { + traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', + spanId: 'a024ad8fea82680e', + sampled: true, + }, }; + const updatedScope = scope.update(localAttributes) as any; expect(updatedScope._tags).toEqual({ @@ -540,6 +577,11 @@ describe('Scope', () => { expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); expect(updatedScope._requestSession).toEqual({ status: 'errored' }); + expect(updatedScope._propagationContext).toEqual({ + traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', + spanId: 'a024ad8fea82680e', + sampled: true, + }); }); }); diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index bab969899cea..052d6f4a6523 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -8,6 +8,7 @@ export interface Contexts extends Record { os?: OsContext; culture?: CultureContext; response?: ResponseContext; + trace?: TraceContext; } export interface AppContext extends Record { 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..e0244319b8f1 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'; @@ -23,6 +24,7 @@ export interface ScopeContext { tags: { [key: string]: Primitive }; fingerprint: string[]; requestSession: RequestSession; + propagationContext: PropagationContext; } /** diff --git a/packages/types/src/tracing.ts b/packages/types/src/tracing.ts index d11db382e2ed..11c4a1658d50 100644 --- a/packages/types/src/tracing.ts +++ b/packages/types/src/tracing.ts @@ -1 +1,11 @@ +import type { DynamicSamplingContext } from './envelope'; + export type TracePropagationTargets = (string | RegExp)[]; + +export interface PropagationContext { + traceId: string; + spanId: string; + sampled: boolean; + parentSpanId?: string; + dsc?: DynamicSamplingContext; +}