@sazabi/react-native 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../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 {\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",
6
+ "export const SDK_NAME = \"@sazabi/react-native\";\nexport const SDK_VERSION = \"0.1.0\";\n/** SDK-emitted console lines carry this prefix and are never re-captured. */\nexport const SDK_CONSOLE_PREFIX = \"[@sazabi/react-native]\";\n",
7
+ "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",
8
+ "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",
9
+ "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",
10
+ "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",
11
+ "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",
12
+ "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",
13
+ "import {\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 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 emit = (event: ClientEvent): void => {\n if (stopped) {\n return;\n }\n try {\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"
14
+ ],
15
+ "mappings": "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",
16
+ "debugId": "ACB6EA50D2850DE964756E2164756E21",
17
+ "names": []
18
+ }
@@ -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;
@@ -0,0 +1,53 @@
1
+ import type { SdkHealthReporter, Transport } from "./client-sdk-core/index.js";
2
+ import type { StorageAdapter } from "./types.js";
3
+ export declare const OUTBOX_INDEX_KEY = "sazabi.outbox.index";
4
+ /** Stamped on every record of a stashed batch; a re-sent copy carries the same value. */
5
+ export declare const OUTBOX_REPLAY_ID_KEY = "app.outbox.id";
6
+ /**
7
+ * Route the outbox's health reports to the running instance. Set before
8
+ * `loadOutbox`, so what the first read discards is reported on this launch.
9
+ */
10
+ export declare const watchOutbox: (adapter: StorageAdapter, report: SdkHealthReporter) => void;
11
+ /** The instance is gone: nothing it would have reported is reported. */
12
+ export declare const unwatchOutbox: (adapter: StorageAdapter, report: SdkHealthReporter) => void;
13
+ /**
14
+ * Read the stored outbox once per adapter. Started by `init()` before any
15
+ * capture installs: a synchronous adapter is loaded before this returns, so
16
+ * a crash at any later point merges into the stored report instead of
17
+ * writing beside it.
18
+ */
19
+ export declare const loadOutbox: (adapter: StorageAdapter, warn?: (message: string) => void) => Promise<void>;
20
+ /**
21
+ * The crash outbox replaces the browser's keepalive flush. A fatal JavaScript
22
+ * error terminates the process shortly after the global handler returns, so
23
+ * the pending queue (the error row included) is encoded into standalone
24
+ * envelopes stamped with one replay id and written to storage: the batch's
25
+ * own key first, then the index that lists it.
26
+ *
27
+ * Nothing the dying process sends can be cleared afterwards: an upload the
28
+ * intake accepted just before the process died would be posted again by the
29
+ * next launch. So when a synchronous adapter took both writes (`true`),
30
+ * nothing is sent here; the caller delivers the outbox once the process has
31
+ * proved it survived, and otherwise the next `init()` does. A
32
+ * promise-returning adapter may never finish the writes, so its batch is
33
+ * also sent at once, and a copy the next launch re-sends carries the same
34
+ * replay id. The saved queue goes once both writes were accepted (every row
35
+ * it held is in this batch), never before: a store that refuses the batch
36
+ * near its limit must not cost the only copy of those rows.
37
+ */
38
+ export declare const stashOutbox: (adapter: StorageAdapter, transport: Transport, replayId: string) => boolean;
39
+ /** Post everything the outbox holds once the stored keys have been read. */
40
+ export declare const deliverOutbox: (adapter: StorageAdapter, transport: Transport) => Promise<void>;
41
+ /**
42
+ * Save the live queue, within the outbox caps, as the app leaves the
43
+ * foreground: the OS may kill a backgrounded app without another callback,
44
+ * and an offline queue would die with it. The queued records are stamped
45
+ * with the replay id, so the live upload and the copy a later launch sends
46
+ * share it. Not before the previous launch's saved queue has been read.
47
+ */
48
+ export declare const saveQueue: (adapter: StorageAdapter, transport: Transport, replayId: string) => void;
49
+ /**
50
+ * `transport`'s live queue was delivered (or stashed by a crash): its saved
51
+ * copy goes, unless the key now holds another transport's queue.
52
+ */
53
+ export declare const clearQueue: (adapter: StorageAdapter, transport: Transport) => void;
@@ -0,0 +1,49 @@
1
+ import type { ClientEvent, EventSeverity, PointerTracker } from "./client-sdk-core/index.js";
2
+ export interface SdkInstance {
3
+ identify(distinctId: string, traits?: Record<string, unknown>): void;
4
+ reset(): void;
5
+ addEvent(name: string, attributes?: Record<string, unknown>): void;
6
+ log(severity: EventSeverity, message: string, metadata?: Record<string, unknown>): void;
7
+ flush(): Promise<void>;
8
+ shutdown(): Promise<void>;
9
+ }
10
+ interface PendingIdentity {
11
+ distinctId: string;
12
+ traits?: Record<string, unknown>;
13
+ }
14
+ export interface CurrentScreen {
15
+ name?: string;
16
+ path?: string;
17
+ /** The navigator's per-visit route key: two pushes of one screen are two visits. */
18
+ key?: string;
19
+ }
20
+ interface SdkState {
21
+ instance?: SdkInstance;
22
+ initialized: boolean;
23
+ warnedWebHost: boolean;
24
+ /** Bumped by every init() and shutdown(), so a consent promise that resolves after a shutdown starts nothing. */
25
+ generation: number;
26
+ pendingIdentity?: PendingIdentity;
27
+ pendingReset: boolean;
28
+ /** The running instance's pointer tracker, fed by every `<SazabiRoot>`. */
29
+ tracker?: PointerTracker;
30
+ /** Where `<SazabiRoot>` reports a render or commit error it caught. */
31
+ renderErrorSink?: (error: unknown) => void;
32
+ /** Where `trackNavigation()` sends its rows. */
33
+ navigationEmit?: (event: ClientEvent) => void;
34
+ /** Applied to the route path a navigation row records: a deep link can carry a query string. */
35
+ navigationRedactUrl?: (url: string) => string;
36
+ /** The route the user is on, stamped on every row as `app.screen.*`. */
37
+ screen: CurrentScreen;
38
+ /** The development note about the unreadable rejection slot, printed once per process. */
39
+ warnedRejectionSlot: boolean;
40
+ }
41
+ /**
42
+ * Process-wide, not module-wide: a second evaluation of this module (a
43
+ * duplicate copy in the bundle, a module registry reset in development)
44
+ * finds the running instance instead of installing a second `ErrorUtils`
45
+ * handler, rejection tracker, and transport beside it, and a `<SazabiRoot>`
46
+ * or `trackNavigation()` imported from that copy feeds the running instance.
47
+ */
48
+ export declare const state: SdkState;
49
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import{q as r,v as t}from"./chunk-c8j7sr2j.js";try{if(!r())t()}catch{}
2
+
3
+ //# debugId=BACD667089F018C964756E2164756E21
4
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/register.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Early-instrumentation entrypoint: `import \"@sazabi/react-native/register\"`.\n *\n * Importing this module installs the SDK's `XMLHttpRequest` patch as a\n * dormant, transparent passthrough at module-evaluation time, before any\n * other library's module-scope code can capture the prototype methods\n * (Metro evaluates an entry file's imports in order, and every static import\n * runs before the first statement). `init()` later activates it with\n * configuration; until then it observes nothing, stores nothing, and sends\n * nothing.\n *\n * Put this import above all other imports in your app's entry module.\n */\nimport { isWebHost } from \"./host\";\nimport { ensureNetworkPatches } from \"./network\";\n\ntry {\n // react-native-web runs in a browser, where @sazabi/browser owns capture.\n if (!isWebHost()) {\n ensureNetworkPatches();\n }\n} catch {\n // Registration must never break the host app; init() re-ensures.\n}\n"
6
+ ],
7
+ "mappings": "+CAgBA,GAAI,CAEF,GAAI,CAAC,EAAU,EACb,EAAqB,EAEvB,KAAM",
8
+ "debugId": "BACD667089F018C964756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,42 @@
1
+ import { type SessionStore } from "./client-sdk-core/index.js";
2
+ import type { StorageAdapter } from "./types.js";
3
+ /**
4
+ * Storage adapters may be synchronous (MMKV) or promise-returning
5
+ * (AsyncStorage). Every call here is best-effort: an adapter that throws or
6
+ * rejects degrades the SDK to in-memory state and never surfaces to the app.
7
+ */
8
+ export declare const readKey: (adapter: StorageAdapter, key: string) => Promise<string | null>;
9
+ export declare const isThenable: (value: unknown) => value is PromiseLike<unknown>;
10
+ /** What a read found; `undefined` means the adapter failed to answer. */
11
+ export type ReadResult = {
12
+ value: string | null;
13
+ } | undefined;
14
+ /** Read a key, synchronously when the adapter can, so the crash outbox loads inside `init()`. */
15
+ export declare const readKeyNow: (adapter: StorageAdapter, key: string) => ReadResult | Promise<ReadResult>;
16
+ /**
17
+ * Every key the adapter holds. `undefined` when the adapter has no listing
18
+ * or it failed: the caller then knows only what the indexes list, never
19
+ * that a key is absent.
20
+ */
21
+ export declare const listKeysNow: (adapter: StorageAdapter) => readonly string[] | undefined | Promise<readonly string[] | undefined>;
22
+ /**
23
+ * `true` once the adapter accepted the write, `false` when it refused. A
24
+ * synchronous adapter answers synchronously, which is how the crash path
25
+ * knows the batch is on disk before the process can die.
26
+ */
27
+ export declare const writeKey: (adapter: StorageAdapter, key: string, value: string) => boolean | Promise<boolean>;
28
+ /** `true` once the adapter accepted the removal, synchronously when it can. */
29
+ export declare const removeKey: (adapter: StorageAdapter, key: string) => boolean | Promise<boolean>;
30
+ export interface HydratingStore {
31
+ /** Synchronous view the session machine reads through. */
32
+ store: SessionStore;
33
+ /** Resolves once every key has been read from the adapter. */
34
+ ready: Promise<void>;
35
+ }
36
+ /**
37
+ * The session machine needs synchronous reads, so persisted state is read
38
+ * once into an in-memory mirror and every later write goes through to the
39
+ * adapter. A key written locally before hydration finishes keeps the local
40
+ * value: a `reset()` must not be undone by a slow read of the old identity.
41
+ */
42
+ export declare const createHydratingStore: (adapter: StorageAdapter, keys: readonly string[]) => HydratingStore;