UNPKG

datadog-ux-utils

Version:

Datadog RUM focused UX & performance toolkit: API guards (retry, breaker, rate), React telemetry (error boundary, profiler, Suspense), web vitals & resource observers, offline queues.

1 lines 25.5 kB
{"version":3,"file":"retry-BGWdGkEk.cjs","sources":["../src/api/circuitBreaker.ts","../src/api/ddFetch.ts","../src/api/dedupe.ts","../src/api/responseSize.ts","../src/api/retry.ts"],"sourcesContent":["/**\n * @file circuitBreaker.ts\n * @description Implements a circuit breaker pattern to protect API calls from repeated failures and overloads.\n */\nimport { addAction } from \"../datadog.ts\";\n\n/**\n * Possible states for a circuit breaker.\n * - \"closed\": normal operation\n * - \"open\": requests are blocked due to repeated failures\n * - \"half\": limited requests allowed to test recovery\n */\ntype State = \"closed\" | \"open\" | \"half\";\n/**\n * Default configuration for circuit breakers.\n * - `failureThreshold`: Number of failures before opening the circuit.\n * - `cooldownMs`: Time to wait before allowing requests after opening.\n * - `halfOpenMax`: Number of requests allowed in half-open state.\n */\nconst defaults = { failureThreshold: 5, cooldownMs: 10000, halfOpenMax: 2 };\n\n/**\n * Internal map of breaker instances by key.\n */\nconst breakers = new Map<\n string,\n {\n state: State;\n failures: number;\n nextTry: number;\n halfOpenInFlight: number;\n cfg: Required<BreakerCfg>;\n }\n>();\n\n/**\n * Configuration options for the circuit breaker utility.\n *\n * @property failureThreshold - Number of failures before opening the circuit.\n * @property cooldownMs - Time to wait before allowing requests after opening.\n * @property halfOpenMax - Number of requests allowed in half-open state.\n */\nexport type BreakerCfg = typeof defaults;\n\n/**\n * Wraps an async operation with a circuit breaker.\n *\n * @template T\n * @param key - Unique key for the breaker instance (e.g., \"GET:/api/patients\").\n * @param fn - Function returning a Promise to protect.\n * @param cfg - Partial breaker configuration (optional).\n * @returns The resolved value from the operation, or throws if the breaker is open.\n *\n * @example\n * // Protect a fetch call with a circuit breaker\n * const result = await wrapWithBreaker('GET:/api/patients', () => fetch('/api/patients'));\n *\n * @example\n * // Custom thresholds\n * const data = await wrapWithBreaker('POST:/api/orders', () => postOrder(), { failureThreshold: 3, cooldownMs: 5000 });\n */\nexport async function wrapWithBreaker<T>(\n key: string,\n fn: () => Promise<T>,\n cfg: Partial<BreakerCfg> = {}\n) {\n const b = getBreaker(key, cfg);\n const now = Date.now();\n\n if (b.state === \"open\") {\n if (now < b.nextTry) {\n throw mkErr(\"open\", key, b.nextTry);\n } else {\n b.state = \"half\";\n b.halfOpenInFlight = 0;\n addAction(\"api_circuit_half_open\", { key });\n }\n }\n if (b.state === \"half\" && b.halfOpenInFlight >= b.cfg.halfOpenMax) {\n throw mkErr(\"half-saturated\", key, b.nextTry);\n }\n\n if (b.state === \"half\") b.halfOpenInFlight++;\n\n try {\n const res = await fn();\n onSuccess(b, key);\n return res;\n } catch (e) {\n onFailure(b, key);\n throw e;\n } finally {\n if (b.state === \"half\") b.halfOpenInFlight--;\n }\n}\n\n/**\n * Gets or creates a breaker instance for the given key.\n * @param key - Unique breaker key.\n * @param cfg - Partial configuration to override defaults.\n * @returns The breaker instance.\n */\nfunction getBreaker(key: string, cfg: Partial<BreakerCfg>) {\n let b = breakers.get(key);\n if (!b) {\n b = {\n state: \"closed\",\n failures: 0,\n nextTry: 0,\n halfOpenInFlight: 0,\n cfg: { ...defaults, ...cfg },\n };\n breakers.set(key, b);\n }\n return b;\n}\n/**\n * Handles a successful operation, resetting failures and closing the breaker if needed.\n * @param b - Breaker instance.\n * @param key - Breaker key.\n */\nfunction onSuccess(b: ReturnType<typeof getBreaker>, key: string) {\n b.failures = 0;\n if (b.state !== \"closed\") {\n b.state = \"closed\";\n addAction(\"api_circuit_closed\", { key });\n }\n}\n/**\n * Handles a failed operation, incrementing failures and opening the breaker if threshold is reached.\n * @param b - Breaker instance.\n * @param key - Breaker key.\n */\nfunction onFailure(b: ReturnType<typeof getBreaker>, key: string) {\n b.failures++;\n if (b.state === \"half\" || b.failures >= b.cfg.failureThreshold) {\n b.state = \"open\";\n b.nextTry = Date.now() + b.cfg.cooldownMs;\n addAction(\"api_circuit_open\", { key, cooldown_ms: b.cfg.cooldownMs });\n }\n}\n/**\n * Creates a circuit breaker error with a descriptive message.\n * @param state - Breaker state (\"open\", \"half-saturated\", etc.).\n * @param key - Breaker key.\n * @param until - Timestamp until which the breaker remains open.\n * @returns Error instance.\n */\nfunction mkErr(state: string, key: string, until: number) {\n const e = new Error(\n `Circuit ${state} for ${key} until ${new Date(until).toISOString()}`\n );\n e.name = \"ApiCircuitOpenError\";\n return e;\n}\n","/**\n * @file ddFetch.ts\n * @description Wraps the fetch API to add Datadog RUM actions for slow API calls and errors.\n */\nimport { datadogRum } from \"@datadog/browser-rum\";\nimport { getUxConfig } from \"../config.ts\";\n\n/**\n * Wraps the fetch API to add Datadog RUM actions for slow API calls and errors.\n * @param input - The request info or URL.\n * @param init - Optional request initialization options.\n * @returns The fetch response.\n */\nexport const ddFetch = async (input: RequestInfo | URL, init?: RequestInit) => {\n const cfg = getUxConfig();\n const start = performance.now();\n let resp: Response;\n\n try {\n resp = await fetch(input, init);\n\n const dur = performance.now() - start;\n if (dur >= cfg.apiSlowMs) {\n maybeAction(\n \"api_slow\",\n {\n url: String(\n typeof input === \"string\" ? input : (input as URL).toString()\n ),\n method: init?.method ?? \"GET\",\n duration_ms: Math.round(dur),\n status: resp.status,\n },\n cfg.actionSampleRate\n );\n }\n\n // size check done in a separate helper to avoid unnecessary work\n // and only if enabled\n return resp;\n } catch (err) {\n maybeError(err, {\n where: \"ddFetch\",\n url: String(\n typeof input === \"string\" ? input : (input as URL).toString()\n ),\n });\n throw err;\n }\n};\n\n/**\n * Times a promise and adds a Datadog RUM action if it is slow.\n * @param label - Label for the promise.\n * @param p - The promise to time.\n * @param meta - Optional metadata to include in the RUM action.\n * @returns The resolved value of the promise.\n */\nexport const timePromise = async <T>(\n label: string,\n p: Promise<T>,\n meta?: Record<string, unknown>\n) => {\n const cfg = getUxConfig();\n const start = performance.now();\n try {\n const val = await p;\n const dur = performance.now() - start;\n if (dur >= cfg.apiSlowMs) {\n maybeAction(\n \"promise_slow\",\n { label, duration_ms: Math.round(dur), ...meta },\n cfg.actionSampleRate\n );\n }\n return val;\n } catch (err) {\n maybeError(err, { label, ...meta });\n throw err;\n }\n};\n\nexport const maybeAction = (\n name: string,\n attrs: Record<string, unknown>,\n rate: number\n) => {\n if (Math.random() * 100 < rate) datadogRum.addAction(name, attrs);\n};\n\nexport const maybeError = (err: unknown, ctx?: Record<string, unknown>) => {\n datadogRum.addError(err as Error, ctx);\n};\n","/**\n * @file dedupe.ts\n * @description Deduplicates identical async calls and optionally caches results for a short TTL, reducing redundant network traffic and improving efficiency.\n */\nimport { addAction } from \"../datadog.ts\";\n\ntype State = \"pending\" | \"cached\";\n\ntype Entry<T> = {\n promise: Promise<T>;\n expireAt: number; // epoch ms when cache entry should be evicted (0 for no cache)\n state: State; // \"pending\" = in-flight, \"cached\" = resolved and cached\n startedAt: number; // when the op started (for telemetry)\n};\n\nconst inFlight = new Map<string, Entry<any>>();\n\n/**\n * Telemetry options for deduplication reporting.\n */\nimport { DedupeTelemetry, DedupeOptions } from \"../types/types.ts\";\n\n/**\n * Deduplicate identical async calls and (optionally) cache results for a short TTL.\n *\n * Backward-compatible signatures:\n * dedupe(key, op) // no caching, no telemetry\n * dedupe(key, op, 5000) // ttlMs as number\n * dedupe(key, op, { ttlMs: 5000, report: true }) // with options & telemetry\n *\n * @param key - Unique key for the operation (e.g., 'GET /api/items?page=1')\n * @param op - Function that returns a Promise\n * @param optionsOrTtl - Either a number (ttlMs) or an options object\n * @returns The resolved value from the operation, or cached value if available.\n *\n * @example\n * // Deduplicate concurrent fetches\n * async function loadPatients() {\n * return dedupe('GET:/api/patients', async () => {\n * const resp = await fetch('/api/patients');\n * return resp.json();\n * }, 5000); // 5s cache after resolution\n * }\n * // Two simultaneous calls will share the same network request\n * const [a, b] = await Promise.all([loadPatients(), loadPatients()]);\n */\nexport function dedupe<T>(\n key: string,\n op: () => Promise<T>,\n optionsOrTtl?: number | DedupeOptions\n): Promise<T> {\n const { ttlMs, report } = normalizeOptions(optionsOrTtl);\n const now = Date.now();\n\n const existing = inFlight.get(key) as Entry<T> | undefined;\n if (existing && (existing.state === \"pending\" || existing.expireAt > now)) {\n // We have a coalesce or a cache hit\n maybeReport(report, existing.state, key, {\n age_ms: now - existing.startedAt,\n ttl_ms: existing.expireAt ? existing.expireAt - now : 0,\n });\n return existing.promise;\n }\n\n // Start fresh operation\n const startedAt = now;\n const entry: Entry<T> = {\n promise: Promise.resolve().then(op), // ensure async boundary\n expireAt: now + (ttlMs > 0 ? ttlMs : 0),\n state: \"pending\",\n startedAt,\n };\n\n inFlight.set(key, entry);\n\n entry.promise = entry.promise\n .then((res) => {\n if (ttlMs > 0) {\n // Convert to cached entry\n entry.state = \"cached\";\n entry.expireAt = Date.now() + ttlMs;\n entry.promise = Promise.resolve(res);\n\n // Schedule eviction\n setTimeout(() => {\n const cur = inFlight.get(key);\n if (cur && cur.state === \"cached\" && cur.expireAt <= Date.now()) {\n inFlight.delete(key);\n }\n }, ttlMs + 25);\n } else {\n // No caching: remove immediately after resolution\n inFlight.delete(key);\n }\n return res;\n })\n .catch((err) => {\n // On failure, drop the entry so callers can retry\n inFlight.delete(key);\n throw err;\n });\n\n return entry.promise;\n}\n\n/** Clear all deduped entries, or a single key if provided. */\nexport function clearDedupe(key?: string) {\n if (key) inFlight.delete(key);\n else inFlight.clear();\n}\n\n/** Number of active entries (pending + cached). */\nexport function dedupeSize() {\n return inFlight.size;\n}\n\n/* ---------------- internals ---------------- */\n\nfunction normalizeOptions(opt?: number | DedupeOptions): {\n ttlMs: number;\n report: Required<Exclude<DedupeTelemetry, boolean>> & { enabled: boolean };\n} {\n if (typeof opt === \"number\") {\n return {\n ttlMs: opt,\n report: { enabled: false, sampleRate: 10, actionName: \"api_dedupe_hit\" },\n };\n }\n const ttlMs = opt?.ttlMs ?? 0;\n\n // report option may be boolean or object\n let enabled = false;\n let sampleRate = 10;\n let actionName = \"api_dedupe_hit\";\n\n if (opt?.report === true) enabled = true;\n else if (typeof opt?.report === \"object\") {\n enabled = true;\n if (typeof opt.report.sampleRate === \"number\")\n sampleRate = clampPct(opt.report.sampleRate);\n if (opt.report.actionName) actionName = opt.report.actionName;\n }\n\n return { ttlMs, report: { enabled, sampleRate, actionName } };\n}\n\nfunction maybeReport(\n report: { enabled: boolean; sampleRate: number; actionName: string },\n kind: State,\n key: string,\n extra?: Record<string, unknown>\n) {\n if (!report.enabled) return;\n if (Math.random() * 100 >= report.sampleRate) return;\n try {\n addAction(report.actionName, { key, kind, ...extra }, report.sampleRate);\n } catch {\n // swallow — telemetry must not affect behavior\n }\n}\n\nfunction clampPct(n: number) {\n if (Number.isNaN(n)) return 0;\n return Math.max(0, Math.min(100, Math.round(n)));\n}\n\n/**\n EXAMPLE\n\n // Deduplicate concurrent fetches\nasync function loadPatients() {\n return dedupe('GET:/api/patients', async () => {\n const resp = await fetch('/api/patients');\n return resp.json();\n }, 5000); // 5s cache after resolution\n}\n\n// Two simultaneous calls will share the same network request\nconst [a, b] = await Promise.all([loadPatients(), loadPatients()]);\n */\n","/**\n * @file responseSize.ts\n * @description Monitors and checks the size of API responses to prevent large payloads and report them to Datadog RUM.\n */\nimport { getUxConfig } from \"../config.ts\";\nimport { datadogRum } from \"@datadog/browser-rum\";\n\n/**\n * Checks the response size and adds a Datadog RUM action if the payload is large.\n * @param resp - The fetch response to check.\n * @param hintUrl - Optional URL for context in the RUM action.\n * @returns The original response.\n */\nexport const withResponseSizeCheck = async (\n resp: Response,\n hintUrl?: string\n) => {\n const cfg = getUxConfig();\n if (!cfg.captureResponseSize) return resp;\n\n try {\n // Prefer Content-Length when present\n const header = resp.headers.get(\"content-length\");\n let sizeBytes = header ? parseInt(header, 10) : NaN;\n\n if (!Number.isFinite(sizeBytes)) {\n // Fallback only if body is not a stream we must preserve\n const clone = resp.clone();\n const buf = await clone.arrayBuffer();\n sizeBytes = buf.byteLength;\n }\n\n const kb = Math.round(sizeBytes / 1024);\n if (kb >= cfg.apiLargeKb) {\n datadogRum.addAction(\"api_large_payload\", {\n url: hintUrl,\n size_kb: kb,\n threshold_kb: cfg.apiLargeKb,\n status: resp.status,\n });\n }\n } catch {\n // silent — measuring must never break the app\n }\n return resp;\n};\n","/**\n * @file retry.ts\n * @description Adds retry logic to async operations and API calls, with exponential backoff and optional telemetry reporting.\n */\nimport { addAction, addError } from \"../datadog.ts\";\n\nexport type RetryConfig = {\n /** Number of retries after the initial attempt. Default 3 */\n retries?: number;\n /** Base delay in ms between retries. Default 200 */\n baseMs?: number;\n /** Maximum delay in ms. Default 5000 */\n maxMs?: number;\n /** Exponential backoff factor. Default 2 */\n factor?: number;\n /** Add +/- 0–baseMs random jitter to delay. Default true */\n jitter?: boolean;\n /** Only retry on errors passing this predicate. Default: retry all */\n shouldRetry?: (err: unknown, attempt: number) => boolean;\n /** Optional DataDog reporting. Default false */\n report?: boolean | { sampleRate?: number; actionName?: string };\n};\n\nconst DEFAULTS: Required<RetryConfig> = {\n retries: 3,\n baseMs: 200,\n maxMs: 5000,\n factor: 2,\n jitter: true,\n shouldRetry: () => true,\n report: false,\n};\n\n/**\n * Retry an async operation with backoff/jitter.\n * @param label Unique label for telemetry (e.g., \"GET /api/items\")\n * @param op Function returning a Promise\n * @param cfg RetryConfig\n */\n/**\n * Retry an async operation with backoff/jitter.\n *\n * @param label - Unique label for telemetry (e.g., \"GET /api/items\")\n * @param op - Function returning a Promise\n * @param cfg - RetryConfig\n * @returns The resolved value from the operation, or throws after all retries fail.\n *\n * @example\n * // Retry GET request up to 5 times, with exponential backoff starting at 300ms\n * const data = await retry(\"GET /api/patients\", async () => {\n * const res = await fetch(\"/api/patients\");\n * if (!res.ok) throw new Error(`Status ${res.status}`);\n * return res.json();\n * }, {\n * retries: 5,\n * baseMs: 300,\n * jitter: true,\n * report: true // enable DataDog telemetry at default 10% sampling\n * });\n */\nexport async function retry<T>(\n label: string,\n op: () => Promise<T>,\n cfg?: RetryConfig\n): Promise<T> {\n const { retries, baseMs, maxMs, factor, jitter, shouldRetry, report } = {\n ...DEFAULTS,\n ...cfg,\n };\n\n const reportCfg =\n typeof report === \"object\"\n ? {\n enabled: true,\n sampleRate: report.sampleRate ?? 10,\n actionName: report.actionName ?? \"api_retry\",\n }\n : report === true\n ? { enabled: true, sampleRate: 10, actionName: \"api_retry\" }\n : { enabled: false, sampleRate: 0, actionName: \"\" };\n\n let attempt = 0;\n let lastErr: unknown;\n\n while (attempt <= retries) {\n try {\n if (attempt > 0 && reportCfg.enabled) {\n maybeReport(reportCfg, label, attempt);\n }\n return await op();\n } catch (err) {\n lastErr = err;\n if (attempt >= retries || !shouldRetry(err, attempt)) {\n if (reportCfg.enabled) {\n addError(err, { label, attempt, final: true }, reportCfg.sampleRate);\n }\n throw err;\n }\n const delay = computeDelay(baseMs, factor, attempt, maxMs, jitter);\n await sleep(delay);\n attempt++;\n }\n }\n\n // We should never reach here\n throw lastErr;\n}\n\nfunction computeDelay(\n base: number,\n factor: number,\n attempt: number,\n max: number,\n jitter: boolean\n) {\n let delay = Math.min(base * Math.pow(factor, attempt), max);\n if (jitter) {\n const rand = Math.floor(Math.random() * base);\n delay += Math.random() < 0.5 ? -rand : rand;\n delay = Math.max(0, delay);\n }\n return delay;\n}\n\nfunction sleep(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction maybeReport(\n cfg: { enabled: boolean; sampleRate: number; actionName: string },\n label: string,\n attempt: number\n) {\n if (!cfg.enabled) return;\n if (Math.random() * 100 >= cfg.sampleRate) return;\n addAction(cfg.actionName, { label, attempt }, cfg.sampleRate);\n}\n\n/**\nEXAMPLE\n\n// Retry GET request up to 5 times, with exponential backoff starting at 300ms\nconst data = await retry(\"GET /api/patients\", async () => {\n const res = await fetch(\"/api/patients\");\n if (!res.ok) throw new Error(`Status ${res.status}`);\n return res.json();\n}, {\n retries: 5,\n baseMs: 300,\n jitter: true,\n report: true // enable DataDog telemetry at default 10% sampling\n});\n*/\n"],"names":["defaults","breakers","wrapWithBreaker","key","fn","cfg","b","getBreaker","now","mkErr","res","onSuccess","e","onFailure","addAction","state","until","ddFetch","input","init","getUxConfig","start","resp","dur","maybeAction","err","maybeError","timePromise","label","p","meta","val","name","attrs","rate","datadogRum","ctx","inFlight","dedupe","op","optionsOrTtl","ttlMs","report","normalizeOptions","existing","maybeReport","startedAt","entry","cur","clearDedupe","dedupeSize","opt","enabled","sampleRate","actionName","clampPct","kind","extra","n","withResponseSizeCheck","hintUrl","header","sizeBytes","kb","DEFAULTS","retry","retries","baseMs","maxMs","factor","jitter","shouldRetry","reportCfg","attempt","lastErr","delay","computeDelay","sleep","base","max","rand","ms","resolve"],"mappings":"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"}