type RequestIds = { requestId: string correlationId?: string traceparent?: string } const LAST_IDS_STORAGE_KEY = 'control:last_request_ids' export class ApiError extends Error { status: number requestId: string correlationId?: string traceparent?: string constructor(args: { status: number message: string requestId: string correlationId?: string traceparent?: string }) { super(args.message) this.name = 'ApiError' this.status = args.status this.requestId = args.requestId this.correlationId = args.correlationId this.traceparent = args.traceparent } } const state: { last?: RequestIds } = {} function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } function loadLastIds(): RequestIds | undefined { try { const raw = localStorage.getItem(LAST_IDS_STORAGE_KEY) if (!raw) return undefined const parsed = JSON.parse(raw) as unknown if (isRecord(parsed) && typeof parsed.requestId === 'string') { const correlationId = typeof parsed.correlationId === 'string' ? parsed.correlationId : undefined const traceparent = typeof parsed.traceparent === 'string' ? parsed.traceparent : undefined return { requestId: parsed.requestId, correlationId, traceparent } } } catch { return undefined } return undefined } function persistLastIds(ids: RequestIds) { try { localStorage.setItem(LAST_IDS_STORAGE_KEY, JSON.stringify(ids)) } catch { return } } function newRequestId(): string { if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { return crypto.randomUUID() } return `${Date.now()}-${Math.random().toString(16).slice(2)}` } export function getLastRequestIds(): RequestIds | undefined { return state.last ?? loadLastIds() } type ApiRequestInit = RequestInit & { correlationId?: string traceparent?: string useLastCorrelationId?: boolean useLastTraceparent?: boolean } export async function apiFetch( input: RequestInfo | URL, init?: ApiRequestInit, ) { const requestId = newRequestId() const headers = new Headers(init?.headers) headers.set('x-request-id', requestId) const last = getLastRequestIds() const correlationId = init?.correlationId ?? (init?.useLastCorrelationId ? last?.correlationId : undefined) const traceparent = init?.traceparent ?? (init?.useLastTraceparent ? last?.traceparent : undefined) if (correlationId) headers.set('x-correlation-id', correlationId) if (traceparent) headers.set('traceparent', traceparent) const res = await fetch(input, { ...init, headers }) const resCorrelationId = res.headers.get('x-correlation-id') ?? correlationId ?? undefined const resTraceparent = res.headers.get('traceparent') ?? traceparent ?? undefined const ids = { requestId, correlationId: resCorrelationId, traceparent: resTraceparent } state.last = ids persistLastIds(ids) if (!res.ok) { const text = await res.text().catch(() => '') const err = new ApiError({ status: res.status, requestId, correlationId: resCorrelationId, traceparent: resTraceparent, message: `API error ${res.status}${text ? `: ${text}` : ''} (request_id=${requestId}${ resCorrelationId ? ` correlation_id=${resCorrelationId}` : '' })`, }) throw err } return res }