import type { Duration, XhrCompleteContext, XhrStartContext, ClocksState, FetchStartContext, FetchResolveContext, ContextManager, SessionManager, Observable, BufferedData, Subscription, } from '@datadog/browser-core' import { RequestType, ResponseBodyAction, BufferedDataType, elapsed, initFetchObservable, timeStampNow, initXhrObservable, } from '@datadog/browser-core' import type { RumConfiguration } from './configuration' import type { LifeCycle } from './lifeCycle' import { LifeCycleEventType } from './lifeCycle' import { isAllowedRequestUrl } from './resource/resourceUtils' import type { Tracer } from './tracing/tracer' import { startTracer } from './tracing/tracer' import type { SpanIdentifier, TraceIdentifier } from './tracing/identifier' import { findGraphQlConfiguration } from './resource/graphql' export interface CustomContext { requestIndex: number spanId?: SpanIdentifier traceId?: TraceIdentifier traceSampled?: boolean } export interface RumFetchStartContext extends FetchStartContext, CustomContext {} export interface RumFetchResolveContext extends FetchResolveContext, CustomContext {} export interface RumXhrStartContext extends XhrStartContext, CustomContext {} export interface RumXhrCompleteContext extends XhrCompleteContext, CustomContext {} export interface RequestStartEvent { requestIndex: number url: string } export interface RequestCompleteEvent { requestIndex: number type: RequestType method: string url: string status: number responseType?: string startClocks: ClocksState duration: Duration spanId?: SpanIdentifier traceId?: TraceIdentifier traceSampled?: boolean xhr?: XMLHttpRequest response?: Response input?: unknown init?: RequestInit error?: Error isAborted: boolean isAbortedOnStart: boolean handlingStack?: string requestBody?: unknown responseBody?: string } let nextRequestIndex = 1 export function startRequestCollection( lifeCycle: LifeCycle, configuration: RumConfiguration, sessionManager: SessionManager, userContext: ContextManager, accountContext: ContextManager, bufferedDataObservable: Observable ) { const tracer = startTracer(configuration, sessionManager, userContext, accountContext) trackXhr(lifeCycle, configuration, tracer, bufferedDataObservable) trackFetch(lifeCycle, configuration, tracer, bufferedDataObservable) } export function trackXhr( lifeCycle: LifeCycle, configuration: RumConfiguration, tracer: Tracer, bufferedDataObservable: Observable ) { const subscriptions: Subscription[] = [] subscriptions.push( initXhrObservable(configuration).subscribe((context) => { if (context.state === 'start' && isAllowedRequestUrl(context.url)) { tracer.traceXhr(context, context.xhr) assignRequestIndex(context as RumXhrStartContext) } }) ) subscriptions.push( bufferedDataObservable.subscribe(({ data, type }) => { if (type !== BufferedDataType.XHR) { return } const context = data as RumXhrStartContext | RumXhrCompleteContext if (!isAllowedRequestUrl(context.url)) { return } switch (context.state) { case 'start': assignRequestIndex(context) lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, { requestIndex: context.requestIndex, url: context.url, }) break case 'complete': tracer.clearTracingIfNeeded(context) lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, { ...context, type: RequestType.XHR, isAbortedOnStart: false, }) break } }) ) return { stop: () => subscriptions.forEach((subscription) => subscription.unsubscribe()) } } export function trackFetch( lifeCycle: LifeCycle, configuration: RumConfiguration, tracer: Tracer, bufferedDataObservable: Observable ) { const subscriptions: Subscription[] = [] subscriptions.push( initFetchObservable({ responseBodyAction: (context) => { if (findGraphQlConfiguration(context.url, configuration)?.trackResponseErrors) { return ResponseBodyAction.COLLECT } return ResponseBodyAction.IGNORE }, }).subscribe((context) => { if (context.state === 'start' && isAllowedRequestUrl(context.url)) { tracer.traceFetch(context) assignRequestIndex(context as RumFetchStartContext) } }) ) subscriptions.push( bufferedDataObservable.subscribe(({ data, type }) => { if (type !== BufferedDataType.FETCH) { return } const context = data as RumFetchResolveContext | RumFetchStartContext if (!isAllowedRequestUrl(context.url)) { return } switch (context.state) { case 'start': assignRequestIndex(context) lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, { requestIndex: context.requestIndex, url: context.url, }) break case 'resolve': tracer.clearTracingIfNeeded(context) lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, { ...context, duration: elapsed(context.startClocks.timeStamp, timeStampNow()), type: RequestType.FETCH, requestBody: context.init?.body, }) break } }) ) return { stop: () => subscriptions.forEach((subscription) => subscription.unsubscribe()) } } function assignRequestIndex(context: CustomContext) { if (!context.requestIndex) { context.requestIndex = nextRequestIndex++ } }