@sazabi/react-native 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/README.md +286 -2
- package/dist/chunk-h2nxjrr9.js +5 -0
- package/dist/chunk-h2nxjrr9.js.map +15 -0
- package/dist/client-sdk-core/attributes.d.ts +8 -0
- package/dist/client-sdk-core/before-send.d.ts +15 -0
- package/dist/client-sdk-core/clock.d.ts +6 -0
- package/dist/client-sdk-core/console.d.ts +23 -0
- package/dist/client-sdk-core/index.d.ts +22 -0
- package/dist/client-sdk-core/intake-url.d.ts +38 -0
- package/dist/client-sdk-core/network-capture.d.ts +90 -0
- package/dist/client-sdk-core/patch-marker.d.ts +8 -0
- package/dist/client-sdk-core/pointer.d.ts +73 -0
- package/dist/client-sdk-core/random.d.ts +11 -0
- package/dist/client-sdk-core/sdk-health.d.ts +37 -0
- package/dist/client-sdk-core/session.d.ts +44 -0
- package/dist/client-sdk-core/trace-context.d.ts +33 -0
- package/dist/client-sdk-core/transport.d.ts +142 -0
- package/dist/client-sdk-core/types.d.ts +82 -0
- package/dist/client-sdk-core/url.d.ts +30 -0
- package/dist/errors.d.ts +46 -0
- package/dist/host.d.ts +30 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +28 -0
- package/dist/interactions.d.ts +40 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/navigation.d.ts +24 -0
- package/dist/network.d.ts +12 -0
- package/dist/outbox.d.ts +53 -0
- package/dist/process-state.d.ts +49 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +4 -0
- package/dist/register.js.map +10 -0
- package/dist/storage.d.ts +42 -0
- package/dist/types.d.ts +117 -0
- package/dist/version.d.ts +4 -0
- package/package.json +62 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../client-sdk-core/src/attributes.ts", "../../client-sdk-core/src/console.ts", "../../client-sdk-core/src/intake-url.ts", "../../client-sdk-core/src/random.ts", "../../client-sdk-core/src/pointer.ts", "../../client-sdk-core/src/session.ts", "../../client-sdk-core/src/trace-context.ts", "../../client-sdk-core/src/sdk-health.ts", "../../client-sdk-core/src/transport.ts", "../../client-sdk-core/src/before-send.ts", "../src/process-state.ts", "../src/version.ts", "../src/errors.ts", "../src/interactions.ts", "../src/lifecycle.ts", "../src/navigation.ts", "../src/storage.ts", "../src/outbox.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { EventAttributes } from \"./types\";\n\nconst MAX_VALUE_LENGTH = 4096;\nconst MAX_FLATTEN_DEPTH = 4;\n\nexport const truncateValue = (value: string, max = MAX_VALUE_LENGTH): string =>\n value.length > max ? value.slice(0, max) : value;\n\n/**\n * Flatten nested metadata into dot-keyed scalar attributes. Non-scalar leaves\n * are JSON-stringified; strings are length-capped so a hostile or buggy app\n * cannot inflate rows.\n */\nexport const flattenAttributes = (\n input: Record<string, unknown>,\n prefix = \"\",\n depth = 0,\n): EventAttributes => {\n const result: EventAttributes = {};\n\n for (const [key, rawValue] of Object.entries(input)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n\n if (rawValue === null || rawValue === undefined) {\n continue;\n }\n\n if (\n typeof rawValue === \"object\" &&\n !Array.isArray(rawValue) &&\n depth < MAX_FLATTEN_DEPTH\n ) {\n Object.assign(\n result,\n flattenAttributes(\n rawValue as Record<string, unknown>,\n fullKey,\n depth + 1,\n ),\n );\n continue;\n }\n\n if (typeof rawValue === \"number\" || typeof rawValue === \"boolean\") {\n result[fullKey] = rawValue;\n } else if (typeof rawValue === \"string\") {\n result[fullKey] = truncateValue(rawValue);\n } else {\n try {\n result[fullKey] = truncateValue(JSON.stringify(rawValue) ?? \"\");\n } catch {\n // Circular or unserializable: drop rather than throw into the host.\n }\n }\n }\n\n return result;\n};\n",
|
|
6
|
+
"import { isWrapped, markWrapped } from \"./patch-marker\";\nimport type { ClientEvent, ConsoleCaptureLevel, EventSeverity } from \"./types\";\n\nexport interface ConsoleCaptureOptions {\n emit(event: ClientEvent): void;\n levels: ConsoleCaptureLevel[];\n /** Lines the SDK itself prints start with this and are never re-captured. */\n selfPrefix: string;\n}\n\nexport interface ConsoleCaptureHandle {\n teardown(): void;\n}\n\nconst SEVERITY_BY_LEVEL: Record<ConsoleCaptureLevel, EventSeverity> = {\n error: \"ERROR\",\n warn: \"WARN\",\n info: \"INFO\",\n debug: \"DEBUG\",\n};\n\n/** Nonzero while `withoutConsoleCapture` runs: those lines are never mirrored. */\nlet uncaptured = 0;\n\n/**\n * Print through the console without mirroring the line into the event\n * stream: for a host-facing message the SDK prints on another component's\n * behalf (the dev-mode rejection warning it relays for React Native).\n */\nexport const withoutConsoleCapture = (print: () => void): void => {\n uncaptured += 1;\n try {\n print();\n } finally {\n uncaptured -= 1;\n }\n};\n\nconst MAX_ARG_CHARS = 1_000;\nconst MAX_BODY_CHARS = 4_000;\n\nconst stringifyArg = (value: unknown): string => {\n let text: string;\n if (typeof value === \"string\") {\n text = value;\n } else if (value instanceof Error) {\n text = `${value.name}: ${value.message}`;\n } else {\n try {\n text = JSON.stringify(value) ?? String(value);\n } catch {\n text = String(value);\n }\n }\n return text.length > MAX_ARG_CHARS\n ? `${text.slice(0, MAX_ARG_CHARS)}…`\n : text;\n};\n\n/**\n * Mirror selected console levels into the event stream as `log` events.\n * The original console method always runs first (devtools behavior is never\n * altered) and capture failures are swallowed. Lines the SDK itself prints\n * (prefixed with `selfPrefix`) are skipped to avoid self-capture.\n */\nexport const installConsoleCapture = (\n options: ConsoleCaptureOptions,\n): ConsoleCaptureHandle => {\n const restores: Array<() => void> = [];\n let emitting = false;\n\n for (const level of options.levels) {\n const original = console[level];\n if (typeof original !== \"function\" || isWrapped(original)) {\n continue;\n }\n\n const wrapped = function (this: unknown, ...args: unknown[]): void {\n original.apply(this ?? console, args);\n if (emitting || uncaptured > 0) return;\n try {\n if (\n typeof args[0] === \"string\" &&\n args[0].startsWith(options.selfPrefix)\n ) {\n return;\n }\n emitting = true;\n const body = args.map(stringifyArg).join(\" \").slice(0, MAX_BODY_CHARS);\n options.emit({\n type: \"log\",\n body: body || `console.${level}`,\n severity: SEVERITY_BY_LEVEL[level],\n timeUnixMs: Date.now(),\n attributes: {\n \"app.console.level\": level,\n },\n });\n } catch {\n // Capture failures must never surface to the host.\n } finally {\n emitting = false;\n }\n };\n\n markWrapped(wrapped);\n try {\n console[level] = wrapped as typeof original;\n } catch {\n // A frozen console, or a non-writable method, stays unwrapped: that\n // level is not mirrored and init() carries on.\n continue;\n }\n restores.push(() => {\n if (console[level] === wrapped) {\n console[level] = original;\n }\n });\n }\n\n return {\n teardown() {\n for (const restore of restores) {\n try {\n restore();\n } catch {\n // Best-effort restore.\n }\n }\n },\n };\n};\n",
|
|
7
|
+
"/**\n * Resolving the intake endpoint from a single URL.\n *\n * Every Sazabi log source is configured with one intake URL, and public keys\n * stay a low-level detail customers never handle. The key is already in the\n * URL: the intake edge reconstructs it from the hostname\n * (terraform/main/cloudfront-functions/intake-router.js), so the SDK does the\n * same reconstruction locally and sends the Bearer header itself.\n *\n * Two keyed shapes exist, matching the edge's two grammars:\n * {hex}.{region}.intake.{domain} prefixless (what log sources issue)\n * {adapter}-{hex}.{region}.intake.{domain} keyed-adapter (legacy, still served)\n *\n * A URL in neither shape is not an error: it may be a proxy or a custom\n * endpoint. The SDK then requires an explicit `publicKey`, so a\n * misconfiguration surfaces as a clear warning rather than silent no-auth.\n */\n\nimport { parseAbsoluteUrl } from \"./url\";\n\nconst KEYED_HOST = /^([0-9a-f]{32})\\./;\nconst KEYED_ADAPTER_HOST = /^[a-z0-9-]+?-([0-9a-f]{32})\\./;\n\nexport interface ResolvedIntakeEndpoint {\n /**\n * Base to POST to: origin plus any proxy path prefix, no trailing slash.\n * The transport appends `/v1/logs`.\n */\n baseUrl: string;\n /** Credential for the Authorization header, derived or explicit. */\n publicKey: string;\n}\n\n/** Public key embedded in a keyed intake hostname, if it carries one. */\nexport const publicKeyFromIntakeUrl = (url: string): string | undefined => {\n const host = parseAbsoluteUrl(url)?.hostname;\n if (!host) return undefined;\n const hex = (KEYED_HOST.exec(host) ?? KEYED_ADAPTER_HOST.exec(host))?.[1];\n return hex ? `sazabi_public_${hex}` : undefined;\n};\n\n/**\n * Normalize a configured intake URL to a base the transport can suffix with\n * `/v1/logs`: drop a terminal `/v1/logs` so both the bare host and the full\n * OTLP endpoint work (the two forms customers copy from setup instructions),\n * and drop query/hash, which the OTLP POST has no use for.\n *\n * A path prefix is preserved. Reducing this to `origin` would break the\n * documented proxy escape hatch two ways: records for a proxy mounted at\n * `https://example.com/otel` would go to `/v1/logs`, and the internal-request\n * filter (a `startsWith` on this base) would suppress network capture for\n * every request sharing that origin, including the customer's own API traffic.\n *\n * Only `http:` and `https:` resolve. Any other scheme would leave the SDK\n * looking initialized while `fetch` rejects the protocol and the transport\n * swallows the error, dropping every event; failing closed here surfaces the\n * misconfiguration as the same init warning as a missing URL.\n */\nconst SIGNAL_PATH = \"/v1/logs\";\n\nexport const intakeBaseFromUrl = (url: string): string | undefined => {\n const parsed = parseAbsoluteUrl(url);\n if (!parsed) {\n return undefined;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return undefined;\n }\n let path = parsed.pathname.replace(/\\/+$/, \"\");\n if (path.endsWith(SIGNAL_PATH)) {\n path = path.slice(0, -SIGNAL_PATH.length);\n }\n return `${parsed.origin}${path}`;\n};\n\n/**\n * Resolve the endpoint from config. An explicit `publicKey` always wins, so a\n * non-keyed or proxied endpoint stays configurable.\n */\nexport const resolveIntakeEndpoint = (config: {\n intakeUrl?: string;\n intakeHost?: string;\n publicKey?: string;\n}): ResolvedIntakeEndpoint | undefined => {\n const configured = config.intakeUrl ?? config.intakeHost;\n if (!configured) return undefined;\n\n const baseUrl = intakeBaseFromUrl(configured);\n if (!baseUrl) return undefined;\n\n const publicKey = config.publicKey ?? publicKeyFromIntakeUrl(configured);\n if (!publicKey) return undefined;\n\n return { baseUrl, publicKey };\n};\n",
|
|
8
|
+
"export type RandomBytes = (length: number) => Uint8Array;\n\n/**\n * Random bytes for session, trace, and gesture ids. Web Crypto where the host\n * has it; a seeded fallback where it does not (React Native without a\n * `crypto.getRandomValues` polyfill). The ids need uniqueness, not secrecy,\n * so the fallback is acceptable there and is never used where crypto exists.\n */\nexport const createRandomBytes = (): RandomBytes => {\n const cryptoApi = (\n globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => unknown } }\n ).crypto;\n const getRandomValues = cryptoApi?.getRandomValues;\n if (typeof getRandomValues === \"function\") {\n return (length) => {\n const bytes = new Uint8Array(length);\n getRandomValues.call(cryptoApi, bytes);\n return bytes;\n };\n }\n\n // xorshift128+ seeded from the clock and Math.random: enough entropy to\n // keep ids unique across launches, with no claim beyond that.\n let s0 = (Date.now() ^ 0x9e3779b9) >>> 0;\n let s1 = (Math.random() * 0xffffffff) >>> 0;\n return (length) => {\n const bytes = new Uint8Array(length);\n for (let i = 0; i < length; i += 1) {\n let x = s0;\n const y = s1;\n s0 = y;\n x ^= x << 23;\n x ^= x >>> 17;\n x ^= y ^ (y >>> 26);\n s1 = x >>> 0;\n bytes[i] = (s0 + s1) & 0xff;\n }\n return bytes;\n };\n};\n\nexport const toHex = (bytes: Uint8Array): string =>\n Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\nexport const randomHex = (\n randomBytes: RandomBytes,\n byteLength: number,\n): string => toHex(randomBytes(byteLength));\n\nexport const newPrefixedId = (\n randomBytes: RandomBytes,\n prefix: string,\n): string => `${prefix}_${Date.now().toString(36)}${randomHex(randomBytes, 8)}`;\n",
|
|
9
|
+
"import { monotonicNow } from \"./clock\";\nimport { newPrefixedId, type RandomBytes } from \"./random\";\nimport type { ClientEvent, EventAttributes } from \"./types\";\n\n/**\n * Pointer episodes: one row per contact from down to up, for mouse, touch,\n * and pen alike. A binding feeds raw contacts in (DOM pointer events in the\n * browser, the responder's touch arrays on mobile) and the tracker emits the\n * episode rows plus the derived `click` and `rage_click` rows. Movement is\n * sampled, never streamed: a position sample every `sampleIntervalMs` is\n * packed into the row, and a contact that outlives `partialIntervalMs` emits\n * a partial row at that cadence so a long scroll is not a blind spot.\n */\n\nexport type PointerType = \"mouse\" | \"touch\" | \"pen\";\n\nexport interface PointerTarget {\n /** Developer-authored name for the widget under the pointer, if any. */\n name?: string;\n /** Stable-ish identifier: a selector on the web, a component path on mobile. */\n id?: string;\n /** Element tag or component kind. */\n tag?: string;\n /** Whether a tap here is on something that should respond. */\n interactive: boolean;\n}\n\nexport interface PointerContact {\n id: number | string;\n type: PointerType;\n primary: boolean;\n x: number;\n y: number;\n pressure?: number;\n /**\n * The button that started a mouse contact (DOM `button`: 0 primary, 1\n * auxiliary, 2 secondary). Absent for touch and pen. Only a primary-button\n * contact can derive a click: a short right or middle press is an episode\n * without a tap, matching the host's `contextmenu` and `auxclick`.\n */\n button?: number;\n target?: PointerTarget;\n}\n\nexport interface DerivedTap {\n x: number;\n y: number;\n target?: PointerTarget;\n /** Absent for a tap no contact produced (keyboard activation, script). */\n gestureId?: string;\n}\n\nexport interface PointerTrackerOptions {\n emit(event: ClientEvent): void;\n randomBytes: RandomBytes;\n /** Spacing of packed position samples. Default 1000. */\n sampleIntervalMs?: number;\n /** Cadence of partial rows for long contacts. Default 10000. */\n partialIntervalMs?: number;\n /** Every derived tap, so a binding can run its own dead-tap detection. */\n onTap?(tap: DerivedTap): void;\n}\n\nexport interface PointerTracker {\n down(contact: PointerContact): void;\n move(id: number | string, x: number, y: number, pressure?: number): void;\n up(id: number | string, x: number, y: number): void;\n cancel(id: number | string): void;\n /**\n * A click that no pointer contact produced: keyboard activation or a\n * script calling `click()`. Emits the `click` row and runs the same rage\n * and dead-tap derivation, with no episode behind it.\n */\n tap(tap: { x: number; y: number; target?: PointerTarget }): void;\n teardown(): void;\n}\n\nconst DEFAULT_SAMPLE_INTERVAL_MS = 1_000;\nconst DEFAULT_PARTIAL_INTERVAL_MS = 10_000;\nconst TAP_MAX_DISTANCE_PX = 10;\nconst TAP_MAX_DURATION_MS = 500;\nconst RAGE_WINDOW_MS = 1_000;\nconst RAGE_RADIUS_PX = 30;\nconst RAGE_TAP_COUNT = 4;\nconst MAX_OPEN_CONTACTS = 10;\n\n/**\n * `cancel` means the contact stopped reaching us before release: the host\n * took the gesture for itself (a native or browser scroll, a pager, a\n * drawer, a system gesture) or the release was lost. The finger may still be\n * moving, so the row ends where the host took over, not where it lifted.\n */\ntype EndedBy = \"up\" | \"cancel\" | \"teardown\";\n\ninterface OpenContact {\n contact: PointerContact;\n gestureId: string;\n startUnixMs: number;\n startPerf: number;\n lastSamplePerf: number;\n lastX: number;\n lastY: number;\n /** Farthest the contact strayed from its start, so a drag that returns is still a drag. */\n maxDistance: number;\n maxPressure?: number;\n maxConcurrent: number;\n /** Samples since the last emitted row, as `t:x,y`. */\n path: string[];\n sampleCount: number;\n partialTimer?: ReturnType<typeof setTimeout>;\n}\n\ninterface RecentTap {\n time: number;\n x: number;\n y: number;\n}\n\ninterface TapDetails {\n x: number;\n y: number;\n target?: PointerTarget;\n /** `none` when no contact produced the click. */\n pointerType: PointerType | \"none\";\n gestureId?: string;\n timeUnixMs: number;\n}\n\nconst distance = (x0: number, y0: number, x1: number, y1: number): number =>\n Math.hypot(x1 - x0, y1 - y0);\n\nconst targetAttributes = (target?: PointerTarget): EventAttributes => ({\n \"app.widget.id\": target?.id,\n \"app.widget.name\": target?.name,\n \"app.widget.tag\": target?.tag,\n});\n\nexport const createPointerTracker = (\n options: PointerTrackerOptions,\n): PointerTracker => {\n const sampleIntervalMs =\n options.sampleIntervalMs ?? DEFAULT_SAMPLE_INTERVAL_MS;\n const partialIntervalMs =\n options.partialIntervalMs ?? DEFAULT_PARTIAL_INTERVAL_MS;\n const open = new Map<number | string, OpenContact>();\n const recentTaps: RecentTap[] = [];\n // Not 0: an early first-second burst must still report.\n let rageReportedAt = Number.NEGATIVE_INFINITY;\n let currentGestureId: string | undefined;\n\n const sample = (entry: OpenContact, now: number): void => {\n entry.path.push(\n `${Math.round(now - entry.startPerf)}:${Math.round(entry.lastX)},${Math.round(entry.lastY)}`,\n );\n entry.sampleCount += 1;\n entry.lastSamplePerf = now;\n };\n\n const emitRow = (\n entry: OpenContact,\n now: number,\n endedBy: EndedBy | undefined,\n ): void => {\n const { contact } = entry;\n const partial = endedBy === undefined;\n const durationMs = Math.round(now - entry.startPerf);\n const travelled = distance(contact.x, contact.y, entry.lastX, entry.lastY);\n try {\n options.emit({\n type: \"pointer\",\n body: `pointer ${contact.type} ${durationMs}ms ${Math.round(travelled)}px${partial ? \" (ongoing)\" : \"\"}`,\n severity: \"INFO\",\n // Final rows sit at the contact's start so the timeline reads\n // causally; partial rows are progress marks and sit where they fire.\n timeUnixMs: partial ? Date.now() : entry.startUnixMs,\n attributes: {\n \"app.pointer.id\": String(contact.id),\n \"app.pointer.type\": contact.type,\n \"app.pointer.primary\": contact.primary,\n \"app.pointer.gesture_id\": entry.gestureId,\n \"app.screen.coordinate.x\": Math.round(contact.x),\n \"app.screen.coordinate.y\": Math.round(contact.y),\n \"app.pointer.end.x\": Math.round(entry.lastX),\n \"app.pointer.end.y\": Math.round(entry.lastY),\n \"app.pointer.distance\": Math.round(travelled),\n \"app.pointer.duration_ms\": durationMs,\n \"app.pointer.pressure\": entry.maxPressure,\n \"app.pointer.concurrent\": entry.maxConcurrent,\n \"app.pointer.path\": entry.path.join(\";\") || undefined,\n \"app.pointer.sample_count\": entry.sampleCount,\n \"app.pointer.partial\": partial ? true : undefined,\n \"app.pointer.ended_by\": endedBy,\n ...targetAttributes(contact.target),\n },\n });\n } catch {\n // Emitting must never throw into the host.\n }\n entry.path = [];\n };\n\n const schedulePartial = (entry: OpenContact): void => {\n entry.partialTimer = setTimeout(() => {\n entry.partialTimer = undefined;\n try {\n const now = monotonicNow();\n if (now - entry.lastSamplePerf >= sampleIntervalMs) {\n sample(entry, now);\n }\n emitRow(entry, now, undefined);\n schedulePartial(entry);\n } catch {\n // Never throw from a timer into the host.\n }\n }, partialIntervalMs);\n };\n\n const emitTap = (tap: TapDetails, now: number): void => {\n const name = tap.target?.name;\n const label = name\n ? `\"${name}\"`\n : (tap.target?.id ?? `${Math.round(tap.x)},${Math.round(tap.y)}`);\n const tapAttributes: EventAttributes = {\n ...targetAttributes(tap.target),\n \"app.pointer.type\": tap.pointerType,\n \"app.pointer.gesture_id\": tap.gestureId,\n \"app.screen.coordinate.x\": Math.round(tap.x),\n \"app.screen.coordinate.y\": Math.round(tap.y),\n };\n\n try {\n options.emit({\n type: \"click\",\n body: `click ${label}`,\n severity: \"INFO\",\n timeUnixMs: tap.timeUnixMs,\n attributes: tapAttributes,\n });\n } catch {\n // Never throw into the host.\n }\n\n // Rage tap: repeated taps in a tight radius within the window.\n recentTaps.push({ time: now, x: tap.x, y: tap.y });\n while (\n recentTaps.length > 0 &&\n now - (recentTaps[0] as RecentTap).time > RAGE_WINDOW_MS\n ) {\n recentTaps.shift();\n }\n const clustered = recentTaps.filter(\n (recent) =>\n Math.abs(recent.x - tap.x) <= RAGE_RADIUS_PX &&\n Math.abs(recent.y - tap.y) <= RAGE_RADIUS_PX,\n );\n if (\n clustered.length >= RAGE_TAP_COUNT &&\n now - rageReportedAt > RAGE_WINDOW_MS\n ) {\n rageReportedAt = now;\n try {\n options.emit({\n type: \"rage_click\",\n body: `rage click ${label}`,\n severity: \"WARN\",\n timeUnixMs: Date.now(),\n attributes: {\n ...tapAttributes,\n \"app.pointer.tap_count\": clustered.length,\n },\n });\n } catch {\n // Never throw into the host.\n }\n }\n\n try {\n options.onTap?.({\n x: tap.x,\n y: tap.y,\n target: tap.target,\n gestureId: tap.gestureId,\n });\n } catch {\n // A binding's tap hook must never break capture.\n }\n };\n\n const deriveTap = (entry: OpenContact, now: number): void => {\n const { contact } = entry;\n if (\n entry.maxConcurrent !== 1 ||\n (contact.button ?? 0) !== 0 ||\n now - entry.startPerf > TAP_MAX_DURATION_MS ||\n entry.maxDistance > TAP_MAX_DISTANCE_PX\n ) {\n return;\n }\n emitTap(\n {\n x: entry.lastX,\n y: entry.lastY,\n target: contact.target,\n pointerType: contact.type,\n gestureId: entry.gestureId,\n timeUnixMs: entry.startUnixMs,\n },\n now,\n );\n };\n\n const end = (\n entry: OpenContact,\n x: number,\n y: number,\n endedBy: EndedBy,\n ): void => {\n const now = monotonicNow();\n open.delete(entry.contact.id);\n if (entry.partialTimer !== undefined) {\n clearTimeout(entry.partialTimer);\n entry.partialTimer = undefined;\n }\n entry.lastX = x;\n entry.lastY = y;\n entry.maxDistance = Math.max(\n entry.maxDistance,\n distance(entry.contact.x, entry.contact.y, x, y),\n );\n emitRow(entry, now, endedBy);\n if (endedBy === \"up\") {\n deriveTap(entry, now);\n }\n if (open.size === 0) {\n currentGestureId = undefined;\n }\n };\n\n return {\n down(contact) {\n try {\n const stale = open.get(contact.id);\n if (stale) {\n // A new down for an open id means its up or cancel was lost (a\n // release outside the window, a native context menu): close it, or\n // the id is blocked forever and its partial timer never stops.\n end(stale, stale.lastX, stale.lastY, \"cancel\");\n }\n if (open.size >= MAX_OPEN_CONTACTS) {\n return;\n }\n const now = monotonicNow();\n // A contact that lands while others are down joins their gesture.\n if (open.size === 0 || currentGestureId === undefined) {\n currentGestureId = newPrefixedId(options.randomBytes, \"gest\");\n }\n const concurrent = open.size + 1;\n for (const other of open.values()) {\n other.maxConcurrent = Math.max(other.maxConcurrent, concurrent);\n }\n const entry: OpenContact = {\n contact,\n gestureId: currentGestureId,\n startUnixMs: Date.now(),\n startPerf: now,\n lastSamplePerf: now,\n lastX: contact.x,\n lastY: contact.y,\n maxDistance: 0,\n maxPressure: contact.pressure,\n maxConcurrent: concurrent,\n path: [],\n sampleCount: 0,\n };\n sample(entry, now);\n open.set(contact.id, entry);\n schedulePartial(entry);\n } catch {\n // Capture failures must never surface to the host.\n }\n },\n move(id, x, y, pressure) {\n try {\n const entry = open.get(id);\n if (!entry) return;\n entry.lastX = x;\n entry.lastY = y;\n entry.maxDistance = Math.max(\n entry.maxDistance,\n distance(entry.contact.x, entry.contact.y, x, y),\n );\n if (pressure !== undefined) {\n entry.maxPressure = Math.max(entry.maxPressure ?? 0, pressure);\n }\n const now = monotonicNow();\n if (now - entry.lastSamplePerf >= sampleIntervalMs) {\n sample(entry, now);\n }\n } catch {\n // Ignore.\n }\n },\n up(id, x, y) {\n try {\n const entry = open.get(id);\n if (entry) end(entry, x, y, \"up\");\n } catch {\n // Ignore.\n }\n },\n cancel(id) {\n try {\n const entry = open.get(id);\n if (entry) end(entry, entry.lastX, entry.lastY, \"cancel\");\n } catch {\n // Ignore.\n }\n },\n tap(tap) {\n try {\n emitTap(\n {\n x: tap.x,\n y: tap.y,\n target: tap.target,\n pointerType: \"none\",\n timeUnixMs: Date.now(),\n },\n monotonicNow(),\n );\n } catch {\n // Ignore.\n }\n },\n teardown() {\n for (const entry of Array.from(open.values())) {\n try {\n end(entry, entry.lastX, entry.lastY, \"teardown\");\n } catch {\n // Best-effort.\n }\n }\n open.clear();\n recentTaps.length = 0;\n },\n };\n};\n",
|
|
10
|
+
"import { newPrefixedId, type RandomBytes } from \"./random\";\n\nconst SESSION_STORAGE_KEY = \"sazabi.session\";\nconst WINDOW_STORAGE_KEY = \"sazabi.windowId\";\nconst DISTINCT_STORAGE_KEY = \"sazabi.distinctId\";\n\nconst SESSION_IDLE_MS = 30 * 60 * 1000;\nconst SESSION_MAX_MS = 24 * 60 * 60 * 1000;\n/**\n * How far the wall clock may run behind the stored activity time before the\n * stored session no longer counts. A clock set back by hours would otherwise\n * keep a session alive past its idle and age limits, since both are measured\n * as `now` minus a time that now lies in the future.\n */\nconst CLOCK_SKEW_MS = 60 * 1000;\n/** Throttle activity writes; click bursts must not spam storage. */\nconst ACTIVITY_PERSIST_INTERVAL_MS = 5_000;\n\ninterface StoredSession {\n id: string;\n startedAt: number;\n lastActivityAt: number;\n}\n\n/**\n * Synchronous key-value access the session machine reads through on every\n * call. A browser binding backs it with `localStorage` (so sibling tabs\n * converge) and `sessionStorage`; a mobile binding backs it with an\n * in-memory mirror hydrated from its storage adapter and written through.\n */\nexport interface SessionStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\n/** A store that lives for the process only: the fallback on every host. */\nexport const createMemoryStore = (): SessionStore => {\n const memory = new Map<string, string>();\n return {\n get: (key) => memory.get(key) ?? null,\n set: (key, value) => {\n memory.set(key, value);\n },\n remove: (key) => {\n memory.delete(key);\n },\n };\n};\n\nexport interface SessionManager {\n getSessionId(): string;\n getWindowId(): string;\n getDistinctId(): string | undefined;\n /**\n * Set the client-asserted identity. Switching from one identity directly\n * to a different one rotates the session and window ids so two users on a\n * shared device never thread into one timeline.\n */\n setDistinctId(distinctId: string): void;\n /** Logout: clear identity and rotate both session and window ids. */\n reset(): void;\n /** Record user activity; may rotate the session on idle/max-age expiry. */\n touch(): void;\n}\n\nexport interface SessionManagerOptions {\n /** Visit-scoped state: session id and identity. */\n store: SessionStore;\n /** Window-scoped state: one browser tab, or one app launch. */\n windowStore: SessionStore;\n randomBytes: RandomBytes;\n}\n\nconst parseStoredSession = (raw: string | null): StoredSession | undefined => {\n if (!raw) return undefined;\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n typeof (parsed as StoredSession).id === \"string\" &&\n typeof (parsed as StoredSession).startedAt === \"number\" &&\n typeof (parsed as StoredSession).lastActivityAt === \"number\"\n ) {\n return parsed as StoredSession;\n }\n } catch {\n // Corrupt value: treat as absent and rotate.\n }\n return undefined;\n};\n\n/**\n * Session identity. `session.id` is the visit (last-writer-wins on rotation\n * races between windows; a split session is cosmetic, not data loss).\n * `session.window_id` is this window. `session.distinct_id` is read through\n * the store so identify/reset in one window is visible to siblings on their\n * next event.\n */\nexport const createSessionManager = (\n options: SessionManagerOptions,\n): SessionManager => {\n const { store, windowStore, randomBytes } = options;\n\n let lastPersistedActivity = 0;\n\n const readOrRotate = (): StoredSession => {\n const now = Date.now();\n const stored = parseStoredSession(store.get(SESSION_STORAGE_KEY));\n\n if (\n stored &&\n now - stored.lastActivityAt <= SESSION_IDLE_MS &&\n now - stored.lastActivityAt >= -CLOCK_SKEW_MS &&\n now - stored.startedAt <= SESSION_MAX_MS\n ) {\n return stored;\n }\n\n const fresh: StoredSession = {\n id: newPrefixedId(randomBytes, \"sess\"),\n startedAt: now,\n lastActivityAt: now,\n };\n store.set(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n lastPersistedActivity = now;\n return fresh;\n };\n\n let windowId = windowStore.get(WINDOW_STORAGE_KEY);\n if (!windowId) {\n windowId = newPrefixedId(randomBytes, \"win\");\n windowStore.set(WINDOW_STORAGE_KEY, windowId);\n }\n\n const rotate = (): void => {\n const now = Date.now();\n store.set(\n SESSION_STORAGE_KEY,\n JSON.stringify({\n id: newPrefixedId(randomBytes, \"sess\"),\n startedAt: now,\n lastActivityAt: now,\n }),\n );\n lastPersistedActivity = now;\n windowId = newPrefixedId(randomBytes, \"win\");\n windowStore.set(WINDOW_STORAGE_KEY, windowId);\n };\n\n return {\n getSessionId() {\n return readOrRotate().id;\n },\n getWindowId() {\n return windowId as string;\n },\n getDistinctId() {\n return store.get(DISTINCT_STORAGE_KEY) ?? undefined;\n },\n setDistinctId(id: string) {\n const current = store.get(DISTINCT_STORAGE_KEY) ?? undefined;\n if (current !== undefined && current !== id) {\n // Identity switch without reset(): defensive rotation so the new\n // user's events never join the previous user's session.\n rotate();\n }\n store.set(DISTINCT_STORAGE_KEY, id);\n },\n reset() {\n store.remove(DISTINCT_STORAGE_KEY);\n rotate();\n },\n touch() {\n const now = Date.now();\n const current = readOrRotate();\n if (now - lastPersistedActivity >= ACTIVITY_PERSIST_INTERVAL_MS) {\n store.set(\n SESSION_STORAGE_KEY,\n JSON.stringify({ ...current, lastActivityAt: now }),\n );\n lastPersistedActivity = now;\n }\n },\n };\n};\n",
|
|
11
|
+
"import { type RandomBytes, toHex } from \"./random\";\n\nexport interface RequestTraceContext {\n /** Headers to inject (`traceparent`, and `tracestate` when present). */\n headers: Record<string, string>;\n traceId: string;\n spanId: string;\n}\n\nexport interface TraceContextManager {\n /** Trace context for one outgoing request, minted at request time. */\n forRequest(): RequestTraceContext;\n}\n\n/** The subset of an OTel SpanContext this module consumes. */\nexport interface HostSpanContext {\n traceId: string;\n spanId: string;\n traceFlags: number;\n traceState?: { serialize(): string };\n}\n\nconst SAMPLED_FLAG = 0x1;\nconst TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;\nconst SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;\nconst ALL_ZERO_TRACE_ID = \"0\".repeat(32);\nconst ALL_ZERO_SPAN_ID = \"0\".repeat(16);\n\nconst randomHexId = (randomBytes: RandomBytes, byteLength: number): string => {\n let bytes: Uint8Array;\n do {\n bytes = randomBytes(byteLength);\n } while (bytes.every((b) => b === 0)); // all-zero ids are invalid per W3C spec\n return toHex(bytes);\n};\n\nconst isValidHostSpanContext = (\n candidate: unknown,\n): candidate is HostSpanContext => {\n if (typeof candidate !== \"object\" || candidate === null) return false;\n const { traceId, spanId, traceFlags } = candidate as HostSpanContext;\n return (\n typeof traceId === \"string\" &&\n TRACE_ID_PATTERN.test(traceId) &&\n traceId !== ALL_ZERO_TRACE_ID &&\n typeof spanId === \"string\" &&\n SPAN_ID_PATTERN.test(spanId) &&\n spanId !== ALL_ZERO_SPAN_ID &&\n typeof traceFlags === \"number\"\n );\n};\n\n// ------------------------------------------------- host OTel interop --------\n// Dependency-free read of the host app's active span, through the\n// @opentelemetry/api package's own cross-copy compatibility contract: every\n// api copy interoperates via the global delegate registered at\n// Symbol.for(\"opentelemetry.js.api.1\"), and context keys are Symbol.for(...)\n// strings. That registry IS the mechanism by which independent api copies\n// find each other, which is what makes it load-bearing rather than private.\n// An integration test against the real api package (devDependency of the\n// browser SDK) pins the contract; any shape drift degrades here to \"no\n// active span\" and we mint our own roots instead.\n\nconst OTEL_GLOBAL_KEY = Symbol.for(\"opentelemetry.js.api.1\");\nconst OTEL_SPAN_CONTEXT_KEY = Symbol.for(\"OpenTelemetry Context Key SPAN\");\n\ninterface OtelGlobalShape {\n context?: {\n active?: () => { getValue?: (key: symbol) => unknown } | undefined;\n };\n}\n\nexport const readHostSpanContext = (): HostSpanContext | undefined => {\n try {\n const otelApi = (globalThis as Record<symbol, unknown>)[OTEL_GLOBAL_KEY] as\n | OtelGlobalShape\n | undefined;\n const activeContext = otelApi?.context?.active?.();\n const span = activeContext?.getValue?.(OTEL_SPAN_CONTEXT_KEY) as\n | { spanContext?: () => unknown }\n | undefined;\n const candidate = span?.spanContext?.();\n return isValidHostSpanContext(candidate) ? candidate : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatTraceparent = (spanContext: HostSpanContext): string => {\n const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, \"0\");\n return `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;\n};\n\nconst readTraceState = (spanContext: HostSpanContext): string | undefined => {\n try {\n const serialized = spanContext.traceState?.serialize();\n return serialized || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * W3C trace context for outgoing requests: hand-rolled header formatting\n * (equivalence-tested against OTel's W3CTraceContextPropagator), zero\n * runtime dependencies. If the host app runs its own OTel and has an active\n * span, that span's context (including its flags and tracestate) is reused\n * so we join the customer's trace instead of forking a new one; otherwise a\n * fresh root is minted with the sampled flag defaulting to off (`-00`).\n */\nexport const createTraceContextManager = (options: {\n sampledFlag: boolean;\n randomBytes: RandomBytes;\n}): TraceContextManager => {\n const mintRootSpanContext = (): HostSpanContext => ({\n traceId: randomHexId(options.randomBytes, 16),\n spanId: randomHexId(options.randomBytes, 8),\n traceFlags: options.sampledFlag ? SAMPLED_FLAG : 0,\n });\n\n return {\n forRequest() {\n const spanContext = readHostSpanContext() ?? mintRootSpanContext();\n const headers: Record<string, string> = {\n traceparent: formatTraceparent(spanContext),\n };\n const traceState = readTraceState(spanContext);\n if (traceState) {\n headers.tracestate = traceState;\n }\n return {\n headers,\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n };\n },\n };\n};\n",
|
|
12
|
+
"import type { ClientEvent, EventAttributes } from \"./types\";\n\n/**\n * What an `sdk_health` row reports: the SDK's own delivery trouble, so a\n * misconfigured or unreachable app is visible to Sazabi before the customer\n * reports it. Each value is a bounded enumeration, never free text.\n *\n * - `intake_rejected`: the intake refused a batch for good (a 4xx other\n * than 408 and 429); `app.sdk.http_status` carries the status.\n * - `intake_unreachable`: uploads failed on the network (a dropped\n * connection, a timeout) and the intake later answered; `app.sdk.attempts`\n * is how many backoff windows the outage spanned (one per failed try,\n * with concurrent uploads failing together counted once).\n * - `outbox_discarded`: stored crash reports were dropped, by the caps or\n * because they were not valid reports; `app.sdk.outbox_discarded` counts\n * what is known lost, and `app.sdk.outbox_unsaved` what a saved queue had\n * left out at the caps (possible losses: their live upload may have\n * finished before the app was killed) (mobile only).\n * - `outbox_persist_failed`: the storage adapter refused a write of the\n * crash outbox or the saved queue (mobile only).\n */\nexport type SdkHealthDiagnostic =\n | \"intake_rejected\"\n | \"intake_unreachable\"\n | \"outbox_discarded\"\n | \"outbox_persist_failed\";\n\nexport interface SdkHealthReport {\n diagnostic: SdkHealthDiagnostic;\n /** The counters of the condition (`app.sdk.http_status`, `app.sdk.attempts`, ...). */\n attributes?: EventAttributes;\n}\n\nexport type SdkHealthReporter = (report: SdkHealthReport) => void;\n\nexport const SDK_DIAGNOSTIC_KEY = \"app.sdk.diagnostic\";\n\n/**\n * Turns health reports into `sdk_health` rows, at most one per diagnostic\n * for the life of the reporter (one SDK session). The bound is what keeps\n * the SDK from storming its own intake: a health row the intake refuses is\n * a rejection like any other and is never reported again, and a flapping\n * network produces one row per launch, not one per outage. Never throws\n * into the host.\n */\nexport const createSdkHealthReporter = (\n emit: (event: ClientEvent) => void,\n): SdkHealthReporter => {\n const reported = new Set<SdkHealthDiagnostic>();\n return (report) => {\n if (reported.has(report.diagnostic)) return;\n reported.add(report.diagnostic);\n try {\n emit({\n type: \"sdk_health\",\n body: `sdk_health ${report.diagnostic}`,\n severity: \"INFO\",\n timeUnixMs: Date.now(),\n attributes: {\n [SDK_DIAGNOSTIC_KEY]: report.diagnostic,\n ...report.attributes,\n },\n });\n } catch {\n // The SDK's own health must never throw into the host.\n }\n };\n};\n",
|
|
13
|
+
"import { truncateValue } from \"./attributes\";\nimport type { SdkHealthReport } from \"./sdk-health\";\nimport type {\n AttributeValue,\n ClientEvent,\n EventAttributes,\n EventSeverity,\n} from \"./types\";\n\ntype OtlpAnyValue =\n | { stringValue: string }\n | { intValue: string }\n | { doubleValue: number }\n | { boolValue: boolean };\n\ninterface OtlpKeyValue {\n key: string;\n value: OtlpAnyValue;\n}\n\nexport interface OtlpLogRecord {\n timeUnixNano: string;\n severityNumber: number;\n severityText: EventSeverity;\n body: { stringValue: string };\n attributes: OtlpKeyValue[];\n /** OTLP/JSON encodes trace/span ids as hex (unlike proto3 JSON's base64). */\n traceId?: string;\n spanId?: string;\n}\n\nconst SEVERITY_NUMBERS: Record<EventSeverity, number> = {\n DEBUG: 5,\n INFO: 9,\n WARN: 13,\n ERROR: 17,\n};\n\nconst MAX_BODY_LENGTH = 1024;\n\nconst encodeValue = (value: AttributeValue): OtlpAnyValue => {\n if (typeof value === \"boolean\") {\n return { boolValue: value };\n }\n if (typeof value === \"number\") {\n // proto3 JSON maps int64 to string; doubles stay numeric.\n return Number.isInteger(value)\n ? { intValue: String(value) }\n : { doubleValue: value };\n }\n return { stringValue: truncateValue(value) };\n};\n\nconst encodeAttributes = (attributes: EventAttributes): OtlpKeyValue[] => {\n const encoded: OtlpKeyValue[] = [];\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined) {\n continue;\n }\n encoded.push({ key, value: encodeValue(value) });\n }\n return encoded;\n};\n\nexport const encodeLogRecord = (\n event: ClientEvent,\n baseAttributes: EventAttributes,\n): OtlpLogRecord => {\n const record: OtlpLogRecord = {\n timeUnixNano: `${event.timeUnixMs}000000`,\n severityNumber: SEVERITY_NUMBERS[event.severity],\n severityText: event.severity,\n body: { stringValue: truncateValue(event.body, MAX_BODY_LENGTH) },\n attributes: encodeAttributes({ ...baseAttributes, ...event.attributes }),\n };\n if (event.traceId) {\n record.traceId = event.traceId;\n }\n if (event.spanId) {\n record.spanId = event.spanId;\n }\n return record;\n};\n\n/**\n * The slice of `fetch` the transport uses. Declared structurally so the core\n * needs no DOM lib; every host's real `fetch` satisfies it.\n */\nexport type FetchLike = (\n url: string,\n init: {\n method: \"POST\";\n headers: Record<string, string>;\n body: string;\n keepalive: boolean;\n /** An `AbortSignal` where the host has `AbortController`; aborts a timed-out upload. */\n signal?: unknown;\n },\n) => Promise<unknown>;\n\n/**\n * UTF-8 byte length of a string, without `TextEncoder` (Hermes has none).\n * Request budgets are bytes on the wire; a string's `length` counts UTF-16\n * units and undercounts non-ASCII text up to threefold.\n */\nexport const utf8Length = (text: string): number => {\n let bytes = text.length;\n for (let index = 0; index < text.length; index += 1) {\n const code = text.charCodeAt(index);\n if (code < 0x80) continue;\n if (code < 0x800) {\n bytes += 1;\n } else if (\n code >= 0xd800 &&\n code <= 0xdbff &&\n (text.charCodeAt(index + 1) & 0xfc00) === 0xdc00\n ) {\n // A surrogate pair: two units, four bytes.\n bytes += 2;\n index += 1;\n } else {\n bytes += 2;\n }\n }\n return bytes;\n};\n\n/**\n * Queued-batch uploads allowed in flight at once. A hung intake holds at most\n * this many requests; later batches wait in the capped queue.\n */\nconst MAX_IN_FLIGHT_UPLOADS = 4;\n/** Hard cap on queued bytes, beside `maxQueuedEvents`; the oldest go first. */\nconst MAX_QUEUED_BYTES = 1_048_576;\n/** An upload with no answer after this long counts as a network failure. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n/** Retry pacing: exponential from the base, capped, with jitter. */\nconst RETRY_BASE_MS = 1_000;\nconst RETRY_MAX_MS = 60_000;\n/** The longest `Retry-After` honored; a longer one waits this long. */\nconst RETRY_AFTER_MAX_MS = 300_000;\n/** Stamped on the first row delivered after events were dropped: how many. */\nexport const DROPPED_EVENTS_KEY = \"app.sdk.dropped_events\";\n/**\n * Names the sending SDK on every upload, `<package>/<version>`: the input\n * for the intake gateway's per-SDK request label (a separate change), which\n * can then count a request by SDK even when it rejects it before the key is\n * known. The gateway's CORS allow-list admits it, so a browser preflight\n * passes.\n */\nexport const SDK_HEADER = \"x-sazabi-sdk\";\n\nexport interface TransportOptions {\n /** Full OTLP logs endpoint, e.g. `https://<key>.<region>.intake.<domain>/v1/logs`. */\n url: string;\n publicKey: string;\n resourceAttributes: EventAttributes;\n /** OTLP instrumentation scope: the emitting SDK's name and version. */\n scope: { name: string; version: string };\n /**\n * The fetch to deliver with, captured before the SDK's network patch\n * installs, so exporter traffic can never recurse through our own wrapper\n * or appear in the captured event stream.\n */\n fetchImpl: FetchLike;\n flushIntervalMs: number;\n maxBatchBytes: number;\n maxQueuedEvents: number;\n /** Reports a dropped batch or a full queue; called once per reason. */\n warn?(message: string): void;\n /**\n * The SDK's own delivery health: a batch the intake refused for good\n * (`intake_rejected`, with the status) and uploads that failed on the\n * network before one got through (`intake_unreachable`, with the number\n * of failed tries). The binding turns these into `sdk_health` rows, once\n * per condition per session.\n */\n onHealth?(report: SdkHealthReport): void;\n /**\n * Called when an upload settles with nothing queued, nothing in flight, and\n * every row of the last `snapshotEnvelopes` delivered or refused for good:\n * a saved row evicted from the full queue, or given up by `shutdown()`\n * while the intake was unreachable, keeps it from firing until the next\n * snapshot, so the host's saved copy stays.\n */\n onIdle?(): void;\n}\n\nexport interface Transport {\n enqueue(record: OtlpLogRecord): void;\n /**\n * Deliver everything queued. `keepalive: true` is for browser unload paths;\n * each request body must stay inside the browser's ~64 KiB keepalive\n * budget, which enqueue-side chunking guarantees via `maxBatchBytes`. Hosts\n * without keepalive ignore the flag.\n */\n flush(options?: { keepalive?: boolean }): Promise<void>;\n /**\n * Encode everything queued into standalone request bodies without sending,\n * leaving the queue empty. The mobile crash outbox persists these and posts\n * them on the next launch; `recordAttributes` are appended to every record\n * (the outbox stamps its replay id there).\n */\n drainEnvelopes(recordAttributes?: EventAttributes): string[];\n /**\n * Encode everything queued into standalone request bodies and keep it\n * queued. `recordAttributes` are stamped on the queued records themselves,\n * so the live upload and the saved copy carry the same values (the mobile\n * SDK saves its queue this way when the app leaves the foreground). Rows an\n * earlier snapshot saved whose upload is still in flight are included as\n * they were sent: the copy this one replaces held them.\n */\n snapshotEnvelopes(recordAttributes: EventAttributes): string[];\n /**\n * While held, `enqueue` never flushes on its own, neither on the byte\n * budget nor by scheduling the timer: the mobile crash path holds the\n * transport while it stamps the fatal row and everything buffered before\n * it, then drains the whole batch into the outbox. Without this, a fatal\n * row that tips the queue over the budget would be flushed out of the queue\n * into a request the dying process never completes.\n */\n holdFlushes(hold: boolean): void;\n /**\n * Deliver a previously drained envelope. Resolves `true` once the envelope\n * is settled: the intake accepted it (2xx) or refused it for good (a 4xx\n * other than 408 and 429, dropped with a warning). Resolves `false` when it\n * is worth sending again (408, 429, 5xx, a network failure, a timeout), and\n * never rejects, so a persisted envelope is deleted only once settled.\n */\n sendEnvelope(body: string): Promise<boolean>;\n shutdown(): Promise<void>;\n}\n\ninterface QueuedRecord {\n record: OtlpLogRecord;\n bytes: number;\n /** Enqueue order: failed batches merge back by it, so eviction stays oldest-first. */\n seq: number;\n /** Stamped by `snapshotEnvelopes`: the host holds a saved copy of this row. */\n saved?: boolean;\n}\n\ntype Outcome =\n | { kind: \"sent\" }\n /** `network` marks a failure with no answer at all: a dropped connection or a timeout. */\n | { kind: \"retry\"; retryAfterMs?: number; network?: true }\n | { kind: \"drop\" | \"split\"; status: number };\n\nconst RETRY: Outcome = { kind: \"retry\", network: true };\n\n/** `Retry-After` as delta-seconds or an HTTP date, in ms; undefined when absent. */\nconst retryAfterMs = (response: unknown): number | undefined => {\n try {\n const raw = (\n response as { headers?: { get?(name: string): string | null } }\n ).headers?.get?.(\"retry-after\");\n if (!raw) return undefined;\n const seconds = Number(raw);\n const ms = Number.isFinite(seconds)\n ? seconds * 1_000\n : Date.parse(raw) - Date.now();\n return ms > 0 ? Math.min(ms, RETRY_AFTER_MAX_MS) : undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * A host fetch answers with a Response-like whose `status` tells what the\n * intake did with the batch; a host that answers with something else (a bare\n * promise from an unusual fetch) is trusted, as it always was.\n */\nconst classify = (response: unknown): Outcome => {\n const status = (response as { status?: unknown } | null)?.status;\n if (typeof status !== \"number\" || (status >= 200 && status < 300)) {\n return { kind: \"sent\" };\n }\n if (status === 408 || status === 429 || status >= 500) {\n return { kind: \"retry\", retryAfterMs: retryAfterMs(response) };\n }\n return { kind: status === 413 ? \"split\" : \"drop\", status };\n};\n\nexport const createTransport = (options: TransportOptions): Transport => {\n const queue: QueuedRecord[] = [];\n let queuedBytes = 0;\n let nextRecordSeq = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let shutDown = false;\n let held = false;\n // Queued-batch uploads still open; each removes itself when it settles.\n const inFlight = new Set<Promise<void>>();\n // The rows of those uploads, for the next snapshot.\n const sending = new Set<QueuedRecord[]>();\n // A row of the last snapshot was lost undelivered: onIdle stays quiet.\n let savedLost = false;\n // Events dropped since the last delivered row said so.\n let dropped = 0;\n // Backoff after a retryable failure: nothing queued is sent before\n // `retryAt` except by shutdown() or an unload flush.\n let attempt = 0;\n let retryAt = 0;\n // Backoff windows opened by a failure with no answer from the intake\n // since it last answered anything (concurrent failures count once).\n let unreachableAttempts = 0;\n const warned = new Set<string>();\n\n const report = (health: SdkHealthReport): void => {\n try {\n options.onHealth?.(health);\n } catch {\n // Never throw into the host.\n }\n };\n\n const warnOnce = (reason: string, message: string): void => {\n if (warned.has(reason)) return;\n warned.add(reason);\n try {\n options.warn?.(message);\n } catch {\n // Never throw into the host.\n }\n };\n\n const buildEnvelope = (records: OtlpLogRecord[]): string =>\n JSON.stringify({\n resourceLogs: [\n {\n resource: {\n attributes: encodeAttributes(options.resourceAttributes),\n },\n scopeLogs: [\n {\n scope: {\n name: options.scope.name,\n version: options.scope.version,\n },\n logRecords: records,\n },\n ],\n },\n ],\n });\n\n const envelopeOverhead = utf8Length(buildEnvelope([])) + 64;\n const chunkBudget = Math.max(4096, options.maxBatchBytes - envelopeOverhead);\n\n const clearTimer = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n\n // The flush interval, or the end of a backoff when one is running.\n const scheduleFlush = (): void => {\n if (timer !== undefined || shutDown) {\n return;\n }\n const wait = retryAt - Date.now();\n timer = setTimeout(\n () => {\n timer = undefined;\n void flush();\n },\n wait > 0 ? wait : options.flushIntervalMs,\n );\n };\n\n // Oldest first, counted: a long offline period keeps the newest events\n // within both caps and says how many it lost. enqueue() refuses a record\n // larger than the byte cap, so this never empties the queue on bytes.\n const evict = (): void => {\n while (\n queue.length > options.maxQueuedEvents ||\n queuedBytes > MAX_QUEUED_BYTES\n ) {\n const removed = queue.shift() as QueuedRecord;\n queuedBytes -= removed.bytes;\n dropped += 1;\n if (removed.saved) savedLost = true;\n warnOnce(\n \"full\",\n \"the event queue is full (is the device offline?); dropping the oldest events.\",\n );\n }\n };\n\n // At most `limit` chunks of `rows`, oldest records first.\n const chunksOf = (rows: QueuedRecord[], limit: number): QueuedRecord[][] => {\n const chunks: QueuedRecord[][] = [];\n if (limit <= 0) {\n return chunks;\n }\n let current: QueuedRecord[] = [];\n let currentBytes = 0;\n\n for (const queued of rows) {\n if (current.length > 0 && currentBytes + queued.bytes > chunkBudget) {\n chunks.push(current);\n current = [];\n currentBytes = 0;\n if (chunks.length >= limit) {\n break;\n }\n }\n current.push(queued);\n currentBytes += queued.bytes;\n }\n if (current.length > 0) {\n chunks.push(current);\n }\n return chunks;\n };\n\n // The same chunks, taken out of the queue; whatever does not fit stays.\n const takeChunks = (limit: number): QueuedRecord[][] => {\n const chunks = chunksOf(queue, limit);\n let taken = 0;\n for (const chunk of chunks) taken += chunk.length;\n for (const removed of queue.splice(0, taken)) {\n queuedBytes -= removed.bytes;\n }\n return chunks;\n };\n\n // A record with `extra` set, replacing any value it already had for a key.\n const stamped = (\n record: OtlpLogRecord,\n extra: OtlpLogRecord[\"attributes\"],\n ): OtlpLogRecord =>\n extra.length === 0\n ? record\n : {\n ...record,\n attributes: [\n ...record.attributes.filter(\n (attribute) => !extra.some(({ key }) => key === attribute.key),\n ),\n ...extra,\n ],\n };\n\n // The records of a chunk, the first carrying the drop count when there is\n // one; `count` is what the caller must give back if the rows never land.\n const recordsOf = (\n chunk: QueuedRecord[],\n ): { records: OtlpLogRecord[]; count: number } => {\n const records = chunk.map((queued) => queued.record);\n const count = dropped;\n const first = records[0];\n if (count > 0 && first) {\n dropped = 0;\n records[0] = {\n ...first,\n attributes: [\n ...first.attributes,\n { key: DROPPED_EVENTS_KEY, value: { intValue: String(count) } },\n ],\n };\n }\n return { records, count: first ? count : 0 };\n };\n\n const post = (body: string, keepalive = false): Promise<Outcome> => {\n const Abort = (globalThis as { AbortController?: new () => unknown })\n .AbortController;\n let controller: { signal?: unknown; abort?(): void } | undefined;\n let request: Promise<unknown>;\n try {\n controller = Abort ? (new Abort() as typeof controller) : undefined;\n request = options.fetchImpl(options.url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${options.publicKey}`,\n [SDK_HEADER]: `${options.scope.name}/${options.scope.version}`,\n },\n body,\n keepalive,\n signal: controller?.signal,\n });\n } catch {\n // A fetch that throws synchronously must not reject out of flush():\n // the SDK would capture its own unhandled rejection, enqueue it, and\n // fail again on the next flush.\n return Promise.resolve(RETRY);\n }\n return new Promise((resolve) => {\n // A hung intake frees the slot: the batch is retried like a failure.\n const timeout = setTimeout(() => {\n try {\n controller?.abort?.();\n } catch {\n // Best-effort.\n }\n resolve(RETRY);\n }, REQUEST_TIMEOUT_MS);\n void Promise.resolve(request)\n .then(classify, () => RETRY)\n .then((outcome) => {\n clearTimeout(timeout);\n resolve(outcome);\n });\n });\n };\n\n const backingOff = (): boolean => Date.now() < retryAt;\n\n // Rows that did not land go back in enqueue order, wherever they sit\n // among rows restored by other uploads, so evict() still drops the oldest.\n const restore = (chunk: QueuedRecord[]): void => {\n queue.push(...chunk);\n queue.sort((a, b) => a.seq - b.seq);\n for (const queued of chunk) queuedBytes += queued.bytes;\n evict();\n };\n\n const backOff = (retryAfter: number | undefined, network: boolean): void => {\n const now = Date.now();\n // Concurrent uploads failing together are one failure, not four.\n if (now >= retryAt) {\n attempt += 1;\n if (network) unreachableAttempts += 1;\n }\n const ceiling = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (attempt - 1));\n const delay = Math.max(\n retryAfter ?? 0,\n ceiling / 2 + (Math.random() * ceiling) / 2,\n );\n retryAt = Math.max(retryAt, now + delay);\n clearTimer();\n scheduleFlush();\n };\n\n /**\n * One delivery attempt and what its answer means: 2xx is done; 408, 429,\n * 5xx, a network failure, and a timeout put the rows back in the queue in\n * enqueue order behind a backoff (the caps still apply); 413 halves the batch\n * and tries each half, putting a half back in the queue instead when a\n * backoff started meanwhile (outside an unload flush or shutdown); any\n * other 4xx, or a single row too large to send, drops the rows with one\n * warning per status. Nothing is retried after shutdown().\n */\n const deliver = async (\n chunk: QueuedRecord[],\n keepalive: boolean,\n ): Promise<void> => {\n const { records, count } = recordsOf(chunk);\n const outcome = await post(buildEnvelope(records), keepalive);\n // Any answer, a refusal included, ends an outage: what it took to get\n // through is reported now, not merged into a later one.\n if (\n unreachableAttempts > 0 &&\n (outcome.kind !== \"retry\" || outcome.network !== true)\n ) {\n const attempts = unreachableAttempts;\n unreachableAttempts = 0;\n report({\n diagnostic: \"intake_unreachable\",\n attributes: { \"app.sdk.attempts\": attempts },\n });\n }\n if (outcome.kind === \"sent\") {\n attempt = 0;\n return;\n }\n dropped += count;\n if (outcome.kind === \"split\" && chunk.length > 1) {\n const half = Math.ceil(chunk.length / 2);\n for (const part of [chunk.slice(0, half), chunk.slice(half)]) {\n // A 429 on the first half (or on any other upload) means wait: the\n // rest goes back to the queue for the timer to send.\n if (!keepalive && !shutDown && backingOff()) {\n restore(part);\n scheduleFlush();\n } else {\n await deliver(part, keepalive);\n }\n }\n return;\n }\n if (outcome.kind === \"retry\") {\n if (shutDown) {\n if (chunk.some((queued) => queued.saved)) savedLost = true;\n return;\n }\n restore(chunk);\n backOff(outcome.retryAfterMs, outcome.network === true);\n return;\n }\n dropped += chunk.length;\n warnOnce(\n String(outcome.status),\n `the intake refused a batch with HTTP ${outcome.status}; dropped ${chunk.length} event(s).`,\n );\n report({\n diagnostic: \"intake_rejected\",\n attributes: { \"app.sdk.http_status\": outcome.status },\n });\n };\n\n // Every upload gets a sequence number so a flush() can wait for exactly\n // the uploads started while it waited, and no others: an older upload\n // that never answers holds its slot, not every later caller.\n let nextUploadSeq = 0;\n const uploadsBySeq = new Map<number, Promise<void>>();\n const startUpload = (chunk: QueuedRecord[], keepalive: boolean) => {\n const seq = nextUploadSeq;\n nextUploadSeq += 1;\n const upload: Promise<void> = deliver(chunk, keepalive).then(() => {\n inFlight.delete(upload);\n sending.delete(chunk);\n uploadsBySeq.delete(seq);\n if (queue.length === 0 && inFlight.size === 0 && !savedLost) {\n try {\n options.onIdle?.();\n } catch {\n // Never throw into the host.\n }\n }\n });\n inFlight.add(upload);\n sending.add(chunk);\n uploadsBySeq.set(seq, upload);\n return upload;\n };\n\n // One shared loop drains the queue: it keeps at most MAX_IN_FLIGHT_UPLOADS\n // requests open, refills each slot as soon as any upload settles (so one\n // hung upload holds only its own slot), and ends once everything queued\n // has been started or a failure started a backoff. Every flush() while it\n // runs joins this loop instead of attaching another waiter to the same\n // hung uploads, so a stalled intake never grows memory beyond the capped\n // queue.\n let draining: Promise<void> | null = null;\n const drainQueue = async (): Promise<void> => {\n while (!backingOff()) {\n for (const chunk of takeChunks(MAX_IN_FLIGHT_UPLOADS - inFlight.size)) {\n startUpload(chunk, false);\n }\n if (queue.length === 0) break;\n // A non-empty queue always fills a free slot, so `inFlight` is never\n // empty here.\n await Promise.race(inFlight);\n }\n };\n\n const flush = async (flushOptions?: {\n keepalive?: boolean;\n }): Promise<void> => {\n clearTimer();\n const keepalive = flushOptions?.keepalive === true;\n\n // An unload flush is the page's last chance and goes out whole, backoff\n // or not, and so does the one shutdown() makes; any other flush joins\n // the shared drain loop and resolves once the uploads started during its\n // wait have settled. During a backoff it sends nothing: the timer\n // retries when the backoff ends.\n if (keepalive || shutDown) {\n await Promise.all(\n takeChunks(Number.POSITIVE_INFINITY).map((chunk) =>\n startUpload(chunk, keepalive),\n ),\n );\n return;\n }\n const firstSeq = nextUploadSeq;\n while (queue.length > 0 && !backingOff()) {\n if (draining === null) {\n draining = drainQueue().finally(() => {\n draining = null;\n });\n }\n await draining;\n }\n if (queue.length > 0) scheduleFlush();\n const mine: Promise<void>[] = [];\n for (const [seq, upload] of uploadsBySeq) {\n if (seq >= firstSeq) mine.push(upload);\n }\n await Promise.all(mine);\n };\n\n return {\n enqueue(record) {\n if (shutDown) {\n return;\n }\n const bytes = utf8Length(JSON.stringify(record)) + 1;\n // One record with hundreds of long attributes can outgrow the byte cap\n // alone: it is dropped and counted, never buffered above the cap.\n if (bytes > MAX_QUEUED_BYTES) {\n dropped += 1;\n warnOnce(\n \"oversized\",\n \"dropped an event larger than the 1 MiB queue limit.\",\n );\n return;\n }\n queue.push({ record, bytes, seq: nextRecordSeq });\n nextRecordSeq += 1;\n queuedBytes += bytes;\n evict();\n\n if (held) {\n return;\n }\n if (queuedBytes >= chunkBudget && !backingOff()) {\n void flush();\n } else {\n scheduleFlush();\n }\n },\n flush,\n drainEnvelopes(recordAttributes) {\n clearTimer();\n const extra = recordAttributes ? encodeAttributes(recordAttributes) : [];\n return takeChunks(Number.POSITIVE_INFINITY).map((chunk) => {\n const { records } = recordsOf(chunk);\n return buildEnvelope(records.map((record) => stamped(record, extra)));\n });\n },\n snapshotEnvelopes(recordAttributes) {\n const extra = encodeAttributes(recordAttributes);\n savedLost = false;\n for (const queued of queue) {\n queued.record = stamped(queued.record, extra);\n const bytes = utf8Length(JSON.stringify(queued.record)) + 1;\n queuedBytes += bytes - queued.bytes;\n queued.bytes = bytes;\n queued.saved = true;\n }\n // A second snapshot while the first one's uploads are out (iOS goes\n // inactive, then background) must not drop their rows from the copy.\n const rows = new Set(queue);\n for (const chunk of sending) {\n for (const queued of chunk) if (queued.saved) rows.add(queued);\n }\n return chunksOf(\n [...rows].sort((a, b) => a.seq - b.seq),\n Number.POSITIVE_INFINITY,\n ).map((chunk) => buildEnvelope(chunk.map((queued) => queued.record)));\n },\n holdFlushes(hold) {\n held = hold;\n },\n async sendEnvelope(body) {\n const outcome = await post(body);\n if (outcome.kind === \"retry\") return false;\n if (outcome.kind !== \"sent\") {\n warnOnce(\n String(outcome.status),\n `the intake refused a stored crash report with HTTP ${outcome.status}; dropped it.`,\n );\n report({\n diagnostic: \"intake_rejected\",\n attributes: { \"app.sdk.http_status\": outcome.status },\n });\n }\n return true;\n },\n async shutdown() {\n shutDown = true;\n await flush();\n },\n };\n};\n",
|
|
14
|
+
"import type { BeforeSend, ClientEvent } from \"./types\";\n\n/**\n * The host's `beforeSend` hook, run so that it can never break the SDK: a\n * hook that throws or returns something other than an event drops the event\n * (a hook that redacts must not fail open) and warns once per reason. An\n * event captured synchronously while the hook runs (a `log()` or\n * `addEvent()` call inside it) is dropped rather than sent around the hook\n * or fed back into it, so a hook can never recurse. A request the hook\n * starts completes later and produces an ordinary network event the hook\n * sees like any other; the hook is responsible for excluding it.\n */\nexport const createBeforeSendGate = (\n hook: BeforeSend | undefined,\n warnOnce: (warning: { reason: string; message: string }) => void,\n): ((event: ClientEvent) => ClientEvent | null) => {\n if (!hook) {\n return (event) => event;\n }\n let running = false;\n return (event) => {\n if (running) {\n warnOnce({\n reason: \"before_send_nested\",\n message:\n \"an event was captured while beforeSend was running; nested events are dropped.\",\n });\n return null;\n }\n running = true;\n let result: ClientEvent | null;\n try {\n result = hook(event);\n } catch (error) {\n warnOnce({\n reason: \"before_send_threw\",\n message: `beforeSend threw (${error instanceof Error ? error.message : String(error)}); the event was dropped.`,\n });\n return null;\n } finally {\n running = false;\n }\n if (result === null) {\n return null;\n }\n if (\n typeof result !== \"object\" ||\n typeof result.type !== \"string\" ||\n typeof result.body !== \"string\" ||\n typeof result.severity !== \"string\" ||\n typeof result.timeUnixMs !== \"number\" ||\n typeof result.attributes !== \"object\" ||\n result.attributes === null\n ) {\n warnOnce({\n reason: \"before_send_invalid\",\n message:\n \"beforeSend must return the event or null; the event was dropped.\",\n });\n return null;\n }\n return result;\n };\n};\n",
|
|
15
|
+
"import type {\n ClientEvent,\n EventSeverity,\n PointerTracker,\n} from \"@sazabi/client-sdk-core\";\n\nexport interface SdkInstance {\n identify(distinctId: string, traits?: Record<string, unknown>): void;\n reset(): void;\n addEvent(name: string, attributes?: Record<string, unknown>): void;\n log(\n severity: EventSeverity,\n message: string,\n metadata?: Record<string, unknown>,\n ): void;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\ninterface PendingIdentity {\n distinctId: string;\n traits?: Record<string, unknown>;\n}\n\nexport interface CurrentScreen {\n name?: string;\n path?: string;\n /** The navigator's per-visit route key: two pushes of one screen are two visits. */\n key?: string;\n}\n\ninterface SdkState {\n instance?: SdkInstance;\n initialized: boolean;\n warnedWebHost: boolean;\n /** Bumped by every init() and shutdown(), so a consent promise that resolves after a shutdown starts nothing. */\n generation: number;\n pendingIdentity?: PendingIdentity;\n pendingReset: boolean;\n /** The running instance's pointer tracker, fed by every `<SazabiRoot>`. */\n tracker?: PointerTracker;\n /** Where `<SazabiRoot>` reports a render or commit error it caught. */\n renderErrorSink?: (error: unknown) => void;\n /** Where `trackNavigation()` sends its rows. */\n navigationEmit?: (event: ClientEvent) => void;\n /** Applied to the route path a navigation row records: a deep link can carry a query string. */\n navigationRedactUrl?: (url: string) => string;\n /** The route the user is on, stamped on every row as `app.screen.*`. */\n screen: CurrentScreen;\n /** The development note about the unreadable rejection slot, printed once per process. */\n warnedRejectionSlot: boolean;\n}\n\n/**\n * Process-wide, not module-wide: a second evaluation of this module (a\n * duplicate copy in the bundle, a module registry reset in development)\n * finds the running instance instead of installing a second `ErrorUtils`\n * handler, rejection tracker, and transport beside it, and a `<SazabiRoot>`\n * or `trackNavigation()` imported from that copy feeds the running instance.\n */\nexport const state: SdkState = ((\n globalThis as { __sazabiReactNative?: SdkState }\n).__sazabiReactNative ??= {\n initialized: false,\n warnedWebHost: false,\n warnedRejectionSlot: false,\n generation: 0,\n pendingReset: false,\n screen: {},\n});\n",
|
|
16
|
+
"export const SDK_NAME = \"@sazabi/react-native\";\nexport const SDK_VERSION = \"0.1.1\";\n/** SDK-emitted console lines carry this prefix and are never re-captured. */\nexport const SDK_CONSOLE_PREFIX = \"[@sazabi/react-native]\";\n",
|
|
17
|
+
"import {\n type ClientEvent,\n truncateValue,\n withoutConsoleCapture,\n} from \"@sazabi/client-sdk-core\";\nimport { isDevHost } from \"./host\";\nimport { state } from \"./process-state\";\nimport type { RejectionTracker } from \"./types\";\nimport { SDK_CONSOLE_PREFIX } from \"./version\";\n\ntype GlobalHandler = (error: unknown, isFatal?: boolean) => void;\n\ninterface ErrorUtilsLike {\n getGlobalHandler?(): GlobalHandler | undefined;\n setGlobalHandler?(handler: GlobalHandler): void;\n}\n\ninterface HermesInternalLike {\n enablePromiseRejectionTracker?(options: RejectionTracker): void;\n}\n\nconst hermes = (): HermesInternalLike | undefined =>\n (globalThis as { HermesInternal?: HermesInternalLike }).HermesInternal;\n\n/** Call a tracker callback without letting its throw reach Hermes or skip the next one. */\nconst notify = (call: () => void): void => {\n try {\n call();\n } catch {\n // Never throw into Hermes.\n }\n};\n\nexport interface ErrorCaptureOptions {\n emit(event: ClientEvent): void;\n /**\n * Receives the fatal error's row instead of `emit`, before the previous\n * handler runs: the crash path must stamp that row with the transport held\n * so it cannot be flushed out of the queue before the outbox drains it.\n */\n onFatal(event: ClientEvent): void;\n unhandledRejections: boolean;\n /** The tracker the host says held the slot before `init()`; runs after ours. */\n previousRejectionTracker?: RejectionTracker;\n}\n\nexport interface ErrorCaptureHandle {\n teardown(): void;\n}\n\n/**\n * Text for anything an app can throw, never throwing itself: a string as is,\n * an object as JSON (a cycle or a throwing `toJSON` falls back to its tag,\n * and a tag that throws too to a fixed string),\n * cut to the attribute limit before it is copied anywhere.\n */\nconst describe = (value: unknown): string => {\n try {\n const text =\n typeof value === \"string\"\n ? value\n : typeof value === \"object\" && value !== null\n ? (JSON.stringify(value) ?? String(value))\n : String(value);\n return truncateValue(text);\n } catch {\n // The tag can throw too (a throwing `Symbol.toStringTag` getter).\n try {\n return truncateValue(Object.prototype.toString.call(value));\n } catch {\n return \"[unreadable thrown value]\";\n }\n }\n};\n\nconst errorEvent = (\n error: unknown,\n kind: \"fatal\" | \"uncaught\" | \"unhandledrejection\",\n): ClientEvent => {\n // Read through guards: an Error may carry a throwing getter, a non-string\n // `name` or `stack`, or a 300 KB message.\n const fields = error instanceof Error ? error : undefined;\n const read = (key: \"name\" | \"message\" | \"stack\"): string | undefined => {\n try {\n const value: unknown = fields?.[key];\n return typeof value === \"string\" ? truncateValue(value) : undefined;\n } catch {\n return undefined;\n }\n };\n const exceptionType =\n read(\"name\") ||\n (kind === \"unhandledrejection\" ? \"UnhandledRejection\" : \"Error\");\n const message = fields ? (read(\"message\") ?? \"\") : describe(error);\n return {\n type: \"error\",\n body: truncateValue(`${exceptionType}: ${message}`, 512),\n severity: \"ERROR\",\n timeUnixMs: Date.now(),\n attributes: {\n \"exception.type\": exceptionType,\n \"exception.message\": message,\n \"exception.stacktrace\": read(\"stack\") ?? \"\",\n \"app.error.kind\": kind,\n },\n };\n};\n\n/**\n * Where `<SazabiRoot>` reports a render or commit error it caught. React 19's\n * renderer hands an error no boundary caught straight to React Native's\n * `ExceptionsManager` (the renderer's `onUncaughtError`), never through\n * `ErrorUtils`, and the release app dies there. The root's boundary records\n * the error through the process-wide sink first (so a root from a second\n * copy of the module reaches the running instance), then rethrows it so\n * React Native's fatal path still runs unchanged.\n */\nexport const captureRenderError = (error: unknown): void => {\n try {\n state.renderErrorSink?.(error);\n } catch {\n // Capture failures must never surface to the host.\n }\n};\n\nconst describeRejection = (rejection: unknown): string => {\n if (rejection instanceof Error) {\n // Hermes and V8 stacks open with the `Name: message` line.\n return rejection.stack || `${rejection.name}: ${rejection.message}`;\n }\n if (typeof rejection === \"string\") return rejection;\n try {\n return JSON.stringify(rejection) ?? String(rejection);\n } catch {\n return String(rejection);\n }\n};\n\n/**\n * React Native's own tracker only warns, and only in development\n * (`polyfillPromise.js` installs it under `__DEV__`, before any module of the\n * app can watch the slot). With no other tracker seen before ours, the chain\n * is its behavior, relayed: the same warning, which LogBox shows, kept out of\n * the console mirror.\n */\nconst warnInDev = (message: () => string): void => {\n if (!isDevHost()) return;\n try {\n // Formatting runs inside the guard: a rejection may be cyclic, have no\n // prototype, or carry a throwing getter.\n const text = message();\n withoutConsoleCapture(() => console.warn(text));\n } catch {\n // Never throw into the host.\n }\n};\n\n/**\n * Uncaught errors route through React Native's global handler; a fatal one\n * terminates the app once the handler returns, which is why the crash outbox\n * runs inside it. The previous handler (React Native's red box in\n * development, a crash reporter's hook) always runs after ours. Render\n * errors reach the same fatal path from `<SazabiRoot>` (`captureRenderError`);\n * a crash reported more than once in one task is recorded once.\n *\n * Hermes exposes one promise-rejection tracker slot and no way to read it:\n * `HermesInternal` is frozen (its methods non-writable and non-configurable,\n * the object non-extensible) and the global that names it is non-writable\n * and non-configurable too (`lib/VM/JSLib/HermesInternal.cpp`,\n * `lib/VM/JSLib/GlobalObject.cpp`), so the installer cannot be wrapped from\n * JavaScript and whoever held the slot before `init()` is unknowable. The\n * host hands that tracker over as `errors.previousRejectionTracker` and ours\n * runs it after recording; `errors.unhandledRejections: false` leaves the\n * slot alone. Without either, a development build is told once per process.\n */\nexport const installErrorCapture = (\n options: ErrorCaptureOptions,\n): ErrorCaptureHandle => {\n const errorUtils = (globalThis as { ErrorUtils?: ErrorUtilsLike }).ErrorUtils;\n const previous = errorUtils?.getGlobalHandler?.();\n // Cleared by teardown. The Hermes rejection slot cannot be handed back (its\n // previous occupant is unknowable) and a later patcher may keep our global\n // handler in its chain, so both callbacks go inert instead: after shutdown\n // nothing is built or emitted, and the session store is never touched.\n let active = true;\n // One fatal row per task: React retries a failed render once before giving\n // up (a component that throws `new Error()` hands the boundary a second\n // instance), and a React 18 renderer rethrows the same crash into\n // `ErrorUtils`, all before the task ends.\n let fatalRecordedThisTask = false;\n\n const recordFatal = (error: unknown): void => {\n if (!active || fatalRecordedThisTask) return;\n fatalRecordedThisTask = true;\n setTimeout(() => {\n fatalRecordedThisTask = false;\n }, 0);\n options.onFatal(errorEvent(error, \"fatal\"));\n };\n\n // `isFatal` is React Native's own flag, passed through as `app.error.kind`:\n // a development build reports an uncaught error as fatal even though the\n // red box keeps the app alive, and `ErrorUtils.reportError` as not fatal.\n const handler: GlobalHandler = (error, isFatal) => {\n const callPrevious = (): void => previous?.(error, isFatal);\n if (!active) {\n callPrevious();\n return;\n }\n try {\n if (isFatal) {\n recordFatal(error);\n } else {\n options.emit(errorEvent(error, \"uncaught\"));\n }\n } catch {\n // Capture failures must never surface to the host.\n }\n // React Native's handler feeds the error back into `console.error`; the\n // error row already holds it, so the console mirror skips that line.\n withoutConsoleCapture(callPrevious);\n };\n errorUtils?.setGlobalHandler?.(handler);\n state.renderErrorSink = recordFatal;\n\n if (options.unhandledRejections) {\n const previousTracker = options.previousRejectionTracker;\n if (\n !previousTracker &&\n !state.warnedRejectionSlot &&\n isDevHost() &&\n typeof hermes()?.enablePromiseRejectionTracker === \"function\"\n ) {\n state.warnedRejectionSlot = true;\n warnInDev(\n () =>\n `${SDK_CONSOLE_PREFIX} init() takes over Hermes's only promise rejection tracker slot, and a tracker another library installed before it cannot be seen. If one exists, pass errors.previousRejectionTracker to keep it running, or errors.unhandledRejections: false to leave the slot alone.`,\n );\n }\n const tracker: RejectionTracker = {\n allRejections: true,\n onUnhandled: (id, rejection) => {\n if (active) {\n try {\n options.emit(errorEvent(rejection, \"unhandledrejection\"));\n } catch {\n // Never throw into the host.\n }\n }\n if (previousTracker) {\n notify(() => previousTracker.onUnhandled(id, rejection));\n return;\n }\n warnInDev(\n () =>\n `Possible unhandled promise rejection (id: ${id}):\\n${describeRejection(rejection)}`,\n );\n },\n onHandled: (id) => {\n if (previousTracker) {\n notify(() => previousTracker.onHandled?.(id));\n return;\n }\n warnInDev(\n () =>\n `Promise rejection handled (id: ${id})\\nThis means you can ignore any previous messages of the form \"Possible unhandled promise rejection (id: ${id}):\"`,\n );\n },\n };\n try {\n hermes()?.enablePromiseRejectionTracker?.(tracker);\n } catch {\n // No tracker on this engine.\n }\n }\n\n return {\n teardown() {\n active = false;\n if (state.renderErrorSink === recordFatal) {\n state.renderErrorSink = undefined;\n }\n try {\n if (previous && errorUtils?.getGlobalHandler?.() === handler) {\n errorUtils.setGlobalHandler?.(previous);\n }\n } catch {\n // Best-effort restore.\n }\n },\n };\n};\n",
|
|
18
|
+
"import type { PointerTarget, PointerTracker } from \"@sazabi/client-sdk-core\";\nimport { Component, createElement, type ReactNode } from \"react\";\nimport { type GestureResponderEvent, View } from \"react-native\";\nimport { captureRenderError } from \"./errors\";\nimport { state } from \"./process-state\";\n\n/**\n * Touch capture for the pointer-episode tracker. `<SazabiRoot>` is a plain\n * `View` that observes every touch bubbling up from the app without claiming\n * the responder, so gestures reach their targets untouched. It works\n * wherever it is mounted and whenever `init()` runs: touches before activation\n * are dropped, never queued.\n */\n\n// The tracker lives in the process-wide state, so a `<SazabiRoot>` imported\n// from a second copy of the module feeds the running instance.\nexport const setActiveTracker = (tracker: PointerTracker | undefined): void => {\n state.tracker = tracker;\n};\n\n// ------------------------------------------------------ target naming -----\n// React Native exposes no DOM, so the widget under a touch is found by\n// walking the React fiber the synthetic event carries. This reads React\n// internals (the same ones Sentry's TouchEventBoundary reads) and is guarded:\n// when the shape drifts, the episode still emits, without a name.\n\ninterface FiberLike {\n memoizedProps?: Record<string, unknown> | null;\n type?: unknown;\n elementType?: unknown;\n return?: FiberLike | null;\n}\n\nconst MAX_FIBER_DEPTH = 60;\nconst MAX_PATH_SEGMENTS = 3;\nconst MAX_NAME_CHARS = 64;\nconst NAME_PROP = \"sazabiName\";\nconst MASK_PROP = \"sazabiMask\";\nconst INTERACTIVE_ROLES = new Set([\n \"button\",\n \"link\",\n \"tab\",\n \"menuitem\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"togglebutton\",\n \"imagebutton\",\n \"searchbox\",\n]);\n\nconst normalizeName = (raw: unknown): string | undefined => {\n if (typeof raw !== \"string\") return undefined;\n const collapsed = raw.replace(/\\s+/g, \" \").trim();\n if (!collapsed) return undefined;\n return collapsed.length > MAX_NAME_CHARS\n ? `${collapsed.slice(0, MAX_NAME_CHARS)}…`\n : collapsed;\n};\n\nconst componentName = (fiber: FiberLike): string | undefined => {\n const type = fiber.type ?? fiber.elementType;\n if (typeof type === \"function\") {\n const named = type as { displayName?: string; name?: string };\n return normalizeName(named.displayName || named.name);\n }\n if (typeof type === \"object\" && type !== null) {\n // forwardRef / memo wrappers carry the inner component.\n const wrapped =\n (type as { render?: unknown; type?: unknown }).render ??\n (type as { type?: unknown }).type;\n if (typeof wrapped === \"function\") {\n const named = wrapped as { displayName?: string; name?: string };\n return normalizeName(named.displayName || named.name);\n }\n }\n return undefined;\n};\n\nexport const describeFiberTarget = (\n targetInstance: unknown,\n): PointerTarget | undefined => {\n try {\n let fiber = targetInstance as FiberLike | null | undefined;\n if (!fiber || typeof fiber !== \"object\") return undefined;\n\n let name: string | undefined;\n let masked = false;\n let interactive = false;\n const path: string[] = [];\n\n for (let depth = 0; fiber && depth < MAX_FIBER_DEPTH; depth += 1) {\n const props = fiber.memoizedProps ?? undefined;\n if (props) {\n if (props[MASK_PROP]) masked = true;\n if (name === undefined) {\n name =\n normalizeName(props[NAME_PROP]) ??\n normalizeName(props.accessibilityLabel) ??\n normalizeName(props.testID);\n }\n if (\n typeof props.onPress === \"function\" ||\n typeof props.onPressIn === \"function\" ||\n (typeof props.accessibilityRole === \"string\" &&\n INTERACTIVE_ROLES.has(props.accessibilityRole)) ||\n (typeof props.role === \"string\" && INTERACTIVE_ROLES.has(props.role))\n ) {\n interactive = true;\n }\n }\n if (path.length < MAX_PATH_SEGMENTS) {\n const segment = componentName(fiber);\n if (segment && segment !== \"SazabiRoot\") path.unshift(segment);\n }\n fiber = fiber.return;\n }\n\n return {\n id: path.length > 0 ? path.join(\" > \") : undefined,\n name: masked ? undefined : name,\n tag: path[path.length - 1],\n interactive,\n };\n } catch {\n return undefined;\n }\n};\n\n// --------------------------------------------------------- touch events --\n\ntype ResponderEvent = GestureResponderEvent & { _targetInst?: unknown };\n\nconst onTouchStart = (event: ResponderEvent): void => {\n const tracker = state.tracker;\n if (!tracker) return;\n try {\n const { changedTouches, touches } = event.nativeEvent;\n const target = describeFiberTarget(event._targetInst);\n for (const touch of changedTouches) {\n tracker.down({\n id: touch.identifier,\n type: \"touch\",\n primary: touches.length <= 1,\n x: touch.pageX,\n y: touch.pageY,\n pressure: touch.force || undefined,\n target,\n });\n }\n } catch {\n // Capture failures must never surface to the host.\n }\n};\n\nconst onTouchMove = (event: ResponderEvent): void => {\n const tracker = state.tracker;\n if (!tracker) return;\n try {\n for (const touch of event.nativeEvent.changedTouches) {\n tracker.move(\n touch.identifier,\n touch.pageX,\n touch.pageY,\n touch.force || undefined,\n );\n }\n } catch {\n // Ignore.\n }\n};\n\nconst onTouchEnd = (event: ResponderEvent): void => {\n const tracker = state.tracker;\n if (!tracker) return;\n try {\n for (const touch of event.nativeEvent.changedTouches) {\n tracker.up(touch.identifier, touch.pageX, touch.pageY);\n }\n } catch {\n // Ignore.\n }\n};\n\n// On Android a native ScrollView that intercepts a drag sends one cancel and\n// then nothing more for that touch, so the episode ends at the handoff.\nconst onTouchCancel = (event: ResponderEvent): void => {\n const tracker = state.tracker;\n if (!tracker) return;\n try {\n for (const touch of event.nativeEvent.changedTouches) {\n tracker.cancel(touch.identifier);\n }\n } catch {\n // Ignore.\n }\n};\n\nconst ROOT_STYLE = { flex: 1 };\n\n/**\n * Wrap the app's root in this once, outside every other component. It\n * renders a single full-size `View` with touch observers, and is the error\n * boundary that records a render or commit error as a `fatal` row (crash\n * outbox included) before rethrowing it: under React 19 such an error never\n * reaches `ErrorUtils`, so without this boundary a release app dies with\n * nothing recorded. The rethrow leaves React Native's own handling (the red\n * box in development, the native crash in release) exactly as it was.\n */\nexport class SazabiRoot extends Component<\n { children?: ReactNode },\n { error?: { value: unknown } }\n> {\n static displayName = \"SazabiRoot\";\n\n state: { error?: { value: unknown } } = {};\n\n static getDerivedStateFromError(error: unknown): {\n error: { value: unknown };\n } {\n // Render phase, before React reports anything: the row and the outbox\n // are written while the process is certainly still alive. A repeat call\n // from a retried render is recorded once.\n captureRenderError(error);\n return { error: { value: error } };\n }\n\n render(): ReturnType<typeof createElement> {\n const failed = this.state.error;\n if (failed) {\n // Rethrown during the boundary's own render, so React treats it as\n // uncaught: no `onCaughtError` report, the fatal path runs as it would\n // without the SDK.\n throw failed.value;\n }\n return createElement(\n View,\n {\n style: ROOT_STYLE,\n collapsable: false,\n onTouchStart,\n onTouchMove,\n onTouchEnd,\n onTouchCancel,\n },\n this.props.children,\n );\n }\n}\n",
|
|
19
|
+
"import type { ClientEvent } from \"@sazabi/client-sdk-core\";\nimport { AppState, type AppStateStatus } from \"react-native\";\n\nexport interface LifecycleCaptureOptions {\n emit(event: ClientEvent): void;\n /** The app is leaving the foreground: the mobile unload flush point. */\n onBackground(): void;\n}\n\nexport interface LifecycleCaptureHandle {\n teardown(): void;\n}\n\n/**\n * `AppState` is the mobile lifecycle: `background` and `inactive` are the\n * unload, `active` is the return. Each edge is a `lifecycle` row so the\n * timeline reads causally, and leaving the foreground flushes the queue\n * while the OS still allows network activity.\n */\nexport const installLifecycleCapture = (\n options: LifecycleCaptureOptions,\n): LifecycleCaptureHandle => {\n let last: AppStateStatus | undefined = AppState.currentState;\n\n const onChange = (state: AppStateStatus): void => {\n try {\n if (state === last) return;\n last = state;\n if (\n state !== \"active\" &&\n state !== \"background\" &&\n state !== \"inactive\"\n ) {\n return;\n }\n options.emit({\n type: \"lifecycle\",\n body: `lifecycle ${state}`,\n severity: \"INFO\",\n timeUnixMs: Date.now(),\n attributes: { \"app.lifecycle.state\": state },\n });\n if (state !== \"active\") {\n options.onBackground();\n }\n } catch {\n // Capture failures must never surface to the host.\n }\n };\n\n let subscription: { remove(): void } | undefined;\n try {\n subscription = AppState.addEventListener(\"change\", onChange);\n } catch {\n // No AppState on this host: lifecycle rows are simply never emitted.\n }\n\n return {\n teardown() {\n try {\n subscription?.remove();\n } catch {\n // Best-effort.\n }\n },\n };\n};\n",
|
|
20
|
+
"import type { ClientEvent } from \"@sazabi/client-sdk-core\";\nimport { type CurrentScreen, state } from \"./process-state\";\nimport type { NavigationContainerRefLike } from \"./types\";\n\n// The screen and the emitter live in the process-wide state, so a\n// `trackNavigation()` imported from a second copy of the module feeds the\n// running instance.\n\n/** The route the user is on, stamped on every row as `app.screen.*`. */\nexport const getCurrentScreen = (): CurrentScreen => state.screen;\n\n/**\n * Bound at start, cleared at shutdown; navigation tracked before init still\n * updates the screen. `redactUrl` is the core's redaction with the app's\n * `network.redactQueryParams`: a deep-link route path can carry a query\n * string (`/reset-password?token=…`), so it is recorded like any other URL.\n */\nexport const setNavigationEmitter = (\n emit: ((event: ClientEvent) => void) | undefined,\n redactUrl?: (url: string) => string,\n): void => {\n state.navigationEmit = emit;\n state.navigationRedactUrl = redactUrl;\n};\n\nconst paramKeys = (params: object | undefined): string | undefined => {\n if (!params) return undefined;\n const keys = Object.keys(params);\n return keys.length > 0 ? keys.join(\",\") : undefined;\n};\n\n/**\n * Screen changes come from the navigator the app already owns. Subscribes to\n * a React Navigation container ref's `state` event (expo-router exposes the\n * same ref) and emits a `navigation` row from `getCurrentRoute()`: the route\n * name, the pathname when the router has one, and the param keys, never the\n * values. A state change that lands on the same route (a param update, a\n * nested navigator settling) emits nothing; a new push of the same screen\n * has a new route key and is a new visit. Returns the unsubscribe.\n */\nexport const trackNavigation = (\n ref: NavigationContainerRefLike,\n): (() => void) => {\n const handleState = (): void => {\n try {\n const route = ref.getCurrentRoute();\n if (!route) return;\n const from = state.screen;\n if (\n route.key === from.key &&\n route.name === from.name &&\n route.path === from.path\n ) {\n return;\n }\n state.screen = { name: route.name, path: route.path, key: route.key };\n state.navigationEmit?.({\n type: \"navigation\",\n body: `navigation ${route.name}`,\n severity: \"INFO\",\n timeUnixMs: Date.now(),\n attributes: {\n \"app.navigation.type\": \"screen\",\n \"app.screen.name\": route.name,\n \"url.path\":\n route.path === undefined\n ? undefined\n : (state.navigationRedactUrl?.(route.path) ?? route.path),\n \"app.navigation.from\": from.name,\n \"app.navigation.params\": paramKeys(route.params),\n },\n });\n } catch {\n // Capture failures must never surface to the host.\n }\n };\n\n handleState();\n let unsubscribe: (() => void) | undefined;\n try {\n unsubscribe = ref.addListener(\"state\", handleState);\n } catch {\n // A ref without the event: the initial screen was still recorded.\n }\n return () => {\n try {\n unsubscribe?.();\n } catch {\n // Best-effort.\n }\n };\n};\n\n/** Test and shutdown hook: forget the screen so a later init starts clean. */\nexport const resetNavigationState = (): void => {\n state.screen = {};\n state.navigationEmit = undefined;\n state.navigationRedactUrl = undefined;\n};\n",
|
|
21
|
+
"import { createMemoryStore, type SessionStore } from \"@sazabi/client-sdk-core\";\nimport type { StorageAdapter } from \"./types\";\n\n/**\n * Storage adapters may be synchronous (MMKV) or promise-returning\n * (AsyncStorage). Every call here is best-effort: an adapter that throws or\n * rejects degrades the SDK to in-memory state and never surfaces to the app.\n */\nexport const readKey = async (\n adapter: StorageAdapter,\n key: string,\n): Promise<string | null> => {\n try {\n const value = await adapter.getItem(key);\n return typeof value === \"string\" ? value : null;\n } catch {\n return null;\n }\n};\n\nexport const isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as { then?: unknown } | null | undefined)?.then === \"function\";\n\n/** What a read found; `undefined` means the adapter failed to answer. */\nexport type ReadResult = { value: string | null } | undefined;\n\n/**\n * Call the adapter and shape its answer, synchronously when it answers\n * synchronously (MMKV). A throw or rejection is `undefined`, never an\n * answer: the outbox must not delete a report it could not read.\n */\nconst callNow = <T>(\n call: () => unknown,\n shape: (value: unknown) => T,\n): T | undefined | Promise<T | undefined> => {\n try {\n const value = call();\n return isThenable(value)\n ? Promise.resolve(value).then(shape, () => undefined)\n : shape(value);\n } catch {\n return undefined;\n }\n};\n\n/** Read a key, synchronously when the adapter can, so the crash outbox loads inside `init()`. */\nexport const readKeyNow = (\n adapter: StorageAdapter,\n key: string,\n): ReadResult | Promise<ReadResult> =>\n callNow(\n () => adapter.getItem(key),\n (value): ReadResult => ({\n value: typeof value === \"string\" ? value : null,\n }),\n );\n\n/**\n * Every key the adapter holds. `undefined` when the adapter has no listing\n * or it failed: the caller then knows only what the indexes list, never\n * that a key is absent.\n */\nexport const listKeysNow = (\n adapter: StorageAdapter,\n): readonly string[] | undefined | Promise<readonly string[] | undefined> =>\n callNow(\n () => adapter.getAllKeys?.(),\n (value): readonly string[] | undefined =>\n Array.isArray(value)\n ? value.filter((key): key is string => typeof key === \"string\")\n : undefined,\n );\n\n/**\n * `true` once the adapter accepted the write, `false` when it refused. A\n * synchronous adapter answers synchronously, which is how the crash path\n * knows the batch is on disk before the process can die.\n */\nexport const writeKey = (\n adapter: StorageAdapter,\n key: string,\n value: string,\n): boolean | Promise<boolean> => {\n try {\n const result = adapter.setItem(key, value);\n return isThenable(result)\n ? Promise.resolve(result).then(\n () => true,\n () => false,\n )\n : true;\n } catch {\n // Persistence is best-effort; the caller decides what a refusal costs.\n return false;\n }\n};\n\n/** `true` once the adapter accepted the removal, synchronously when it can. */\nexport const removeKey = (\n adapter: StorageAdapter,\n key: string,\n): boolean | Promise<boolean> => {\n try {\n const result = adapter.removeItem(key);\n return isThenable(result)\n ? Promise.resolve(result).then(\n () => true,\n () => false,\n )\n : true;\n } catch {\n // Persistence is best-effort.\n return false;\n }\n};\n\nexport interface HydratingStore {\n /** Synchronous view the session machine reads through. */\n store: SessionStore;\n /** Resolves once every key has been read from the adapter. */\n ready: Promise<void>;\n}\n\n/**\n * The session machine needs synchronous reads, so persisted state is read\n * once into an in-memory mirror and every later write goes through to the\n * adapter. A key written locally before hydration finishes keeps the local\n * value: a `reset()` must not be undone by a slow read of the old identity.\n */\nexport const createHydratingStore = (\n adapter: StorageAdapter,\n keys: readonly string[],\n): HydratingStore => {\n const mirror = createMemoryStore();\n const writtenLocally = new Set<string>();\n\n const ready = (async () => {\n for (const key of keys) {\n const value = await readKey(adapter, key);\n if (value !== null && !writtenLocally.has(key)) {\n mirror.set(key, value);\n }\n }\n })();\n\n return {\n store: {\n get: (key) => mirror.get(key),\n set(key, value) {\n mirror.set(key, value);\n writtenLocally.add(key);\n void writeKey(adapter, key, value);\n },\n remove(key) {\n mirror.remove(key);\n writtenLocally.add(key);\n void removeKey(adapter, key);\n },\n },\n ready,\n };\n};\n",
|
|
22
|
+
"import type { SdkHealthReporter, Transport } from \"@sazabi/client-sdk-core\";\nimport {\n isThenable,\n listKeysNow,\n type ReadResult,\n readKeyNow,\n removeKey,\n writeKey,\n} from \"./storage\";\nimport type { StorageAdapter } from \"./types\";\n\n/**\n * The outbox stores each batch under its own key, `sazabi.outbox.<id>` (a\n * JSON list of envelopes), and lists the ids under an index key. A crash\n * writes only its own batch and then the index, a few hundred bytes, instead\n * of rewriting every stored report: React Native kills the process within\n * milliseconds of the handler returning, and a promise-returning store\n * (AsyncStorage) never finished a rewrite of the whole list in that window.\n *\n * The two writes are issued back to back but land separately (AsyncStorage's\n * `multiSet` is one SQLite transaction on Android but one file per large\n * value plus a manifest on iOS, so no single call is atomic on both), and a\n * process seen dying between them on an Android device left a batch key no\n * index listed. The crash path stays two writes, as a dying process has no\n * time for more; the next launch reconciles instead: with an adapter that\n * lists its keys (`getAllKeys`), every `sazabi.outbox.<id>` key neither index\n * names is read as a stored batch and listed, so it is delivered and its key\n * removed like any other. The listing is read once per launch, issued just\n * before the index reads, before any capture installs.\n *\n * The ordering contract that makes an unlisted key mean exactly that: a\n * batch key is written before the index lists it, and removed before the\n * index stops listing it. So a listed id whose key reads as missing is a\n * delivery (or a drop) whose cleanup did not finish and is dropped, while a\n * key no index names can only be a crash whose index write never landed. A\n * key the store refuses to remove is overwritten with an empty list, which\n * reads as holding no report and goes on the next launch.\n */\nconst BATCH_KEY_PREFIX = \"sazabi.outbox.\";\nexport const OUTBOX_INDEX_KEY = \"sazabi.outbox.index\";\n/**\n * Batches stashed by this launch before its read of the index came back.\n * They are listed under their own key so a crash right after `init()` never\n * overwrites a previous launch's index that has not been loaded yet; the\n * read merges them into the index and clears this key. A synchronous adapter\n * is read inside `init()`, so only a promise-returning one ever uses it.\n */\nconst UNMERGED_INDEX_KEY = \"sazabi.outbox.index.unmerged\";\n/**\n * The format before per-batch keys: the whole outbox as one JSON list of\n * envelopes, plus the same list for batches stashed before the read. Read\n * and moved into batch keys, then removed; never written.\n */\nconst LEGACY_OUTBOX_KEY = \"sazabi.outbox\";\nconst LEGACY_UNMERGED_KEY = \"sazabi.outbox.unmerged\";\n/**\n * The live queue as it stood when the app last left the foreground, as\n * `{ id, envelopes }`, so events not yet delivered survive the OS killing a\n * backgrounded app. Removed once the queue has been delivered; the next\n * launch moves what it finds into a batch keyed by its replay id.\n */\nconst QUEUE_KEY = \"sazabi.outbox.queue\";\n/** Stamped on every record of a stashed batch; a re-sent copy carries the same value. */\nexport const OUTBOX_REPLAY_ID_KEY = \"app.outbox.id\";\nconst MAX_OUTBOX_ENVELOPES = 20;\n/**\n * Hard cap on the stored outbox, in UTF-16 units (what a string store\n * counts): Android's AsyncStorage reads a row through a 2 MB cursor window,\n * and a crash report must never be the row that no longer loads.\n */\nconst MAX_OUTBOX_LENGTH = 524_288;\n\nconst batchKey = (id: string): string => `${BATCH_KEY_PREFIX}${id}`;\n\n/**\n * What a batch id looks like: the 16 hex digits of a replay id, or the two\n * fixed ids of the legacy move. Only a key of that shape is read back as an\n * unlisted batch, so the index, the unmerged index, the saved queue, and any\n * key a later version adds under the prefix are never read as a corrupt\n * batch and removed.\n */\nconst BATCH_ID = /^(?:[0-9a-f]{16}|legacy|legacy-unmerged)$/;\n\n/** The ids of the batch keys among `keys` that `listed` does not name. */\nconst unlistedBatchIds = (\n keys: readonly string[] | undefined,\n listed: ReadonlySet<string>,\n): string[] =>\n (keys ?? [])\n .filter((key) => key.startsWith(BATCH_KEY_PREFIX))\n .map((key) => key.slice(BATCH_KEY_PREFIX.length))\n .filter((id) => BATCH_ID.test(id) && !listed.has(id));\n\n/** One stored batch; `envelopes` is unset when its key could not be read. */\ninterface Batch {\n id: string;\n envelopes?: string[];\n}\n\n/**\n * Everything the keys should hold right now: what earlier launches left plus\n * what this launch stashed, minus what the intake has accepted. Every\n * delivery, old or new, reconciles against this one state and persists all\n * of it, so a launch's upload that finishes after a fresh crash can neither\n * erase the new batch nor resurrect its own accepted envelopes. The state is\n * the adapter's, not the SDK instance's: a delivery that outlives a\n * `shutdown()` keeps reconciling against the same state, and a later\n * `init()` on the same adapter starts from it rather than from an empty one.\n */\ninterface OutboxState {\n batches: Batch[];\n /** Stashed before `loaded`; listed under `UNMERGED_INDEX_KEY` until then. */\n unmerged: Batch[];\n /**\n * Whether the index has been read and merged. A read that failed leaves\n * this false for the rest of the process, so the index is never\n * overwritten with a list that is missing what it held.\n */\n loaded: boolean;\n /**\n * Whether `UNMERGED_INDEX_KEY` has been read: unset while the first read\n * is pending, `false` once a read failed and none has succeeded. A failed\n * read leaves a crash this launch knows nothing about possibly listed in\n * the key, so it is never written or removed then (the next launch reads\n * it again); a pending one still takes this launch's stash, as the dying\n * process has nowhere else to put it.\n */\n unmergedRead?: boolean;\n /** Whether the previous launch's saved queue has been read and moved. */\n queueRead?: boolean;\n /**\n * The transport whose queue `QUEUE_KEY` holds. Only that transport's\n * delivery clears the key: an upload of an instance already shut down\n * must not remove the copy a later `init()` on the same adapter saved.\n */\n queueSaved?: Transport;\n /** The read of the stored keys, started once per adapter per process. */\n loading?: Promise<void>;\n /** Envelopes with an upload outstanding: a second delivery never posts them again. */\n inFlight: Set<string>;\n warn?: (message: string) => void;\n /**\n * The running instance's health reporter (`watchOutbox`): told once how\n * many stored reports a load or a stash discarded (`outbox_discarded`,\n * with what a saved queue left out as `outbox_unsaved`) and when the\n * store refused a write (`outbox_persist_failed`). Unset after\n * `shutdown()`, so a delivery that outlives the instance is quiet.\n */\n report?: SdkHealthReporter;\n /** Stored envelopes known lost since the last report: by the caps, or as corrupt. */\n discarded: number;\n /**\n * Envelopes a replayed saved queue had left out at the caps. Their live\n * upload may or may not have finished before the kill, so they are\n * possible losses, never counted as discarded.\n */\n unsaved: number;\n}\n\nconst stateByAdapter = new WeakMap<StorageAdapter, OutboxState>();\n\nconst stateFor = (adapter: StorageAdapter): OutboxState => {\n const state = stateByAdapter.get(adapter);\n if (state) return state;\n const fresh: OutboxState = {\n batches: [],\n unmerged: [],\n loaded: false,\n inFlight: new Set(),\n discarded: 0,\n unsaved: 0,\n };\n stateByAdapter.set(adapter, fresh);\n return fresh;\n};\n\n/**\n * Route the outbox's health reports to the running instance. Set before\n * `loadOutbox`, so what the first read discards is reported on this launch.\n */\nexport const watchOutbox = (\n adapter: StorageAdapter,\n report: SdkHealthReporter,\n): void => {\n stateFor(adapter).report = report;\n};\n\n/** The instance is gone: nothing it would have reported is reported. */\nexport const unwatchOutbox = (\n adapter: StorageAdapter,\n report: SdkHealthReporter,\n): void => {\n const state = stateFor(adapter);\n if (state.report === report) state.report = undefined;\n};\n\n/**\n * Tell the instance how many stored reports were discarded since it last\n * heard, and how many a saved queue had left out (possible losses, apart).\n */\nconst reportDiscarded = (state: OutboxState): void => {\n if (state.discarded === 0 && state.unsaved === 0) return;\n const { discarded, unsaved } = state;\n state.discarded = 0;\n state.unsaved = 0;\n state.report?.({\n diagnostic: \"outbox_discarded\",\n attributes: {\n \"app.sdk.outbox_discarded\": discarded,\n \"app.sdk.outbox_unsaved\": unsaved,\n },\n });\n};\n\n/**\n * A write the store refused: the instance that issued it hears of the first\n * one. The reporter is taken when the write starts, so a write still pending\n * through `shutdown()` and a later `init()` on the same adapter reports to\n * no one, never to the new instance (whose one report would be spent on a\n * session it did not run).\n */\nconst reportRefused = (\n state: OutboxState,\n accepted: boolean | Promise<boolean>,\n): void => {\n const report = state.report;\n if (!report) return;\n void after(accepted, (ok) => {\n if (!ok && state.report === report) {\n report({ diagnostic: \"outbox_persist_failed\" });\n }\n });\n};\n\n/** Run `next` on a value now, or once it resolves. */\nconst after = <T, U>(\n value: T | Promise<T>,\n next: (resolved: T) => U | Promise<U>,\n): U | Promise<U> =>\n isThenable(value)\n ? Promise.resolve(value as Promise<T>).then(next)\n : next(value as T);\n\n/** Every value, synchronously when every one is synchronous. */\nconst all = <T>(values: Array<T | Promise<T>>): T[] | Promise<T[]> =>\n values.some(isThenable) ? Promise.all(values) : (values as T[]);\n\n/** Read keys, synchronously when every answer is synchronous. */\nconst readKeys = (\n adapter: StorageAdapter,\n keys: string[],\n): ReadResult[] | Promise<ReadResult[]> =>\n all(keys.map((key) => readKeyNow(adapter, key)));\n\nconst writeJson = (\n adapter: StorageAdapter,\n key: string,\n list: unknown[],\n): boolean | Promise<boolean> =>\n list.length === 0\n ? removeKey(adapter, key)\n : writeKey(adapter, key, JSON.stringify(list));\n\nconst ids = (batches: Batch[]): string[] => batches.map((batch) => batch.id);\n\n/**\n * Each batch id once, first place kept: a launch that wrote the index and\n * died before removing the unmerged index leaves an id in both, and trimming\n * a list that counts it twice would drop reports that fit the caps.\n */\nconst uniqueBatches = (batches: Batch[]): Batch[] => {\n const seen = new Set<string>();\n return batches.filter((batch) => {\n if (seen.has(batch.id)) return false;\n seen.add(batch.id);\n return true;\n });\n};\n\n/**\n * Before the index is loaded only the unmerged index is written, so the\n * previous launch's index stays where it is; after, the merged list is the\n * index and the unmerged key goes, but only once the index write was\n * accepted and only if the key was read. An unmerged key whose read failed\n * is left alone, written or removed: its contents are unknown. Answers\n * `true` synchronously when a synchronous adapter took the write.\n */\nconst persist = (adapter: StorageAdapter): boolean | Promise<boolean> => {\n const state = stateFor(adapter);\n if (!state.loaded) {\n return (\n state.unmergedRead !== false &&\n writeJson(adapter, UNMERGED_INDEX_KEY, ids(state.unmerged))\n );\n }\n return after(\n writeJson(adapter, OUTBOX_INDEX_KEY, ids(state.batches)),\n (accepted) =>\n accepted && state.unmergedRead\n ? after(removeKey(adapter, UNMERGED_INDEX_KEY), () => accepted)\n : accepted,\n );\n};\n\n/** The newest envelopes within both caps, at least one. */\nconst capEnvelopes = (envelopes: string[]): string[] => {\n let length = 0;\n let keep = 0;\n for (let index = envelopes.length - 1; index >= 0; index -= 1) {\n length += (envelopes[index] as string).length;\n if (\n keep === MAX_OUTBOX_ENVELOPES ||\n (length > MAX_OUTBOX_LENGTH && keep > 0)\n )\n break;\n keep += 1;\n }\n return envelopes.slice(envelopes.length - keep);\n};\n\n/** Drop the oldest batches past either cap, keeping the newest; returns them, counted as discarded. */\nconst trim = (state: OutboxState, batches: Batch[]): Batch[] => {\n let count = 0;\n let length = 0;\n for (let index = batches.length - 1; index >= 0; index -= 1) {\n const envelopes = batches[index]?.envelopes ?? [];\n count += envelopes.length;\n for (const body of envelopes) length += body.length;\n if (\n index < batches.length - 1 &&\n (count > MAX_OUTBOX_ENVELOPES || length > MAX_OUTBOX_LENGTH)\n ) {\n const dropped = batches.splice(0, index + 1);\n for (const batch of dropped) {\n state.discarded += batch.envelopes?.length ?? 0;\n }\n return dropped;\n }\n }\n return [];\n};\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n};\n\n/** The strings in a parsed value; anything but a list (a corrupt value, another version's shape) is empty. */\nconst stringsOf = (parsed: unknown): string[] =>\n Array.isArray(parsed)\n ? parsed.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n\n/** A stored list of ids. */\nconst parseList = (raw: string | null | undefined): string[] =>\n stringsOf(raw ? parseJson(raw) : undefined);\n\n/**\n * Stored envelopes, validated on read: a value that is not a JSON list, and\n * every entry that is not a JSON object, is discarded rather than posted,\n * with one warning. `discarded` (how many entries, or one for a value that\n * is no list at all) tells the caller the key holds something it should\n * not, so the key is rewritten or removed and the warning does not come\n * back on the next launch; the count is reported as `outbox_discarded`.\n */\nconst parseEnvelopes = (\n state: OutboxState,\n raw: string | null | undefined,\n): { envelopes: string[]; discarded: number } => {\n const parsed = raw ? parseJson(raw) : undefined;\n const envelopes = stringsOf(parsed).filter((body) => {\n const envelope = parseJson(body);\n return typeof envelope === \"object\" && envelope !== null;\n });\n const discarded = !raw\n ? 0\n : Array.isArray(parsed)\n ? parsed.length - envelopes.length\n : 1;\n state.discarded += discarded;\n if (discarded) {\n // Once per process: the same value is read again by every init().\n state.warn?.(\"discarded a stored crash report that is not valid JSON.\");\n state.warn = undefined;\n }\n return { envelopes, discarded };\n};\n\nconst merge = (\n adapter: StorageAdapter,\n state: OutboxState,\n [index, unmergedIndex, legacy, legacyUnmerged, queue]: ReadResult[],\n batchReads: Map<string, ReadResult>,\n unlisted: string[],\n): void => {\n if (unmergedIndex) state.unmergedRead = true;\n else if (state.unmergedRead === undefined) state.unmergedRead = false;\n // An id whose key reads as missing was delivered (its key removed before\n // the index was rewritten) or never written: gone. One whose key holds no\n // valid report is gone too (its key is removed once the index no longer\n // lists it), and one holding some is rewritten with just those, the batch\n // before the index as always: otherwise the corrupt value would be read,\n // warned about, and kept on every launch.\n const corrupt = new Set<string>();\n const readBatches = new Map<string, Batch[]>();\n const batchesOf = (listed: string[]): Batch[] =>\n listed.flatMap((id) => {\n // An id listed in both indexes is read, judged, and rewritten once.\n const seen = readBatches.get(id);\n if (seen) return seen;\n const read = batchReads.get(id);\n let batches: Batch[];\n if (!read) batches = [{ id }];\n else if (read.value === null) batches = [];\n else {\n const { envelopes, discarded } = parseEnvelopes(state, read.value);\n if (envelopes.length === 0) {\n corrupt.add(id);\n batches = [];\n } else {\n if (discarded) void writeJson(adapter, batchKey(id), envelopes);\n batches = [{ id, envelopes }];\n }\n }\n readBatches.set(id, batches);\n return batches;\n });\n const stored = batchesOf(parseList(index?.value));\n const held = batchesOf(parseList(unmergedIndex?.value));\n // A batch key no index names: a crash whose index write never landed.\n // Read and judged like a listed one, then listed below, so it is\n // delivered and its key removed like any other. One this launch stashed\n // itself is already in the state and not read back. Several come in the\n // store's own order, which says nothing about their age, so a trim at\n // the caps may prefer an older one; a replay id carries no clock.\n const own = new Set(ids([...state.batches, ...state.unmerged]));\n const found = batchesOf(unlisted.filter((id) => !own.has(id)));\n if (!index && state.unmergedRead === false) {\n // Nowhere safe to list anything: leave every key for the next launch,\n // which reads and counts them again; nothing was discarded here.\n state.discarded = 0;\n state.unsaved = 0;\n return;\n }\n // The old single-list format and the previous launch's saved queue move\n // into batch keys, each written before the index lists it; an old key\n // goes only once both its batch write and the index write were accepted\n // (a store near its quota may refuse the batch and take the small index),\n // and otherwise stays for the next launch to move again. A batch id\n // already listed (a move whose cleanup did not finish) is not added twice.\n const known = new Set(\n ids([...stored, ...held, ...found, ...state.batches, ...state.unmerged]),\n );\n const moved: Batch[] = [];\n const move = (\n id: string | undefined,\n envelopes: string[],\n ): boolean | Promise<boolean> => {\n if (!id || envelopes.length === 0 || known.has(id)) return true;\n known.add(id);\n // The key this move writes replaces a corrupt one of the same id (a\n // legacy move whose cleanup did not finish): it must not be removed.\n corrupt.delete(id);\n moved.push({ id, envelopes });\n return writeJson(adapter, batchKey(id), envelopes);\n };\n const sources: Array<[string, boolean | Promise<boolean>]> = [];\n for (const [result, id, key] of [\n [legacy, \"legacy\", LEGACY_OUTBOX_KEY],\n [legacyUnmerged, \"legacy-unmerged\", LEGACY_UNMERGED_KEY],\n ] as const) {\n if (!result?.value) continue;\n sources.push([\n key,\n move(id, parseEnvelopes(state, result.value).envelopes),\n ]);\n }\n const saved = queue?.value\n ? (parseJson(queue.value) as {\n id?: unknown;\n envelopes?: unknown;\n discarded?: unknown;\n } | null)\n : undefined;\n // Rows the save left out at the caps: the copy is being replayed, so the\n // app was killed before the live queue settled, but the transport posts\n // the oldest first and these may have been delivered before the kill.\n // Possible losses, reported apart from what is known gone.\n if (typeof saved?.discarded === \"number\" && saved.discarded > 0) {\n state.unsaved += Math.floor(saved.discarded);\n }\n const queueMoved = move(\n typeof saved?.id === \"string\" ? saved.id : undefined,\n parseEnvelopes(state, JSON.stringify(saved?.envelopes ?? [])).envelopes,\n );\n let dropped: Batch[];\n if (index) {\n // A crash during the read may already have stashed a batch; everything\n // stored joins it rather than replacing it.\n state.batches = uniqueBatches([\n ...stored,\n ...held,\n ...found,\n ...moved,\n ...state.unmerged,\n ]);\n state.unmerged = [];\n state.loaded = true;\n dropped = trim(state, state.batches);\n } else {\n // `found` is empty here: nothing is unlisted while the index is unread.\n state.unmerged = uniqueBatches([...held, ...moved, ...state.unmerged]);\n dropped = trim(state, state.unmerged);\n }\n // What this read threw away, on the launch that found it: the crash that\n // overfilled the store could not say so itself.\n reportDiscarded(state);\n void after(persist(adapter), (accepted) => {\n for (const id of ids(dropped)) void removeKey(adapter, batchKey(id));\n if (!accepted) return;\n for (const id of corrupt) void removeKey(adapter, batchKey(id));\n return after(\n all([queueMoved, ...sources.map(([, written]) => written)]),\n ([queueWritten, ...written]) => {\n for (const [at, [key]] of sources.entries()) {\n if (written[at]) void removeKey(adapter, key);\n }\n // A saved queue that could not be moved keeps its key, and saves\n // stay off for the launch so they never overwrite it.\n if (!queue || !queueWritten) return;\n // Saves of this launch's queue start only once the previous\n // launch's copy is gone, so its removal can never delete the new one.\n return after(queue.value ? removeKey(adapter, QUEUE_KEY) : true, () => {\n state.queueRead = true;\n });\n },\n );\n });\n};\n\n/**\n * Read the stored outbox once per adapter. Started by `init()` before any\n * capture installs: a synchronous adapter is loaded before this returns, so\n * a crash at any later point merges into the stored report instead of\n * writing beside it.\n */\nexport const loadOutbox = (\n adapter: StorageAdapter,\n warn?: (message: string) => void,\n): Promise<void> => {\n const state = stateFor(adapter);\n if (state.loading) return state.loading;\n state.warn ??= warn;\n // The listing is issued before the index reads, so on a store that runs\n // calls in order (AsyncStorage) it is taken no later than the indexes it\n // is compared against: a batch whose index write lands between the two\n // is listed, and a key the listing misses is at worst found next launch.\n const keysRead = listKeysNow(adapter);\n const loaded = after(\n readKeys(adapter, [\n OUTBOX_INDEX_KEY,\n UNMERGED_INDEX_KEY,\n LEGACY_OUTBOX_KEY,\n LEGACY_UNMERGED_KEY,\n QUEUE_KEY,\n ]),\n (indexes) =>\n after(keysRead, (keys) => {\n const listed = [\n ...new Set([\n ...parseList(indexes[0]?.value),\n ...parseList(indexes[1]?.value),\n ]),\n ];\n // Only with both indexes read: an index that failed to read may\n // list any key, so nothing is unlisted until a launch reads it.\n const unlisted =\n indexes[0] && indexes[1]\n ? unlistedBatchIds(keys, new Set(listed))\n : [];\n const toRead = [...listed, ...unlisted];\n return after(readKeys(adapter, toRead.map(batchKey)), (reads) => {\n merge(\n adapter,\n state,\n indexes,\n new Map(toRead.map((id, index) => [id, reads[index]])),\n unlisted,\n );\n });\n }),\n );\n if (!isThenable(loaded)) {\n // A failed read is tried again by the next init() in this process.\n if (state.loaded) state.loading = Promise.resolve();\n return Promise.resolve();\n }\n const loading = loaded.then(() => {\n if (!state.loaded) state.loading = undefined;\n });\n state.loading = loading;\n return loading;\n};\n\n/**\n * Post envelopes and drop from the state only the settled ones, accepted or\n * refused for good (a 4xx other than 408 and 429, which the transport warns\n * about once): an offline launch, a 429, or a 5xx keeps the rest for the\n * next launch instead of deleting the only copy of a crash, and a report the\n * intake will never take is not posted on every launch forever.\n */\nconst deliver = async (\n adapter: StorageAdapter,\n transport: Transport,\n envelopes: string[],\n): Promise<void> => {\n const state = stateFor(adapter);\n const fresh = envelopes.filter((body) => !state.inFlight.has(body));\n if (fresh.length === 0) return;\n for (const body of fresh) state.inFlight.add(body);\n let settled: boolean[];\n try {\n settled = await Promise.all(\n fresh.map((body) => transport.sendEnvelope(body)),\n );\n } finally {\n for (const body of fresh) state.inFlight.delete(body);\n }\n const delivered = new Set(fresh.filter((_, index) => settled[index]));\n if (delivered.size === 0) return;\n const emptied: Array<[Batch[], Batch]> = [];\n for (const list of [state.batches, state.unmerged]) {\n for (const batch of list) {\n const left = batch.envelopes?.filter((body) => !delivered.has(body));\n if (!left || left.length === batch.envelopes?.length) continue;\n batch.envelopes = left;\n if (left.length > 0) void writeJson(adapter, batchKey(batch.id), left);\n else emptied.push([list, batch]);\n }\n }\n // A settled batch's key goes before the index stops naming it (the\n // ordering contract in the file comment): a process dying in between\n // leaves a listed id whose key is missing, which the next launch drops,\n // never an unlisted key it would read as a lost crash and post again. A\n // key the store refuses to remove is left holding no report instead, and\n // one it refuses to touch at all stays listed with its contents, so the\n // next launch posts it again under the same replay id rather than losing\n // track of a key it cannot clear.\n const cleared = await all(\n emptied.map(([, batch]) =>\n after(\n removeKey(adapter, batchKey(batch.id)),\n (removed) => removed || writeKey(adapter, batchKey(batch.id), \"[]\"),\n ),\n ),\n );\n emptied.forEach(([list, batch], at) => {\n const index = list.indexOf(batch);\n if (cleared[at] && index >= 0) list.splice(index, 1);\n });\n await persist(adapter);\n};\n\n/**\n * The crash outbox replaces the browser's keepalive flush. A fatal JavaScript\n * error terminates the process shortly after the global handler returns, so\n * the pending queue (the error row included) is encoded into standalone\n * envelopes stamped with one replay id and written to storage: the batch's\n * own key first, then the index that lists it.\n *\n * Nothing the dying process sends can be cleared afterwards: an upload the\n * intake accepted just before the process died would be posted again by the\n * next launch. So when a synchronous adapter took both writes (`true`),\n * nothing is sent here; the caller delivers the outbox once the process has\n * proved it survived, and otherwise the next `init()` does. A\n * promise-returning adapter may never finish the writes, so its batch is\n * also sent at once, and a copy the next launch re-sends carries the same\n * replay id. The saved queue goes once both writes were accepted (every row\n * it held is in this batch), never before: a store that refuses the batch\n * near its limit must not cost the only copy of those rows.\n */\nexport const stashOutbox = (\n adapter: StorageAdapter,\n transport: Transport,\n replayId: string,\n): boolean => {\n const drained = transport.drainEnvelopes({\n [OUTBOX_REPLAY_ID_KEY]: replayId,\n });\n const envelopes = capEnvelopes(drained);\n if (envelopes.length === 0) return true;\n const state = stateFor(adapter);\n state.discarded += drained.length - envelopes.length;\n const list = state.loaded ? state.batches : state.unmerged;\n list.push({ id: replayId, envelopes });\n const dropped = trim(state, list);\n const written = writeJson(adapter, batchKey(replayId), envelopes);\n const indexed = persist(adapter);\n const durable = indexed === true && written === true;\n for (const id of ids(dropped)) void removeKey(adapter, batchKey(id));\n // Heard only if the process survives the crash (a development build, a\n // handler that does not terminate); a dying process has no time to say so.\n reportDiscarded(state);\n reportRefused(state, written);\n // `persist` skips the index write (answering `false`) while the unmerged\n // index is unreadable; only a write the store was given can be refused.\n if (state.loaded || state.unmergedRead !== false) {\n reportRefused(state, indexed);\n }\n if (state.queueSaved === transport) {\n // No delivery clears the saved queue from here on: it goes only once\n // both writes are on disk, and a refused write leaves it for the next\n // launch to replay. A newer save in the meantime is never removed.\n state.queueSaved = undefined;\n void after(all([written, indexed]), (accepted) => {\n if (accepted.every(Boolean) && state.queueSaved === undefined) {\n void removeKey(adapter, QUEUE_KEY);\n }\n });\n }\n if (!durable) void deliver(adapter, transport, envelopes);\n return durable;\n};\n\n/** Post everything the outbox holds once the stored keys have been read. */\nexport const deliverOutbox = async (\n adapter: StorageAdapter,\n transport: Transport,\n): Promise<void> => {\n await loadOutbox(adapter);\n const state = stateFor(adapter);\n await deliver(adapter, transport, [\n ...new Set(\n [...state.batches, ...state.unmerged].flatMap(\n (batch) => batch.envelopes ?? [],\n ),\n ),\n ]);\n};\n\n/**\n * Save the live queue, within the outbox caps, as the app leaves the\n * foreground: the OS may kill a backgrounded app without another callback,\n * and an offline queue would die with it. The queued records are stamped\n * with the replay id, so the live upload and the copy a later launch sends\n * share it. Not before the previous launch's saved queue has been read.\n */\nexport const saveQueue = (\n adapter: StorageAdapter,\n transport: Transport,\n replayId: string,\n): void => {\n const state = stateFor(adapter);\n if (!state.queueRead) return;\n const snapshot = transport.snapshotEnvelopes({\n [OUTBOX_REPLAY_ID_KEY]: replayId,\n });\n const envelopes = capEnvelopes(snapshot);\n state.queueSaved = envelopes.length > 0 ? transport : undefined;\n if (state.queueSaved) {\n // What the caps left out is still in the live queue; the copy records\n // the count, and the launch that replays the copy reports it as\n // `outbox_unsaved` (the live upload may have delivered it).\n const discarded = snapshot.length - envelopes.length;\n reportRefused(\n state,\n writeKey(\n adapter,\n QUEUE_KEY,\n JSON.stringify(\n discarded > 0\n ? { id: replayId, envelopes, discarded }\n : { id: replayId, envelopes },\n ),\n ),\n );\n } else {\n void removeKey(adapter, QUEUE_KEY);\n }\n};\n\n/**\n * `transport`'s live queue was delivered (or stashed by a crash): its saved\n * copy goes, unless the key now holds another transport's queue.\n */\nexport const clearQueue = (\n adapter: StorageAdapter,\n transport: Transport,\n): void => {\n const state = stateFor(adapter);\n if (state.queueSaved !== transport) return;\n state.queueSaved = undefined;\n void removeKey(adapter, QUEUE_KEY);\n};\n",
|
|
23
|
+
"import {\n createBeforeSendGate,\n type ClientEvent,\n createPointerTracker,\n createRandomBytes,\n createSdkHealthReporter,\n createSessionManager,\n createTraceContextManager,\n createTransport,\n createMemoryStore,\n encodeLogRecord,\n type EventAttributes,\n type EventSeverity,\n type FetchLike,\n flattenAttributes,\n installConsoleCapture,\n isUnderBase,\n randomHex,\n redactUrl,\n resolveIntakeEndpoint,\n type SessionManager,\n type SessionStore,\n} from \"@sazabi/client-sdk-core\";\nimport { installErrorCapture } from \"./errors\";\nimport {\n devServerOrigin,\n hostResourceAttributes,\n isDevHost,\n isWebHost,\n resolveUrl,\n} from \"./host\";\nimport { SazabiRoot, setActiveTracker } from \"./interactions\";\nimport { installLifecycleCapture } from \"./lifecycle\";\nimport {\n getCurrentScreen,\n resetNavigationState,\n setNavigationEmitter,\n trackNavigation,\n} from \"./navigation\";\nimport { installNetworkCapture, uninstallNetworkPatches } from \"./network\";\nimport {\n clearQueue,\n deliverOutbox,\n loadOutbox,\n saveQueue,\n stashOutbox,\n unwatchOutbox,\n watchOutbox,\n} from \"./outbox\";\nimport { type CurrentScreen, state } from \"./process-state\";\nimport { createHydratingStore } from \"./storage\";\nimport type { NetworkCaptureConfig, ReactNativeSdkConfig } from \"./types\";\nimport { SDK_CONSOLE_PREFIX, SDK_NAME, SDK_VERSION } from \"./version\";\n\nexport type {\n AttributeValue,\n BeforeSend,\n ClientEvent,\n ClientEventType,\n ConsoleCaptureConfig,\n ConsoleCaptureLevel,\n ErrorCaptureConfig,\n RejectionTracker,\n EventAttributes,\n EventSeverity,\n NavigationContainerRefLike,\n NetworkCaptureConfig,\n PointerCaptureConfig,\n ReactNativeSdkConfig,\n StorageAdapter,\n} from \"./types\";\nexport { SazabiRoot, trackNavigation };\n\n// The props `<SazabiRoot>` reads off the touched element's fiber, typed on\n// the components that take them: every `View`, `Pressable` (its props extend\n// `ViewProps`), `Text`, and the `Touchable*` family. Shipped in the published\n// declarations, so `<Pressable sazabiName=\"Pay now\">` type-checks with no\n// setup.\ndeclare module \"react-native\" {\n interface ViewProps {\n /** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */\n sazabiName?: string;\n /** Keeps the name of this element and everything inside it out of touch rows. */\n sazabiMask?: boolean;\n }\n interface TextProps {\n /** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */\n sazabiName?: string;\n /** Keeps the name of this element and everything inside it out of touch rows. */\n sazabiMask?: boolean;\n }\n interface TouchableWithoutFeedbackProps {\n /** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */\n sazabiName?: string;\n /** Keeps the name of this element and everything inside it out of touch rows. */\n sazabiMask?: boolean;\n }\n}\nexport { SDK_VERSION } from \"./version\";\n\nconst DEFAULT_FLUSH_INTERVAL_MS = 5_000;\nconst DEFAULT_MAX_BATCH_BYTES = 57_344;\nconst DEFAULT_MAX_QUEUED_EVENTS = 500;\n/** Events captured before persisted session state is read are held here. */\nconst MAX_PRE_SESSION_EVENTS = 200;\nconst DEFAULT_REQUEST_ID_HEADERS = [\n \"x-request-id\",\n \"cf-ray\",\n \"x-vercel-id\",\n \"x-amz-cf-id\",\n \"fly-request-id\",\n];\n/** Keys the session machine persists through the storage adapter. */\nconst PERSISTED_SESSION_KEYS = [\"sazabi.session\", \"sazabi.distinctId\"];\n/**\n * How long a process must outlive a fatal error before it uploads the crash\n * outbox itself. React Native kills the process shortly after the handler\n * returns; anything the dying process sent could not be cleared, and the next\n * launch would send it again. A process still running after this long (a\n * development build, a handler that does not terminate) delivers the outbox\n * itself; otherwise the next launch does.\n */\nconst FATAL_SURVIVAL_MS = 2_000;\n/**\n * `telemetry.sdk.language`. The OpenTelemetry well-known values name a\n * runtime as well as a language (`webjs` is JavaScript in a browser, `nodejs`\n * is Node.js), and neither describes Hermes or JavaScriptCore inside a React\n * Native app; the semantic conventions allow a custom value when no listed\n * one applies.\n */\nconst SDK_LANGUAGE = \"javascript\";\n\n/**\n * What happened between `init()` and the session existing, in order: events\n * wait with the identity changes around them, so a sign-out and sign-in\n * during a slow storage read still put each row under the right user and\n * session when the queue replays.\n */\ntype PreSessionOp =\n | { kind: \"event\"; event: ClientEvent; screen: CurrentScreen }\n | { kind: \"identify\"; distinctId: string }\n | { kind: \"reset\" };\n\nconst identifyEvent = (\n distinctId: string,\n traits: Record<string, unknown> | undefined,\n): ClientEvent => ({\n type: \"custom\",\n body: `identify ${distinctId}`,\n severity: \"INFO\",\n timeUnixMs: Date.now(),\n attributes: {\n \"app.custom.name\": \"identify\",\n ...(traits ? flattenAttributes(traits, \"custom\") : {}),\n },\n});\n\nconst start = (config: ReactNativeSdkConfig): void => {\n // Bound before capture activates. React Native's fetch rides XMLHttpRequest,\n // which the dormant patch may already wrap; intake URLs take its internal\n // passthrough branch, so transport traffic never recurses into capture.\n const hostFetch = (globalThis as { fetch?: FetchLike }).fetch;\n if (typeof hostFetch !== \"function\") {\n return;\n }\n const prePatchFetch: FetchLike = (url, init) => hostFetch(url, init);\n\n const endpoint = resolveIntakeEndpoint(config);\n if (!endpoint) {\n return;\n }\n const intakeUrl = `${endpoint.baseUrl}/v1/logs`;\n const randomBytes = createRandomBytes();\n const storage = config.storage;\n // The prefix keeps the console mirror from recording the SDK's own line.\n const warn = (message: string): void =>\n console.warn(`${SDK_CONSOLE_PREFIX} ${message}`);\n\n const transport = createTransport({\n url: intakeUrl,\n publicKey: endpoint.publicKey,\n resourceAttributes: {\n \"service.name\": config.serviceName,\n \"service.version\": config.serviceVersion,\n \"deployment.environment\": config.environment,\n \"telemetry.sdk.name\": SDK_NAME,\n \"telemetry.sdk.version\": SDK_VERSION,\n \"telemetry.sdk.language\": SDK_LANGUAGE,\n ...hostResourceAttributes(),\n },\n scope: { name: SDK_NAME, version: SDK_VERSION },\n fetchImpl: prePatchFetch,\n flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,\n maxBatchBytes: config.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES,\n maxQueuedEvents: config.maxQueuedEvents ?? DEFAULT_MAX_QUEUED_EVENTS,\n warn,\n // The intake refused a batch, or was unreachable for a while: one\n // `sdk_health` row per condition per launch (defined below; the\n // transport reports only from settled uploads, after start() returns).\n onHealth: (report) => reportHealth(report),\n // Everything queued, and every row the last save holds, has been\n // delivered: the copy saved when the app last left the foreground has\n // nothing left to replay. Only this transport's own copy goes.\n onIdle: () => {\n if (storage) clearQueue(storage, transport);\n },\n });\n\n // Session state lives behind the storage adapter, which may be\n // asynchronous. Capture installs now; events emitted before the persisted\n // state is read wait here and are stamped once the session exists, so a\n // cold start never splits its first seconds into a throwaway session.\n let session: SessionManager | undefined;\n const preSessionOps: PreSessionOp[] = [];\n let preSessionEventCount = 0;\n // Set by shutdown() once its synchronous teardowns have run: a callback\n // that outlives them (a slow response's loadend, a navigator event) must\n // not touch the session store for a row the stopped transport would drop\n // anyway, while the teardowns themselves (the tracker closing a touch\n // that is still down) still get their final rows out.\n let stopped = false;\n\n const networkConfig: NetworkCaptureConfig = config.network ?? {};\n // The route path is recorded under the same rule as request URLs: a\n // deep link's `?token=…` never leaves the device in plain text.\n const redactScreenUrl = (url: string): string =>\n redactUrl(url, networkConfig.redactQueryParams);\n\n // `screen` is the one the event happened on: a buffered event replayed\n // after a slow storage read must not take the screen the app moved to.\n const stamp = (\n event: ClientEvent,\n active: SessionManager,\n screen: CurrentScreen = getCurrentScreen(),\n ): void => {\n active.touch();\n const base: EventAttributes = {\n \"app.event_type\": event.type,\n \"session.id\": active.getSessionId(),\n \"session.window_id\": active.getWindowId(),\n \"session.distinct_id\": active.getDistinctId(),\n \"app.screen.name\": screen.name,\n \"app.screen.url\":\n screen.path === undefined ? undefined : redactScreenUrl(screen.path),\n \"derived.source\": \"sdk\",\n };\n transport.enqueue(encodeLogRecord(event, base));\n };\n\n const warnedBeforeSend = new Set<string>();\n const warnBeforeSendOnce = (warning: {\n reason: string;\n message: string;\n }): void => {\n if (warnedBeforeSend.has(warning.reason)) return;\n warnedBeforeSend.add(warning.reason);\n const { message } = warning;\n warn(message);\n };\n const beforeSend = createBeforeSendGate(\n config.beforeSend,\n warnBeforeSendOnce,\n );\n\n const emit = (captured: ClientEvent): void => {\n if (stopped) {\n return;\n }\n try {\n const event = beforeSend(captured);\n if (!event) {\n return;\n }\n if (session) {\n stamp(event, session);\n } else if (\n preSessionEventCount < MAX_PRE_SESSION_EVENTS ||\n // At most one per diagnostic, and already marked reported: a full\n // pre-session queue must not swallow the launch's only health row.\n event.type === \"sdk_health\"\n ) {\n preSessionOps.push({\n kind: \"event\",\n event,\n screen: getCurrentScreen(),\n });\n preSessionEventCount += 1;\n }\n } catch {\n // Emitting must never throw into the host.\n }\n };\n\n const safeEmitEvent = (build: () => ClientEvent): void => {\n try {\n emit(build());\n } catch {\n // Never throw into the host.\n }\n };\n\n // The SDK's own delivery health as `sdk_health` rows, at most one per\n // condition per launch, so a refused health row is never reported again.\n const reportHealth = createSdkHealthReporter(emit);\n\n // Read the stored crash outbox before anything can crash: a synchronous\n // adapter is merged by the time this returns, so a fatal error at any later\n // point extends the stored report instead of writing beside it. What the\n // read discards is reported on this launch.\n if (storage) {\n watchOutbox(storage, reportHealth);\n void loadOutbox(storage, warn);\n }\n\n // The intake, and in development the Metro server (LogBox symbolication,\n // bundle and status requests), are never app traffic.\n const devServer = devServerOrigin();\n // Matched by parsed origin and whole path segments, never a string prefix:\n // `https://<intake>.evil.test/` is app traffic.\n const isInternalUrl = (url: string): boolean =>\n isUnderBase({ url, base: endpoint.baseUrl }) ||\n (devServer !== undefined && isUnderBase({ url, base: devServer })) ||\n (isDevHost() && url.includes(\"/symbolicate\"));\n\n const teardowns: Array<() => void> = [];\n if (storage) teardowns.push(() => unwatchOutbox(storage, reportHealth));\n // Set by a fatal error: from then on this process uploads no outbox until\n // it has outlived FATAL_SURVIVAL_MS.\n let survivalTimer: ReturnType<typeof setTimeout> | undefined;\n let crashed = false;\n teardowns.push(() => {\n if (survivalTimer !== undefined) clearTimeout(survivalTimer);\n survivalTimer = undefined;\n });\n\n const errorCapture = installErrorCapture({\n emit,\n unhandledRejections: config.errors?.unhandledRejections !== false,\n previousRejectionTracker: config.errors?.previousRejectionTracker,\n onFatal: (fatalEvent) => {\n crashed = true;\n // Nothing may leave the queue on its own until the outbox has it: a\n // fatal row that tips the queue over the byte budget would otherwise\n // be flushed into a request the dying process never completes.\n transport.holdFlushes(true);\n try {\n // A crash before the persisted session was read: the buffered rows\n // would die with the process. Start the session from whatever has\n // been read so far and stamp them now; continuity with the previous\n // launch is lost in that window, the crash is not.\n startSession();\n emit(fatalEvent);\n if (storage) {\n stashOutbox(storage, transport, randomHex(randomBytes, 8));\n }\n } finally {\n transport.holdFlushes(false);\n }\n if (!storage) {\n void transport.flush();\n return;\n }\n if (survivalTimer === undefined && !stopped) {\n survivalTimer = setTimeout(() => {\n survivalTimer = undefined;\n void deliverOutbox(storage, transport);\n }, FATAL_SURVIVAL_MS);\n }\n },\n });\n teardowns.push(() => errorCapture.teardown());\n\n if (config.pointer?.capture !== false) {\n const tracker = createPointerTracker({\n emit,\n randomBytes,\n sampleIntervalMs: config.pointer?.sampleIntervalMs,\n partialIntervalMs: config.pointer?.partialIntervalMs,\n });\n setActiveTracker(tracker);\n teardowns.push(() => {\n setActiveTracker(undefined);\n tracker.teardown();\n });\n }\n\n setNavigationEmitter(emit, redactScreenUrl);\n teardowns.push(() => setNavigationEmitter(undefined));\n\n if (config.console?.capture !== false) {\n const consoleCapture = installConsoleCapture({\n emit,\n levels: config.console?.levels ?? [\"error\", \"warn\"],\n selfPrefix: SDK_CONSOLE_PREFIX,\n });\n teardowns.push(() => consoleCapture.teardown());\n }\n\n const networkCapture = networkConfig.capture ?? true;\n const propagateTraceContext = networkConfig.propagateTraceContext ?? true;\n if (networkCapture || propagateTraceContext) {\n const network = installNetworkCapture({\n emit,\n traceContext: createTraceContextManager({\n sampledFlag: networkConfig.sampledFlag ?? false,\n randomBytes,\n }),\n capture: networkCapture,\n propagateTraceContext,\n allowlist: networkConfig.allowlist ?? [],\n redactQueryParams: networkConfig.redactQueryParams,\n requestIdHeaders:\n networkConfig.requestIdHeaders ?? DEFAULT_REQUEST_ID_HEADERS,\n isInternalUrl,\n resolveUrl,\n // No origin and no CORS preflight on a native host: every request is\n // a correlation candidate unless the allowlist narrows it.\n isDefaultInjectionTarget: () => true,\n });\n teardowns.push(() => network.teardown());\n }\n\n const lifecycle = installLifecycleCapture({\n emit,\n onBackground: () => {\n // The OS may kill a backgrounded app without another callback: what\n // is queued (all of it, offline) is saved for the next launch first.\n if (storage) saveQueue(storage, transport, randomHex(randomBytes, 8));\n void transport.flush();\n },\n });\n teardowns.push(() => lifecycle.teardown());\n\n // Session store: hydrated from the adapter when there is one, memory only\n // otherwise. The window id is one app launch either way.\n let sessionStore: SessionStore;\n let ready: Promise<void>;\n if (storage) {\n const hydrating = createHydratingStore(storage, PERSISTED_SESSION_KEYS);\n sessionStore = hydrating.store;\n ready = hydrating.ready;\n } else {\n sessionStore = createMemoryStore();\n ready = Promise.resolve();\n }\n\n const startSession = (): void => {\n // A hydration that lands after shutdown() starts nothing: no session\n // write, no outbox upload, and the identity claimed for a later init()\n // stays where it is.\n if (session || stopped) {\n return;\n }\n const active = createSessionManager({\n store: sessionStore,\n windowStore: createMemoryStore(),\n randomBytes,\n });\n session = active;\n // Identity claimed before init() comes first: it never produced a row.\n if (state.pendingReset) {\n active.reset();\n state.pendingReset = false;\n }\n const claimedBeforeInit = state.pendingIdentity;\n state.pendingIdentity = undefined;\n if (claimedBeforeInit) {\n active.setDistinctId(claimedBeforeInit.distinctId);\n emit(\n identifyEvent(claimedBeforeInit.distinctId, claimedBeforeInit.traits),\n );\n }\n // Then everything since init(), in the order it happened.\n for (const op of preSessionOps.splice(0)) {\n try {\n if (op.kind === \"identify\") {\n active.setDistinctId(op.distinctId);\n } else if (op.kind === \"reset\") {\n active.reset();\n } else {\n stamp(op.event, active, op.screen);\n }\n } catch {\n // Never throw into the host.\n }\n }\n if (storage && !crashed) {\n void deliverOutbox(storage, transport);\n }\n };\n\n // Identity changes with no event between them collapse to their net effect\n // (a reset if there was one, then the last identify after it), so a loop of\n // identify() calls during a slow storage read cannot grow the queue: with\n // no row stamped in between, the skipped steps change nothing visible.\n const queueIdentity = (op: PreSessionOp): void => {\n let runStart = preSessionOps.length;\n while (runStart > 0 && preSessionOps[runStart - 1]?.kind !== \"event\") {\n runStart -= 1;\n }\n const run = preSessionOps.splice(runStart);\n if (op.kind === \"reset\" || run.some((queued) => queued.kind === \"reset\")) {\n preSessionOps.push({ kind: \"reset\" });\n }\n if (op.kind === \"identify\") {\n preSessionOps.push(op);\n }\n };\n\n state.instance = {\n identify(distinctId, traits) {\n safeEmitEvent(() => {\n if (session) {\n session.setDistinctId(distinctId);\n } else {\n queueIdentity({ kind: \"identify\", distinctId });\n }\n return identifyEvent(distinctId, traits);\n });\n },\n reset() {\n try {\n if (session) {\n session.reset();\n } else {\n queueIdentity({ kind: \"reset\" });\n }\n } catch {\n // Never throw into the host.\n }\n },\n addEvent(name, attributes) {\n safeEmitEvent(() => ({\n type: \"custom\",\n body: `custom ${name}`,\n severity: \"INFO\",\n timeUnixMs: Date.now(),\n attributes: {\n \"app.custom.name\": name,\n ...(attributes ? flattenAttributes(attributes, \"custom\") : {}),\n },\n }));\n },\n log(severity, message, metadata) {\n safeEmitEvent(() => ({\n type: \"log\",\n body: message,\n severity,\n timeUnixMs: Date.now(),\n attributes: metadata ? flattenAttributes(metadata) : {},\n }));\n },\n async flush() {\n await ready;\n await transport.flush();\n },\n async shutdown() {\n for (const teardown of teardowns.reverse()) {\n try {\n teardown();\n } catch {\n // Teardown must be best-effort.\n }\n }\n stopped = true;\n uninstallNetworkPatches();\n resetNavigationState();\n await ready;\n await transport.shutdown();\n },\n };\n\n void ready.then(startSession, startSession);\n};\n\n/** `init()` must never throw into the app, whatever a hostile host does. */\nconst safeStart = (config: ReactNativeSdkConfig): void => {\n try {\n start(config);\n } catch {\n // Capture that could not install stays off.\n }\n};\n\n/**\n * Initialize the SDK. Idempotent: repeat calls are ignored. Installs nothing\n * under react-native-web (use `@sazabi/browser` there). When `consent` is\n * provided, nothing is installed until it resolves true.\n */\nexport const init = (config: ReactNativeSdkConfig): void => {\n if (isWebHost()) {\n if (!state.warnedWebHost) {\n state.warnedWebHost = true;\n console.warn(\n `${SDK_CONSOLE_PREFIX} this app is running in a browser; use @sazabi/browser instead. SDK disabled.`,\n );\n }\n return;\n }\n if (state.initialized) {\n return;\n }\n state.initialized = true;\n state.generation += 1;\n const thisGeneration = state.generation;\n\n if (!config.serviceName || !resolveIntakeEndpoint(config)) {\n console.warn(\n `${SDK_CONSOLE_PREFIX} init() requires intakeUrl and serviceName; SDK disabled. ` +\n \"Pass publicKey too if the intake URL is a proxy that carries no key.\",\n );\n return;\n }\n\n if (config.consent === undefined) {\n safeStart(config);\n return;\n }\n\n try {\n void Promise.resolve(config.consent())\n .then((granted) => {\n // A consent that lands after shutdown(), or after a later init(),\n // belongs to an initialization that no longer exists.\n if (granted === true && thisGeneration === state.generation) {\n safeStart(config);\n }\n })\n .catch(() => {\n // Consent evaluation failed: stay dormant.\n });\n } catch {\n // Synchronous throw from consent(): stay dormant.\n }\n};\n\n/** Set the client-asserted user identity (`session.distinct_id`). */\nexport const identify = (\n distinctId: string,\n traits?: Record<string, unknown>,\n): void => {\n if (state.instance) {\n state.instance.identify(distinctId, traits);\n } else {\n state.pendingIdentity = { distinctId, traits };\n }\n};\n\n/**\n * Logout: clear the client-asserted identity and rotate both session and\n * window ids, so later activity on this device never threads into the\n * previous user's timeline.\n */\nexport const reset = (): void => {\n if (state.instance) {\n state.instance.reset();\n } else {\n state.pendingReset = true;\n state.pendingIdentity = undefined;\n }\n};\n\n/** Emit a custom mark into the session event stream. */\nexport const addEvent = (\n name: string,\n attributes?: Record<string, unknown>,\n): void => {\n state.instance?.addEvent(name, attributes);\n};\n\n/**\n * Emit an application log line (`app.event_type: \"log\"`) with session context\n * attached. Drops silently before init/consent, like all capture.\n */\nexport const log = (\n severity: EventSeverity,\n message: string,\n metadata?: Record<string, unknown>,\n): void => {\n state.instance?.log(severity, message, metadata);\n};\n\n/** Force-flush queued events. */\nexport const flush = async (): Promise<void> => {\n await state.instance?.flush();\n};\n\n/** Tear down all patches/listeners (restoring originals) and flush. */\nexport const shutdown = async (): Promise<void> => {\n const active = state.instance;\n state.instance = undefined;\n state.initialized = false;\n state.generation += 1;\n state.pendingIdentity = undefined;\n state.pendingReset = false;\n await active?.shutdown();\n};\n"
|
|
24
|
+
],
|
|
25
|
+
"mappings": "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",
|
|
26
|
+
"debugId": "B9F1449F62B086F864756E2164756E21",
|
|
27
|
+
"names": []
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PointerTarget, PointerTracker } from "./client-sdk-core/index.js";
|
|
2
|
+
import { Component, createElement, type ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Touch capture for the pointer-episode tracker. `<SazabiRoot>` is a plain
|
|
5
|
+
* `View` that observes every touch bubbling up from the app without claiming
|
|
6
|
+
* the responder, so gestures reach their targets untouched. It works
|
|
7
|
+
* wherever it is mounted and whenever `init()` runs: touches before activation
|
|
8
|
+
* are dropped, never queued.
|
|
9
|
+
*/
|
|
10
|
+
export declare const setActiveTracker: (tracker: PointerTracker | undefined) => void;
|
|
11
|
+
export declare const describeFiberTarget: (targetInstance: unknown) => PointerTarget | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Wrap the app's root in this once, outside every other component. It
|
|
14
|
+
* renders a single full-size `View` with touch observers, and is the error
|
|
15
|
+
* boundary that records a render or commit error as a `fatal` row (crash
|
|
16
|
+
* outbox included) before rethrowing it: under React 19 such an error never
|
|
17
|
+
* reaches `ErrorUtils`, so without this boundary a release app dies with
|
|
18
|
+
* nothing recorded. The rethrow leaves React Native's own handling (the red
|
|
19
|
+
* box in development, the native crash in release) exactly as it was.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SazabiRoot extends Component<{
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}, {
|
|
24
|
+
error?: {
|
|
25
|
+
value: unknown;
|
|
26
|
+
};
|
|
27
|
+
}> {
|
|
28
|
+
static displayName: string;
|
|
29
|
+
state: {
|
|
30
|
+
error?: {
|
|
31
|
+
value: unknown;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
static getDerivedStateFromError(error: unknown): {
|
|
35
|
+
error: {
|
|
36
|
+
value: unknown;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
render(): ReturnType<typeof createElement>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClientEvent } from "./client-sdk-core/index.js";
|
|
2
|
+
export interface LifecycleCaptureOptions {
|
|
3
|
+
emit(event: ClientEvent): void;
|
|
4
|
+
/** The app is leaving the foreground: the mobile unload flush point. */
|
|
5
|
+
onBackground(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface LifecycleCaptureHandle {
|
|
8
|
+
teardown(): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `AppState` is the mobile lifecycle: `background` and `inactive` are the
|
|
12
|
+
* unload, `active` is the return. Each edge is a `lifecycle` row so the
|
|
13
|
+
* timeline reads causally, and leaving the foreground flushes the queue
|
|
14
|
+
* while the OS still allows network activity.
|
|
15
|
+
*/
|
|
16
|
+
export declare const installLifecycleCapture: (options: LifecycleCaptureOptions) => LifecycleCaptureHandle;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ClientEvent } from "./client-sdk-core/index.js";
|
|
2
|
+
import { type CurrentScreen } from "./process-state.js";
|
|
3
|
+
import type { NavigationContainerRefLike } from "./types.js";
|
|
4
|
+
/** The route the user is on, stamped on every row as `app.screen.*`. */
|
|
5
|
+
export declare const getCurrentScreen: () => CurrentScreen;
|
|
6
|
+
/**
|
|
7
|
+
* Bound at start, cleared at shutdown; navigation tracked before init still
|
|
8
|
+
* updates the screen. `redactUrl` is the core's redaction with the app's
|
|
9
|
+
* `network.redactQueryParams`: a deep-link route path can carry a query
|
|
10
|
+
* string (`/reset-password?token=…`), so it is recorded like any other URL.
|
|
11
|
+
*/
|
|
12
|
+
export declare const setNavigationEmitter: (emit: ((event: ClientEvent) => void) | undefined, redactUrl?: (url: string) => string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Screen changes come from the navigator the app already owns. Subscribes to
|
|
15
|
+
* a React Navigation container ref's `state` event (expo-router exposes the
|
|
16
|
+
* same ref) and emits a `navigation` row from `getCurrentRoute()`: the route
|
|
17
|
+
* name, the pathname when the router has one, and the param keys, never the
|
|
18
|
+
* values. A state change that lands on the same route (a param update, a
|
|
19
|
+
* nested navigator settling) emits nothing; a new push of the same screen
|
|
20
|
+
* has a new route key and is a new visit. Returns the unsubscribe.
|
|
21
|
+
*/
|
|
22
|
+
export declare const trackNavigation: (ref: NavigationContainerRefLike) => (() => void);
|
|
23
|
+
/** Test and shutdown hook: forget the screen so a later init starts clean. */
|
|
24
|
+
export declare const resetNavigationState: () => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type NetworkCaptureOptions } from "./client-sdk-core/index.js";
|
|
2
|
+
export interface NetworkCaptureHandle {
|
|
3
|
+
teardown(): void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* React Native's `fetch` is a polyfill over its own `XMLHttpRequest`, so the
|
|
7
|
+
* core's XHR patch alone sees every request this host makes (`fetch`,
|
|
8
|
+
* `axios`, direct XHR) and a `fetch` wrapper would emit each one twice.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ensureNetworkPatches: () => void;
|
|
11
|
+
export declare const uninstallNetworkPatches: () => void;
|
|
12
|
+
export declare const installNetworkCapture: (options: NetworkCaptureOptions) => NetworkCaptureHandle;
|