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 16.7 kB
{"version":3,"file":"offlineQueue.persistent-CDVTmE70.cjs","sources":["../src/telemetry/offlineQueue.ts","../src/telemetry/offlineQueue.persistent.ts"],"sourcesContent":["/**\n * @file offlineQueue.ts\n * @description Buffers telemetry events in memory while offline and flushes them when connectivity returns. Does not persist to localStorage.\n */\nimport {\n addAction as realAddAction,\n addError as realAddError,\n} from \"../datadog.ts\";\n\ntype QueuedAction = {\n type: \"action\" | \"error\";\n name?: string; // only for type=action\n attrs?: Record<string, unknown>;\n err?: unknown; // only for type=error\n context?: Record<string, unknown>;\n sampleRate?: number;\n};\n\nlet queue: QueuedAction[] = [];\nlet maxBuffered = 200;\nlet installed = false;\n\n/**\n * Installs the in-memory offline telemetry queue.\n *\n * Buffers telemetry events (actions/errors) in memory while offline and flushes them when connectivity returns. Does not persist to localStorage.\n *\n * @param maxBufferedEvents - Maximum number of events to keep in memory while offline.\n * @returns Cleanup function to uninstall the queue wrapper and restore original telemetry functions.\n *\n * @example\n * ```ts\n * import { installTelemetryQueue } from \"datadog-ux-utils/telemetry\";\n *\n * installTelemetryQueue(300); // keep up to 300 events while offline\n * ```\n */\nexport function installTelemetryQueue(maxBufferedEvents = 200) {\n if (installed) return uninstall;\n installed = true;\n maxBuffered = maxBufferedEvents;\n\n // Monkey-patch our telemetry functions\n (globalThis as any).__dd_addAction = realAddAction;\n (globalThis as any).__dd_addError = realAddError;\n\n (globalThis as any).addAction = function (\n name: string,\n attrs?: Record<string, unknown>,\n sampleRate?: number\n ) {\n if (navigator.onLine) {\n realAddAction(name, attrs, sampleRate);\n } else {\n enqueue({ type: \"action\", name, attrs, sampleRate });\n }\n };\n\n (globalThis as any).addError = function (\n err: unknown,\n context?: Record<string, unknown>,\n sampleRate?: number\n ) {\n if (navigator.onLine) {\n realAddError(err, context, sampleRate);\n } else {\n enqueue({ type: \"error\", err, context, sampleRate });\n }\n };\n\n window.addEventListener(\"online\", flushQueue);\n\n return uninstall;\n}\n\n/**\n * Uninstalls the queue wrapper and restores original addAction/addError functions.\n *\n * @returns void\n */\nfunction uninstall() {\n if (!installed) return;\n installed = false;\n (globalThis as any).addAction = (globalThis as any).__dd_addAction;\n (globalThis as any).addError = (globalThis as any).__dd_addError;\n window.removeEventListener(\"online\", flushQueue);\n queue = [];\n}\n\nfunction enqueue(ev: QueuedAction) {\n if (queue.length >= maxBuffered) {\n queue.shift(); // drop oldest\n }\n queue.push(ev);\n}\n\nfunction flushQueue() {\n if (!navigator.onLine || !queue.length) return;\n const toFlush = queue.slice();\n queue = [];\n\n for (const ev of toFlush) {\n if (ev.type === \"action\") {\n realAddAction(ev.name!, ev.attrs, ev.sampleRate);\n } else {\n realAddError(ev.err, ev.context, ev.sampleRate);\n }\n }\n}\n\n/**\n * Example usage:\nimport { installTelemetryQueue } from \"dd-ux-utils/telemetry/offlineQueue\";\n\ninstallTelemetryQueue(300); // keep up to 300 events while offline\n*/\n","/**\n * @file offlineQueue.persistent.ts\n * @description Buffers telemetry events while offline, persists to localStorage, and flushes when connectivity returns. Designed to cooperate with your datadog.ts via two global hooks set by this module.\n *\n * Usage (after initDatadog):\n * installPersistentTelemetryQueue({ maxBuffered: 500, storageKey: 'dd_offline_v1' });\n */\n\ntype QueuedAction = {\n t: \"a\"; // action\n n: string; // name\n a?: Record<string, unknown>; // attrs\n s?: number; // sampleRate\n ts: number; // enqueue timestamp\n};\n\ntype QueuedError = {\n t: \"e\"; // error\n e: string; // serialized error\n c?: Record<string, unknown>; // context\n s?: number; // sampleRate\n ts: number; // enqueue timestamp\n};\n\ntype QueuedEvent = QueuedAction | QueuedError;\n\nimport { PersistentQueueOptions } from \"../types/types.ts\";\n\nconst DEFAULTS: Required<PersistentQueueOptions> = {\n storageKey: \"dd_offline_queue_v1\",\n maxBuffered: 400,\n byteCap: 1_500_000, // ~1.5MB, under typical 5MB LS limits\n flushOnInit: true,\n writeDebounceMs: 150,\n};\n\nlet cfg: Required<PersistentQueueOptions>;\nlet q: QueuedEvent[] = [];\nlet installed = false;\nlet writeTimer: number | null = null;\n\n// Real senders installed by datadog.ts via our global hooks\ntype SendAction = (\n name: string,\n attrs?: Record<string, unknown>,\n sampleRate?: number\n) => void;\ntype SendError = (\n err: Error,\n context?: Record<string, unknown>,\n sampleRate?: number\n) => void;\n\nfunction getSenders(): { sendAction?: SendAction; sendError?: SendError } {\n const g = globalThis as any;\n return {\n sendAction: g.__DD_SEND_ACTION__ as SendAction | undefined,\n sendError: g.__DD_SEND_ERROR__ as SendError | undefined,\n };\n}\n\n/**\n * Installs the persistent offline telemetry queue.\n *\n * Buffers telemetry events (actions/errors) while offline, persists to localStorage, and flushes when connectivity returns.\n * Safe to call multiple times. Returns an uninstall function.\n *\n * @param opts - Persistent queue configuration (see {@link PersistentQueueOptions}).\n * @returns Uninstall function to remove listeners and hooks.\n *\n * @example\n * ```ts\n * import { installPersistentTelemetryQueue } from \"datadog-ux-utils/telemetry\";\n *\n * installPersistentTelemetryQueue({\n * maxBuffered: 600,\n * storageKey: \"dd_offline_queue_v1\",\n * byteCap: 2_000_000,\n * flushOnInit: true,\n * writeDebounceMs: 120,\n * });\n * ```\n */\nexport function installPersistentTelemetryQueue(\n opts: PersistentQueueOptions = {}\n) {\n if (installed) return uninstall;\n installed = true;\n cfg = { ...DEFAULTS, ...opts };\n\n // Load existing queue from LS\n q = readLS(cfg.storageKey);\n\n // Expose enqueue hooks that datadog.ts will call if present\n const g = globalThis as any;\n g.__DD_ENQUEUE_ACTION__ = enqueueAction;\n g.__DD_ENQUEUE_ERROR__ = enqueueError;\n\n // Listen for connectivity changes\n window.addEventListener(\"online\", tryFlush);\n document.addEventListener(\"visibilitychange\", onVisChange);\n\n if (cfg.flushOnInit) {\n tryFlush();\n }\n\n return uninstall;\n}\n\n/**\n * Uninstalls the persistent telemetry queue and removes listeners/hooks. Leaves stored events intact in localStorage.\n *\n * @returns void\n */\nfunction uninstall() {\n if (!installed) return;\n installed = false;\n const g = globalThis as any;\n delete g.__DD_ENQUEUE_ACTION__;\n delete g.__DD_ENQUEUE_ERROR__;\n window.removeEventListener(\"online\", tryFlush);\n document.removeEventListener(\"visibilitychange\", onVisChange);\n}\n\nfunction onVisChange() {\n // If the page just became visible and we are online, try to flush.\n if (document.visibilityState === \"visible\") tryFlush();\n}\n\n/* ---------------- enqueue paths ---------------- */\n\nfunction enqueueAction(\n name: string,\n attrs?: Record<string, unknown>,\n sampleRate?: number\n) {\n if (navigator.onLine) {\n // If online, send immediately\n const { sendAction } = getSenders();\n if (sendAction) return sendAction(name, attrs, sampleRate);\n }\n // Else queue persistently\n push({ t: \"a\", n: name, a: attrs, s: sampleRate, ts: Date.now() });\n}\n\nfunction enqueueError(\n err: unknown,\n context?: Record<string, unknown>,\n sampleRate?: number\n) {\n if (navigator.onLine) {\n const { sendError } = getSenders();\n if (sendError) return sendError(asError(err), context, sampleRate);\n }\n push({\n t: \"e\",\n e: serializeError(err),\n c: context,\n s: sampleRate,\n ts: Date.now(),\n });\n}\n\nfunction push(ev: QueuedEvent) {\n q.push(ev);\n\n // Size controls\n if (q.length > cfg.maxBuffered) {\n q.splice(0, q.length - cfg.maxBuffered);\n }\n shrinkToByteCap();\n\n scheduleWrite();\n}\n\nfunction scheduleWrite() {\n if (writeTimer != null) return;\n writeTimer = setTimeout(() => {\n writeTimer = null;\n writeLS(cfg.storageKey, q);\n }, cfg.writeDebounceMs) as unknown as number;\n}\n\n/* ---------------- flushing ---------------- */\n\nfunction tryFlush() {\n if (!navigator.onLine || q.length === 0) return;\n\n const { sendAction, sendError } = getSenders();\n if (!sendAction || !sendError) return; // datadog not wired yet\n\n // Send a copy so that if sending throws we do not lose the queue\n const copy = q.slice();\n for (const ev of copy) {\n try {\n if (ev.t === \"a\") sendAction(ev.n, ev.a, ev.s);\n else sendError(deserializeError(ev.e), ev.c, ev.s);\n } catch {\n // If any send fails, stop flushing to avoid loops\n break;\n }\n // Remove from in-memory queue as we go\n q.shift();\n }\n\n // Persist the updated queue\n writeLS(cfg.storageKey, q);\n}\n\n/* ---------------- storage helpers ---------------- */\n\nfunction readLS(key: string): QueuedEvent[] {\n try {\n const raw = localStorage.getItem(key);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Basic shape validation\n return parsed.filter((v) => v && (v.t === \"a\" || v.t === \"e\"));\n } catch {\n return [];\n }\n}\n\nfunction writeLS(key: string, events: QueuedEvent[]) {\n try {\n localStorage.setItem(key, JSON.stringify(events));\n } catch {\n // Storage full. Try to shrink aggressively and retry once.\n if (events.length > 0) {\n events.splice(0, Math.ceil(events.length * 0.2)); // drop oldest 20%\n try {\n localStorage.setItem(key, JSON.stringify(events));\n } catch {\n /* give up */\n }\n }\n }\n}\n\nfunction shrinkToByteCap() {\n try {\n let raw = JSON.stringify(q);\n if (raw.length <= cfg.byteCap) return;\n // Drop oldest until under cap\n // Use a coarse 10% chunk drop to avoid O(n^2) re-serializing\n while (q.length && raw.length > cfg.byteCap) {\n q.splice(0, Math.max(1, Math.floor(q.length * 0.1)));\n raw = JSON.stringify(q);\n }\n } catch {\n // If serialization fails, clear queue to avoid lockups\n q = [];\n }\n}\n\n/* ---------------- misc utils ---------------- */\n\nfunction asError(err: unknown): Error {\n if (err instanceof Error) return err;\n try {\n return new Error(typeof err === \"string\" ? err : JSON.stringify(err));\n } catch {\n return new Error(\"Unknown error\");\n }\n}\n\nfunction serializeError(err: unknown): string {\n if (err instanceof Error) {\n return JSON.stringify({\n n: err.name,\n m: err.message,\n s: (err as any).stack,\n });\n }\n try {\n return JSON.stringify(err);\n } catch {\n return '\"Unknown error\"';\n }\n}\n\nfunction deserializeError(s: string): Error {\n try {\n const o = JSON.parse(s);\n if (o && typeof o === \"object\" && (\"m\" in o || \"message\" in o)) {\n const e = new Error(o.m ?? o.message ?? \"Error\");\n if (o.n) e.name = o.n;\n if (o.s) (e as any).stack = o.s;\n return e;\n }\n return new Error(typeof o === \"string\" ? o : \"Error\");\n } catch {\n return new Error(\"Error\");\n }\n}\n\n/** Example\nimport { installPersistentTelemetryQueue } from \"@milliman/dd-ux-utils/telemetry/offlineQueue.persistent\";\n\ninstallPersistentTelemetryQueue({\n maxBuffered: 600,\n storageKey: \"dd_offline_queue_v1\",\n byteCap: 2_000_000,\n flushOnInit: true,\n writeDebounceMs: 120,\n});\n*/\n"],"names":["queue","maxBuffered","installed","installTelemetryQueue","maxBufferedEvents","realAddAction","realAddError","name","attrs","sampleRate","enqueue","err","context","flushQueue","uninstall","ev","toFlush","DEFAULTS","cfg","q","writeTimer","getSenders","g","installPersistentTelemetryQueue","opts","readLS","enqueueAction","enqueueError","tryFlush","onVisChange","sendAction","push","sendError","asError","serializeError","shrinkToByteCap","scheduleWrite","writeLS","copy","deserializeError","key","raw","parsed","v","events","s","o","e"],"mappings":"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"}