Skip to content

Commit

Permalink
ref(tracing-internal): Export fetch instrumentation (#9473)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Nov 17, 2023
1 parent f48b697 commit 1e2bf6e
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 239 deletions.
6 changes: 1 addition & 5 deletions packages/tracing-internal/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ export * from '../exports';
export type { RequestInstrumentationOptions } from './request';

export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing';
export {
instrumentOutgoingRequests,
defaultRequestInstrumentationOptions,
addTracingHeadersToFetchRequest,
} from './request';
export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request';

export {
addPerformanceInstrumentationHandler,
Expand Down
208 changes: 4 additions & 204 deletions packages/tracing-internal/src/browser/request.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* eslint-disable max-lines */
import { getCurrentHub, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core';
import type { Client, Scope, Span } from '@sentry/types';
import type { HandlerDataFetch, Span } from '@sentry/types';
import {
addInstrumentationHandler,
BAGGAGE_HEADER_NAME,
browserPerformanceTimeOrigin,
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
isInstanceOf,
SENTRY_XHR_DATA_KEY,
stringMatchesSomePattern,
} from '@sentry/utils';

import { instrumentFetchRequest } from '../common/fetch';
import { addPerformanceInstrumentationHandler } from './instrument';

export const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
Expand Down Expand Up @@ -66,26 +66,6 @@ export interface RequestInstrumentationOptions {
shouldCreateSpanForRequest?(this: void, url: string): boolean;
}

/** Data returned from fetch callback */
export interface FetchData {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any[]; // the arguments passed to the fetch call itself
fetchData?: {
method: string;
url: string;
// span_id
__span?: string;
};

// TODO Should this be unknown instead? If we vendor types, make it a Response
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response?: any;
error?: unknown;

startTimestamp: number;
endTimestamp?: number;
}

/** Data returned from XHR request */
export interface XHRData {
xhr?: {
Expand All @@ -105,17 +85,6 @@ export interface XHRData {
endTimestamp?: number;
}

type PolymorphicRequestHeaders =
| Record<string, string | undefined>
| Array<[string, string]>
// the below is not preicsely the Header type used in Request, but it'll pass duck-typing
| {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
append: (key: string, value: string) => void;
get: (key: string) => string | null | undefined;
};

export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions = {
traceFetch: true,
traceXHR: true,
Expand Down Expand Up @@ -154,8 +123,8 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
const spans: Record<string, Span> = {};

if (traceFetch) {
addInstrumentationHandler('fetch', (handlerData: FetchData) => {
const createdSpan = fetchCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
addInstrumentationHandler('fetch', (handlerData: HandlerDataFetch) => {
const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
if (enableHTTPTimings && createdSpan) {
addHTTPTimings(createdSpan);
}
Expand Down Expand Up @@ -276,175 +245,6 @@ export function shouldAttachHeaders(url: string, tracePropagationTargets: (strin
return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
}

/**
* Create and track fetch request spans
*
* @returns Span if a span was created, otherwise void.
*/
export function fetchCallback(
handlerData: FetchData,
shouldCreateSpan: (url: string) => boolean,
shouldAttachHeaders: (url: string) => boolean,
spans: Record<string, Span>,
): Span | undefined {
if (!hasTracingEnabled() || !handlerData.fetchData) {
return undefined;
}

const shouldCreateSpanResult = shouldCreateSpan(handlerData.fetchData.url);

if (handlerData.endTimestamp && shouldCreateSpanResult) {
const spanId = handlerData.fetchData.__span;
if (!spanId) return;

const span = spans[spanId];
if (span) {
if (handlerData.response) {
// TODO (kmclb) remove this once types PR goes through
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
span.setHttpStatus(handlerData.response.status);

const contentLength: string =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');

const contentLengthNum = parseInt(contentLength);
if (contentLengthNum > 0) {
span.setData('http.response_content_length', contentLengthNum);
}
} else if (handlerData.error) {
span.setStatus('internal_error');
}
span.finish();

// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete spans[spanId];
}
return undefined;
}

const hub = getCurrentHub();
const scope = hub.getScope();
const client = hub.getClient();
const parentSpan = scope.getSpan();

const { method, url } = handlerData.fetchData;

const span =
shouldCreateSpanResult && parentSpan
? parentSpan.startChild({
data: {
url,
type: 'fetch',
'http.method': method,
},
description: `${method} ${url}`,
op: 'http.client',
origin: 'auto.http.browser',
})
: undefined;

if (span) {
handlerData.fetchData.__span = span.spanId;
spans[span.spanId] = span;
}

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

// In case the user hasn't set the second argument of a fetch call we default it to `{}`.
handlerData.args[1] = handlerData.args[1] || {};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: { [key: string]: any } = handlerData.args[1];

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
options.headers = addTracingHeadersToFetchRequest(request, client, scope, options, span);
}

return span;
}

/**
* Adds sentry-trace and baggage headers to the various forms of fetch headers
*/
export function addTracingHeadersToFetchRequest(
request: string | unknown, // unknown is actually type Request but we can't export DOM types from this package,
client: Client,
scope: Scope,
options: {
headers?:
| {
[key: string]: string[] | string | undefined;
}
| PolymorphicRequestHeaders;
},
requestSpan?: Span,
): PolymorphicRequestHeaders | undefined {
const span = requestSpan || scope.getSpan();

const transaction = span && span.transaction;

const { traceId, sampled, dsc } = scope.getPropagationContext();

const sentryTraceHeader = span ? span.toTraceparent() : generateSentryTraceHeader(traceId, undefined, sampled);
const dynamicSamplingContext = transaction
? transaction.getDynamicSamplingContext()
: dsc
? dsc
: getDynamicSamplingContextFromClient(traceId, client, scope);

const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);

const headers =
typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : options.headers;

if (!headers) {
return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
} else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
const newHeaders = new Headers(headers as Headers);

newHeaders.append('sentry-trace', sentryTraceHeader);

if (sentryBaggageHeader) {
// If the same header is appended multiple times the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}

return newHeaders as PolymorphicRequestHeaders;
} else if (Array.isArray(headers)) {
const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];

if (sentryBaggageHeader) {
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]);
}

return newHeaders as PolymorphicRequestHeaders;
} else {
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
const newBaggageHeaders: string[] = [];

if (Array.isArray(existingBaggageHeader)) {
newBaggageHeaders.push(...existingBaggageHeader);
} else if (existingBaggageHeader) {
newBaggageHeaders.push(existingBaggageHeader);
}

if (sentryBaggageHeader) {
newBaggageHeaders.push(sentryBaggageHeader);
}

return {
...(headers as Exclude<typeof headers, Headers>),
'sentry-trace': sentryTraceHeader,
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
};
}
}

/**
* Create and track xhr request spans
*
Expand Down

0 comments on commit 1e2bf6e

Please sign in to comment.