123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
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<string, unknown> {
|
|
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
|
|
}
|