import type { EndpointBuilder } from '../domain/configuration' import type { Context } from '../tools/serialisation/context' import { fetch } from '../browser/fetch' import { monitor, monitorError } from '../tools/monitor' import type { RawError } from '../domain/error/error.types' import { Observable } from '../tools/observable' import { ONE_KIBI_BYTE } from '../tools/utils/byteUtils' import { newRetryState, sendWithRetryStrategy } from './sendWithRetryStrategy' /** * beacon payload max queue size implementation is 64kb * ensure that we leave room for logs, rum and potential other users */ export const RECOMMENDED_REQUEST_BYTES_LIMIT = 16 * ONE_KIBI_BYTE /** * Use POST request without content type to: * - avoid CORS preflight requests * - allow usage of sendBeacon * * multiple elements are sent separated by \n in order * to be parsed correctly without content type header */ export interface HttpRequest { observable: Observable> send(this: void, payload: Body): void sendOnExit(this: void, payload: Body): void } export interface HttpResponse extends Context { status: number type?: ResponseType } export interface BandwidthStats { ongoingByteCount: number ongoingRequestCount: number } export type HttpRequestEvent = | { // A request to send the given payload failed. (We may retry.) type: 'failure' bandwidth: BandwidthStats payload: Body } | { // The given payload was discarded because the request queue is full. type: 'queue-full' bandwidth: BandwidthStats payload: Body } | { // A request to send the given payload succeeded. type: 'success' bandwidth: BandwidthStats payload: Body } export interface Payload { data: string | FormData | Blob bytesCount: number retry?: RetryInfo encoding?: 'deflate' } export interface RetryInfo { count: number lastFailureStatus: number } export function createHttpRequest( endpointBuilders: EndpointBuilder[], reportError: (error: RawError) => void, bytesLimit: number = RECOMMENDED_REQUEST_BYTES_LIMIT ): HttpRequest { const observable = new Observable>() const retryState = newRetryState() return { observable, send: (payload: Body) => { for (const endpointBuilder of endpointBuilders) { sendWithRetryStrategy( payload, retryState, (payload, onResponse) => { fetchStrategy(endpointBuilder, payload, onResponse) }, endpointBuilder.trackType, reportError, observable ) } }, /** * Since fetch keepalive behaves like regular fetch on Firefox, * keep using sendBeaconStrategy on exit */ sendOnExit: (payload: Body) => { for (const endpointBuilder of endpointBuilders) { sendBeaconStrategy(endpointBuilder, bytesLimit, payload) } }, } } function sendBeaconStrategy(endpointBuilder: EndpointBuilder, bytesLimit: number, payload: Payload) { const canUseBeacon = !!navigator.sendBeacon && payload.bytesCount < bytesLimit if (canUseBeacon) { try { const beaconUrl = endpointBuilder.build('beacon', payload) const isQueued = navigator.sendBeacon(beaconUrl, payload.data) if (isQueued) { return } } catch (e) { reportBeaconError(e) } } fetchStrategy(endpointBuilder, payload) } let hasReportedBeaconError = false function reportBeaconError(e: unknown) { if (!hasReportedBeaconError) { hasReportedBeaconError = true monitorError(e) } } export function fetchStrategy( endpointBuilder: EndpointBuilder, payload: Payload, onResponse?: (r: HttpResponse) => void ) { const fetchUrl = endpointBuilder.build('fetch', payload) fetch(fetchUrl, { method: 'POST', body: payload.data, mode: 'cors' }) .then(monitor((response: Response) => onResponse?.({ status: response.status, type: response.type }))) .catch(monitor(() => onResponse?.({ status: 0 }))) }