Skip to content

Commit d439fc5

Browse files
authored
feat(tracing): Add tracing without performance to browser and client Sveltekit (#8458)
Adds tracing without performance support to 1. fetch requests in browser 2. xhr requests in browser 3. Sveltekit fetch monkeypatching (pulled into this PR because it also uses `addTracingHeadersToFetchRequest`
1 parent 1d8c81f commit d439fc5

File tree

4 files changed

+137
-99
lines changed

4 files changed

+137
-99
lines changed

packages/sveltekit/src/client/load.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
129129
return originalFetch;
130130
}
131131

132+
const options = client.getOptions();
133+
132134
const browserTracingIntegration = client.getIntegrationById('BrowserTracing') as BrowserTracing | undefined;
133135
const breadcrumbsIntegration = client.getIntegrationById('Breadcrumbs') as Breadcrumbs | undefined;
134136

@@ -147,7 +149,10 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
147149
const shouldAttachHeaders: (url: string) => boolean = url => {
148150
return (
149151
!!shouldTraceFetch &&
150-
stringMatchesSomePattern(url, browserTracingOptions.tracePropagationTargets || ['localhost', /^\//])
152+
stringMatchesSomePattern(
153+
url,
154+
options.tracePropagationTargets || browserTracingOptions.tracePropagationTargets || ['localhost', /^\//],
155+
)
151156
);
152157
};
153158

@@ -177,20 +182,15 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
177182
};
178183

179184
const patchedInit: RequestInit = { ...init };
180-
const activeSpan = getCurrentHub().getScope().getSpan();
181-
const activeTransaction = activeSpan && activeSpan.transaction;
182-
183-
const createSpan = activeTransaction && shouldCreateSpan(rawUrl);
184-
const attachHeaders = createSpan && activeTransaction && shouldAttachHeaders(rawUrl);
185-
186-
// only attach headers if we should create a span
187-
if (attachHeaders) {
188-
const dsc = activeTransaction.getDynamicSamplingContext();
185+
const hub = getCurrentHub();
186+
const scope = hub.getScope();
187+
const client = hub.getClient();
189188

189+
if (client && shouldAttachHeaders(rawUrl)) {
190190
const headers = addTracingHeadersToFetchRequest(
191191
input as string | Request,
192-
dsc,
193-
activeSpan,
192+
client,
193+
scope,
194194
patchedInit as {
195195
headers:
196196
| {
@@ -207,7 +207,7 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
207207

208208
const patchedFetchArgs = [input, patchedInit];
209209

210-
if (createSpan) {
210+
if (shouldCreateSpan(rawUrl)) {
211211
fetchPromise = trace(
212212
{
213213
name: `${method} ${requestData.url}`, // this will become the description of the span

packages/sveltekit/test/client/load.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const mockedGetIntegrationById = vi.fn(id => {
6161
const mockedGetClient = vi.fn(() => {
6262
return {
6363
getIntegrationById: mockedGetIntegrationById,
64+
getOptions: () => ({}),
6465
};
6566
});
6667

@@ -77,6 +78,11 @@ vi.mock('@sentry/core', async () => {
7778
getClient: mockedGetClient,
7879
getScope: () => {
7980
return {
81+
getPropagationContext: () => ({
82+
traceId: '1234567890abcdef1234567890abcdef',
83+
spanId: '1234567890abcdef',
84+
sampled: false,
85+
}),
8086
getSpan: () => {
8187
return {
8288
transaction: {
@@ -371,7 +377,7 @@ describe('wrapLoadWithSentry', () => {
371377
mockedBrowserTracing.options.traceFetch = true;
372378
});
373379

374-
it("doesn't create a span nor propagate headers, if `shouldCreateSpanForRequest` returns false", async () => {
380+
it("doesn't create a span if `shouldCreateSpanForRequest` returns false", async () => {
375381
mockedBrowserTracing.options.shouldCreateSpanForRequest = () => false;
376382

377383
const wrappedLoad = wrapLoadWithSentry(load);
@@ -391,10 +397,6 @@ describe('wrapLoadWithSentry', () => {
391397
expect.any(Function),
392398
);
393399

394-
expect(mockedSveltekitFetch).toHaveBeenCalledWith(
395-
...[originalFetchArgs[0], originalFetchArgs.length === 2 ? originalFetchArgs[1] : {}],
396-
);
397-
398400
mockedBrowserTracing.options.shouldCreateSpanForRequest = () => true;
399401
});
400402

packages/tracing-internal/src/browser/request.ts

Lines changed: 115 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* eslint-disable max-lines */
2-
import { getCurrentHub, hasTracingEnabled } from '@sentry/core';
3-
import type { DynamicSamplingContext, Span } from '@sentry/types';
2+
import { getCurrentHub, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core';
3+
import type { Client, Scope, Span } from '@sentry/types';
44
import {
55
addInstrumentationHandler,
66
BAGGAGE_HEADER_NAME,
77
browserPerformanceTimeOrigin,
88
dynamicSamplingContextToSentryBaggageHeader,
9+
generateSentryTraceHeader,
910
isInstanceOf,
1011
SENTRY_XHR_DATA_KEY,
1112
stringMatchesSomePattern,
@@ -219,12 +220,14 @@ export function fetchCallback(
219220
shouldCreateSpan: (url: string) => boolean,
220221
shouldAttachHeaders: (url: string) => boolean,
221222
spans: Record<string, Span>,
222-
): Span | void {
223-
if (!hasTracingEnabled() || !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))) {
224-
return;
223+
): Span | undefined {
224+
if (!hasTracingEnabled() || !handlerData.fetchData) {
225+
return undefined;
225226
}
226227

227-
if (handlerData.endTimestamp) {
228+
const shouldCreateSpanResult = shouldCreateSpan(handlerData.fetchData.url);
229+
230+
if (handlerData.endTimestamp && shouldCreateSpanResult) {
228231
const spanId = handlerData.fetchData.__span;
229232
if (!spanId) return;
230233

@@ -251,27 +254,35 @@ export function fetchCallback(
251254
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
252255
delete spans[spanId];
253256
}
254-
return;
257+
return undefined;
255258
}
256259

257-
const currentSpan = getCurrentHub().getScope().getSpan();
258-
const activeTransaction = currentSpan && currentSpan.transaction;
259-
260-
if (currentSpan && activeTransaction) {
261-
const { method, url } = handlerData.fetchData;
262-
const span = currentSpan.startChild({
263-
data: {
264-
url,
265-
type: 'fetch',
266-
'http.method': method,
267-
},
268-
description: `${method} ${url}`,
269-
op: 'http.client',
270-
});
271-
260+
const hub = getCurrentHub();
261+
const scope = hub.getScope();
262+
const client = hub.getClient();
263+
const parentSpan = scope.getSpan();
264+
265+
const { method, url } = handlerData.fetchData;
266+
267+
const span =
268+
shouldCreateSpanResult && parentSpan
269+
? parentSpan.startChild({
270+
data: {
271+
url,
272+
type: 'fetch',
273+
'http.method': method,
274+
},
275+
description: `${method} ${url}`,
276+
op: 'http.client',
277+
})
278+
: undefined;
279+
280+
if (span) {
272281
handlerData.fetchData.__span = span.spanId;
273282
spans[span.spanId] = span;
283+
}
274284

285+
if (shouldAttachHeaders(handlerData.fetchData.url) && client) {
275286
const request: string | Request = handlerData.args[0];
276287

277288
// In case the user hasn't set the second argument of a fetch call we default it to `{}`.
@@ -280,35 +291,42 @@ export function fetchCallback(
280291
// eslint-disable-next-line @typescript-eslint/no-explicit-any
281292
const options: { [key: string]: any } = handlerData.args[1];
282293

283-
if (shouldAttachHeaders(handlerData.fetchData.url)) {
284-
options.headers = addTracingHeadersToFetchRequest(
285-
request,
286-
activeTransaction.getDynamicSamplingContext(),
287-
span,
288-
options,
289-
);
290-
}
291-
return span;
294+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
295+
options.headers = addTracingHeadersToFetchRequest(request, client, scope, options);
292296
}
297+
298+
return span;
293299
}
294300

295301
/**
296302
* Adds sentry-trace and baggage headers to the various forms of fetch headers
297303
*/
298304
export function addTracingHeadersToFetchRequest(
299305
request: string | unknown, // unknown is actually type Request but we can't export DOM types from this package,
300-
dynamicSamplingContext: Partial<DynamicSamplingContext>,
301-
span: Span,
306+
client: Client,
307+
scope: Scope,
302308
options: {
303309
headers?:
304310
| {
305311
[key: string]: string[] | string | undefined;
306312
}
307313
| PolymorphicRequestHeaders;
308314
},
309-
): PolymorphicRequestHeaders {
315+
): PolymorphicRequestHeaders | undefined {
316+
const span = scope.getSpan();
317+
318+
const transaction = span && span.transaction;
319+
320+
const { traceId, sampled, dsc } = scope.getPropagationContext();
321+
322+
const sentryTraceHeader = span ? span.toTraceparent() : generateSentryTraceHeader(traceId, undefined, sampled);
323+
const dynamicSamplingContext = transaction
324+
? transaction.getDynamicSamplingContext()
325+
: dsc
326+
? dsc
327+
: getDynamicSamplingContextFromClient(traceId, client, scope);
328+
310329
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
311-
const sentryTraceHeader = span.toTraceparent();
312330

313331
const headers =
314332
typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : options.headers;
@@ -364,25 +382,24 @@ export function addTracingHeadersToFetchRequest(
364382
*
365383
* @returns Span if a span was created, otherwise void.
366384
*/
385+
// eslint-disable-next-line complexity
367386
export function xhrCallback(
368387
handlerData: XHRData,
369388
shouldCreateSpan: (url: string) => boolean,
370389
shouldAttachHeaders: (url: string) => boolean,
371390
spans: Record<string, Span>,
372-
): Span | void {
391+
): Span | undefined {
373392
const xhr = handlerData.xhr;
374393
const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY];
375394

376-
if (
377-
!hasTracingEnabled() ||
378-
(xhr && xhr.__sentry_own_request__) ||
379-
!(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url))
380-
) {
381-
return;
395+
if (!hasTracingEnabled() || (xhr && xhr.__sentry_own_request__) || !xhr || !sentryXhrData) {
396+
return undefined;
382397
}
383398

399+
const shouldCreateSpanResult = shouldCreateSpan(sentryXhrData.url);
400+
384401
// check first if the request has finished and is tracked by an existing span which should now end
385-
if (handlerData.endTimestamp) {
402+
if (handlerData.endTimestamp && shouldCreateSpanResult) {
386403
const spanId = xhr.__sentry_xhr_span_id__;
387404
if (!spanId) return;
388405

@@ -394,45 +411,68 @@ export function xhrCallback(
394411
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
395412
delete spans[spanId];
396413
}
397-
return;
414+
return undefined;
398415
}
399416

400-
const currentSpan = getCurrentHub().getScope().getSpan();
401-
const activeTransaction = currentSpan && currentSpan.transaction;
402-
403-
if (currentSpan && activeTransaction) {
404-
const span = currentSpan.startChild({
405-
data: {
406-
...sentryXhrData.data,
407-
type: 'xhr',
408-
'http.method': sentryXhrData.method,
409-
url: sentryXhrData.url,
410-
},
411-
description: `${sentryXhrData.method} ${sentryXhrData.url}`,
412-
op: 'http.client',
413-
});
414-
417+
const hub = getCurrentHub();
418+
const scope = hub.getScope();
419+
const parentSpan = scope.getSpan();
420+
421+
const span =
422+
shouldCreateSpanResult && parentSpan
423+
? parentSpan.startChild({
424+
data: {
425+
...sentryXhrData.data,
426+
type: 'xhr',
427+
'http.method': sentryXhrData.method,
428+
url: sentryXhrData.url,
429+
},
430+
description: `${sentryXhrData.method} ${sentryXhrData.url}`,
431+
op: 'http.client',
432+
})
433+
: undefined;
434+
435+
if (span) {
415436
xhr.__sentry_xhr_span_id__ = span.spanId;
416437
spans[xhr.__sentry_xhr_span_id__] = span;
438+
}
417439

418-
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
419-
try {
420-
xhr.setRequestHeader('sentry-trace', span.toTraceparent());
440+
if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
441+
if (span) {
442+
const transaction = span && span.transaction;
443+
const dynamicSamplingContext = transaction && transaction.getDynamicSamplingContext();
444+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
445+
setHeaderOnXhr(xhr, span.toTraceparent(), sentryBaggageHeader);
446+
} else {
447+
const client = hub.getClient();
448+
const { traceId, sampled, dsc } = scope.getPropagationContext();
449+
const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled);
450+
const dynamicSamplingContext =
451+
dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined);
452+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
453+
setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader);
454+
}
455+
}
421456

422-
const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext();
423-
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
457+
return span;
458+
}
424459

425-
if (sentryBaggageHeader) {
426-
// From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
427-
// We can therefore simply set a baggage header without checking what was there before
428-
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
429-
xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
430-
}
431-
} catch (_) {
432-
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
433-
}
460+
function setHeaderOnXhr(
461+
xhr: NonNullable<XHRData['xhr']>,
462+
sentryTraceHeader: string,
463+
sentryBaggageHeader: string | undefined,
464+
): void {
465+
try {
466+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
467+
xhr.setRequestHeader!('sentry-trace', sentryTraceHeader);
468+
if (sentryBaggageHeader) {
469+
// From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
470+
// We can therefore simply set a baggage header without checking what was there before
471+
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
472+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
473+
xhr.setRequestHeader!(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
434474
}
435-
436-
return span;
475+
} catch (_) {
476+
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
437477
}
438478
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,7 @@ describe('callbacks', () => {
9898
// each case is [shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys]
9999
[true, true, expect.objectContaining(fetchSpan), ['sentry-trace', 'baggage']],
100100
[true, false, expect.objectContaining(fetchSpan), []],
101-
// If there's no span then there's no parent span id to stick into a header, so no headers, even if there's a
102-
// `tracingOrigins` match
103-
[false, true, undefined, []],
101+
[false, true, undefined, ['sentry-trace', 'baggage']],
104102
[false, false, undefined, []],
105103
])(
106104
'span creation/header attachment interaction - shouldCreateSpan: %s, shouldAttachHeaders: %s',
@@ -284,9 +282,7 @@ describe('callbacks', () => {
284282
// each case is [shouldCreateSpanReturnValue, shouldAttachHeadersReturnValue, expectedSpan, expectedHeaderKeys]
285283
[true, true, expect.objectContaining(xhrSpan), ['sentry-trace', 'baggage']],
286284
[true, false, expect.objectContaining(xhrSpan), []],
287-
// If there's no span then there's no parent span id to stick into a header, so no headers, even if there's a
288-
// `tracingOrigins` match
289-
[false, true, undefined, []],
285+
[false, true, undefined, ['sentry-trace', 'baggage']],
290286
[false, false, undefined, []],
291287
])(
292288
'span creation/header attachment interaction - shouldCreateSpan: %s, shouldAttachHeaders: %s',

0 commit comments

Comments
 (0)