Skip to content

Commit e39c754

Browse files
authored
fix(tracing): Clean up sampling decision inheritance (#2921)
Fixes a number of problems with propagation of sampling decisions from parent to child when the parent is in one service and the child is in another. See PR description for full details.
1 parent bb40f48 commit e39c754

File tree

7 files changed

+310
-28
lines changed

7 files changed

+310
-28
lines changed

packages/tracing/src/browser/browsertracing.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
6161
markBackgroundTransactions: boolean;
6262

6363
/**
64-
* beforeNavigate is called before a pageload/navigation transaction is created and allows for users
65-
* to set custom transaction context. Default behavior is to return `window.location.pathname`.
64+
* beforeNavigate is called before a pageload/navigation transaction is created and allows users to modify transaction
65+
* context data, or drop the transaction entirely (by setting `sampled = false` in the context).
6666
*
67-
* If undefined is returned, a pageload/navigation transaction will not be created.
67+
* Note: For legacy reasons, transactions can also be dropped by returning `undefined`.
68+
*
69+
* @param context: The context data which will be passed to `startTransaction` by default
70+
*
71+
* @returns A (potentially) modified context object, with `sampled = false` if the transaction should be dropped.
6872
*/
6973
beforeNavigate?(context: TransactionContext): TransactionContext | undefined;
7074

@@ -187,22 +191,26 @@ export class BrowserTracing implements Integration {
187191
// eslint-disable-next-line @typescript-eslint/unbound-method
188192
const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options;
189193

190-
// if beforeNavigate returns undefined, we should not start a transaction.
194+
const parentContextFromHeader = context.op === 'pageload' ? getHeaderContext() : undefined;
195+
191196
const expandedContext = {
192197
...context,
193-
...getHeaderContext(),
198+
...parentContextFromHeader,
194199
trimEnd: true,
195200
};
196201
const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext;
197202

198-
if (modifiedContext === undefined) {
199-
logger.log(`[Tracing] Did not create ${context.op} idleTransaction due to beforeNavigate`);
200-
return undefined;
203+
// For backwards compatibility reasons, beforeNavigate can return undefined to "drop" the transaction (prevent it
204+
// from being sent to Sentry).
205+
const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext;
206+
207+
if (finalContext.sampled === false) {
208+
logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
201209
}
202210

203211
const hub = this._getCurrentHub();
204-
const idleTransaction = startIdleTransaction(hub, modifiedContext, idleTimeout, true);
205-
logger.log(`[Tracing] Starting ${modifiedContext.op} transaction on scope`);
212+
const idleTransaction = startIdleTransaction(hub, finalContext, idleTimeout, true);
213+
logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
206214
idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => {
207215
this._metrics.addPerformanceEntries(transaction);
208216
adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp);

packages/tracing/src/browser/request.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { getCurrentHub } from '@sentry/hub';
12
import { addInstrumentationHandler, isInstanceOf, isMatchingPattern } from '@sentry/utils';
23

34
import { Span } from '../span';
4-
import { getActiveTransaction } from '../utils';
5+
import { getActiveTransaction, hasTracingEnabled } from '../utils';
56

67
export const DEFAULT_TRACING_ORIGINS = ['localhost', /^\//];
78

@@ -142,7 +143,13 @@ export function fetchCallback(
142143
shouldCreateSpan: (url: string) => boolean,
143144
spans: Record<string, Span>,
144145
): void {
145-
if (!handlerData.fetchData || !shouldCreateSpan(handlerData.fetchData.url)) {
146+
const currentClientOptions = getCurrentHub()
147+
.getClient()
148+
?.getOptions();
149+
if (
150+
!(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
151+
!(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))
152+
) {
146153
return;
147154
}
148155

@@ -209,19 +216,18 @@ export function xhrCallback(
209216
shouldCreateSpan: (url: string) => boolean,
210217
spans: Record<string, Span>,
211218
): void {
212-
if (!handlerData || !handlerData.xhr || !handlerData.xhr.__sentry_xhr__) {
219+
const currentClientOptions = getCurrentHub()
220+
.getClient()
221+
?.getOptions();
222+
if (
223+
!(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
224+
!(handlerData.xhr && handlerData.xhr.__sentry_xhr__ && shouldCreateSpan(handlerData.xhr.__sentry_xhr__.url)) ||
225+
handlerData.xhr.__sentry_own_request__
226+
) {
213227
return;
214228
}
215229

216230
const xhr = handlerData.xhr.__sentry_xhr__;
217-
if (!shouldCreateSpan(xhr.url)) {
218-
return;
219-
}
220-
221-
// We only capture complete, non-sentry requests
222-
if (handlerData.xhr.__sentry_own_request__) {
223-
return;
224-
}
225231

226232
// check first if the request has finished and is tracked by an existing span which should now end
227233
if (handlerData.endTimestamp && handlerData.xhr.__sentry_xhr_span_id__) {

packages/tracing/src/hubextensions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ function sample<T extends Transaction>(hub: Hub, transaction: T, samplingContext
6262
return transaction;
6363
}
6464

65+
// if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that
66+
if (transaction.sampled !== undefined) {
67+
return transaction;
68+
}
69+
6570
// we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` were defined, so one of these should
6671
// work; prefer the hook if so
6772
const sampleRate =

packages/tracing/test/browser/browsertracing.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BrowserTracing,
88
BrowserTracingOptions,
99
DEFAULT_MAX_TRANSACTION_DURATION_SECONDS,
10+
getHeaderContext,
1011
getMetaContent,
1112
} from '../../src/browser/browsertracing';
1213
import { defaultRequestInstrumentionOptions } from '../../src/browser/request';
@@ -177,14 +178,15 @@ describe('BrowserTracing', () => {
177178
expect(mockBeforeNavigation).toHaveBeenCalledTimes(1);
178179
});
179180

180-
it('does not create a transaction if it returns undefined', () => {
181+
// TODO add this back in once getTransaction() returns sampled = false transactions, too
182+
it.skip('creates a transaction with sampled = false if it returns undefined', () => {
181183
const mockBeforeNavigation = jest.fn().mockReturnValue(undefined);
182184
createBrowserTracing(true, {
183185
beforeNavigate: mockBeforeNavigation,
184186
routingInstrumentation: customRoutingInstrumentation,
185187
});
186188
const transaction = getActiveTransaction(hub) as IdleTransaction;
187-
expect(transaction).not.toBeDefined();
189+
expect(transaction.sampled).toBe(false);
188190

189191
expect(mockBeforeNavigation).toHaveBeenCalledTimes(1);
190192
});
@@ -379,5 +381,67 @@ describe('BrowserTracing', () => {
379381
expect(metaTagValue).toBe(content);
380382
});
381383
});
384+
385+
describe('getHeaderContext', () => {
386+
it('correctly parses a valid sentry-trace meta header', () => {
387+
document.head.innerHTML = `<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-0">`;
388+
389+
const headerContext = getHeaderContext();
390+
391+
expect(headerContext).toBeDefined();
392+
expect(headerContext!.traceId).toEqual('12312012123120121231201212312012');
393+
expect(headerContext!.parentSpanId).toEqual('1121201211212012');
394+
expect(headerContext!.parentSampled).toEqual(false);
395+
});
396+
397+
it('returns undefined if the header is malformed', () => {
398+
document.head.innerHTML = `<meta name="sentry-trace" content="12312012-112120121-0">`;
399+
400+
const headerContext = getHeaderContext();
401+
402+
expect(headerContext).toBeUndefined();
403+
});
404+
405+
it("returns undefined if the header isn't there", () => {
406+
document.head.innerHTML = `<meta name="dogs" content="12312012123120121231201212312012-1121201211212012-0">`;
407+
408+
const headerContext = getHeaderContext();
409+
410+
expect(headerContext).toBeUndefined();
411+
});
412+
});
413+
414+
describe('using the data', () => {
415+
// TODO add this back in once getTransaction() returns sampled = false transactions, too
416+
it.skip('uses the data for pageload transactions', () => {
417+
// make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one
418+
document.head.innerHTML = `<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-0">`;
419+
420+
// pageload transactions are created as part of the BrowserTracing integration's initialization
421+
createBrowserTracing(true);
422+
const transaction = getActiveTransaction(hub) as IdleTransaction;
423+
424+
expect(transaction).toBeDefined();
425+
expect(transaction.op).toBe('pageload');
426+
expect(transaction.traceId).toEqual('12312012123120121231201212312012');
427+
expect(transaction.parentSpanId).toEqual('1121201211212012');
428+
expect(transaction.sampled).toBe(false);
429+
});
430+
431+
it('ignores the data for navigation transactions', () => {
432+
mockChangeHistory = () => undefined;
433+
document.head.innerHTML = `<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-0">`;
434+
435+
createBrowserTracing(true);
436+
437+
mockChangeHistory({ to: 'here', from: 'there' });
438+
const transaction = getActiveTransaction(hub) as IdleTransaction;
439+
440+
expect(transaction).toBeDefined();
441+
expect(transaction.op).toBe('navigation');
442+
expect(transaction.traceId).not.toEqual('12312012123120121231201212312012');
443+
expect(transaction.parentSpanId).toBeUndefined();
444+
});
445+
});
382446
});
383447
});

packages/tracing/test/browser/request.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ beforeAll(() => {
2020
global.Request = {};
2121
});
2222

23+
const hasTracingEnabled = jest.spyOn(tracingUtils, 'hasTracingEnabled');
2324
const addInstrumentationHandler = jest.spyOn(utils, 'addInstrumentationHandler');
2425
const setRequestHeader = jest.fn();
2526

@@ -108,6 +109,30 @@ describe('callbacks', () => {
108109
expect(spans).toEqual({});
109110
});
110111

112+
it('does not add fetch request spans if tracing is disabled', () => {
113+
hasTracingEnabled.mockReturnValueOnce(false);
114+
const spans = {};
115+
116+
fetchCallback(fetchHandlerData, alwaysCreateSpan, spans);
117+
expect(spans).toEqual({});
118+
});
119+
120+
it('does not add fetch request headers if tracing is disabled', () => {
121+
hasTracingEnabled.mockReturnValueOnce(false);
122+
123+
// make a local copy so the global one doesn't get mutated
124+
const handlerData: FetchData = {
125+
args: ['http://dogs.are.great/', {}],
126+
fetchData: { url: 'http://dogs.are.great/', method: 'GET' },
127+
startTimestamp: 1353501072000,
128+
};
129+
130+
fetchCallback(handlerData, alwaysCreateSpan, {});
131+
132+
const headers = (handlerData.args[1].headers as Record<string, string>) || {};
133+
expect(headers['sentry-trace']).not.toBeDefined();
134+
});
135+
111136
it('creates and finishes fetch span on active transaction', () => {
112137
const spans = {};
113138

@@ -174,6 +199,22 @@ describe('callbacks', () => {
174199
expect(spans).toEqual({});
175200
});
176201

202+
it('does not add xhr request spans if tracing is disabled', () => {
203+
hasTracingEnabled.mockReturnValueOnce(false);
204+
const spans = {};
205+
206+
xhrCallback(xhrHandlerData, alwaysCreateSpan, spans);
207+
expect(spans).toEqual({});
208+
});
209+
210+
it('does not add xhr request headers if tracing is disabled', () => {
211+
hasTracingEnabled.mockReturnValueOnce(false);
212+
213+
xhrCallback(xhrHandlerData, alwaysCreateSpan, {});
214+
215+
expect(setRequestHeader).not.toHaveBeenCalled();
216+
});
217+
177218
it('adds sentry-trace header to XHR requests', () => {
178219
xhrCallback(xhrHandlerData, alwaysCreateSpan, {});
179220

0 commit comments

Comments
 (0)