Skip to content

Commit f1ede57

Browse files
authored
feat(tracing): Add PropagationContext to scope (#8421)
ref #8352 For more details about PropagationContext, see https://www.notion.so/sentry/Tracing-without-performance-efab307eb7f64e71a04f09dc72722530 Building off of work in both #8403 and #8418, this PR adds `PropagationContext` and uses that to always set a trace context on outgoing error events. Currently if there is an active span on the scope, we automatically attach that span's trace context to all outgoing events. Now, we want to rely on either the active span or fallback to the propagation context to ensure that there is always a trace being generated and propagated. Next up we'll work on updating the node/browser SDKs to update the propagation context. For example, we should update the propagation context for node based on the incoming sentry-trace/baggage headers.
1 parent 7de917e commit f1ede57

File tree

16 files changed

+188
-26
lines changed

16 files changed

+188
-26
lines changed

packages/browser-integration-tests/suites/replay/captureReplay/test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT
5656
version: SDK_VERSION,
5757
name: 'sentry.javascript.browser',
5858
},
59-
sdkProcessingMetadata: {},
6059
request: {
6160
url: expect.stringContaining('/dist/index.html'),
6261
headers: {
@@ -94,7 +93,6 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT
9493
version: SDK_VERSION,
9594
name: 'sentry.javascript.browser',
9695
},
97-
sdkProcessingMetadata: {},
9896
request: {
9997
url: expect.stringContaining('/dist/index.html'),
10098
headers: {

packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe
5656
version: SDK_VERSION,
5757
name: 'sentry.javascript.browser',
5858
},
59-
sdkProcessingMetadata: {},
6059
request: {
6160
url: expect.stringContaining('/dist/index.html'),
6261
headers: {
@@ -94,7 +93,6 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe
9493
version: SDK_VERSION,
9594
name: 'sentry.javascript.browser',
9695
},
97-
sdkProcessingMetadata: {},
9896
request: {
9997
url: expect.stringContaining('/dist/index.html'),
10098
headers: {

packages/browser-integration-tests/utils/replayEventTemplates.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const DEFAULT_REPLAY_EVENT = {
3030
version: SDK_VERSION,
3131
name: 'sentry.javascript.browser',
3232
},
33-
sdkProcessingMetadata: {},
3433
request: {
3534
url: expect.stringContaining('/dist/index.html'),
3635
headers: {

packages/core/src/baseclient.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
Integration,
1616
IntegrationClass,
1717
Outcome,
18+
PropagationContext,
1819
SdkMetadata,
1920
Session,
2021
SessionAggregates,
@@ -29,6 +30,7 @@ import {
2930
addItemToEnvelope,
3031
checkOrSetAlreadyCaught,
3132
createAttachmentEnvelopeItem,
33+
dropUndefinedKeys,
3234
isPlainObject,
3335
isPrimitive,
3436
isThenable,
@@ -41,6 +43,7 @@ import {
4143
} from '@sentry/utils';
4244

4345
import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
46+
import { DEFAULT_ENVIRONMENT } from './constants';
4447
import { createEventEnvelope, createSessionEnvelope } from './envelope';
4548
import type { IntegrationIndex } from './integration';
4649
import { setupIntegration, setupIntegrations } from './integration';
@@ -507,7 +510,49 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
507510
if (!hint.integrations && integrations.length > 0) {
508511
hint.integrations = integrations;
509512
}
510-
return prepareEvent(options, event, hint, scope);
513+
return prepareEvent(options, event, hint, scope).then(evt => {
514+
if (evt === null) {
515+
return evt;
516+
}
517+
518+
// If a trace context is not set on the event, we use the propagationContext set on the event to
519+
// generate a trace context. If the propagationContext does not have a dynamic sampling context, we
520+
// also generate one for it.
521+
const { propagationContext } = evt.sdkProcessingMetadata || {};
522+
const trace = evt.contexts && evt.contexts.trace;
523+
if (!trace && propagationContext) {
524+
const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext;
525+
evt.contexts = {
526+
trace: {
527+
trace_id,
528+
span_id: spanId,
529+
parent_span_id: parentSpanId,
530+
},
531+
...evt.contexts,
532+
};
533+
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+
}
548+
549+
evt.sdkProcessingMetadata = {
550+
dynamicSamplingContext,
551+
...evt.sdkProcessingMetadata,
552+
};
553+
}
554+
return evt;
555+
});
511556
}
512557

513558
/**

packages/core/src/scope.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
Extra,
1212
Extras,
1313
Primitive,
14+
PropagationContext,
1415
RequestSession,
1516
Scope as ScopeInterface,
1617
ScopeContext,
@@ -29,6 +30,7 @@ import {
2930
isThenable,
3031
logger,
3132
SyncPromise,
33+
uuid4,
3234
} from '@sentry/utils';
3335

3436
import { updateSession } from './session';
@@ -70,6 +72,9 @@ export class Scope implements ScopeInterface {
7072
/** Attachments */
7173
protected _attachments: Attachment[];
7274

75+
/** Propagation Context for distributed tracing */
76+
protected _propagationContext: PropagationContext;
77+
7378
/**
7479
* A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get
7580
* sent to Sentry
@@ -108,6 +113,7 @@ export class Scope implements ScopeInterface {
108113
this._extra = {};
109114
this._contexts = {};
110115
this._sdkProcessingMetadata = {};
116+
this._propagationContext = generatePropagationContext();
111117
}
112118

113119
/**
@@ -131,6 +137,7 @@ export class Scope implements ScopeInterface {
131137
newScope._requestSession = scope._requestSession;
132138
newScope._attachments = [...scope._attachments];
133139
newScope._sdkProcessingMetadata = { ...scope._sdkProcessingMetadata };
140+
newScope._propagationContext = { ...scope._propagationContext };
134141
}
135142
return newScope;
136143
}
@@ -347,6 +354,9 @@ export class Scope implements ScopeInterface {
347354
if (captureContext._requestSession) {
348355
this._requestSession = captureContext._requestSession;
349356
}
357+
if (captureContext._propagationContext) {
358+
this._propagationContext = captureContext._propagationContext;
359+
}
350360
} else if (isPlainObject(captureContext)) {
351361
// eslint-disable-next-line no-param-reassign
352362
captureContext = captureContext as ScopeContext;
@@ -365,6 +375,9 @@ export class Scope implements ScopeInterface {
365375
if (captureContext.requestSession) {
366376
this._requestSession = captureContext.requestSession;
367377
}
378+
if (captureContext.propagationContext) {
379+
this._propagationContext = captureContext.propagationContext;
380+
}
368381
}
369382

370383
return this;
@@ -387,6 +400,7 @@ export class Scope implements ScopeInterface {
387400
this._session = undefined;
388401
this._notifyScopeListeners();
389402
this._attachments = [];
403+
this._propagationContext = generatePropagationContext();
390404
return this;
391405
}
392406

@@ -500,7 +514,11 @@ export class Scope implements ScopeInterface {
500514
event.breadcrumbs = [...(event.breadcrumbs || []), ...this._breadcrumbs];
501515
event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined;
502516

503-
event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, ...this._sdkProcessingMetadata };
517+
event.sdkProcessingMetadata = {
518+
...event.sdkProcessingMetadata,
519+
...this._sdkProcessingMetadata,
520+
propagationContext: this._propagationContext,
521+
};
504522

505523
return this._notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint);
506524
}
@@ -514,6 +532,14 @@ export class Scope implements ScopeInterface {
514532
return this;
515533
}
516534

535+
/**
536+
* @inheritdoc
537+
*/
538+
public setPropagationContext(context: PropagationContext): this {
539+
this._propagationContext = context;
540+
return this;
541+
}
542+
517543
/**
518544
* This will be called after {@link applyToEvent} is finished.
519545
*/
@@ -598,3 +624,11 @@ function getGlobalEventProcessors(): EventProcessor[] {
598624
export function addGlobalEventProcessor(callback: EventProcessor): void {
599625
getGlobalEventProcessors().push(callback);
600626
}
627+
628+
function generatePropagationContext(): PropagationContext {
629+
return {
630+
traceId: uuid4(),
631+
spanId: uuid4().substring(16),
632+
sampled: false,
633+
};
634+
}

packages/core/test/lib/base.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,28 @@ describe('BaseClient', () => {
492492
);
493493
});
494494

495+
test('it adds a trace context all events', () => {
496+
expect.assertions(1);
497+
498+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
499+
const client = new TestClient(options);
500+
const scope = new Scope();
501+
502+
client.captureEvent({ message: 'message' }, { event_id: 'wat' }, scope);
503+
504+
expect(TestClient.instance!.event!).toEqual(
505+
expect.objectContaining({
506+
contexts: {
507+
trace: {
508+
parent_span_id: undefined,
509+
span_id: expect.any(String),
510+
trace_id: expect.any(String),
511+
},
512+
},
513+
}),
514+
);
515+
});
516+
495517
test('adds `event_id` from hint if available', () => {
496518
expect.assertions(1);
497519

packages/hub/test/scope.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ describe('Scope', () => {
1212
GLOBAL_OBJ.__SENTRY__.globalEventProcessors = undefined;
1313
});
1414

15+
describe('init', () => {
16+
test('it creates a propagation context', () => {
17+
const scope = new Scope();
18+
19+
// @ts-ignore asserting on private properties
20+
expect(scope._propagationContext).toEqual({
21+
traceId: expect.any(String),
22+
spanId: expect.any(String),
23+
sampled: false,
24+
dsc: undefined,
25+
parentSpanId: undefined,
26+
});
27+
});
28+
});
29+
1530
describe('attributes modification', () => {
1631
test('setFingerprint', () => {
1732
const scope = new Scope();
@@ -193,6 +208,14 @@ describe('Scope', () => {
193208
expect(parentScope.getRequestSession()).toEqual({ status: 'ok' });
194209
expect(scope.getRequestSession()).toEqual({ status: 'ok' });
195210
});
211+
212+
test('should clone propagation context', () => {
213+
const parentScope = new Scope();
214+
const scope = Scope.clone(parentScope);
215+
216+
// @ts-ignore accessing private property for test
217+
expect(scope._propagationContext).toEqual(parentScope._propagationContext);
218+
});
196219
});
197220

198221
describe('applyToEvent', () => {
@@ -220,7 +243,11 @@ describe('Scope', () => {
220243
expect(processedEvent!.transaction).toEqual('/abc');
221244
expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test');
222245
expect(processedEvent!.contexts).toEqual({ os: { id: '1' } });
223-
expect(processedEvent!.sdkProcessingMetadata).toEqual({ dogs: 'are great!' });
246+
expect(processedEvent!.sdkProcessingMetadata).toEqual({
247+
dogs: 'are great!',
248+
// @ts-expect-error accessing private property for test
249+
propagationContext: scope._propagationContext,
250+
});
224251
});
225252
});
226253

@@ -339,7 +366,7 @@ describe('Scope', () => {
339366
scope.setSpan(span);
340367
const event: Event = {
341368
contexts: {
342-
trace: { a: 'c' },
369+
trace: { a: 'c' } as any,
343370
},
344371
};
345372
return scope.applyToEvent(event).then(processedEvent => {
@@ -383,6 +410,8 @@ describe('Scope', () => {
383410

384411
test('clear', () => {
385412
const scope = new Scope();
413+
// @ts-expect-error accessing private property
414+
const oldPropagationContext = scope._propagationContext;
386415
scope.setExtra('a', 2);
387416
scope.setTag('a', 'b');
388417
scope.setUser({ id: '1' });
@@ -393,6 +422,14 @@ describe('Scope', () => {
393422
scope.clear();
394423
expect((scope as any)._extra).toEqual({});
395424
expect((scope as any)._requestSession).toEqual(undefined);
425+
// @ts-expect-error accessing private property
426+
expect(scope._propagationContext).toEqual({
427+
traceId: expect.any(String),
428+
spanId: expect.any(String),
429+
sampled: false,
430+
});
431+
// @ts-expect-error accessing private property
432+
expect(scope._propagationContext).not.toEqual(oldPropagationContext);
396433
});
397434

398435
test('clearBreadcrumbs', () => {
@@ -486,6 +523,8 @@ describe('Scope', () => {
486523
expect(updatedScope._level).toEqual('warning');
487524
expect(updatedScope._fingerprint).toEqual(['bar']);
488525
expect(updatedScope._requestSession.status).toEqual('ok');
526+
// @ts-ignore accessing private property for test
527+
expect(updatedScope._propagationContext).toEqual(localScope._propagationContext);
489528
});
490529

491530
test('given an empty instance of Scope, it should preserve all the original scope data', () => {
@@ -518,7 +557,13 @@ describe('Scope', () => {
518557
tags: { bar: '3', baz: '4' },
519558
user: { id: '42' },
520559
requestSession: { status: 'errored' as RequestSessionStatus },
560+
propagationContext: {
561+
traceId: '8949daf83f4a4a70bee4c1eb9ab242ed',
562+
spanId: 'a024ad8fea82680e',
563+
sampled: true,
564+
},
521565
};
566+
522567
const updatedScope = scope.update(localAttributes) as any;
523568

524569
expect(updatedScope._tags).toEqual({
@@ -540,6 +585,11 @@ describe('Scope', () => {
540585
expect(updatedScope._level).toEqual('warning');
541586
expect(updatedScope._fingerprint).toEqual(['bar']);
542587
expect(updatedScope._requestSession).toEqual({ status: 'errored' });
588+
expect(updatedScope._propagationContext).toEqual({
589+
traceId: '8949daf83f4a4a70bee4c1eb9ab242ed',
590+
spanId: 'a024ad8fea82680e',
591+
sampled: true,
592+
});
543593
});
544594
});
545595

packages/node/test/async/domain.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getCurrentHub, Hub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core';
2-
import * as domain from 'domain';
1+
import type { Hub } from '@sentry/core';
2+
import { getCurrentHub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core';
33

44
import { setDomainAsyncContextStrategy } from '../../src/async/domain';
55

@@ -9,13 +9,6 @@ describe('domains', () => {
99
setAsyncContextStrategy(undefined);
1010
});
1111

12-
test('without domain', () => {
13-
// @ts-ignore property active does not exist on domain
14-
expect(domain.active).toBeFalsy();
15-
const hub = getCurrentHub();
16-
expect(hub).toEqual(new Hub());
17-
});
18-
1912
test('hub scope inheritance', () => {
2013
setDomainAsyncContextStrategy();
2114

0 commit comments

Comments
 (0)