Skip to content

Commit 303369c

Browse files
author
Luca Forstner
committed
feat(browser): Add onRequestSpanStart hook to browser tracing integration
1 parent b10085d commit 303369c

File tree

6 files changed

+72
-17
lines changed

6 files changed

+72
-17
lines changed

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
startTrackingLongTasks,
1010
startTrackingWebVitals,
1111
} from '@sentry-internal/browser-utils';
12-
import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource } from '@sentry/core';
12+
import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, WebFetchHeaders } from '@sentry/core';
1313
import {
1414
GLOBAL_OBJ,
1515
SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON,
@@ -195,6 +195,11 @@ export interface BrowserTracingOptions {
195195
* Default: (url: string) => true
196196
*/
197197
shouldCreateSpanForRequest?(this: void, url: string): boolean;
198+
199+
/**
200+
* Is called when spans are started for outgoing requests.
201+
*/
202+
onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
198203
}
199204

200205
const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {

packages/browser/src/tracing/request.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
extractNetworkProtocol,
66
} from '@sentry-internal/browser-utils';
77
import type { XhrHint } from '@sentry-internal/browser-utils';
8-
import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/core';
8+
import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span, WebFetchHeaders } from '@sentry/core';
99
import {
1010
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1111
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -98,6 +98,11 @@ export interface RequestInstrumentationOptions {
9898
* Default: (url: string) => true
9999
*/
100100
shouldCreateSpanForRequest?(this: void, url: string): boolean;
101+
102+
/**
103+
* Is called when spans are started for outgoing requests.
104+
*/
105+
onRequestSpanStart(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
101106
}
102107

103108
const responseToSpanId = new WeakMap<object, string>();
@@ -108,6 +113,9 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions
108113
traceXHR: true,
109114
enableHTTPTimings: true,
110115
trackFetchStreamPerformance: false,
116+
onRequestSpanStart() {
117+
// noop
118+
},
111119
};
112120

113121
/** Registers span creators for xhr and fetch requests */
@@ -119,10 +127,9 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
119127
shouldCreateSpanForRequest,
120128
enableHTTPTimings,
121129
tracePropagationTargets,
130+
onRequestSpanStart,
122131
} = {
123-
traceFetch: defaultRequestInstrumentationOptions.traceFetch,
124-
traceXHR: defaultRequestInstrumentationOptions.traceXHR,
125-
trackFetchStreamPerformance: defaultRequestInstrumentationOptions.trackFetchStreamPerformance,
132+
...defaultRequestInstrumentationOptions,
126133
..._options,
127134
};
128135

@@ -179,19 +186,31 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
179186
'http.url': fullUrl,
180187
'server.address': host,
181188
});
182-
}
183189

184-
if (enableHTTPTimings && createdSpan) {
185-
addHTTPTimings(createdSpan);
190+
if (enableHTTPTimings) {
191+
addHTTPTimings(createdSpan);
192+
}
193+
194+
onRequestSpanStart(createdSpan, { headers: handlerData.headers });
186195
}
187196
});
188197
}
189198

190199
if (traceXHR) {
191200
addXhrInstrumentationHandler(handlerData => {
192201
const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
193-
if (enableHTTPTimings && createdSpan) {
194-
addHTTPTimings(createdSpan);
202+
if (createdSpan) {
203+
if (enableHTTPTimings) {
204+
addHTTPTimings(createdSpan);
205+
}
206+
207+
let headers;
208+
try {
209+
headers = new Headers(handlerData.xhr.__sentry_xhr_v3__?.request_headers);
210+
} catch {
211+
// noop
212+
}
213+
onRequestSpanStart(createdSpan, { headers });
195214
}
196215
});
197216
}

packages/core/src/fetch.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing';
44
import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan';
55
import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanAttributes, SpanOrigin } from './types-hoist';
66
import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage';
7-
import { isInstanceOf } from './utils-hoist/is';
7+
import { isInstanceOf, isRequest } from './utils-hoist/is';
88
import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url';
99
import { hasSpansEnabled } from './utils/hasSpansEnabled';
1010
import { getActiveSpan } from './utils/spanUtils';
@@ -227,10 +227,6 @@ function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string
227227
);
228228
}
229229

230-
function isRequest(request: unknown): request is Request {
231-
return typeof Request !== 'undefined' && isInstanceOf(request, Request);
232-
}
233-
234230
function isHeaders(headers: unknown): headers is Headers {
235231
return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers);
236232
}

packages/core/src/types-hoist/instrument.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface HandlerDataFetch {
6161
error?: unknown;
6262
// This is to be consumed by the HttpClient integration
6363
virtualError?: unknown;
64+
/** Headers that the user passed to the fetch request. */
65+
headers?: WebFetchHeaders;
6466
}
6567

6668
export interface HandlerDataDom {

packages/core/src/utils-hoist/instrument/fetch.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import type { HandlerDataFetch } from '../../types-hoist';
2+
import type { HandlerDataFetch, WebFetchHeaders } from '../../types-hoist';
33

4-
import { isError } from '../is';
4+
import { isError, isRequest } from '../is';
55
import { addNonEnumerableProperty, fill } from '../object';
66
import { supportsNativeFetch } from '../supports';
77
import { timestampInSeconds } from '../time';
@@ -67,6 +67,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
6767
startTimestamp: timestampInSeconds() * 1000,
6868
// // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation
6969
virtualError,
70+
headers: getHeadersFromFetchArgs(args),
7071
};
7172

7273
// if there is no callback, fetch is instrumented directly
@@ -253,3 +254,26 @@ export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: str
253254
method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET',
254255
};
255256
}
257+
258+
function getHeadersFromFetchArgs(fetchArgs: unknown[]): WebFetchHeaders | undefined {
259+
const [requestArgument, optionsArgument] = fetchArgs;
260+
261+
try {
262+
if (
263+
typeof optionsArgument === 'object' &&
264+
optionsArgument !== null &&
265+
'headers' in optionsArgument &&
266+
optionsArgument.headers
267+
) {
268+
return new Headers(optionsArgument.headers as any);
269+
}
270+
271+
if (isRequest(requestArgument)) {
272+
return new Headers(requestArgument.headers);
273+
}
274+
} catch {
275+
// noop
276+
}
277+
278+
return;
279+
}

packages/core/src/utils-hoist/is.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,12 @@ export function isVueViewModel(wat: unknown): boolean {
201201
// Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property.
202202
return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue));
203203
}
204+
205+
/**
206+
* Checks whether the given parameter is a Standard Web API Request instance.
207+
*
208+
* Returns false if Request is not available in the current runtime.
209+
*/
210+
export function isRequest(request: unknown): request is Request {
211+
return typeof Request !== 'undefined' && isInstanceOf(request, Request);
212+
}

0 commit comments

Comments
 (0)