Skip to content

Commit 545ca89

Browse files
committed
feat: added hook to browser integration settings
1 parent 0837acc commit 545ca89

File tree

6 files changed

+136
-37
lines changed

6 files changed

+136
-37
lines changed

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/* eslint-disable max-lines */
2-
import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, WebFetchHeaders } from '@sentry/core';
2+
import type {
3+
Client,
4+
IntegrationFn,
5+
RequestHookInfo,
6+
ResponseHookInfo,
7+
Span,
8+
StartSpanOptions,
9+
TransactionSource,
10+
} from '@sentry/core';
311
import {
412
addNonEnumerableProperty,
513
browserPerformanceTimeOrigin,
@@ -297,7 +305,12 @@ export interface BrowserTracingOptions {
297305
* You can use it to annotate the span with additional data or attributes, for example by setting
298306
* attributes based on the passed request headers.
299307
*/
300-
onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
308+
onRequestSpanStart?(span: Span, requestInformation: RequestHookInfo): void;
309+
310+
/**
311+
* Is called when spans end for outgoing requests, providing access to response headers.
312+
*/
313+
onRequestSpanEnd?(span: Span, responseInformation: ResponseHookInfo): void;
301314
}
302315

303316
const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
@@ -365,6 +378,7 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
365378
consistentTraceSampling,
366379
enableReportPageLoaded,
367380
onRequestSpanStart,
381+
onRequestSpanEnd,
368382
} = {
369383
...DEFAULT_BROWSER_TRACING_OPTIONS,
370384
...options,
@@ -692,6 +706,7 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
692706
shouldCreateSpanForRequest,
693707
enableHTTPTimings,
694708
onRequestSpanStart,
709+
onRequestSpanEnd,
695710
});
696711
},
697712
};

packages/browser/src/tracing/request.ts

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span, WebFetchHeaders } from '@sentry/core';
1+
import type {
2+
Client,
3+
HandlerDataXhr,
4+
RequestHookInfo,
5+
ResponseHookInfo,
6+
SentryWrappedXMLHttpRequest,
7+
Span,
8+
} from '@sentry/core';
29
import {
310
addFetchEndInstrumentationHandler,
411
addFetchInstrumentationHandler,
@@ -26,7 +33,7 @@ import {
2633
SENTRY_XHR_DATA_KEY,
2734
} from '@sentry-internal/browser-utils';
2835
import type { BrowserClient } from '../client';
29-
import { WINDOW } from '../helpers';
36+
import { baggageHeaderHasSentryValues, createHeadersSafely, getFullURL, isPerformanceResourceTiming } from './utils';
3037

3138
/** Options for Request Instrumentation */
3239
export interface RequestInstrumentationOptions {
@@ -102,7 +109,12 @@ export interface RequestInstrumentationOptions {
102109
/**
103110
* Is called when spans are started for outgoing requests.
104111
*/
105-
onRequestSpanStart?(span: Span, requestInformation: { headers?: WebFetchHeaders }): void;
112+
onRequestSpanStart?(span: Span, requestInformation: RequestHookInfo): void;
113+
114+
/**
115+
* Is called when spans end for outgoing requests, providing access to response headers.
116+
*/
117+
onRequestSpanEnd?(span: Span, responseInformation: ResponseHookInfo): void;
106118
}
107119

108120
const responseToSpanId = new WeakMap<object, string>();
@@ -125,6 +137,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
125137
enableHTTPTimings,
126138
tracePropagationTargets,
127139
onRequestSpanStart,
140+
onRequestSpanEnd,
128141
} = {
129142
...defaultRequestInstrumentationOptions,
130143
..._options,
@@ -171,6 +184,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
171184
addFetchInstrumentationHandler(handlerData => {
172185
const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans, {
173186
propagateTraceparent,
187+
onRequestSpanEnd,
174188
});
175189

176190
if (handlerData.response && handlerData.fetchData.__span) {
@@ -205,34 +219,22 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
205219
shouldAttachHeadersWithTargets,
206220
spans,
207221
propagateTraceparent,
222+
onRequestSpanEnd,
208223
);
209224

210225
if (createdSpan) {
211226
if (enableHTTPTimings) {
212227
addHTTPTimings(createdSpan);
213228
}
214229

215-
let headers;
216-
try {
217-
headers = new Headers(handlerData.xhr.__sentry_xhr_v3__?.request_headers);
218-
} catch {
219-
// noop
220-
}
221-
onRequestSpanStart?.(createdSpan, { headers });
230+
onRequestSpanStart?.(createdSpan, {
231+
headers: createHeadersSafely(handlerData.xhr.__sentry_xhr_v3__?.request_headers),
232+
});
222233
}
223234
});
224235
}
225236
}
226237

227-
function isPerformanceResourceTiming(entry: PerformanceEntry): entry is PerformanceResourceTiming {
228-
return (
229-
entry.entryType === 'resource' &&
230-
'initiatorType' in entry &&
231-
typeof (entry as PerformanceResourceTiming).nextHopProtocol === 'string' &&
232-
(entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
233-
);
234-
}
235-
236238
/**
237239
* Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
238240
* so that when timings hit their per-browser limit they don't need to be removed.
@@ -315,6 +317,7 @@ function xhrCallback(
315317
shouldAttachHeaders: (url: string) => boolean,
316318
spans: Record<string, Span>,
317319
propagateTraceparent?: boolean,
320+
onRequestSpanEnd?: RequestInstrumentationOptions['onRequestSpanEnd'],
318321
): Span | undefined {
319322
const xhr = handlerData.xhr;
320323
const sentryXhrData = xhr?.[SENTRY_XHR_DATA_KEY];
@@ -337,6 +340,10 @@ function xhrCallback(
337340
setHttpStatus(span, sentryXhrData.status_code);
338341
span.end();
339342

343+
onRequestSpanEnd?.(span, {
344+
headers: createHeadersSafely(sentryXhrData.request_headers),
345+
});
346+
340347
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
341348
delete spans[spanId];
342349
}
@@ -438,18 +445,3 @@ function setHeaderOnXhr(
438445
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
439446
}
440447
}
441-
442-
function baggageHeaderHasSentryValues(baggageHeader: string): boolean {
443-
return baggageHeader.split(',').some(value => value.trim().startsWith('sentry-'));
444-
}
445-
446-
function getFullURL(url: string): string | undefined {
447-
try {
448-
// By adding a base URL to new URL(), this will also work for relative urls
449-
// If `url` is a full URL, the base URL is ignored anyhow
450-
const parsed = new URL(url, WINDOW.location.origin);
451-
return parsed.href;
452-
} catch {
453-
return undefined;
454-
}
455-
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { WINDOW } from '../helpers';
2+
3+
/**
4+
* Checks if the baggage header has Sentry values.
5+
*/
6+
export function baggageHeaderHasSentryValues(baggageHeader: string): boolean {
7+
return baggageHeader.split(',').some(value => value.trim().startsWith('sentry-'));
8+
}
9+
10+
/**
11+
* Gets the full URL from a given URL string.
12+
*/
13+
export function getFullURL(url: string): string | undefined {
14+
try {
15+
// By adding a base URL to new URL(), this will also work for relative urls
16+
// If `url` is a full URL, the base URL is ignored anyhow
17+
const parsed = new URL(url, WINDOW.location.origin);
18+
return parsed.href;
19+
} catch {
20+
return undefined;
21+
}
22+
}
23+
24+
/**
25+
* Checks if the entry is a PerformanceResourceTiming.
26+
*/
27+
export function isPerformanceResourceTiming(entry: PerformanceEntry): entry is PerformanceResourceTiming {
28+
return (
29+
entry.entryType === 'resource' &&
30+
'initiatorType' in entry &&
31+
typeof (entry as PerformanceResourceTiming).nextHopProtocol === 'string' &&
32+
(entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
33+
);
34+
}
35+
36+
/**
37+
* Creates a Headers object from a record of string key-value pairs, and returns undefined if it fails.
38+
*/
39+
export function createHeadersSafely(headers: Record<string, string> | undefined): Headers | undefined {
40+
try {
41+
return new Headers(headers);
42+
} catch {
43+
// noop
44+
return undefined;
45+
}
46+
}

packages/core/src/fetch.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { setHttpStatus, SPAN_STATUS_ERROR, startInactiveSpan } from './tracing';
44
import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan';
55
import type { FetchBreadcrumbHint } from './types-hoist/breadcrumb';
66
import type { HandlerDataFetch } from './types-hoist/instrument';
7+
import type { ResponseHookInfo } from './types-hoist/request';
78
import type { Span, SpanAttributes, SpanOrigin } from './types-hoist/span';
89
import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils/baggage';
910
import { hasSpansEnabled } from './utils/hasSpansEnabled';
@@ -24,6 +25,7 @@ type PolymorphicRequestHeaders =
2425
interface InstrumentFetchRequestOptions {
2526
spanOrigin?: SpanOrigin;
2627
propagateTraceparent?: boolean;
28+
onRequestSpanEnd?: (span: Span, responseInformation: ResponseHookInfo) => void;
2729
}
2830

2931
/**
@@ -82,6 +84,8 @@ export function instrumentFetchRequest(
8284
if (span) {
8385
endSpan(span, handlerData);
8486

87+
_callOnRequestSpanEnd(span, handlerData, spanOriginOrOptions);
88+
8589
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
8690
delete spans[spanId];
8791
}
@@ -141,6 +145,24 @@ export function instrumentFetchRequest(
141145
return span;
142146
}
143147

148+
/**
149+
* Calls the onRequestSpanEnd callback if it is defined.
150+
*/
151+
export function _callOnRequestSpanEnd(
152+
span: Span,
153+
handlerData: HandlerDataFetch,
154+
spanOriginOrOptions?: SpanOrigin | InstrumentFetchRequestOptions,
155+
): void {
156+
const onRequestSpanEnd =
157+
typeof spanOriginOrOptions === 'object' && spanOriginOrOptions !== null
158+
? spanOriginOrOptions.onRequestSpanEnd
159+
: undefined;
160+
161+
onRequestSpanEnd?.(span, {
162+
headers: handlerData.response?.headers,
163+
});
164+
}
165+
144166
/**
145167
* Adds sentry-trace and baggage headers to the various forms of fetch headers.
146168
* exported only for testing purposes

packages/core/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,13 @@ export type {
390390
SendFeedbackParams,
391391
UserFeedback,
392392
} from './types-hoist/feedback';
393-
export type { QueryParams, RequestEventData, SanitizedRequestData } from './types-hoist/request';
393+
export type {
394+
QueryParams,
395+
RequestEventData,
396+
RequestHookInfo,
397+
ResponseHookInfo,
398+
SanitizedRequestData,
399+
} from './types-hoist/request';
394400
export type { Runtime } from './types-hoist/runtime';
395401
export type { SdkInfo } from './types-hoist/sdkinfo';
396402
export type { SdkMetadata } from './types-hoist/sdkmetadata';

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { WebFetchHeaders } from './webfetchapi';
2+
13
/**
24
* Request data included in an event as sent to Sentry.
35
*/
@@ -24,3 +26,19 @@ export type SanitizedRequestData = {
2426
'http.fragment'?: string;
2527
'http.query'?: string;
2628
};
29+
30+
export interface RequestHookInfo {
31+
headers?: WebFetchHeaders;
32+
}
33+
34+
export interface ResponseHookInfo {
35+
/**
36+
* Headers from the response.
37+
*/
38+
headers?: WebFetchHeaders;
39+
40+
/**
41+
* Error that may have occurred during the request.
42+
*/
43+
error?: unknown;
44+
}

0 commit comments

Comments
 (0)