this repo has no description
1import { _optionalChain } from '@sentry/utils/esm/buildPolyfills';
2import { hasTracingEnabled, getCurrentHub } from '@sentry/core';
3import { addInstrumentationHandler, browserPerformanceTimeOrigin, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, BAGGAGE_HEADER_NAME, SENTRY_XHR_DATA_KEY, stringMatchesSomePattern } from '@sentry/utils';
4
5/* eslint-disable max-lines */
6
7const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
8
9/** Options for Request Instrumentation */
10
11const defaultRequestInstrumentationOptions = {
12 traceFetch: true,
13 traceXHR: true,
14 // TODO (v8): Remove this property
15 tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS,
16 tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
17 _experiments: {},
18};
19
20/** Registers span creators for xhr and fetch requests */
21function instrumentOutgoingRequests(_options) {
22 // eslint-disable-next-line deprecation/deprecation
23 const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = {
24 traceFetch: defaultRequestInstrumentationOptions.traceFetch,
25 traceXHR: defaultRequestInstrumentationOptions.traceXHR,
26 ..._options,
27 };
28
29 const shouldCreateSpan =
30 typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_) => true;
31
32 // TODO(v8) Remove tracingOrigins here
33 // The only reason we're passing it in here is because this instrumentOutgoingRequests function is publicly exported
34 // and we don't want to break the API. We can remove it in v8.
35 const shouldAttachHeadersWithTargets = (url) =>
36 shouldAttachHeaders(url, tracePropagationTargets || tracingOrigins);
37
38 const spans = {};
39
40 if (traceFetch) {
41 addInstrumentationHandler('fetch', (handlerData) => {
42 const createdSpan = fetchCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
43 if (_optionalChain([_experiments, 'optionalAccess', _2 => _2.enableHTTPTimings]) && createdSpan) {
44 addHTTPTimings(createdSpan);
45 }
46 });
47 }
48
49 if (traceXHR) {
50 addInstrumentationHandler('xhr', (handlerData) => {
51 const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
52 if (_optionalChain([_experiments, 'optionalAccess', _3 => _3.enableHTTPTimings]) && createdSpan) {
53 addHTTPTimings(createdSpan);
54 }
55 });
56 }
57}
58
59/**
60 * Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
61 * so that when timings hit their per-browser limit they don't need to be removed.
62 *
63 * @param span A span that has yet to be finished, must contain `url` on data.
64 */
65function addHTTPTimings(span) {
66 const url = span.data.url;
67 const observer = new PerformanceObserver(list => {
68 const entries = list.getEntries() ;
69 entries.forEach(entry => {
70 if ((entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') && entry.name.endsWith(url)) {
71 const spanData = resourceTimingEntryToSpanData(entry);
72 spanData.forEach(data => span.setData(...data));
73 observer.disconnect();
74 }
75 });
76 });
77 observer.observe({
78 entryTypes: ['resource'],
79 });
80}
81
82function resourceTimingEntryToSpanData(resourceTiming) {
83 const version = resourceTiming.nextHopProtocol.split('/')[1] || 'none';
84
85 const timingSpanData = [];
86 if (version) {
87 timingSpanData.push(['network.protocol.version', version]);
88 }
89
90 if (!browserPerformanceTimeOrigin) {
91 return timingSpanData;
92 }
93 return [
94 ...timingSpanData,
95 ['http.request.connect_start', (browserPerformanceTimeOrigin + resourceTiming.connectStart) / 1000],
96 ['http.request.request_start', (browserPerformanceTimeOrigin + resourceTiming.requestStart) / 1000],
97 ['http.request.response_start', (browserPerformanceTimeOrigin + resourceTiming.responseStart) / 1000],
98 ];
99}
100
101/**
102 * A function that determines whether to attach tracing headers to a request.
103 * This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
104 * We only export this fuction for testing purposes.
105 */
106function shouldAttachHeaders(url, tracePropagationTargets) {
107 return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
108}
109
110/**
111 * Create and track fetch request spans
112 *
113 * @returns Span if a span was created, otherwise void.
114 */
115function fetchCallback(
116 handlerData,
117 shouldCreateSpan,
118 shouldAttachHeaders,
119 spans,
120) {
121 if (!hasTracingEnabled() || !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))) {
122 return;
123 }
124
125 if (handlerData.endTimestamp) {
126 const spanId = handlerData.fetchData.__span;
127 if (!spanId) return;
128
129 const span = spans[spanId];
130 if (span) {
131 if (handlerData.response) {
132 // TODO (kmclb) remove this once types PR goes through
133 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
134 span.setHttpStatus(handlerData.response.status);
135
136 const contentLength =
137 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
138 handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');
139
140 const contentLengthNum = parseInt(contentLength);
141 if (contentLengthNum > 0) {
142 span.setData('http.response_content_length', contentLengthNum);
143 }
144 } else if (handlerData.error) {
145 span.setStatus('internal_error');
146 }
147 span.finish();
148
149 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
150 delete spans[spanId];
151 }
152 return;
153 }
154
155 const currentSpan = getCurrentHub().getScope().getSpan();
156 const activeTransaction = currentSpan && currentSpan.transaction;
157
158 if (currentSpan && activeTransaction) {
159 const { method, url } = handlerData.fetchData;
160 const span = currentSpan.startChild({
161 data: {
162 url,
163 type: 'fetch',
164 'http.method': method,
165 },
166 description: `${method} ${url}`,
167 op: 'http.client',
168 });
169
170 handlerData.fetchData.__span = span.spanId;
171 spans[span.spanId] = span;
172
173 const request = handlerData.args[0];
174
175 // In case the user hasn't set the second argument of a fetch call we default it to `{}`.
176 handlerData.args[1] = handlerData.args[1] || {};
177
178 // eslint-disable-next-line @typescript-eslint/no-explicit-any
179 const options = handlerData.args[1];
180
181 if (shouldAttachHeaders(handlerData.fetchData.url)) {
182 options.headers = addTracingHeadersToFetchRequest(
183 request,
184 activeTransaction.getDynamicSamplingContext(),
185 span,
186 options,
187 );
188 }
189 return span;
190 }
191}
192
193/**
194 * Adds sentry-trace and baggage headers to the various forms of fetch headers
195 */
196function addTracingHeadersToFetchRequest(
197 request, // unknown is actually type Request but we can't export DOM types from this package,
198 dynamicSamplingContext,
199 span,
200 options
201
202,
203) {
204 const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
205 const sentryTraceHeader = span.toTraceparent();
206
207 const headers =
208 typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request ).headers : options.headers;
209
210 if (!headers) {
211 return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
212 } else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
213 const newHeaders = new Headers(headers );
214
215 newHeaders.append('sentry-trace', sentryTraceHeader);
216
217 if (sentryBaggageHeader) {
218 // If the same header is appended multiple times the browser will merge the values into a single request header.
219 // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
220 newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
221 }
222
223 return newHeaders ;
224 } else if (Array.isArray(headers)) {
225 const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];
226
227 if (sentryBaggageHeader) {
228 // If there are multiple entries with the same key, the browser will merge the values into a single request header.
229 // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
230 newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]);
231 }
232
233 return newHeaders ;
234 } else {
235 const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
236 const newBaggageHeaders = [];
237
238 if (Array.isArray(existingBaggageHeader)) {
239 newBaggageHeaders.push(...existingBaggageHeader);
240 } else if (existingBaggageHeader) {
241 newBaggageHeaders.push(existingBaggageHeader);
242 }
243
244 if (sentryBaggageHeader) {
245 newBaggageHeaders.push(sentryBaggageHeader);
246 }
247
248 return {
249 ...(headers ),
250 'sentry-trace': sentryTraceHeader,
251 baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
252 };
253 }
254}
255
256/**
257 * Create and track xhr request spans
258 *
259 * @returns Span if a span was created, otherwise void.
260 */
261function xhrCallback(
262 handlerData,
263 shouldCreateSpan,
264 shouldAttachHeaders,
265 spans,
266) {
267 const xhr = handlerData.xhr;
268 const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY];
269
270 if (
271 !hasTracingEnabled() ||
272 (xhr && xhr.__sentry_own_request__) ||
273 !(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url))
274 ) {
275 return;
276 }
277
278 // check first if the request has finished and is tracked by an existing span which should now end
279 if (handlerData.endTimestamp) {
280 const spanId = xhr.__sentry_xhr_span_id__;
281 if (!spanId) return;
282
283 const span = spans[spanId];
284 if (span) {
285 span.setHttpStatus(sentryXhrData.status_code);
286 span.finish();
287
288 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
289 delete spans[spanId];
290 }
291 return;
292 }
293
294 const currentSpan = getCurrentHub().getScope().getSpan();
295 const activeTransaction = currentSpan && currentSpan.transaction;
296
297 if (currentSpan && activeTransaction) {
298 const span = currentSpan.startChild({
299 data: {
300 ...sentryXhrData.data,
301 type: 'xhr',
302 'http.method': sentryXhrData.method,
303 url: sentryXhrData.url,
304 },
305 description: `${sentryXhrData.method} ${sentryXhrData.url}`,
306 op: 'http.client',
307 });
308
309 xhr.__sentry_xhr_span_id__ = span.spanId;
310 spans[xhr.__sentry_xhr_span_id__] = span;
311
312 if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
313 try {
314 xhr.setRequestHeader('sentry-trace', span.toTraceparent());
315
316 const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext();
317 const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
318
319 if (sentryBaggageHeader) {
320 // From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
321 // We can therefore simply set a baggage header without checking what was there before
322 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
323 xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
324 }
325 } catch (_) {
326 // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
327 }
328 }
329
330 return span;
331 }
332}
333
334export { DEFAULT_TRACE_PROPAGATION_TARGETS, addTracingHeadersToFetchRequest, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, shouldAttachHeaders };
335//# sourceMappingURL=request.js.map