iris-recorder 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/user.ts","../src/session.ts","../src/transport.ts","../src/collectors/masking.ts","../src/track.ts","../src/collectors/replay.ts","../src/collectors/errors.ts","../src/collectors/network.ts","../src/collectors/websocket.ts","../src/collectors/console.ts","../src/collectors/vitals.ts","../src/collectors/navigation.ts","../src/collectors/frustration.ts"],"sourcesContent":["import type { IrisUser } from '@iris/types';\nimport { buildMeta } from './session.js';\nimport { Transport } from './transport.js';\nimport { createTracker, type TrackFn, type TrackOptions } from './track.js';\nimport { collectReplay } from './collectors/replay.js';\nimport { collectErrors } from './collectors/errors.js';\nimport { collectNetwork, type NetworkOptions } from './collectors/network.js';\nimport { collectWebSockets } from './collectors/websocket.js';\nimport { collectConsole, type ConsoleOptions } from './collectors/console.js';\nimport { collectVitals } from './collectors/vitals.js';\nimport { collectNavigation } from './collectors/navigation.js';\nimport { collectFrustration } from './collectors/frustration.js';\n\nexport interface RecordConfig {\n /** Iris server origin, e.g. https://rum.twcfm.ca */\n endpoint: string;\n /** project public key (pk_live_…) */\n projectKey: string;\n /** build identifier, used to match source maps. Pass your git SHA. */\n release?: string;\n /** app-supplied user identity for this session — a small object of string\n (or nested) values, e.g. `{ id: 'staff-042', name: 'Daisy', station: { till: 'till-3' } }` */\n user?: IrisUser;\n /** legacy string form of `user` — still supported; same as `user: { id: … }` */\n userId?: string;\n\n // toggles (all on by default)\n replay?: boolean;\n errors?: boolean;\n /** `false` to skip console capture entirely, or options — same shape as\n `network`. `{ captureIrisLogs: true }` also records Iris's own `[iris]`\n debug lines (filtered out by default). */\n console?: false | ConsoleOptions;\n vitals?: boolean;\n navigation?: boolean;\n /** rage / dead / error click detection */\n frustration?: boolean;\n\n // privacy\n maskAllInputs?: boolean;\n maskTextSelector?: string;\n blockSelector?: string;\n network?: false | NetworkOptions;\n /** `false` disables `iris.track()` (it becomes a no-op), or tune its budgets */\n track?: false | TrackOptions;\n\n /** log capture/flush activity to the console — useful while wiring up */\n debug?: boolean;\n}\n\nexport interface IrisHandle {\n /** Stop recording and flush whatever's buffered. */\n stop: () => void;\n /** Force an immediate flush. */\n flush: () => void;\n /** Attach the signed-in user to this session (applies from the next flush).\n Takes a small user object — or a plain string id, the legacy form. */\n identify: (user: string | IrisUser) => void;\n /**\n * Record an app-defined business event, e.g.\n * `iris.track('receipt_printed', { total: 42.5 })`. Props are masked with\n * the same policy as network bodies (`maskKeys` / `maskExtraKeys`) and the\n * per-session event budget applies.\n */\n track: TrackFn;\n sessionId: string;\n}\n\n/**\n * Start recording. One call, drop it at the top of your app:\n *\n * import { record } from 'iris-recorder';\n * record({ endpoint: 'https://rum.twcfm.ca', projectKey: 'pk_live_…' });\n */\nexport function record(config: RecordConfig): IrisHandle {\n const meta = buildMeta(config.release, config.user ?? config.userId);\n const transport = new Transport({\n endpoint: config.endpoint,\n projectKey: config.projectKey,\n meta,\n debug: config.debug,\n });\n const push = transport.push.bind(transport);\n const teardowns: Array<() => void> = [];\n\n // app-defined business events (`iris.track`) share the network masking policy;\n // `track: false` keeps the method callable but records nothing\n const track: TrackFn =\n config.track === false\n ? () => {}\n : createTracker(\n push,\n typeof config.network === 'object' ? config.network : {},\n typeof config.track === 'object' ? config.track : {},\n );\n\n if (config.replay !== false) {\n teardowns.push(\n collectReplay(push, {\n maskAllInputs: config.maskAllInputs ?? true,\n maskTextSelector: config.maskTextSelector,\n blockSelector: config.blockSelector,\n }),\n );\n }\n if (config.errors !== false) teardowns.push(collectErrors(push));\n if (config.console !== false) {\n teardowns.push(\n collectConsole(push, typeof config.console === 'object' ? config.console : {}),\n );\n }\n if (config.navigation !== false) teardowns.push(collectNavigation(push));\n if (config.frustration !== false) teardowns.push(collectFrustration(push));\n if (config.vitals !== false) collectVitals(push);\n\n if (config.network !== false) {\n // The collector applies its own header denylist + body/query masking; we just\n // make sure we never record our own ingest beacons. Match the ingest *path*,\n // not the whole origin — matching the origin would drop every app request to a\n // host Iris shares (e.g. app on localhost:3000, Iris on localhost). Callers can\n // override anything (captureBodies, maskKeys, redactHeaders, …) via `network: { … }`.\n const netOpts: NetworkOptions = {\n ignoreUrls: [`${config.endpoint}/v1/ingest`],\n ...(typeof config.network === 'object' ? config.network : {}),\n };\n teardowns.push(collectNetwork(push, netOpts));\n // streaming traffic shares the same masking/ignore config as fetch/XHR\n if (netOpts.captureWebSockets !== false) {\n teardowns.push(collectWebSockets(push, netOpts, config.debug));\n }\n }\n\n if (config.debug) {\n console.debug('[iris] recording', meta.sessionId, '→', config.endpoint);\n }\n\n return {\n sessionId: meta.sessionId,\n identify: (user: string | IrisUser) => transport.identify(user),\n track,\n flush: () => transport.flush(false),\n stop: () => {\n teardowns.forEach((t) => t());\n transport.stop();\n },\n };\n}\n\nexport type { ConsoleOptions, NetworkOptions, TrackOptions };\nexport type { IrisUser };\n","import type { IrisUser } from '@iris/types';\n\n// \"Small user object\" budgets — enforced before anything leaves the page.\nconst MAX_DEPTH = 3;\nconst MAX_KEYS = 32; // total, across all nesting levels\nconst MAX_KEY_CHARS = 64;\nconst MAX_VALUE_CHARS = 300;\n\n/**\n * Normalize whatever the app handed us into a small, well-shaped IrisUser:\n * strings pass through (truncated), numbers/booleans are stringified, nested\n * plain objects recurse up to MAX_DEPTH, everything else (arrays, functions,\n * null, …) is dropped. Returns undefined when nothing usable remains.\n */\nexport function sanitizeUser(input: unknown): IrisUser | undefined {\n const budget = { keys: MAX_KEYS };\n const out = sanitizeLevel(input, 1, budget);\n return out && Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction sanitizeLevel(\n input: unknown,\n depth: number,\n budget: { keys: number },\n): IrisUser | undefined {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const out: IrisUser = {};\n for (const [rawKey, rawVal] of Object.entries(input as Record<string, unknown>)) {\n if (budget.keys <= 0) break;\n const key = rawKey.slice(0, MAX_KEY_CHARS);\n if (!key) continue;\n if (typeof rawVal === 'string') {\n out[key] = rawVal.slice(0, MAX_VALUE_CHARS);\n budget.keys -= 1;\n } else if (typeof rawVal === 'number' || typeof rawVal === 'boolean') {\n out[key] = String(rawVal);\n budget.keys -= 1;\n } else if (\n typeof rawVal === 'object' &&\n rawVal !== null &&\n !Array.isArray(rawVal) &&\n depth < MAX_DEPTH\n ) {\n budget.keys -= 1; // the nested key itself spends budget\n const nested = sanitizeLevel(rawVal, depth + 1, budget);\n if (nested && Object.keys(nested).length > 0) out[key] = nested;\n else budget.keys += 1; // nothing usable inside — refund the key\n }\n // anything else (array, function, null, undefined, symbol) is dropped\n }\n return out;\n}\n\n/**\n * The one string that stands in for the user where only one fits — the legacy\n * `meta.userId` wire field and the `sessions.user_id` column. Prefers explicit\n * identifiers, then falls back to the first top-level string value.\n */\nexport function displayUserId(user: IrisUser): string | undefined {\n for (const k of ['id', 'userId', 'email', 'username', 'name']) {\n const v = user[k];\n if (typeof v === 'string' && v) return v;\n }\n for (const v of Object.values(user)) if (typeof v === 'string' && v) return v;\n return undefined;\n}\n","import type { IrisUser, SessionMeta } from '@iris/types';\nimport { displayUserId, sanitizeUser } from './user.js';\n\n/** Generate (or resume) a session id, kept in sessionStorage so a reload\n * within the same tab continues the same recording. */\nexport function getSessionId(): string {\n const KEY = 'iris.sid';\n try {\n const existing = sessionStorage.getItem(KEY);\n if (existing) return existing;\n const id = crypto.randomUUID();\n sessionStorage.setItem(KEY, id);\n return id;\n } catch {\n // sessionStorage blocked (private mode / sandbox) — ephemeral id\n return crypto.randomUUID();\n }\n}\n\nexport function buildMeta(release?: string, user?: IrisUser | string): SessionMeta {\n // a plain-string id (the legacy form) is just a one-field user object\n const u = sanitizeUser(typeof user === 'string' ? { id: user } : user);\n return {\n sessionId: getSessionId(),\n user: u,\n // legacy mirror: pre-0.4 backends only know the string form\n userId: u ? displayUserId(u) : undefined,\n release,\n startedAt: Date.now(),\n url: location.href,\n userAgent: navigator.userAgent,\n };\n}\n","import { gzipSync, strToU8 } from 'fflate';\nimport type { IngestBatch, IrisEvent, IrisUser, SessionMeta } from '@iris/types';\nimport { displayUserId, sanitizeUser } from './user.js';\n\nexport interface TransportOptions {\n endpoint: string;\n projectKey: string;\n meta: SessionMeta;\n /** flush when the queued payload exceeds this many bytes (rough) */\n flushBytes?: number;\n /** flush at least this often */\n flushMs?: number;\n /** log flush activity to the console (off by default) */\n debug?: boolean;\n}\n\n/**\n * Batches events, gzips them, and ships them. The unload tail is the\n * whole game here: on `visibilitychange → hidden` we flush via\n * sendBeacon, which survives the page going away.\n */\nexport class Transport {\n private queue: IrisEvent[] = [];\n private bytes = 0;\n private timer: ReturnType<typeof setInterval> | undefined;\n private readonly flushBytes: number;\n /** cap the retry buffer so a downed server can never balloon host-page memory */\n private readonly maxBytes = 4_000_000;\n\n constructor(private readonly opts: TransportOptions) {\n this.flushBytes = opts.flushBytes ?? 64_000;\n\n this.timer = setInterval(() => this.flush(false), opts.flushMs ?? 5_000);\n\n addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') this.flush(true);\n });\n // pagehide is the most reliable \"tab is closing\" signal on mobile Safari\n addEventListener('pagehide', () => this.flush(true));\n }\n\n push(event: IrisEvent): void {\n this.queue.push(event);\n this.bytes += roughSize(event);\n if (this.bytes >= this.flushBytes) this.flush(false);\n }\n\n flush(useBeacon: boolean): void {\n if (this.queue.length === 0) return;\n\n // Detach the queue, but keep the reference so we can put it back if the\n // send fails. Dropping a batch outright loses data — and if it's the first\n // batch, it loses the rrweb full snapshot and the session can't play back.\n const events = this.queue;\n this.queue = [];\n this.bytes = 0;\n\n const batch: IngestBatch = { meta: this.opts.meta, events };\n const body = gzipSync(strToU8(JSON.stringify(batch)));\n const url = `${this.opts.endpoint}/v1/ingest`;\n this.log(`flush via ${useBeacon ? 'beacon' : 'fetch'}: ${events.length} events, ${body.length}b gz → ${url}`);\n\n if (useBeacon && 'sendBeacon' in navigator) {\n let ok = false;\n try {\n ok = navigator.sendBeacon(url, new Blob([body], { type: 'application/octet-stream' }));\n } catch {\n ok = false;\n }\n // Browser refused the beacon (size/quota) — keep the events for a later\n // flush. On a true unload there's no later flush, but the common case\n // (tab hidden, page still alive) recovers on the next interval/fetch.\n if (!ok) {\n this.log('beacon refused — re-queueing');\n this.requeue(events);\n }\n return;\n }\n\n // keepalive only on the unload path: it caps the body at ~64KB, so a normal\n // periodic flush must NOT set it or large snapshots silently fail to send.\n void fetch(url, {\n method: 'POST',\n body,\n keepalive: useBeacon,\n headers: {\n 'content-encoding': 'gzip',\n 'content-type': 'application/octet-stream',\n 'x-iris-key': this.opts.projectKey,\n },\n })\n .then((res) => {\n this.log(`ingest responded ${res.status}`);\n if (!res.ok) this.requeue(events);\n })\n .catch((err) => {\n // network error — telemetry must never throw; retry on the next flush\n this.log('ingest request failed', err);\n this.requeue(events);\n });\n }\n\n /** Attach or update the user identity carried on subsequent batches.\n Accepts the legacy plain-string id or a small user object. */\n identify(user: string | IrisUser): void {\n const u = sanitizeUser(typeof user === 'string' ? { id: user } : user);\n if (!u) return; // nothing usable — keep whatever identity we had\n this.opts.meta.user = u;\n this.opts.meta.userId = displayUserId(u);\n this.log(`identified as ${this.opts.meta.userId ?? '(unnamed user)'}`);\n }\n\n private log(...args: unknown[]): void {\n if (this.opts.debug) console.debug('[iris]', ...args);\n }\n\n /** Put un-sent events back at the front (preserving order), bounded by maxBytes. */\n private requeue(events: IrisEvent[]): void {\n const merged = events.concat(this.queue);\n let bytes = 0;\n for (const e of merged) bytes += roughSize(e);\n if (bytes > this.maxBytes) return; // give up rather than grow without bound\n this.queue = merged;\n this.bytes = bytes;\n }\n\n stop(): void {\n if (this.timer) clearInterval(this.timer);\n this.flush(true);\n }\n}\n\nfunction roughSize(event: IrisEvent): number {\n // cheap estimate; avoids JSON.stringify per push\n return JSON.stringify(event).length;\n}\n","/**\n * Shared masking policy for everything the SDK captures off the network:\n * URLs (query params), headers, and payload text. Used by the fetch/XHR\n * collector and the WebSocket/EventSource collector so one policy covers both.\n */\n\n// Headers stripped entirely — they carry credentials, never useful to keep.\nexport const DEFAULT_REDACT_HEADERS = [\n 'authorization',\n 'cookie',\n 'set-cookie',\n 'proxy-authorization',\n 'x-api-key',\n 'x-auth-token',\n 'x-csrf-token',\n 'x-xsrf-token',\n];\n\n// Body/query keys whose *values* look secret — masked in place, structure kept.\nexport const DEFAULT_MASK_KEYS =\n /pass(word|wd)?|secret|token|api[-_]?key|auth|credential|session|cvv|card[-_]?(number|num)?|ssn|pin/i;\n\nexport const DEFAULT_MAX_BODY = 10_000;\nexport const REDACTED = '[redacted]';\nexport const MASKED = '[masked]';\n\nexport interface MaskerOptions {\n /** extra header names to redact (case-insensitive); merged with the built-in denylist */\n redactHeaders?: string[];\n /** keys whose values are masked in JSON/form bodies and URL query strings */\n maskKeys?: RegExp;\n /**\n * Extra body/query key names to mask (exact, case-insensitive). Additive:\n * merged with the default pattern (or with `maskKeys` if that's overridden).\n */\n maskExtraKeys?: string[];\n /** max captured length (chars) for each body; longer bodies are truncated */\n maxBodyChars?: number;\n}\n\nexport function createMasker(opts: MaskerOptions = {}) {\n const redact = new Set([\n ...DEFAULT_REDACT_HEADERS,\n ...(opts.redactHeaders ?? []).map((h) => h.toLowerCase()),\n ]);\n const maskKeys = opts.maskKeys ?? DEFAULT_MASK_KEYS;\n const extraKeys = new Set((opts.maskExtraKeys ?? []).map((k) => k.toLowerCase()));\n /** the one key test every masking path goes through */\n const isMaskedKey = (k: string) => maskKeys.test(k) || extraKeys.has(k.toLowerCase());\n const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY;\n\n const baseHref = () => (typeof location !== 'undefined' ? location.href : undefined);\n\n const maskUrl = (url: string): string => {\n try {\n const u = new URL(url, baseHref());\n let touched = false;\n for (const k of [...u.searchParams.keys()]) {\n if (isMaskedKey(k)) {\n u.searchParams.set(k, MASKED);\n touched = true;\n }\n }\n return touched ? u.toString() : url;\n } catch {\n return url;\n }\n };\n\n const maskHeaders = (h: Record<string, string>): Record<string, string> => {\n const out: Record<string, string> = {};\n for (const k of Object.keys(h)) out[k] = redact.has(k.toLowerCase()) ? REDACTED : h[k]!;\n return out;\n };\n\n const maskJson = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(maskJson);\n if (v && typeof v === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n out[k] = isMaskedKey(k) ? MASKED : maskJson(val);\n }\n return out;\n }\n return v;\n };\n\n const truncate = (s: string, cap = maxBody) =>\n s.length > cap ? `${s.slice(0, cap)}…[+${s.length - cap} chars]` : s;\n\n /** Mask a JSON or urlencoded payload; anything else passes through (truncated). */\n const maskBody = (raw: unknown, cap = maxBody): string | undefined => {\n if (raw == null || raw === '') return undefined;\n const s = String(raw);\n let masked: string;\n try {\n masked = JSON.stringify(maskJson(JSON.parse(s)));\n } catch {\n // urlencoded form body? mask matching keys; otherwise leave as-is\n if (/^[^={}\\s]+=[^&]*(?:&[^=]+=[^&]*)*$/.test(s)) {\n try {\n const p = new URLSearchParams(s);\n for (const k of [...p.keys()]) if (isMaskedKey(k)) p.set(k, MASKED);\n masked = p.toString();\n } catch {\n masked = s;\n }\n } else {\n masked = s;\n }\n }\n return truncate(masked, cap);\n };\n\n /** Recursively mask secret-looking keys in a plain value (objects stay objects). */\n const maskObject = (v: unknown): unknown => maskJson(v);\n\n return { maskUrl, maskHeaders, maskBody, maskObject, truncate, isMaskedKey, maxBodyChars: maxBody };\n}\n\nexport type Masker = ReturnType<typeof createMasker>;\n","import type { IrisEvent, TrackEvent } from '@iris/types';\nimport { createMasker, type MaskerOptions } from './collectors/masking.js';\n\nconst DEFAULT_MAX_EVENTS = 500;\nconst DEFAULT_MAX_PROPS_CHARS = 8_000;\nconst MAX_NAME_CHARS = 120;\n\nexport interface TrackOptions {\n /** per-session budget of tracked events — a loop can't flood the session (default 500) */\n maxEvents?: number;\n /** serialized-props size cap in chars; oversized props become a note (default 8 000) */\n maxPropsChars?: number;\n}\n\nexport type TrackFn = (name: string, props?: Record<string, unknown>) => void;\n\n/**\n * Build the `iris.track()` function: app-defined business events\n * (`track('receipt_printed', { total: 42.5 })`) that land on the session\n * timeline. Props go through the same masking policy as network bodies\n * (`maskKeys` / `maskExtraKeys`), get size-capped, and the whole thing is\n * budgeted per session. Never throws into the host app.\n */\nexport function createTracker(\n push: (e: IrisEvent) => void,\n maskOpts: MaskerOptions = {},\n opts: TrackOptions = {},\n): TrackFn {\n const masker = createMasker(maskOpts);\n const maxEvents = opts.maxEvents ?? DEFAULT_MAX_EVENTS;\n const maxPropsChars = opts.maxPropsChars ?? DEFAULT_MAX_PROPS_CHARS;\n let count = 0;\n let warned = false;\n\n return (name: string, props?: Record<string, unknown>) => {\n try {\n const cleanName = typeof name === 'string' ? name.trim().slice(0, MAX_NAME_CHARS) : '';\n if (!cleanName) return;\n\n if (count >= maxEvents) {\n if (!warned) {\n warned = true;\n // eslint-disable-next-line no-console\n console.warn(\n `[iris] track() budget exhausted (${maxEvents} events) — further events are dropped`,\n );\n }\n return;\n }\n count++;\n\n const ev: TrackEvent = { type: 'custom', ts: Date.now(), name: cleanName };\n if (props != null && typeof props === 'object') {\n // masking recurses and stringify measures — both can throw on circular\n // structures; the event survives with a note either way\n let masked: Record<string, unknown> | null = null;\n let size = Infinity;\n try {\n masked = masker.maskObject(props) as Record<string, unknown>;\n size = JSON.stringify(masked).length;\n } catch {\n masked = null;\n }\n ev.props =\n masked != null && size <= maxPropsChars\n ? masked\n : { '[iris]': `props dropped (${Number.isFinite(size) ? `${size} chars > ${maxPropsChars}` : 'not serializable'})` };\n }\n push(ev);\n } catch {\n /* telemetry must never break the host app */\n }\n };\n}\n","import { record } from 'rrweb';\nimport type { IrisEvent } from '@iris/types';\n\nexport interface ReplayOptions {\n maskAllInputs?: boolean;\n /** CSS selector whose text is masked in the replay */\n maskTextSelector?: string;\n /** CSS selector whose subtree is blocked (rendered as a placeholder) */\n blockSelector?: string;\n}\n\n/** Wire rrweb up to the transport. Returns a stop() fn. */\nexport function collectReplay(\n push: (e: IrisEvent) => void,\n opts: ReplayOptions = {},\n): () => void {\n const stop = record({\n emit(event) {\n push({ type: 'replay', ts: Date.now(), data: event });\n },\n maskAllInputs: opts.maskAllInputs ?? true,\n maskTextSelector: opts.maskTextSelector,\n blockSelector: opts.blockSelector,\n // sample mousemove/scroll so we don't flood the timeline\n sampling: { mousemove: 50, scroll: 150, input: 'last' },\n // Emit a fresh full snapshot periodically. If the opening segment is ever\n // lost in transit, playback can still start from a later checkpoint — and\n // it makes seeking in long sessions much faster.\n checkoutEveryNms: 30_000,\n });\n\n return () => stop?.();\n}\n","import type { IrisEvent } from '@iris/types';\n\n/** Capture uncaught exceptions and unhandled promise rejections. */\nexport function collectErrors(push: (e: IrisEvent) => void): () => void {\n const onError = (ev: globalThis.ErrorEvent) => {\n push({\n type: 'error',\n ts: Date.now(),\n source: 'error',\n name: ev.error?.name ?? 'Error',\n message: ev.message ?? String(ev.error),\n stack: ev.error?.stack,\n });\n };\n\n const onRejection = (ev: PromiseRejectionEvent) => {\n const reason = ev.reason;\n push({\n type: 'error',\n ts: Date.now(),\n source: 'unhandledrejection',\n name: reason?.name ?? 'UnhandledRejection',\n message: reason?.message ?? String(reason),\n stack: reason?.stack,\n });\n };\n\n addEventListener('error', onError);\n addEventListener('unhandledrejection', onRejection);\n\n return () => {\n removeEventListener('error', onError);\n removeEventListener('unhandledrejection', onRejection);\n };\n}\n","import type { IrisEvent, NetworkEvent } from '@iris/types';\nimport { createMasker, MASKED } from './masking.js';\n\nexport interface NetworkOptions {\n /**\n * Capture request/response headers + bodies (on by default). Iris monitors\n * first-party internal apps, so bodies are useful for debugging — but they're\n * always masked (see below) so secrets never leave the page.\n */\n captureBodies?: boolean;\n /** extra header names to redact (case-insensitive); merged with the built-in denylist */\n redactHeaders?: string[];\n /** keys whose values are masked in JSON/form bodies and URL query strings; overrides the default */\n maskKeys?: RegExp;\n /** extra key names to mask in req/res bodies + query strings (exact, case-insensitive); additive */\n maskExtraKeys?: string[];\n /** max captured length (chars) for each body; longer bodies are truncated */\n maxBodyChars?: number;\n /** don't record calls to our own ingest endpoint */\n ignoreUrls?: (string | RegExp)[];\n /** capture WebSocket + EventSource traffic (on by default) — see websocket.ts */\n captureWebSockets?: boolean;\n /** per-connection, per-direction frame budget before frames are dropped (default 200) */\n wsMaxFrames?: number;\n /** max captured length (chars) per WebSocket/SSE frame (default 2 000) */\n wsMaxFrameChars?: number;\n /**\n * Which socket frame kinds to record (connection lifecycle is always kept):\n * 'text' = every text frame · 'json' = only text that parses to an\n * object/array · 'binary' = binary frames (size-only). Default\n * `['text', 'binary']`; e.g. `['json']` skips binary and unstructured text\n * (heartbeats). Filtered frames don't consume the frame budget.\n */\n wsFrameTypes?: Array<'text' | 'json' | 'binary'>;\n}\n\n// Don't pull megabytes into memory just to truncate them. Applies both to\n// content-length'd responses and (via the capped stream reader) chunked ones.\nconst MAX_READ_BYTES = 512_000;\n\n/** What a request carried, extracted without consuming the caller's body. */\ninterface ReqBodyInfo {\n text?: string;\n /** body that must be read async (a cloned Request); resolves undefined on failure */\n textPromise?: Promise<string | undefined>;\n size?: number;\n /** text was already masked during extraction (FormData) — only truncate it */\n premasked?: boolean;\n}\n\n/** Monkey-patch fetch + XHR to time, log, and (masked) capture requests. */\nexport function collectNetwork(\n push: (e: IrisEvent) => void,\n opts: NetworkOptions = {},\n): () => void {\n const ignore = opts.ignoreUrls ?? [];\n const captureBodies = opts.captureBodies !== false; // default ON\n const { maskUrl, maskHeaders, maskBody, truncate, isMaskedKey } = createMasker(opts);\n\n // Hardened: only ever match a string, so a non-string url can't throw in `.some`.\n const shouldIgnore = (url: string) =>\n typeof url === 'string' &&\n ignore.some((m) => (typeof m === 'string' ? url.includes(m) : m.test(url)));\n\n // ── header / body extraction ─────────────────────────────────────\n const headersToObj = (h: unknown): Record<string, string> => {\n const out: Record<string, string> = {};\n if (!h) return out;\n if (typeof Headers !== 'undefined' && h instanceof Headers) {\n h.forEach((v, k) => (out[k] = v));\n } else if (Array.isArray(h)) {\n for (const pair of h) if (pair && pair.length >= 2) out[String(pair[0])] = String(pair[1]);\n } else if (typeof h === 'object') {\n for (const [k, v] of Object.entries(h as Record<string, unknown>)) out[k] = String(v);\n }\n return out;\n };\n\n const isTexty = (ct: string | undefined) =>\n !!ct && /(json|text|xml|javascript|x-www-form-urlencoded|html)/i.test(ct);\n // SSE must never be body-read: text() on an open stream buffers it forever\n // and the network event would only push when the stream finally dies.\n const isEventStream = (ct: string | undefined) => !!ct && /event-stream/i.test(ct);\n\n const parseHeaderString = (raw: string | null): Record<string, string> => {\n const out: Record<string, string> = {};\n if (!raw) return out;\n for (const line of raw.trim().split(/[\\r\\n]+/)) {\n const i = line.indexOf(':');\n if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim();\n }\n return out;\n };\n\n /** Flatten FormData to `k=v&…` lines, masking secret keys and stubbing files. */\n const formDataText = (fd: FormData): string => {\n const parts: string[] = [];\n fd.forEach((v, k) => {\n if (typeof v === 'string') parts.push(`${k}=${isMaskedKey(k) ? MASKED : v}`);\n else parts.push(`${k}=[file ${v.name || 'blob'} ${v.size}B]`);\n });\n return parts.join('&');\n };\n\n /**\n * Extract what we can from a request body without consuming it. Handles the\n * init body (string/URLSearchParams/FormData sync; binary size-only) and a\n * body carried inside a Request object (cloned, read async).\n */\n const requestBodyInfo = (input: unknown, init?: RequestInit): ReqBodyInfo => {\n const b = init?.body;\n if (typeof b === 'string') return { text: b, size: b.length };\n if (typeof URLSearchParams !== 'undefined' && b instanceof URLSearchParams) {\n const s = b.toString();\n return { text: s, size: s.length };\n }\n if (typeof FormData !== 'undefined' && b instanceof FormData) {\n const s = formDataText(b);\n return { text: s, size: s.length, premasked: true };\n }\n if (typeof Blob !== 'undefined' && b instanceof Blob) return { size: b.size };\n if (typeof ArrayBuffer !== 'undefined') {\n if (b instanceof ArrayBuffer) return { size: b.byteLength };\n if (ArrayBuffer.isView(b as object)) return { size: (b as ArrayBufferView).byteLength };\n }\n // body carried inside a Request object: fetch(new Request(url, { body }))\n if (b == null && typeof Request !== 'undefined' && input instanceof Request) {\n const m = input.method.toUpperCase();\n if (m !== 'GET' && m !== 'HEAD' && !input.bodyUsed) {\n const ct = input.headers.get('content-type') ?? undefined;\n try {\n if (ct && /multipart\\/form-data/i.test(ct)) {\n return {\n textPromise: input\n .clone()\n .formData()\n .then(formDataText)\n .catch(() => undefined),\n premasked: true,\n };\n }\n if (isTexty(ct)) {\n return { textPromise: input.clone().text().catch(() => undefined) };\n }\n } catch {\n /* clone can throw on locked/streamed bodies — skip capture */\n }\n }\n }\n return {};\n };\n\n /**\n * Read a cloned response as text with a hard byte cap, so chunked responses\n * without a content-length can't buffer unbounded memory. Cancels the clone's\n * stream once the cap is hit.\n */\n const readTextCapped = async (\n res: Response,\n ): Promise<{ text: string; bytes: number; complete: boolean }> => {\n const body = res.body;\n if (!body || typeof body.getReader !== 'function') {\n const text = await res.text();\n return { text, bytes: text.length, complete: true };\n }\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let text = '';\n let bytes = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return { text: text + decoder.decode(), bytes, complete: true };\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_READ_BYTES) {\n void reader.cancel().catch(() => {});\n return { text, bytes, complete: false };\n }\n }\n };\n\n // ── fetch ──\n const origFetch = window.fetch;\n window.fetch = async function (...args: Parameters<typeof fetch>) {\n const start = Date.now();\n const input = args[0];\n const rawUrl =\n typeof input === 'string' ? input\n : input instanceof URL ? input.href\n : input instanceof Request ? input.url\n : String(input);\n const method = (\n args[1]?.method ?? (input instanceof Request ? input.method : 'GET')\n ).toUpperCase();\n\n // Snapshot the request body BEFORE dispatch — a Request-object body must be\n // cloned while its stream is still unread.\n let reqInfo: ReqBodyInfo = {};\n if (captureBodies && !shouldIgnore(rawUrl)) {\n try {\n reqInfo = requestBodyInfo(input, args[1]);\n } catch {\n reqInfo = {};\n }\n }\n\n try {\n const res = await origFetch.apply(this, args);\n // Capturing must never reject a successful fetch — swallow any failure.\n try {\n if (!shouldIgnore(rawUrl)) {\n const ev: NetworkEvent = {\n type: 'network',\n ts: start,\n method,\n url: maskUrl(rawUrl),\n status: res.status,\n durationMs: Date.now() - start,\n initiator: 'fetch',\n };\n if (!captureBodies) {\n push(ev);\n } else {\n const reqHeaders =\n args[1]?.headers ?? (input instanceof Request ? input.headers : undefined);\n ev.reqHeaders = maskHeaders(headersToObj(reqHeaders));\n ev.resHeaders = maskHeaders(headersToObj(res.headers));\n const ct = res.headers.get('content-type') ?? undefined;\n if (ct) ev.contentType = ct;\n const len = Number(res.headers.get('content-length'));\n if (Number.isFinite(len)) ev.resSize = len;\n\n // clone BEFORE returning res so the caller's body is untouched\n let resRead: Promise<{ text: string; bytes: number; complete: boolean }> | null = null;\n if (\n isTexty(ct) &&\n !isEventStream(ct) &&\n !(Number.isFinite(len) && len > MAX_READ_BYTES)\n ) {\n resRead = readTextCapped(res.clone());\n }\n\n // finish (and push) in the background once the async reads settle\n void (async () => {\n try {\n const rb = reqInfo.text ?? (reqInfo.textPromise ? await reqInfo.textPromise : undefined);\n if (rb) {\n ev.reqBody = reqInfo.premasked ? truncate(rb) : maskBody(rb);\n ev.reqSize = reqInfo.size ?? rb.length;\n } else if (reqInfo.size != null) {\n ev.reqSize = reqInfo.size; // binary body — size only\n }\n if (resRead) {\n const r = await resRead;\n if (r.text) ev.resBody = maskBody(r.text);\n if (!Number.isFinite(len)) ev.resSize = r.bytes;\n }\n } catch {\n /* capture failure must not block the event */\n }\n push(ev);\n })();\n }\n }\n } catch {\n /* logging failure must not reject a successful fetch */\n }\n return res;\n } catch (err) {\n try {\n if (!shouldIgnore(rawUrl)) {\n push({\n type: 'network',\n ts: start,\n method,\n url: maskUrl(rawUrl),\n status: 0,\n durationMs: Date.now() - start,\n initiator: 'fetch',\n });\n }\n } catch {\n /* logging failure must not alter the original rejection */\n }\n throw err;\n }\n };\n\n // ── XMLHttpRequest ──\n const origOpen = XMLHttpRequest.prototype.open;\n const origSend = XMLHttpRequest.prototype.send;\n const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;\n\n XMLHttpRequest.prototype.open = function (\n this: XMLHttpRequest,\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n (this as any).__iris = {\n method: String(method).toUpperCase(),\n url: String(url),\n start: 0,\n reqHeaders: {} as Record<string, string>,\n reqBody: undefined as string | undefined,\n reqSize: undefined as number | undefined,\n premasked: false,\n };\n return (origOpen as (...a: unknown[]) => void).apply(this, [method, url, ...rest]);\n };\n\n XMLHttpRequest.prototype.setRequestHeader = function (\n this: XMLHttpRequest,\n name: string,\n value: string,\n ) {\n const ctx = (this as any).__iris;\n if (ctx) ctx.reqHeaders[name] = value;\n return origSetHeader.apply(this, [name, value]);\n };\n\n XMLHttpRequest.prototype.send = function (this: XMLHttpRequest, ...sendArgs: any[]) {\n const ctx = (this as any).__iris;\n if (ctx) {\n ctx.start = Date.now();\n const body = sendArgs[0];\n if (typeof body === 'string') {\n ctx.reqBody = body;\n ctx.reqSize = body.length;\n } else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {\n ctx.reqBody = body.toString();\n ctx.reqSize = ctx.reqBody.length;\n } else if (typeof FormData !== 'undefined' && body instanceof FormData) {\n ctx.reqBody = formDataText(body);\n ctx.reqSize = ctx.reqBody.length;\n ctx.premasked = true;\n } else if (typeof Blob !== 'undefined' && body instanceof Blob) {\n ctx.reqSize = body.size;\n } else if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {\n ctx.reqSize = body.byteLength;\n } else if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) {\n ctx.reqSize = (body as ArrayBufferView).byteLength;\n }\n\n this.addEventListener('loadend', () => {\n try {\n if (shouldIgnore(ctx.url)) return;\n const ev: NetworkEvent = {\n type: 'network',\n ts: ctx.start,\n method: ctx.method,\n url: maskUrl(ctx.url),\n status: this.status,\n durationMs: Date.now() - ctx.start,\n initiator: 'xhr',\n };\n if (captureBodies) {\n ev.reqHeaders = maskHeaders(ctx.reqHeaders);\n ev.resHeaders = maskHeaders(parseHeaderString(this.getAllResponseHeaders()));\n const ct = this.getResponseHeader('content-type') ?? undefined;\n if (ct) ev.contentType = ct;\n if (ctx.reqBody != null) {\n ev.reqBody = ctx.premasked ? truncate(ctx.reqBody) : maskBody(ctx.reqBody);\n }\n if (ctx.reqSize != null) ev.reqSize = ctx.reqSize;\n const rt = this.responseType;\n if (rt === '' || rt === 'text') {\n try {\n const text = this.responseText;\n if (text) {\n ev.resBody = maskBody(text);\n ev.resSize = text.length;\n }\n } catch {\n /* responseText not accessible — skip body */\n }\n } else if (rt === 'json') {\n try {\n const r = this.response;\n if (r != null) {\n const s = JSON.stringify(r);\n ev.resBody = maskBody(s);\n ev.resSize = s.length;\n }\n } catch {\n /* response not serializable — skip body */\n }\n } else if (rt === 'arraybuffer') {\n const r = this.response as ArrayBuffer | null;\n if (r) ev.resSize = r.byteLength;\n } else if (rt === 'blob') {\n const r = this.response as Blob | null;\n if (r) ev.resSize = r.size;\n }\n }\n push(ev);\n } catch {\n /* swallow — instrumentation never throws into the host */\n }\n });\n }\n return origSend.apply(this, sendArgs as []);\n };\n\n return () => {\n window.fetch = origFetch;\n XMLHttpRequest.prototype.open = origOpen;\n XMLHttpRequest.prototype.send = origSend;\n XMLHttpRequest.prototype.setRequestHeader = origSetHeader;\n };\n}\n","import type { IrisEvent, WsEvent } from '@iris/types';\nimport { createMasker } from './masking.js';\nimport type { NetworkOptions } from './network.js';\n\nconst DEFAULT_MAX_FRAMES = 200;\nconst DEFAULT_FRAME_CHARS = 2_000;\n\n// Module-level so ids stay unique even across record() teardown/restart.\nlet nextConn = 0;\n\n/** Per-connection instrumentation state stashed on the socket instance. */\ninterface WsCtx {\n connId: string;\n budget: { sent: number; recv: number; dropped: number };\n}\n\n/**\n * Patch window.WebSocket (and EventSource) to record connection lifecycles and\n * frames. Subclassing keeps `instanceof WebSocket`, readyState, binaryType and\n * the static constants working, so socket libraries are unaffected. Outbound\n * capture hooks `WebSocket.prototype.send` (not a subclass override), so even\n * libraries that call `WebSocket.prototype.send.call(sock, …)` are seen; the\n * hook is a no-op for sockets we didn't instrument.\n *\n * EventSource: the default `message` event AND any named event the app\n * subscribes to (`es.addEventListener('order-update', …)`) are recorded — named\n * frames carry the event name.\n *\n * Volume is the hazard with sockets (heartbeats, tickers), so every connection\n * has a per-direction frame budget; frames past it are counted and reported as\n * `dropped` on the close event. Text frames are masked with the same policy as\n * network bodies; binary frames record size only.\n */\nexport function collectWebSockets(\n push: (e: IrisEvent) => void,\n opts: NetworkOptions = {},\n debug = false,\n): () => void {\n const masker = createMasker(opts);\n const maxFrames = opts.wsMaxFrames ?? DEFAULT_MAX_FRAMES;\n const frameChars = opts.wsMaxFrameChars ?? DEFAULT_FRAME_CHARS;\n const frameTypes = new Set(opts.wsFrameTypes ?? ['text', 'binary']);\n const ignore = opts.ignoreUrls ?? [];\n\n /** 'json' means structured payloads — objects/arrays, not bare scalars. */\n const isJsonPayload = (s: string): boolean => {\n const t = s.trim();\n if (!t.startsWith('{') && !t.startsWith('[')) return false;\n try {\n JSON.parse(t);\n return true;\n } catch {\n return false;\n }\n };\n\n /** Frame-kind filter (wsFrameTypes). Filtered frames never touch the budget. */\n const shouldCaptureFrame = (data: unknown): boolean => {\n if (typeof data === 'string') {\n if (frameTypes.has('text')) return true;\n if (frameTypes.has('json')) return isJsonPayload(data);\n return false;\n }\n return frameTypes.has('binary');\n };\n\n const shouldIgnore = (url: string) =>\n typeof url === 'string' &&\n ignore.some((m) => (typeof m === 'string' ? url.includes(m) : m.test(url)));\n\n // Telemetry must never throw into the host app.\n const emit = (e: WsEvent) => {\n try {\n push(e);\n } catch {\n /* swallow */\n }\n };\n\n // `[iris]`-prefixed, so the console collector keeps it out of sessions.\n const debugLog = (...args: unknown[]) => {\n if (debug) console.debug('[iris]', ...args);\n };\n\n const frameInfo = (data: unknown): Pick<WsEvent, 'data' | 'size' | 'binary'> => {\n if (typeof data === 'string') return { data: masker.maskBody(data, frameChars), size: data.length };\n if (typeof Blob !== 'undefined' && data instanceof Blob) return { binary: true, size: data.size };\n if (typeof ArrayBuffer !== 'undefined') {\n if (data instanceof ArrayBuffer) return { binary: true, size: data.byteLength };\n if (ArrayBuffer.isView(data as object))\n return { binary: true, size: (data as ArrayBufferView).byteLength };\n }\n return {};\n };\n\n // ── WebSocket ────────────────────────────────────────────────────\n const OrigWS = window.WebSocket;\n let wsPatched = false;\n let origSend: typeof WebSocket.prototype.send | undefined;\n if (typeof OrigWS === 'function') {\n wsPatched = true;\n\n class IrisWebSocket extends OrigWS {\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url as string, protocols);\n try {\n const raw = typeof url === 'string' ? url : String(url);\n if (shouldIgnore(raw)) return;\n const connId = `ws${++nextConn}`;\n const budget = { sent: 0, recv: 0, dropped: 0 };\n (this as unknown as { __iris?: WsCtx }).__iris = { connId, budget };\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'open',\n transport: 'websocket',\n url: masker.maskUrl(raw),\n });\n this.addEventListener('message', (m: MessageEvent) => {\n if (!shouldCaptureFrame(m.data)) return;\n if (budget.recv >= maxFrames) {\n budget.dropped++;\n return;\n }\n budget.recv++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'message',\n transport: 'websocket',\n ...frameInfo(m.data),\n });\n });\n this.addEventListener('close', (c: CloseEvent) => {\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'close',\n transport: 'websocket',\n code: c.code,\n reason: c.reason || undefined,\n dropped: budget.dropped || undefined,\n });\n debugLog(\n `ws ${connId} closed (${c.code}) — frames ↑${budget.sent} ↓${budget.recv}, dropped ${budget.dropped}`,\n );\n });\n this.addEventListener('error', () => {\n emit({ type: 'ws', ts: Date.now(), connId, op: 'error', transport: 'websocket' });\n });\n } catch {\n /* instrumentation must never break the socket */\n }\n }\n }\n\n // Outbound frames: hook the *prototype* so calls that bypass the subclass\n // (saved method refs, `.call` on the base prototype) are still captured.\n origSend = OrigWS.prototype.send;\n const boundOrigSend = origSend;\n OrigWS.prototype.send = function (this: WebSocket, data: Parameters<WebSocket['send']>[0]) {\n try {\n const ctx = (this as unknown as { __iris?: WsCtx }).__iris;\n if (ctx && shouldCaptureFrame(data)) {\n if (ctx.budget.sent >= maxFrames) {\n ctx.budget.dropped++;\n } else {\n ctx.budget.sent++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId: ctx.connId,\n op: 'send',\n transport: 'websocket',\n ...frameInfo(data),\n });\n }\n }\n } catch {\n /* swallow */\n }\n return boundOrigSend.call(this, data);\n };\n\n window.WebSocket = IrisWebSocket as unknown as typeof WebSocket;\n }\n\n // ── EventSource ──────────────────────────────────────────────────\n const OrigES = (window as { EventSource?: typeof EventSource }).EventSource;\n let esPatched = false;\n if (typeof OrigES === 'function') {\n esPatched = true;\n\n /** Per-instance stream state for named-event mirroring. */\n interface EsCtx {\n record: (m: MessageEvent, eventName?: string) => void;\n mirrored: Set<string>;\n }\n\n class IrisEventSource extends OrigES {\n constructor(url: string | URL, init?: EventSourceInit) {\n super(url as string, init);\n try {\n const raw = typeof url === 'string' ? url : String(url);\n if (shouldIgnore(raw)) return;\n const connId = `es${++nextConn}`;\n const state = { recv: 0, dropped: 0 };\n const record = (m: MessageEvent, eventName?: string) => {\n if (!shouldCaptureFrame(m.data)) return;\n if (state.recv >= maxFrames) {\n state.dropped++;\n return;\n }\n state.recv++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'message',\n transport: 'eventsource',\n ...(eventName ? { event: eventName } : {}),\n ...frameInfo(m.data),\n });\n };\n (this as unknown as { __iris?: EsCtx }).__iris = { record, mirrored: new Set() };\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'open',\n transport: 'eventsource',\n url: masker.maskUrl(raw),\n });\n super.addEventListener('message', (m: MessageEvent) => record(m));\n super.addEventListener('error', () => {\n emit({ type: 'ws', ts: Date.now(), connId, op: 'error', transport: 'eventsource' });\n });\n // EventSource has no close event — closing is a client-side call.\n const origClose = this.close.bind(this);\n this.close = () => {\n try {\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'close',\n transport: 'eventsource',\n dropped: state.dropped || undefined,\n });\n debugLog(`es ${connId} closed — frames ↓${state.recv}, dropped ${state.dropped}`);\n } catch {\n /* swallow */\n }\n origClose();\n };\n } catch {\n /* instrumentation must never break the stream */\n }\n }\n\n // Named SSE events (`es.addEventListener('order-update', …)`) never fire\n // the default `message` event — mirror whatever names the app subscribes\n // to so those frames are recorded too. Params are typed loosely because\n // the DOM lib's overload set won't unify with a single override signature.\n override addEventListener(type: string, listener: unknown, options?: unknown): void {\n try {\n const ctx = (this as unknown as { __iris?: EsCtx }).__iris;\n if (\n ctx &&\n type !== 'message' &&\n type !== 'error' &&\n type !== 'open' &&\n !ctx.mirrored.has(type)\n ) {\n ctx.mirrored.add(type);\n super.addEventListener(type, ((m: Event) =>\n ctx.record(m as MessageEvent, type)) as EventListener);\n }\n } catch {\n /* swallow */\n }\n super.addEventListener(\n type,\n listener as EventListenerOrEventListenerObject,\n options as boolean | AddEventListenerOptions | undefined,\n );\n }\n }\n\n (window as { EventSource?: typeof EventSource }).EventSource =\n IrisEventSource as unknown as typeof EventSource;\n }\n\n return () => {\n if (wsPatched) {\n window.WebSocket = OrigWS;\n if (origSend) OrigWS.prototype.send = origSend;\n }\n if (esPatched) (window as { EventSource?: typeof EventSource }).EventSource = OrigES;\n };\n}\n","import type { ConsoleEvent, IrisEvent } from '@iris/types';\n\nconst LEVELS: ConsoleEvent['level'][] = ['log', 'info', 'warn', 'error', 'debug'];\n\nexport interface ConsoleOptions {\n /**\n * Also record Iris's own `[iris]`-prefixed output (the `debug: true`\n * flush/ingest chatter) into the session. Off by default — it floods the\n * timeline. Either way the lines still print to the real console.\n */\n captureIrisLogs?: boolean;\n}\n\n/** Everything the SDK logs about itself starts with this prefix. */\nconst isIrisLog = (args: unknown[]) =>\n typeof args[0] === 'string' && args[0].startsWith('[iris]');\n\n/** Wrap console methods so output shows up on the session timeline. */\nexport function collectConsole(\n push: (e: IrisEvent) => void,\n opts: ConsoleOptions = {},\n): () => void {\n const captureIrisLogs = opts.captureIrisLogs === true;\n const originals: Partial<Record<ConsoleEvent['level'], (...a: unknown[]) => void>> = {};\n\n for (const level of LEVELS) {\n originals[level] = console[level].bind(console);\n console[level] = (...args: unknown[]) => {\n try {\n if (captureIrisLogs || !isIrisLog(args)) {\n push({ type: 'console', ts: Date.now(), level, args: args.map(stringify) });\n }\n } catch {\n /* never let logging break logging */\n }\n originals[level]!(...args);\n };\n }\n\n return () => {\n for (const level of LEVELS) {\n if (originals[level]) console[level] = originals[level]!;\n }\n };\n}\n\nfunction stringify(v: unknown): string {\n if (typeof v === 'string') return v;\n try {\n return JSON.stringify(v);\n } catch {\n return String(v);\n }\n}\n","import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';\nimport type { IrisEvent, VitalName } from '@iris/types';\n\n/** Report Core Web Vitals as they settle. */\nexport function collectVitals(push: (e: IrisEvent) => void): void {\n const report = (metric: VitalName) => (m: { value: number; rating: string }) => {\n push({\n type: 'vital',\n ts: Date.now(),\n metric,\n value: m.value,\n rating: m.rating as 'good' | 'needs-improvement' | 'poor',\n });\n };\n\n onLCP(report('LCP'));\n onCLS(report('CLS'));\n onINP(report('INP'));\n onFCP(report('FCP'));\n onTTFB(report('TTFB'));\n}\n","import type { IrisEvent } from '@iris/types';\n\n/**\n * Track client-side route changes. SPAs navigate without a full page load, so\n * we patch the History API (`pushState`/`replaceState`) and listen for\n * `popstate`/`hashchange`. Each real URL change emits a NavigationEvent, which\n * the worker turns into a breadcrumb and a `page_count` bump.\n */\nexport function collectNavigation(push: (e: IrisEvent) => void): () => void {\n let current = location.href;\n\n const emit = () => {\n try {\n const to = location.href;\n if (to === current) return;\n const from = current;\n current = to;\n push({ type: 'navigation', ts: Date.now(), from, to });\n } catch {\n /* swallow — telemetry must never break the host app */\n }\n };\n\n const origPush = history.pushState;\n const origReplace = history.replaceState;\n\n history.pushState = function (\n ...args: Parameters<typeof history.pushState>\n ) {\n const ret = origPush.apply(this, args);\n emit();\n return ret;\n };\n history.replaceState = function (\n ...args: Parameters<typeof history.replaceState>\n ) {\n const ret = origReplace.apply(this, args);\n emit();\n return ret;\n };\n\n addEventListener('popstate', emit);\n addEventListener('hashchange', emit);\n\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n removeEventListener('popstate', emit);\n removeEventListener('hashchange', emit);\n };\n}\n","import type { ClickSignalKind, IrisEvent } from '@iris/types';\n\nexport interface FrustrationOptions {\n /** clicks within rageWindowMs + rageRadiusPx to count as a rage burst */\n rageThreshold?: number;\n rageWindowMs?: number;\n rageRadiusPx?: number;\n /** an actionable click with no DOM change / navigation within this window is \"dead\" */\n deadWindowMs?: number;\n /** an error within this window after a click makes it an \"error click\" */\n errorWindowMs?: number;\n}\n\n/**\n * Detect high-signal \"frustration\" clicks — the signature LogRocket metric:\n *\n * • rage — the user jabs the same spot repeatedly (something feels stuck)\n * • dead — a click on something that looks clickable but does nothing\n * • error — a click immediately followed by a JS error\n *\n * All detection is heuristic and best-effort; like every collector it swallows\n * its own errors so it can never break the host page.\n */\nexport function collectFrustration(\n push: (e: IrisEvent) => void,\n opts: FrustrationOptions = {},\n): () => void {\n const rageThreshold = opts.rageThreshold ?? 3;\n const rageWindowMs = opts.rageWindowMs ?? 1000;\n const rageRadiusPx = opts.rageRadiusPx ?? 30;\n const deadWindowMs = opts.deadWindowMs ?? 2500;\n const errorWindowMs = opts.errorWindowMs ?? 2500;\n\n let lastActivity = 0; // last DOM mutation / scroll\n let lastError = 0; // last window error / rejection\n let lastRageAt = 0; // when we last emitted a rage burst\n const recent: { x: number; y: number; t: number }[] = [];\n\n const bump = () => {\n lastActivity = Date.now();\n };\n const mo = new MutationObserver(bump);\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n addEventListener('scroll', bump, true);\n\n const onError = () => {\n lastError = Date.now();\n };\n addEventListener('error', onError, true);\n addEventListener('unhandledrejection', onError, true);\n\n const emit = (kind: ClickSignalKind, t: number, el: Element | null, x: number, y: number, count?: number) => {\n push({\n type: 'click',\n ts: t,\n kind,\n selector: describe(el),\n text: labelOf(el),\n x,\n y,\n ...(count ? { count } : {}),\n });\n };\n\n const onClick = (ev: MouseEvent) => {\n try {\n const now = Date.now();\n const x = ev.clientX;\n const y = ev.clientY;\n const el = ev.target as Element | null;\n\n // ── rage: repeated near-clicks in a short window ──\n while (recent.length && now - recent[0]!.t > rageWindowMs) recent.shift();\n const nearBefore = recent.filter((c) => Math.hypot(c.x - x, c.y - y) <= rageRadiusPx).length;\n recent.push({ x, y, t: now });\n if (nearBefore + 1 >= rageThreshold) {\n lastRageAt = now;\n emit('rage', now, el, x, y, nearBefore + 1);\n recent.length = 0; // one signal per burst\n }\n\n // Only treat isolated clicks as candidate dead/error clicks — repeated\n // near-clicks are a rage burst and shouldn't also spawn N dead signals.\n if (nearBefore > 0) return;\n\n // ── error click: an error fires shortly after the click ──\n const errBaseline = lastError;\n setTimeout(() => {\n if (lastError > errBaseline) emit('error', now, el, x, y);\n }, errorWindowMs);\n\n // ── dead click: actionable target, but nothing happened ──\n if (!deadEligible(el)) return;\n const activityBaseline = lastActivity;\n const urlAtClick = location.href;\n setTimeout(() => {\n const changed = lastActivity > activityBaseline || location.href !== urlAtClick;\n const becameError = lastError > errBaseline;\n if (!changed && !becameError && lastRageAt < now) emit('dead', now, el, x, y);\n }, deadWindowMs);\n } catch {\n /* swallow — telemetry must never break the host app */\n }\n };\n\n addEventListener('click', onClick, true);\n\n return () => {\n mo.disconnect();\n removeEventListener('scroll', bump, true);\n removeEventListener('error', onError, true);\n removeEventListener('unhandledrejection', onError, true);\n removeEventListener('click', onClick, true);\n };\n}\n\n/** A short, stable-ish description of an element: `tag#id.firstclass`. */\nfunction describe(el: Element | null): string {\n if (!el || !el.tagName) return 'unknown';\n let s = el.tagName.toLowerCase();\n if (el.id) s += `#${el.id}`;\n const cls = (el.getAttribute('class') ?? '').trim().split(/\\s+/).filter(Boolean)[0];\n if (cls) s += `.${cls}`;\n return s;\n}\n\n/** Privacy-safe label: aria-label or trimmed text, unless inside a masked/blocked region. */\nfunction labelOf(el: Element | null): string | undefined {\n if (!el) return undefined;\n try {\n if (el.closest('.twc-mask, .twc-block')) return undefined;\n const aria = el.getAttribute('aria-label');\n const raw = (aria ?? el.textContent ?? '').replace(/\\s+/g, ' ').trim();\n if (!raw) return undefined;\n return raw.length > 50 ? `${raw.slice(0, 50)}…` : raw;\n } catch {\n return undefined;\n }\n}\n\n/** Looks clickable but isn't a form control (clicking those is functional, not dead). */\nfunction deadEligible(el: Element | null): boolean {\n if (!el) return false;\n if (el.closest('input, textarea, select, label')) return false;\n if (el.closest('a, button, [role=\"button\"], [onclick]')) return true;\n try {\n return getComputedStyle(el).cursor === 'pointer';\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","record","__toCommonJS","sanitizeUser","input","out","sanitizeLevel","depth","budget","rawKey","rawVal","key","nested","displayUserId","user","k","v","getSessionId","KEY","existing","id","e","buildMeta","release","user","u","sanitizeUser","displayUserId","import_fflate","Transport","opts","__publicField","_a","_b","event","roughSize","useBeacon","events","batch","body","url","ok","e","res","err","user","u","sanitizeUser","displayUserId","args","merged","bytes","DEFAULT_REDACT_HEADERS","DEFAULT_MASK_KEYS","REDACTED","MASKED","createMasker","opts","_a","_b","_c","_d","redact","DEFAULT_REDACT_HEADERS","h","maskKeys","DEFAULT_MASK_KEYS","extraKeys","k","isMaskedKey","maxBody","baseHref","maskUrl","url","u","touched","e","maskHeaders","out","maskJson","v","val","truncate","s","cap","raw","masked","p","DEFAULT_MAX_EVENTS","DEFAULT_MAX_PROPS_CHARS","MAX_NAME_CHARS","createTracker","push","maskOpts","opts","_a","_b","masker","createMasker","maxEvents","maxPropsChars","count","warned","name","props","cleanName","ev","masked","size","e","import_rrweb","collectReplay","push","opts","_a","stop","event","collectErrors","push","onError","ev","_a","_b","_c","_d","onRejection","reason","MAX_READ_BYTES","collectNetwork","push","opts","_a","ignore","captureBodies","maskUrl","maskHeaders","maskBody","truncate","isMaskedKey","createMasker","shouldIgnore","url","m","headersToObj","h","out","v","k","pair","isTexty","ct","isEventStream","parseHeaderString","raw","line","i","formDataText","fd","parts","MASKED","requestBodyInfo","input","init","b","s","e","readTextCapped","res","body","text","reader","decoder","bytes","done","value","origFetch","args","_b","_c","_d","_e","start","rawUrl","method","reqInfo","ev","reqHeaders","len","resRead","rb","r","err","origOpen","origSend","origSetHeader","rest","name","ctx","sendArgs","rt","DEFAULT_MAX_FRAMES","DEFAULT_FRAME_CHARS","nextConn","collectWebSockets","push","opts","debug","_a","_b","_c","_d","masker","createMasker","maxFrames","frameChars","frameTypes","ignore","isJsonPayload","s","t","e","shouldCaptureFrame","data","shouldIgnore","url","m","emit","debugLog","args","frameInfo","OrigWS","wsPatched","origSend","IrisWebSocket","protocols","raw","connId","budget","c","boundOrigSend","ctx","OrigES","esPatched","IrisEventSource","init","state","record","eventName","origClose","type","listener","options","LEVELS","isIrisLog","args","collectConsole","push","opts","captureIrisLogs","originals","level","stringify","e","v","import_web_vitals","collectVitals","push","report","metric","m","collectNavigation","push","current","emit","to","from","e","origPush","origReplace","args","ret","collectFrustration","push","opts","_a","_b","_c","_d","_e","rageThreshold","rageWindowMs","rageRadiusPx","deadWindowMs","errorWindowMs","lastActivity","lastError","lastRageAt","recent","bump","mo","onError","emit","kind","t","el","x","y","count","describe","labelOf","onClick","ev","now","nearBefore","c","errBaseline","deadEligible","activityBaseline","urlAtClick","changed","becameError","e","s","cls","aria","raw","record","config","_a","_b","meta","buildMeta","transport","Transport","push","teardowns","track","createTracker","collectReplay","collectErrors","collectConsole","collectNavigation","collectFrustration","collectVitals","netOpts","collectNetwork","collectWebSockets","user","t"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/user.ts","../src/session.ts","../src/transport.ts","../src/collectors/masking.ts","../src/track.ts","../src/collectors/replay.ts","../src/collectors/errors.ts","../src/collectors/network.ts","../src/collectors/websocket.ts","../src/collectors/console.ts","../src/collectors/vitals.ts","../src/collectors/navigation.ts","../src/collectors/frustration.ts"],"sourcesContent":["import type { IrisUser } from '@iris/types';\r\nimport { buildMeta } from './session.js';\r\nimport { Transport } from './transport.js';\r\nimport { createTracker, type TrackFn, type TrackOptions } from './track.js';\r\nimport { collectReplay } from './collectors/replay.js';\r\nimport { collectErrors } from './collectors/errors.js';\r\nimport { collectNetwork, type NetworkOptions } from './collectors/network.js';\r\nimport { collectWebSockets } from './collectors/websocket.js';\r\nimport { collectConsole, type ConsoleOptions } from './collectors/console.js';\r\nimport { collectVitals } from './collectors/vitals.js';\r\nimport { collectNavigation } from './collectors/navigation.js';\r\nimport { collectFrustration } from './collectors/frustration.js';\r\n\r\nexport interface RecordConfig {\r\n /** Iris server origin, e.g. https://rum.twcfm.ca */\r\n endpoint: string;\r\n /** project public key (pk_live_…) */\r\n projectKey: string;\r\n /** build identifier, used to match source maps. Pass your git SHA. */\r\n release?: string;\r\n /** app-supplied user identity for this session — a small object of string\r\n (or nested) values, e.g. `{ id: 'staff-042', name: 'Daisy', station: { till: 'till-3' } }` */\r\n user?: IrisUser;\r\n /** legacy string form of `user` — still supported; same as `user: { id: … }` */\r\n userId?: string;\r\n /** continue an existing session instead of this tab's own — the `sessionId`\r\n of a handle on another of your origins, passed along when the app sends\r\n the visitor there (sessionStorage doesn't cross origins). Ignored unless\r\n it's a UUID. */\r\n sessionId?: string;\r\n\r\n // toggles (all on by default)\r\n replay?: boolean;\r\n errors?: boolean;\r\n /** `false` to skip console capture entirely, or options — same shape as\r\n `network`. `{ captureIrisLogs: true }` also records Iris's own `[iris]`\r\n debug lines (filtered out by default). */\r\n console?: false | ConsoleOptions;\r\n vitals?: boolean;\r\n navigation?: boolean;\r\n /** rage / dead / error click detection */\r\n frustration?: boolean;\r\n\r\n // privacy\r\n maskAllInputs?: boolean;\r\n maskTextSelector?: string;\r\n blockSelector?: string;\r\n network?: false | NetworkOptions;\r\n /** `false` disables `iris.track()` (it becomes a no-op), or tune its budgets */\r\n track?: false | TrackOptions;\r\n\r\n /** log capture/flush activity to the console — useful while wiring up */\r\n debug?: boolean;\r\n}\r\n\r\nexport interface IrisHandle {\r\n /** Stop recording and flush whatever's buffered. */\r\n stop: () => void;\r\n /** Force an immediate flush. */\r\n flush: () => void;\r\n /** Attach the signed-in user to this session (applies from the next flush).\r\n Takes a small user object — or a plain string id, the legacy form. */\r\n identify: (user: string | IrisUser) => void;\r\n /**\r\n * Record an app-defined business event, e.g.\r\n * `iris.track('receipt_printed', { total: 42.5 })`. Props are masked with\r\n * the same policy as network bodies (`maskKeys` / `maskExtraKeys`) and the\r\n * per-session event budget applies.\r\n */\r\n track: TrackFn;\r\n sessionId: string;\r\n}\r\n\r\n/**\r\n * Start recording. One call, drop it at the top of your app:\r\n *\r\n * import { record } from 'iris-recorder';\r\n * record({ endpoint: 'https://rum.twcfm.ca', projectKey: 'pk_live_…' });\r\n */\r\nexport function record(config: RecordConfig): IrisHandle {\r\n const meta = buildMeta(config.release, config.user ?? config.userId, config.sessionId);\r\n const transport = new Transport({\r\n endpoint: config.endpoint,\r\n projectKey: config.projectKey,\r\n meta,\r\n debug: config.debug,\r\n });\r\n const push = transport.push.bind(transport);\r\n const teardowns: Array<() => void> = [];\r\n\r\n // app-defined business events (`iris.track`) share the network masking policy;\r\n // `track: false` keeps the method callable but records nothing\r\n const track: TrackFn =\r\n config.track === false\r\n ? () => {}\r\n : createTracker(\r\n push,\r\n typeof config.network === 'object' ? config.network : {},\r\n typeof config.track === 'object' ? config.track : {},\r\n );\r\n\r\n if (config.replay !== false) {\r\n teardowns.push(\r\n collectReplay(push, {\r\n maskAllInputs: config.maskAllInputs ?? true,\r\n maskTextSelector: config.maskTextSelector,\r\n blockSelector: config.blockSelector,\r\n }),\r\n );\r\n }\r\n if (config.errors !== false) teardowns.push(collectErrors(push));\r\n if (config.console !== false) {\r\n teardowns.push(\r\n collectConsole(push, typeof config.console === 'object' ? config.console : {}),\r\n );\r\n }\r\n if (config.navigation !== false) teardowns.push(collectNavigation(push));\r\n if (config.frustration !== false) teardowns.push(collectFrustration(push));\r\n if (config.vitals !== false) collectVitals(push);\r\n\r\n if (config.network !== false) {\r\n // The collector applies its own header denylist + body/query masking; we just\r\n // make sure we never record our own ingest beacons. Match the ingest *path*,\r\n // not the whole origin — matching the origin would drop every app request to a\r\n // host Iris shares (e.g. app on localhost:3000, Iris on localhost). Callers can\r\n // override anything (captureBodies, maskKeys, redactHeaders, …) via `network: { … }`.\r\n const netOpts: NetworkOptions = {\r\n ignoreUrls: [`${config.endpoint}/v1/ingest`],\r\n ...(typeof config.network === 'object' ? config.network : {}),\r\n };\r\n teardowns.push(collectNetwork(push, netOpts));\r\n // streaming traffic shares the same masking/ignore config as fetch/XHR\r\n if (netOpts.captureWebSockets !== false) {\r\n teardowns.push(collectWebSockets(push, netOpts, config.debug));\r\n }\r\n }\r\n\r\n if (config.debug) {\r\n console.debug('[iris] recording', meta.sessionId, '→', config.endpoint);\r\n }\r\n\r\n return {\r\n sessionId: meta.sessionId,\r\n identify: (user: string | IrisUser) => transport.identify(user),\r\n track,\r\n flush: () => transport.flush(false),\r\n stop: () => {\r\n teardowns.forEach((t) => t());\r\n transport.stop();\r\n },\r\n };\r\n}\r\n\r\nexport { currentSessionId } from './session.js';\r\nexport type { ConsoleOptions, NetworkOptions, TrackOptions };\r\nexport type { IrisUser };\r\n","import type { IrisUser } from '@iris/types';\n\n// \"Small user object\" budgets — enforced before anything leaves the page.\nconst MAX_DEPTH = 3;\nconst MAX_KEYS = 32; // total, across all nesting levels\nconst MAX_KEY_CHARS = 64;\nconst MAX_VALUE_CHARS = 300;\n\n/**\n * Normalize whatever the app handed us into a small, well-shaped IrisUser:\n * strings pass through (truncated), numbers/booleans are stringified, nested\n * plain objects recurse up to MAX_DEPTH, everything else (arrays, functions,\n * null, …) is dropped. Returns undefined when nothing usable remains.\n */\nexport function sanitizeUser(input: unknown): IrisUser | undefined {\n const budget = { keys: MAX_KEYS };\n const out = sanitizeLevel(input, 1, budget);\n return out && Object.keys(out).length > 0 ? out : undefined;\n}\n\nfunction sanitizeLevel(\n input: unknown,\n depth: number,\n budget: { keys: number },\n): IrisUser | undefined {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const out: IrisUser = {};\n for (const [rawKey, rawVal] of Object.entries(input as Record<string, unknown>)) {\n if (budget.keys <= 0) break;\n const key = rawKey.slice(0, MAX_KEY_CHARS);\n if (!key) continue;\n if (typeof rawVal === 'string') {\n out[key] = rawVal.slice(0, MAX_VALUE_CHARS);\n budget.keys -= 1;\n } else if (typeof rawVal === 'number' || typeof rawVal === 'boolean') {\n out[key] = String(rawVal);\n budget.keys -= 1;\n } else if (\n typeof rawVal === 'object' &&\n rawVal !== null &&\n !Array.isArray(rawVal) &&\n depth < MAX_DEPTH\n ) {\n budget.keys -= 1; // the nested key itself spends budget\n const nested = sanitizeLevel(rawVal, depth + 1, budget);\n if (nested && Object.keys(nested).length > 0) out[key] = nested;\n else budget.keys += 1; // nothing usable inside — refund the key\n }\n // anything else (array, function, null, undefined, symbol) is dropped\n }\n return out;\n}\n\n/**\n * The one string that stands in for the user where only one fits — the legacy\n * `meta.userId` wire field and the `sessions.user_id` column. Prefers explicit\n * identifiers, then falls back to the first top-level string value.\n */\nexport function displayUserId(user: IrisUser): string | undefined {\n for (const k of ['id', 'userId', 'email', 'username', 'name']) {\n const v = user[k];\n if (typeof v === 'string' && v) return v;\n }\n for (const v of Object.values(user)) if (typeof v === 'string' && v) return v;\n return undefined;\n}\n","import type { IrisUser, SessionMeta } from '@iris/types';\nimport { displayUserId, sanitizeUser } from './user.js';\n\nconst KEY = 'iris.sid';\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** True when `id` has the shape of an Iris session id (a UUID). */\nexport function isSessionId(id: unknown): id is string {\n return typeof id === 'string' && UUID_RE.test(id);\n}\n\n/** The session id this tab is already recording under, without starting one.\n * Undefined in a fresh tab, or when sessionStorage is blocked. */\nexport function currentSessionId(): string | undefined {\n try {\n const existing = sessionStorage.getItem(KEY);\n return isSessionId(existing) ? existing : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Generate (or resume) a session id, kept in sessionStorage so a reload\n * within the same tab continues the same recording.\n *\n * `carried` continues a session begun elsewhere — sessionStorage is\n * per-origin, so an app that hands a visitor to another of its hosts passes\n * the id along. It replaces this tab's own id, so a reload keeps it too. */\nexport function getSessionId(carried?: string): string {\n try {\n if (isSessionId(carried)) {\n sessionStorage.setItem(KEY, carried);\n return carried;\n }\n const existing = currentSessionId();\n if (existing) return existing;\n const id = crypto.randomUUID();\n sessionStorage.setItem(KEY, id);\n return id;\n } catch {\n // sessionStorage blocked (private mode / sandbox) — ephemeral id\n return isSessionId(carried) ? carried : crypto.randomUUID();\n }\n}\n\nexport function buildMeta(\n release?: string,\n user?: IrisUser | string,\n sessionId?: string,\n): SessionMeta {\n // a plain-string id (the legacy form) is just a one-field user object\n const u = sanitizeUser(typeof user === 'string' ? { id: user } : user);\n return {\n sessionId: getSessionId(sessionId),\n user: u,\n // legacy mirror: pre-0.4 backends only know the string form\n userId: u ? displayUserId(u) : undefined,\n release,\n startedAt: Date.now(),\n url: location.href,\n userAgent: navigator.userAgent,\n };\n}\n","import { gzipSync, strToU8 } from 'fflate';\nimport type { IngestBatch, IrisEvent, IrisUser, SessionMeta } from '@iris/types';\nimport { displayUserId, sanitizeUser } from './user.js';\n\nexport interface TransportOptions {\n endpoint: string;\n projectKey: string;\n meta: SessionMeta;\n /** flush when the queued payload exceeds this many bytes (rough) */\n flushBytes?: number;\n /** flush at least this often */\n flushMs?: number;\n /** log flush activity to the console (off by default) */\n debug?: boolean;\n}\n\n/**\n * Batches events, gzips them, and ships them. The unload tail is the\n * whole game here: on `visibilitychange → hidden` we flush via\n * sendBeacon, which survives the page going away.\n */\nexport class Transport {\n private queue: IrisEvent[] = [];\n private bytes = 0;\n private timer: ReturnType<typeof setInterval> | undefined;\n private readonly flushBytes: number;\n /** cap the retry buffer so a downed server can never balloon host-page memory */\n private readonly maxBytes = 4_000_000;\n\n constructor(private readonly opts: TransportOptions) {\n this.flushBytes = opts.flushBytes ?? 64_000;\n\n this.timer = setInterval(() => this.flush(false), opts.flushMs ?? 5_000);\n\n addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') this.flush(true);\n });\n // pagehide is the most reliable \"tab is closing\" signal on mobile Safari\n addEventListener('pagehide', () => this.flush(true));\n }\n\n push(event: IrisEvent): void {\n this.queue.push(event);\n this.bytes += roughSize(event);\n if (this.bytes >= this.flushBytes) this.flush(false);\n }\n\n flush(useBeacon: boolean): void {\n if (this.queue.length === 0) return;\n\n // Detach the queue, but keep the reference so we can put it back if the\n // send fails. Dropping a batch outright loses data — and if it's the first\n // batch, it loses the rrweb full snapshot and the session can't play back.\n const events = this.queue;\n this.queue = [];\n this.bytes = 0;\n\n const batch: IngestBatch = { meta: this.opts.meta, events };\n const body = gzipSync(strToU8(JSON.stringify(batch)));\n const url = `${this.opts.endpoint}/v1/ingest`;\n this.log(`flush via ${useBeacon ? 'beacon' : 'fetch'}: ${events.length} events, ${body.length}b gz → ${url}`);\n\n if (useBeacon && 'sendBeacon' in navigator) {\n let ok = false;\n try {\n ok = navigator.sendBeacon(url, new Blob([body], { type: 'application/octet-stream' }));\n } catch {\n ok = false;\n }\n // Browser refused the beacon (size/quota) — keep the events for a later\n // flush. On a true unload there's no later flush, but the common case\n // (tab hidden, page still alive) recovers on the next interval/fetch.\n if (!ok) {\n this.log('beacon refused — re-queueing');\n this.requeue(events);\n }\n return;\n }\n\n // keepalive only on the unload path: it caps the body at ~64KB, so a normal\n // periodic flush must NOT set it or large snapshots silently fail to send.\n void fetch(url, {\n method: 'POST',\n body,\n keepalive: useBeacon,\n headers: {\n 'content-encoding': 'gzip',\n 'content-type': 'application/octet-stream',\n 'x-iris-key': this.opts.projectKey,\n },\n })\n .then((res) => {\n this.log(`ingest responded ${res.status}`);\n if (!res.ok) this.requeue(events);\n })\n .catch((err) => {\n // network error — telemetry must never throw; retry on the next flush\n this.log('ingest request failed', err);\n this.requeue(events);\n });\n }\n\n /** Attach or update the user identity carried on subsequent batches.\n Accepts the legacy plain-string id or a small user object. */\n identify(user: string | IrisUser): void {\n const u = sanitizeUser(typeof user === 'string' ? { id: user } : user);\n if (!u) return; // nothing usable — keep whatever identity we had\n this.opts.meta.user = u;\n this.opts.meta.userId = displayUserId(u);\n this.log(`identified as ${this.opts.meta.userId ?? '(unnamed user)'}`);\n }\n\n private log(...args: unknown[]): void {\n if (this.opts.debug) console.debug('[iris]', ...args);\n }\n\n /** Put un-sent events back at the front (preserving order), bounded by maxBytes. */\n private requeue(events: IrisEvent[]): void {\n const merged = events.concat(this.queue);\n let bytes = 0;\n for (const e of merged) bytes += roughSize(e);\n if (bytes > this.maxBytes) return; // give up rather than grow without bound\n this.queue = merged;\n this.bytes = bytes;\n }\n\n stop(): void {\n if (this.timer) clearInterval(this.timer);\n this.flush(true);\n }\n}\n\nfunction roughSize(event: IrisEvent): number {\n // cheap estimate; avoids JSON.stringify per push\n return JSON.stringify(event).length;\n}\n","/**\n * Shared masking policy for everything the SDK captures off the network:\n * URLs (query params), headers, and payload text. Used by the fetch/XHR\n * collector and the WebSocket/EventSource collector so one policy covers both.\n */\n\n// Headers stripped entirely — they carry credentials, never useful to keep.\nexport const DEFAULT_REDACT_HEADERS = [\n 'authorization',\n 'cookie',\n 'set-cookie',\n 'proxy-authorization',\n 'x-api-key',\n 'x-auth-token',\n 'x-csrf-token',\n 'x-xsrf-token',\n];\n\n// Body/query keys whose *values* look secret — masked in place, structure kept.\nexport const DEFAULT_MASK_KEYS =\n /pass(word|wd)?|secret|token|api[-_]?key|auth|credential|session|cvv|card[-_]?(number|num)?|ssn|pin/i;\n\nexport const DEFAULT_MAX_BODY = 10_000;\nexport const REDACTED = '[redacted]';\nexport const MASKED = '[masked]';\n\nexport interface MaskerOptions {\n /** extra header names to redact (case-insensitive); merged with the built-in denylist */\n redactHeaders?: string[];\n /** keys whose values are masked in JSON/form bodies and URL query strings */\n maskKeys?: RegExp;\n /**\n * Extra body/query key names to mask (exact, case-insensitive). Additive:\n * merged with the default pattern (or with `maskKeys` if that's overridden).\n */\n maskExtraKeys?: string[];\n /** max captured length (chars) for each body; longer bodies are truncated */\n maxBodyChars?: number;\n}\n\nexport function createMasker(opts: MaskerOptions = {}) {\n const redact = new Set([\n ...DEFAULT_REDACT_HEADERS,\n ...(opts.redactHeaders ?? []).map((h) => h.toLowerCase()),\n ]);\n const maskKeys = opts.maskKeys ?? DEFAULT_MASK_KEYS;\n const extraKeys = new Set((opts.maskExtraKeys ?? []).map((k) => k.toLowerCase()));\n /** the one key test every masking path goes through */\n const isMaskedKey = (k: string) => maskKeys.test(k) || extraKeys.has(k.toLowerCase());\n const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY;\n\n const baseHref = () => (typeof location !== 'undefined' ? location.href : undefined);\n\n const maskUrl = (url: string): string => {\n try {\n const u = new URL(url, baseHref());\n let touched = false;\n for (const k of [...u.searchParams.keys()]) {\n if (isMaskedKey(k)) {\n u.searchParams.set(k, MASKED);\n touched = true;\n }\n }\n return touched ? u.toString() : url;\n } catch {\n return url;\n }\n };\n\n const maskHeaders = (h: Record<string, string>): Record<string, string> => {\n const out: Record<string, string> = {};\n for (const k of Object.keys(h)) out[k] = redact.has(k.toLowerCase()) ? REDACTED : h[k]!;\n return out;\n };\n\n const maskJson = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(maskJson);\n if (v && typeof v === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n out[k] = isMaskedKey(k) ? MASKED : maskJson(val);\n }\n return out;\n }\n return v;\n };\n\n const truncate = (s: string, cap = maxBody) =>\n s.length > cap ? `${s.slice(0, cap)}…[+${s.length - cap} chars]` : s;\n\n /** Mask a JSON or urlencoded payload; anything else passes through (truncated). */\n const maskBody = (raw: unknown, cap = maxBody): string | undefined => {\n if (raw == null || raw === '') return undefined;\n const s = String(raw);\n let masked: string;\n try {\n masked = JSON.stringify(maskJson(JSON.parse(s)));\n } catch {\n // urlencoded form body? mask matching keys; otherwise leave as-is\n if (/^[^={}\\s]+=[^&]*(?:&[^=]+=[^&]*)*$/.test(s)) {\n try {\n const p = new URLSearchParams(s);\n for (const k of [...p.keys()]) if (isMaskedKey(k)) p.set(k, MASKED);\n masked = p.toString();\n } catch {\n masked = s;\n }\n } else {\n masked = s;\n }\n }\n return truncate(masked, cap);\n };\n\n /** Recursively mask secret-looking keys in a plain value (objects stay objects). */\n const maskObject = (v: unknown): unknown => maskJson(v);\n\n return { maskUrl, maskHeaders, maskBody, maskObject, truncate, isMaskedKey, maxBodyChars: maxBody };\n}\n\nexport type Masker = ReturnType<typeof createMasker>;\n","import type { IrisEvent, TrackEvent } from '@iris/types';\nimport { createMasker, type MaskerOptions } from './collectors/masking.js';\n\nconst DEFAULT_MAX_EVENTS = 500;\nconst DEFAULT_MAX_PROPS_CHARS = 8_000;\nconst MAX_NAME_CHARS = 120;\n\nexport interface TrackOptions {\n /** per-session budget of tracked events — a loop can't flood the session (default 500) */\n maxEvents?: number;\n /** serialized-props size cap in chars; oversized props become a note (default 8 000) */\n maxPropsChars?: number;\n}\n\nexport type TrackFn = (name: string, props?: Record<string, unknown>) => void;\n\n/**\n * Build the `iris.track()` function: app-defined business events\n * (`track('receipt_printed', { total: 42.5 })`) that land on the session\n * timeline. Props go through the same masking policy as network bodies\n * (`maskKeys` / `maskExtraKeys`), get size-capped, and the whole thing is\n * budgeted per session. Never throws into the host app.\n */\nexport function createTracker(\n push: (e: IrisEvent) => void,\n maskOpts: MaskerOptions = {},\n opts: TrackOptions = {},\n): TrackFn {\n const masker = createMasker(maskOpts);\n const maxEvents = opts.maxEvents ?? DEFAULT_MAX_EVENTS;\n const maxPropsChars = opts.maxPropsChars ?? DEFAULT_MAX_PROPS_CHARS;\n let count = 0;\n let warned = false;\n\n return (name: string, props?: Record<string, unknown>) => {\n try {\n const cleanName = typeof name === 'string' ? name.trim().slice(0, MAX_NAME_CHARS) : '';\n if (!cleanName) return;\n\n if (count >= maxEvents) {\n if (!warned) {\n warned = true;\n // eslint-disable-next-line no-console\n console.warn(\n `[iris] track() budget exhausted (${maxEvents} events) — further events are dropped`,\n );\n }\n return;\n }\n count++;\n\n const ev: TrackEvent = { type: 'custom', ts: Date.now(), name: cleanName };\n if (props != null && typeof props === 'object') {\n // masking recurses and stringify measures — both can throw on circular\n // structures; the event survives with a note either way\n let masked: Record<string, unknown> | null = null;\n let size = Infinity;\n try {\n masked = masker.maskObject(props) as Record<string, unknown>;\n size = JSON.stringify(masked).length;\n } catch {\n masked = null;\n }\n ev.props =\n masked != null && size <= maxPropsChars\n ? masked\n : { '[iris]': `props dropped (${Number.isFinite(size) ? `${size} chars > ${maxPropsChars}` : 'not serializable'})` };\n }\n push(ev);\n } catch {\n /* telemetry must never break the host app */\n }\n };\n}\n","import { record } from 'rrweb';\r\nimport type { IrisEvent } from '@iris/types';\r\n\r\nexport interface ReplayOptions {\r\n maskAllInputs?: boolean;\r\n /** CSS selector whose text is masked in the replay */\r\n maskTextSelector?: string;\r\n /** CSS selector whose subtree is blocked (rendered as a placeholder) */\r\n blockSelector?: string;\r\n}\r\n\r\n/** Wire rrweb up to the transport. Returns a stop() fn. */\r\nexport function collectReplay(\r\n push: (e: IrisEvent) => void,\r\n opts: ReplayOptions = {},\r\n): () => void {\r\n const stop = record({\r\n emit(event) {\r\n push({ type: 'replay', ts: Date.now(), data: event });\r\n },\r\n maskAllInputs: opts.maskAllInputs ?? true,\r\n maskTextSelector: opts.maskTextSelector,\r\n blockSelector: opts.blockSelector,\r\n // sample mousemove/scroll so we don't flood the timeline\r\n sampling: { mousemove: 50, scroll: 150, input: 'last' },\r\n // Emit a fresh full snapshot periodically. If the opening segment is ever\r\n // lost in transit, playback can still start from a later checkpoint — and\r\n // it makes seeking in long sessions much faster.\r\n checkoutEveryNms: 30_000,\r\n });\r\n\r\n return () => stop?.();\r\n}\r\n","import type { IrisEvent } from '@iris/types';\n\n/** Capture uncaught exceptions and unhandled promise rejections. */\nexport function collectErrors(push: (e: IrisEvent) => void): () => void {\n const onError = (ev: globalThis.ErrorEvent) => {\n push({\n type: 'error',\n ts: Date.now(),\n source: 'error',\n name: ev.error?.name ?? 'Error',\n message: ev.message ?? String(ev.error),\n stack: ev.error?.stack,\n });\n };\n\n const onRejection = (ev: PromiseRejectionEvent) => {\n const reason = ev.reason;\n push({\n type: 'error',\n ts: Date.now(),\n source: 'unhandledrejection',\n name: reason?.name ?? 'UnhandledRejection',\n message: reason?.message ?? String(reason),\n stack: reason?.stack,\n });\n };\n\n addEventListener('error', onError);\n addEventListener('unhandledrejection', onRejection);\n\n return () => {\n removeEventListener('error', onError);\n removeEventListener('unhandledrejection', onRejection);\n };\n}\n","import type { IrisEvent, NetworkEvent } from '@iris/types';\nimport { createMasker, MASKED } from './masking.js';\n\nexport interface NetworkOptions {\n /**\n * Capture request/response headers + bodies (on by default). Iris monitors\n * first-party internal apps, so bodies are useful for debugging — but they're\n * always masked (see below) so secrets never leave the page.\n */\n captureBodies?: boolean;\n /** extra header names to redact (case-insensitive); merged with the built-in denylist */\n redactHeaders?: string[];\n /** keys whose values are masked in JSON/form bodies and URL query strings; overrides the default */\n maskKeys?: RegExp;\n /** extra key names to mask in req/res bodies + query strings (exact, case-insensitive); additive */\n maskExtraKeys?: string[];\n /** max captured length (chars) for each body; longer bodies are truncated */\n maxBodyChars?: number;\n /** don't record calls to our own ingest endpoint */\n ignoreUrls?: (string | RegExp)[];\n /** capture WebSocket + EventSource traffic (on by default) — see websocket.ts */\n captureWebSockets?: boolean;\n /** per-connection, per-direction frame budget before frames are dropped (default 200) */\n wsMaxFrames?: number;\n /** max captured length (chars) per WebSocket/SSE frame (default 2 000) */\n wsMaxFrameChars?: number;\n /**\n * Which socket frame kinds to record (connection lifecycle is always kept):\n * 'text' = every text frame · 'json' = only text that parses to an\n * object/array · 'binary' = binary frames (size-only). Default\n * `['text', 'binary']`; e.g. `['json']` skips binary and unstructured text\n * (heartbeats). Filtered frames don't consume the frame budget.\n */\n wsFrameTypes?: Array<'text' | 'json' | 'binary'>;\n}\n\n// Don't pull megabytes into memory just to truncate them. Applies both to\n// content-length'd responses and (via the capped stream reader) chunked ones.\nconst MAX_READ_BYTES = 512_000;\n\n/** What a request carried, extracted without consuming the caller's body. */\ninterface ReqBodyInfo {\n text?: string;\n /** body that must be read async (a cloned Request); resolves undefined on failure */\n textPromise?: Promise<string | undefined>;\n size?: number;\n /** text was already masked during extraction (FormData) — only truncate it */\n premasked?: boolean;\n}\n\n/** Monkey-patch fetch + XHR to time, log, and (masked) capture requests. */\nexport function collectNetwork(\n push: (e: IrisEvent) => void,\n opts: NetworkOptions = {},\n): () => void {\n const ignore = opts.ignoreUrls ?? [];\n const captureBodies = opts.captureBodies !== false; // default ON\n const { maskUrl, maskHeaders, maskBody, truncate, isMaskedKey } = createMasker(opts);\n\n // Hardened: only ever match a string, so a non-string url can't throw in `.some`.\n const shouldIgnore = (url: string) =>\n typeof url === 'string' &&\n ignore.some((m) => (typeof m === 'string' ? url.includes(m) : m.test(url)));\n\n // ── header / body extraction ─────────────────────────────────────\n const headersToObj = (h: unknown): Record<string, string> => {\n const out: Record<string, string> = {};\n if (!h) return out;\n if (typeof Headers !== 'undefined' && h instanceof Headers) {\n h.forEach((v, k) => (out[k] = v));\n } else if (Array.isArray(h)) {\n for (const pair of h) if (pair && pair.length >= 2) out[String(pair[0])] = String(pair[1]);\n } else if (typeof h === 'object') {\n for (const [k, v] of Object.entries(h as Record<string, unknown>)) out[k] = String(v);\n }\n return out;\n };\n\n const isTexty = (ct: string | undefined) =>\n !!ct && /(json|text|xml|javascript|x-www-form-urlencoded|html)/i.test(ct);\n // SSE must never be body-read: text() on an open stream buffers it forever\n // and the network event would only push when the stream finally dies.\n const isEventStream = (ct: string | undefined) => !!ct && /event-stream/i.test(ct);\n\n const parseHeaderString = (raw: string | null): Record<string, string> => {\n const out: Record<string, string> = {};\n if (!raw) return out;\n for (const line of raw.trim().split(/[\\r\\n]+/)) {\n const i = line.indexOf(':');\n if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim();\n }\n return out;\n };\n\n /** Flatten FormData to `k=v&…` lines, masking secret keys and stubbing files. */\n const formDataText = (fd: FormData): string => {\n const parts: string[] = [];\n fd.forEach((v, k) => {\n if (typeof v === 'string') parts.push(`${k}=${isMaskedKey(k) ? MASKED : v}`);\n else parts.push(`${k}=[file ${v.name || 'blob'} ${v.size}B]`);\n });\n return parts.join('&');\n };\n\n /**\n * Extract what we can from a request body without consuming it. Handles the\n * init body (string/URLSearchParams/FormData sync; binary size-only) and a\n * body carried inside a Request object (cloned, read async).\n */\n const requestBodyInfo = (input: unknown, init?: RequestInit): ReqBodyInfo => {\n const b = init?.body;\n if (typeof b === 'string') return { text: b, size: b.length };\n if (typeof URLSearchParams !== 'undefined' && b instanceof URLSearchParams) {\n const s = b.toString();\n return { text: s, size: s.length };\n }\n if (typeof FormData !== 'undefined' && b instanceof FormData) {\n const s = formDataText(b);\n return { text: s, size: s.length, premasked: true };\n }\n if (typeof Blob !== 'undefined' && b instanceof Blob) return { size: b.size };\n if (typeof ArrayBuffer !== 'undefined') {\n if (b instanceof ArrayBuffer) return { size: b.byteLength };\n if (ArrayBuffer.isView(b as object)) return { size: (b as ArrayBufferView).byteLength };\n }\n // body carried inside a Request object: fetch(new Request(url, { body }))\n if (b == null && typeof Request !== 'undefined' && input instanceof Request) {\n const m = input.method.toUpperCase();\n if (m !== 'GET' && m !== 'HEAD' && !input.bodyUsed) {\n const ct = input.headers.get('content-type') ?? undefined;\n try {\n if (ct && /multipart\\/form-data/i.test(ct)) {\n return {\n textPromise: input\n .clone()\n .formData()\n .then(formDataText)\n .catch(() => undefined),\n premasked: true,\n };\n }\n if (isTexty(ct)) {\n return { textPromise: input.clone().text().catch(() => undefined) };\n }\n } catch {\n /* clone can throw on locked/streamed bodies — skip capture */\n }\n }\n }\n return {};\n };\n\n /**\n * Read a cloned response as text with a hard byte cap, so chunked responses\n * without a content-length can't buffer unbounded memory. Cancels the clone's\n * stream once the cap is hit.\n */\n const readTextCapped = async (\n res: Response,\n ): Promise<{ text: string; bytes: number; complete: boolean }> => {\n const body = res.body;\n if (!body || typeof body.getReader !== 'function') {\n const text = await res.text();\n return { text, bytes: text.length, complete: true };\n }\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let text = '';\n let bytes = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return { text: text + decoder.decode(), bytes, complete: true };\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_READ_BYTES) {\n void reader.cancel().catch(() => {});\n return { text, bytes, complete: false };\n }\n }\n };\n\n // ── fetch ──\n const origFetch = window.fetch;\n window.fetch = async function (...args: Parameters<typeof fetch>) {\n const start = Date.now();\n const input = args[0];\n const rawUrl =\n typeof input === 'string' ? input\n : input instanceof URL ? input.href\n : input instanceof Request ? input.url\n : String(input);\n const method = (\n args[1]?.method ?? (input instanceof Request ? input.method : 'GET')\n ).toUpperCase();\n\n // Snapshot the request body BEFORE dispatch — a Request-object body must be\n // cloned while its stream is still unread.\n let reqInfo: ReqBodyInfo = {};\n if (captureBodies && !shouldIgnore(rawUrl)) {\n try {\n reqInfo = requestBodyInfo(input, args[1]);\n } catch {\n reqInfo = {};\n }\n }\n\n try {\n const res = await origFetch.apply(this, args);\n // Capturing must never reject a successful fetch — swallow any failure.\n try {\n if (!shouldIgnore(rawUrl)) {\n const ev: NetworkEvent = {\n type: 'network',\n ts: start,\n method,\n url: maskUrl(rawUrl),\n status: res.status,\n durationMs: Date.now() - start,\n initiator: 'fetch',\n };\n if (!captureBodies) {\n push(ev);\n } else {\n const reqHeaders =\n args[1]?.headers ?? (input instanceof Request ? input.headers : undefined);\n ev.reqHeaders = maskHeaders(headersToObj(reqHeaders));\n ev.resHeaders = maskHeaders(headersToObj(res.headers));\n const ct = res.headers.get('content-type') ?? undefined;\n if (ct) ev.contentType = ct;\n const len = Number(res.headers.get('content-length'));\n if (Number.isFinite(len)) ev.resSize = len;\n\n // clone BEFORE returning res so the caller's body is untouched\n let resRead: Promise<{ text: string; bytes: number; complete: boolean }> | null = null;\n if (\n isTexty(ct) &&\n !isEventStream(ct) &&\n !(Number.isFinite(len) && len > MAX_READ_BYTES)\n ) {\n resRead = readTextCapped(res.clone());\n }\n\n // finish (and push) in the background once the async reads settle\n void (async () => {\n try {\n const rb = reqInfo.text ?? (reqInfo.textPromise ? await reqInfo.textPromise : undefined);\n if (rb) {\n ev.reqBody = reqInfo.premasked ? truncate(rb) : maskBody(rb);\n ev.reqSize = reqInfo.size ?? rb.length;\n } else if (reqInfo.size != null) {\n ev.reqSize = reqInfo.size; // binary body — size only\n }\n if (resRead) {\n const r = await resRead;\n if (r.text) ev.resBody = maskBody(r.text);\n if (!Number.isFinite(len)) ev.resSize = r.bytes;\n }\n } catch {\n /* capture failure must not block the event */\n }\n push(ev);\n })();\n }\n }\n } catch {\n /* logging failure must not reject a successful fetch */\n }\n return res;\n } catch (err) {\n try {\n if (!shouldIgnore(rawUrl)) {\n push({\n type: 'network',\n ts: start,\n method,\n url: maskUrl(rawUrl),\n status: 0,\n durationMs: Date.now() - start,\n initiator: 'fetch',\n });\n }\n } catch {\n /* logging failure must not alter the original rejection */\n }\n throw err;\n }\n };\n\n // ── XMLHttpRequest ──\n const origOpen = XMLHttpRequest.prototype.open;\n const origSend = XMLHttpRequest.prototype.send;\n const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;\n\n XMLHttpRequest.prototype.open = function (\n this: XMLHttpRequest,\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n (this as any).__iris = {\n method: String(method).toUpperCase(),\n url: String(url),\n start: 0,\n reqHeaders: {} as Record<string, string>,\n reqBody: undefined as string | undefined,\n reqSize: undefined as number | undefined,\n premasked: false,\n };\n return (origOpen as (...a: unknown[]) => void).apply(this, [method, url, ...rest]);\n };\n\n XMLHttpRequest.prototype.setRequestHeader = function (\n this: XMLHttpRequest,\n name: string,\n value: string,\n ) {\n const ctx = (this as any).__iris;\n if (ctx) ctx.reqHeaders[name] = value;\n return origSetHeader.apply(this, [name, value]);\n };\n\n XMLHttpRequest.prototype.send = function (this: XMLHttpRequest, ...sendArgs: any[]) {\n const ctx = (this as any).__iris;\n if (ctx) {\n ctx.start = Date.now();\n const body = sendArgs[0];\n if (typeof body === 'string') {\n ctx.reqBody = body;\n ctx.reqSize = body.length;\n } else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {\n ctx.reqBody = body.toString();\n ctx.reqSize = ctx.reqBody.length;\n } else if (typeof FormData !== 'undefined' && body instanceof FormData) {\n ctx.reqBody = formDataText(body);\n ctx.reqSize = ctx.reqBody.length;\n ctx.premasked = true;\n } else if (typeof Blob !== 'undefined' && body instanceof Blob) {\n ctx.reqSize = body.size;\n } else if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {\n ctx.reqSize = body.byteLength;\n } else if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) {\n ctx.reqSize = (body as ArrayBufferView).byteLength;\n }\n\n this.addEventListener('loadend', () => {\n try {\n if (shouldIgnore(ctx.url)) return;\n const ev: NetworkEvent = {\n type: 'network',\n ts: ctx.start,\n method: ctx.method,\n url: maskUrl(ctx.url),\n status: this.status,\n durationMs: Date.now() - ctx.start,\n initiator: 'xhr',\n };\n if (captureBodies) {\n ev.reqHeaders = maskHeaders(ctx.reqHeaders);\n ev.resHeaders = maskHeaders(parseHeaderString(this.getAllResponseHeaders()));\n const ct = this.getResponseHeader('content-type') ?? undefined;\n if (ct) ev.contentType = ct;\n if (ctx.reqBody != null) {\n ev.reqBody = ctx.premasked ? truncate(ctx.reqBody) : maskBody(ctx.reqBody);\n }\n if (ctx.reqSize != null) ev.reqSize = ctx.reqSize;\n const rt = this.responseType;\n if (rt === '' || rt === 'text') {\n try {\n const text = this.responseText;\n if (text) {\n ev.resBody = maskBody(text);\n ev.resSize = text.length;\n }\n } catch {\n /* responseText not accessible — skip body */\n }\n } else if (rt === 'json') {\n try {\n const r = this.response;\n if (r != null) {\n const s = JSON.stringify(r);\n ev.resBody = maskBody(s);\n ev.resSize = s.length;\n }\n } catch {\n /* response not serializable — skip body */\n }\n } else if (rt === 'arraybuffer') {\n const r = this.response as ArrayBuffer | null;\n if (r) ev.resSize = r.byteLength;\n } else if (rt === 'blob') {\n const r = this.response as Blob | null;\n if (r) ev.resSize = r.size;\n }\n }\n push(ev);\n } catch {\n /* swallow — instrumentation never throws into the host */\n }\n });\n }\n return origSend.apply(this, sendArgs as []);\n };\n\n return () => {\n window.fetch = origFetch;\n XMLHttpRequest.prototype.open = origOpen;\n XMLHttpRequest.prototype.send = origSend;\n XMLHttpRequest.prototype.setRequestHeader = origSetHeader;\n };\n}\n","import type { IrisEvent, WsEvent } from '@iris/types';\nimport { createMasker } from './masking.js';\nimport type { NetworkOptions } from './network.js';\n\nconst DEFAULT_MAX_FRAMES = 200;\nconst DEFAULT_FRAME_CHARS = 2_000;\n\n// Module-level so ids stay unique even across record() teardown/restart.\nlet nextConn = 0;\n\n/** Per-connection instrumentation state stashed on the socket instance. */\ninterface WsCtx {\n connId: string;\n budget: { sent: number; recv: number; dropped: number };\n}\n\n/**\n * Patch window.WebSocket (and EventSource) to record connection lifecycles and\n * frames. Subclassing keeps `instanceof WebSocket`, readyState, binaryType and\n * the static constants working, so socket libraries are unaffected. Outbound\n * capture hooks `WebSocket.prototype.send` (not a subclass override), so even\n * libraries that call `WebSocket.prototype.send.call(sock, …)` are seen; the\n * hook is a no-op for sockets we didn't instrument.\n *\n * EventSource: the default `message` event AND any named event the app\n * subscribes to (`es.addEventListener('order-update', …)`) are recorded — named\n * frames carry the event name.\n *\n * Volume is the hazard with sockets (heartbeats, tickers), so every connection\n * has a per-direction frame budget; frames past it are counted and reported as\n * `dropped` on the close event. Text frames are masked with the same policy as\n * network bodies; binary frames record size only.\n */\nexport function collectWebSockets(\n push: (e: IrisEvent) => void,\n opts: NetworkOptions = {},\n debug = false,\n): () => void {\n const masker = createMasker(opts);\n const maxFrames = opts.wsMaxFrames ?? DEFAULT_MAX_FRAMES;\n const frameChars = opts.wsMaxFrameChars ?? DEFAULT_FRAME_CHARS;\n const frameTypes = new Set(opts.wsFrameTypes ?? ['text', 'binary']);\n const ignore = opts.ignoreUrls ?? [];\n\n /** 'json' means structured payloads — objects/arrays, not bare scalars. */\n const isJsonPayload = (s: string): boolean => {\n const t = s.trim();\n if (!t.startsWith('{') && !t.startsWith('[')) return false;\n try {\n JSON.parse(t);\n return true;\n } catch {\n return false;\n }\n };\n\n /** Frame-kind filter (wsFrameTypes). Filtered frames never touch the budget. */\n const shouldCaptureFrame = (data: unknown): boolean => {\n if (typeof data === 'string') {\n if (frameTypes.has('text')) return true;\n if (frameTypes.has('json')) return isJsonPayload(data);\n return false;\n }\n return frameTypes.has('binary');\n };\n\n const shouldIgnore = (url: string) =>\n typeof url === 'string' &&\n ignore.some((m) => (typeof m === 'string' ? url.includes(m) : m.test(url)));\n\n // Telemetry must never throw into the host app.\n const emit = (e: WsEvent) => {\n try {\n push(e);\n } catch {\n /* swallow */\n }\n };\n\n // `[iris]`-prefixed, so the console collector keeps it out of sessions.\n const debugLog = (...args: unknown[]) => {\n if (debug) console.debug('[iris]', ...args);\n };\n\n const frameInfo = (data: unknown): Pick<WsEvent, 'data' | 'size' | 'binary'> => {\n if (typeof data === 'string') return { data: masker.maskBody(data, frameChars), size: data.length };\n if (typeof Blob !== 'undefined' && data instanceof Blob) return { binary: true, size: data.size };\n if (typeof ArrayBuffer !== 'undefined') {\n if (data instanceof ArrayBuffer) return { binary: true, size: data.byteLength };\n if (ArrayBuffer.isView(data as object))\n return { binary: true, size: (data as ArrayBufferView).byteLength };\n }\n return {};\n };\n\n // ── WebSocket ────────────────────────────────────────────────────\n const OrigWS = window.WebSocket;\n let wsPatched = false;\n let origSend: typeof WebSocket.prototype.send | undefined;\n if (typeof OrigWS === 'function') {\n wsPatched = true;\n\n class IrisWebSocket extends OrigWS {\n constructor(url: string | URL, protocols?: string | string[]) {\n super(url as string, protocols);\n try {\n const raw = typeof url === 'string' ? url : String(url);\n if (shouldIgnore(raw)) return;\n const connId = `ws${++nextConn}`;\n const budget = { sent: 0, recv: 0, dropped: 0 };\n (this as unknown as { __iris?: WsCtx }).__iris = { connId, budget };\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'open',\n transport: 'websocket',\n url: masker.maskUrl(raw),\n });\n this.addEventListener('message', (m: MessageEvent) => {\n if (!shouldCaptureFrame(m.data)) return;\n if (budget.recv >= maxFrames) {\n budget.dropped++;\n return;\n }\n budget.recv++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'message',\n transport: 'websocket',\n ...frameInfo(m.data),\n });\n });\n this.addEventListener('close', (c: CloseEvent) => {\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'close',\n transport: 'websocket',\n code: c.code,\n reason: c.reason || undefined,\n dropped: budget.dropped || undefined,\n });\n debugLog(\n `ws ${connId} closed (${c.code}) — frames ↑${budget.sent} ↓${budget.recv}, dropped ${budget.dropped}`,\n );\n });\n this.addEventListener('error', () => {\n emit({ type: 'ws', ts: Date.now(), connId, op: 'error', transport: 'websocket' });\n });\n } catch {\n /* instrumentation must never break the socket */\n }\n }\n }\n\n // Outbound frames: hook the *prototype* so calls that bypass the subclass\n // (saved method refs, `.call` on the base prototype) are still captured.\n origSend = OrigWS.prototype.send;\n const boundOrigSend = origSend;\n OrigWS.prototype.send = function (this: WebSocket, data: Parameters<WebSocket['send']>[0]) {\n try {\n const ctx = (this as unknown as { __iris?: WsCtx }).__iris;\n if (ctx && shouldCaptureFrame(data)) {\n if (ctx.budget.sent >= maxFrames) {\n ctx.budget.dropped++;\n } else {\n ctx.budget.sent++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId: ctx.connId,\n op: 'send',\n transport: 'websocket',\n ...frameInfo(data),\n });\n }\n }\n } catch {\n /* swallow */\n }\n return boundOrigSend.call(this, data);\n };\n\n window.WebSocket = IrisWebSocket as unknown as typeof WebSocket;\n }\n\n // ── EventSource ──────────────────────────────────────────────────\n const OrigES = (window as { EventSource?: typeof EventSource }).EventSource;\n let esPatched = false;\n if (typeof OrigES === 'function') {\n esPatched = true;\n\n /** Per-instance stream state for named-event mirroring. */\n interface EsCtx {\n record: (m: MessageEvent, eventName?: string) => void;\n mirrored: Set<string>;\n }\n\n class IrisEventSource extends OrigES {\n constructor(url: string | URL, init?: EventSourceInit) {\n super(url as string, init);\n try {\n const raw = typeof url === 'string' ? url : String(url);\n if (shouldIgnore(raw)) return;\n const connId = `es${++nextConn}`;\n const state = { recv: 0, dropped: 0 };\n const record = (m: MessageEvent, eventName?: string) => {\n if (!shouldCaptureFrame(m.data)) return;\n if (state.recv >= maxFrames) {\n state.dropped++;\n return;\n }\n state.recv++;\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'message',\n transport: 'eventsource',\n ...(eventName ? { event: eventName } : {}),\n ...frameInfo(m.data),\n });\n };\n (this as unknown as { __iris?: EsCtx }).__iris = { record, mirrored: new Set() };\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'open',\n transport: 'eventsource',\n url: masker.maskUrl(raw),\n });\n super.addEventListener('message', (m: MessageEvent) => record(m));\n super.addEventListener('error', () => {\n emit({ type: 'ws', ts: Date.now(), connId, op: 'error', transport: 'eventsource' });\n });\n // EventSource has no close event — closing is a client-side call.\n const origClose = this.close.bind(this);\n this.close = () => {\n try {\n emit({\n type: 'ws',\n ts: Date.now(),\n connId,\n op: 'close',\n transport: 'eventsource',\n dropped: state.dropped || undefined,\n });\n debugLog(`es ${connId} closed — frames ↓${state.recv}, dropped ${state.dropped}`);\n } catch {\n /* swallow */\n }\n origClose();\n };\n } catch {\n /* instrumentation must never break the stream */\n }\n }\n\n // Named SSE events (`es.addEventListener('order-update', …)`) never fire\n // the default `message` event — mirror whatever names the app subscribes\n // to so those frames are recorded too. Params are typed loosely because\n // the DOM lib's overload set won't unify with a single override signature.\n override addEventListener(type: string, listener: unknown, options?: unknown): void {\n try {\n const ctx = (this as unknown as { __iris?: EsCtx }).__iris;\n if (\n ctx &&\n type !== 'message' &&\n type !== 'error' &&\n type !== 'open' &&\n !ctx.mirrored.has(type)\n ) {\n ctx.mirrored.add(type);\n super.addEventListener(type, ((m: Event) =>\n ctx.record(m as MessageEvent, type)) as EventListener);\n }\n } catch {\n /* swallow */\n }\n super.addEventListener(\n type,\n listener as EventListenerOrEventListenerObject,\n options as boolean | AddEventListenerOptions | undefined,\n );\n }\n }\n\n (window as { EventSource?: typeof EventSource }).EventSource =\n IrisEventSource as unknown as typeof EventSource;\n }\n\n return () => {\n if (wsPatched) {\n window.WebSocket = OrigWS;\n if (origSend) OrigWS.prototype.send = origSend;\n }\n if (esPatched) (window as { EventSource?: typeof EventSource }).EventSource = OrigES;\n };\n}\n","import type { ConsoleEvent, IrisEvent } from '@iris/types';\n\nconst LEVELS: ConsoleEvent['level'][] = ['log', 'info', 'warn', 'error', 'debug'];\n\nexport interface ConsoleOptions {\n /**\n * Also record Iris's own `[iris]`-prefixed output (the `debug: true`\n * flush/ingest chatter) into the session. Off by default — it floods the\n * timeline. Either way the lines still print to the real console.\n */\n captureIrisLogs?: boolean;\n}\n\n/** Everything the SDK logs about itself starts with this prefix. */\nconst isIrisLog = (args: unknown[]) =>\n typeof args[0] === 'string' && args[0].startsWith('[iris]');\n\n/** Wrap console methods so output shows up on the session timeline. */\nexport function collectConsole(\n push: (e: IrisEvent) => void,\n opts: ConsoleOptions = {},\n): () => void {\n const captureIrisLogs = opts.captureIrisLogs === true;\n const originals: Partial<Record<ConsoleEvent['level'], (...a: unknown[]) => void>> = {};\n\n for (const level of LEVELS) {\n originals[level] = console[level].bind(console);\n console[level] = (...args: unknown[]) => {\n try {\n if (captureIrisLogs || !isIrisLog(args)) {\n push({ type: 'console', ts: Date.now(), level, args: args.map(stringify) });\n }\n } catch {\n /* never let logging break logging */\n }\n originals[level]!(...args);\n };\n }\n\n return () => {\n for (const level of LEVELS) {\n if (originals[level]) console[level] = originals[level]!;\n }\n };\n}\n\nfunction stringify(v: unknown): string {\n if (typeof v === 'string') return v;\n try {\n return JSON.stringify(v);\n } catch {\n return String(v);\n }\n}\n","import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';\nimport type { IrisEvent, VitalName } from '@iris/types';\n\n/** Report Core Web Vitals as they settle. */\nexport function collectVitals(push: (e: IrisEvent) => void): void {\n const report = (metric: VitalName) => (m: { value: number; rating: string }) => {\n push({\n type: 'vital',\n ts: Date.now(),\n metric,\n value: m.value,\n rating: m.rating as 'good' | 'needs-improvement' | 'poor',\n });\n };\n\n onLCP(report('LCP'));\n onCLS(report('CLS'));\n onINP(report('INP'));\n onFCP(report('FCP'));\n onTTFB(report('TTFB'));\n}\n","import type { IrisEvent } from '@iris/types';\n\n/**\n * Track client-side route changes. SPAs navigate without a full page load, so\n * we patch the History API (`pushState`/`replaceState`) and listen for\n * `popstate`/`hashchange`. Each real URL change emits a NavigationEvent, which\n * the worker turns into a breadcrumb and a `page_count` bump.\n */\nexport function collectNavigation(push: (e: IrisEvent) => void): () => void {\n let current = location.href;\n\n const emit = () => {\n try {\n const to = location.href;\n if (to === current) return;\n const from = current;\n current = to;\n push({ type: 'navigation', ts: Date.now(), from, to });\n } catch {\n /* swallow — telemetry must never break the host app */\n }\n };\n\n const origPush = history.pushState;\n const origReplace = history.replaceState;\n\n history.pushState = function (\n ...args: Parameters<typeof history.pushState>\n ) {\n const ret = origPush.apply(this, args);\n emit();\n return ret;\n };\n history.replaceState = function (\n ...args: Parameters<typeof history.replaceState>\n ) {\n const ret = origReplace.apply(this, args);\n emit();\n return ret;\n };\n\n addEventListener('popstate', emit);\n addEventListener('hashchange', emit);\n\n return () => {\n history.pushState = origPush;\n history.replaceState = origReplace;\n removeEventListener('popstate', emit);\n removeEventListener('hashchange', emit);\n };\n}\n","import type { ClickSignalKind, IrisEvent } from '@iris/types';\n\nexport interface FrustrationOptions {\n /** clicks within rageWindowMs + rageRadiusPx to count as a rage burst */\n rageThreshold?: number;\n rageWindowMs?: number;\n rageRadiusPx?: number;\n /** an actionable click with no DOM change / navigation within this window is \"dead\" */\n deadWindowMs?: number;\n /** an error within this window after a click makes it an \"error click\" */\n errorWindowMs?: number;\n}\n\n/**\n * Detect high-signal \"frustration\" clicks — the signature LogRocket metric:\n *\n * • rage — the user jabs the same spot repeatedly (something feels stuck)\n * • dead — a click on something that looks clickable but does nothing\n * • error — a click immediately followed by a JS error\n *\n * All detection is heuristic and best-effort; like every collector it swallows\n * its own errors so it can never break the host page.\n */\nexport function collectFrustration(\n push: (e: IrisEvent) => void,\n opts: FrustrationOptions = {},\n): () => void {\n const rageThreshold = opts.rageThreshold ?? 3;\n const rageWindowMs = opts.rageWindowMs ?? 1000;\n const rageRadiusPx = opts.rageRadiusPx ?? 30;\n const deadWindowMs = opts.deadWindowMs ?? 2500;\n const errorWindowMs = opts.errorWindowMs ?? 2500;\n\n let lastActivity = 0; // last DOM mutation / scroll\n let lastError = 0; // last window error / rejection\n let lastRageAt = 0; // when we last emitted a rage burst\n const recent: { x: number; y: number; t: number }[] = [];\n\n const bump = () => {\n lastActivity = Date.now();\n };\n const mo = new MutationObserver(bump);\n mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n addEventListener('scroll', bump, true);\n\n const onError = () => {\n lastError = Date.now();\n };\n addEventListener('error', onError, true);\n addEventListener('unhandledrejection', onError, true);\n\n const emit = (kind: ClickSignalKind, t: number, el: Element | null, x: number, y: number, count?: number) => {\n push({\n type: 'click',\n ts: t,\n kind,\n selector: describe(el),\n text: labelOf(el),\n x,\n y,\n ...(count ? { count } : {}),\n });\n };\n\n const onClick = (ev: MouseEvent) => {\n try {\n const now = Date.now();\n const x = ev.clientX;\n const y = ev.clientY;\n const el = ev.target as Element | null;\n\n // ── rage: repeated near-clicks in a short window ──\n while (recent.length && now - recent[0]!.t > rageWindowMs) recent.shift();\n const nearBefore = recent.filter((c) => Math.hypot(c.x - x, c.y - y) <= rageRadiusPx).length;\n recent.push({ x, y, t: now });\n if (nearBefore + 1 >= rageThreshold) {\n lastRageAt = now;\n emit('rage', now, el, x, y, nearBefore + 1);\n recent.length = 0; // one signal per burst\n }\n\n // Only treat isolated clicks as candidate dead/error clicks — repeated\n // near-clicks are a rage burst and shouldn't also spawn N dead signals.\n if (nearBefore > 0) return;\n\n // ── error click: an error fires shortly after the click ──\n const errBaseline = lastError;\n setTimeout(() => {\n if (lastError > errBaseline) emit('error', now, el, x, y);\n }, errorWindowMs);\n\n // ── dead click: actionable target, but nothing happened ──\n if (!deadEligible(el)) return;\n const activityBaseline = lastActivity;\n const urlAtClick = location.href;\n setTimeout(() => {\n const changed = lastActivity > activityBaseline || location.href !== urlAtClick;\n const becameError = lastError > errBaseline;\n if (!changed && !becameError && lastRageAt < now) emit('dead', now, el, x, y);\n }, deadWindowMs);\n } catch {\n /* swallow — telemetry must never break the host app */\n }\n };\n\n addEventListener('click', onClick, true);\n\n return () => {\n mo.disconnect();\n removeEventListener('scroll', bump, true);\n removeEventListener('error', onError, true);\n removeEventListener('unhandledrejection', onError, true);\n removeEventListener('click', onClick, true);\n };\n}\n\n/** A short, stable-ish description of an element: `tag#id.firstclass`. */\nfunction describe(el: Element | null): string {\n if (!el || !el.tagName) return 'unknown';\n let s = el.tagName.toLowerCase();\n if (el.id) s += `#${el.id}`;\n const cls = (el.getAttribute('class') ?? '').trim().split(/\\s+/).filter(Boolean)[0];\n if (cls) s += `.${cls}`;\n return s;\n}\n\n/** Privacy-safe label: aria-label or trimmed text, unless inside a masked/blocked region. */\nfunction labelOf(el: Element | null): string | undefined {\n if (!el) return undefined;\n try {\n if (el.closest('.twc-mask, .twc-block')) return undefined;\n const aria = el.getAttribute('aria-label');\n const raw = (aria ?? el.textContent ?? '').replace(/\\s+/g, ' ').trim();\n if (!raw) return undefined;\n return raw.length > 50 ? `${raw.slice(0, 50)}…` : raw;\n } catch {\n return undefined;\n }\n}\n\n/** Looks clickable but isn't a form control (clicking those is functional, not dead). */\nfunction deadEligible(el: Element | null): boolean {\n if (!el) return false;\n if (el.closest('input, textarea, select, label')) return false;\n if (el.closest('a, button, [role=\"button\"], [onclick]')) return true;\n try {\n return getComputedStyle(el).cursor === 'pointer';\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","currentSessionId","record","__toCommonJS","sanitizeUser","input","out","sanitizeLevel","depth","budget","rawKey","rawVal","key","nested","displayUserId","user","k","v","KEY","UUID_RE","isSessionId","id","currentSessionId","existing","getSessionId","carried","e","buildMeta","release","user","sessionId","u","sanitizeUser","displayUserId","import_fflate","Transport","opts","__publicField","_a","_b","event","roughSize","useBeacon","events","batch","body","url","ok","e","res","err","user","u","sanitizeUser","displayUserId","args","merged","bytes","DEFAULT_REDACT_HEADERS","DEFAULT_MASK_KEYS","REDACTED","MASKED","createMasker","opts","_a","_b","_c","_d","redact","DEFAULT_REDACT_HEADERS","h","maskKeys","DEFAULT_MASK_KEYS","extraKeys","k","isMaskedKey","maxBody","baseHref","maskUrl","url","u","touched","e","maskHeaders","out","maskJson","v","val","truncate","s","cap","raw","masked","p","DEFAULT_MAX_EVENTS","DEFAULT_MAX_PROPS_CHARS","MAX_NAME_CHARS","createTracker","push","maskOpts","opts","_a","_b","masker","createMasker","maxEvents","maxPropsChars","count","warned","name","props","cleanName","ev","masked","size","e","import_rrweb","collectReplay","push","opts","_a","stop","event","collectErrors","push","onError","ev","_a","_b","_c","_d","onRejection","reason","MAX_READ_BYTES","collectNetwork","push","opts","_a","ignore","captureBodies","maskUrl","maskHeaders","maskBody","truncate","isMaskedKey","createMasker","shouldIgnore","url","m","headersToObj","h","out","v","k","pair","isTexty","ct","isEventStream","parseHeaderString","raw","line","i","formDataText","fd","parts","MASKED","requestBodyInfo","input","init","b","s","e","readTextCapped","res","body","text","reader","decoder","bytes","done","value","origFetch","args","_b","_c","_d","_e","start","rawUrl","method","reqInfo","ev","reqHeaders","len","resRead","rb","r","err","origOpen","origSend","origSetHeader","rest","name","ctx","sendArgs","rt","DEFAULT_MAX_FRAMES","DEFAULT_FRAME_CHARS","nextConn","collectWebSockets","push","opts","debug","_a","_b","_c","_d","masker","createMasker","maxFrames","frameChars","frameTypes","ignore","isJsonPayload","s","t","e","shouldCaptureFrame","data","shouldIgnore","url","m","emit","debugLog","args","frameInfo","OrigWS","wsPatched","origSend","IrisWebSocket","protocols","raw","connId","budget","c","boundOrigSend","ctx","OrigES","esPatched","IrisEventSource","init","state","record","eventName","origClose","type","listener","options","LEVELS","isIrisLog","args","collectConsole","push","opts","captureIrisLogs","originals","level","stringify","e","v","import_web_vitals","collectVitals","push","report","metric","m","collectNavigation","push","current","emit","to","from","e","origPush","origReplace","args","ret","collectFrustration","push","opts","_a","_b","_c","_d","_e","rageThreshold","rageWindowMs","rageRadiusPx","deadWindowMs","errorWindowMs","lastActivity","lastError","lastRageAt","recent","bump","mo","onError","emit","kind","t","el","x","y","count","describe","labelOf","onClick","ev","now","nearBefore","c","errBaseline","deadEligible","activityBaseline","urlAtClick","changed","becameError","e","s","cls","aria","raw","record","config","_a","_b","meta","buildMeta","transport","Transport","push","teardowns","track","createTracker","collectReplay","collectErrors","collectConsole","collectNavigation","collectFrustration","collectVitals","netOpts","collectNetwork","collectWebSockets","user","t"]}
package/dist/index.d.cts CHANGED
@@ -57,6 +57,10 @@ interface ConsoleOptions {
57
57
  captureIrisLogs?: boolean;
58
58
  }
59
59
 
60
+ /** The session id this tab is already recording under, without starting one.
61
+ * Undefined in a fresh tab, or when sessionStorage is blocked. */
62
+ declare function currentSessionId(): string | undefined;
63
+
60
64
  interface RecordConfig {
61
65
  /** Iris server origin, e.g. https://rum.twcfm.ca */
62
66
  endpoint: string;
@@ -69,6 +73,11 @@ interface RecordConfig {
69
73
  user?: IrisUser;
70
74
  /** legacy string form of `user` — still supported; same as `user: { id: … }` */
71
75
  userId?: string;
76
+ /** continue an existing session instead of this tab's own — the `sessionId`
77
+ of a handle on another of your origins, passed along when the app sends
78
+ the visitor there (sessionStorage doesn't cross origins). Ignored unless
79
+ it's a UUID. */
80
+ sessionId?: string;
72
81
  replay?: boolean;
73
82
  errors?: boolean;
74
83
  /** `false` to skip console capture entirely, or options — same shape as
@@ -113,4 +122,4 @@ interface IrisHandle {
113
122
  */
114
123
  declare function record(config: RecordConfig): IrisHandle;
115
124
 
116
- export { type ConsoleOptions, type IrisHandle, type IrisUser, type NetworkOptions, type RecordConfig, type TrackOptions, record };
125
+ export { type ConsoleOptions, type IrisHandle, type IrisUser, type NetworkOptions, type RecordConfig, type TrackOptions, currentSessionId, record };
package/dist/index.d.ts CHANGED
@@ -57,6 +57,10 @@ interface ConsoleOptions {
57
57
  captureIrisLogs?: boolean;
58
58
  }
59
59
 
60
+ /** The session id this tab is already recording under, without starting one.
61
+ * Undefined in a fresh tab, or when sessionStorage is blocked. */
62
+ declare function currentSessionId(): string | undefined;
63
+
60
64
  interface RecordConfig {
61
65
  /** Iris server origin, e.g. https://rum.twcfm.ca */
62
66
  endpoint: string;
@@ -69,6 +73,11 @@ interface RecordConfig {
69
73
  user?: IrisUser;
70
74
  /** legacy string form of `user` — still supported; same as `user: { id: … }` */
71
75
  userId?: string;
76
+ /** continue an existing session instead of this tab's own — the `sessionId`
77
+ of a handle on another of your origins, passed along when the app sends
78
+ the visitor there (sessionStorage doesn't cross origins). Ignored unless
79
+ it's a UUID. */
80
+ sessionId?: string;
72
81
  replay?: boolean;
73
82
  errors?: boolean;
74
83
  /** `false` to skip console capture entirely, or options — same shape as
@@ -113,4 +122,4 @@ interface IrisHandle {
113
122
  */
114
123
  declare function record(config: RecordConfig): IrisHandle;
115
124
 
116
- export { type ConsoleOptions, type IrisHandle, type IrisUser, type NetworkOptions, type RecordConfig, type TrackOptions, record };
125
+ export { type ConsoleOptions, type IrisHandle, type IrisUser, type NetworkOptions, type RecordConfig, type TrackOptions, currentSessionId, record };