@wireai/activation 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/analytics/reportClientEvent.ts","../../src/utils/warnInDev.ts","../../src/analytics/currentSession.ts","../../src/reviews/transport.ts","../../src/analytics/screenTracking.ts","../../src/analytics/useScreenTracking.ts","../../src/analytics/wireDoctor.ts","../../src/permissions/permissionEvents.ts","../../src/analytics/analyticsEvent.ts","../../src/device/appVersion.ts","../../src/device/deviceModel.ts","../../src/device/deviceContext.ts","../../src/analytics/contextEnvelope.ts","../../src/analytics/eventQueue.ts","../../src/identity/userIdentity.ts","../../src/context/userContext.ts","../../src/identity/identityRecord.ts","../../src/context/deviceId.ts","../../src/analytics/analyticsFacade.ts","../../src/analytics/useAnalytics.ts"],"names":["useRef","useEffect","runtimeRequire","interop","safeInterop","Platform","Dimensions","I18nManager","_a","_b"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * reportClientEvent — forward DEVICE-ONLY onboarding events to the Wire AI analytics\n * backend (`POST {serverUrl}/v1/events`), completing the funnel for events the server\n * can't observe on its own.\n *\n * The backend already records the server-observable funnel during the A2A flow\n * (`session_started`, `screen_shown`, `answer_submitted`, `completed`, `llm_fallback`,\n * and even `screen_skipped` — it derives that from the kit's skip sentinel). The one\n * event no server request can capture is `dropped`: the user closing the app / unmounting\n * the flow without finishing. That's what this reporter is for.\n *\n * Contract (server: routers/onboarding.py → analytics/events.py):\n * POST {serverUrl}/v1/events\n * Authorization: Bearer {apiKey}\n * { \"events\": [ { event_type, session_id, screen_index?, component?, question_key?,\n * latency_ms?, meta?, device?, user_context? } ] }\n * The server fills `app_id` + `environment` from the resolving key (never send app_id),\n * and silently skips malformed events — one bad payload never fails the batch.\n *\n * ⚠️ Correlation: `session_id` MUST equal the A2A `contextId` the server uses to key the\n * server-side events, or the funnel report (which groups by `session_id`) treats this as a\n * phantom session. See `makeSessionId` + WireOnboarding for how the kit seeds it.\n *\n * Fire-and-forget: this never throws into the UI and never awaits — analytics must never\n * be able to break onboarding.\n */\nimport type { DeviceContext } from \"../device/deviceContext\";\n\n/** Event types a CLIENT may report. The rest of the funnel is server-side; sending those\n * here would double-count. `screen_skipped` is included for completeness, but the kit does\n * NOT emit it — the backend already derives it from the skip sentinel (see OnboardingFlow).\n * `client_fallback` is emitted by the kit when the AI flow degrades to the static fallback,\n * so the dashboard's fallback-rate counts the whole-flow case (distinct from the server's\n * per-turn `llm_fallback`). The server back-fills a `session_started` for it if unseen.\n * This is the SINGLE fallback signal — hosts must NOT also report their own.\n * `identify` binds the host's opaque `user_id` to this `session_id` (late binding — the user\n * registered during/after onboarding). It carries no funnel weight; the server maps the\n * session to the user and back-fills a `session_started` if it never saw the session.\n * `app_event` is the host's own in-app event (the `createAnalytics` façade's `track`/`screen`\n * route through the offline queue as first-class `ClientEvent`s). The server already ingests\n * it — `reportAppEvent` (reviews/transport) has fired `event_type='app_event'` all along; this\n * widening just lets the same shape flow through the durable queue instead of a blind POST. */\nexport type ClientEventType =\n | \"screen_skipped\"\n | \"dropped\"\n | \"client_fallback\"\n | \"identify\"\n | \"app_event\";\n\n/** One client-reported event. Mirrors the server's `OnboardingEvent` (client-settable fields). */\nexport type ClientEvent = {\n event_type: ClientEventType;\n /** Must match the server-side A2A contextId for this onboarding (see makeSessionId). */\n session_id: string;\n /** 0-based index of the screen the event refers to (matches server `screen_shown`). */\n screen_index?: number;\n component?: string;\n question_key?: string;\n latency_ms?: number;\n /** JSON-stringified extras; the server stores it verbatim. */\n meta?: string;\n /**\n * Privacy-label-neutral device snapshot (platform / form factor / locale / host appVersion).\n * Sent as an object; the server sanitizes + persists it and derives a coarse country. Old\n * servers ignore this unknown field — fully backward compatible. See device/deviceContext.ts.\n */\n device?: DeviceContext;\n /**\n * Host-injected, non-PII context (signup method, referral, plan, hashed user id). Old servers\n * ignore it. MUST NOT contain PII like raw emails — see the README `userContext` section.\n */\n user_context?: Record<string, string | number | boolean>;\n /**\n * The host's OPAQUE PSEUDONYMOUS user id (their internal id, NOT an email/name). Required on\n * `identify`, optional (rides along) on other events. Trimmed + capped at 128 chars host-side.\n * Lets the backend reconcile onboarding sessions to real users. Old servers ignore it.\n */\n user_id?: string;\n /**\n * Client-stamped timestamp of when the event was ENQUEUED on the device. Optional and ADDITIVE.\n *\n * TWO REPRESENTATIONS, on purpose:\n * - INTERNAL (epoch-ms `number`): what the offline queue stamps at enqueue time (see\n * `createEventQueue`), so two otherwise byte-identical events fired seconds apart (a genuine\n * repeat, e.g. the user taps \"share\" twice) are NOT collapsed by the queue's identical-JSON\n * de-dup — while two truly simultaneous re-enqueues of the same instant (a redundant\n * re-render) still share a `ts` and collapse. The de-dup signature depends on this number.\n * - WIRE (ISO8601 UTC `string`): what actually leaves the device. {@link buildEventsRequest}\n * converts the number on its way out, because the server declares `ts: str | None` and\n * pydantic v2 does NOT coerce a number into it — a numeric `ts` made the server answer HTTP\n * 200 with `{written: 0, skipped: N, errors: [{field: \"ts\", reason: \"validation_error\"}]}`,\n * silently discarding EVERY `app_event` through 0.13.0.\n *\n * A caller-set ISO string is passed through as-is. Never a wall-clock the server trusts (it\n * derives its own receive time); an old/strict server that does not model it ignores the field.\n */\n ts?: number | string;\n};\n\n/** Where to POST. Derived from `WireOnboardingConfig` (`serverUrl` + `apiKey`). */\nexport type ClientEventTarget = {\n /** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */\n serverUrl: string;\n /** Tenant API key; sent as `Authorization: Bearer`. */\n apiKey: string;\n};\n\n/**\n * A unique-per-onboarding session id. Used both as the client event `session_id` AND as the\n * seed the kit forwards to the backend so the SERVER adopts it as the A2A `contextId` — making\n * client and server agree (see WireOnboarding + the SDK-correlation note in the kit docs).\n * No crypto dependency: timestamp + random is collision-safe for a single device's onboarding.\n */\nexport const makeSessionId = (): string =>\n `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n\n/**\n * Serialize the internal epoch-ms `ts` to the ISO8601 UTC string the WIRE requires.\n *\n * The server's event model declares `ts: str | None` and pydantic v2 does NOT coerce int → str, so\n * a numeric `ts` fails per-event validation: the endpoint still answers HTTP 200, but with\n * `{written: 0, skipped: N, errors: [{index, reason: \"validation_error\", field: \"ts\"}]}` — every\n * `app_event` from `track()`/`screen()` silently discarded behind a green response.\n *\n * WHY HERE and not at the queue's `stamp()`: this is the single choke point all FIVE send paths go\n * through (offline queue, fire-and-forget, awaitable, review transport, session-start). Converting\n * here leaves the queue's numeric `ts` — and therefore its identical-JSON de-dup signature — exactly\n * as it was, and it also converts the persisted 0.13.0 backlogs (which hold a numeric `ts`) on their\n * way out.\n *\n * ⚠️ NOT the same list as the ack-consumer list in {@link warnOnSkippedEvents}, which is FOUR. The\n * review transport builds its request here but fires and forgets without reading the response, so\n * it rides this conversion and is absent from that one. Count the call sites before editing either.\n *\n * NEVER mutates the caller's event: an event that needs a change is copied. A string `ts` (a\n * caller-set ISO stamp) and an absent `ts` pass through untouched. A non-finite (`NaN`/`Infinity`)\n * or out-of-range number — the latter makes `toISOString` throw — drops the `ts` field from the\n * copy rather than killing the whole batch.\n */\nconst toWireEvents = (events: ClientEvent[]): ClientEvent[] =>\n events.map((event) => {\n if (typeof event.ts !== \"number\") return event;\n const { ts, ...rest } = event;\n if (!Number.isFinite(ts)) return rest;\n try {\n return { ...rest, ts: new Date(ts).toISOString() };\n } catch {\n // Out-of-range epoch-ms — send the event WITHOUT a ts rather than lose the batch.\n return rest;\n }\n });\n\n/**\n * The ONE place the `/v1/events` POST is described (url + method + headers + body). Both the\n * fire-and-forget {@link reportClientEvents} and the awaitable {@link reportClientEventsAwait}\n * build their request here so there is a SINGLE definition of the events transport — no second\n * copy of the endpoint path, headers, or envelope shape to drift. Returns `null` when there is\n * nothing to send (no target / no events) or serialization throws, so callers just bail.\n *\n * It is also where the internal epoch-ms `ts` becomes the wire's ISO8601 string — see\n * {@link toWireEvents} for why the conversion belongs at this choke point.\n *\n * `options.dryRun` asks the server to VALIDATE the batch and write nothing, which is what the wire\n * doctor's round-trip check needs: a real POST, through this one builder, that cannot pollute a\n * tenant's funnel. The `dry_run` key is added ONLY on an explicit `true`, so every production\n * caller (which passes no options at all) still serializes the exact same bytes it did before the\n * parameter existed. That byte-identity is asserted in `buildEventsRequest.test.ts`; every shipped\n * send path rides this envelope, so a diagnostic is not allowed to change it for them.\n */\nexport const buildEventsRequest = (\n target: { serverUrl: string; apiKey?: string } | undefined,\n events: ClientEvent[],\n options?: { dryRun?: boolean },\n): { url: string; init: RequestInit } | null => {\n if (!target?.serverUrl || events.length === 0) return null;\n try {\n const url = `${target.serverUrl.replace(/\\/$/, \"\")}/v1/events`;\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n // The key is CONDITIONALLY assigned rather than set to a falsy default: an absent property and\n // a `dry_run: false` property do not serialize the same, and only absence keeps the body byte\n // identical for the production call sites.\n const envelope: { events: ClientEvent[]; dry_run?: true } = { events: toWireEvents(events) };\n if (options?.dryRun === true) envelope.dry_run = true;\n const body = JSON.stringify(envelope);\n return { url, init: { method: \"POST\", headers, body } };\n } catch {\n // URL construction or JSON serialization failed — nothing to send.\n return null;\n }\n};\n\n/** What the `/v1/events` endpoint says in its 200 body. The deployed server DOES send `errors[]`,\n * each entry carrying `{index, reason, field}` — `field` names the property that failed validation\n * (it is what identified the `ts` rejection), so it is folded into the reason string here. Still\n * read defensively: an older server sends no `errors` at all. */\nexport type EventsAck = { written?: number; skipped: number; reasons: string[] };\n\n/**\n * Read the `/v1/events` ACK body. Resolves `undefined` when there is nothing readable — no `.json`\n * (an old server, a bare test mock), an already-consumed body, or a hostile response object. NEVER\n * throws, and NEVER dev-gated: the `skipped` count is a RETURN VALUE for the awaitable path, not only\n * a warning, so it has to be read in production too.\n *\n * ⚠️ The body can be read exactly once, so a caller that both decides on `skipped` AND warns must do\n * both from ONE call to this.\n *\n * Exported from this MODULE (not from the `./analytics` barrel) so the wire doctor can read an ack\n * with the same reader the send paths use. A second ack reader would be a second thing to drift,\n * and the drift it exists to catch is exactly the kind that hides behind a 200.\n */\nexport const readEventsAck = async (res: unknown): Promise<EventsAck | undefined> => {\n try {\n const json = (res as { json?: () => Promise<unknown> } | null | undefined)?.json;\n if (typeof json !== \"function\") return undefined;\n const body = (await Promise.resolve(json.call(res))) as\n | { written?: unknown; skipped?: unknown; errors?: unknown }\n | null\n | undefined;\n const skipped = body?.skipped;\n if (typeof skipped !== \"number\" || !Number.isFinite(skipped)) return undefined;\n const reasons = Array.isArray(body?.errors)\n ? body.errors\n .map((e) => {\n const entry = e as { reason?: unknown; field?: unknown } | null;\n const reason = entry?.reason;\n if (typeof reason !== \"string\") return undefined;\n // `field` is the whole point of a validation error — a bare \"validation_error\" sends the\n // reader hunting; \"validation_error (field: ts)\" names the property the server refused.\n const field = entry?.field;\n return typeof field === \"string\" && field.length > 0\n ? `${reason} (field: ${field})`\n : reason;\n })\n .filter((r): r is string => typeof r === \"string\")\n : [];\n return { written: typeof body?.written === \"number\" ? body.written : undefined, skipped, reasons };\n } catch {\n // Unreadable / already-consumed body / hostile object — best-effort, treat as \"no ack\".\n return undefined;\n }\n};\n\n/**\n * The DISCARDED warning text, built from what the server ACTUALLY said.\n *\n * It used to staple a cause onto a bare integer — \"the usual cause is a missing or empty session_id\".\n * Since `ensureCurrentSessionId` shipped, no kit path can emit an event without a `session_id`, so\n * that is now the LEAST likely explanation, and naming it sent every reader looking in the one place\n * the problem is not. The endpoint folds four real failures and two idempotent no-ops into one\n * integer, so unless the server volunteers reasons, the honest thing to report is the count and where\n * the reason lives.\n */\nconst describeDiscarded = (ack: EventsAck): string =>\n `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${ack.skipped} event(s) ` +\n \"(skipped in the response body) — they are gone, not retried. The server reported: \" +\n `skipped=${ack.skipped}${ack.written !== undefined ? `, written=${ack.written}` : \"\"}` +\n (ack.reasons.length > 0\n ? `, reasons: ${ack.reasons.join(\", \")}.`\n : \". It gave no reason (the endpoint folds every rejection into one count), so check the \" +\n \"server's ingest log for this request rather than guessing.\");\n\n/** RN sets this global; absent under node/SSR. Read defensively inside {@link warnOnSkippedEvents}. */\ndeclare const __DEV__: boolean | undefined;\n\n/**\n * Read the `/v1/events` ACK body and warn (dev builds only) when the server DISCARDED events.\n *\n * The endpoint answers HTTP **200** with `{ ok, written, skipped }` — an event it refuses is counted\n * in `skipped`, never surfaced in the status code. Every send path here reads `res.ok` alone, so a\n * whole batch can evaporate behind a green response. As of 0.13.0 ALL FOUR send paths consume the ack:\n * the offline queue, session-start, the fire-and-forget POST, and the awaitable one (which also acts\n * on it — see {@link reportClientEventsAwait}).\n *\n * LOG ONLY: returns immediately, never throws, and never influences retry / dequeue / return values.\n * A response with no usable `.json` (an old server, a test mock) is silently ignored.\n *\n * DEV-GATED FIRST: the `__DEV__` check is the FIRST statement, before the body is even looked at.\n * The check used to sit inside the `.then`, so a release build parsed the JSON of every\n * persistent-path POST to build a warning no one would ever read. Nothing here runs in production.\n */\nexport const warnOnSkippedEvents = (res: unknown): void => {\n if (typeof __DEV__ === \"undefined\" || !__DEV__) return;\n if (typeof console === \"undefined\" || !console.warn) return;\n void readEventsAck(res).then((ack) => {\n if (!ack || ack.skipped <= 0) return;\n console.warn(describeDiscarded(ack));\n });\n};\n\n/**\n * POST one or more client events, fire-and-forget. A missing/invalid target, a build error,\n * a missing `fetch`, or a network failure is swallowed — the call returns immediately and the\n * request (if any) runs in the background.\n */\nexport const reportClientEvents = (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): void => {\n try {\n const req = buildEventsRequest(target, events);\n if (!req) return;\n void fetch(req.url, req.init)\n .then((res) => {\n // Still fire-and-forget — nothing is retried, nothing is returned. But a batch that\n // evaporated behind a 200 is exactly what this path used to make invisible, so in dev it\n // now says so, like the queue and session-start paths already did.\n warnOnSkippedEvents(res);\n })\n .catch(() => {\n // Network/transport error — analytics is best-effort, swallow.\n });\n } catch {\n // A missing `fetch` — swallow.\n }\n};\n\n/** Convenience single-event wrapper around {@link reportClientEvents}. */\nexport const reportClientEvent = (\n target: ClientEventTarget | undefined,\n event: ClientEvent,\n): void => reportClientEvents(target, [event]);\n\n/**\n * AWAITABLE sibling of {@link reportClientEvents}: POST one or more client events through the SAME\n * `/v1/events` path, but resolve only once the server has RESPONDED — so a decision re-fetch fired\n * immediately after is guaranteed to see the event in the session stream (this is the guarantee\n * `wire.track` needs before it triggers decision revalidation). Never throws: a missing/invalid\n * target, a missing `fetch`, a network error, or a non-2xx status all resolve to `false`.\n *\n * ⚠️ 0.13.0: a 2xx is NO LONGER SUFFICIENT. The endpoint answers HTTP 200 with `{ ok, written,\n * skipped }` and counts an event it refuses in `skipped`, so this used to resolve `true` for an event\n * the server had thrown away — and `wire.track` then bumped decision revalidation, making every\n * subscribed gate re-fetch against a stream the action never entered. It now reads the ack and\n * resolves `false` when the server reports a positive `skipped`.\n *\n * BACKWARD COMPATIBLE BY CONSTRUCTION: only an explicit positive `skipped` demotes a 200. An old\n * server that sends no such field, a body that cannot be parsed, or a response with no `.json` at all\n * resolves `true` exactly as before — the change can produce no false negatives.\n */\nexport const reportClientEventsAwait = async (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): Promise<boolean> => (await reportClientEventsOutcome(target, events)) === \"delivered\";\n\n/**\n * WHY a failed send is not one thing. `false` collapses two situations that call for OPPOSITE\n * responses, and a caller that wants to make the event durable has to tell them apart:\n *\n * • `unreachable` — the request never got an answer (no `fetch`, a network error, a non-2xx).\n * Nothing is wrong with the EVENT. Retrying later is exactly right, and is what every\n * offline-first path in this kit does.\n * • `refused` — the server READ the event and threw it away (HTTP 200 with `skipped > 0`,\n * a per-event validation failure), or there is no transport to build a request from at all.\n * Retrying cannot change the answer: buffering here would park a poison event in a PERSISTED\n * backlog that re-sends and is re-refused on every drain and every launch, forever.\n *\n * The boolean sibling above is this function with the two collapsed back together, so there is one\n * implementation and the two can never drift.\n */\nexport type SendOutcome = \"delivered\" | \"refused\" | \"unreachable\";\n\nexport const reportClientEventsOutcome = async (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): Promise<SendOutcome> => {\n try {\n const req = buildEventsRequest(target, events);\n // No target / nothing serializable: there is no endpoint to retry against.\n if (!req) return \"refused\";\n const res = await fetch(req.url, req.init);\n if (!res || !res.ok) return \"unreachable\";\n // ONE body read, used for both the verdict and the dev warning — it can only be read once.\n const ack = await readEventsAck(res);\n if (!ack || ack.skipped <= 0) return \"delivered\";\n if (typeof __DEV__ !== \"undefined\" && __DEV__ && typeof console !== \"undefined\" && console.warn) {\n console.warn(describeDiscarded(ack));\n }\n // The server answered and declined THIS event. A retry is a re-decline.\n return \"refused\";\n } catch {\n // Unreachable / missing-fetch / network — best-effort, and retryable.\n return \"unreachable\";\n }\n};\n\n/** Convenience single-event wrapper around {@link reportClientEventsAwait}. */\nexport const reportClientEventAwait = (\n target: ClientEventTarget | undefined,\n event: ClientEvent,\n): Promise<boolean> => reportClientEventsAwait(target, [event]);\n","/**\n * warnInDev — the ONE developer-warning primitive.\n *\n * The kit warns a developer in four places (the onboarding host, the env-config helper, the analytics\n * façade, the current-session registry) and until 0.13.0 each carried its own copy of the same five\n * lines. Three were byte-identical; the fourth differed only by returning whether it actually warned.\n * Four copies of a guard is four chances for one of them to drift out of the `__DEV__` gate, which is\n * the failure that matters: a warning that runs in production is a string built for nobody.\n *\n * ON THE TREE-SHAKING NOTE THIS REPLACES: `analytics/currentSession` used to justify its copy as\n * keeping the module import-free for the tree-shaken `./analytics` bundle. That rationale had already\n * lapsed — the module imports `makeSessionId` from `./reportClientEvent` — and this module has no\n * imports of its own, so it adds one leaf to the graph and nothing to the bundle. The `treeShake`\n * canary still holds the real guarantee (the analytics graph reaches no UI module).\n */\n\n/** RN sets this global; absent under node/SSR. Read defensively, never assumed. */\ndeclare const __DEV__: boolean | undefined;\n\n/**\n * Emit a one-line developer warning, but ONLY in a dev build (RN `__DEV__`). No-op in prod/tests.\n *\n * Returns whether it ACTUALLY warned, which a caller holding a once-flag must honour: marking\n * \"already warned\" after a no-op would burn the single warning in production, and the one dev build\n * that needed it would then run silent.\n */\nexport const warnInDev = (message: string): boolean => {\n if (typeof __DEV__ !== \"undefined\" && __DEV__ && typeof console !== \"undefined\" && console.warn) {\n console.warn(message);\n return true;\n }\n return false;\n};\n","/**\n * currentSession — a tiny registry of the CURRENT per-open `session_id`.\n *\n * WHY it exists (kills the phantom-session): the per-open emitters (`reportSessionStart` and the\n * `useSessionStart` / `useLifecycleEvents` hooks) mint a fresh `session_id` for each app-open and\n * post `app.session_started` with it — so the SERVER knows that id. But other client paths\n * (`identify`, host `app_event`s through the analytics façade) used to reference a DIFFERENT id\n * (a frozen per-instance id), which the server had never seen, so it back-filled a synthetic\n * `session_started` — inflating session counts (the \"phantom-session\" bug).\n *\n * This registry is the single seam that lets those paths reuse the LIVE per-open session id the\n * server already ingested. `reportSessionStart` writes the current id here on every open; the façade\n * reads it so `identify`/app-events correlate to the real session instead of minting a phantom.\n *\n * ── WHY A globalThis SLOT, NOT A PLAIN MODULE VARIABLE ────────────────────────────────────────\n * This module is exported from TWO package entry points — the main `.` bundle (`src/index.ts`) and\n * the `./analytics` subpath (`src/analytics/index.ts`). Under `dist` resolution (node `import`/\n * `require`, which is how tests, SSR and some tooling load the kit) tsup inlines a SEPARATE copy of\n * this module into each bundle, so a plain `let` would give the SETTER (reached via `.` →\n * `reportSessionStart`) and the READER (reached via `./analytics` → façade / `userIdentity`) TWO\n * different variables: the reader would see `undefined` even after an open set the id, and gating\n * would fire under a null session id. On-device this was masked only because Metro's `react-native`\n * export condition resolves both subpaths back to this one `src/` file (a single instance) — a\n * bundler accident, not a guarantee.\n *\n * The bundler-agnostic fix: keep the ONE live value in a well-known `globalThis` slot keyed by a\n * `Symbol.for(...)`. `Symbol.for` uses the runtime-global symbol registry, so every inlined copy of\n * this module resolves the SAME symbol and reads/writes the SAME slot — one identity no matter how\n * many times the module is duplicated across bundles. `globalThis` is present and identical in\n * Hermes/React Native, Node and SSR (we never touch `window`), so this is safe on every host.\n *\n * PROCESS-LOCAL, NOT PERSISTED: the slot lives on the runtime global, so it tracks the CURRENT\n * process's open and a fresh open overwrites it. There is no cross-launch state.\n * `resetCurrentSessionId` clears the slot so a unit test starts from a clean registry.\n *\n * ── WHY `ensureCurrentSessionId` EXISTS (the silent-drop contract) ─────────────────────────────\n * The server's event model declares `session_id: str = Field(min_length=1)` — REQUIRED, non-empty.\n * `POST /v1/events` validates each event inside a try/except that increments a `skipped` counter and\n * still returns HTTP 200. So an event posted without a `session_id` is accepted by the wire and\n * DISCARDED by the server, and a fire-and-forget client can never learn it happened. That is the\n * worst of both: no error, no data. Screen tracking in a host that never mounted the lifecycle hook\n * fell into exactly that hole — every screen view posted, 200'd, and dropped.\n *\n * `ensureCurrentSessionId` closes it: it returns the registered id when an open HAS been registered\n * (unchanged behaviour for every host that fires `reportSessionStart` first), and otherwise mints one,\n * REGISTERS it, and returns it — so every later event in the process correlates to that same id\n * instead of each emitting its own orphan. A minted id is a fallback, not a substitute for a real\n * app-open: it warns once in dev, naming the fix.\n */\nimport { makeSessionId } from \"./reportClientEvent\";\n// The shared primitive returns whether it ACTUALLY warned, which the once-flag below depends on:\n// marking \"already warned\" after a prod no-op would burn the single warning and leave the one dev\n// build that needed it silent. (The old local copy justified itself as keeping this module\n// import-free for the tree-shaken analytics bundle; that had already lapsed — it imports\n// `makeSessionId` right here — and `utils/warnInDev` has no imports of its own.)\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/**\n * Well-known key into the runtime-global symbol registry. `Symbol.for` (NOT a plain `Symbol()`) is\n * what makes this cross-bundle: it returns the SAME symbol for the same string across every copy of\n * this module, so duplicated inlined copies all address one slot.\n *\n * @globalSlot LIVE — every app-open overwrites this with that open's id, so a reader that captures\n * it into a module-local (or a `const` taken once at mount) posts the PREVIOUS open's session to a\n * server that has already moved on. Read it at the moment of use, through `getCurrentSessionId()` /\n * `ensureCurrentSessionId()`. `reviews/runtime`'s `currentOpenId()` deliberately pins one sample of\n * it for the launch; that is a documented latch DERIVED from this slot, not a cache of it.\n */\nconst CURRENT_SESSION_ID_SLOT: unique symbol = Symbol.for(\n \"@wireai/activation:currentSessionId\",\n);\n\ntype GlobalWithSlot = typeof globalThis & {\n [CURRENT_SESSION_ID_SLOT]?: string | undefined;\n};\n\nconst globalSlot = globalThis as GlobalWithSlot;\n\n/**\n * Record the current per-open `session_id`. Called by `reportSessionStart` when it emits an\n * app-open. A blank / non-string id is ignored (the previous id stays current). Idempotent.\n */\nexport const setCurrentSessionId = (id: string | undefined): void => {\n if (typeof id === \"string\" && id.length > 0) {\n globalSlot[CURRENT_SESSION_ID_SLOT] = id;\n }\n};\n\n/** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */\nexport const getCurrentSessionId = (): string | undefined =>\n globalSlot[CURRENT_SESSION_ID_SLOT];\n\n/** Test-only: forget the current session id so a unit test starts from a clean registry. */\nexport const resetCurrentSessionId = (): void => {\n globalSlot[CURRENT_SESSION_ID_SLOT] = undefined;\n};\n\n/** The one-time message. Hoisted so a prod mint does not rebuild a string nobody will read. */\nconst MINT_WARNING =\n \"[wireai] No app-open session was registered, so a session id was minted for this event \" +\n \"(the server drops an event that has no session_id, and still answers 200). Mount \" +\n \"useLifecycleEvents at your app root so events correlate to a real app-open.\";\n\n/**\n * \"Have we already warned about a minted session id?\" — its OWN `Symbol.for` slot, for the same\n * cross-bundle reason as the id itself: a plain module `let` would warn once per inlined copy, i.e.\n * once per bundle, not once per process. NOT cleared by `resetCurrentSessionId`: \"warn once\" is a\n * process-lifetime promise, and a test that resets the id between mints is still one process.\n *\n * @globalSlot LATCH — written `true` on the first mint that actually warned, and never again. A\n * second differing write would re-arm a warning the process has already spent, turning \"once\" into\n * \"once per whoever cleared it\".\n */\nconst MINT_WARNED_SLOT: unique symbol = Symbol.for(\n \"@wireai/activation:currentSessionIdMintWarned\",\n);\n\ntype GlobalWithWarnSlot = typeof globalThis & { [MINT_WARNED_SLOT]?: boolean };\n\nconst warnSlot = globalThis as GlobalWithWarnSlot;\n\n/**\n * The current per-open `session_id`, MINTING and registering one when no app-open has been\n * registered yet. Always returns a non-empty string. Idempotent (a second call returns the same id)\n * and never throws.\n *\n * Use this on every path that puts a `session_id` on the wire. The server REQUIRES a non-empty\n * `session_id` and drops the event otherwise while still answering 200 (see the module header), so\n * \"no id yet\" must never mean \"send it without one\".\n *\n * BACKWARD-COMPATIBLE BY CONSTRUCTION: when `reportSessionStart` / `useLifecycleEvents` has already\n * registered the real per-open id, this is `getCurrentSessionId()` and nothing changes. It only ever\n * mints in the case that used to produce a silently discarded event.\n *\n * A mint means the host never registered an app-open, so the minted id is one the server has not\n * seen a `session_started` for — the events land, but the session is thinner than a real open.\n * Hence the one-time dev warning naming the fix (mount `useLifecycleEvents` at the app root).\n */\nexport const ensureCurrentSessionId = (): string => {\n const existing = globalSlot[CURRENT_SESSION_ID_SLOT];\n if (typeof existing === \"string\" && existing.length > 0) return existing;\n const minted = makeSessionId();\n globalSlot[CURRENT_SESSION_ID_SLOT] = minted;\n if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {\n warnSlot[MINT_WARNED_SLOT] = true;\n }\n return minted;\n};\n","/**\n * transport.ts — kit → Wire server requests for the review module. Non-blocking and never\n * throwing (analytics/reviews must never break the app). Mirrors analytics/reportClientEvent: a\n * thin fetch wrapper with a Bearer tenant key. `submitReview` additionally REPORTS the fate of the\n * row to its caller — accepted / rejected / unsent, three outcomes on purpose; the rest swallow\n * every error.\n *\n * • submitReview → POST {serverUrl}/v1/reviews (the review row)\n * • fetchReviewDecision → GET {serverUrl}/v1/reviews/decision (best-effort, the AI seam)\n * • reportAppEvent → POST {serverUrl}/v1/events (generic app.* namespace)\n *\n * `reportAppEvent` is the strategic extension: it lets a host report arbitrary in-app\n * events through the SAME transport (stored server-side as event_type='app_event',\n * question_key=<name>), which is what the backend review-firing rules evaluate on — and\n * it seeds the broader app-analytics stream. Keep payloads minimal + non-PII.\n */\nimport { ensureCurrentSessionId } from \"../analytics/currentSession\";\nimport { buildEventsRequest, type ClientEvent } from \"../analytics/reportClientEvent\";\nimport type { SubmitResult } from \"../utils/submitResult\";\nimport type { ReviewDecisionResponse, ReviewSubmission, ReviewTarget } from \"./types\";\n\n/**\n * What became of a review POST — the SHARED three-outcome verdict, re-exported under the name the\n * reviews surface has always published. The split is load-bearing and the reasoning lives once, in\n * `utils/submitResult.ts`, because `submitQuestionnaireResponse` answers the identical question.\n * See `submitReview` below for what reads it.\n */\nexport type ReviewSubmitResult = SubmitResult;\n\n/**\n * POST a review (the 1-4 feedback path, and the text-less 5-star row).\n *\n * NON-BLOCKING, and ACKED. It never throws and never makes a caller wait — the request is fired\n * synchronously and the UI is free to advance on the next line — but it RESOLVES with what became\n * of the row.\n *\n * WHY THE RETURN VALUE EXISTS: a 1-4 star rating with mandatory free text is the kit's most\n * valuable payload and it has no persisted queue behind it (unlike `analytics/eventQueue`). If the\n * response is dropped on the floor, a caller cannot tell a delivered submission from one that died\n * in the socket, so it must latch the rating as posted and a detractor who rated offline is lost\n * AND never asked again. Reading the ack is what lets `ReviewGate` keep an undelivered row\n * recoverable.\n *\n * ── WHY IT IS NOT A BOOLEAN ──────────────────────────────────────────────────────────────────\n *\n * It was, for exactly one unpublished release, and the boolean was the bug. `false` meant both\n * \"nothing reached the server\" and \"the server answered non-2xx\", and the one caller that reads\n * this (`ReviewGate.postOnce`) treats `false` as \"still owed\" and re-posts. But the server mints\n * its own row id (`create_review` / `_new_id()`) and `CreateReviewRequest` carries no id, so there\n * is NO idempotency key on the wire: a 502 returned AFTER the insert commits means the re-post\n * writes a SECOND row, double-counting `count` and corrupting `avg` — the precise corruption the\n * one-row latch exists to prevent. A response of any status proves the server was reached, and\n * that is a different question from whether it liked the row. So the two are different values.\n *\n * ── THE RESIDUAL, STATED HONESTLY ────────────────────────────────────────────────────────────\n *\n * `unsent` is not proof the server never got the row. A connection dropped after the request was\n * written — or after the row committed — surfaces as a thrown/rejected `fetch` here, exactly like\n * an offline device. Retrying only on `unsent` is therefore SAFER, not SAFE: it removes the\n * double-post the server itself told us about, and leaves the narrow window where the answer never\n * made it back onto the wire. Closing that window needs a CLIENT-MINTED IDEMPOTENCY KEY the server\n * upserts on, which is a server change (`CreateReviewRequest` + `create_review`) and is not\n * something the kit can fake. Until it exists, prefer losing a row over inventing one: a lost\n * detractor is a gap in the data, a duplicated one is a lie in the data.\n */\nexport const submitReview = async (\n target: ReviewTarget | undefined,\n review: ReviewSubmission,\n): Promise<ReviewSubmitResult> => {\n if (!target?.serverUrl) return \"unsent\";\n try {\n const url = `${target.serverUrl.replace(/\\/$/, \"\")}/v1/reviews`;\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(review),\n });\n // No response object at all is not an answer — treat it as nothing having reached the server\n // rather than as a rejection, or a stubbed-out `fetch` would silently latch the row away.\n if (!res) return \"unsent\";\n return (res as { ok?: boolean }).ok ? \"accepted\" : \"rejected\";\n } catch {\n /* URL/JSON/missing-fetch/network — nothing came back, and never surfaced to the UI */\n return \"unsent\";\n }\n};\n\n/**\n * Options for the best-effort review decision fetch.\n *\n * ── WHAT THE DEPLOYED SERVER ACTUALLY READS (verified 2026-07-17) ────────────────────────\n *\n * `GET /v1/reviews/decision` declares exactly two query params — `session_id` and\n * `device_key` — plus the `Authorization` header. That is the whole wire. Verified against\n * the deployed OpenAPI schema, not against intent.\n *\n * This type is CLOSED on purpose. Hosts that hand-rolled this fetch invented `user_id` and\n * `session_count` query params believing \"the server ignores what it doesn't read, so passing\n * it is always safe\". Both are no-ops: the route declares neither. `session_count` is real, but\n * only on the questionnaire POST body — which is precisely how it copy-pasted its way into a\n * reviews call site and sat there doing nothing. They cost a wire lie — a call site that reads as though\n * identity and a session counter reach the firing brain when neither does. So they are not\n * offered here. Pass identity as `deviceKey`; the session count the server reasons about is the\n * one IT derives from the event stream keyed by `deviceKey`, not one the client asserts.\n *\n * The client-side session floor is a LOCAL rule, not a wire param: use `ReviewConfig.minSessions`\n * (evaluated by `useReviewGate` against the kit's own `wire_review_<id>_sessions` counter).\n */\nexport interface FetchReviewDecisionOptions {\n /**\n * The onboarding session id, when there IS one. OPTIONAL on purpose: the review gate lives on\n * the home feed, where a user legitimately has no onboarding session. `deviceKey` is the real\n * identity for this call. (The server relaxed `session_id` to optional on 2026-07-16 and the\n * relaxation IS deployed — the route no longer 422s without it. Permissive wire: do NOT make\n * this required here.)\n */\n sessionId?: string;\n /**\n * A stable, non-PII device id — THE identity for this call. The decision endpoint reads it for\n * cooldown + min-sessions, and it is the key the server groups a device's events under. A host\n * whose own identity is a user id passes that id here rather than reaching for a `user_id`\n * param the route does not declare.\n */\n deviceKey?: string;\n}\n\n/**\n * Best-effort fetch of the SERVER's review firing decision. The mirror of\n * `fetchQuestionnaireDecision`, and the primitive whose absence caused a live incident.\n *\n * ── WHY THIS LIVES IN THE KIT (Malik, 2026-07-16) ────────────────────────────────────────\n *\n * The questionnaire module has always had its decision fetch; reviews never did. So a host\n * hand-rolled one, and got it subtly wrong: its helper collapsed an explicit `{fire:false}`\n * into `undefined`. `decideReview` is `(local, decision) => decision ?? local`, so `undefined`\n * means \"the server has no opinion, use the local rules\" — and the local timer fired. The\n * server could therefore only ever turn review prompts ON, never OFF. A real user was asked to\n * rate the app ~3 minutes into their FIRST session, having seen nothing yet, and left 1 star.\n *\n * The bug was not that the host was careless. It was that the kit made every host invent this.\n * So the primitive moves here and hosts keep a thin call site.\n *\n * ── THE CONTRACT THAT MATTERS ────────────────────────────────────────────────────────────\n *\n * • 2xx → the FULL `{fire, reason, arm}`, INCLUDING `fire:false`. Never collapse a false\n * into null. That collapse IS the bug: a false must reach `decideReview` intact so\n * it can override the local rules and keep the gate shut.\n * • else → null, and ONLY then. Null means \"the server genuinely has no opinion\", which is\n * the one case where falling back to local rules is correct.\n *\n * Never throws: unreachable, non-2xx, bad JSON, or a missing `fetch` all resolve to null. The\n * kit does not call this internally; a host awaits it and passes the result straight to\n * `useReviewGate({ decision })`.\n *\n * ── DON'T RACE THIS AGAINST A LOCAL TIMER ────────────────────────────────────────────────\n *\n * The return type is `ReviewDecisionResponse`, which carries `arm` alongside `{fire, reason}`.\n * Hand the WHOLE object to the gate and echo `arm` into the submission's `meta.firing_arm`;\n * narrowing it to `{fire, reason}` on the way through silently kills per-arm attribution\n * across a reweighting of the experiment.\n *\n * A host that starts its own dwell timer in parallel with this fetch has built a race a slow\n * server loses: the timer fires, the local rules show the prompt, and the `{fire:false}` still\n * in flight arrives too late to stop it. Do not hand-roll that. `useReviewGate` already owns\n * the wait — set `ReviewConfig.timeoutFallbackMs` and the local rules stay parked until either\n * the decision lands or the window expires, whichever comes first.\n *\n * const decision = await fetchReviewDecision(target, { deviceKey });\n * const gate = useReviewGate({\n * config: { id: \"home\", minSessions: 2, timeoutFallbackMs: 3000 },\n * decision: decision ?? undefined, // pass it whole — keep `arm`\n * storage,\n * });\n */\nexport const fetchReviewDecision = async (\n target: ReviewTarget | undefined,\n options: FetchReviewDecisionOptions = {},\n): Promise<ReviewDecisionResponse | null> => {\n if (!target?.serverUrl) return null;\n try {\n const base = target.serverUrl.replace(/\\/$/, \"\");\n const params = new URLSearchParams();\n if (options.sessionId) params.set(\"session_id\", options.sessionId);\n if (options.deviceKey) params.set(\"device_key\", options.deviceKey);\n const qs = params.toString();\n const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : \"\"}`;\n const headers: Record<string, string> = {};\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n const res = await fetch(url, { headers });\n if (!res || !res.ok) return null;\n const json = (await res.json()) as ReviewDecisionResponse | null;\n // A body without a boolean `fire` is not a decision. Guard it explicitly rather than\n // letting `{}` through as a truthy object that `decideReview` would treat as a verdict\n // (`{}.fire === undefined` is falsy, so it would silently read as \"never fire\").\n if (!json || typeof json.fire !== \"boolean\") return null;\n return json;\n } catch {\n /* unreachable / non-2xx / bad JSON / missing-fetch → the server has no opinion */\n return null;\n }\n};\n\n/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */\nexport interface ReportAppEventOptions {\n /**\n * The onboarding/session id to correlate with, when known. Optional: when omitted the event\n * still carries the CURRENT per-open session id (`ensureCurrentSessionId()`), because an event\n * with no `session_id` is dropped server-side behind a 200. Pass one only to override.\n */\n sessionId?: string;\n /** A stable, non-PII device id — the review-decision endpoint reads it for min-sessions. */\n deviceKey?: string;\n /** Small non-PII extras. */\n meta?: Record<string, unknown>;\n}\n\n/**\n * Report a generic in-app event through the existing events transport. Stored server-side\n * as `event_type='app_event'`, `question_key=<name>`. Fire-and-forget. Keep `name` a short\n * stable identifier and `meta` small + non-PII.\n *\n * reportAppEvent(target, \"content_share\", { sessionId, deviceKey });\n *\n * ── `session_id` IS NON-NEGOTIABLE ON THE WIRE ───────────────────────────────────────────\n * The server's event model declares `session_id` required + non-empty, and `POST /v1/events`\n * validates per event inside a try/except that counts the failure as `skipped` and STILL returns\n * HTTP 200. An event sent without a `session_id` is therefore accepted and discarded, and a\n * fire-and-forget caller never finds out. This used to be reachable through the ordinary API:\n * `options.sessionId` was optional, so a host calling `reportAppEvent(target, \"screen\", { deviceKey })`\n * posted every screen view into that hole. So the id is no longer conditional — an explicit\n * `sessionId` wins, otherwise the CURRENT per-open id is used (minted + registered if no app-open\n * has been registered yet).\n */\nexport const reportAppEvent = (\n target: ReviewTarget | undefined,\n name: string,\n options: ReportAppEventOptions = {},\n): void => {\n if (!target?.serverUrl || !name) return;\n try {\n // An explicit id wins; a missing OR BLANK one falls back to the current per-open id. A bare\n // `??` would let `sessionId: \"\"` through, and the server rejects an empty string exactly like\n // a missing key (`min_length=1`), so the blank case has to fall back too.\n const supplied = options.sessionId ?? \"\";\n const event: Record<string, unknown> = {\n event_type: \"app_event\",\n question_key: name,\n session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId(),\n };\n // device_key rides in the non-PII user_context bucket the server sanitizes; the\n // review-decision endpoint reads it to group a device's sessions.\n if (options.deviceKey) event.user_context = { device_key: options.deviceKey };\n if (options.meta && Object.keys(options.meta).length > 0) {\n event.meta = JSON.stringify(options.meta);\n }\n // Route through the ONE canonical /v1/events builder (url + headers + body) instead of\n // re-describing the endpoint here; still fire-and-forget.\n const req = buildEventsRequest(target, [event as unknown as ClientEvent]);\n if (!req) return;\n void fetch(req.url, req.init).catch(() => {\n /* best-effort, swallow */\n });\n } catch {\n /* swallow */\n }\n};\n","/**\n * screenTracking — an OPT-IN, dependency-free automatic screen-view helper.\n *\n * Given the host app's navigation STATE (or a nav ref, via the useScreenTracking hook), this\n * emits exactly ONE `screen` app-event per REAL screen change through the kit's existing\n * `reportAppEvent` transport (`POST {serverUrl}/v1/events`, `event_type='app_event'`,\n * `question_key='screen'`, `meta={ screen }`). Param-only changes and re-renders are de-duped\n * away because we resolve and compare the route NAME only — never the params.\n *\n * Dependency-free core: this file imports NO navigation library. React-Navigation / expo-router\n * are host concerns; the host supplies plain state objects and refs, typed structurally here\n * (`NavigationStateLike`). It is also React-free — the optional React glue lives in\n * `useScreenTracking.ts`. `reportAppEvent` is imported from the pure `../reviews/transport`\n * module (NOT the `../reviews` barrel, which would drag the review UI into an analytics-only\n * bundle and defeat tree-shaking).\n *\n * Privacy: only the route NAME ever leaves the device. Route params, query strings, and any\n * user data are never read into the event. Fire-and-forget — this never throws into the UI.\n */\nimport { reportAppEvent } from \"../reviews/transport\";\n\n/** A single route inside a React-Navigation-shaped state (structural — no `@react-navigation`). */\nexport interface NavigationRouteLike {\n name: string;\n /** A nested navigator's own state, when this route hosts one. */\n state?: NavigationStateLike;\n /** Route params are intentionally left `unknown` — this helper never reads them. */\n params?: unknown;\n}\n\n/** A React-Navigation-shaped navigator state (structural type; no library import). */\nexport interface NavigationStateLike {\n /** Index of the active route within `routes`. */\n index?: number;\n routes?: NavigationRouteLike[];\n}\n\n/** Options for {@link createScreenTracker}. */\nexport interface ScreenTrackerOptions {\n /**\n * Where to POST. `{ serverUrl, apiKey }` — same shape the kit's review/analytics config\n * exposes. When omitted, the tracker still de-dups and fires `onScreen`, but sends nothing.\n */\n target?: { serverUrl: string; apiKey: string };\n /**\n * The onboarding/session id to correlate screen views with, when known. Omitting it no longer\n * means the view goes out WITHOUT a `session_id` (the server requires one and drops the event\n * behind an HTTP 200 — that is why screen tracking silently produced nothing for a host that\n * never mounted the lifecycle hook). `reportAppEvent` falls back to the current per-open id.\n */\n sessionId?: string;\n /** A stable, non-PII device id — groups a device's sessions server-side. */\n deviceKey?: string;\n /** Called on every REAL screen change (after de-dup), before the network emit. */\n onScreen?: (screen: string) => void;\n /**\n * Per-screen filter. Return `false` to skip the NETWORK emit for a screen (last-screen memory\n * is still advanced + `onScreen` still fires) — e.g. to keep a sensitive route out of analytics.\n */\n shouldTrack?: (screen: string) => boolean;\n}\n\n/** The screen tracker returned by {@link createScreenTracker}. */\nexport interface ScreenTracker {\n /** Report the active screen. Ignores `undefined`/empty and de-dups repeats of the last screen. */\n track: (screen: string | undefined) => void;\n /** Clear the last-screen memory (e.g. on logout) so the next `track` always emits. */\n reset: () => void;\n}\n\n/**\n * Walk a React-Navigation-shaped state to the DEEPEST active route and return its NAME (never\n * its params). Recurses `routes[index]` while a nested `.state` exists. Returns `undefined` for\n * a missing/empty/malformed state — the caller treats that as \"nothing to report\".\n */\nexport const getActiveRouteName = (\n state: NavigationStateLike | undefined,\n): string | undefined => {\n let current: NavigationStateLike | undefined = state;\n let name: string | undefined;\n // Bounded by the finite nesting depth of a real navigator tree.\n while (current && Array.isArray(current.routes) && current.routes.length > 0) {\n const index = typeof current.index === \"number\" ? current.index : 0;\n const route = current.routes[index];\n if (!route) break;\n name = route.name;\n current = route.state;\n }\n return name;\n};\n\n/**\n * Build a stateful screen tracker. `track` de-dups against the last reported screen so only a\n * REAL change emits; `reset` clears that memory. Fire-and-forget throughout — a missing `target`\n * skips the network but keeps the de-dup + `onScreen` behaviour intact.\n */\nexport const createScreenTracker = (options: ScreenTrackerOptions = {}): ScreenTracker => {\n let lastScreen: string | undefined;\n return {\n track: (screen: string | undefined): void => {\n if (!screen || screen === lastScreen) return;\n lastScreen = screen;\n options.onScreen?.(screen);\n if (options.shouldTrack && !options.shouldTrack(screen)) return;\n reportAppEvent(options.target, \"screen\", {\n sessionId: options.sessionId,\n deviceKey: options.deviceKey,\n meta: { screen },\n });\n },\n reset: (): void => {\n lastScreen = undefined;\n },\n };\n};\n\n/**\n * Adapt a tracker into a React-Navigation `onStateChange` handler — the one-place wiring:\n *\n * <NavigationContainer onStateChange={screenTrackingHandler(tracker)}>\n *\n * It resolves the deepest active route name and hands it to `tracker.track` (which de-dups).\n */\nexport const screenTrackingHandler =\n (tracker: ScreenTracker) =>\n (state: NavigationStateLike | undefined): void =>\n tracker.track(getActiveRouteName(state));\n","/**\n * useScreenTracking — a THIN optional React hook over the pure screen-tracking core.\n *\n * It builds one tracker for the component's lifetime and subscribes to the host's navigation\n * ref: it reports the current route on mount and on every `state` event, and unsubscribes on\n * unmount. React is a REQUIRED peer of the kit, so importing it here is allowed; the hook adds\n * NO navigation-library dependency — the ref is typed structurally (`NavigationRefLike`).\n *\n * const navigationRef = useNavigationContainerRef(); // host's @react-navigation ref\n * useScreenTracking(navigationRef, { target, sessionId });\n * // ...<NavigationContainer ref={navigationRef}>\n */\nimport { useEffect, useRef } from \"react\";\n\nimport { createScreenTracker, type ScreenTracker, type ScreenTrackerOptions } from \"./screenTracking\";\n\n/**\n * The structural slice of a React-Navigation container ref this hook needs — no\n * `@react-navigation` import. `getCurrentRoute` yields the active route; `addListener(\"state\", …)`\n * fires on every navigation state change and returns its own unsubscribe.\n */\nexport interface NavigationRefLike {\n getCurrentRoute?: () => { name?: string } | undefined;\n addListener?: (type: \"state\", callback: () => void) => () => void;\n}\n\n/**\n * Subscribe screen tracking to a host navigation ref. Safe to call with a not-yet-ready ref\n * (the effect no-ops until `addListener` exists). Returns nothing — it wires side effects only.\n */\nexport const useScreenTracking = (\n navigationRef: NavigationRefLike | undefined,\n options: ScreenTrackerOptions = {},\n): void => {\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // One tracker per mount; kept in a ref so re-renders never rebuild the de-dup memory.\n const trackerRef = useRef<ScreenTracker | undefined>(undefined);\n if (!trackerRef.current) {\n const dynamicOptions: ScreenTrackerOptions = {\n get target() {\n return optionsRef.current.target;\n },\n get sessionId() {\n return optionsRef.current.sessionId;\n },\n get deviceKey() {\n return optionsRef.current.deviceKey;\n },\n get onScreen() {\n return optionsRef.current.onScreen;\n },\n get shouldTrack() {\n return optionsRef.current.shouldTrack;\n },\n };\n trackerRef.current = createScreenTracker(dynamicOptions);\n }\n\n useEffect(() => {\n const tracker = trackerRef.current;\n if (!tracker || !navigationRef?.addListener) return;\n const report = (): void => tracker.track(navigationRef.getCurrentRoute?.()?.name);\n report(); // initial screen on mount\n const unsubscribe = navigationRef.addListener(\"state\", report);\n return unsubscribe;\n // Re-subscribe only when the ref identity changes; option changes are read live off the closure.\n }, [navigationRef]);\n};\n","/**\n * wireDoctor: a DEV-ONLY, opt-in self-check that proves a fresh integration end to end before the\n * first real user ever runs it.\n *\n * WHY IT EXISTS: the `/v1/events` endpoint answers HTTP **200** for a batch it throws away. It\n * reports the refusal in the response body (`{written, skipped, errors:[{reason, field}]}`), so a\n * mis-wired integration looks perfectly healthy from the outside while every event evaporates. A\n * numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you\n * discover in a funnel report weeks later into something the first run tells you.\n *\n * SHAPE: four independent checks, each a `{name, ok, detail}` unit that can be read (and tested)\n * without the others. The report is data, never a thrown error and never a side effect on the host:\n *\n * 1. `target` : is there a server URL and a key, and do they look like a key and a URL?\n * 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)\n * 3. `storage` : can the offline queue persist? (a write / read / delete probe)\n * 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)\n *\n * NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor\n * that can crash the screen it is diagnosing is worse than no doctor.\n *\n * ⛔ NEVER PRINTS A SECRET. The `target` check reports the key's SHAPE (present / absent, length,\n * whether the prefix is the expected one) and never any character of the key itself, in any\n * `detail`, any log line, or any error. `wireDoctor.test.ts` asserts that with a sentinel key.\n *\n * COSTS A NON-CALLER NOTHING: it is a plain function behind the `./analytics` subpath, the package\n * is `sideEffects: false`, and this module has no top-level side effects, so a host that never\n * imports it never bundles it. It also mints no `globalThis` slot and retains no timer or listener.\n *\n * USAGE (dev builds only):\n *\n * import { wireDoctor } from \"@wireai/activation/analytics\";\n *\n * const report = await wireDoctor({\n * target: { serverUrl: \"https://api.example.com\", apiKey: DRIVELINE_KEY },\n * storage: AsyncStorage,\n * });\n * console.log(report.ok, report.checks);\n */\n// Imported DIRECTLY from the transport module, never through `./index`: the barrel would pull the\n// whole analytics surface into anything that touches the doctor. `readEventsAck` is module-exported\n// for exactly this, so the doctor reads an ack with the SAME reader the send paths use rather than\n// growing a second one to drift.\nimport {\n buildEventsRequest,\n makeSessionId,\n readEventsAck,\n type ClientEvent,\n type ClientEventTarget,\n} from \"./reportClientEvent\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\n\n/** RN sets this global; absent under node/SSR. Read defensively, exactly as `warnOnSkippedEvents` does. */\ndeclare const __DEV__: boolean | undefined;\n\n/** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */\nexport type WireDoctorCheck = {\n /**\n * Stable id: `target` | `reachability` | `storage` | `round_trip` | `dev_only` | `internal_error`.\n *\n * `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.\n * `internal_error` is the separate catch-all for a failure that got past every check's own\n * swallow. They are distinct names because a consumer branching on `dev_only` would otherwise\n * read an internal fault as \"this is a release build\" and report a healthy skip.\n */\n name: string;\n ok: boolean;\n /** Human-readable result. ⛔ Never contains any part of an API key. */\n detail: string;\n};\n\n/** What {@link wireDoctor} resolves to. `ok` is true only when EVERY check passed. */\nexport type WireDoctorReport = {\n ok: boolean;\n checks: WireDoctorCheck[];\n};\n\n/** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */\nexport type WireDoctorOptions = {\n /** The same `{serverUrl, apiKey}` the kit is configured with. */\n target: ClientEventTarget | undefined;\n /**\n * The host storage the offline queue would use (AsyncStorage-compatible). Omit it and the storage\n * check reports the DEGRADED in-memory mode rather than failing.\n */\n storage?: WireOnboardingStorage;\n};\n\n/**\n * The probe's own storage key. Deliberately NOT under the queue's `wireai:evtq:` namespace, so a\n * doctor run can never read, overwrite or delete a pending backlog. A diagnostic that eats a user's\n * unsent events is a worse bug than the one it was written to find.\n */\nconst PROBE_STORAGE_KEY = \"wireai:doctor:probe\";\n\n/** The `question_key` the synthetic round-trip event carries. `dry_run` means it is never written. */\nconst PROBE_QUESTION_KEY = \"wire_doctor_probe\";\n\n/** Tenant keys start with this. Used ONLY for a boolean comparison; ⛔ never interpolated into a detail. */\nconst EXPECTED_KEY_PREFIX = \"wai_\";\n\n/** Ceiling on each network check, so a hung server degrades to a failed check, never a hung caller. */\nconst NETWORK_TIMEOUT_MS = 10_000;\n\nconst check = (name: string, ok: boolean, detail: string): WireDoctorCheck => ({ name, ok, detail });\n\n/**\n * `fetch` with a timeout that is ALWAYS cleared, including on rejection. Resolves `undefined`\n * instead of throwing, so every caller stays on the happy path. Returns no timer to the caller and\n * leaves none pending: a diagnostic must not keep the JS thread alive after it has answered.\n */\nconst fetchWithTimeout = async (url: string, init?: RequestInit): Promise<Response | undefined> => {\n if (typeof fetch === \"undefined\") return undefined;\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : undefined;\n const timer = setTimeout(() => controller?.abort(), NETWORK_TIMEOUT_MS);\n try {\n return await fetch(url, controller ? { ...init, signal: controller.signal } : init);\n } catch {\n // Unreachable host, DNS failure, abort, or a missing fetch implementation.\n return undefined;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/**\n * CHECK 1: is the target usable at all?\n *\n * Reports the key's shape and NEVER its content: present/absent, character length, and whether the\n * prefix matches what a tenant key starts with. Length and a yes/no are enough to tell \"you pasted\n * the wrong string\" from \"you pasted nothing\", which is the entire diagnostic value here.\n */\nconst checkTarget = (target: ClientEventTarget | undefined): WireDoctorCheck => {\n const serverUrl = target?.serverUrl;\n if (typeof serverUrl !== \"string\" || serverUrl.trim().length === 0) {\n return check(\"target\", false, \"no serverUrl configured: set `serverUrl` on the kit config.\");\n }\n let parsedHost = false;\n try {\n const parsed = new URL(serverUrl);\n parsedHost = parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n } catch {\n parsedHost = false;\n }\n if (!parsedHost) {\n return check(\"target\", false, \"serverUrl is not a valid http(s) URL.\");\n }\n const apiKey = target?.apiKey;\n if (typeof apiKey !== \"string\" || apiKey.length === 0) {\n return check(\"target\", false, \"serverUrl looks valid, but no apiKey is configured.\");\n }\n // ⛔ `EXPECTED_KEY_PREFIX` is compared, never printed: it is itself a substring of a real key.\n const prefixOk = apiKey.startsWith(EXPECTED_KEY_PREFIX);\n return check(\n \"target\",\n true,\n `serverUrl is a valid http(s) URL; apiKey present (${apiKey.length} chars, expected prefix: ${\n prefixOk ? \"yes\" : \"no\"\n }).`,\n );\n};\n\n/**\n * CHECK 2: is the server there, and is it a Wire server?\n *\n * `GET /v1/events/contract` is public, cheap, and it answers the question the round-trip check\n * cannot answer on its own: a failure here means \"wrong URL / server down\", not \"bad payload\".\n */\nconst checkReachability = async (serverUrl: string): Promise<WireDoctorCheck> => {\n const url = `${serverUrl.replace(/\\/$/, \"\")}/v1/events/contract`;\n const res = await fetchWithTimeout(url, { method: \"GET\" });\n if (!res) {\n return check(\"reachability\", false, \"could not reach the server (network error or timeout).\");\n }\n const status = (res as { status?: unknown }).status;\n const ok = !!(res as { ok?: boolean }).ok;\n if (!ok) {\n return check(\n \"reachability\",\n false,\n `the server answered ${typeof status === \"number\" ? status : \"an error\"} for the events contract; ` +\n \"check the serverUrl points at a Wire server.\",\n );\n }\n return check(\"reachability\", true, \"the server answered the events contract.\");\n};\n\n/**\n * CHECK 3: can the offline queue actually persist?\n *\n * Write, read back, compare. Without working storage the queue silently degrades to in-memory only,\n * so a backgrounded app loses whatever it had not flushed, and nothing anywhere says so.\n *\n * The probe key is REMOVED in a `finally`, so a failing read or a throwing adapter still cleans up.\n */\nconst checkStorage = async (storage: WireOnboardingStorage | undefined): Promise<WireDoctorCheck> => {\n if (!storage) {\n return check(\n \"storage\",\n true,\n \"no storage injected: the queue runs in DEGRADED in-memory mode and loses pending events on an app kill. \" +\n \"Pass AsyncStorage (or an MMKV wrapper) to make it durable.\",\n );\n }\n const token = `probe_${Date.now().toString(36)}`;\n try {\n await storage.setItem(PROBE_STORAGE_KEY, token);\n const read = await storage.getItem(PROBE_STORAGE_KEY);\n if (read !== token) {\n return check(\n \"storage\",\n false,\n \"the storage adapter accepted a write but did not read the same value back, so the queue cannot persist.\",\n );\n }\n // ⚠️ \"write / read\" only: this returns BEFORE the `finally` runs its delete, and a throwing\n // `removeItem` is deliberately swallowed there, so the delete is attempted, never asserted.\n return check(\"storage\", true, \"storage write / read round trip succeeded.\");\n } catch {\n return check(\"storage\", false, \"the storage adapter threw, so the queue cannot persist events.\");\n } finally {\n // Always, including after a failed write or read: never leave the probe key behind.\n try {\n await storage.removeItem(PROBE_STORAGE_KEY);\n } catch {\n // A remove that throws is not worth failing the run over; nothing else depends on it.\n }\n }\n};\n\n/**\n * CHECK 4: does a real event survive real server validation?\n *\n * POSTs ONE synthetic event through the REAL {@link buildEventsRequest} with `dry_run: true`, so it\n * travels the exact bytes a production event travels and the server validates it exactly the same\n * way, but writes nothing. The probe is inert: it pollutes no funnel and no metric.\n *\n * ⚠️ The verdict is `written === 1 && skipped === 0`, read from the ACK BODY. A 200 is not a\n * receipt here: the endpoint returns 200 for a batch it discarded. A `ts`-shaped drift shows up as\n * `skipped: 1` with the server's own `field: \"ts\"`, which is the whole point of this check.\n */\nconst checkRoundTrip = async (target: ClientEventTarget): Promise<WireDoctorCheck> => {\n const event: ClientEvent = {\n event_type: \"app_event\",\n session_id: makeSessionId(),\n question_key: PROBE_QUESTION_KEY,\n };\n const req = buildEventsRequest(target, [event], { dryRun: true });\n if (!req) {\n return check(\"round_trip\", false, \"could not build the events request from this target.\");\n }\n const res = await fetchWithTimeout(req.url, req.init);\n if (!res) {\n return check(\"round_trip\", false, \"the events POST failed (network error or timeout).\");\n }\n if (!(res as { ok?: boolean }).ok) {\n const status = (res as { status?: unknown }).status;\n return check(\n \"round_trip\",\n false,\n `the events endpoint answered ${typeof status === \"number\" ? status : \"an error\"}; ` +\n \"a 401 here means the apiKey is not accepted.\",\n );\n }\n const ack = await readEventsAck(res);\n if (!ack) {\n return check(\n \"round_trip\",\n false,\n \"the server answered 2xx but sent no readable ack body, so it is unknown whether the event validated.\",\n );\n }\n if (ack.written === 1 && ack.skipped === 0) {\n return check(\"round_trip\", true, \"the server validated the probe event: written=1, skipped=0.\");\n }\n // The server's own reasons, verbatim, each already carrying the field it refused.\n return check(\n \"round_trip\",\n false,\n `the server DISCARDED the probe event: written=${ack.written ?? \"unknown\"}, skipped=${ack.skipped}` +\n (ack.reasons.length > 0\n ? `, reasons: ${ack.reasons.join(\", \")}.`\n : \". It gave no reason; check the server's ingest log for this request.\"),\n );\n};\n\n/**\n * Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.\n *\n * DEV-ONLY BY CONTRACT: the `__DEV__` guard is the FIRST statement, so a release build performs no\n * network call, no storage write, and no work at all. The report says it was skipped rather than\n * pretending everything passed, because a green report that never ran is the exact failure mode\n * this whole feature exists to remove.\n */\nexport const wireDoctor = async (options: WireDoctorOptions): Promise<WireDoctorReport> => {\n if (typeof __DEV__ === \"undefined\" || !__DEV__) {\n return {\n ok: false,\n checks: [\n check(\n \"dev_only\",\n false,\n \"wireDoctor is dev-only and did NOT run: __DEV__ is unset or false. Nothing was checked.\",\n ),\n ],\n };\n }\n try {\n const target = options?.target;\n const checks: WireDoctorCheck[] = [checkTarget(target)];\n // Reachability and the round trip both need a usable target; running them against a broken one\n // would report a network failure and bury the real cause, which check 1 already named.\n if (checks[0]!.ok && target) {\n checks.push(await checkReachability(target.serverUrl));\n checks.push(await checkStorage(options?.storage));\n checks.push(await checkRoundTrip(target));\n } else {\n checks.push(await checkStorage(options?.storage));\n }\n return { ok: checks.every((c) => c.ok), checks };\n } catch {\n // Belt and braces: every check above already swallows its own failures, so reaching here means\n // something hostile got past them. Still a report, still not a throw. The name is\n // `internal_error`, NOT `dev_only`: this ran and broke, which is the opposite of \"never ran\".\n return {\n ok: false,\n checks: [check(\"internal_error\", false, \"wireDoctor could not complete: an unexpected error was swallowed.\")],\n };\n }\n};\n","/**\n * permissionEvents - the canonical Wire names for a permission-priming funnel.\n *\n * Same job `purchaseEvents.ts` does for the subscription funnel and `analyticsEvent.ts` does for\n * the onboarding funnel: every app was naming these itself (`push_permission`, `NOTIF_PROMPT`,\n * `notifications_allowed`), so the same funnel read differently per tenant and no cross-app report\n * was possible. These are the ONE set of names.\n *\n * They are `app_event` `question_key` values on the wire, exactly like `WIRE_PURCHASE_EVENTS`, so\n * they are ALSO the exact strings a review / questionnaire firing trigger matches on. Never rename\n * one: a rename silently unfires every trigger configured against the old string.\n *\n * PURE + dependency-free: no React, no React Native, no transport, no imports outside the type\n * declarations - so an analytics-only bundle can carry the names without carrying a screen.\n */\nimport type { PermissionStage, WirePermissionKind, WirePermissionStatus } from \"./types\";\n\nexport const WIRE_PERMISSION_EVENTS = {\n /** The priming screen became visible. The denominator for every rate below. */\n screenShown: \"wire_permission_screen_shown\",\n /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */\n primerAccepted: \"wire_permission_primer_accepted\",\n /** The OS granted it. */\n granted: \"wire_permission_granted\",\n /** The OS refused it (a permanently blocked answer reports here too, with `status: \"blocked\"`). */\n denied: \"wire_permission_denied\",\n /** The user took the secondary. The one native prompt was NOT spent. */\n skipped: \"wire_permission_skipped\",\n /** A blocked user was redirected to the OS settings page. */\n settingsOpened: \"wire_permission_settings_opened\",\n} as const;\n\nexport type WirePermissionEventName =\n (typeof WIRE_PERMISSION_EVENTS)[keyof typeof WIRE_PERMISSION_EVENTS];\n\n/** The canonical event name for a stage. Exhaustive over the union (a new stage is a compile error). */\nexport const permissionEventName = (stage: PermissionStage): WirePermissionEventName => {\n switch (stage) {\n case \"shown\":\n return WIRE_PERMISSION_EVENTS.screenShown;\n case \"accepted\":\n return WIRE_PERMISSION_EVENTS.primerAccepted;\n case \"granted\":\n return WIRE_PERMISSION_EVENTS.granted;\n case \"denied\":\n return WIRE_PERMISSION_EVENTS.denied;\n case \"skipped\":\n return WIRE_PERMISSION_EVENTS.skipped;\n case \"settings\":\n return WIRE_PERMISSION_EVENTS.settingsOpened;\n default: {\n const _exhaustive: never = stage;\n return _exhaustive;\n }\n }\n};\n\n/**\n * The small, non-PII props that ride a permission event. `permission` is always present so one\n * funnel can be sliced per permission; `status` only appears when the OS actually answered, which\n * is what keeps a `blocked` refusal distinguishable from a plain `denied` without a second event.\n */\nexport const permissionEventProps = (\n permission: WirePermissionKind,\n status?: WirePermissionStatus,\n): Record<string, string> => (status ? { permission, status } : { permission });\n\n/**\n * Normalize whatever the host's `request` / `getStatus` actually returned.\n *\n * A native permission bridge is the host's code, and hosts return `\"undetermined\"`, `true`, or a\n * whole Expo response object. Anything the kit does not recognise is treated as `denied`: it is the\n * only reading that cannot invent a grant, and every outcome continues the flow anyway.\n */\nexport const normalizePermissionStatus = (value: unknown): WirePermissionStatus =>\n value === \"granted\" || value === \"denied\" || value === \"blocked\" ? value : \"denied\";\n","/**\n * Canonical analytics names for the onboarding funnel. The kit already emits a typed\n * `OnboardingEvent` (`started | turn | error | retry | fallback`) — but one app logged them as\n * `onboarding_*` and another as `AI_ONBOARDING_*`, so the same funnel reads differently per app.\n * This maps the kit event to ONE canonical `wire_onboarding_*` name + params, and the app logs\n * it through whatever transport it already has (Firebase, Amplitude, console). The app still\n * owns the logger; only the NAMES are standardized.\n *\n * <WireOnboarding\n * onEvent={(e) => { const a = toAnalyticsEvent(e); logEvent(a.name, a.params); }}\n * onComplete={(r) => { logEvent(WIRE_ONBOARDING_EVENTS.completed, { answers: Object.keys(r.answers).length }); persist(r); }}\n * />\n *\n * `completed` has no kit `OnboardingEvent` (the kit signals completion via `onComplete`, not\n * `onEvent`) — the app logs it explicitly on `onComplete` using the constant below, so the\n * funnel name stays canonical.\n */\nimport {\n permissionEventName,\n permissionEventProps,\n type WirePermissionEventName,\n} from \"../permissions/permissionEvents\";\nimport type { OnboardingEvent } from \"../types\";\n\nexport const WIRE_ONBOARDING_EVENTS = {\n started: \"wire_onboarding_started\",\n /** A persisted session was restored after an app kill (fires instead of `started`). */\n resumed: \"wire_onboarding_resumed\",\n turn: \"wire_onboarding_turn\",\n error: \"wire_onboarding_error\",\n retry: \"wire_onboarding_retry\",\n fallback: \"wire_onboarding_fallback\",\n /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */\n completed: \"wire_onboarding_completed\",\n} as const;\n\nexport type WireOnboardingEventName =\n (typeof WIRE_ONBOARDING_EVENTS)[keyof typeof WIRE_ONBOARDING_EVENTS];\n\nexport type AnalyticsEvent = {\n /**\n * A permission screen maps to its own canonical `wire_permission_*` name rather than to an\n * onboarding one: it is a distinct funnel (see `permissions/permissionEvents.ts`), and folding it\n * into `wire_onboarding_turn` would make every permission rate unreadable.\n */\n name: WireOnboardingEventName | WirePermissionEventName;\n params?: Record<string, unknown>;\n};\n\n/**\n * Map a kit `OnboardingEvent` to its canonical `{ name, params }`. Exhaustive over the union\n * (the `never` default makes a new event type a compile error here — intentional).\n */\nexport const toAnalyticsEvent = (event: OnboardingEvent): AnalyticsEvent => {\n switch (event.type) {\n case \"started\":\n return { name: WIRE_ONBOARDING_EVENTS.started };\n case \"resumed\":\n return { name: WIRE_ONBOARDING_EVENTS.resumed };\n case \"turn\":\n return {\n name: WIRE_ONBOARDING_EVENTS.turn,\n params: { step: event.step, component: event.component },\n };\n case \"error\":\n return { name: WIRE_ONBOARDING_EVENTS.error, params: { reason: event.reason } };\n case \"retry\":\n return {\n name: WIRE_ONBOARDING_EVENTS.retry,\n params: { reason: event.reason, attempt: event.attempt },\n };\n case \"fallback\":\n return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };\n case \"permission\":\n return {\n name: permissionEventName(event.stage),\n params: permissionEventProps(event.permission, event.status),\n };\n default: {\n const _exhaustive: never = event;\n return _exhaustive;\n }\n }\n};\n","/**\n * appVersion — best-effort, DEPENDENCY-FREE auto-detection of the host app's version string.\n *\n * WHY this exists: analytics segments the funnel `by_app_version`, but that breakdown is only\n * populated when a `device.appVersion` rides the event. `config.appVersion` (see types.ts) has\n * always been the way to supply it — but it is easy for a host to forget, and then the release\n * breakdown is silently empty. This module fills that gap: when the host does NOT pass a version,\n * the kit makes a best-effort read of the app version the host already ships in its Expo config,\n * so the breakdown works out of the box. An explicit `config.appVersion` always WINS over this.\n *\n * WHY it adds NO dependency (the kit's hard rule): `expo-constants` / `expo-application` are read\n * through a GUARDED require with a STRING-LITERAL specifier, inside a try/catch. Both halves are\n * required: the literal is what lets Metro COLLECT the dep, and the try/catch is what marks it\n * OPTIONAL, so an absent module becomes a `null` dependencyMap entry whose require throws \"Cannot\n * find module\" straight into the catch. (`withWireOnboarding` in metro/index.js adds a stub-to-\n * empty-module safety net for hosts that disable Metro's `allowOptionalDependencies`.) Nothing is\n * added to `package.json`; nothing is forced on the host.\n *\n * The specifier MUST stay a literal. A variable specifier (`const req = require; req(name)`) is not\n * \"lazier\" — it is broken: Metro collects dependencies statically, so a variable collects NOTHING,\n * the module never enters the bundle, and the aliased `require` is Metro's own numeric-id-keyed\n * `metroRequire`, which can never resolve a package-name string. That shape is why this function\n * returned `undefined` on every host despite `expo-constants` being installed, which silently\n * emptied the `by_app_version` breakdown this module exists to fill. See icons/expoIcons.ts for\n * the full autopsy, and reviews/storeReview.ts for the literal-specifier counter-example.\n *\n * PRIVACY: an app version string is not PII and identifies no user or device, so surfacing it\n * changes no App Privacy / Data Safety declaration (same guarantee as the rest of deviceContext).\n *\n * NEVER THROWS: every read is guarded; a missing/odd value yields `undefined`, never an exception.\n * Analytics must never be able to break onboarding.\n */\n\n// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime. Declared locally so\n// this type-checks without ambient Node types; the `typeof` guard keeps the reference ESM-safe.\ndeclare const require: ((id: string) => unknown) | undefined;\n\n/** A `require`-like resolver. Injectable in tests; production uses the guarded runtime require. */\nexport type OptionalRequire = (moduleName: string) => unknown;\n\n/** Trim + reject non-strings/empties so we only ever emit a real version string. */\nexport const coerceVersion = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Guarded runtime require.\n *\n * Every specifier below is a STRING LITERAL, because that is the only shape Metro's dependency\n * collector matches (callee literally `require`, argument literally a string). The `moduleName`\n * parameter exists only to keep the `OptionalRequire` seam shape for tests; an unrecognised name\n * resolves to undefined. See the header for why the old variable-specifier shape could not work.\n */\nconst runtimeRequire: OptionalRequire = (moduleName) => {\n if (typeof require !== \"function\") return undefined;\n try {\n switch (moduleName) {\n case \"expo-constants\":\n return require(\"expo-constants\");\n case \"expo-application\":\n return require(\"expo-application\");\n default:\n return undefined;\n }\n } catch {\n return undefined;\n }\n};\n\n/** Read a module's `default` (Expo modules are consumed as default exports) or the namespace. */\nconst interop = (mod: unknown): Record<string, unknown> | undefined => {\n if (!mod || typeof mod !== \"object\") return undefined;\n const def = (mod as { default?: unknown }).default;\n if (def && typeof def === \"object\") return def as Record<string, unknown>;\n return mod as Record<string, unknown>;\n};\n\n/** Resolve a module namespace, swallowing a throwing require (an uninstalled module throws). */\nconst safeInterop = (\n requireModule: OptionalRequire,\n moduleName: string,\n): Record<string, unknown> | undefined => {\n try {\n return interop(requireModule(moduleName));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Detect the host app version, preferring `expo-constants` (`expoConfig.version`, then\n * `nativeAppVersion`) and finally `expo-application` (`nativeApplicationVersion`). Returns the\n * first real string, or `undefined` when none of those are available. Pure and never throws.\n *\n * `requireModule` is injectable so tests can exercise the \"found\" path without the native modules;\n * production defaults to the guarded runtime require above.\n */\nexport const detectAppVersion = (\n requireModule: OptionalRequire = runtimeRequire,\n): string | undefined => {\n try {\n const constants = safeInterop(requireModule, \"expo-constants\");\n if (constants) {\n const expoConfig = constants.expoConfig;\n if (expoConfig && typeof expoConfig === \"object\") {\n const fromExpoConfig = coerceVersion((expoConfig as { version?: unknown }).version);\n if (fromExpoConfig) return fromExpoConfig;\n }\n const fromNative = coerceVersion(constants.nativeAppVersion);\n if (fromNative) return fromNative;\n }\n\n const application = safeInterop(requireModule, \"expo-application\");\n if (application) {\n const fromApplication = coerceVersion(application.nativeApplicationVersion);\n if (fromApplication) return fromApplication;\n }\n } catch {\n // Any unexpected read error → \"unknown\"; analytics must never crash onboarding.\n }\n return undefined;\n};\n","/**\n * deviceModel — best-effort, DEPENDENCY-FREE detection of the device MODEL on iOS.\n *\n * WHY this exists (the asymmetry it closes): `collectDeviceContext` already reports `device.model`\n * on Android straight from `Platform.constants.Model` (e.g. \"SM-G991B\"), but iOS `Platform.constants`\n * exposes NO model — only `osVersion` / `interfaceIdiom`. So the analytics `by_model` breakdown was\n * Android-only. This module fills the iOS gap with the SAME zero-dependency technique the kit already\n * uses for `appVersion` (see device/appVersion.ts): a GUARDED, STRING-LITERAL `require` of the common\n * Expo `expo-device` module, inside a try/catch — the literal is what lets Metro COLLECT the dep and\n * the try/catch is what marks it OPTIONAL, so an absent module degrades instead of failing the build.\n * (`withWireOnboarding` in metro/index.js adds a stub-to-empty-module safety net for hosts that\n * disable Metro's `allowOptionalDependencies`.) If the host has it, iOS gets a model out of the box;\n * if not, the read yields `undefined` and iOS `model` stays omitted — nothing is forced, and nothing\n * is added to `package.json`.\n *\n * The specifier MUST stay a literal. A variable specifier (`const req = require; req(name)`) collects\n * NOTHING under Metro's static dependency collector, so the module never enters the bundle and the\n * aliased `require` — really Metro's numeric-id-keyed `metroRequire` — can never resolve a package\n * name. See icons/expoIcons.ts for the full autopsy.\n *\n * WHY it is NOT PII: `expo-device`'s `modelName` (\"iPhone 14 Pro\") and `modelId` (\"iPhone15,2\") are a\n * device CLASS shared by millions of units — the same privacy category as the Android `Model` the kit\n * already sends. It is NOT a unique device id / IDFA / fingerprint, so surfacing it changes no App\n * Privacy / Data Safety declaration (identical guarantee to the rest of deviceContext). It deliberately\n * does NOT read `expo-device`'s `deviceName` (that is the user-set name, e.g. \"Malik's iPhone\", and IS\n * personal data).\n *\n * NEVER THROWS: every read is guarded; a missing/odd value yields `undefined`, never an exception.\n */\n\n// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime. Declared locally so\n// this type-checks without ambient Node types; the `typeof` guard keeps the reference ESM-safe.\ndeclare const require: ((id: string) => unknown) | undefined;\n\n/** A `require`-like resolver. Injectable in tests; production uses the guarded runtime require. */\nexport type OptionalRequire = (moduleName: string) => unknown;\n\n/** Trim + reject non-strings/empties so we only ever emit a real model string. */\nconst coerceModel = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Guarded runtime require.\n *\n * The specifier is a STRING LITERAL, because that is the only shape Metro's dependency collector\n * matches (callee literally `require`, argument literally a string). The `moduleName` parameter\n * exists only to keep the `OptionalRequire` seam shape for tests; an unrecognised name resolves to\n * undefined. (See the twin guard in ./appVersion.ts.)\n */\nconst runtimeRequire: OptionalRequire = (moduleName) => {\n if (moduleName !== \"expo-device\") return undefined;\n if (typeof require !== \"function\") return undefined;\n try {\n return require(\"expo-device\");\n } catch {\n return undefined;\n }\n};\n\n/** Read a module's `default` (Expo modules are often consumed as default exports) or the namespace. */\nconst interop = (mod: unknown): Record<string, unknown> | undefined => {\n if (!mod || typeof mod !== \"object\") return undefined;\n const def = (mod as { default?: unknown }).default;\n if (def && typeof def === \"object\") return def as Record<string, unknown>;\n return mod as Record<string, unknown>;\n};\n\n/** Resolve a module namespace, swallowing a throwing require (an uninstalled module throws). */\nconst safeInterop = (\n requireModule: OptionalRequire,\n moduleName: string,\n): Record<string, unknown> | undefined => {\n try {\n return interop(requireModule(moduleName));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Detect the device model via `expo-device`, preferring the human-readable `modelName`\n * (\"iPhone 14 Pro\") and falling back to the identifier `modelId` (\"iPhone15,2\"). Returns the first\n * real string, or `undefined` when `expo-device` is absent. Pure and never throws.\n *\n * `requireModule` is injectable so tests can exercise the \"found\" path without the native module;\n * production defaults to the guarded runtime require above.\n */\nexport const detectNativeModel = (\n requireModule: OptionalRequire = runtimeRequire,\n): string | undefined => {\n try {\n const device = safeInterop(requireModule, \"expo-device\");\n if (device) {\n const modelName = coerceModel(device.modelName);\n if (modelName) return modelName;\n const modelId = coerceModel(device.modelId);\n if (modelId) return modelId;\n }\n } catch {\n // Any unexpected read error → undefined; analytics must never crash onboarding.\n }\n return undefined;\n};\n","/**\n * deviceContext — collect a small, privacy-label-neutral snapshot of the device so\n * onboarding analytics can segment the funnel (platform / form factor / locale) WITHOUT\n * adding a single dependency to the kit or changing a host app's App Privacy / Data Safety\n * declarations.\n *\n * HARD RULE (why this file adds no dependency):\n * The kit stays dependency-free. Everything here comes from `Platform`, `Dimensions`,\n * `I18nManager`, and the standard `Intl` global — plus a best-effort `appVersion` read via\n * `detectAppVersion()`, which itself adds NO dependency (it reaches for `expo-constants` /\n * `expo-application` through a guarded, variable-specifier require that a host without them\n * simply never resolves — see device/appVersion.ts). There are NO advertising IDs, NO\n * `getUniqueId`/IDFA/GAID/fingerprinting APIs, and nothing that would require a new\n * privacy-label entry. A host can adopt this without touching its store declarations.\n *\n * DEFENSIVE BY DESIGN: `collectDeviceContext()` never throws. Every read is guarded and\n * a missing/unavailable field is simply omitted (Hermes may ship without full `Intl`,\n * `Platform.constants` differs per OS and RN version, etc.). Analytics must never be able\n * to break onboarding.\n */\nimport { Dimensions, I18nManager, Platform } from \"react-native\";\n\nimport { detectAppVersion } from \"./appVersion\";\nimport { detectNativeModel } from \"./deviceModel\";\n\n/** Coarse device class. iOS uses the reported interface idiom; else a screen-size heuristic. */\nexport type DeviceFormFactor = \"phone\" | \"tablet\";\n\n/**\n * A privacy-label-neutral device snapshot. ALL fields except `platform` are optional and are\n * omitted when unavailable. Nothing here identifies a user or device uniquely.\n */\nexport type DeviceContext = {\n /** `Platform.OS` — \"ios\" | \"android\" | \"windows\" | \"macos\" | \"web\". Always present. */\n platform: typeof Platform.OS;\n /** OS version string (iOS `osVersion`/`Platform.Version`, Android `Release`). */\n osVersion?: string;\n /** Android device brand (e.g. \"samsung\"). Android only. */\n brand?: string;\n /**\n * Device model. On Android it is read directly from `Platform.constants.Model` (e.g. \"SM-G991B\").\n * iOS `Platform.constants` exposes NO model, so on iOS it is a BEST-EFFORT read of `expo-device`'s\n * `modelName` (\"iPhone 14 Pro\"), falling back to `modelId` (\"iPhone15,2\") — dependency-free via a\n * guarded require (see device/deviceModel.ts). ASYMMETRY: without `expo-device` installed, iOS\n * `model` is omitted (there is no dependency-free iOS model in the already-used RN surface, and the\n * kit will not add a native dep for it); Android needs no extra module. Not PII (a device class,\n * not a unique id), so it changes no privacy-label declaration.\n */\n model?: string;\n /** iOS interface idiom (\"phone\" | \"pad\" | …), when reported. iOS only. */\n interfaceIdiom?: string;\n /** Derived device class. */\n formFactor?: DeviceFormFactor;\n /** `Dimensions.get('screen')` width in dp. */\n screenWidth?: number;\n /** `Dimensions.get('screen')` height in dp. */\n screenHeight?: number;\n /** Screen pixel density (`scale`). */\n screenScale?: number;\n /** Right-to-left layout (`I18nManager.isRTL`). */\n isRTL?: boolean;\n /** Resolved locale (e.g. \"en-US\"), from `Intl` when available. */\n locale?: string;\n /** IANA time zone (e.g. \"Europe/Berlin\"), from `Intl` when available. */\n timeZone?: string;\n /**\n * Host app version (e.g. \"1.4.2\"). BEST-EFFORT auto-detected here via `detectAppVersion()`\n * (reads `expo-constants` / `expo-application` when present; adds no dependency — see\n * device/appVersion.ts). An explicit host-injected `config.appVersion` always WINS: the merge\n * sites (`WireOnboarding`, the session-analytics hooks, the context envelope) overwrite this\n * with the host value when one is supplied. Omitted when neither source yields a version.\n */\n appVersion?: string;\n};\n\n/** iOS idiom wins; otherwise the shortest side in dp (>= 600 → tablet) picks the class. */\nconst deriveFormFactor = (\n iosIdiom: string | undefined,\n width: number | undefined,\n height: number | undefined,\n): DeviceFormFactor | undefined => {\n if (iosIdiom === \"pad\") return \"tablet\";\n if (iosIdiom === \"phone\") return \"phone\";\n if (typeof width === \"number\" && typeof height === \"number\") {\n return Math.min(width, height) >= 600 ? \"tablet\" : \"phone\";\n }\n return undefined;\n};\n\n/**\n * Collect the device snapshot. Pure, synchronous, and never throws — call it once per\n * onboarding session. Missing fields are omitted rather than sent as null/undefined so the\n * payload (and the server's stored dict) stays compact.\n */\nexport const collectDeviceContext = (): DeviceContext => {\n const ctx: DeviceContext = { platform: Platform.OS };\n\n // OS version (fallback; per-OS constants below may refine it).\n try {\n const version = Platform.Version;\n if (version !== undefined && version !== null && String(version)) {\n ctx.osVersion = String(version);\n }\n } catch {\n // ignore\n }\n\n // `Platform.constants` shape differs by OS and RN version — read every field defensively.\n let constants: Record<string, unknown> = {};\n try {\n constants = (Platform.constants ?? {}) as Record<string, unknown>;\n } catch {\n constants = {};\n }\n\n let iosIdiom: string | undefined;\n try {\n if (Platform.OS === \"android\") {\n const brand = constants.Brand;\n const model = constants.Model;\n const release = constants.Release;\n if (typeof brand === \"string\" && brand) ctx.brand = brand;\n if (typeof model === \"string\" && model) ctx.model = model;\n if (release !== undefined && release !== null && String(release)) {\n ctx.osVersion = String(release);\n }\n } else if (Platform.OS === \"ios\") {\n const osVersion = constants.osVersion;\n const idiom = constants.interfaceIdiom;\n if (osVersion !== undefined && osVersion !== null && String(osVersion)) {\n ctx.osVersion = String(osVersion);\n }\n if (typeof idiom === \"string\" && idiom) {\n ctx.interfaceIdiom = idiom;\n iosIdiom = idiom;\n }\n // iOS has no model in `Platform.constants`; best-effort via `expo-device` (adds no dep,\n // omitted when the module is absent — see device/deviceModel.ts).\n const iosModel = detectNativeModel();\n if (iosModel) ctx.model = iosModel;\n }\n } catch {\n // ignore per-OS constant reads\n }\n\n // Screen dimensions + derived form factor.\n try {\n const screen = Dimensions.get(\"screen\");\n if (screen) {\n if (typeof screen.width === \"number\") ctx.screenWidth = screen.width;\n if (typeof screen.height === \"number\") ctx.screenHeight = screen.height;\n if (typeof screen.scale === \"number\") ctx.screenScale = screen.scale;\n const formFactor = deriveFormFactor(iosIdiom, screen.width, screen.height);\n if (formFactor) ctx.formFactor = formFactor;\n }\n } catch {\n // ignore\n }\n\n // Layout direction.\n try {\n ctx.isRTL = I18nManager.isRTL;\n } catch {\n // ignore\n }\n\n // Locale + time zone via the standard Intl global. Hermes may ship without full Intl,\n // so referencing it can throw ReferenceError — the try/catch covers that too.\n try {\n const resolved = Intl.DateTimeFormat().resolvedOptions();\n if (resolved.locale) ctx.locale = resolved.locale;\n if (resolved.timeZone) ctx.timeZone = resolved.timeZone;\n } catch {\n // Intl unavailable — omit locale/timeZone.\n }\n\n // Best-effort host app version (adds no dependency; omitted when unavailable). An explicit\n // `config.appVersion` overrides this downstream at the merge sites.\n const appVersion = detectAppVersion();\n if (appVersion) ctx.appVersion = appVersion;\n\n return ctx;\n};\n","/**\n * contextEnvelope — a small, PRIVACY-NEUTRAL context bundle stamped onto every outgoing\n * analytics event, giving the Wire dashboard the Sentry/Firebase-parity segmentation fields\n * (device model, OS + version, screen, locale, timezone, form factor) plus a few host-injected\n * scalars (session correlation id, app version + native build number, connectivity type).\n *\n * WHY a separate builder (not just `collectDeviceContext`): the envelope COMPOSES the existing\n * device snapshot with the handful of extras a host can cheaply supply but the kit can't collect\n * dependency-free (native build number, connectivity type). It never re-implements device\n * collection — it reuses `collectDeviceContext()` verbatim (see device/deviceContext.ts).\n *\n * HARD PRIVACY RULE (why this file, like deviceContext.ts, adds nothing new):\n * NEVER GPS / location, NEVER an advertising id (IDFA / GAID), NEVER a device fingerprint.\n * Location is derived SERVER-SIDE from IP-geo only — nothing here carries a coordinate or an\n * ad id, so a host adopting this changes no App Privacy / Data Safety declaration. There is a\n * test (contextEnvelope.test.ts) that asserts the ABSENCE of any such field.\n *\n * DEPENDENCY-FREE: the only import is the kit's own `collectDeviceContext`. `networkType` and\n * `appBuild` are HOST-INJECTED — there is no dependency-free RN core signal for either, so the\n * envelope simply omits them when the host does not pass them (no forced peer dependency).\n */\nimport { collectDeviceContext, type DeviceContext } from \"../device/deviceContext\";\n\n/**\n * The context stamped onto every event. `device` is always present (from\n * `collectDeviceContext`); every scalar is optional and OMITTED when the host does not supply it.\n */\nexport type ContextEnvelope = {\n /** The privacy-neutral device snapshot (reused from `collectDeviceContext`). */\n device: DeviceContext;\n /** Correlation id for this app-open / flow (caller-supplied). */\n sessionId?: string;\n /** App version, e.g. \"1.4.2\" (mirrors `device.appVersion`; host-injected, else auto-detected). */\n appVersion?: string;\n /** Host native build number, e.g. \"412\" (from `expo-constants` `nativeBuildVersion`). */\n appBuild?: string;\n /** Host connectivity signal, e.g. \"wifi\" | \"cellular\" (from `@react-native-community/netinfo`). */\n networkType?: string;\n};\n\n/** Host-injected inputs for {@link buildContextEnvelope}. All optional; each is omitted when absent. */\nexport type ContextEnvelopeInput = {\n sessionId?: string;\n appVersion?: string;\n appBuild?: string;\n networkType?: string;\n};\n\n/**\n * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block (which\n * already carries a best-effort auto-detected `appVersion`) and layers the host-injected scalars\n * on top. An explicit `input.appVersion` overrides the auto-detected `device.appVersion`, and the\n * outer `appVersion` scalar mirrors whichever version is effective.\n *\n * Returns a NEW object on every call (no shared mutable reference), so a caller can hold or mutate\n * the result without leaking into the next envelope. Never throws — `collectDeviceContext` is\n * itself guarded, and the rest is plain assignment.\n */\nexport const buildContextEnvelope = (input: ContextEnvelopeInput = {}): ContextEnvelope => {\n // Fresh copy so the returned envelope never aliases a cached device snapshot.\n const device: DeviceContext = { ...collectDeviceContext() };\n\n // `device.appVersion` is auto-detected best-effort by `collectDeviceContext`; an explicit\n // host `input.appVersion` always wins.\n if (input.appVersion) device.appVersion = input.appVersion;\n\n // The outer scalar mirrors the effective version (host-supplied, else auto-detected) so the\n // queue can stamp `user_context.app_version` even when the host never passed one.\n const effectiveAppVersion = input.appVersion ?? device.appVersion;\n\n const envelope: ContextEnvelope = { device };\n if (input.sessionId) envelope.sessionId = input.sessionId;\n if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;\n if (input.appBuild) envelope.appBuild = input.appBuild;\n if (input.networkType) envelope.networkType = input.networkType;\n\n return envelope;\n};\n","/**\n * eventQueue — the OFFLINE-FIRST, persistent transport buffer for client analytics events.\n *\n * The existing `reportClientEvents` is a blind fire-and-forget POST: it returns `void`, has no\n * success signal, and drops events when the network is down. The analytics data story depends on\n * NEVER losing an event offline, so this queue adds the missing durability layer on top of the\n * same `POST {serverUrl}/v1/events` contract:\n *\n * • Persists pending events to the host-injected `WireOnboardingStorage` (survives app kills).\n * • Batches them into one request body `{ events: [...] }`.\n * • Owns its OWN awaited `fetch` that reads `res.ok` — the only way to drive retry + dequeue,\n * since `reportClientEvents` cannot ack. A 2xx dequeues the batch; a non-ok / rejected / thrown\n * response keeps it and schedules an exponential backoff retry.\n * • Flushes on `enqueue`, on an explicit `flush()`, and on `notifyOnline()` (host reconnect).\n * • Caps the buffer (drop-OLDEST under pressure) and de-dups identical pending events.\n * • Stamps the current context envelope (device + host scalars) onto every event before send.\n *\n * FIRE-AND-FORGET (load-bearing): `enqueue` returns immediately and NEVER throws into the UI. A\n * missing `fetch`, a hung/broken storage, a rejecting network, or a JSON error is swallowed and\n * degrades gracefully — analytics must never be able to break the app. In-memory fallback covers\n * the no-storage case (survives re-renders, not app kills).\n *\n * DEPENDENCY-FREE: no network-detection or persistence library. Connectivity is host-driven via\n * `notifyOnline()`; persistence is the host-injected AsyncStorage-compatible subset.\n */\nimport type { ContextEnvelope } from \"./contextEnvelope\";\nimport {\n buildEventsRequest,\n warnOnSkippedEvents,\n type ClientEvent,\n type ClientEventTarget,\n} from \"./reportClientEvent\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/** Envelope source: a fixed envelope or a provider evaluated at enqueue time (fresh network type). */\nexport type EnvelopeSource = ContextEnvelope | (() => ContextEnvelope | undefined);\n\n/** Options for {@link createEventQueue}. Only `target` is conceptually required to actually send. */\nexport type EventQueueOptions = {\n /** Where to POST — the tenant transport (`serverUrl` + `apiKey`), same as `WireOnboardingConfig`. */\n target: ClientEventTarget | undefined;\n /**\n * Host persistence (AsyncStorage-compatible subset). When omitted, the queue runs in the\n * documented DEGRADED in-memory mode — it survives re-renders but not an app kill.\n */\n storage?: WireOnboardingStorage;\n /** Tenant/app id used to namespace the default storage key (`wireai:evtq:<appId>`). */\n appId?: string;\n /** Explicit storage key override (wins over the `appId`-derived default). */\n storageKey?: string;\n /** The context envelope stamped onto every event before send (device + host scalars). */\n envelope?: EnvelopeSource;\n /** Max pending events; enqueuing past this DROPS THE OLDEST first (default 200). */\n maxSize?: number;\n /** Events per POST batch (default 20). */\n batchSize?: number;\n /** First retry delay in ms; doubles each failed attempt (default 1000). */\n baseBackoffMs?: number;\n /** Backoff ceiling in ms (default 30000). */\n maxBackoffMs?: number;\n /** Max AUTOMATIC backoff retries before pausing (default 6); `notifyOnline()`/`flush()` re-arm it. */\n maxRetries?: number;\n};\n\n/** The queue's public surface. `enqueue` is fire-and-forget (returns immediately, never throws). */\nexport type EventQueue = {\n /** Buffer one event (envelope-stamped), persist, and schedule a flush. Never throws. */\n enqueue(event: ClientEvent): void;\n /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */\n flush(): void;\n /** Host reconnect signal: reset backoff and drain immediately. Fire-and-forget. */\n notifyOnline(): void;\n /** Current pending (in-memory) count. */\n size(): number;\n /**\n * Tear this queue down when its owner goes away (a hook's effect cleanup, a host disposing its\n * own instance). Idempotent, never throws, and REQUIRED for any queue that can be re-created:\n * a remount builds a second queue while the first is still holding the storage claim and a live\n * backoff timer, and the two then fight over one persisted slot.\n *\n * It clears the retry timer, releases the claimed storage key so a replacement gets the SAME slot\n * instead of a rotated `…#2` nobody reads next launch, and makes the queue inert — `enqueue`,\n * `flush` and every write become no-ops, so a timer that already fired cannot `removeItem` the\n * slot the live queue owns. Anything still buffered in memory at that point stays on disk under\n * the released key, which is exactly where the replacement queue looks for it.\n */\n dispose(): void;\n};\n\nconst DEFAULTS = {\n maxSize: 200,\n batchSize: 20,\n baseBackoffMs: 1000,\n maxBackoffMs: 30000,\n maxRetries: 6,\n} as const;\n\n/** Ceiling on the persisted-backlog read — a hung adapter degrades to an empty start, never a stall. */\nconst READ_TIMEOUT_MS = 1500;\n\n// ── One storage slot per QUEUE, not per appId ────────────────────────────────────────────────────\n//\n// The default key was derived from `appId` alone, so two `createAnalytics` instances for one tenant —\n// the documented double-wiring, a façade for `track`/`screen` plus an activation instance — shared ONE\n// persisted backlog while keeping SEPARATE in-memory buffers. Every symptom of that is silent:\n// each `persist()` overwrites the other's blob with its own view of \"pending\"; a queue that drains to\n// empty calls `removeItem` and DELETES a sibling's still-pending events; and on relaunch whatever\n// survived is loaded by both instances and sent twice. `useLifecycleEvents` already avoided all of it\n// by handing its queue a dedicated explicit key — this generalizes that.\n//\n// A `Symbol.for` slot for the same reason as every other registry here: tsup inlines a copy of this\n// module into each bundle, and a plain module `let` would let the `.` and `./analytics` copies each\n// think they were the first claimant of the same key.\n//\n// @globalSlot LATCH — the claimed-key Set is created once and its identity is then stable. A second\n// write hands the next queue an EMPTY claim list, so it re-claims a key a live queue already owns\n// and the two silently share one persisted backlog again. Its MEMBERS are live, which is why\n// `claimedQueueKeys()` re-reads the slot on every call rather than caching the Set.\nconst QUEUE_KEY_SLOT: unique symbol = Symbol.for(\"@wireai/activation:eventQueueKeys\");\n\ntype GlobalWithQueueKeys = typeof globalThis & { [QUEUE_KEY_SLOT]?: Set<string> };\n\nconst queueKeyGlobal = globalThis as GlobalWithQueueKeys;\n\nconst claimedQueueKeys = (): Set<string> => {\n const existing = queueKeyGlobal[QUEUE_KEY_SLOT];\n if (existing) return existing;\n const created = new Set<string>();\n queueKeyGlobal[QUEUE_KEY_SLOT] = created;\n return created;\n};\n\n/**\n * Claim `preferred` for this queue, or the next free `preferred#N` when a live queue already holds it.\n *\n * ONLY the appId-derived DEFAULT is ever rotated. An EXPLICIT `storageKey` is the host (or\n * `useLifecycleEvents`) declaring which slot a queue owns, and that hook re-creates its queue on every\n * remount — rotating there would silently walk it off its own backlog once per remount, which is a\n * worse bug than the one being fixed.\n */\nconst claimQueueKey = (preferred: string, explicit: boolean): string => {\n const claimed = claimedQueueKeys();\n if (explicit || !claimed.has(preferred)) {\n claimed.add(preferred);\n return preferred;\n }\n let ordinal = 2;\n while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;\n const key = `${preferred}#${ordinal}`;\n claimed.add(key);\n warnInDev(\n `[wireai] a second event queue was created for the same appId, and \"${preferred}\" is already ` +\n \"claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, \" +\n \"delete each other's pending events when one drains to empty, and double-send on relaunch, so \" +\n `this queue was given \"${key}\" instead. Prefer ONE analytics instance per app; if you really ` +\n \"need two, pass an explicit `storageKey` to each so the slots are yours to reason about.\",\n );\n return key;\n};\n\n/**\n * Give a claimed key back, so the NEXT queue for the same appId gets the real slot instead of a\n * rotated one. Called only from {@link EventQueue.dispose}.\n *\n * WHY THIS HAS TO EXIST: the claim was write-only, and \"already claimed\" was read as \"held by a LIVE\n * one\" — liveness nothing ever tracked. A remount (nav, Fast Refresh, StrictMode's double-invoke, an\n * appId that arrives async) therefore rotated the replacement onto `…#2`, and from then on the whole\n * launch persisted into a slot the next launch never reads: it claims the base key, finds the older\n * blob, and the `#N` ones accumulate forever. Releasing on teardown makes the claim mean what its\n * warning already claimed it meant.\n */\nconst releaseQueueKey = (key: string): void => {\n claimedQueueKeys().delete(key);\n};\n\n/** Test-only: forget every claimed queue key. A real RELAUNCH is a new process, so a test that\n * simulates one in-process must call this or its second queue reads as a concurrent sibling.\n * Exported from `@wireai/activation/analytics`, matching `resetAutoDeviceKeys` / `resetCurrentSessionId`. */\nexport const resetEventQueueKeys = (): void => claimedQueueKeys().clear();\n\n/** Internal buffered item. `id` is a local monotonic handle for deterministic dequeue-after-ack;\n * it is NEVER sent to the server. `sig` is the de-dup signature (serialized stamped event). */\ntype QueuedItem = { id: number; event: ClientEvent; sig: string };\n\n/** Persisted shape — the local id + the (already envelope-stamped) event. `sig` is recomputed on load. */\ntype PersistedItem = { id: number; event: ClientEvent };\n\n/**\n * The verdict of a read that ran out of time. A DISTINCT value, and never `undefined` — an\n * `undefined` here is byte-identical to \"the adapter answered, there is no backlog\", and that\n * conflation destroys data silently: `parsePersisted(undefined)` gives `[]`, hydration takes its\n * `length === 0` early return without throwing, and the next `persist()` writes the in-memory\n * pending list over a blob nobody has read — or, with nothing pending, `removeItem`s it outright.\n */\nconst READ_TIMED_OUT: unique symbol = Symbol(\"wireai:storage-read-timeout\");\n\nconst withTimeout = <T>(p: Promise<T>, ms: number): Promise<T | typeof READ_TIMED_OUT> => {\n let timer: ReturnType<typeof setTimeout>;\n const timeout = new Promise<typeof READ_TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);\n });\n return Promise.race([p, timeout]).finally(() => clearTimeout(timer));\n};\n\n/** Detach a timer from the event loop where the runtime supports it (Node test process / some RNs). */\nconst unrefTimer = (timer: ReturnType<typeof setTimeout>): void => {\n const t = timer as unknown as { unref?: () => void };\n if (typeof t.unref === \"function\") t.unref();\n};\n\nconst parsePersisted = (raw: string | null | undefined): PersistedItem[] => {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n const items: PersistedItem[] = [];\n for (const entry of parsed) {\n if (\n entry &&\n typeof entry === \"object\" &&\n typeof (entry as PersistedItem).id === \"number\" &&\n (entry as PersistedItem).event &&\n typeof (entry as PersistedItem).event === \"object\"\n ) {\n items.push(entry as PersistedItem);\n }\n }\n return items;\n } catch {\n // Corrupt backlog → start empty; the next persist overwrites it.\n return [];\n }\n};\n\n/**\n * Create an offline-first event queue. Loads any persisted backlog on creation so a\n * killed-and-relaunched app resumes where it left off. Returns the {@link EventQueue} surface.\n */\nexport const createEventQueue = (options: EventQueueOptions): EventQueue => {\n const target = options.target;\n const storage = options.storage;\n // One slot per QUEUE: an explicit key is taken verbatim; the appId-derived default is rotated\n // to `…#2` when a live queue already holds it, so two instances can never share one backlog.\n //\n // Only claimed when there IS storage. The defect is entirely about the persisted slot, and a\n // storage-less queue (the documented degraded in-memory mode) never reads or writes the key — so\n // claiming there would warn about a collision that cannot happen.\n const defaultKey = options.storageKey ?? `wireai:evtq:${options.appId ?? \"default\"}`;\n const key = storage ? claimQueueKey(defaultKey, options.storageKey !== undefined) : defaultKey;\n const maxSize = options.maxSize ?? DEFAULTS.maxSize;\n const batchSize = options.batchSize ?? DEFAULTS.batchSize;\n const baseBackoffMs = options.baseBackoffMs ?? DEFAULTS.baseBackoffMs;\n const maxBackoffMs = options.maxBackoffMs ?? DEFAULTS.maxBackoffMs;\n const maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;\n\n let pending: QueuedItem[] = [];\n let nextId = 0;\n let flushing = false;\n let attempt = 0;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n // Set by `dispose()`. Every write path and the drain check it, because a queue whose owner is\n // gone must not touch a storage slot a replacement queue now owns.\n let disposed = false;\n // The persisted blob may exist and has NOT been read yet. While this is true every write is\n // suppressed: the only thing the queue could write is a view of `pending` that does not include\n // the backlog it has not seen, and `setItem`/`removeItem` would destroy it. Cleared as soon as the\n // read settles, whichever way it settles, so a read that ultimately fails resumes normal\n // persistence rather than suppressing it for the life of the process.\n //\n // ARMED AT CONSTRUCTION, not when the 1500ms timeout fires. It used to be set ONLY in the timeout\n // branch, so for the first 1500ms of every launch the comment above was false and the queue wrote\n // freely over a blob it had not read. The sharp form is not the overwrite but the DELETE: a queue\n // that drains to empty inside that window calls `removeItem` on the slot, and an adapter that\n // resolves a read against its current state (a bridge that queues operations and runs them in\n // order — not every adapter snapshots at call time) then answers `null`. The whole backlog is\n // gone, with no error anywhere.\n let backlogUnread = storage !== undefined;\n\n const resolveEnvelope = (): ContextEnvelope | undefined => {\n try {\n return typeof options.envelope === \"function\" ? options.envelope() : options.envelope;\n } catch {\n return undefined;\n }\n };\n\n // Stamp the current envelope onto a COPY of the event (never mutate the caller's object):\n // device → event.device (when the event has none)\n // sessionId → event.session_id (when the event has none)\n // appVersion / appBuild / networkType → event.user_context (the non-PII bucket the server\n // sanitizes), never overwriting a key the caller already set.\n const stamp = (event: ClientEvent): ClientEvent => {\n const env = resolveEnvelope();\n const stamped: ClientEvent = { ...event };\n // Client enqueue timestamp: distinguishes a GENUINE repeat (same event fired seconds apart)\n // from a REDUNDANT re-enqueue of the same instant. The de-dup signature below includes it, so\n // two identical events enqueued in the same millisecond still collapse (a re-render), while the\n // same action repeated later carries a fresh `ts` and survives. A caller-set `ts` is preserved.\n // This stays a NUMBER in the queue (the de-dup signature depends on it); `buildEventsRequest`\n // serializes it to the ISO8601 string the server requires at send time.\n if (stamped.ts === undefined) stamped.ts = Date.now();\n if (!env) return stamped;\n if (!stamped.device && env.device) stamped.device = env.device;\n if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;\n const uc: Record<string, string | number | boolean> = { ...(stamped.user_context ?? {}) };\n if (env.appVersion && uc.app_version === undefined) uc.app_version = env.appVersion;\n if (env.appBuild && uc.app_build === undefined) uc.app_build = env.appBuild;\n if (env.networkType && uc.network_type === undefined) uc.network_type = env.networkType;\n if (Object.keys(uc).length > 0) stamped.user_context = uc;\n return stamped;\n };\n\n const persist = (): void => {\n if (!storage) return;\n // A disposed queue no longer owns this slot — a replacement may. See `dispose`.\n if (disposed) return;\n // NEVER write over a blob that has not been read yet. See `backlogUnread`.\n if (backlogUnread) return;\n try {\n if (pending.length === 0) {\n void storage.removeItem(key).catch(() => {});\n return;\n }\n const payload: PersistedItem[] = pending.map((item) => ({ id: item.id, event: item.event }));\n void storage.setItem(key, JSON.stringify(payload)).catch(() => {});\n } catch {\n // Best-effort: a failed write just means the backlog is not durable this launch.\n }\n };\n\n /**\n * The two lifecycle events are the funnel's DENOMINATOR: the server computes `min_sessions` by\n * counting distinct `app.session_started` grouped by `device_key`, and `app.first_open` is the top\n * of the activation funnel. Losing one is not \"an event fewer\", it is an app-open that never\n * happened as far as every rule reading them is concerned.\n */\n const isCountedOpenEvent = (event: ClientEvent): boolean =>\n typeof event.question_key === \"string\" && event.question_key.startsWith(\"app.\");\n\n const enforceSizeCap = (): void => {\n let overflow = pending.length - maxSize;\n if (overflow <= 0) return;\n // Drop the OLDEST first so the newest events are never the ones lost under pressure — but spend\n // ORDINARY events first, because at an app-open the lifecycle pair IS the oldest thing in the\n // buffer. A host on the documented shared-sink wiring that goes offline for a long session\n // pushes past the cap on screen views alone, and pure drop-oldest evicted `app.first_open` +\n // `app.session_started` before anything else — deleting the open from the funnel while 200\n // screen views survived.\n const kept: QueuedItem[] = [];\n for (const item of pending) {\n if (overflow > 0 && !isCountedOpenEvent(item.event)) {\n overflow--;\n continue;\n }\n kept.push(item);\n }\n // Only when the buffer is NOTHING BUT counted events does the cap fall on them, oldest first.\n // The cap is a hard ceiling: protecting a class must never turn it into an unbounded buffer.\n if (overflow > 0) kept.splice(0, overflow);\n pending = kept;\n };\n\n const safeSig = (event: ClientEvent): string => {\n try {\n return JSON.stringify(event);\n } catch {\n // Non-serializable event → give it a unique signature so it is never wrongly de-duped.\n return `__nosig_${nextId}_${Math.random()}`;\n }\n };\n\n /**\n * Merge a loaded backlog into the in-memory buffer: persisted (older) events go AHEAD of\n * whatever was enqueued while the read was in flight, duplicates collapse, and the size cap\n * applies as usual.\n *\n * Ids are minted FRESH from the running `nextId` rather than reset to 0. A drain may already be\n * in flight holding a batch of ids it will filter out on ack, and re-numbering from 0 would make\n * those ids point at different events — the ack would then dequeue (silently drop) whichever\n * events happened to inherit them. Monotonic ids are never reused, so an in-flight ack stays\n * correct whatever lands in between.\n */\n const mergePersisted = (persistedItems: PersistedItem[]): void => {\n if (persistedItems.length === 0) return;\n const seen = new Set(pending.map((item) => item.sig));\n const restored: QueuedItem[] = [];\n for (const persisted of persistedItems) {\n const sig = safeSig(persisted.event);\n if (seen.has(sig)) continue; // already in memory — collapse the duplicate\n seen.add(sig);\n restored.push({ id: nextId++, event: persisted.event, sig });\n }\n if (restored.length === 0) return;\n pending = [...restored, ...pending];\n enforceSizeCap();\n persist();\n };\n\n // Load any persisted backlog. Anything enqueued before this settles stays in memory; the merge\n // above puts persisted (older) events ahead of it.\n //\n // A read that blows READ_TIMEOUT_MS is NOT treated as \"no backlog\". The race only unblocks the\n // DRAIN — the original read is kept and merged whenever it lands, and until then every write is\n // suppressed so the unread blob survives intact.\n const loadPromise: Promise<void> = (async () => {\n if (!storage) return;\n try {\n const read = Promise.resolve(storage.getItem(key));\n // A rejecting read must not surface as an unhandled rejection when the race is won by the\n // timeout; the recovery path below re-attaches its own handlers.\n read.catch(() => {});\n const raced = await withTimeout(read, READ_TIMEOUT_MS);\n if (raced === READ_TIMED_OUT) {\n backlogUnread = true;\n warnInDev(\n `[wireai] the persisted analytics backlog at \"${key}\" took longer than ${READ_TIMEOUT_MS}ms ` +\n \"to read, so the queue started without it. The stored events are NOT discarded: writes \" +\n \"are held back until the read lands, and the backlog is merged in then. If you see this \" +\n \"on every cold start, your storage adapter is too slow to be on the launch path.\",\n );\n void read\n .then((late) => {\n backlogUnread = false;\n mergePersisted(parsePersisted(late));\n // Send whatever was just recovered; without this it would wait for the next enqueue.\n flush();\n })\n .catch(() => {\n // The slow read ultimately failed → nothing to preserve, resume normal persistence.\n backlogUnread = false;\n persist();\n });\n return;\n }\n backlogUnread = false;\n mergePersisted(parsePersisted(raced));\n // Persist explicitly: writes were suppressed for the whole read window, and `mergePersisted`\n // returns early (without persisting) when the backlog was empty — so without this an event\n // enqueued during the window would sit in memory undurable until the next enqueue.\n persist();\n } catch {\n // Unreadable backlog → start empty; nothing enqueued in-memory is lost. Writes must resume,\n // or the suppression that protected the unread blob would outlive the read for the whole\n // process and nothing would ever persist again.\n backlogUnread = false;\n persist();\n }\n })();\n\n // The queue's OWN awaited POST. Reads `res.ok` to drive retry/dequeue. NEVER throws — a missing\n // fetch, a rejecting network, or a JSON error resolves to `false` (batch stays, retry schedules).\n const postBatch = async (events: ClientEvent[]): Promise<boolean> => {\n // Build the /v1/events request through the ONE canonical builder (url + headers + body) so this\n // queue never re-describes the endpoint. The abort-timeout stays: the queue owns retry/dequeue,\n // so a hung request must be cut loose to schedule a backoff rather than block the drain forever.\n const req = buildEventsRequest(target, events);\n if (!req) return false;\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : undefined;\n const timer = setTimeout(() => controller?.abort(), 15_000);\n try {\n const res = await fetch(req.url, { ...req.init, signal: controller?.signal });\n // A 200 can still carry `skipped:N` — events the server threw away. Log-only: the ack below\n // stays `res.ok`, so retry/dequeue behaviour is unchanged.\n warnOnSkippedEvents(res);\n return !!(res && (res as { ok?: boolean }).ok);\n } catch {\n return false;\n } finally {\n clearTimeout(timer);\n }\n };\n\n const clearRetry = (): void => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n }\n };\n\n const scheduleRetry = (): void => {\n // Bounded automatic retry. Past the cap the backlog simply waits for the next\n // `notifyOnline()` / `flush()` (both re-arm attempt), so events are paused, never dropped.\n if (attempt >= maxRetries) return;\n const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);\n attempt++;\n clearRetry();\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n void drain();\n }, delay);\n unrefTimer(retryTimer);\n };\n\n const drain = async (): Promise<void> => {\n if (disposed) return;\n try {\n await loadPromise;\n } catch {\n // load already swallows; guard the await defensively.\n }\n // Re-checked AFTER the await: the owner can go away while the cold-start read is in flight, and\n // a drain that resumes then would send and dequeue under a key another queue now owns.\n if (disposed) return;\n if (flushing) return;\n flushing = true;\n try {\n while (pending.length > 0) {\n const batch = pending.slice(0, batchSize);\n const ok = await postBatch(batch.map((item) => item.event));\n if (!ok) {\n scheduleRetry();\n return;\n }\n // Dequeue exactly the acked batch by id (pending may have grown while in flight).\n const acked = new Set(batch.map((item) => item.id));\n pending = pending.filter((item) => !acked.has(item.id));\n persist();\n attempt = 0;\n clearRetry();\n }\n } finally {\n flushing = false;\n }\n };\n\n const flush = (): void => {\n try {\n void drain();\n } catch {\n // drain never throws synchronously, but guard the kick anyway.\n }\n };\n\n const enqueue = (event: ClientEvent): void => {\n // A disposed queue is inert: it cannot persist (the slot belongs to its replacement) and must\n // not send, so buffering here would only pretend to accept the event.\n if (disposed) return;\n try {\n const stamped = stamp(event);\n const sig = safeSig(stamped);\n // Collapse a redundant re-enqueue of an identical pending event.\n for (const item of pending) {\n if (item.sig === sig) return;\n }\n pending.push({ id: nextId++, event: stamped, sig });\n enforceSizeCap();\n persist();\n // Only kick a drain when no retry is already pending — avoids hammering fetch while offline.\n if (retryTimer === undefined) flush();\n } catch {\n // Fire-and-forget: nothing in enqueue may surface to the UI.\n }\n };\n\n const notifyOnline = (): void => {\n // Host reconnected: reset the backoff and drain now.\n attempt = 0;\n clearRetry();\n flush();\n };\n\n const size = (): number => pending.length;\n\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n // The retry is the dangerous half: `unrefTimer` is a Node-only affordance (Hermes has no\n // `unref`), so on a device the backoff timer of an unmounted queue is fully alive, up to six\n // attempts and a 30s ceiling. Left running it wakes up, drains, empties, and `removeItem`s a\n // slot the live queue is using.\n clearRetry();\n // Only claimed when there IS storage (see the claim above), so only released then.\n if (storage) releaseQueueKey(key);\n };\n\n return { enqueue, flush, notifyOnline, size, dispose };\n};\n","/**\n * User identity — bind an onboarding session to the HOST's own user id so the funnel can\n * be reconciled to real users later (console sessions ↔ your user table / GA4 users).\n *\n * The id is an OPAQUE PSEUDONYMOUS STRING the host owns — its internal user id, NOT an\n * email/name/phone. Same host-injection philosophy as `storage` and `userContext`: the kit\n * mints nothing and reads nothing; the host passes what it wants. Dependency-free.\n *\n * Three binding moments (see WireOnboarding + the README \"User identity\" section):\n * 1. MOUNT — pass `userId` and it rides the A2A session-start metadata; the server\n * binds it when it creates the session.\n * 2. MID-SESSION— the user registers DURING onboarding: change the `userId` prop and the\n * kit emits an `identify` client event that attaches the id to the live\n * session (late binding — the key scenario).\n * 3. POST-FLOW — the user registers AFTER onboarding: call `identifyOnboarding(...)` with\n * the same host storage (it recovers the persisted contextId, before completion\n * clears it) or, more reliably, the `contextId` you captured from the\n * `started`/`resumed` `onEvent` (both events carry a `contextId` field).\n *\n * ⚠️ NO PII. Pass an opaque id (or a hash), never a raw email/name/phone. The id is capped at\n * {@link USER_ID_MAX_LENGTH} chars (longer ids are truncated, not rejected).\n */\nimport { getCurrentSessionId } from \"../analytics/currentSession\";\nimport { reportClientEvent } from \"../analytics/reportClientEvent\";\nimport {\n peekPersistedSession,\n sessionStorageKey,\n type WireOnboardingStorage,\n} from \"../session/persistedSession\";\n\n/** Max accepted user-id length. Longer strings are truncated (never rejected). Keep in sync\n * with the server's `USER_ID_MAX_LENGTH` (analytics/events.py). */\nexport const USER_ID_MAX_LENGTH = 128;\n\n/**\n * Normalize a host-supplied user id: trim, drop empty, and cap at {@link USER_ID_MAX_LENGTH}.\n * Returns `undefined` for a missing/blank/non-string value so callers can `if (id)`-gate.\n * PII is a host concern — this only bounds length, it does not (and cannot) detect an email.\n */\nexport const sanitizeUserId = (raw: unknown): string | undefined => {\n if (typeof raw !== \"string\") return undefined;\n const trimmed = raw.trim();\n if (!trimmed) return undefined;\n return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;\n};\n\n/**\n * A permissive email-SHAPE test (`local@domain.tld`) — NOT an RFC validator. Its ONE job is to\n * catch the common integration mistake of binding a RAW EMAIL as the opaque `user_id`: that leaks\n * PII into the top-level id (which the server treats as an opaque key and may surface), when the\n * email belongs in the opt-in `user_context.user_email` field instead. `identify()` uses this to\n * refuse an email-shaped id (with a dev warning) unless the host opts in explicitly. Trims first.\n */\nexport const looksLikeEmail = (value: unknown): boolean =>\n typeof value === \"string\" && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n\n/** Options for {@link identifyOnboarding}. */\nexport type IdentifyOnboardingOptions = {\n /** Tenant transport, same shape as `WireOnboardingConfig` (only these two fields are used). */\n config: { serverUrl: string; apiKey: string };\n /** The host's opaque user id to bind. Trimmed + capped; NO PII. */\n userId: string;\n /**\n * The onboarding session id to bind to (the A2A contextId) — the `contextId` field carried on\n * the `started`/`resumed` `onEvent`. Pass this when you captured it there. Required after the\n * flow COMPLETED, since completion clears the persisted session. Wins over the storage lookup.\n */\n contextId?: string;\n /**\n * The SAME host storage you passed to `<WireOnboarding storage={…} />`. When `contextId` is\n * omitted, the helper reads the persisted contextId from it (works while the session is still\n * persisted — i.e. dropped or mid-flow, before completion clears it).\n */\n storage?: WireOnboardingStorage;\n /** App id, to derive the default storage key `wireai:session:<appId>` when reading from storage. */\n appId?: string;\n /** Storage key override — pass the same `persistKey` you gave `<WireOnboarding>`, if any. */\n persistKey?: string;\n /**\n * OPT-IN LAST RESORT, default `false`. When no ONBOARDING session can be resolved (no `contextId`,\n * nothing in `storage`), bind the user to the LIVE PER-OPEN app session instead and return\n * `\"app_session\"`.\n *\n * ⚠️ These are two different id spaces sharing one wire field. An onboarding session id is the A2A\n * `contextId`; a per-open id is what `app.session_started` registers. The server's onboarding funnel\n * groups by `session_id`, so a per-open id posted here does not attach the user to their onboarding\n * — it writes a row nothing in that funnel can join. Until 0.13.0 this happened SILENTLY and\n * returned `true`, in exactly the documented post-completion case (completion clears the persisted\n * session), so the funnel stayed unattributed while the host was told it had worked.\n *\n * Turn it on only if binding the id to *some* session the server saw is genuinely worth more to you\n * than knowing the onboarding bind failed — and read the return value, which now says which it was.\n */\n allowAppSessionFallback?: boolean;\n};\n\n/**\n * What {@link identifyOnboarding} bound, and to WHICH id space — because `true` could not say.\n *\n * • `\"onboarding\"` — bound to the A2A `contextId`. This is the one that attributes the funnel.\n * • `\"app_session\"` — bound to the live per-open app session, via `allowAppSessionFallback`. The\n * server saw that session, but it is not this user's onboarding.\n * • `false` — nothing was dispatched (no user id, no server url, no resolvable session).\n *\n * ⚠️ 0.13.0 widened this from `boolean`. `\"onboarding\"` is truthy, so an `if (await identify…)` still\n * behaves identically; only an explicit `: boolean` annotation needs updating.\n */\nexport type IdentifyOnboardingBinding = \"onboarding\" | \"app_session\" | false;\n\n/**\n * Attach a host user id to an onboarding session AFTER the fact (post-registration), by sending\n * an `identify` client event to `/v1/events`. Resolves the contextId from an explicit\n * `contextId` or, failing that, from the persisted session in the host `storage`.\n *\n * Fire-and-forget under the hood (never throws, never blocks onboarding). Resolves to the\n * {@link IdentifyOnboardingBinding} that says WHICH id space was bound, or `false` when nothing could\n * be (no user id, no server url, or no resolvable session).\n */\nexport const identifyOnboarding = async (\n opts: IdentifyOnboardingOptions,\n): Promise<IdentifyOnboardingBinding> => {\n const userId = sanitizeUserId(opts.userId);\n if (!userId || !opts.config?.serverUrl) return false;\n\n let contextId = opts.contextId?.trim() || undefined;\n if (!contextId && opts.storage) {\n const key = opts.persistKey ?? (opts.appId ? sessionStorageKey(opts.appId) : undefined);\n if (key) {\n const stored = await peekPersistedSession(opts.storage, key);\n contextId = stored?.id;\n }\n }\n let space: Exclude<IdentifyOnboardingBinding, false> = \"onboarding\";\n // The OPT-IN last resort. Until 0.13.0 this ran unconditionally: with no captured contextId\n // it posted the LIVE PER-OPEN session id in the `session_id` field — which on this endpoint means\n // the ONBOARDING session — and then returned `true`. Two disjoint id spaces share that field, so\n // the row it wrote could never join the onboarding funnel, and the host got a success signal for a\n // bind that had not happened. It is now off unless the caller asks, and when it does fire it says\n // so in the return value instead of impersonating an onboarding bind.\n if (!contextId && opts.allowAppSessionFallback) {\n contextId = getCurrentSessionId();\n space = \"app_session\";\n }\n if (!contextId) return false;\n\n reportClientEvent(\n { serverUrl: opts.config.serverUrl, apiKey: opts.config.apiKey },\n { event_type: \"identify\", session_id: contextId, user_id: userId },\n );\n return space;\n};\n","/**\n * userContext — the ONE extensible object a host passes once and the kit flows into every\n * analytics event's `user_context` (plus the top-level opaque `user_id`).\n *\n * WHY it exists: hosts already hand the kit fragments of \"who this user is\" — `config.appVersion`,\n * `useSessionStart({ deviceKey, userId })`, `<WireOnboarding userContext={…} />` — but there was no\n * single object that carries app version + device key + user id + (opt-in) email + arbitrary extras\n * together, with one precedence rule, into every event. `WireUserContext` is that object;\n * `resolveUserContext` is the pure merge that turns it into the wire shape.\n *\n * PRECEDENCE (the one rule): an explicit `WireUserContext` field WINS over the #42 auto-detected\n * `device`/`appVersion`. A missing field is OMITTED, never sent empty.\n *\n * WHERE EACH FIELD LANDS (deliberate separation so nothing leaks across buckets):\n * • `userId` → the event's TOP-LEVEL opaque `user_id` (via `sanitizeUserId`). NEVER the bucket.\n * • `userEmail` → its OWN key `user_context.user_email`. NEVER merged into `userId`. OPT-IN PII.\n * • `deviceKey` → `user_context.device_key` (the server's `_event_device_key` reads it there).\n * • `appVersion`→ `user_context.app_version` (and returned as `appVersion` for `device.appVersion`).\n * • `extra` → NAMESPACED under a `custom.` key prefix, coerced to scalars, so a host extra can\n * never collide with a reserved `user_context` key.\n *\n * DEPENDENCY-FREE: the only import is the kit's own `sanitizeUserId`. The optional email hash is a\n * dependency-free FNV-1a fold (see {@link hashEmailFnv1a}) — no crypto library, no async.\n */\nimport { sanitizeUserId } from \"../identity/userIdentity\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\n\n/**\n * The single, extensible user-context object. A host passes it ONCE (at analytics init) and may\n * update it post-mount (e.g. attach `userId`/`userEmail` at login) via `setUserContext(partial)`.\n * Every field is optional; missing fields are omitted from the wire payload.\n */\nexport interface WireUserContext {\n /**\n * Host app version, e.g. \"1.4.2\". EXPLICIT — wins over the #42 auto-detected `device.appVersion`.\n * Lands in `user_context.app_version`. Omitted when neither this nor auto-detect yields a version.\n */\n appVersion?: string;\n /**\n * A stable, non-PII device id the host owns. Lands in `user_context.device_key` (NOT `session_id`),\n * where the server groups a device's sessions. Host-supplied; the kit never mints or reads one.\n */\n deviceKey?: string;\n /**\n * The host's OPAQUE PSEUDONYMOUS user id (their internal id — NOT an email/name/phone). Sanitized +\n * capped (see `sanitizeUserId`) and placed on the event's top-level `user_id`. NEVER the bucket.\n */\n userId?: string;\n /**\n * OPT-IN PII. The user's email, its OWN field (`user_context.user_email`) — NEVER merged into\n * `userId`. The kit NEVER auto-collects this; a host passes it only WITH the user's consent (EU\n * users: treat as personal data). For a non-reversible form, set {@link hashEmail} `true` (the kit\n * folds it with a dependency-free hash and stamps `user_context.user_email_hashed: true`), OR\n * pre-hash host-side with a cryptographic digest and pass that here with `hashEmail` falsy.\n */\n userEmail?: string;\n /**\n * When `true`, {@link userEmail} is folded with the kit's dependency-free {@link hashEmailFnv1a}\n * before it leaves the device, and `user_context.user_email_hashed` is set `true`. NOTE: FNV-1a is\n * a lightweight NON-cryptographic fold (obfuscation, not a secure digest). For a cryptographic\n * hash, compute it host-side (e.g. SHA-256 via `expo-crypto`) and pass the digest as `userEmail`\n * with `hashEmail` falsy. Default: raw email is sent as-is (opt-in already gated it upstream).\n */\n hashEmail?: boolean;\n /**\n * Arbitrary host context (signup method, referral, plan tier…). Each value is coerced to a scalar\n * (`string | number | boolean`; non-scalars and non-finite numbers are DROPPED) and NAMESPACED\n * under a `custom.` key prefix in `user_context` (e.g. `user_context[\"custom.referral\"]`) so it can\n * never collide with a reserved key. No raw PII — use {@link userEmail} for email.\n */\n extra?: Record<string, string | number | boolean>;\n}\n\n/**\n * The wire-shaped result of {@link resolveUserContext}. `userContext` is the non-PII/opt-in-PII\n * bucket stamped onto the event; `userId` is the top-level opaque id; `appVersion`/`deviceKey` are\n * echoed for callers that also place them elsewhere (e.g. `device.appVersion`). Absent fields are\n * omitted so a caller can spread this without sending empties.\n */\nexport interface ResolvedUserContext {\n /** The opaque, sanitized user id → the event's top-level `user_id`. Omitted when unset/blank. */\n userId?: string;\n /** The stable device id → `user_context.device_key`. Omitted when unset. */\n deviceKey?: string;\n /** The effective app version (explicit > auto-detected) → `user_context.app_version`. */\n appVersion?: string;\n /** The `user_context` bucket (device_key, app_version, user_email[+ _hashed], custom.*). */\n userContext?: Record<string, string | number | boolean>;\n}\n\n/** The prefix applied to every host `extra` key so it can never collide with a reserved key. */\nexport const EXTRA_KEY_PREFIX = \"custom.\" as const;\n\n/** A finite scalar the wire accepts. Non-finite numbers (NaN/Infinity) are NOT scalars here. */\nexport const isWireScalar = (value: unknown): value is string | number | boolean => {\n const t = typeof value;\n if (t === \"string\" || t === \"boolean\") return true;\n if (t === \"number\") return Number.isFinite(value as number);\n return false;\n};\n\n/**\n * Fold an email to a stable, dependency-free 32-bit FNV-1a hex token (lowercased + trimmed first so\n * the same address always folds identically). This is OBFUSCATION, not a cryptographic digest — it\n * is not collision-resistant. For a real hash, pre-hash host-side and pass the digest as `userEmail`.\n */\nexport const hashEmailFnv1a = (email: string): string => {\n const normalized = email.trim().toLowerCase();\n let hash = 0x811c9dc5; // FNV offset basis (32-bit)\n for (let i = 0; i < normalized.length; i++) {\n hash ^= normalized.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193); // FNV prime (32-bit), kept in 32-bit via imul\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n};\n\n/** Trim a candidate string; return `undefined` for a non-string / blank so callers can `if`-gate.\n * Shared with `activation/wireActivation`, which carried a byte-identical private copy named `clean`.\n * Not re-exported from the package barrel — this is an internal helper, not public surface. */\nexport const cleanString = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Coerce a host `extra` map into the namespaced, scalar-only bucket shape. Every kept value is\n * placed under `custom.<key>`; non-scalar values (objects, arrays, null, functions, NaN/Infinity)\n * are DROPPED. Returns an object (possibly empty).\n */\nexport const namespaceExtra = (\n extra: Record<string, unknown> | undefined,\n): Record<string, string | number | boolean> => {\n const out: Record<string, string | number | boolean> = {};\n if (!extra || typeof extra !== \"object\") return out;\n for (const [key, value] of Object.entries(extra)) {\n const cleanKey = cleanString(key);\n if (!cleanKey) continue;\n if (!isWireScalar(value)) continue; // drop anything that isn't a finite scalar\n out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;\n }\n return out;\n};\n\n/**\n * The storage key the analytics façade persists the bound opaque `user_id` under (namespaced per\n * `appId`, mirroring {@link deviceIdStorageKey}). Exported so a logout path can target it directly.\n */\nexport const analyticsUserIdStorageKey = (appId?: string): string =>\n `wireai:analytics:userId:${appId ?? \"default\"}`;\n\n/**\n * Return a COPY of a {@link WireUserContext} with every USER-scoped (PII / pseudonymous) field\n * removed — `userId`, `userEmail`, `hashEmail`, and `extra` — while KEEPING the non-PII device-scope\n * fields (`appVersion`, `deviceKey`). This is the in-memory half of logout: after it, the same\n * analytics instance keeps its stable `device_key` (which groups a DEVICE, not a user) but no longer\n * stamps the previous user's id/email onto events. Pure; never mutates the input.\n */\nexport const clearPiiFromContext = (ctx: WireUserContext = {}): WireUserContext => {\n const rest: WireUserContext = {};\n if (typeof ctx.appVersion === \"string\") rest.appVersion = ctx.appVersion;\n if (typeof ctx.deviceKey === \"string\") rest.deviceKey = ctx.deviceKey;\n return rest;\n};\n\n/** Options for {@link clearUserContext}. */\nexport interface ClearUserContextOptions {\n /** Host persistence (AsyncStorage subset) — the persisted bound `user_id` is removed from here. */\n storage?: WireOnboardingStorage;\n /** Tenant/app id — namespaces the persisted key (`wireai:analytics:userId:<appId>`). */\n appId?: string;\n}\n\n/**\n * LOGOUT primitive: purge the persisted, bound opaque `user_id` for an app so the NEXT user on a\n * shared device is not silently attributed to the previous one. Removes the\n * `wireai:analytics:userId:<appId>` key that the analytics façade persists and reuses across\n * launches. Fire-and-forget: a missing storage or a failing adapter resolves quietly.\n *\n * COVERAGE. The stateful `createAnalytics(...)` instance also exposes {@link Analytics.reset}, which\n * does this AND clears the in-memory binding + PII in one call — prefer it when you hold the\n * instance. This standalone helper covers the `createWireActivation` / `wire` path (whose config is\n * captured immutably, so it has no `reset`): call `clearUserContext({ storage, appId })` on logout,\n * and RECREATE the `wire` / analytics instance without the user's `userContext` (userId/userEmail)\n * so no further events carry the previous user's identity. The non-PII per-install `device_key`\n * (`wireai:analytics:deviceKey:<appId>`) is intentionally left in place — it groups a device, not a\n * person, and stays stable across users of the same install.\n */\nexport const clearUserContext = async (opts: ClearUserContextOptions = {}): Promise<void> => {\n const storage = opts.storage;\n if (!storage) return;\n try {\n await storage.removeItem(analyticsUserIdStorageKey(opts.appId));\n } catch {\n /* best-effort, swallow — logout must never throw into the UI */\n }\n};\n\n/**\n * The `userContext` value to hand `<WireOnboarding userContext={...} />` so an onboarding session\n * and the app's later events (purchases, actions, screens) share ONE join key.\n *\n * WHY it exists as a named function instead of an inline object literal: the wire key is\n * `device_key`, the prop-facing name is `deviceKey`, and the analytics surfaces auto-mint the value\n * for you. A host that hand-writes `userContext={{ deviceKey }}` produces a bucket the server's\n * device lookup does not read, and the resulting funnel is silently EMPTY rather than wrong. This is\n * the one place that spelling is decided.\n *\n * Pass the SAME `deviceKey` you gave `createAnalytics` / `createWireActivation`. `session_id` is not\n * a join key across those two families: an onboarding session id is the A2A `contextId` and an\n * app-event session id is the per-open id, so intersecting them returns nothing.\n */\nexport const activationJoinContext = (\n deviceKey: string,\n): Record<string, string | number | boolean> => resolveUserContext({ deviceKey }).userContext ?? {};\n\n/** Options for {@link resolveUserContext}. */\nexport interface ResolveUserContextOptions {\n /**\n * The kit's best-effort auto-detected app version (#42; from `detectAppVersion()`/the device\n * snapshot). Used ONLY when the explicit `WireUserContext.appVersion` is absent — explicit wins.\n */\n autoAppVersion?: string;\n}\n\n/**\n * Merge a {@link WireUserContext} into the wire shape with the precedence rule (explicit field >\n * auto-detected). Pure, never throws. Missing fields are omitted so the result can be spread onto an\n * event without sending empties.\n */\nexport const resolveUserContext = (\n ctx: WireUserContext = {},\n opts: ResolveUserContextOptions = {},\n): ResolvedUserContext => {\n const result: ResolvedUserContext = {};\n const bucket: Record<string, string | number | boolean> = {};\n\n // userId → top-level opaque id (NEVER the bucket). Sanitized + capped host-side.\n const userId = sanitizeUserId(ctx.userId);\n if (userId) result.userId = userId;\n\n // deviceKey → user_context.device_key (NOT session_id).\n const deviceKey = cleanString(ctx.deviceKey);\n if (deviceKey) {\n result.deviceKey = deviceKey;\n bucket.device_key = deviceKey;\n }\n\n // appVersion → explicit wins over auto-detected (#42); echoed for device.appVersion callers.\n const appVersion = cleanString(ctx.appVersion) ?? cleanString(opts.autoAppVersion);\n if (appVersion) {\n result.appVersion = appVersion;\n bucket.app_version = appVersion;\n }\n\n // userEmail → its OWN key. OPT-IN PII, optionally folded. NEVER touches userId.\n const email = cleanString(ctx.userEmail);\n if (email) {\n if (ctx.hashEmail) {\n bucket.user_email = hashEmailFnv1a(email);\n bucket.user_email_hashed = true;\n } else {\n bucket.user_email = email;\n }\n }\n\n // extra → namespaced + scalar-coerced.\n Object.assign(bucket, namespaceExtra(ctx.extra));\n\n if (Object.keys(bucket).length > 0) result.userContext = bucket;\n return result;\n};\n","/**\n * identityRecord — the ONE provenance-carrying shape for an id the kit puts on the wire.\n *\n * WHY IT EXISTS. `session_id` and `device_key` are bare `string`s minted independently by four\n * subsystems, and nothing anywhere recorded WHERE a given id came from. Every id-layer defect this\n * release fixes is a direct consequence of that one omission:\n *\n * • a rejecting storage adapter's in-memory id was indistinguishable from a persisted one, so the\n * kit injected a fresh per-launch join key on every launch — nothing carried `durable`.\n * • an app-OPEN session id could be posted into a field that means the ONBOARDING session, and the\n * caller was told `true` — nothing carried `space`.\n * • an auto-minted `wdev_*` could be injected beside a device id the host demonstrably owns on\n * another surface, silently — nothing carried `source`.\n *\n * WHAT THIS IS, AND WHAT IT DELIBERATELY IS NOT. It is a small record plus a process-wide registry of\n * the ids a HOST supplied. It is NOT a branded-type refactor (`OnboardingSessionId` / `AppSessionId` /\n * `DeviceKey` across every signature) — that is real value and it is deferred, because it touches\n * every file and is not what makes a number correct this week. Nothing here changes the wire.\n *\n * WHY A `Symbol.for` REGISTRY. Same reason as `analytics/currentSession` and `context/deviceId`: tsup\n * inlines a separate copy of a module into each bundle (`.` and `./analytics`), so a plain module\n * `let` would give every bundle its own registry and the cross-surface question this exists to answer\n * (\"did ANY surface in this process get a host-supplied device key?\") would read `no` from the wrong\n * copy. `Symbol.for` resolves to one slot on `globalThis` no matter how many copies exist.\n */\n\n/**\n * Which id space a value belongs to. These are NOT interchangeable, and the whole point of naming\n * them is that a value from one space must never be posted into a field that means another:\n * • `onboarding-session` — the A2A `contextId` for ONE onboarding run.\n * • `app-session` — the per-app-open session id (`app.session_started`).\n * • `device` — the per-install `device_key`; the only cross-family join key.\n */\nexport type IdentitySpace = \"onboarding-session\" | \"app-session\" | \"device\";\n\n/** Where the value came from: the host handed it over, or the kit minted it. */\nexport type IdentitySource = \"host\" | \"auto\";\n\n/** An id plus everything a consumer needs to decide whether it may use it. */\nexport type IdentityRecord = {\n /** The id itself, trimmed. Never empty (a blank input yields no record at all). */\n value: string;\n /** Which id space {@link value} belongs to. */\n space: IdentitySpace;\n /** `host` = the integrator supplied it; `auto` = the kit minted it. */\n source: IdentitySource;\n /**\n * Whether the value was actually PERSISTED (or adopted from persistence), as opposed to living\n * only in this process's memory. A non-durable auto id is a DIFFERENT id on the next launch, which\n * for a `device` value is worse than no value at all: the server counts `min_sessions` by distinct\n * opens grouped on `device_key`, so a per-launch key corrupts the counter rather than leaving it\n * empty. A host-supplied value is durable by definition — the host owns its lifetime.\n */\n durable: boolean;\n};\n\n/** Input to {@link resolveIdentity}. `value` is `unknown` so callers can pass a raw prop through. */\nexport type ResolveIdentityInput = {\n value: unknown;\n space: IdentitySpace;\n source: IdentitySource;\n /** Defaults to `true` for a host value (the host owns its lifetime) and `false` otherwise. */\n durable?: boolean;\n /** Tenant/app id — two tenants in one process never share a provenance entry. */\n scope?: string;\n};\n\n/**\n * Well-known key into the runtime-global symbol registry — one provenance registry per process.\n *\n * @globalSlot LATCH — the REGISTRY OBJECT is created once and its identity is then stable. A second\n * write drops every host id recorded so far, so `hostIdentity()` answers \"no host key in this\n * process\" for a process that demonstrably has one, and the kit injects its own `wdev_*` beside it\n * without warning. Its CONTENTS are live (`host` gains an entry on every host-sourced\n * `resolveIdentity`), so both accessors go through `provenanceRegistry()` on every call.\n */\nconst IDENTITY_PROVENANCE_SLOT: unique symbol = Symbol.for(\"@wireai/activation:identityProvenance\");\n\n/** `\"<space>:<scope>\"` → the HOST-supplied value seen for it. Auto values are never recorded. */\ntype ProvenanceRegistry = { host: Map<string, string> };\n\ntype GlobalWithProvenance = typeof globalThis & {\n [IDENTITY_PROVENANCE_SLOT]?: ProvenanceRegistry;\n};\n\nconst provenanceGlobal = globalThis as GlobalWithProvenance;\n\nconst provenanceRegistry = (): ProvenanceRegistry => {\n const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];\n if (existing) return existing;\n const created: ProvenanceRegistry = { host: new Map() };\n provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;\n return created;\n};\n\nconst provenanceKey = (space: IdentitySpace, scope?: string): string =>\n `${space}:${scope ?? \"default\"}`;\n\n/**\n * Build an {@link IdentityRecord} from a candidate value, or `undefined` when there is nothing usable\n * (a non-string, or blank after trimming) — so a caller can `if (record)`-gate instead of guessing\n * whether an empty string means \"none\" or \"not yet\".\n *\n * SIDE EFFECT, deliberate and the reason this is a function and not an object literal: a `host`-sourced\n * record is RECORDED on the process registry, so a later surface can ask {@link hostIdentity} whether\n * this process demonstrably owns a host id in that space. That is what turns \"the kit injected its own\n * key\" from a silent third id space into a warnable condition. Never throws.\n */\nexport const resolveIdentity = (input: ResolveIdentityInput): IdentityRecord | undefined => {\n if (typeof input.value !== \"string\") return undefined;\n const value = input.value.trim();\n if (!value) return undefined;\n const durable = input.durable ?? input.source === \"host\";\n if (input.source === \"host\") {\n provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);\n }\n return { value, space: input.space, source: input.source, durable };\n};\n\n/**\n * The HOST-supplied id this process has seen for a space, or `undefined` when every surface so far\n * let the kit mint its own. Answers the cross-surface question no single mount can answer alone:\n * \"does this app own a device id that this particular mount was not given?\"\n */\nexport const hostIdentity = (space: IdentitySpace, scope?: string): string | undefined =>\n provenanceRegistry().host.get(provenanceKey(space, scope));\n\n/** Test-only: forget every recorded host identity so a unit test starts from a clean registry. */\nexport const resetIdentityProvenance = (): void => {\n provenanceRegistry().host.clear();\n};\n","/**\n * deviceId — mint a stable, NON-PII, per-install device id the kit owns when the host supplies\n * none. This is the headline of \"device fully automatic\": the analytics façade auto-mints ONE id,\n * persists it via the host's `storage` abstraction, and reuses it on every subsequent open — so\n * `user_context.device_key` is ALWAYS present and the server's review/questionnaire gating +\n * A/B stickiness (both key on `device_key`) work out of the box, with zero host wiring.\n *\n * WHY it is NOT PII and adds NO dependency (the kit's hard rules):\n * The id is a random token generated from `Date.now()` + `Math.random()` — it carries NO hardware\n * identifier, NO IDFA/GAID, NO fingerprint. It is a first-party per-install correlation key, the\n * same privacy category as a first-party cookie: it groups a single install's sessions and cannot\n * identify a person or be joined across apps. There is NO `uuid` (or any) dependency — a\n * time+random scheme is sufficient because the id is minted ONCE and then persisted, so global\n * uniqueness across the fleet is not required (a per-install collision is astronomically unlikely\n * and inconsequential — worst case two installs share a bucket).\n *\n * A host that wants its OWN device id still wins: pass `WireUserContext.deviceKey` and the kit uses\n * that verbatim and never mints/persists an auto id.\n */\n\nimport { resolveIdentity, type IdentityRecord } from \"../identity/identityRecord\";\n\n/** Prefix so an auto-minted id is visibly the kit's (distinguishable from a host-supplied `deviceKey`). */\nexport const AUTO_DEVICE_ID_PREFIX = \"wdev_\";\n\n/** The storage key the façade persists the auto-minted id under (namespaced per `appId`). */\nexport const deviceIdStorageKey = (appId?: string): string =>\n `wireai:analytics:deviceKey:${appId ?? \"default\"}`;\n\n/** One 32-bit base-36 chunk of randomness. Two chunks are concatenated for a wider token. */\nconst randomChunk = (): string =>\n Math.floor(Math.random() * 0x100000000)\n .toString(36)\n .padStart(6, \"0\");\n\n/**\n * Mint a fresh per-install device id. Dependency-free (`Date.now()` + `Math.random()`), never\n * throws, and returns a NEW value on every call — the façade mints ONCE and persists, so this is\n * called at most once per install (then the persisted value is reused). Two random chunks plus the\n * timestamp keep the token wide enough that a per-install collision is not a practical concern.\n */\nexport const mintDeviceId = (): string => {\n const time = Date.now().toString(36);\n return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;\n};\n\n// ── The ONE auto device key per install ──────────────────────────────────────────────────────\n//\n// WHY A REGISTRY AND NOT A `let` PER FACTORY: `createAnalytics` and `createWireActivation` each\n// used to mint their OWN id synchronously and then race a storage read to overwrite it. A host that\n// creates BOTH (the documented wiring: a façade for `track`/`screen`, an activation instance for the\n// gate-firing `wire.track`) therefore had TWO auto ids for ONE install. Every event carried whichever\n// id its own surface minted, so the `device_key` the server groups a device's sessions under — the\n// key `min_sessions`, A/B arm stickiness, and the purchase↔onboarding join all read — SPLIT in two.\n// On a first run both also wrote their own id to the same storage slot, so which one survived was a\n// coin flip. Same failure class as joining two event families on disjoint id spaces: no error, just\n// halved counts and a join that misses.\n//\n// The fix is the pattern this repo already uses for `currentSession` and `activation revalidation`:\n// ONE value in a `globalThis` slot keyed by `Symbol.for(...)`, so every inlined copy of this module\n// (tsup duplicates modules across the `.` / `./analytics` bundles) addresses the SAME registry.\n// Keyed by `appId` so two tenants in one process never share an id.\n//\n// RESIDUAL WINDOW: the storage read is async, so an event emitted in the milliseconds before\n// hydration completes carries the freshly minted id rather than the persisted one. The registry makes\n// every surface agree on WHICH id that is; it does not make the read sync. A caller that can afford to\n// wait (`useLifecycleEvents` / `useSessionStart`, both app-open paths — see `hydrateDeviceIdentity`)\n// must await instead: their events are the ONLY ones the server counts `min_sessions` from, so a\n// per-launch id there is not a millisecond of noise, it is a counter that can never exceed 1. Waiting\n// is only half of it — the settled outcome can still be NON-DURABLE, and those callers refuse it.\n\n/**\n * Well-known key into the runtime-global symbol registry — one auto-id registry across every bundle.\n *\n * @globalSlot LATCH — the REGISTRY OBJECT is created once and its identity is then stable. A second\n * write empties `keys`/`hydrating`/`pending`, so the next surface mints a SECOND auto id for one\n * install and the `device_key` the server joins sessions on splits in two — the halved-counter\n * defect this registry exists to close. Its CONTENTS are live (the id per `appId` is replaced when\n * an async hydration adopts a persisted value), so callers must re-read through\n * `resolveAutoDeviceKey()` rather than hold the string a mount happened to see first.\n */\nconst AUTO_DEVICE_KEY_SLOT: unique symbol = Symbol.for(\"@wireai/activation:autoDeviceKeys\");\n\n/** What a hydration settled on: the id, and whether persistence actually CONFIRMED it.\n *\n * `durable: false` means the id lives only in this process's memory — the adapter rejected, threw,\n * or there was no adapter at all. That distinction is the whole point here: a string is a string, so\n * before 0.13.0 a caller could not tell a persisted id from a per-launch mint, and the auto-join\n * gate (`Boolean(storage)`) was reading the PRESENCE of the prop rather than the SUCCESS of the\n * write. See {@link hydrateDeviceIdentity}. */\ntype HydrationOutcome = { value: string; durable: boolean };\n\n/** The shared registry: the live id per `appId`, the set of appIds whose hydration already started,\n * and the in-flight (or settled) hydration promise per `appId` so a waiter can join it. */\ntype AutoDeviceKeyRegistry = {\n keys: Map<string, string>;\n hydrating: Set<string>;\n pending?: Map<string, Promise<HydrationOutcome>>;\n};\n\ntype GlobalWithDeviceKeys = typeof globalThis & {\n [AUTO_DEVICE_KEY_SLOT]?: AutoDeviceKeyRegistry;\n};\n\nconst deviceKeyGlobal = globalThis as GlobalWithDeviceKeys;\n\nconst autoDeviceKeyRegistry = (): AutoDeviceKeyRegistry => {\n const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];\n if (existing) return existing;\n const created: AutoDeviceKeyRegistry = { keys: new Map(), hydrating: new Set() };\n deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;\n return created;\n};\n\n/** The persistence subset {@link resolveAutoDeviceKey} needs (a strict subset of `WireOnboardingStorage`). */\nexport type DeviceKeyStorage = {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n};\n\n/** Options for {@link resolveAutoDeviceKey}. Omitting `storage` gives a PROCESS-scoped id, not a\n * per-install one — see the caller notes: a caller with no persistence must decide whether a\n * per-launch id is better or worse than no id for its metric. */\nexport interface ResolveAutoDeviceKeyOptions {\n /** Tenant/app id — namespaces both the registry entry and the storage slot. */\n appId?: string;\n /** Host persistence. Present → the id survives launches. Absent → process-scoped only. */\n storage?: DeviceKeyStorage;\n}\n\n/**\n * Start (or join) the SINGLE-FLIGHT storage read for `appId` and resolve to the {@link HydrationOutcome}\n * it settles on. The promise is parked on the registry so a later `hydrateAutoDeviceKey` awaits the\n * SAME read instead of starting a second one. Never rejects: any storage failure resolves to the live\n * id with `durable: false`.\n *\n * TWO OUTCOMES, NOT ONE STRING:\n * • ADOPTED (`durable: true`) — a persisted id was read back, or the freshly minted one was\n * written successfully. The next launch will see the same id.\n * • DEGRADED (`durable: false`) — the adapter rejected, threw, or returned nothing and then failed\n * the write. The id is real but PROCESS-scoped, so anything that\n * counts a device across launches must refuse it.\n *\n * A DEGRADED outcome also drops the registry latches so the NEXT caller starts a fresh read. A\n * cold-boot storage lock is transient; caching it as a verdict for the process lifetime turned a\n * one-second problem into a whole-launch one, and nothing ever retried.\n */\nconst startHydration = (\n registry: AutoDeviceKeyRegistry,\n appId: string,\n storage: DeviceKeyStorage,\n minted: string,\n): Promise<HydrationOutcome> => {\n if (!registry.pending) registry.pending = new Map();\n const existing = registry.pending.get(appId);\n if (existing) return existing;\n\n const slot = deviceIdStorageKey(appId);\n const degraded = (): HydrationOutcome => ({ value: registry.keys.get(appId) ?? minted, durable: false });\n const adopted = (value: string): HydrationOutcome => ({ value, durable: true });\n let run: Promise<HydrationOutcome>;\n try {\n run = Promise.resolve(storage.getItem(slot))\n .then((saved) => {\n const persisted = typeof saved === \"string\" && saved.trim() ? saved.trim() : undefined;\n if (persisted) {\n registry.keys.set(appId, persisted);\n return adopted(persisted);\n }\n // First run on this install: persist the id we just minted so the next launch adopts it.\n // ONLY a resolved write earns `durable` — a rejected one leaves the id in memory alone.\n return Promise.resolve(storage.setItem(slot, minted)).then(\n () => adopted(registry.keys.get(appId) ?? minted),\n degraded,\n );\n })\n .catch(degraded);\n } catch {\n // A storage adapter that throws synchronously — degrade to the in-memory id.\n run = Promise.resolve(degraded());\n }\n registry.pending.set(appId, run);\n // Retry-on-failure: release the latches once a degraded outcome settles, so a later caller\n // is not permanently bound to one bad read. Registered AFTER the `set` above, so the clean-up can\n // never race ahead of the entry it is clearing.\n void run.then((outcome) => {\n if (outcome.durable) return;\n registry.pending?.delete(appId);\n registry.hydrating.delete(appId);\n });\n return run;\n};\n\n/**\n * The ONE auto-minted `device_key` for an install, shared by every kit surface.\n *\n * SYNCHRONOUS by contract (a fire-and-forget event path cannot await): returns the current live id\n * immediately, minting one on first call. When `storage` is supplied it also kicks off a SINGLE\n * hydration per `appId` that adopts the persisted id (or persists the freshly minted one). Callers\n * should call this per EVENT rather than caching the return value, so an event built after hydration\n * carries the persisted id.\n *\n * A host-supplied `deviceKey` always wins — callers must short-circuit before reaching this.\n * Never throws: a missing, hung, or rejecting storage adapter degrades to the in-memory id.\n */\nexport const resolveAutoDeviceKey = (opts: ResolveAutoDeviceKeyOptions = {}): string => {\n const registry = autoDeviceKeyRegistry();\n const appId = opts.appId ?? \"default\";\n\n let id = registry.keys.get(appId);\n if (!id) {\n id = mintDeviceId();\n registry.keys.set(appId, id);\n }\n\n const storage = opts.storage;\n // Single-flight: the FIRST caller with storage owns hydration for this appId; later callers just\n // read whatever the registry currently holds.\n if (storage && !registry.hydrating.has(appId)) {\n registry.hydrating.add(appId);\n void startHydration(registry, appId, storage, id);\n }\n\n return registry.keys.get(appId) ?? id;\n};\n\n/**\n * The AWAITABLE sibling of {@link resolveAutoDeviceKey}: resolve to the auto `device_key` AFTER the\n * persisted id has been read back (or written, on a first run), so the caller stamps the id this\n * install will keep rather than the one that was minted a millisecond ago.\n *\n * WHY IT EXISTS: `resolveAutoDeviceKey` is synchronous by contract, so a caller firing at mount got\n * the freshly minted id and the persisted one landed milliseconds later. For most events that is\n * noise. For `app.session_started` it is the whole metric: the server computes `min_sessions` by\n * counting distinct opens grouped by `device_key`, so a per-launch key there makes the counter\n * structurally incapable of exceeding 1, and it splits `first_open` off from every event that\n * follows it. Only a caller that can afford one storage read should use this; the fire-and-forget\n * event paths must stay on the sync function.\n *\n * Never throws or rejects: a missing, hung, or rejecting adapter resolves to the in-memory id, and\n * with no `storage` it resolves immediately (there is nothing to hydrate from).\n *\n * ⚠️ IT RETURNS A BARE STRING, so it CANNOT say whether the id survives the launch — a degraded\n * adapter resolves to the in-memory mint and reads identically to a persisted one. No kit surface\n * uses it any more (0.14.0 moved the two lifecycle hooks off it): a caller writing a cross-launch\n * join key wants {@link hydrateDeviceIdentity} and its `durable` flag. Kept as public API for a host\n * that only wants \"the id\", never as the way to decide whether to stamp one.\n */\nexport const hydrateAutoDeviceKey = async (\n opts: ResolveAutoDeviceKeyOptions = {},\n): Promise<string> => (await hydrateDeviceIdentity(opts))?.value ?? resolveAutoDeviceKey(opts);\n\n/**\n * The PROVENANCE-CARRYING sibling of {@link hydrateAutoDeviceKey}: the same awaited read, but it\n * answers \"is this id one this install will KEEP?\" instead of only \"what is the id?\".\n *\n * WHY IT EXISTS. `<WireOnboarding>` gated auto-injection on `Boolean(storage)` — the presence of\n * the prop — because a string carries no provenance and there was nothing better to gate on. A\n * REJECTING adapter therefore injected a fresh `wdev_*` on every launch: strictly worse than\n * injecting nothing, since the server counts `min_sessions` by distinct opens grouped on `device_key`,\n * so a per-launch key corrupts that counter AND inflates distinct-device counts. Callers that write a\n * key onto the wire as a cross-launch join must read `durable` and refuse a `false`.\n *\n * Resolves `undefined` only when there is no usable id at all. With no `storage` it resolves\n * immediately with `durable: false` — a process-scoped id is exactly what \"no persistence\" means.\n * Never throws or rejects.\n */\nexport const hydrateDeviceIdentity = async (\n opts: ResolveAutoDeviceKeyOptions = {},\n): Promise<IdentityRecord | undefined> => {\n // Mint + register synchronously first, so a waiter and a concurrent sync caller share ONE id.\n const id = resolveAutoDeviceKey(opts);\n const appId = opts.appId ?? \"default\";\n const record = (value: string, durable: boolean): IdentityRecord | undefined =>\n resolveIdentity({ value, space: \"device\", source: \"auto\", durable, scope: appId });\n\n if (!opts.storage) return record(id, false);\n const registry = autoDeviceKeyRegistry();\n const pending = registry.pending?.get(appId);\n // No pending entry means a previous hydration already settled DEGRADED and released its latches\n // (see `startHydration`), so the live id is the in-memory one — real, but not durable.\n if (!pending) return record(registry.keys.get(appId) ?? id, false);\n const outcome = await pending;\n return record(outcome.value, outcome.durable);\n};\n\n/** Test-only: forget every auto id + hydration flag so a unit test starts from a clean registry. */\nexport const resetAutoDeviceKeys = (): void => {\n const registry = autoDeviceKeyRegistry();\n registry.keys.clear();\n registry.hydrating.clear();\n registry.pending?.clear();\n};\n","/**\n * analyticsFacade — a Segment/PostHog-shaped developer API (`track` / `screen` / `identify`)\n * over the kit's OWN offline-first event queue. One package, one key, one line:\n *\n * const analytics = createAnalytics({ serverUrl, apiKey, storage });\n * analytics.track(\"content_share\", { source: \"feed\" });\n * analytics.screen(\"Home\", { tab: \"explore\" });\n * analytics.identify(\"u_123\", { plan: \"pro\" });\n *\n * WHY a façade: the primitives already exist (`createEventQueue` for durable offline-first\n * transport, `buildContextEnvelope` for the non-PII device context, the `identify` client-event\n * contract for user binding), but a host had to wire them together by hand. This composes them\n * into the familiar analytics-SDK surface so a consumer gets the ergonomics with NO second\n * install and NO second key — the same `{ serverUrl, apiKey }` creds as onboarding.\n *\n * ROUTING: every call builds a `ClientEvent` and `enqueue`s it. The queue stamps the context\n * envelope (device + host scalars), persists offline, batches, retries, and dequeues on ack —\n * so `track`/`screen`/`identify` inherit offline-first durability for free. `screen` routes\n * through the SAME queue rather than the direct `reportAppEvent` POST (a deliberate upgrade:\n * screen views survive being offline too).\n *\n * DEPENDENCY-FREE + TREE-SHAKEABLE: this module imports ONLY the queue, the envelope builder,\n * the client-event types + session-id seed, and the identity sanitizer. It reaches NO\n * onboarding / showcase / review UI, so an analytics-only consumer bundles none of it. React is\n * absent here on purpose — the optional React glue is the thin `useAnalytics` hook.\n *\n * FIRE-AND-FORGET: no method throws into the UI or blocks — the queue already guarantees that.\n */\nimport { buildContextEnvelope, type ContextEnvelope } from \"./contextEnvelope\";\nimport { ensureCurrentSessionId } from \"./currentSession\";\nimport { createEventQueue, type EventQueue, type EventQueueOptions } from \"./eventQueue\";\nimport type { ClientEvent } from \"./reportClientEvent\";\nimport {\n analyticsUserIdStorageKey,\n clearPiiFromContext,\n resolveUserContext,\n type WireUserContext,\n} from \"../context/userContext\";\nimport { resolveAutoDeviceKey, type ResolveAutoDeviceKeyOptions } from \"../context/deviceId\";\nimport { detectAppVersion } from \"../device/appVersion\";\nimport { resolveIdentity } from \"../identity/identityRecord\";\nimport { looksLikeEmail, sanitizeUserId } from \"../identity/userIdentity\";\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/** Arbitrary non-PII event properties. Serialized to the event's `meta` (a JSON string) on the wire. */\nexport type AnalyticsProps = Record<string, unknown>;\n\n/**\n * Tenant transport + context inputs for {@link createAnalytics}. `serverUrl`/`apiKey` are the\n * SAME creds as onboarding (never a second key). The rest feed the context envelope + the queue's\n * offline persistence — all optional.\n */\nexport type CreateAnalyticsConfig = {\n /** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */\n serverUrl: string;\n /** Tenant API key; sent as `Authorization: Bearer`. */\n apiKey: string;\n /**\n * ⚠️ OPT-OUT KNOB, not a default. Supplying a `sessionId` FREEZES the correlation id: every event\n * this instance ever sends (including `identify`) is pinned to that one id, and the instance stops\n * following the LIVE per-open session the server registered via `app.session_started`. Lifecycle\n * analytics then collapse onto a single device-scoped session — one \"first open\", forever.\n *\n * OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and\n * when no open has been registered yet it mints one AND registers it, so every later surface joins\n * the same session instead of each inventing its own. Pass one ONLY if your host runs its own\n * session lifecycle and owns the id the server should correlate on.\n */\n sessionId?: string;\n /** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */\n appId?: string;\n /**\n * Host persistence (AsyncStorage-compatible subset) for offline-first durability. When omitted,\n * the queue runs in the documented in-memory mode (survives re-renders, not app kills).\n */\n storage?: EventQueueOptions[\"storage\"];\n /** Host app version, e.g. \"1.4.2\" (host-injected; stamped onto every event's context). */\n appVersion?: string;\n /** Host native build number, e.g. \"412\" (host-injected). */\n appBuild?: string;\n /** Host connectivity signal, e.g. \"wifi\" | \"cellular\" — read fresh per event via the provider. */\n networkType?: string;\n /**\n * The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque\n * user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via\n * {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional.\n *\n * NOTE on `deviceKey`: you do NOT need to supply one. When omitted, the kit auto-mints a stable,\n * non-PII per-install `device_key`, persists it via `storage`, and reuses it every open (in-memory\n * fallback without storage) — so `user_context.device_key` is ALWAYS present for the server's\n * review/questionnaire gating + A/B stickiness. Supply `deviceKey` only to use your OWN id (it wins).\n */\n userContext?: WireUserContext;\n /**\n * ESCAPE HATCH for the email-shape guard. By default `identify(id)` and a `setUserContext({ userId })`\n * REFUSE to bind an id that looks like an email (`local@domain.tld`) and warn in dev — because a\n * raw email in the opaque `user_id` is a PII leak; an email belongs in the opt-in\n * `userContext.userEmail` field. Set `true` ONLY if your real internal user id genuinely IS an\n * email address and you accept it as the pseudonymous key. Default `false` (guard on).\n */\n allowEmailAsUserId?: boolean;\n};\n\n/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */\nexport type AnalyticsOptions = Partial<\n Pick<EventQueueOptions, \"maxSize\" | \"batchSize\" | \"baseBackoffMs\" | \"maxBackoffMs\" | \"maxRetries\">\n>;\n\n/**\n * The developer-facing analytics surface. `track`/`screen`/`identify` are the Segment/PostHog-shaped\n * API; `flush`/`notifyOnline`/`size` expose the underlying queue so a host can drive reconnect\n * draining (load-bearing for offline-first) and inspect the pending buffer.\n */\nexport type Analytics = {\n /** Record an in-app event: `event_type='app_event'`, `question_key=<event>`, `props`→`meta`. */\n track(event: string, props?: AnalyticsProps): void;\n /** Record a screen view: `event_type='app_event'`, `question_key='screen'`, `meta={ screen, ...props }`. */\n screen(name: string, props?: AnalyticsProps): void;\n /** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */\n identify(userId: string, traits?: AnalyticsProps): void;\n /**\n * Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow\n * merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also\n * binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.\n */\n setUserContext(partial: Partial<WireUserContext>): void;\n /**\n * LOGOUT: unbind the current user so a shared device never attributes user B's events to user A.\n * Clears the in-memory `boundUserId`, strips the PII / pseudonymous fields (`userId`, `userEmail`,\n * `extra`) from the bound {@link WireUserContext} (keeping the non-PII `device_key` + `appVersion`,\n * which group a DEVICE not a user), and removes the persisted `wireai:analytics:userId:<appId>`\n * key so it cannot be rehydrated on the next launch. Subsequent events are anonymous until the\n * next `identify` / `setUserContext`. Fire-and-forget; mirrors the `reset()` convention on the\n * screen tracker. The standalone `clearUserContext({ storage, appId })` covers the `wire` path.\n */\n reset(): void;\n /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */\n flush(): void;\n /** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */\n notifyOnline(): void;\n /** Current pending (in-memory) count. */\n size(): number;\n /**\n * Tear the instance's event queue down when its owner goes away (see {@link EventQueue.dispose}).\n * Idempotent, never throws. Call it if you build an instance per screen / per mount: the queue\n * claims a persisted storage slot, and a replacement built before the old one released it lands on\n * a rotated `…#2` key that no later launch ever reads. `useAnalytics` does this for you.\n */\n dispose(): void;\n};\n\n/**\n * Create a bound analytics instance. Seeds one correlation `sessionId`, builds an offline-first\n * queue over the tenant transport, and passes a fresh-per-event context envelope PROVIDER so the\n * connectivity type is read at enqueue time. The bound user id starts unset (see `identify`).\n */\nexport const createAnalytics = (\n config: CreateAnalyticsConfig,\n options: AnalyticsOptions = {},\n): Analytics => {\n // The session id every event correlates to. Precedence: an explicit `config.sessionId` freezes the\n // id (opt-out of the reuse); otherwise reuse the LIVE per-open session the server saw (via\n // `app.session_started`) so `identify`/app-events don't mint a fresh id the server back-fills into a\n // phantom session.\n //\n // `ensureCurrentSessionId` (not the bare `getCurrentSessionId`) is what closes the facade-first\n // ordering hole: a `track` that runs BEFORE the root lifecycle effect used to fall back to a\n // per-instance id this facade never REGISTERED, so the server saw a session it had no\n // `session_started` for and back-filled a phantom one. The two paths that already mint on the wire\n // (`wire.track`, `reportAppEvent`) both register; this one only read. Registering makes the\n // fallback id the id every LATER surface joins on, and when an open IS registered this is exactly\n // `getCurrentSessionId()` — so nothing changes for a host that mounts lifecycle first.\n const resolveSessionId = (): string => config.sessionId ?? ensureCurrentSessionId();\n\n // A frozen id is almost always a mistake (it silently flattens every open into ONE session), so\n // name it once at construction — same dev-only channel as the email-shape guard below.\n if (config.sessionId) {\n warnInDev(\n \"[wireai] createAnalytics({ sessionId }) PINS every event from this instance to that one \" +\n \"frozen id and opts out of the live per-open session (app.session_started) — lifecycle \" +\n \"analytics collapse onto a single device-scoped id. Remove it unless your host runs its \" +\n \"own session lifecycle.\",\n );\n }\n\n // The auto-detected host app version, read ONCE here (cheap, sync, never throws). It backs the\n // `user_context.app_version` fallback below: without it a host that passes no `config.appVersion`\n // got the detected version on `device.appVersion` only, leaving the user_context field absent.\n const detectedAppVersion = detectAppVersion();\n\n // The mutable rich user-context: seeded at init, updated via `setUserContext`. Resolved fresh on\n // every event so a post-mount update (login) takes effect immediately. Declared before the envelope\n // provider so the provider can read the current `userContext.appVersion` (see below).\n let userContext: WireUserContext = { ...(config.userContext ?? {}) };\n\n // Auto device id (the headline: \"device\" fully automatic). When the host supplies NO `deviceKey`,\n // the kit mints ONE stable, non-PII per-install id, PERSISTS it via the host `storage`, and reuses it\n // on every subsequent open — so `user_context.device_key` is ALWAYS present (the server's\n // review/questionnaire gating + A/B stickiness both key on it) with zero host wiring. A host-supplied\n // `deviceKey` still wins (see `applyContext`). Falls back to an in-memory id (stable for this\n // instance) when no storage is available.\n const hostDeviceKeyAtInit =\n typeof config.userContext?.deviceKey === \"string\" && config.userContext.deviceKey.trim()\n ? config.userContext.deviceKey.trim()\n : undefined;\n // Record a HOST-supplied key on the process provenance registry, so a `<WireOnboarding>` mount that\n // was NOT given one can tell \"this app owns no device id\" (fine, inject) from \"this app owns one\n // and forgot it here\" (the silent third id space). Recording only; nothing reads it here.\n resolveIdentity({\n value: hostDeviceKeyAtInit,\n space: \"device\",\n source: \"host\",\n scope: config.appId,\n });\n // The auto id comes from the ONE process-wide registry (`resolveAutoDeviceKey`), NOT a mint local to\n // this instance. A host that also builds a `createWireActivation` instance used to get a SECOND,\n // different auto id for the same install, splitting `device_key` across two id spaces — see the\n // registry note in context/deviceId.ts. Resolved lazily per event so hydration is picked up.\n const autoDeviceKeyOptions: ResolveAutoDeviceKeyOptions = {\n appId: config.appId,\n // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).\n storage: hostDeviceKeyAtInit ? undefined : config.storage,\n };\n // Start hydration AT CONSTRUCTION (not at the first event) so the persisted id is adopted as early\n // as it used to be. The return value is deliberately discarded — every event re-resolves.\n if (!hostDeviceKeyAtInit) resolveAutoDeviceKey(autoDeviceKeyOptions);\n\n // A provider (not a fixed value) so `networkType`, the current session id, AND the effective app\n // version are evaluated fresh on every enqueue. An explicit `WireUserContext.appVersion` (a host that\n // set the version ONLY inside `userContext`) now flows into `device.appVersion` too — not just\n // `user_context.app_version` — so the server's `by_app_version` breakdown (which reads\n // `device.appVersion`) agrees. Explicit wins over the auto-detected device version;\n // `buildContextEnvelope` keeps the auto value when neither is set.\n const envelope = (): ContextEnvelope =>\n buildContextEnvelope({\n sessionId: resolveSessionId(),\n appVersion: userContext.appVersion ?? config.appVersion,\n appBuild: config.appBuild,\n networkType: config.networkType,\n });\n\n const queue: EventQueue = createEventQueue({\n target: { serverUrl: config.serverUrl, apiKey: config.apiKey },\n storage: config.storage,\n appId: config.appId,\n envelope,\n ...options,\n });\n\n // Per-session, in-memory user binding. Seeded from the init context, then persisted across\n // launches when storage is provided.\n let boundUserId: string | undefined = sanitizeUserId(config.userContext?.userId);\n const storageKey = analyticsUserIdStorageKey(config.appId);\n\n // SUPERSESSION LATCH for the construction-time hydration below.\n //\n // The read's guard used to be `saved && !boundUserId` alone, which asks \"did something bind an id\n // while I was reading?\". A LOGOUT is the one case where the answer is a deliberate `undefined`, so\n // a read that landed after `reset()` sailed through the guard and re-bound the user who had just\n // logged out — onto every subsequent event, which is precisely what `reset()` promises cannot\n // happen. The window is the first storage read of a cold start, and a logout inside it is a real\n // sequence, not a contrived one. An empty binding must be able to mean \"cleared on purpose\".\n let hydrationSuperseded = false;\n\n if (config.storage) {\n void config.storage\n .getItem(storageKey)\n .then((saved) => {\n // A `reset()` that already ran wins, whatever this read says.\n if (hydrationSuperseded) return;\n // Don't clobber an explicit init-context user id with a stale persisted one.\n if (saved && !boundUserId) boundUserId = saved;\n })\n .catch(() => {});\n }\n\n // Stamp the resolved rich context onto an event: the `user_context` bucket (device_key, app_version,\n // opt-in user_email, namespaced `custom.*`) and the top-level opaque `user_id`. Never overwrites a\n // key the caller already set (so `identify`'s explicit `user_id` and any caller `user_context` win).\n const applyContext = (event: ClientEvent): void => {\n // Host `deviceKey` wins; otherwise the auto-minted/persisted per-install id fills it in so\n // `user_context.device_key` is always present.\n const hostDeviceKey =\n typeof userContext.deviceKey === \"string\" && userContext.deviceKey.trim()\n ? userContext.deviceKey\n : undefined;\n const resolved = resolveUserContext(\n // `??` is lazy on purpose: a host-supplied key must never even touch the auto registry.\n { ...userContext, deviceKey: hostDeviceKey ?? resolveAutoDeviceKey(autoDeviceKeyOptions) },\n { autoAppVersion: config.appVersion ?? detectedAppVersion },\n );\n if (resolved.userContext) {\n event.user_context = { ...resolved.userContext, ...(event.user_context ?? {}) };\n }\n if (boundUserId && !event.user_id) event.user_id = boundUserId;\n };\n\n // Email-shape guard for the OPAQUE user id. A raw email bound as `user_id` is a PII leak (it\n // belongs in the opt-in `user_context.user_email`), so refuse it and warn in dev — unless the host\n // opted in via `allowEmailAsUserId`. Returns the id to bind, or `undefined` to refuse.\n const guardUserId = (clean: string): string | undefined => {\n if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;\n warnInDev(\n \"[wireai] identify() was called with an email-shaped id. A raw email must NOT be the opaque \" +\n \"user_id (PII leak) — pass it as userContext.userEmail instead. Binding was skipped. Set \" +\n \"allowEmailAsUserId:true on createAnalytics if your user id genuinely is an email.\",\n );\n return undefined;\n };\n\n const setUserContext = (partial: Partial<WireUserContext>): void => {\n if (!partial || typeof partial !== \"object\") return;\n // Deep-merge `extra` so a partial update adds keys instead of replacing the whole map.\n const mergedExtra =\n partial.extra || userContext.extra\n ? { ...(userContext.extra ?? {}), ...(partial.extra ?? {}) }\n : undefined;\n userContext = { ...userContext, ...partial };\n if (mergedExtra) userContext.extra = mergedExtra;\n // A user id supplied here binds like `identify` so subsequent events carry `user_id` — through\n // the SAME email-shape guard (a raw email must not become the opaque user_id).\n const uid = sanitizeUserId(partial.userId);\n if (uid) {\n const bindable = guardUserId(uid);\n if (bindable) {\n boundUserId = bindable;\n if (config.storage) void config.storage.setItem(storageKey, bindable).catch(() => {});\n }\n }\n };\n\n const reset = (): void => {\n // Supersede any construction-time hydration still in flight, BEFORE clearing the binding: the\n // read cannot tell \"nobody bound anything yet\" from \"the user just logged out\", so the answer\n // has to come from here. Without this the read re-binds the logged-out user.\n hydrationSuperseded = true;\n // In-memory binding cleared: subsequent events carry no user_id until the next identify.\n boundUserId = undefined;\n // Strip PII from the rich context but keep the device-scope fields (device_key / app_version).\n userContext = clearPiiFromContext(userContext);\n // Remove the persisted binding so a relaunch can't rehydrate the previous user's id.\n if (config.storage) void config.storage.removeItem(storageKey).catch(() => {});\n };\n\n const track = (event: string, props?: AnalyticsProps): void => {\n if (!event) return;\n const clientEvent: ClientEvent = {\n event_type: \"app_event\",\n session_id: resolveSessionId(),\n question_key: event,\n };\n if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n const screen = (name: string, props?: AnalyticsProps): void => {\n if (!name) return;\n // Reuse the existing screen event shape (question_key='screen'); route through the queue for offline-first.\n const meta = { screen: name, ...(props ?? {}) };\n const clientEvent: ClientEvent = {\n event_type: \"app_event\",\n session_id: resolveSessionId(),\n question_key: \"screen\",\n meta: JSON.stringify(meta),\n };\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n const identify = (userId: string, traits?: AnalyticsProps): void => {\n const clean = sanitizeUserId(userId);\n // Blank / non-string → no binding, no event (sanitizeUserId returns undefined). >128 → truncated.\n if (!clean) return;\n // Email-shape guard: refuse to bind (and emit) a raw email as the opaque user_id unless opted in.\n const bindable = guardUserId(clean);\n if (!bindable) return;\n boundUserId = bindable;\n if (config.storage) {\n void config.storage.setItem(storageKey, bindable).catch(() => {});\n }\n const clientEvent: ClientEvent = {\n event_type: \"identify\",\n // Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to\n // the session it already saw instead of back-filling a phantom `session_started`.\n session_id: resolveSessionId(),\n user_id: bindable,\n };\n if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n return {\n track,\n screen,\n identify,\n setUserContext,\n reset,\n flush: queue.flush,\n notifyOnline: queue.notifyOnline,\n size: queue.size,\n dispose: queue.dispose,\n };\n};\n","/**\n * useAnalytics — a THIN optional React hook over the pure {@link createAnalytics} factory.\n *\n * It builds ONE analytics instance for the component's lifetime and returns it, so re-renders\n * never rebuild the queue or lose the in-memory user binding. React is a REQUIRED peer of the\n * kit, so importing it here is allowed; the hook adds NO other dependency. Mirrors the existing\n * `createScreenTracker` / `useScreenTracking` split — the factory stays React-free, this is the\n * glue.\n *\n * const analytics = useAnalytics({ serverUrl, apiKey, storage, appId });\n * analytics.track(\"content_share\", { source: \"feed\" });\n * // ...on reconnect: analytics.notifyOnline();\n */\nimport { useEffect, useRef } from \"react\";\n\nimport {\n createAnalytics,\n type Analytics,\n type AnalyticsOptions,\n type CreateAnalyticsConfig,\n} from \"./analyticsFacade\";\n\n/**\n * Build a per-mount analytics instance. `config`/`options` are read once at first render (the\n * instance is stable for the component's lifetime, held in a ref). Returns the {@link Analytics}\n * surface so the component can `track` / `screen` / `identify` and drive `notifyOnline` on reconnect.\n */\nexport const useAnalytics = (\n config: CreateAnalyticsConfig,\n options: AnalyticsOptions = {},\n): Analytics => {\n // One instance per mount; kept in a ref so re-renders never rebuild the queue or drop the binding.\n const ref = useRef<Analytics | undefined>(undefined);\n const prevKeys = useRef<string>(\"\");\n\n const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;\n if (!ref.current || prevKeys.current !== currentKeys) {\n prevKeys.current = currentKeys;\n // The instance being REPLACED (credentials changed, or an `appId` that arrived async) owns a\n // claimed storage slot and a live backoff timer. Hand them back before building the replacement,\n // or the new instance rotates onto a `…#2` key that no later launch reads, and the old one's\n // retry can still wake up and `removeItem` the slot the new one is using.\n ref.current?.dispose();\n ref.current = createAnalytics(config, options);\n }\n\n // A LATE-ARRIVING host device key must still reach the instance.\n //\n // `createAnalytics` copies `config.userContext` into a closure at construction and never re-reads\n // the prop, and `config.userContext.deviceKey` is deliberately NOT part of `currentKeys` (rebuilding\n // the instance would throw away the event queue's pending buffer). So a host that hydrates its\n // device id asynchronously — an AsyncStorage read that resolves after first render — used to be\n // stamped with the kit's auto-minted `wdev_*` id FOREVER, while a sibling `useWireActivation`\n // (which does key on it) rebuilt and used the real one. One install, two `device_key` values, in\n // the same app, on the key every gating rule and the purchase↔onboarding join reads.\n //\n // `setUserContext` is the non-destructive seam for exactly this: it updates the bound context in\n // place, so subsequent events carry the host key with no queue rebuild.\n const hostDeviceKey =\n typeof config.userContext?.deviceKey === \"string\" && config.userContext.deviceKey.trim()\n ? config.userContext.deviceKey.trim()\n : undefined;\n useEffect(() => {\n if (hostDeviceKey) ref.current?.setUserContext({ deviceKey: hostDeviceKey });\n }, [hostDeviceKey]);\n\n // UNMOUNT: give the storage claim and the retry timer back. A screen that mounts this hook is\n // built and torn down repeatedly (navigation, Fast Refresh, StrictMode's double-invoke), and each\n // rebuild used to leave the previous queue holding the appId's slot — so every remount after the\n // first persisted into `…#2`, `…#3`, and the next launch read none of them.\n useEffect(\n () => () => {\n ref.current?.dispose();\n ref.current = undefined;\n },\n [],\n );\n\n return ref.current;\n};\n"]}
1
+ {"version":3,"sources":["../../src/analytics/reportClientEvent.ts","../../src/utils/warnInDev.ts","../../src/analytics/currentSession.ts","../../src/reviews/transport.ts","../../src/analytics/screenTracking.ts","../../src/analytics/useScreenTracking.ts","../../src/analytics/wireDoctor.ts","../../src/permissions/permissionEvents.ts","../../src/analytics/analyticsEvent.ts","../../src/device/appVersion.ts","../../src/device/deviceModel.ts","../../src/device/deviceContext.ts","../../src/analytics/contextEnvelope.ts","../../src/analytics/eventQueue.ts","../../src/identity/userIdentity.ts","../../src/context/userContext.ts","../../src/identity/identityRecord.ts","../../src/context/deviceId.ts","../../src/analytics/analyticsFacade.ts","../../src/analytics/useAnalytics.ts"],"names":["useRef","useEffect","runtimeRequire","interop","safeInterop","Platform","Dimensions","I18nManager","_a","_b"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * reportClientEvent — forward DEVICE-ONLY onboarding events to the Wire AI analytics\n * backend (`POST {serverUrl}/v1/events`), completing the funnel for events the server\n * can't observe on its own.\n *\n * The backend already records the server-observable funnel during the A2A flow\n * (`session_started`, `screen_shown`, `answer_submitted`, `completed`, `llm_fallback`,\n * and even `screen_skipped` — it derives that from the kit's skip sentinel). The one\n * event no server request can capture is `dropped`: the user closing the app / unmounting\n * the flow without finishing. That's what this reporter is for.\n *\n * Contract (server: routers/onboarding.py → analytics/events.py):\n * POST {serverUrl}/v1/events\n * Authorization: Bearer {apiKey}\n * { \"events\": [ { event_type, session_id, screen_index?, component?, question_key?,\n * latency_ms?, meta?, device?, user_context? } ] }\n * The server fills `app_id` + `environment` from the resolving key (never send app_id),\n * and silently skips malformed events — one bad payload never fails the batch.\n *\n * ⚠️ Correlation: `session_id` MUST equal the A2A `contextId` the server uses to key the\n * server-side events, or the funnel report (which groups by `session_id`) treats this as a\n * phantom session. See `makeSessionId` + WireOnboarding for how the kit seeds it.\n *\n * Fire-and-forget: this never throws into the UI and never awaits — analytics must never\n * be able to break onboarding.\n */\nimport type { DeviceContext } from \"../device/deviceContext\";\n\n/** Event types a CLIENT may report. The rest of the funnel is server-side; sending those\n * here would double-count. `screen_skipped` is included for completeness, but the kit does\n * NOT emit it — the backend already derives it from the skip sentinel (see OnboardingFlow).\n * `client_fallback` is emitted by the kit when the AI flow degrades to the static fallback,\n * so the dashboard's fallback-rate counts the whole-flow case (distinct from the server's\n * per-turn `llm_fallback`). The server back-fills a `session_started` for it if unseen.\n * This is the SINGLE fallback signal — hosts must NOT also report their own.\n * `identify` binds the host's opaque `user_id` to this `session_id` (late binding — the user\n * registered during/after onboarding). It carries no funnel weight; the server maps the\n * session to the user and back-fills a `session_started` if it never saw the session.\n * `app_event` is the host's own in-app event (the `createAnalytics` façade's `track`/`screen`\n * route through the offline queue as first-class `ClientEvent`s). The server already ingests\n * it — `reportAppEvent` (reviews/transport) has fired `event_type='app_event'` all along; this\n * widening just lets the same shape flow through the durable queue instead of a blind POST. */\nexport type ClientEventType =\n | \"screen_skipped\"\n | \"dropped\"\n | \"client_fallback\"\n | \"identify\"\n | \"app_event\";\n\n/** One client-reported event. Mirrors the server's `OnboardingEvent` (client-settable fields). */\nexport type ClientEvent = {\n event_type: ClientEventType;\n /** Must match the server-side A2A contextId for this onboarding (see makeSessionId). */\n session_id: string;\n /** 0-based index of the screen the event refers to (matches server `screen_shown`). */\n screen_index?: number;\n component?: string;\n question_key?: string;\n latency_ms?: number;\n /** JSON-stringified extras; the server stores it verbatim. */\n meta?: string;\n /**\n * Privacy-label-neutral device snapshot (platform / form factor / locale / host appVersion).\n * Sent as an object; the server sanitizes + persists it and derives a coarse country. Old\n * servers ignore this unknown field — fully backward compatible. See device/deviceContext.ts.\n */\n device?: DeviceContext;\n /**\n * Host-injected, non-PII context (signup method, referral, plan, hashed user id). Old servers\n * ignore it. MUST NOT contain PII like raw emails — see the README `userContext` section.\n */\n user_context?: Record<string, string | number | boolean>;\n /**\n * The host's OPAQUE PSEUDONYMOUS user id (their internal id, NOT an email/name). Required on\n * `identify`, optional (rides along) on other events. Trimmed + capped at 128 chars host-side.\n * Lets the backend reconcile onboarding sessions to real users. Old servers ignore it.\n */\n user_id?: string;\n /**\n * Client-stamped timestamp of when the event was ENQUEUED on the device. Optional and ADDITIVE.\n *\n * TWO REPRESENTATIONS, on purpose:\n * - INTERNAL (epoch-ms `number`): what the offline queue stamps at enqueue time (see\n * `createEventQueue`), so two otherwise byte-identical events fired seconds apart (a genuine\n * repeat, e.g. the user taps \"share\" twice) are NOT collapsed by the queue's identical-JSON\n * de-dup — while two truly simultaneous re-enqueues of the same instant (a redundant\n * re-render) still share a `ts` and collapse. The de-dup signature depends on this number.\n * - WIRE (ISO8601 UTC `string`): what actually leaves the device. {@link buildEventsRequest}\n * converts the number on its way out, because the server declares `ts: str | None` and\n * pydantic v2 does NOT coerce a number into it — a numeric `ts` made the server answer HTTP\n * 200 with `{written: 0, skipped: N, errors: [{field: \"ts\", reason: \"validation_error\"}]}`,\n * silently discarding EVERY `app_event` through 0.13.0.\n *\n * A caller-set ISO string is passed through as-is. Never a wall-clock the server trusts (it\n * derives its own receive time); an old/strict server that does not model it ignores the field.\n */\n ts?: number | string;\n};\n\n/** Where to POST. Derived from `WireOnboardingConfig` (`serverUrl` + `apiKey`). */\nexport type ClientEventTarget = {\n /** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */\n serverUrl: string;\n /** Tenant API key; sent as `Authorization: Bearer`. */\n apiKey: string;\n};\n\n/**\n * A unique-per-onboarding session id. Used both as the client event `session_id` AND as the\n * seed the kit forwards to the backend so the SERVER adopts it as the A2A `contextId` — making\n * client and server agree (see WireOnboarding + the SDK-correlation note in the kit docs).\n * No crypto dependency: timestamp + random is collision-safe for a single device's onboarding.\n */\nexport const makeSessionId = (): string =>\n `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n\n/**\n * Serialize the internal epoch-ms `ts` to the ISO8601 UTC string the WIRE requires.\n *\n * The server's event model declares `ts: str | None` and pydantic v2 does NOT coerce int → str, so\n * a numeric `ts` fails per-event validation: the endpoint still answers HTTP 200, but with\n * `{written: 0, skipped: N, errors: [{index, reason: \"validation_error\", field: \"ts\"}]}` — every\n * `app_event` from `track()`/`screen()` silently discarded behind a green response.\n *\n * WHY HERE and not at the queue's `stamp()`: this is the single choke point all FIVE send paths go\n * through (offline queue, fire-and-forget, awaitable, review transport, session-start). Converting\n * here leaves the queue's numeric `ts` — and therefore its identical-JSON de-dup signature — exactly\n * as it was, and it also converts the persisted 0.13.0 backlogs (which hold a numeric `ts`) on their\n * way out.\n *\n * ⚠️ NOT the same list as the ack-consumer list in {@link warnOnSkippedEvents}, which is FOUR. The\n * review transport builds its request here but fires and forgets without reading the response, so\n * it rides this conversion and is absent from that one. Count the call sites before editing either.\n *\n * NEVER mutates the caller's event: an event that needs a change is copied. A string `ts` (a\n * caller-set ISO stamp) and an absent `ts` pass through untouched. A non-finite (`NaN`/`Infinity`)\n * or out-of-range number — the latter makes `toISOString` throw — drops the `ts` field from the\n * copy rather than killing the whole batch.\n */\nconst toWireEvents = (events: ClientEvent[]): ClientEvent[] =>\n events.map((event) => {\n if (typeof event.ts !== \"number\") return event;\n const { ts, ...rest } = event;\n if (!Number.isFinite(ts)) return rest;\n try {\n return { ...rest, ts: new Date(ts).toISOString() };\n } catch {\n // Out-of-range epoch-ms — send the event WITHOUT a ts rather than lose the batch.\n return rest;\n }\n });\n\n/**\n * The ONE place the `/v1/events` POST is described (url + method + headers + body). Both the\n * fire-and-forget {@link reportClientEvents} and the awaitable {@link reportClientEventsAwait}\n * build their request here so there is a SINGLE definition of the events transport — no second\n * copy of the endpoint path, headers, or envelope shape to drift. Returns `null` when there is\n * nothing to send (no target / no events) or serialization throws, so callers just bail.\n *\n * It is also where the internal epoch-ms `ts` becomes the wire's ISO8601 string — see\n * {@link toWireEvents} for why the conversion belongs at this choke point.\n *\n * `options.dryRun` asks the server to VALIDATE the batch and write nothing, which is what the wire\n * doctor's round-trip check needs: a real POST, through this one builder, that cannot pollute a\n * tenant's funnel. The `dry_run` key is added ONLY on an explicit `true`, so every production\n * caller (which passes no options at all) still serializes the exact same bytes it did before the\n * parameter existed. That byte-identity is asserted in `buildEventsRequest.test.ts`; every shipped\n * send path rides this envelope, so a diagnostic is not allowed to change it for them.\n */\nexport const buildEventsRequest = (\n target: { serverUrl: string; apiKey?: string } | undefined,\n events: ClientEvent[],\n options?: { dryRun?: boolean },\n): { url: string; init: RequestInit } | null => {\n if (!target?.serverUrl || events.length === 0) return null;\n try {\n const url = `${target.serverUrl.replace(/\\/$/, \"\")}/v1/events`;\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n // The key is CONDITIONALLY assigned rather than set to a falsy default: an absent property and\n // a `dry_run: false` property do not serialize the same, and only absence keeps the body byte\n // identical for the production call sites.\n const envelope: { events: ClientEvent[]; dry_run?: true } = { events: toWireEvents(events) };\n if (options?.dryRun === true) envelope.dry_run = true;\n const body = JSON.stringify(envelope);\n return { url, init: { method: \"POST\", headers, body } };\n } catch {\n // URL construction or JSON serialization failed — nothing to send.\n return null;\n }\n};\n\n/** What the `/v1/events` endpoint says in its 200 body. The deployed server DOES send `errors[]`,\n * each entry carrying `{index, reason, field}` — `field` names the property that failed validation\n * (it is what identified the `ts` rejection), so it is folded into the reason string here. Still\n * read defensively: an older server sends no `errors` at all. */\nexport type EventsAck = { written?: number; skipped: number; reasons: string[] };\n\n/**\n * Read the `/v1/events` ACK body. Resolves `undefined` when there is nothing readable — no `.json`\n * (an old server, a bare test mock), an already-consumed body, or a hostile response object. NEVER\n * throws, and NEVER dev-gated: the `skipped` count is a RETURN VALUE for the awaitable path, not only\n * a warning, so it has to be read in production too.\n *\n * ⚠️ The body can be read exactly once, so a caller that both decides on `skipped` AND warns must do\n * both from ONE call to this.\n *\n * Exported from this MODULE (not from the `./analytics` barrel) so the wire doctor can read an ack\n * with the same reader the send paths use. A second ack reader would be a second thing to drift,\n * and the drift it exists to catch is exactly the kind that hides behind a 200.\n */\nexport const readEventsAck = async (res: unknown): Promise<EventsAck | undefined> => {\n try {\n const json = (res as { json?: () => Promise<unknown> } | null | undefined)?.json;\n if (typeof json !== \"function\") return undefined;\n const body = (await Promise.resolve(json.call(res))) as\n | { written?: unknown; skipped?: unknown; errors?: unknown }\n | null\n | undefined;\n const skipped = body?.skipped;\n if (typeof skipped !== \"number\" || !Number.isFinite(skipped)) return undefined;\n const reasons = Array.isArray(body?.errors)\n ? body.errors\n .map((e) => {\n const entry = e as { reason?: unknown; field?: unknown } | null;\n const reason = entry?.reason;\n if (typeof reason !== \"string\") return undefined;\n // `field` is the whole point of a validation error — a bare \"validation_error\" sends the\n // reader hunting; \"validation_error (field: ts)\" names the property the server refused.\n const field = entry?.field;\n return typeof field === \"string\" && field.length > 0\n ? `${reason} (field: ${field})`\n : reason;\n })\n .filter((r): r is string => typeof r === \"string\")\n : [];\n return { written: typeof body?.written === \"number\" ? body.written : undefined, skipped, reasons };\n } catch {\n // Unreadable / already-consumed body / hostile object — best-effort, treat as \"no ack\".\n return undefined;\n }\n};\n\n/**\n * The DISCARDED warning text, built from what the server ACTUALLY said.\n *\n * It used to staple a cause onto a bare integer — \"the usual cause is a missing or empty session_id\".\n * Since `ensureCurrentSessionId` shipped, no kit path can emit an event without a `session_id`, so\n * that is now the LEAST likely explanation, and naming it sent every reader looking in the one place\n * the problem is not. The endpoint folds four real failures and two idempotent no-ops into one\n * integer, so unless the server volunteers reasons, the honest thing to report is the count and where\n * the reason lives.\n */\nconst describeDiscarded = (ack: EventsAck): string =>\n `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${ack.skipped} event(s) ` +\n \"(skipped in the response body) — they are gone, not retried. The server reported: \" +\n `skipped=${ack.skipped}${ack.written !== undefined ? `, written=${ack.written}` : \"\"}` +\n (ack.reasons.length > 0\n ? `, reasons: ${ack.reasons.join(\", \")}.`\n : \". It gave no reason (the endpoint folds every rejection into one count), so check the \" +\n \"server's ingest log for this request rather than guessing.\");\n\n/** RN sets this global; absent under node/SSR. Read defensively inside {@link warnOnSkippedEvents}. */\ndeclare const __DEV__: boolean | undefined;\n\n/**\n * Read the `/v1/events` ACK body and warn (dev builds only) when the server DISCARDED events.\n *\n * The endpoint answers HTTP **200** with `{ ok, written, skipped }` — an event it refuses is counted\n * in `skipped`, never surfaced in the status code. Every send path here reads `res.ok` alone, so a\n * whole batch can evaporate behind a green response. As of 0.13.0 ALL FOUR send paths consume the ack:\n * the offline queue, session-start, the fire-and-forget POST, and the awaitable one (which also acts\n * on it — see {@link reportClientEventsAwait}).\n *\n * LOG ONLY: returns immediately, never throws, and never influences retry / dequeue / return values.\n * A response with no usable `.json` (an old server, a test mock) is silently ignored.\n *\n * DEV-GATED FIRST: the `__DEV__` check is the FIRST statement, before the body is even looked at.\n * The check used to sit inside the `.then`, so a release build parsed the JSON of every\n * persistent-path POST to build a warning no one would ever read. Nothing here runs in production.\n */\nexport const warnOnSkippedEvents = (res: unknown): void => {\n if (typeof __DEV__ === \"undefined\" || !__DEV__) return;\n if (typeof console === \"undefined\" || !console.warn) return;\n void readEventsAck(res).then((ack) => {\n if (!ack || ack.skipped <= 0) return;\n console.warn(describeDiscarded(ack));\n });\n};\n\n/**\n * POST one or more client events, fire-and-forget. A missing/invalid target, a build error,\n * a missing `fetch`, or a network failure is swallowed — the call returns immediately and the\n * request (if any) runs in the background.\n */\nexport const reportClientEvents = (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): void => {\n try {\n const req = buildEventsRequest(target, events);\n if (!req) return;\n void fetch(req.url, req.init)\n .then((res) => {\n // Still fire-and-forget — nothing is retried, nothing is returned. But a batch that\n // evaporated behind a 200 is exactly what this path used to make invisible, so in dev it\n // now says so, like the queue and session-start paths already did.\n warnOnSkippedEvents(res);\n })\n .catch(() => {\n // Network/transport error — analytics is best-effort, swallow.\n });\n } catch {\n // A missing `fetch` — swallow.\n }\n};\n\n/** Convenience single-event wrapper around {@link reportClientEvents}. */\nexport const reportClientEvent = (\n target: ClientEventTarget | undefined,\n event: ClientEvent,\n): void => reportClientEvents(target, [event]);\n\n/**\n * AWAITABLE sibling of {@link reportClientEvents}: POST one or more client events through the SAME\n * `/v1/events` path, but resolve only once the server has RESPONDED — so a decision re-fetch fired\n * immediately after is guaranteed to see the event in the session stream (this is the guarantee\n * `wire.track` needs before it triggers decision revalidation). Never throws: a missing/invalid\n * target, a missing `fetch`, a network error, or a non-2xx status all resolve to `false`.\n *\n * ⚠️ 0.13.0: a 2xx is NO LONGER SUFFICIENT. The endpoint answers HTTP 200 with `{ ok, written,\n * skipped }` and counts an event it refuses in `skipped`, so this used to resolve `true` for an event\n * the server had thrown away — and `wire.track` then bumped decision revalidation, making every\n * subscribed gate re-fetch against a stream the action never entered. It now reads the ack and\n * resolves `false` when the server reports a positive `skipped`.\n *\n * BACKWARD COMPATIBLE BY CONSTRUCTION: only an explicit positive `skipped` demotes a 200. An old\n * server that sends no such field, a body that cannot be parsed, or a response with no `.json` at all\n * resolves `true` exactly as before — the change can produce no false negatives.\n */\nexport const reportClientEventsAwait = async (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): Promise<boolean> => (await reportClientEventsOutcome(target, events)) === \"delivered\";\n\n/**\n * WHY a failed send is not one thing. `false` collapses two situations that call for OPPOSITE\n * responses, and a caller that wants to make the event durable has to tell them apart:\n *\n * • `unreachable` — the request never got an answer (no `fetch`, a network error, a non-2xx).\n * Nothing is wrong with the EVENT. Retrying later is exactly right, and is what every\n * offline-first path in this kit does.\n * • `refused` — the server READ the event and threw it away (HTTP 200 with `skipped > 0`,\n * a per-event validation failure), or there is no transport to build a request from at all.\n * Retrying cannot change the answer: buffering here would park a poison event in a PERSISTED\n * backlog that re-sends and is re-refused on every drain and every launch, forever.\n *\n * The boolean sibling above is this function with the two collapsed back together, so there is one\n * implementation and the two can never drift.\n */\nexport type SendOutcome = \"delivered\" | \"refused\" | \"unreachable\";\n\nexport const reportClientEventsOutcome = async (\n target: ClientEventTarget | undefined,\n events: ClientEvent[],\n): Promise<SendOutcome> => {\n try {\n const req = buildEventsRequest(target, events);\n // No target / nothing serializable: there is no endpoint to retry against.\n if (!req) return \"refused\";\n const res = await fetch(req.url, req.init);\n if (!res || !res.ok) return \"unreachable\";\n // ONE body read, used for both the verdict and the dev warning — it can only be read once.\n const ack = await readEventsAck(res);\n if (!ack || ack.skipped <= 0) return \"delivered\";\n if (typeof __DEV__ !== \"undefined\" && __DEV__ && typeof console !== \"undefined\" && console.warn) {\n console.warn(describeDiscarded(ack));\n }\n // The server answered and declined THIS event. A retry is a re-decline.\n return \"refused\";\n } catch {\n // Unreachable / missing-fetch / network — best-effort, and retryable.\n return \"unreachable\";\n }\n};\n\n/** Convenience single-event wrapper around {@link reportClientEventsAwait}. */\nexport const reportClientEventAwait = (\n target: ClientEventTarget | undefined,\n event: ClientEvent,\n): Promise<boolean> => reportClientEventsAwait(target, [event]);\n","/**\n * warnInDev — the ONE developer-warning primitive.\n *\n * The kit warns a developer in four places (the onboarding host, the env-config helper, the analytics\n * façade, the current-session registry) and until 0.13.0 each carried its own copy of the same five\n * lines. Three were byte-identical; the fourth differed only by returning whether it actually warned.\n * Four copies of a guard is four chances for one of them to drift out of the `__DEV__` gate, which is\n * the failure that matters: a warning that runs in production is a string built for nobody.\n *\n * ON THE TREE-SHAKING NOTE THIS REPLACES: `analytics/currentSession` used to justify its copy as\n * keeping the module import-free for the tree-shaken `./analytics` bundle. That rationale had already\n * lapsed — the module imports `makeSessionId` from `./reportClientEvent` — and this module has no\n * imports of its own, so it adds one leaf to the graph and nothing to the bundle. The `treeShake`\n * canary still holds the real guarantee (the analytics graph reaches no UI module).\n */\n\n/** RN sets this global; absent under node/SSR. Read defensively, never assumed. */\ndeclare const __DEV__: boolean | undefined;\n\n/**\n * Emit a one-line developer warning, but ONLY in a dev build (RN `__DEV__`). No-op in prod/tests.\n *\n * Returns whether it ACTUALLY warned, which a caller holding a once-flag must honour: marking\n * \"already warned\" after a no-op would burn the single warning in production, and the one dev build\n * that needed it would then run silent.\n */\nexport const warnInDev = (message: string): boolean => {\n if (typeof __DEV__ !== \"undefined\" && __DEV__ && typeof console !== \"undefined\" && console.warn) {\n console.warn(message);\n return true;\n }\n return false;\n};\n","/**\n * currentSession — a tiny registry of the CURRENT per-open `session_id`.\n *\n * WHY it exists (kills the phantom-session): the per-open emitters (`reportSessionStart` and the\n * `useSessionStart` / `useLifecycleEvents` hooks) mint a fresh `session_id` for each app-open and\n * post `app.session_started` with it — so the SERVER knows that id. But other client paths\n * (`identify`, host `app_event`s through the analytics façade) used to reference a DIFFERENT id\n * (a frozen per-instance id), which the server had never seen, so it back-filled a synthetic\n * `session_started` — inflating session counts (the \"phantom-session\" bug).\n *\n * This registry is the single seam that lets those paths reuse the LIVE per-open session id the\n * server already ingested. `reportSessionStart` writes the current id here on every open; the façade\n * reads it so `identify`/app-events correlate to the real session instead of minting a phantom.\n *\n * ── WHY A globalThis SLOT, NOT A PLAIN MODULE VARIABLE ────────────────────────────────────────\n * This module is exported from TWO package entry points — the main `.` bundle (`src/index.ts`) and\n * the `./analytics` subpath (`src/analytics/index.ts`). Under `dist` resolution (node `import`/\n * `require`, which is how tests, SSR and some tooling load the kit) tsup inlines a SEPARATE copy of\n * this module into each bundle, so a plain `let` would give the SETTER (reached via `.` →\n * `reportSessionStart`) and the READER (reached via `./analytics` → façade / `userIdentity`) TWO\n * different variables: the reader would see `undefined` even after an open set the id, and gating\n * would fire under a null session id. On-device this was masked only because Metro's `react-native`\n * export condition resolves both subpaths back to this one `src/` file (a single instance) — a\n * bundler accident, not a guarantee.\n *\n * The bundler-agnostic fix: keep the ONE live value in a well-known `globalThis` slot keyed by a\n * `Symbol.for(...)`. `Symbol.for` uses the runtime-global symbol registry, so every inlined copy of\n * this module resolves the SAME symbol and reads/writes the SAME slot — one identity no matter how\n * many times the module is duplicated across bundles. `globalThis` is present and identical in\n * Hermes/React Native, Node and SSR (we never touch `window`), so this is safe on every host.\n *\n * PROCESS-LOCAL, NOT PERSISTED: the slot lives on the runtime global, so it tracks the CURRENT\n * process's open and a fresh open overwrites it. There is no cross-launch state.\n * `resetCurrentSessionId` clears the slot so a unit test starts from a clean registry.\n *\n * ── WHY `ensureCurrentSessionId` EXISTS (the silent-drop contract) ─────────────────────────────\n * The server's event model declares `session_id: str = Field(min_length=1)` — REQUIRED, non-empty.\n * `POST /v1/events` validates each event inside a try/except that increments a `skipped` counter and\n * still returns HTTP 200. So an event posted without a `session_id` is accepted by the wire and\n * DISCARDED by the server, and a fire-and-forget client can never learn it happened. That is the\n * worst of both: no error, no data. Screen tracking in a host that never mounted the lifecycle hook\n * fell into exactly that hole — every screen view posted, 200'd, and dropped.\n *\n * `ensureCurrentSessionId` closes it: it returns the registered id when an open HAS been registered\n * (unchanged behaviour for every host that fires `reportSessionStart` first), and otherwise mints one,\n * REGISTERS it, and returns it — so every later event in the process correlates to that same id\n * instead of each emitting its own orphan. A minted id is a fallback, not a substitute for a real\n * app-open: it warns once in dev, naming the fix.\n */\nimport { makeSessionId } from \"./reportClientEvent\";\n// The shared primitive returns whether it ACTUALLY warned, which the once-flag below depends on:\n// marking \"already warned\" after a prod no-op would burn the single warning and leave the one dev\n// build that needed it silent. (The old local copy justified itself as keeping this module\n// import-free for the tree-shaken analytics bundle; that had already lapsed — it imports\n// `makeSessionId` right here — and `utils/warnInDev` has no imports of its own.)\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/**\n * Well-known key into the runtime-global symbol registry. `Symbol.for` (NOT a plain `Symbol()`) is\n * what makes this cross-bundle: it returns the SAME symbol for the same string across every copy of\n * this module, so duplicated inlined copies all address one slot.\n *\n * @globalSlot LIVE — every app-open overwrites this with that open's id, so a reader that captures\n * it into a module-local (or a `const` taken once at mount) posts the PREVIOUS open's session to a\n * server that has already moved on. Read it at the moment of use, through `getCurrentSessionId()` /\n * `ensureCurrentSessionId()`. `reviews/runtime`'s `currentOpenId()` derives its own pinned value\n * from this slot: it holds one sample for the duration of an app-open and re-pins when a genuinely\n * NEW open is registered here, so it follows this slot deliberately and late, never eagerly.\n */\nconst CURRENT_SESSION_ID_SLOT: unique symbol = Symbol.for(\n \"@wireai/activation:currentSessionId\",\n);\n\ntype GlobalWithSlot = typeof globalThis & {\n [CURRENT_SESSION_ID_SLOT]?: string | undefined;\n};\n\nconst globalSlot = globalThis as GlobalWithSlot;\n\n/**\n * Record the current per-open `session_id`. Called by `reportSessionStart` when it emits an\n * app-open. A blank / non-string id is ignored (the previous id stays current). Idempotent.\n */\nexport const setCurrentSessionId = (id: string | undefined): void => {\n if (typeof id === \"string\" && id.length > 0) {\n globalSlot[CURRENT_SESSION_ID_SLOT] = id;\n }\n};\n\n/** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */\nexport const getCurrentSessionId = (): string | undefined =>\n globalSlot[CURRENT_SESSION_ID_SLOT];\n\n/** Test-only: forget the current session id so a unit test starts from a clean registry. */\nexport const resetCurrentSessionId = (): void => {\n globalSlot[CURRENT_SESSION_ID_SLOT] = undefined;\n};\n\n/** The one-time message. Hoisted so a prod mint does not rebuild a string nobody will read. */\nconst MINT_WARNING =\n \"[wireai] No app-open session was registered, so a session id was minted for this event \" +\n \"(the server drops an event that has no session_id, and still answers 200). Mount \" +\n \"useLifecycleEvents at your app root so events correlate to a real app-open.\";\n\n/**\n * \"Have we already warned about a minted session id?\" — its OWN `Symbol.for` slot, for the same\n * cross-bundle reason as the id itself: a plain module `let` would warn once per inlined copy, i.e.\n * once per bundle, not once per process. NOT cleared by `resetCurrentSessionId`: \"warn once\" is a\n * process-lifetime promise, and a test that resets the id between mints is still one process.\n *\n * @globalSlot LATCH — written `true` on the first mint that actually warned, and never again. A\n * second differing write would re-arm a warning the process has already spent, turning \"once\" into\n * \"once per whoever cleared it\".\n */\nconst MINT_WARNED_SLOT: unique symbol = Symbol.for(\n \"@wireai/activation:currentSessionIdMintWarned\",\n);\n\ntype GlobalWithWarnSlot = typeof globalThis & { [MINT_WARNED_SLOT]?: boolean };\n\nconst warnSlot = globalThis as GlobalWithWarnSlot;\n\n/**\n * The current per-open `session_id`, MINTING and registering one when no app-open has been\n * registered yet. Always returns a non-empty string. Idempotent (a second call returns the same id)\n * and never throws.\n *\n * Use this on every path that puts a `session_id` on the wire. The server REQUIRES a non-empty\n * `session_id` and drops the event otherwise while still answering 200 (see the module header), so\n * \"no id yet\" must never mean \"send it without one\".\n *\n * BACKWARD-COMPATIBLE BY CONSTRUCTION: when `reportSessionStart` / `useLifecycleEvents` has already\n * registered the real per-open id, this is `getCurrentSessionId()` and nothing changes. It only ever\n * mints in the case that used to produce a silently discarded event.\n *\n * A mint means the host never registered an app-open, so the minted id is one the server has not\n * seen a `session_started` for — the events land, but the session is thinner than a real open.\n * Hence the one-time dev warning naming the fix (mount `useLifecycleEvents` at the app root).\n */\nexport const ensureCurrentSessionId = (): string => {\n const existing = globalSlot[CURRENT_SESSION_ID_SLOT];\n if (typeof existing === \"string\" && existing.length > 0) return existing;\n const minted = makeSessionId();\n globalSlot[CURRENT_SESSION_ID_SLOT] = minted;\n if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {\n warnSlot[MINT_WARNED_SLOT] = true;\n }\n return minted;\n};\n","/**\n * transport.ts — kit → Wire server requests for the review module. Non-blocking and never\n * throwing (analytics/reviews must never break the app). Mirrors analytics/reportClientEvent: a\n * thin fetch wrapper with a Bearer tenant key. `submitReview` additionally REPORTS the fate of the\n * row to its caller — accepted / rejected / unsent, three outcomes on purpose; the rest swallow\n * every error.\n *\n * • submitReview → POST {serverUrl}/v1/reviews (the review row)\n * • fetchReviewDecision → GET {serverUrl}/v1/reviews/decision (best-effort, the AI seam)\n * • reportAppEvent → POST {serverUrl}/v1/events (generic app.* namespace)\n *\n * `reportAppEvent` is the strategic extension: it lets a host report arbitrary in-app\n * events through the SAME transport (stored server-side as event_type='app_event',\n * question_key=<name>), which is what the backend review-firing rules evaluate on — and\n * it seeds the broader app-analytics stream. Keep payloads minimal + non-PII.\n */\nimport { ensureCurrentSessionId } from \"../analytics/currentSession\";\nimport { buildEventsRequest, type ClientEvent } from \"../analytics/reportClientEvent\";\nimport type { SubmitResult } from \"../utils/submitResult\";\nimport type { ReviewDecisionResponse, ReviewSubmission, ReviewTarget } from \"./types\";\n\n/**\n * What became of a review POST — the SHARED three-outcome verdict, re-exported under the name the\n * reviews surface has always published. The split is load-bearing and the reasoning lives once, in\n * `utils/submitResult.ts`, because `submitQuestionnaireResponse` answers the identical question.\n * See `submitReview` below for what reads it.\n */\nexport type ReviewSubmitResult = SubmitResult;\n\n/**\n * POST a review (the 1-4 feedback path, and the text-less 5-star row).\n *\n * NON-BLOCKING, and ACKED. It never throws and never makes a caller wait — the request is fired\n * synchronously and the UI is free to advance on the next line — but it RESOLVES with what became\n * of the row.\n *\n * WHY THE RETURN VALUE EXISTS: a 1-4 star rating with mandatory free text is the kit's most\n * valuable payload and it has no persisted queue behind it (unlike `analytics/eventQueue`). If the\n * response is dropped on the floor, a caller cannot tell a delivered submission from one that died\n * in the socket, so it must latch the rating as posted and a detractor who rated offline is lost\n * AND never asked again. Reading the ack is what lets `ReviewGate` keep an undelivered row\n * recoverable.\n *\n * ── WHY IT IS NOT A BOOLEAN ──────────────────────────────────────────────────────────────────\n *\n * It was, for exactly one unpublished release, and the boolean was the bug. `false` meant both\n * \"nothing reached the server\" and \"the server answered non-2xx\", and the one caller that reads\n * this (`ReviewGate.postOnce`) treats `false` as \"still owed\" and re-posts. But the server mints\n * its own row id (`create_review` / `_new_id()`) and `CreateReviewRequest` carries no id, so there\n * is NO idempotency key on the wire: a 502 returned AFTER the insert commits means the re-post\n * writes a SECOND row, double-counting `count` and corrupting `avg` — the precise corruption the\n * one-row latch exists to prevent. A response of any status proves the server was reached, and\n * that is a different question from whether it liked the row. So the two are different values.\n *\n * ── THE RESIDUAL, STATED HONESTLY ────────────────────────────────────────────────────────────\n *\n * `unsent` is not proof the server never got the row. A connection dropped after the request was\n * written — or after the row committed — surfaces as a thrown/rejected `fetch` here, exactly like\n * an offline device. Retrying only on `unsent` is therefore SAFER, not SAFE: it removes the\n * double-post the server itself told us about, and leaves the narrow window where the answer never\n * made it back onto the wire. Closing that window needs a CLIENT-MINTED IDEMPOTENCY KEY the server\n * upserts on, which is a server change (`CreateReviewRequest` + `create_review`) and is not\n * something the kit can fake. Until it exists, prefer losing a row over inventing one: a lost\n * detractor is a gap in the data, a duplicated one is a lie in the data.\n */\nexport const submitReview = async (\n target: ReviewTarget | undefined,\n review: ReviewSubmission,\n): Promise<ReviewSubmitResult> => {\n if (!target?.serverUrl) return \"unsent\";\n try {\n const url = `${target.serverUrl.replace(/\\/$/, \"\")}/v1/reviews`;\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n const res = await fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(review),\n });\n // No response object at all is not an answer — treat it as nothing having reached the server\n // rather than as a rejection, or a stubbed-out `fetch` would silently latch the row away.\n if (!res) return \"unsent\";\n return (res as { ok?: boolean }).ok ? \"accepted\" : \"rejected\";\n } catch {\n /* URL/JSON/missing-fetch/network — nothing came back, and never surfaced to the UI */\n return \"unsent\";\n }\n};\n\n/**\n * Options for the best-effort review decision fetch.\n *\n * ── WHAT THE DEPLOYED SERVER ACTUALLY READS (verified 2026-07-17) ────────────────────────\n *\n * `GET /v1/reviews/decision` declares exactly two query params — `session_id` and\n * `device_key` — plus the `Authorization` header. That is the whole wire. Verified against\n * the deployed OpenAPI schema, not against intent.\n *\n * This type is CLOSED on purpose. Hosts that hand-rolled this fetch invented `user_id` and\n * `session_count` query params believing \"the server ignores what it doesn't read, so passing\n * it is always safe\". Both are no-ops: the route declares neither. `session_count` is real, but\n * only on the questionnaire POST body — which is precisely how it copy-pasted its way into a\n * reviews call site and sat there doing nothing. They cost a wire lie — a call site that reads as though\n * identity and a session counter reach the firing brain when neither does. So they are not\n * offered here. Pass identity as `deviceKey`; the session count the server reasons about is the\n * one IT derives from the event stream keyed by `deviceKey`, not one the client asserts.\n *\n * The client-side session floor is a LOCAL rule, not a wire param: use `ReviewConfig.minSessions`\n * (evaluated by `useReviewGate` against the kit's own `wire_review_<id>_sessions` counter).\n */\nexport interface FetchReviewDecisionOptions {\n /**\n * The onboarding session id, when there IS one. OPTIONAL on purpose: the review gate lives on\n * the home feed, where a user legitimately has no onboarding session. `deviceKey` is the real\n * identity for this call. (The server relaxed `session_id` to optional on 2026-07-16 and the\n * relaxation IS deployed — the route no longer 422s without it. Permissive wire: do NOT make\n * this required here.)\n */\n sessionId?: string;\n /**\n * A stable, non-PII device id — THE identity for this call. The decision endpoint reads it for\n * cooldown + min-sessions, and it is the key the server groups a device's events under. A host\n * whose own identity is a user id passes that id here rather than reaching for a `user_id`\n * param the route does not declare.\n */\n deviceKey?: string;\n}\n\n/**\n * Best-effort fetch of the SERVER's review firing decision. The mirror of\n * `fetchQuestionnaireDecision`, and the primitive whose absence caused a live incident.\n *\n * ── WHY THIS LIVES IN THE KIT (Malik, 2026-07-16) ────────────────────────────────────────\n *\n * The questionnaire module has always had its decision fetch; reviews never did. So a host\n * hand-rolled one, and got it subtly wrong: its helper collapsed an explicit `{fire:false}`\n * into `undefined`. `decideReview` is `(local, decision) => decision ?? local`, so `undefined`\n * means \"the server has no opinion, use the local rules\" — and the local timer fired. The\n * server could therefore only ever turn review prompts ON, never OFF. A real user was asked to\n * rate the app ~3 minutes into their FIRST session, having seen nothing yet, and left 1 star.\n *\n * The bug was not that the host was careless. It was that the kit made every host invent this.\n * So the primitive moves here and hosts keep a thin call site.\n *\n * ── THE CONTRACT THAT MATTERS ────────────────────────────────────────────────────────────\n *\n * • 2xx → the FULL `{fire, reason, arm}`, INCLUDING `fire:false`. Never collapse a false\n * into null. That collapse IS the bug: a false must reach `decideReview` intact so\n * it can override the local rules and keep the gate shut.\n * • else → null, and ONLY then. Null means \"the server genuinely has no opinion\", which is\n * the one case where falling back to local rules is correct.\n *\n * Never throws: unreachable, non-2xx, bad JSON, or a missing `fetch` all resolve to null. The\n * kit does not call this internally; a host awaits it and passes the result straight to\n * `useReviewGate({ decision })`.\n *\n * ── DON'T RACE THIS AGAINST A LOCAL TIMER ────────────────────────────────────────────────\n *\n * The return type is `ReviewDecisionResponse`, which carries `arm` alongside `{fire, reason}`.\n * Hand the WHOLE object to the gate and echo `arm` into the submission's `meta.firing_arm`;\n * narrowing it to `{fire, reason}` on the way through silently kills per-arm attribution\n * across a reweighting of the experiment.\n *\n * A host that starts its own dwell timer in parallel with this fetch has built a race a slow\n * server loses: the timer fires, the local rules show the prompt, and the `{fire:false}` still\n * in flight arrives too late to stop it. Do not hand-roll that. `useReviewGate` already owns\n * the wait — set `ReviewConfig.timeoutFallbackMs` and the local rules stay parked until either\n * the decision lands or the window expires, whichever comes first.\n *\n * const decision = await fetchReviewDecision(target, { deviceKey });\n * const gate = useReviewGate({\n * config: { id: \"home\", minSessions: 2, timeoutFallbackMs: 3000 },\n * decision: decision ?? undefined, // pass it whole — keep `arm`\n * storage,\n * });\n */\nexport const fetchReviewDecision = async (\n target: ReviewTarget | undefined,\n options: FetchReviewDecisionOptions = {},\n): Promise<ReviewDecisionResponse | null> => {\n if (!target?.serverUrl) return null;\n try {\n const base = target.serverUrl.replace(/\\/$/, \"\");\n const params = new URLSearchParams();\n if (options.sessionId) params.set(\"session_id\", options.sessionId);\n if (options.deviceKey) params.set(\"device_key\", options.deviceKey);\n const qs = params.toString();\n const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : \"\"}`;\n const headers: Record<string, string> = {};\n if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;\n const res = await fetch(url, { headers });\n if (!res || !res.ok) return null;\n const json = (await res.json()) as ReviewDecisionResponse | null;\n // A body without a boolean `fire` is not a decision. Guard it explicitly rather than\n // letting `{}` through as a truthy object that `decideReview` would treat as a verdict\n // (`{}.fire === undefined` is falsy, so it would silently read as \"never fire\").\n if (!json || typeof json.fire !== \"boolean\") return null;\n return json;\n } catch {\n /* unreachable / non-2xx / bad JSON / missing-fetch → the server has no opinion */\n return null;\n }\n};\n\n/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */\nexport interface ReportAppEventOptions {\n /**\n * The onboarding/session id to correlate with, when known. Optional: when omitted the event\n * still carries the CURRENT per-open session id (`ensureCurrentSessionId()`), because an event\n * with no `session_id` is dropped server-side behind a 200. Pass one only to override.\n */\n sessionId?: string;\n /** A stable, non-PII device id — the review-decision endpoint reads it for min-sessions. */\n deviceKey?: string;\n /** Small non-PII extras. */\n meta?: Record<string, unknown>;\n}\n\n/**\n * Report a generic in-app event through the existing events transport. Stored server-side\n * as `event_type='app_event'`, `question_key=<name>`. Fire-and-forget. Keep `name` a short\n * stable identifier and `meta` small + non-PII.\n *\n * reportAppEvent(target, \"content_share\", { sessionId, deviceKey });\n *\n * ── `session_id` IS NON-NEGOTIABLE ON THE WIRE ───────────────────────────────────────────\n * The server's event model declares `session_id` required + non-empty, and `POST /v1/events`\n * validates per event inside a try/except that counts the failure as `skipped` and STILL returns\n * HTTP 200. An event sent without a `session_id` is therefore accepted and discarded, and a\n * fire-and-forget caller never finds out. This used to be reachable through the ordinary API:\n * `options.sessionId` was optional, so a host calling `reportAppEvent(target, \"screen\", { deviceKey })`\n * posted every screen view into that hole. So the id is no longer conditional — an explicit\n * `sessionId` wins, otherwise the CURRENT per-open id is used (minted + registered if no app-open\n * has been registered yet).\n */\nexport const reportAppEvent = (\n target: ReviewTarget | undefined,\n name: string,\n options: ReportAppEventOptions = {},\n): void => {\n if (!target?.serverUrl || !name) return;\n try {\n // An explicit id wins; a missing OR BLANK one falls back to the current per-open id. A bare\n // `??` would let `sessionId: \"\"` through, and the server rejects an empty string exactly like\n // a missing key (`min_length=1`), so the blank case has to fall back too.\n const supplied = options.sessionId ?? \"\";\n const event: Record<string, unknown> = {\n event_type: \"app_event\",\n question_key: name,\n session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId(),\n };\n // device_key rides in the non-PII user_context bucket the server sanitizes; the\n // review-decision endpoint reads it to group a device's sessions.\n if (options.deviceKey) event.user_context = { device_key: options.deviceKey };\n if (options.meta && Object.keys(options.meta).length > 0) {\n event.meta = JSON.stringify(options.meta);\n }\n // Route through the ONE canonical /v1/events builder (url + headers + body) instead of\n // re-describing the endpoint here; still fire-and-forget.\n const req = buildEventsRequest(target, [event as unknown as ClientEvent]);\n if (!req) return;\n void fetch(req.url, req.init).catch(() => {\n /* best-effort, swallow */\n });\n } catch {\n /* swallow */\n }\n};\n","/**\n * screenTracking — an OPT-IN, dependency-free automatic screen-view helper.\n *\n * Given the host app's navigation STATE (or a nav ref, via the useScreenTracking hook), this\n * emits exactly ONE `screen` app-event per REAL screen change through the kit's existing\n * `reportAppEvent` transport (`POST {serverUrl}/v1/events`, `event_type='app_event'`,\n * `question_key='screen'`, `meta={ screen }`). Param-only changes and re-renders are de-duped\n * away because we resolve and compare the route NAME only — never the params.\n *\n * Dependency-free core: this file imports NO navigation library. React-Navigation / expo-router\n * are host concerns; the host supplies plain state objects and refs, typed structurally here\n * (`NavigationStateLike`). It is also React-free — the optional React glue lives in\n * `useScreenTracking.ts`. `reportAppEvent` is imported from the pure `../reviews/transport`\n * module (NOT the `../reviews` barrel, which would drag the review UI into an analytics-only\n * bundle and defeat tree-shaking).\n *\n * Privacy: only the route NAME ever leaves the device. Route params, query strings, and any\n * user data are never read into the event. Fire-and-forget — this never throws into the UI.\n */\nimport { reportAppEvent } from \"../reviews/transport\";\n\n/** A single route inside a React-Navigation-shaped state (structural — no `@react-navigation`). */\nexport interface NavigationRouteLike {\n name: string;\n /** A nested navigator's own state, when this route hosts one. */\n state?: NavigationStateLike;\n /** Route params are intentionally left `unknown` — this helper never reads them. */\n params?: unknown;\n}\n\n/** A React-Navigation-shaped navigator state (structural type; no library import). */\nexport interface NavigationStateLike {\n /** Index of the active route within `routes`. */\n index?: number;\n routes?: NavigationRouteLike[];\n}\n\n/** Options for {@link createScreenTracker}. */\nexport interface ScreenTrackerOptions {\n /**\n * Where to POST. `{ serverUrl, apiKey }` — same shape the kit's review/analytics config\n * exposes. When omitted, the tracker still de-dups and fires `onScreen`, but sends nothing.\n */\n target?: { serverUrl: string; apiKey: string };\n /**\n * The onboarding/session id to correlate screen views with, when known. Omitting it no longer\n * means the view goes out WITHOUT a `session_id` (the server requires one and drops the event\n * behind an HTTP 200 — that is why screen tracking silently produced nothing for a host that\n * never mounted the lifecycle hook). `reportAppEvent` falls back to the current per-open id.\n */\n sessionId?: string;\n /** A stable, non-PII device id — groups a device's sessions server-side. */\n deviceKey?: string;\n /** Called on every REAL screen change (after de-dup), before the network emit. */\n onScreen?: (screen: string) => void;\n /**\n * Per-screen filter. Return `false` to skip the NETWORK emit for a screen (last-screen memory\n * is still advanced + `onScreen` still fires) — e.g. to keep a sensitive route out of analytics.\n */\n shouldTrack?: (screen: string) => boolean;\n}\n\n/** The screen tracker returned by {@link createScreenTracker}. */\nexport interface ScreenTracker {\n /** Report the active screen. Ignores `undefined`/empty and de-dups repeats of the last screen. */\n track: (screen: string | undefined) => void;\n /** Clear the last-screen memory (e.g. on logout) so the next `track` always emits. */\n reset: () => void;\n}\n\n/**\n * Walk a React-Navigation-shaped state to the DEEPEST active route and return its NAME (never\n * its params). Recurses `routes[index]` while a nested `.state` exists. Returns `undefined` for\n * a missing/empty/malformed state — the caller treats that as \"nothing to report\".\n */\nexport const getActiveRouteName = (\n state: NavigationStateLike | undefined,\n): string | undefined => {\n let current: NavigationStateLike | undefined = state;\n let name: string | undefined;\n // Bounded by the finite nesting depth of a real navigator tree.\n while (current && Array.isArray(current.routes) && current.routes.length > 0) {\n const index = typeof current.index === \"number\" ? current.index : 0;\n const route = current.routes[index];\n if (!route) break;\n name = route.name;\n current = route.state;\n }\n return name;\n};\n\n/**\n * Build a stateful screen tracker. `track` de-dups against the last reported screen so only a\n * REAL change emits; `reset` clears that memory. Fire-and-forget throughout — a missing `target`\n * skips the network but keeps the de-dup + `onScreen` behaviour intact.\n */\nexport const createScreenTracker = (options: ScreenTrackerOptions = {}): ScreenTracker => {\n let lastScreen: string | undefined;\n return {\n track: (screen: string | undefined): void => {\n if (!screen || screen === lastScreen) return;\n lastScreen = screen;\n options.onScreen?.(screen);\n if (options.shouldTrack && !options.shouldTrack(screen)) return;\n reportAppEvent(options.target, \"screen\", {\n sessionId: options.sessionId,\n deviceKey: options.deviceKey,\n meta: { screen },\n });\n },\n reset: (): void => {\n lastScreen = undefined;\n },\n };\n};\n\n/**\n * Adapt a tracker into a React-Navigation `onStateChange` handler — the one-place wiring:\n *\n * <NavigationContainer onStateChange={screenTrackingHandler(tracker)}>\n *\n * It resolves the deepest active route name and hands it to `tracker.track` (which de-dups).\n */\nexport const screenTrackingHandler =\n (tracker: ScreenTracker) =>\n (state: NavigationStateLike | undefined): void =>\n tracker.track(getActiveRouteName(state));\n","/**\n * useScreenTracking — a THIN optional React hook over the pure screen-tracking core.\n *\n * It builds one tracker for the component's lifetime and subscribes to the host's navigation\n * ref: it reports the current route on mount and on every `state` event, and unsubscribes on\n * unmount. React is a REQUIRED peer of the kit, so importing it here is allowed; the hook adds\n * NO navigation-library dependency — the ref is typed structurally (`NavigationRefLike`).\n *\n * const navigationRef = useNavigationContainerRef(); // host's @react-navigation ref\n * useScreenTracking(navigationRef, { target, sessionId });\n * // ...<NavigationContainer ref={navigationRef}>\n */\nimport { useEffect, useRef } from \"react\";\n\nimport { createScreenTracker, type ScreenTracker, type ScreenTrackerOptions } from \"./screenTracking\";\n\n/**\n * The structural slice of a React-Navigation container ref this hook needs — no\n * `@react-navigation` import. `getCurrentRoute` yields the active route; `addListener(\"state\", …)`\n * fires on every navigation state change and returns its own unsubscribe.\n */\nexport interface NavigationRefLike {\n getCurrentRoute?: () => { name?: string } | undefined;\n addListener?: (type: \"state\", callback: () => void) => () => void;\n}\n\n/**\n * Subscribe screen tracking to a host navigation ref. Safe to call with a not-yet-ready ref\n * (the effect no-ops until `addListener` exists). Returns nothing — it wires side effects only.\n */\nexport const useScreenTracking = (\n navigationRef: NavigationRefLike | undefined,\n options: ScreenTrackerOptions = {},\n): void => {\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // One tracker per mount; kept in a ref so re-renders never rebuild the de-dup memory.\n const trackerRef = useRef<ScreenTracker | undefined>(undefined);\n if (!trackerRef.current) {\n const dynamicOptions: ScreenTrackerOptions = {\n get target() {\n return optionsRef.current.target;\n },\n get sessionId() {\n return optionsRef.current.sessionId;\n },\n get deviceKey() {\n return optionsRef.current.deviceKey;\n },\n get onScreen() {\n return optionsRef.current.onScreen;\n },\n get shouldTrack() {\n return optionsRef.current.shouldTrack;\n },\n };\n trackerRef.current = createScreenTracker(dynamicOptions);\n }\n\n useEffect(() => {\n const tracker = trackerRef.current;\n if (!tracker || !navigationRef?.addListener) return;\n const report = (): void => tracker.track(navigationRef.getCurrentRoute?.()?.name);\n report(); // initial screen on mount\n const unsubscribe = navigationRef.addListener(\"state\", report);\n return unsubscribe;\n // Re-subscribe only when the ref identity changes; option changes are read live off the closure.\n }, [navigationRef]);\n};\n","/**\n * wireDoctor: a DEV-ONLY, opt-in self-check that proves a fresh integration end to end before the\n * first real user ever runs it.\n *\n * WHY IT EXISTS: the `/v1/events` endpoint answers HTTP **200** for a batch it throws away. It\n * reports the refusal in the response body (`{written, skipped, errors:[{reason, field}]}`), so a\n * mis-wired integration looks perfectly healthy from the outside while every event evaporates. A\n * numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you\n * discover in a funnel report weeks later into something the first run tells you.\n *\n * SHAPE: four independent checks, each a `{name, ok, detail}` unit that can be read (and tested)\n * without the others. The report is data, never a thrown error and never a side effect on the host:\n *\n * 1. `target` : is there a server URL and a key, and do they look like a key and a URL?\n * 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)\n * 3. `storage` : can the offline queue persist? (a write / read / delete probe)\n * 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)\n *\n * NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor\n * that can crash the screen it is diagnosing is worse than no doctor.\n *\n * ⛔ NEVER PRINTS A SECRET. The `target` check reports the key's SHAPE (present / absent, length,\n * whether the prefix is the expected one) and never any character of the key itself, in any\n * `detail`, any log line, or any error. `wireDoctor.test.ts` asserts that with a sentinel key.\n *\n * COSTS A NON-CALLER NOTHING: it is a plain function behind the `./analytics` subpath, the package\n * is `sideEffects: false`, and this module has no top-level side effects, so a host that never\n * imports it never bundles it. It also mints no `globalThis` slot and retains no timer or listener.\n *\n * USAGE (dev builds only):\n *\n * import { wireDoctor } from \"@wireai/activation/analytics\";\n *\n * const report = await wireDoctor({\n * target: { serverUrl: \"https://api.example.com\", apiKey: DRIVELINE_KEY },\n * storage: AsyncStorage,\n * });\n * console.log(report.ok, report.checks);\n */\n// Imported DIRECTLY from the transport module, never through `./index`: the barrel would pull the\n// whole analytics surface into anything that touches the doctor. `readEventsAck` is module-exported\n// for exactly this, so the doctor reads an ack with the SAME reader the send paths use rather than\n// growing a second one to drift.\nimport {\n buildEventsRequest,\n makeSessionId,\n readEventsAck,\n type ClientEvent,\n type ClientEventTarget,\n} from \"./reportClientEvent\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\n\n/** RN sets this global; absent under node/SSR. Read defensively, exactly as `warnOnSkippedEvents` does. */\ndeclare const __DEV__: boolean | undefined;\n\n/** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */\nexport type WireDoctorCheck = {\n /**\n * Stable id: `target` | `reachability` | `storage` | `round_trip` | `dev_only` | `internal_error`.\n *\n * `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.\n * `internal_error` is the separate catch-all for a failure that got past every check's own\n * swallow. They are distinct names because a consumer branching on `dev_only` would otherwise\n * read an internal fault as \"this is a release build\" and report a healthy skip.\n */\n name: string;\n ok: boolean;\n /** Human-readable result. ⛔ Never contains any part of an API key. */\n detail: string;\n};\n\n/** What {@link wireDoctor} resolves to. `ok` is true only when EVERY check passed. */\nexport type WireDoctorReport = {\n ok: boolean;\n checks: WireDoctorCheck[];\n};\n\n/** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */\nexport type WireDoctorOptions = {\n /** The same `{serverUrl, apiKey}` the kit is configured with. */\n target: ClientEventTarget | undefined;\n /**\n * The host storage the offline queue would use (AsyncStorage-compatible). Omit it and the storage\n * check reports the DEGRADED in-memory mode rather than failing.\n */\n storage?: WireOnboardingStorage;\n};\n\n/**\n * The probe's own storage key. Deliberately NOT under the queue's `wireai:evtq:` namespace, so a\n * doctor run can never read, overwrite or delete a pending backlog. A diagnostic that eats a user's\n * unsent events is a worse bug than the one it was written to find.\n */\nconst PROBE_STORAGE_KEY = \"wireai:doctor:probe\";\n\n/** The `question_key` the synthetic round-trip event carries. `dry_run` means it is never written. */\nconst PROBE_QUESTION_KEY = \"wire_doctor_probe\";\n\n/** Tenant keys start with this. Used ONLY for a boolean comparison; ⛔ never interpolated into a detail. */\nconst EXPECTED_KEY_PREFIX = \"wai_\";\n\n/** Ceiling on each network check, so a hung server degrades to a failed check, never a hung caller. */\nconst NETWORK_TIMEOUT_MS = 10_000;\n\nconst check = (name: string, ok: boolean, detail: string): WireDoctorCheck => ({ name, ok, detail });\n\n/**\n * `fetch` with a timeout that is ALWAYS cleared, including on rejection. Resolves `undefined`\n * instead of throwing, so every caller stays on the happy path. Returns no timer to the caller and\n * leaves none pending: a diagnostic must not keep the JS thread alive after it has answered.\n */\nconst fetchWithTimeout = async (url: string, init?: RequestInit): Promise<Response | undefined> => {\n if (typeof fetch === \"undefined\") return undefined;\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : undefined;\n const timer = setTimeout(() => controller?.abort(), NETWORK_TIMEOUT_MS);\n try {\n return await fetch(url, controller ? { ...init, signal: controller.signal } : init);\n } catch {\n // Unreachable host, DNS failure, abort, or a missing fetch implementation.\n return undefined;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/**\n * CHECK 1: is the target usable at all?\n *\n * Reports the key's shape and NEVER its content: present/absent, character length, and whether the\n * prefix matches what a tenant key starts with. Length and a yes/no are enough to tell \"you pasted\n * the wrong string\" from \"you pasted nothing\", which is the entire diagnostic value here.\n */\nconst checkTarget = (target: ClientEventTarget | undefined): WireDoctorCheck => {\n const serverUrl = target?.serverUrl;\n if (typeof serverUrl !== \"string\" || serverUrl.trim().length === 0) {\n return check(\"target\", false, \"no serverUrl configured: set `serverUrl` on the kit config.\");\n }\n let parsedHost = false;\n try {\n const parsed = new URL(serverUrl);\n parsedHost = parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n } catch {\n parsedHost = false;\n }\n if (!parsedHost) {\n return check(\"target\", false, \"serverUrl is not a valid http(s) URL.\");\n }\n const apiKey = target?.apiKey;\n if (typeof apiKey !== \"string\" || apiKey.length === 0) {\n return check(\"target\", false, \"serverUrl looks valid, but no apiKey is configured.\");\n }\n // ⛔ `EXPECTED_KEY_PREFIX` is compared, never printed: it is itself a substring of a real key.\n const prefixOk = apiKey.startsWith(EXPECTED_KEY_PREFIX);\n return check(\n \"target\",\n true,\n `serverUrl is a valid http(s) URL; apiKey present (${apiKey.length} chars, expected prefix: ${\n prefixOk ? \"yes\" : \"no\"\n }).`,\n );\n};\n\n/**\n * CHECK 2: is the server there, and is it a Wire server?\n *\n * `GET /v1/events/contract` is public, cheap, and it answers the question the round-trip check\n * cannot answer on its own: a failure here means \"wrong URL / server down\", not \"bad payload\".\n */\nconst checkReachability = async (serverUrl: string): Promise<WireDoctorCheck> => {\n const url = `${serverUrl.replace(/\\/$/, \"\")}/v1/events/contract`;\n const res = await fetchWithTimeout(url, { method: \"GET\" });\n if (!res) {\n return check(\"reachability\", false, \"could not reach the server (network error or timeout).\");\n }\n const status = (res as { status?: unknown }).status;\n const ok = !!(res as { ok?: boolean }).ok;\n if (!ok) {\n return check(\n \"reachability\",\n false,\n `the server answered ${typeof status === \"number\" ? status : \"an error\"} for the events contract; ` +\n \"check the serverUrl points at a Wire server.\",\n );\n }\n return check(\"reachability\", true, \"the server answered the events contract.\");\n};\n\n/**\n * CHECK 3: can the offline queue actually persist?\n *\n * Write, read back, compare. Without working storage the queue silently degrades to in-memory only,\n * so a backgrounded app loses whatever it had not flushed, and nothing anywhere says so.\n *\n * The probe key is REMOVED in a `finally`, so a failing read or a throwing adapter still cleans up.\n */\nconst checkStorage = async (storage: WireOnboardingStorage | undefined): Promise<WireDoctorCheck> => {\n if (!storage) {\n return check(\n \"storage\",\n true,\n \"no storage injected: the queue runs in DEGRADED in-memory mode and loses pending events on an app kill. \" +\n \"Pass AsyncStorage (or an MMKV wrapper) to make it durable.\",\n );\n }\n const token = `probe_${Date.now().toString(36)}`;\n try {\n await storage.setItem(PROBE_STORAGE_KEY, token);\n const read = await storage.getItem(PROBE_STORAGE_KEY);\n if (read !== token) {\n return check(\n \"storage\",\n false,\n \"the storage adapter accepted a write but did not read the same value back, so the queue cannot persist.\",\n );\n }\n // ⚠️ \"write / read\" only: this returns BEFORE the `finally` runs its delete, and a throwing\n // `removeItem` is deliberately swallowed there, so the delete is attempted, never asserted.\n return check(\"storage\", true, \"storage write / read round trip succeeded.\");\n } catch {\n return check(\"storage\", false, \"the storage adapter threw, so the queue cannot persist events.\");\n } finally {\n // Always, including after a failed write or read: never leave the probe key behind.\n try {\n await storage.removeItem(PROBE_STORAGE_KEY);\n } catch {\n // A remove that throws is not worth failing the run over; nothing else depends on it.\n }\n }\n};\n\n/**\n * CHECK 4: does a real event survive real server validation?\n *\n * POSTs ONE synthetic event through the REAL {@link buildEventsRequest} with `dry_run: true`, so it\n * travels the exact bytes a production event travels and the server validates it exactly the same\n * way, but writes nothing. The probe is inert: it pollutes no funnel and no metric.\n *\n * ⚠️ The verdict is `written === 1 && skipped === 0`, read from the ACK BODY. A 200 is not a\n * receipt here: the endpoint returns 200 for a batch it discarded. A `ts`-shaped drift shows up as\n * `skipped: 1` with the server's own `field: \"ts\"`, which is the whole point of this check.\n */\nconst checkRoundTrip = async (target: ClientEventTarget): Promise<WireDoctorCheck> => {\n const event: ClientEvent = {\n event_type: \"app_event\",\n session_id: makeSessionId(),\n question_key: PROBE_QUESTION_KEY,\n };\n const req = buildEventsRequest(target, [event], { dryRun: true });\n if (!req) {\n return check(\"round_trip\", false, \"could not build the events request from this target.\");\n }\n const res = await fetchWithTimeout(req.url, req.init);\n if (!res) {\n return check(\"round_trip\", false, \"the events POST failed (network error or timeout).\");\n }\n if (!(res as { ok?: boolean }).ok) {\n const status = (res as { status?: unknown }).status;\n return check(\n \"round_trip\",\n false,\n `the events endpoint answered ${typeof status === \"number\" ? status : \"an error\"}; ` +\n \"a 401 here means the apiKey is not accepted.\",\n );\n }\n const ack = await readEventsAck(res);\n if (!ack) {\n return check(\n \"round_trip\",\n false,\n \"the server answered 2xx but sent no readable ack body, so it is unknown whether the event validated.\",\n );\n }\n if (ack.written === 1 && ack.skipped === 0) {\n return check(\"round_trip\", true, \"the server validated the probe event: written=1, skipped=0.\");\n }\n // The server's own reasons, verbatim, each already carrying the field it refused.\n return check(\n \"round_trip\",\n false,\n `the server DISCARDED the probe event: written=${ack.written ?? \"unknown\"}, skipped=${ack.skipped}` +\n (ack.reasons.length > 0\n ? `, reasons: ${ack.reasons.join(\", \")}.`\n : \". It gave no reason; check the server's ingest log for this request.\"),\n );\n};\n\n/**\n * Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.\n *\n * DEV-ONLY BY CONTRACT: the `__DEV__` guard is the FIRST statement, so a release build performs no\n * network call, no storage write, and no work at all. The report says it was skipped rather than\n * pretending everything passed, because a green report that never ran is the exact failure mode\n * this whole feature exists to remove.\n */\nexport const wireDoctor = async (options: WireDoctorOptions): Promise<WireDoctorReport> => {\n if (typeof __DEV__ === \"undefined\" || !__DEV__) {\n return {\n ok: false,\n checks: [\n check(\n \"dev_only\",\n false,\n \"wireDoctor is dev-only and did NOT run: __DEV__ is unset or false. Nothing was checked.\",\n ),\n ],\n };\n }\n try {\n const target = options?.target;\n const checks: WireDoctorCheck[] = [checkTarget(target)];\n // Reachability and the round trip both need a usable target; running them against a broken one\n // would report a network failure and bury the real cause, which check 1 already named.\n if (checks[0]!.ok && target) {\n checks.push(await checkReachability(target.serverUrl));\n checks.push(await checkStorage(options?.storage));\n checks.push(await checkRoundTrip(target));\n } else {\n checks.push(await checkStorage(options?.storage));\n }\n return { ok: checks.every((c) => c.ok), checks };\n } catch {\n // Belt and braces: every check above already swallows its own failures, so reaching here means\n // something hostile got past them. Still a report, still not a throw. The name is\n // `internal_error`, NOT `dev_only`: this ran and broke, which is the opposite of \"never ran\".\n return {\n ok: false,\n checks: [check(\"internal_error\", false, \"wireDoctor could not complete: an unexpected error was swallowed.\")],\n };\n }\n};\n","/**\n * permissionEvents - the canonical Wire names for a permission-priming funnel.\n *\n * Same job `purchaseEvents.ts` does for the subscription funnel and `analyticsEvent.ts` does for\n * the onboarding funnel: every app was naming these itself (`push_permission`, `NOTIF_PROMPT`,\n * `notifications_allowed`), so the same funnel read differently per tenant and no cross-app report\n * was possible. These are the ONE set of names.\n *\n * They are `app_event` `question_key` values on the wire, exactly like `WIRE_PURCHASE_EVENTS`, so\n * they are ALSO the exact strings a review / questionnaire firing trigger matches on. Never rename\n * one: a rename silently unfires every trigger configured against the old string.\n *\n * PURE + dependency-free: no React, no React Native, no transport, no imports outside the type\n * declarations - so an analytics-only bundle can carry the names without carrying a screen.\n */\nimport type { PermissionStage, WirePermissionKind, WirePermissionStatus } from \"./types\";\n\nexport const WIRE_PERMISSION_EVENTS = {\n /** The priming screen became visible. The denominator for every rate below. */\n screenShown: \"wire_permission_screen_shown\",\n /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */\n primerAccepted: \"wire_permission_primer_accepted\",\n /** The OS granted it. */\n granted: \"wire_permission_granted\",\n /** The OS refused it (a permanently blocked answer reports here too, with `status: \"blocked\"`). */\n denied: \"wire_permission_denied\",\n /** The user took the secondary. The one native prompt was NOT spent. */\n skipped: \"wire_permission_skipped\",\n /** A blocked user was redirected to the OS settings page. */\n settingsOpened: \"wire_permission_settings_opened\",\n} as const;\n\nexport type WirePermissionEventName =\n (typeof WIRE_PERMISSION_EVENTS)[keyof typeof WIRE_PERMISSION_EVENTS];\n\n/** The canonical event name for a stage. Exhaustive over the union (a new stage is a compile error). */\nexport const permissionEventName = (stage: PermissionStage): WirePermissionEventName => {\n switch (stage) {\n case \"shown\":\n return WIRE_PERMISSION_EVENTS.screenShown;\n case \"accepted\":\n return WIRE_PERMISSION_EVENTS.primerAccepted;\n case \"granted\":\n return WIRE_PERMISSION_EVENTS.granted;\n case \"denied\":\n return WIRE_PERMISSION_EVENTS.denied;\n case \"skipped\":\n return WIRE_PERMISSION_EVENTS.skipped;\n case \"settings\":\n return WIRE_PERMISSION_EVENTS.settingsOpened;\n default: {\n const _exhaustive: never = stage;\n return _exhaustive;\n }\n }\n};\n\n/**\n * The small, non-PII props that ride a permission event. `permission` is always present so one\n * funnel can be sliced per permission; `status` only appears when the OS actually answered, which\n * is what keeps a `blocked` refusal distinguishable from a plain `denied` without a second event.\n */\nexport const permissionEventProps = (\n permission: WirePermissionKind,\n status?: WirePermissionStatus,\n): Record<string, string> => (status ? { permission, status } : { permission });\n\n/**\n * Normalize whatever the host's `request` / `getStatus` actually returned.\n *\n * A native permission bridge is the host's code, and hosts return `\"undetermined\"`, `true`, or a\n * whole Expo response object. Anything the kit does not recognise is treated as `denied`: it is the\n * only reading that cannot invent a grant, and every outcome continues the flow anyway.\n */\nexport const normalizePermissionStatus = (value: unknown): WirePermissionStatus =>\n value === \"granted\" || value === \"denied\" || value === \"blocked\" ? value : \"denied\";\n","/**\n * Canonical analytics names for the onboarding funnel. The kit already emits a typed\n * `OnboardingEvent` (`started | turn | error | retry | fallback`) — but one app logged them as\n * `onboarding_*` and another as `AI_ONBOARDING_*`, so the same funnel reads differently per app.\n * This maps the kit event to ONE canonical `wire_onboarding_*` name + params, and the app logs\n * it through whatever transport it already has (Firebase, Amplitude, console). The app still\n * owns the logger; only the NAMES are standardized.\n *\n * <WireOnboarding\n * onEvent={(e) => { const a = toAnalyticsEvent(e); logEvent(a.name, a.params); }}\n * onComplete={(r) => { logEvent(WIRE_ONBOARDING_EVENTS.completed, { answers: Object.keys(r.answers).length }); persist(r); }}\n * />\n *\n * `completed` has no kit `OnboardingEvent` (the kit signals completion via `onComplete`, not\n * `onEvent`) — the app logs it explicitly on `onComplete` using the constant below, so the\n * funnel name stays canonical.\n */\nimport {\n permissionEventName,\n permissionEventProps,\n type WirePermissionEventName,\n} from \"../permissions/permissionEvents\";\nimport type { OnboardingEvent } from \"../types\";\n\nexport const WIRE_ONBOARDING_EVENTS = {\n started: \"wire_onboarding_started\",\n /** A persisted session was restored after an app kill (fires instead of `started`). */\n resumed: \"wire_onboarding_resumed\",\n turn: \"wire_onboarding_turn\",\n error: \"wire_onboarding_error\",\n retry: \"wire_onboarding_retry\",\n fallback: \"wire_onboarding_fallback\",\n /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */\n completed: \"wire_onboarding_completed\",\n} as const;\n\nexport type WireOnboardingEventName =\n (typeof WIRE_ONBOARDING_EVENTS)[keyof typeof WIRE_ONBOARDING_EVENTS];\n\nexport type AnalyticsEvent = {\n /**\n * A permission screen maps to its own canonical `wire_permission_*` name rather than to an\n * onboarding one: it is a distinct funnel (see `permissions/permissionEvents.ts`), and folding it\n * into `wire_onboarding_turn` would make every permission rate unreadable.\n */\n name: WireOnboardingEventName | WirePermissionEventName;\n params?: Record<string, unknown>;\n};\n\n/**\n * Map a kit `OnboardingEvent` to its canonical `{ name, params }`. Exhaustive over the union\n * (the `never` default makes a new event type a compile error here — intentional).\n */\nexport const toAnalyticsEvent = (event: OnboardingEvent): AnalyticsEvent => {\n switch (event.type) {\n case \"started\":\n return { name: WIRE_ONBOARDING_EVENTS.started };\n case \"resumed\":\n return { name: WIRE_ONBOARDING_EVENTS.resumed };\n case \"turn\":\n return {\n name: WIRE_ONBOARDING_EVENTS.turn,\n params: { step: event.step, component: event.component },\n };\n case \"error\":\n return { name: WIRE_ONBOARDING_EVENTS.error, params: { reason: event.reason } };\n case \"retry\":\n return {\n name: WIRE_ONBOARDING_EVENTS.retry,\n params: { reason: event.reason, attempt: event.attempt },\n };\n case \"fallback\":\n return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };\n case \"permission\":\n return {\n name: permissionEventName(event.stage),\n params: permissionEventProps(event.permission, event.status),\n };\n default: {\n const _exhaustive: never = event;\n return _exhaustive;\n }\n }\n};\n","/**\n * appVersion — best-effort, DEPENDENCY-FREE auto-detection of the host app's version string.\n *\n * WHY this exists: analytics segments the funnel `by_app_version`, but that breakdown is only\n * populated when a `device.appVersion` rides the event. `config.appVersion` (see types.ts) has\n * always been the way to supply it — but it is easy for a host to forget, and then the release\n * breakdown is silently empty. This module fills that gap: when the host does NOT pass a version,\n * the kit makes a best-effort read of the app version the host already ships in its Expo config,\n * so the breakdown works out of the box. An explicit `config.appVersion` always WINS over this.\n *\n * WHY it adds NO dependency (the kit's hard rule): `expo-constants` / `expo-application` are read\n * through a GUARDED require with a STRING-LITERAL specifier, inside a try/catch. Both halves are\n * required: the literal is what lets Metro COLLECT the dep, and the try/catch is what marks it\n * OPTIONAL, so an absent module becomes a `null` dependencyMap entry whose require throws \"Cannot\n * find module\" straight into the catch. (`withWireOnboarding` in metro/index.js adds a stub-to-\n * empty-module safety net for hosts that disable Metro's `allowOptionalDependencies`.) Nothing is\n * added to `package.json`; nothing is forced on the host.\n *\n * The specifier MUST stay a literal. A variable specifier (`const req = require; req(name)`) is not\n * \"lazier\" — it is broken: Metro collects dependencies statically, so a variable collects NOTHING,\n * the module never enters the bundle, and the aliased `require` is Metro's own numeric-id-keyed\n * `metroRequire`, which can never resolve a package-name string. That shape is why this function\n * returned `undefined` on every host despite `expo-constants` being installed, which silently\n * emptied the `by_app_version` breakdown this module exists to fill. See icons/expoIcons.ts for\n * the full autopsy, and reviews/storeReview.ts for the literal-specifier counter-example.\n *\n * PRIVACY: an app version string is not PII and identifies no user or device, so surfacing it\n * changes no App Privacy / Data Safety declaration (same guarantee as the rest of deviceContext).\n *\n * NEVER THROWS: every read is guarded; a missing/odd value yields `undefined`, never an exception.\n * Analytics must never be able to break onboarding.\n */\n\n// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime. Declared locally so\n// this type-checks without ambient Node types; the `typeof` guard keeps the reference ESM-safe.\ndeclare const require: ((id: string) => unknown) | undefined;\n\n/** A `require`-like resolver. Injectable in tests; production uses the guarded runtime require. */\nexport type OptionalRequire = (moduleName: string) => unknown;\n\n/** Trim + reject non-strings/empties so we only ever emit a real version string. */\nexport const coerceVersion = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Guarded runtime require.\n *\n * Every specifier below is a STRING LITERAL, because that is the only shape Metro's dependency\n * collector matches (callee literally `require`, argument literally a string). The `moduleName`\n * parameter exists only to keep the `OptionalRequire` seam shape for tests; an unrecognised name\n * resolves to undefined. See the header for why the old variable-specifier shape could not work.\n */\nconst runtimeRequire: OptionalRequire = (moduleName) => {\n if (typeof require !== \"function\") return undefined;\n try {\n switch (moduleName) {\n case \"expo-constants\":\n return require(\"expo-constants\");\n case \"expo-application\":\n return require(\"expo-application\");\n default:\n return undefined;\n }\n } catch {\n return undefined;\n }\n};\n\n/** Read a module's `default` (Expo modules are consumed as default exports) or the namespace. */\nconst interop = (mod: unknown): Record<string, unknown> | undefined => {\n if (!mod || typeof mod !== \"object\") return undefined;\n const def = (mod as { default?: unknown }).default;\n if (def && typeof def === \"object\") return def as Record<string, unknown>;\n return mod as Record<string, unknown>;\n};\n\n/** Resolve a module namespace, swallowing a throwing require (an uninstalled module throws). */\nconst safeInterop = (\n requireModule: OptionalRequire,\n moduleName: string,\n): Record<string, unknown> | undefined => {\n try {\n return interop(requireModule(moduleName));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Detect the host app version, preferring `expo-constants` (`expoConfig.version`, then\n * `nativeAppVersion`) and finally `expo-application` (`nativeApplicationVersion`). Returns the\n * first real string, or `undefined` when none of those are available. Pure and never throws.\n *\n * `requireModule` is injectable so tests can exercise the \"found\" path without the native modules;\n * production defaults to the guarded runtime require above.\n */\nexport const detectAppVersion = (\n requireModule: OptionalRequire = runtimeRequire,\n): string | undefined => {\n try {\n const constants = safeInterop(requireModule, \"expo-constants\");\n if (constants) {\n const expoConfig = constants.expoConfig;\n if (expoConfig && typeof expoConfig === \"object\") {\n const fromExpoConfig = coerceVersion((expoConfig as { version?: unknown }).version);\n if (fromExpoConfig) return fromExpoConfig;\n }\n const fromNative = coerceVersion(constants.nativeAppVersion);\n if (fromNative) return fromNative;\n }\n\n const application = safeInterop(requireModule, \"expo-application\");\n if (application) {\n const fromApplication = coerceVersion(application.nativeApplicationVersion);\n if (fromApplication) return fromApplication;\n }\n } catch {\n // Any unexpected read error → \"unknown\"; analytics must never crash onboarding.\n }\n return undefined;\n};\n","/**\n * deviceModel — best-effort, DEPENDENCY-FREE detection of the device MODEL on iOS.\n *\n * WHY this exists (the asymmetry it closes): `collectDeviceContext` already reports `device.model`\n * on Android straight from `Platform.constants.Model` (e.g. \"SM-G991B\"), but iOS `Platform.constants`\n * exposes NO model — only `osVersion` / `interfaceIdiom`. So the analytics `by_model` breakdown was\n * Android-only. This module fills the iOS gap with the SAME zero-dependency technique the kit already\n * uses for `appVersion` (see device/appVersion.ts): a GUARDED, STRING-LITERAL `require` of the common\n * Expo `expo-device` module, inside a try/catch — the literal is what lets Metro COLLECT the dep and\n * the try/catch is what marks it OPTIONAL, so an absent module degrades instead of failing the build.\n * (`withWireOnboarding` in metro/index.js adds a stub-to-empty-module safety net for hosts that\n * disable Metro's `allowOptionalDependencies`.) If the host has it, iOS gets a model out of the box;\n * if not, the read yields `undefined` and iOS `model` stays omitted — nothing is forced, and nothing\n * is added to `package.json`.\n *\n * The specifier MUST stay a literal. A variable specifier (`const req = require; req(name)`) collects\n * NOTHING under Metro's static dependency collector, so the module never enters the bundle and the\n * aliased `require` — really Metro's numeric-id-keyed `metroRequire` — can never resolve a package\n * name. See icons/expoIcons.ts for the full autopsy.\n *\n * WHY it is NOT PII: `expo-device`'s `modelName` (\"iPhone 14 Pro\") and `modelId` (\"iPhone15,2\") are a\n * device CLASS shared by millions of units — the same privacy category as the Android `Model` the kit\n * already sends. It is NOT a unique device id / IDFA / fingerprint, so surfacing it changes no App\n * Privacy / Data Safety declaration (identical guarantee to the rest of deviceContext). It deliberately\n * does NOT read `expo-device`'s `deviceName` (that is the user-set name, e.g. \"Malik's iPhone\", and IS\n * personal data).\n *\n * NEVER THROWS: every read is guarded; a missing/odd value yields `undefined`, never an exception.\n */\n\n// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime. Declared locally so\n// this type-checks without ambient Node types; the `typeof` guard keeps the reference ESM-safe.\ndeclare const require: ((id: string) => unknown) | undefined;\n\n/** A `require`-like resolver. Injectable in tests; production uses the guarded runtime require. */\nexport type OptionalRequire = (moduleName: string) => unknown;\n\n/** Trim + reject non-strings/empties so we only ever emit a real model string. */\nconst coerceModel = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Guarded runtime require.\n *\n * The specifier is a STRING LITERAL, because that is the only shape Metro's dependency collector\n * matches (callee literally `require`, argument literally a string). The `moduleName` parameter\n * exists only to keep the `OptionalRequire` seam shape for tests; an unrecognised name resolves to\n * undefined. (See the twin guard in ./appVersion.ts.)\n */\nconst runtimeRequire: OptionalRequire = (moduleName) => {\n if (moduleName !== \"expo-device\") return undefined;\n if (typeof require !== \"function\") return undefined;\n try {\n return require(\"expo-device\");\n } catch {\n return undefined;\n }\n};\n\n/** Read a module's `default` (Expo modules are often consumed as default exports) or the namespace. */\nconst interop = (mod: unknown): Record<string, unknown> | undefined => {\n if (!mod || typeof mod !== \"object\") return undefined;\n const def = (mod as { default?: unknown }).default;\n if (def && typeof def === \"object\") return def as Record<string, unknown>;\n return mod as Record<string, unknown>;\n};\n\n/** Resolve a module namespace, swallowing a throwing require (an uninstalled module throws). */\nconst safeInterop = (\n requireModule: OptionalRequire,\n moduleName: string,\n): Record<string, unknown> | undefined => {\n try {\n return interop(requireModule(moduleName));\n } catch {\n return undefined;\n }\n};\n\n/**\n * Detect the device model via `expo-device`, preferring the human-readable `modelName`\n * (\"iPhone 14 Pro\") and falling back to the identifier `modelId` (\"iPhone15,2\"). Returns the first\n * real string, or `undefined` when `expo-device` is absent. Pure and never throws.\n *\n * `requireModule` is injectable so tests can exercise the \"found\" path without the native module;\n * production defaults to the guarded runtime require above.\n */\nexport const detectNativeModel = (\n requireModule: OptionalRequire = runtimeRequire,\n): string | undefined => {\n try {\n const device = safeInterop(requireModule, \"expo-device\");\n if (device) {\n const modelName = coerceModel(device.modelName);\n if (modelName) return modelName;\n const modelId = coerceModel(device.modelId);\n if (modelId) return modelId;\n }\n } catch {\n // Any unexpected read error → undefined; analytics must never crash onboarding.\n }\n return undefined;\n};\n","/**\n * deviceContext — collect a small, privacy-label-neutral snapshot of the device so\n * onboarding analytics can segment the funnel (platform / form factor / locale) WITHOUT\n * adding a single dependency to the kit or changing a host app's App Privacy / Data Safety\n * declarations.\n *\n * HARD RULE (why this file adds no dependency):\n * The kit stays dependency-free. Everything here comes from `Platform`, `Dimensions`,\n * `I18nManager`, and the standard `Intl` global — plus a best-effort `appVersion` read via\n * `detectAppVersion()`, which itself adds NO dependency (it reaches for `expo-constants` /\n * `expo-application` through a guarded, variable-specifier require that a host without them\n * simply never resolves — see device/appVersion.ts). There are NO advertising IDs, NO\n * `getUniqueId`/IDFA/GAID/fingerprinting APIs, and nothing that would require a new\n * privacy-label entry. A host can adopt this without touching its store declarations.\n *\n * DEFENSIVE BY DESIGN: `collectDeviceContext()` never throws. Every read is guarded and\n * a missing/unavailable field is simply omitted (Hermes may ship without full `Intl`,\n * `Platform.constants` differs per OS and RN version, etc.). Analytics must never be able\n * to break onboarding.\n */\nimport { Dimensions, I18nManager, Platform } from \"react-native\";\n\nimport { detectAppVersion } from \"./appVersion\";\nimport { detectNativeModel } from \"./deviceModel\";\n\n/** Coarse device class. iOS uses the reported interface idiom; else a screen-size heuristic. */\nexport type DeviceFormFactor = \"phone\" | \"tablet\";\n\n/**\n * A privacy-label-neutral device snapshot. ALL fields except `platform` are optional and are\n * omitted when unavailable. Nothing here identifies a user or device uniquely.\n */\nexport type DeviceContext = {\n /** `Platform.OS` — \"ios\" | \"android\" | \"windows\" | \"macos\" | \"web\". Always present. */\n platform: typeof Platform.OS;\n /** OS version string (iOS `osVersion`/`Platform.Version`, Android `Release`). */\n osVersion?: string;\n /** Android device brand (e.g. \"samsung\"). Android only. */\n brand?: string;\n /**\n * Device model. On Android it is read directly from `Platform.constants.Model` (e.g. \"SM-G991B\").\n * iOS `Platform.constants` exposes NO model, so on iOS it is a BEST-EFFORT read of `expo-device`'s\n * `modelName` (\"iPhone 14 Pro\"), falling back to `modelId` (\"iPhone15,2\") — dependency-free via a\n * guarded require (see device/deviceModel.ts). ASYMMETRY: without `expo-device` installed, iOS\n * `model` is omitted (there is no dependency-free iOS model in the already-used RN surface, and the\n * kit will not add a native dep for it); Android needs no extra module. Not PII (a device class,\n * not a unique id), so it changes no privacy-label declaration.\n */\n model?: string;\n /** iOS interface idiom (\"phone\" | \"pad\" | …), when reported. iOS only. */\n interfaceIdiom?: string;\n /** Derived device class. */\n formFactor?: DeviceFormFactor;\n /** `Dimensions.get('screen')` width in dp. */\n screenWidth?: number;\n /** `Dimensions.get('screen')` height in dp. */\n screenHeight?: number;\n /** Screen pixel density (`scale`). */\n screenScale?: number;\n /** Right-to-left layout (`I18nManager.isRTL`). */\n isRTL?: boolean;\n /** Resolved locale (e.g. \"en-US\"), from `Intl` when available. */\n locale?: string;\n /** IANA time zone (e.g. \"Europe/Berlin\"), from `Intl` when available. */\n timeZone?: string;\n /**\n * Host app version (e.g. \"1.4.2\"). BEST-EFFORT auto-detected here via `detectAppVersion()`\n * (reads `expo-constants` / `expo-application` when present; adds no dependency — see\n * device/appVersion.ts). An explicit host-injected `config.appVersion` always WINS: the merge\n * sites (`WireOnboarding`, the session-analytics hooks, the context envelope) overwrite this\n * with the host value when one is supplied. Omitted when neither source yields a version.\n */\n appVersion?: string;\n};\n\n/** iOS idiom wins; otherwise the shortest side in dp (>= 600 → tablet) picks the class. */\nconst deriveFormFactor = (\n iosIdiom: string | undefined,\n width: number | undefined,\n height: number | undefined,\n): DeviceFormFactor | undefined => {\n if (iosIdiom === \"pad\") return \"tablet\";\n if (iosIdiom === \"phone\") return \"phone\";\n if (typeof width === \"number\" && typeof height === \"number\") {\n return Math.min(width, height) >= 600 ? \"tablet\" : \"phone\";\n }\n return undefined;\n};\n\n/**\n * Collect the device snapshot. Pure, synchronous, and never throws — call it once per\n * onboarding session. Missing fields are omitted rather than sent as null/undefined so the\n * payload (and the server's stored dict) stays compact.\n */\nexport const collectDeviceContext = (): DeviceContext => {\n const ctx: DeviceContext = { platform: Platform.OS };\n\n // OS version (fallback; per-OS constants below may refine it).\n try {\n const version = Platform.Version;\n if (version !== undefined && version !== null && String(version)) {\n ctx.osVersion = String(version);\n }\n } catch {\n // ignore\n }\n\n // `Platform.constants` shape differs by OS and RN version — read every field defensively.\n let constants: Record<string, unknown> = {};\n try {\n constants = (Platform.constants ?? {}) as Record<string, unknown>;\n } catch {\n constants = {};\n }\n\n let iosIdiom: string | undefined;\n try {\n if (Platform.OS === \"android\") {\n const brand = constants.Brand;\n const model = constants.Model;\n const release = constants.Release;\n if (typeof brand === \"string\" && brand) ctx.brand = brand;\n if (typeof model === \"string\" && model) ctx.model = model;\n if (release !== undefined && release !== null && String(release)) {\n ctx.osVersion = String(release);\n }\n } else if (Platform.OS === \"ios\") {\n const osVersion = constants.osVersion;\n const idiom = constants.interfaceIdiom;\n if (osVersion !== undefined && osVersion !== null && String(osVersion)) {\n ctx.osVersion = String(osVersion);\n }\n if (typeof idiom === \"string\" && idiom) {\n ctx.interfaceIdiom = idiom;\n iosIdiom = idiom;\n }\n // iOS has no model in `Platform.constants`; best-effort via `expo-device` (adds no dep,\n // omitted when the module is absent — see device/deviceModel.ts).\n const iosModel = detectNativeModel();\n if (iosModel) ctx.model = iosModel;\n }\n } catch {\n // ignore per-OS constant reads\n }\n\n // Screen dimensions + derived form factor.\n try {\n const screen = Dimensions.get(\"screen\");\n if (screen) {\n if (typeof screen.width === \"number\") ctx.screenWidth = screen.width;\n if (typeof screen.height === \"number\") ctx.screenHeight = screen.height;\n if (typeof screen.scale === \"number\") ctx.screenScale = screen.scale;\n const formFactor = deriveFormFactor(iosIdiom, screen.width, screen.height);\n if (formFactor) ctx.formFactor = formFactor;\n }\n } catch {\n // ignore\n }\n\n // Layout direction.\n try {\n ctx.isRTL = I18nManager.isRTL;\n } catch {\n // ignore\n }\n\n // Locale + time zone via the standard Intl global. Hermes may ship without full Intl,\n // so referencing it can throw ReferenceError — the try/catch covers that too.\n try {\n const resolved = Intl.DateTimeFormat().resolvedOptions();\n if (resolved.locale) ctx.locale = resolved.locale;\n if (resolved.timeZone) ctx.timeZone = resolved.timeZone;\n } catch {\n // Intl unavailable — omit locale/timeZone.\n }\n\n // Best-effort host app version (adds no dependency; omitted when unavailable). An explicit\n // `config.appVersion` overrides this downstream at the merge sites.\n const appVersion = detectAppVersion();\n if (appVersion) ctx.appVersion = appVersion;\n\n return ctx;\n};\n","/**\n * contextEnvelope — a small, PRIVACY-NEUTRAL context bundle stamped onto every outgoing\n * analytics event, giving the Wire dashboard the Sentry/Firebase-parity segmentation fields\n * (device model, OS + version, screen, locale, timezone, form factor) plus a few host-injected\n * scalars (session correlation id, app version + native build number, connectivity type).\n *\n * WHY a separate builder (not just `collectDeviceContext`): the envelope COMPOSES the existing\n * device snapshot with the handful of extras a host can cheaply supply but the kit can't collect\n * dependency-free (native build number, connectivity type). It never re-implements device\n * collection — it reuses `collectDeviceContext()` verbatim (see device/deviceContext.ts).\n *\n * HARD PRIVACY RULE (why this file, like deviceContext.ts, adds nothing new):\n * NEVER GPS / location, NEVER an advertising id (IDFA / GAID), NEVER a device fingerprint.\n * Location is derived SERVER-SIDE from IP-geo only — nothing here carries a coordinate or an\n * ad id, so a host adopting this changes no App Privacy / Data Safety declaration. There is a\n * test (contextEnvelope.test.ts) that asserts the ABSENCE of any such field.\n *\n * DEPENDENCY-FREE: the only import is the kit's own `collectDeviceContext`. `networkType` and\n * `appBuild` are HOST-INJECTED — there is no dependency-free RN core signal for either, so the\n * envelope simply omits them when the host does not pass them (no forced peer dependency).\n */\nimport { collectDeviceContext, type DeviceContext } from \"../device/deviceContext\";\n\n/**\n * The context stamped onto every event. `device` is always present (from\n * `collectDeviceContext`); every scalar is optional and OMITTED when the host does not supply it.\n */\nexport type ContextEnvelope = {\n /** The privacy-neutral device snapshot (reused from `collectDeviceContext`). */\n device: DeviceContext;\n /** Correlation id for this app-open / flow (caller-supplied). */\n sessionId?: string;\n /** App version, e.g. \"1.4.2\" (mirrors `device.appVersion`; host-injected, else auto-detected). */\n appVersion?: string;\n /** Host native build number, e.g. \"412\" (from `expo-constants` `nativeBuildVersion`). */\n appBuild?: string;\n /** Host connectivity signal, e.g. \"wifi\" | \"cellular\" (from `@react-native-community/netinfo`). */\n networkType?: string;\n};\n\n/** Host-injected inputs for {@link buildContextEnvelope}. All optional; each is omitted when absent. */\nexport type ContextEnvelopeInput = {\n sessionId?: string;\n appVersion?: string;\n appBuild?: string;\n networkType?: string;\n};\n\n/**\n * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block (which\n * already carries a best-effort auto-detected `appVersion`) and layers the host-injected scalars\n * on top. An explicit `input.appVersion` overrides the auto-detected `device.appVersion`, and the\n * outer `appVersion` scalar mirrors whichever version is effective.\n *\n * Returns a NEW object on every call (no shared mutable reference), so a caller can hold or mutate\n * the result without leaking into the next envelope. Never throws — `collectDeviceContext` is\n * itself guarded, and the rest is plain assignment.\n */\nexport const buildContextEnvelope = (input: ContextEnvelopeInput = {}): ContextEnvelope => {\n // Fresh copy so the returned envelope never aliases a cached device snapshot.\n const device: DeviceContext = { ...collectDeviceContext() };\n\n // `device.appVersion` is auto-detected best-effort by `collectDeviceContext`; an explicit\n // host `input.appVersion` always wins.\n if (input.appVersion) device.appVersion = input.appVersion;\n\n // The outer scalar mirrors the effective version (host-supplied, else auto-detected) so the\n // queue can stamp `user_context.app_version` even when the host never passed one.\n const effectiveAppVersion = input.appVersion ?? device.appVersion;\n\n const envelope: ContextEnvelope = { device };\n if (input.sessionId) envelope.sessionId = input.sessionId;\n if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;\n if (input.appBuild) envelope.appBuild = input.appBuild;\n if (input.networkType) envelope.networkType = input.networkType;\n\n return envelope;\n};\n","/**\n * eventQueue — the OFFLINE-FIRST, persistent transport buffer for client analytics events.\n *\n * The existing `reportClientEvents` is a blind fire-and-forget POST: it returns `void`, has no\n * success signal, and drops events when the network is down. The analytics data story depends on\n * NEVER losing an event offline, so this queue adds the missing durability layer on top of the\n * same `POST {serverUrl}/v1/events` contract:\n *\n * • Persists pending events to the host-injected `WireOnboardingStorage` (survives app kills).\n * • Batches them into one request body `{ events: [...] }`.\n * • Owns its OWN awaited `fetch` that reads `res.ok` — the only way to drive retry + dequeue,\n * since `reportClientEvents` cannot ack. A 2xx dequeues the batch; a non-ok / rejected / thrown\n * response keeps it and schedules an exponential backoff retry.\n * • Flushes on `enqueue`, on an explicit `flush()`, and on `notifyOnline()` (host reconnect).\n * • Caps the buffer (drop-OLDEST under pressure) and de-dups identical pending events.\n * • Stamps the current context envelope (device + host scalars) onto every event before send.\n *\n * FIRE-AND-FORGET (load-bearing): `enqueue` returns immediately and NEVER throws into the UI. A\n * missing `fetch`, a hung/broken storage, a rejecting network, or a JSON error is swallowed and\n * degrades gracefully — analytics must never be able to break the app. In-memory fallback covers\n * the no-storage case (survives re-renders, not app kills).\n *\n * DEPENDENCY-FREE: no network-detection or persistence library. Connectivity is host-driven via\n * `notifyOnline()`; persistence is the host-injected AsyncStorage-compatible subset.\n */\nimport type { ContextEnvelope } from \"./contextEnvelope\";\nimport {\n buildEventsRequest,\n warnOnSkippedEvents,\n type ClientEvent,\n type ClientEventTarget,\n} from \"./reportClientEvent\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/** Envelope source: a fixed envelope or a provider evaluated at enqueue time (fresh network type). */\nexport type EnvelopeSource = ContextEnvelope | (() => ContextEnvelope | undefined);\n\n/** Options for {@link createEventQueue}. Only `target` is conceptually required to actually send. */\nexport type EventQueueOptions = {\n /** Where to POST — the tenant transport (`serverUrl` + `apiKey`), same as `WireOnboardingConfig`. */\n target: ClientEventTarget | undefined;\n /**\n * Host persistence (AsyncStorage-compatible subset). When omitted, the queue runs in the\n * documented DEGRADED in-memory mode — it survives re-renders but not an app kill.\n */\n storage?: WireOnboardingStorage;\n /** Tenant/app id used to namespace the default storage key (`wireai:evtq:<appId>`). */\n appId?: string;\n /** Explicit storage key override (wins over the `appId`-derived default). */\n storageKey?: string;\n /** The context envelope stamped onto every event before send (device + host scalars). */\n envelope?: EnvelopeSource;\n /** Max pending events; enqueuing past this DROPS THE OLDEST first (default 200). */\n maxSize?: number;\n /** Events per POST batch (default 20). */\n batchSize?: number;\n /** First retry delay in ms; doubles each failed attempt (default 1000). */\n baseBackoffMs?: number;\n /** Backoff ceiling in ms (default 30000). */\n maxBackoffMs?: number;\n /** Max AUTOMATIC backoff retries before pausing (default 6); `notifyOnline()`/`flush()` re-arm it. */\n maxRetries?: number;\n};\n\n/** The queue's public surface. `enqueue` is fire-and-forget (returns immediately, never throws). */\nexport type EventQueue = {\n /** Buffer one event (envelope-stamped), persist, and schedule a flush. Never throws. */\n enqueue(event: ClientEvent): void;\n /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */\n flush(): void;\n /** Host reconnect signal: reset backoff and drain immediately. Fire-and-forget. */\n notifyOnline(): void;\n /** Current pending (in-memory) count. */\n size(): number;\n /**\n * Tear this queue down when its owner goes away (a hook's effect cleanup, a host disposing its\n * own instance). Idempotent, never throws, and REQUIRED for any queue that can be re-created:\n * a remount builds a second queue while the first is still holding the storage claim and a live\n * backoff timer, and the two then fight over one persisted slot.\n *\n * It clears the retry timer, releases the claimed storage key so a replacement gets the SAME slot\n * instead of a rotated `…#2` nobody reads next launch, and makes the queue inert — `enqueue`,\n * `flush` and every write become no-ops, so a timer that already fired cannot `removeItem` the\n * slot the live queue owns. Anything still buffered in memory at that point stays on disk under\n * the released key, which is exactly where the replacement queue looks for it.\n */\n dispose(): void;\n};\n\nconst DEFAULTS = {\n maxSize: 200,\n batchSize: 20,\n baseBackoffMs: 1000,\n maxBackoffMs: 30000,\n maxRetries: 6,\n} as const;\n\n/** Ceiling on the persisted-backlog read — a hung adapter degrades to an empty start, never a stall. */\nconst READ_TIMEOUT_MS = 1500;\n\n// ── One storage slot per QUEUE, not per appId ────────────────────────────────────────────────────\n//\n// The default key was derived from `appId` alone, so two `createAnalytics` instances for one tenant —\n// the documented double-wiring, a façade for `track`/`screen` plus an activation instance — shared ONE\n// persisted backlog while keeping SEPARATE in-memory buffers. Every symptom of that is silent:\n// each `persist()` overwrites the other's blob with its own view of \"pending\"; a queue that drains to\n// empty calls `removeItem` and DELETES a sibling's still-pending events; and on relaunch whatever\n// survived is loaded by both instances and sent twice. `useLifecycleEvents` already avoided all of it\n// by handing its queue a dedicated explicit key — this generalizes that.\n//\n// A `Symbol.for` slot for the same reason as every other registry here: tsup inlines a copy of this\n// module into each bundle, and a plain module `let` would let the `.` and `./analytics` copies each\n// think they were the first claimant of the same key.\n//\n// @globalSlot LATCH — the claimed-key Set is created once and its identity is then stable. A second\n// write hands the next queue an EMPTY claim list, so it re-claims a key a live queue already owns\n// and the two silently share one persisted backlog again. Its MEMBERS are live, which is why\n// `claimedQueueKeys()` re-reads the slot on every call rather than caching the Set.\nconst QUEUE_KEY_SLOT: unique symbol = Symbol.for(\"@wireai/activation:eventQueueKeys\");\n\ntype GlobalWithQueueKeys = typeof globalThis & { [QUEUE_KEY_SLOT]?: Set<string> };\n\nconst queueKeyGlobal = globalThis as GlobalWithQueueKeys;\n\nconst claimedQueueKeys = (): Set<string> => {\n const existing = queueKeyGlobal[QUEUE_KEY_SLOT];\n if (existing) return existing;\n const created = new Set<string>();\n queueKeyGlobal[QUEUE_KEY_SLOT] = created;\n return created;\n};\n\n/**\n * Claim `preferred` for this queue, or the next free `preferred#N` when a live queue already holds it.\n *\n * ONLY the appId-derived DEFAULT is ever rotated. An EXPLICIT `storageKey` is the host (or\n * `useLifecycleEvents`) declaring which slot a queue owns, and that hook re-creates its queue on every\n * remount — rotating there would silently walk it off its own backlog once per remount, which is a\n * worse bug than the one being fixed.\n */\nconst claimQueueKey = (preferred: string, explicit: boolean): string => {\n const claimed = claimedQueueKeys();\n if (explicit || !claimed.has(preferred)) {\n claimed.add(preferred);\n return preferred;\n }\n let ordinal = 2;\n while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;\n const key = `${preferred}#${ordinal}`;\n claimed.add(key);\n warnInDev(\n `[wireai] a second event queue was created for the same appId, and \"${preferred}\" is already ` +\n \"claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, \" +\n \"delete each other's pending events when one drains to empty, and double-send on relaunch, so \" +\n `this queue was given \"${key}\" instead. Prefer ONE analytics instance per app; if you really ` +\n \"need two, pass an explicit `storageKey` to each so the slots are yours to reason about.\",\n );\n return key;\n};\n\n/**\n * Give a claimed key back, so the NEXT queue for the same appId gets the real slot instead of a\n * rotated one. Called only from {@link EventQueue.dispose}.\n *\n * WHY THIS HAS TO EXIST: the claim was write-only, and \"already claimed\" was read as \"held by a LIVE\n * one\" — liveness nothing ever tracked. A remount (nav, Fast Refresh, StrictMode's double-invoke, an\n * appId that arrives async) therefore rotated the replacement onto `…#2`, and from then on the whole\n * launch persisted into a slot the next launch never reads: it claims the base key, finds the older\n * blob, and the `#N` ones accumulate forever. Releasing on teardown makes the claim mean what its\n * warning already claimed it meant.\n */\nconst releaseQueueKey = (key: string): void => {\n claimedQueueKeys().delete(key);\n};\n\n/** Test-only: forget every claimed queue key. A real RELAUNCH is a new process, so a test that\n * simulates one in-process must call this or its second queue reads as a concurrent sibling.\n * Exported from `@wireai/activation/analytics`, matching `resetAutoDeviceKeys` / `resetCurrentSessionId`. */\nexport const resetEventQueueKeys = (): void => claimedQueueKeys().clear();\n\n/** Internal buffered item. `id` is a local monotonic handle for deterministic dequeue-after-ack;\n * it is NEVER sent to the server. `sig` is the de-dup signature (serialized stamped event). */\ntype QueuedItem = { id: number; event: ClientEvent; sig: string };\n\n/** Persisted shape — the local id + the (already envelope-stamped) event. `sig` is recomputed on load. */\ntype PersistedItem = { id: number; event: ClientEvent };\n\n/**\n * The verdict of a read that ran out of time. A DISTINCT value, and never `undefined` — an\n * `undefined` here is byte-identical to \"the adapter answered, there is no backlog\", and that\n * conflation destroys data silently: `parsePersisted(undefined)` gives `[]`, hydration takes its\n * `length === 0` early return without throwing, and the next `persist()` writes the in-memory\n * pending list over a blob nobody has read — or, with nothing pending, `removeItem`s it outright.\n */\nconst READ_TIMED_OUT: unique symbol = Symbol(\"wireai:storage-read-timeout\");\n\nconst withTimeout = <T>(p: Promise<T>, ms: number): Promise<T | typeof READ_TIMED_OUT> => {\n let timer: ReturnType<typeof setTimeout>;\n const timeout = new Promise<typeof READ_TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);\n });\n return Promise.race([p, timeout]).finally(() => clearTimeout(timer));\n};\n\n/** Detach a timer from the event loop where the runtime supports it (Node test process / some RNs). */\nconst unrefTimer = (timer: ReturnType<typeof setTimeout>): void => {\n const t = timer as unknown as { unref?: () => void };\n if (typeof t.unref === \"function\") t.unref();\n};\n\nconst parsePersisted = (raw: string | null | undefined): PersistedItem[] => {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n const items: PersistedItem[] = [];\n for (const entry of parsed) {\n if (\n entry &&\n typeof entry === \"object\" &&\n typeof (entry as PersistedItem).id === \"number\" &&\n (entry as PersistedItem).event &&\n typeof (entry as PersistedItem).event === \"object\"\n ) {\n items.push(entry as PersistedItem);\n }\n }\n return items;\n } catch {\n // Corrupt backlog → start empty; the next persist overwrites it.\n return [];\n }\n};\n\n/**\n * Create an offline-first event queue. Loads any persisted backlog on creation so a\n * killed-and-relaunched app resumes where it left off. Returns the {@link EventQueue} surface.\n */\nexport const createEventQueue = (options: EventQueueOptions): EventQueue => {\n const target = options.target;\n const storage = options.storage;\n // One slot per QUEUE: an explicit key is taken verbatim; the appId-derived default is rotated\n // to `…#2` when a live queue already holds it, so two instances can never share one backlog.\n //\n // Only claimed when there IS storage. The defect is entirely about the persisted slot, and a\n // storage-less queue (the documented degraded in-memory mode) never reads or writes the key — so\n // claiming there would warn about a collision that cannot happen.\n const defaultKey = options.storageKey ?? `wireai:evtq:${options.appId ?? \"default\"}`;\n const key = storage ? claimQueueKey(defaultKey, options.storageKey !== undefined) : defaultKey;\n const maxSize = options.maxSize ?? DEFAULTS.maxSize;\n const batchSize = options.batchSize ?? DEFAULTS.batchSize;\n const baseBackoffMs = options.baseBackoffMs ?? DEFAULTS.baseBackoffMs;\n const maxBackoffMs = options.maxBackoffMs ?? DEFAULTS.maxBackoffMs;\n const maxRetries = options.maxRetries ?? DEFAULTS.maxRetries;\n\n let pending: QueuedItem[] = [];\n let nextId = 0;\n let flushing = false;\n let attempt = 0;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n // Set by `dispose()`. Every write path and the drain check it, because a queue whose owner is\n // gone must not touch a storage slot a replacement queue now owns.\n let disposed = false;\n // The persisted blob may exist and has NOT been read yet. While this is true every write is\n // suppressed: the only thing the queue could write is a view of `pending` that does not include\n // the backlog it has not seen, and `setItem`/`removeItem` would destroy it. Cleared as soon as the\n // read settles, whichever way it settles, so a read that ultimately fails resumes normal\n // persistence rather than suppressing it for the life of the process.\n //\n // ARMED AT CONSTRUCTION, not when the 1500ms timeout fires. It used to be set ONLY in the timeout\n // branch, so for the first 1500ms of every launch the comment above was false and the queue wrote\n // freely over a blob it had not read. The sharp form is not the overwrite but the DELETE: a queue\n // that drains to empty inside that window calls `removeItem` on the slot, and an adapter that\n // resolves a read against its current state (a bridge that queues operations and runs them in\n // order — not every adapter snapshots at call time) then answers `null`. The whole backlog is\n // gone, with no error anywhere.\n let backlogUnread = storage !== undefined;\n\n const resolveEnvelope = (): ContextEnvelope | undefined => {\n try {\n return typeof options.envelope === \"function\" ? options.envelope() : options.envelope;\n } catch {\n return undefined;\n }\n };\n\n // Stamp the current envelope onto a COPY of the event (never mutate the caller's object):\n // device → event.device (when the event has none)\n // sessionId → event.session_id (when the event has none)\n // appVersion / appBuild / networkType → event.user_context (the non-PII bucket the server\n // sanitizes), never overwriting a key the caller already set.\n const stamp = (event: ClientEvent): ClientEvent => {\n const env = resolveEnvelope();\n const stamped: ClientEvent = { ...event };\n // Client enqueue timestamp: distinguishes a GENUINE repeat (same event fired seconds apart)\n // from a REDUNDANT re-enqueue of the same instant. The de-dup signature below includes it, so\n // two identical events enqueued in the same millisecond still collapse (a re-render), while the\n // same action repeated later carries a fresh `ts` and survives. A caller-set `ts` is preserved.\n // This stays a NUMBER in the queue (the de-dup signature depends on it); `buildEventsRequest`\n // serializes it to the ISO8601 string the server requires at send time.\n if (stamped.ts === undefined) stamped.ts = Date.now();\n if (!env) return stamped;\n if (!stamped.device && env.device) stamped.device = env.device;\n if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;\n const uc: Record<string, string | number | boolean> = { ...(stamped.user_context ?? {}) };\n if (env.appVersion && uc.app_version === undefined) uc.app_version = env.appVersion;\n if (env.appBuild && uc.app_build === undefined) uc.app_build = env.appBuild;\n if (env.networkType && uc.network_type === undefined) uc.network_type = env.networkType;\n if (Object.keys(uc).length > 0) stamped.user_context = uc;\n return stamped;\n };\n\n const persist = (): void => {\n if (!storage) return;\n // A disposed queue no longer owns this slot — a replacement may. See `dispose`.\n if (disposed) return;\n // NEVER write over a blob that has not been read yet. See `backlogUnread`.\n if (backlogUnread) return;\n try {\n if (pending.length === 0) {\n void storage.removeItem(key).catch(() => {});\n return;\n }\n const payload: PersistedItem[] = pending.map((item) => ({ id: item.id, event: item.event }));\n void storage.setItem(key, JSON.stringify(payload)).catch(() => {});\n } catch {\n // Best-effort: a failed write just means the backlog is not durable this launch.\n }\n };\n\n /**\n * The two lifecycle events are the funnel's DENOMINATOR: the server computes `min_sessions` by\n * counting distinct `app.session_started` grouped by `device_key`, and `app.first_open` is the top\n * of the activation funnel. Losing one is not \"an event fewer\", it is an app-open that never\n * happened as far as every rule reading them is concerned.\n */\n const isCountedOpenEvent = (event: ClientEvent): boolean =>\n typeof event.question_key === \"string\" && event.question_key.startsWith(\"app.\");\n\n const enforceSizeCap = (): void => {\n let overflow = pending.length - maxSize;\n if (overflow <= 0) return;\n // Drop the OLDEST first so the newest events are never the ones lost under pressure — but spend\n // ORDINARY events first, because at an app-open the lifecycle pair IS the oldest thing in the\n // buffer. A host on the documented shared-sink wiring that goes offline for a long session\n // pushes past the cap on screen views alone, and pure drop-oldest evicted `app.first_open` +\n // `app.session_started` before anything else — deleting the open from the funnel while 200\n // screen views survived.\n const kept: QueuedItem[] = [];\n for (const item of pending) {\n if (overflow > 0 && !isCountedOpenEvent(item.event)) {\n overflow--;\n continue;\n }\n kept.push(item);\n }\n // Only when the buffer is NOTHING BUT counted events does the cap fall on them, oldest first.\n // The cap is a hard ceiling: protecting a class must never turn it into an unbounded buffer.\n if (overflow > 0) kept.splice(0, overflow);\n pending = kept;\n };\n\n const safeSig = (event: ClientEvent): string => {\n try {\n return JSON.stringify(event);\n } catch {\n // Non-serializable event → give it a unique signature so it is never wrongly de-duped.\n return `__nosig_${nextId}_${Math.random()}`;\n }\n };\n\n /**\n * Merge a loaded backlog into the in-memory buffer: persisted (older) events go AHEAD of\n * whatever was enqueued while the read was in flight, duplicates collapse, and the size cap\n * applies as usual.\n *\n * Ids are minted FRESH from the running `nextId` rather than reset to 0. A drain may already be\n * in flight holding a batch of ids it will filter out on ack, and re-numbering from 0 would make\n * those ids point at different events — the ack would then dequeue (silently drop) whichever\n * events happened to inherit them. Monotonic ids are never reused, so an in-flight ack stays\n * correct whatever lands in between.\n */\n const mergePersisted = (persistedItems: PersistedItem[]): void => {\n if (persistedItems.length === 0) return;\n const seen = new Set(pending.map((item) => item.sig));\n const restored: QueuedItem[] = [];\n for (const persisted of persistedItems) {\n const sig = safeSig(persisted.event);\n if (seen.has(sig)) continue; // already in memory — collapse the duplicate\n seen.add(sig);\n restored.push({ id: nextId++, event: persisted.event, sig });\n }\n if (restored.length === 0) return;\n pending = [...restored, ...pending];\n enforceSizeCap();\n persist();\n };\n\n // Load any persisted backlog. Anything enqueued before this settles stays in memory; the merge\n // above puts persisted (older) events ahead of it.\n //\n // A read that blows READ_TIMEOUT_MS is NOT treated as \"no backlog\". The race only unblocks the\n // DRAIN — the original read is kept and merged whenever it lands, and until then every write is\n // suppressed so the unread blob survives intact.\n const loadPromise: Promise<void> = (async () => {\n if (!storage) return;\n try {\n const read = Promise.resolve(storage.getItem(key));\n // A rejecting read must not surface as an unhandled rejection when the race is won by the\n // timeout; the recovery path below re-attaches its own handlers.\n read.catch(() => {});\n const raced = await withTimeout(read, READ_TIMEOUT_MS);\n if (raced === READ_TIMED_OUT) {\n backlogUnread = true;\n warnInDev(\n `[wireai] the persisted analytics backlog at \"${key}\" took longer than ${READ_TIMEOUT_MS}ms ` +\n \"to read, so the queue started without it. The stored events are NOT discarded: writes \" +\n \"are held back until the read lands, and the backlog is merged in then. If you see this \" +\n \"on every cold start, your storage adapter is too slow to be on the launch path.\",\n );\n void read\n .then((late) => {\n backlogUnread = false;\n mergePersisted(parsePersisted(late));\n // Send whatever was just recovered; without this it would wait for the next enqueue.\n flush();\n })\n .catch(() => {\n // The slow read ultimately failed → nothing to preserve, resume normal persistence.\n backlogUnread = false;\n persist();\n });\n return;\n }\n backlogUnread = false;\n mergePersisted(parsePersisted(raced));\n // Persist explicitly: writes were suppressed for the whole read window, and `mergePersisted`\n // returns early (without persisting) when the backlog was empty — so without this an event\n // enqueued during the window would sit in memory undurable until the next enqueue.\n persist();\n } catch {\n // Unreadable backlog → start empty; nothing enqueued in-memory is lost. Writes must resume,\n // or the suppression that protected the unread blob would outlive the read for the whole\n // process and nothing would ever persist again.\n backlogUnread = false;\n persist();\n }\n })();\n\n // The queue's OWN awaited POST. Reads `res.ok` to drive retry/dequeue. NEVER throws — a missing\n // fetch, a rejecting network, or a JSON error resolves to `false` (batch stays, retry schedules).\n const postBatch = async (events: ClientEvent[]): Promise<boolean> => {\n // Build the /v1/events request through the ONE canonical builder (url + headers + body) so this\n // queue never re-describes the endpoint. The abort-timeout stays: the queue owns retry/dequeue,\n // so a hung request must be cut loose to schedule a backoff rather than block the drain forever.\n const req = buildEventsRequest(target, events);\n if (!req) return false;\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : undefined;\n const timer = setTimeout(() => controller?.abort(), 15_000);\n try {\n const res = await fetch(req.url, { ...req.init, signal: controller?.signal });\n // A 200 can still carry `skipped:N` — events the server threw away. Log-only: the ack below\n // stays `res.ok`, so retry/dequeue behaviour is unchanged.\n warnOnSkippedEvents(res);\n return !!(res && (res as { ok?: boolean }).ok);\n } catch {\n return false;\n } finally {\n clearTimeout(timer);\n }\n };\n\n const clearRetry = (): void => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer);\n retryTimer = undefined;\n }\n };\n\n const scheduleRetry = (): void => {\n // Bounded automatic retry. Past the cap the backlog simply waits for the next\n // `notifyOnline()` / `flush()` (both re-arm attempt), so events are paused, never dropped.\n if (attempt >= maxRetries) return;\n const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);\n attempt++;\n clearRetry();\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n void drain();\n }, delay);\n unrefTimer(retryTimer);\n };\n\n const drain = async (): Promise<void> => {\n if (disposed) return;\n try {\n await loadPromise;\n } catch {\n // load already swallows; guard the await defensively.\n }\n // Re-checked AFTER the await: the owner can go away while the cold-start read is in flight, and\n // a drain that resumes then would send and dequeue under a key another queue now owns.\n if (disposed) return;\n if (flushing) return;\n flushing = true;\n try {\n while (pending.length > 0) {\n const batch = pending.slice(0, batchSize);\n const ok = await postBatch(batch.map((item) => item.event));\n if (!ok) {\n scheduleRetry();\n return;\n }\n // Dequeue exactly the acked batch by id (pending may have grown while in flight).\n const acked = new Set(batch.map((item) => item.id));\n pending = pending.filter((item) => !acked.has(item.id));\n persist();\n attempt = 0;\n clearRetry();\n }\n } finally {\n flushing = false;\n }\n };\n\n const flush = (): void => {\n try {\n void drain();\n } catch {\n // drain never throws synchronously, but guard the kick anyway.\n }\n };\n\n const enqueue = (event: ClientEvent): void => {\n // A disposed queue is inert: it cannot persist (the slot belongs to its replacement) and must\n // not send, so buffering here would only pretend to accept the event.\n if (disposed) return;\n try {\n const stamped = stamp(event);\n const sig = safeSig(stamped);\n // Collapse a redundant re-enqueue of an identical pending event.\n for (const item of pending) {\n if (item.sig === sig) return;\n }\n pending.push({ id: nextId++, event: stamped, sig });\n enforceSizeCap();\n persist();\n // Only kick a drain when no retry is already pending — avoids hammering fetch while offline.\n if (retryTimer === undefined) flush();\n } catch {\n // Fire-and-forget: nothing in enqueue may surface to the UI.\n }\n };\n\n const notifyOnline = (): void => {\n // Host reconnected: reset the backoff and drain now.\n attempt = 0;\n clearRetry();\n flush();\n };\n\n const size = (): number => pending.length;\n\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n // The retry is the dangerous half: `unrefTimer` is a Node-only affordance (Hermes has no\n // `unref`), so on a device the backoff timer of an unmounted queue is fully alive, up to six\n // attempts and a 30s ceiling. Left running it wakes up, drains, empties, and `removeItem`s a\n // slot the live queue is using.\n clearRetry();\n // Only claimed when there IS storage (see the claim above), so only released then.\n if (storage) releaseQueueKey(key);\n };\n\n return { enqueue, flush, notifyOnline, size, dispose };\n};\n","/**\n * User identity — bind an onboarding session to the HOST's own user id so the funnel can\n * be reconciled to real users later (console sessions ↔ your user table / GA4 users).\n *\n * The id is an OPAQUE PSEUDONYMOUS STRING the host owns — its internal user id, NOT an\n * email/name/phone. Same host-injection philosophy as `storage` and `userContext`: the kit\n * mints nothing and reads nothing; the host passes what it wants. Dependency-free.\n *\n * Three binding moments (see WireOnboarding + the README \"User identity\" section):\n * 1. MOUNT — pass `userId` and it rides the A2A session-start metadata; the server\n * binds it when it creates the session.\n * 2. MID-SESSION— the user registers DURING onboarding: change the `userId` prop and the\n * kit emits an `identify` client event that attaches the id to the live\n * session (late binding — the key scenario).\n * 3. POST-FLOW — the user registers AFTER onboarding: call `identifyOnboarding(...)` with\n * the same host storage (it recovers the persisted contextId, before completion\n * clears it) or, more reliably, the `contextId` you captured from the\n * `started`/`resumed` `onEvent` (both events carry a `contextId` field).\n *\n * ⚠️ NO PII. Pass an opaque id (or a hash), never a raw email/name/phone. The id is capped at\n * {@link USER_ID_MAX_LENGTH} chars (longer ids are truncated, not rejected).\n */\nimport { getCurrentSessionId } from \"../analytics/currentSession\";\nimport { reportClientEvent } from \"../analytics/reportClientEvent\";\nimport {\n peekPersistedSession,\n sessionStorageKey,\n type WireOnboardingStorage,\n} from \"../session/persistedSession\";\n\n/** Max accepted user-id length. Longer strings are truncated (never rejected). Keep in sync\n * with the server's `USER_ID_MAX_LENGTH` (analytics/events.py). */\nexport const USER_ID_MAX_LENGTH = 128;\n\n/**\n * Normalize a host-supplied user id: trim, drop empty, and cap at {@link USER_ID_MAX_LENGTH}.\n * Returns `undefined` for a missing/blank/non-string value so callers can `if (id)`-gate.\n * PII is a host concern — this only bounds length, it does not (and cannot) detect an email.\n */\nexport const sanitizeUserId = (raw: unknown): string | undefined => {\n if (typeof raw !== \"string\") return undefined;\n const trimmed = raw.trim();\n if (!trimmed) return undefined;\n return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;\n};\n\n/**\n * A permissive email-SHAPE test (`local@domain.tld`) — NOT an RFC validator. Its ONE job is to\n * catch the common integration mistake of binding a RAW EMAIL as the opaque `user_id`: that leaks\n * PII into the top-level id (which the server treats as an opaque key and may surface), when the\n * email belongs in the opt-in `user_context.user_email` field instead. `identify()` uses this to\n * refuse an email-shaped id (with a dev warning) unless the host opts in explicitly. Trims first.\n */\nexport const looksLikeEmail = (value: unknown): boolean =>\n typeof value === \"string\" && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n\n/** Options for {@link identifyOnboarding}. */\nexport type IdentifyOnboardingOptions = {\n /** Tenant transport, same shape as `WireOnboardingConfig` (only these two fields are used). */\n config: { serverUrl: string; apiKey: string };\n /** The host's opaque user id to bind. Trimmed + capped; NO PII. */\n userId: string;\n /**\n * The onboarding session id to bind to (the A2A contextId) — the `contextId` field carried on\n * the `started`/`resumed` `onEvent`. Pass this when you captured it there. Required after the\n * flow COMPLETED, since completion clears the persisted session. Wins over the storage lookup.\n */\n contextId?: string;\n /**\n * The SAME host storage you passed to `<WireOnboarding storage={…} />`. When `contextId` is\n * omitted, the helper reads the persisted contextId from it (works while the session is still\n * persisted — i.e. dropped or mid-flow, before completion clears it).\n */\n storage?: WireOnboardingStorage;\n /** App id, to derive the default storage key `wireai:session:<appId>` when reading from storage. */\n appId?: string;\n /** Storage key override — pass the same `persistKey` you gave `<WireOnboarding>`, if any. */\n persistKey?: string;\n /**\n * OPT-IN LAST RESORT, default `false`. When no ONBOARDING session can be resolved (no `contextId`,\n * nothing in `storage`), bind the user to the LIVE PER-OPEN app session instead and return\n * `\"app_session\"`.\n *\n * ⚠️ These are two different id spaces sharing one wire field. An onboarding session id is the A2A\n * `contextId`; a per-open id is what `app.session_started` registers. The server's onboarding funnel\n * groups by `session_id`, so a per-open id posted here does not attach the user to their onboarding\n * — it writes a row nothing in that funnel can join. Until 0.13.0 this happened SILENTLY and\n * returned `true`, in exactly the documented post-completion case (completion clears the persisted\n * session), so the funnel stayed unattributed while the host was told it had worked.\n *\n * Turn it on only if binding the id to *some* session the server saw is genuinely worth more to you\n * than knowing the onboarding bind failed — and read the return value, which now says which it was.\n */\n allowAppSessionFallback?: boolean;\n};\n\n/**\n * What {@link identifyOnboarding} bound, and to WHICH id space — because `true` could not say.\n *\n * • `\"onboarding\"` — bound to the A2A `contextId`. This is the one that attributes the funnel.\n * • `\"app_session\"` — bound to the live per-open app session, via `allowAppSessionFallback`. The\n * server saw that session, but it is not this user's onboarding.\n * • `false` — nothing was dispatched (no user id, no server url, no resolvable session).\n *\n * ⚠️ 0.13.0 widened this from `boolean`. `\"onboarding\"` is truthy, so an `if (await identify…)` still\n * behaves identically; only an explicit `: boolean` annotation needs updating.\n */\nexport type IdentifyOnboardingBinding = \"onboarding\" | \"app_session\" | false;\n\n/**\n * Attach a host user id to an onboarding session AFTER the fact (post-registration), by sending\n * an `identify` client event to `/v1/events`. Resolves the contextId from an explicit\n * `contextId` or, failing that, from the persisted session in the host `storage`.\n *\n * Fire-and-forget under the hood (never throws, never blocks onboarding). Resolves to the\n * {@link IdentifyOnboardingBinding} that says WHICH id space was bound, or `false` when nothing could\n * be (no user id, no server url, or no resolvable session).\n */\nexport const identifyOnboarding = async (\n opts: IdentifyOnboardingOptions,\n): Promise<IdentifyOnboardingBinding> => {\n const userId = sanitizeUserId(opts.userId);\n if (!userId || !opts.config?.serverUrl) return false;\n\n let contextId = opts.contextId?.trim() || undefined;\n if (!contextId && opts.storage) {\n const key = opts.persistKey ?? (opts.appId ? sessionStorageKey(opts.appId) : undefined);\n if (key) {\n const stored = await peekPersistedSession(opts.storage, key);\n contextId = stored?.id;\n }\n }\n let space: Exclude<IdentifyOnboardingBinding, false> = \"onboarding\";\n // The OPT-IN last resort. Until 0.13.0 this ran unconditionally: with no captured contextId\n // it posted the LIVE PER-OPEN session id in the `session_id` field — which on this endpoint means\n // the ONBOARDING session — and then returned `true`. Two disjoint id spaces share that field, so\n // the row it wrote could never join the onboarding funnel, and the host got a success signal for a\n // bind that had not happened. It is now off unless the caller asks, and when it does fire it says\n // so in the return value instead of impersonating an onboarding bind.\n if (!contextId && opts.allowAppSessionFallback) {\n contextId = getCurrentSessionId();\n space = \"app_session\";\n }\n if (!contextId) return false;\n\n reportClientEvent(\n { serverUrl: opts.config.serverUrl, apiKey: opts.config.apiKey },\n { event_type: \"identify\", session_id: contextId, user_id: userId },\n );\n return space;\n};\n","/**\n * userContext — the ONE extensible object a host passes once and the kit flows into every\n * analytics event's `user_context` (plus the top-level opaque `user_id`).\n *\n * WHY it exists: hosts already hand the kit fragments of \"who this user is\" — `config.appVersion`,\n * `useSessionStart({ deviceKey, userId })`, `<WireOnboarding userContext={…} />` — but there was no\n * single object that carries app version + device key + user id + (opt-in) email + arbitrary extras\n * together, with one precedence rule, into every event. `WireUserContext` is that object;\n * `resolveUserContext` is the pure merge that turns it into the wire shape.\n *\n * PRECEDENCE (the one rule): an explicit `WireUserContext` field WINS over the #42 auto-detected\n * `device`/`appVersion`. A missing field is OMITTED, never sent empty.\n *\n * WHERE EACH FIELD LANDS (deliberate separation so nothing leaks across buckets):\n * • `userId` → the event's TOP-LEVEL opaque `user_id` (via `sanitizeUserId`). NEVER the bucket.\n * • `userEmail` → its OWN key `user_context.user_email`. NEVER merged into `userId`. OPT-IN PII.\n * • `deviceKey` → `user_context.device_key` (the server's `_event_device_key` reads it there).\n * • `appVersion`→ `user_context.app_version` (and returned as `appVersion` for `device.appVersion`).\n * • `extra` → NAMESPACED under a `custom.` key prefix, coerced to scalars, so a host extra can\n * never collide with a reserved `user_context` key.\n *\n * DEPENDENCY-FREE: the only import is the kit's own `sanitizeUserId`. The optional email hash is a\n * dependency-free FNV-1a fold (see {@link hashEmailFnv1a}) — no crypto library, no async.\n */\nimport { sanitizeUserId } from \"../identity/userIdentity\";\nimport type { WireOnboardingStorage } from \"../session/persistedSession\";\n\n/**\n * The single, extensible user-context object. A host passes it ONCE (at analytics init) and may\n * update it post-mount (e.g. attach `userId`/`userEmail` at login) via `setUserContext(partial)`.\n * Every field is optional; missing fields are omitted from the wire payload.\n */\nexport interface WireUserContext {\n /**\n * Host app version, e.g. \"1.4.2\". EXPLICIT — wins over the #42 auto-detected `device.appVersion`.\n * Lands in `user_context.app_version`. Omitted when neither this nor auto-detect yields a version.\n */\n appVersion?: string;\n /**\n * A stable, non-PII device id the host owns. Lands in `user_context.device_key` (NOT `session_id`),\n * where the server groups a device's sessions. Host-supplied; the kit never mints or reads one.\n */\n deviceKey?: string;\n /**\n * The host's OPAQUE PSEUDONYMOUS user id (their internal id — NOT an email/name/phone). Sanitized +\n * capped (see `sanitizeUserId`) and placed on the event's top-level `user_id`. NEVER the bucket.\n */\n userId?: string;\n /**\n * OPT-IN PII. The user's email, its OWN field (`user_context.user_email`) — NEVER merged into\n * `userId`. The kit NEVER auto-collects this; a host passes it only WITH the user's consent (EU\n * users: treat as personal data). For a non-reversible form, set {@link hashEmail} `true` (the kit\n * folds it with a dependency-free hash and stamps `user_context.user_email_hashed: true`), OR\n * pre-hash host-side with a cryptographic digest and pass that here with `hashEmail` falsy.\n */\n userEmail?: string;\n /**\n * When `true`, {@link userEmail} is folded with the kit's dependency-free {@link hashEmailFnv1a}\n * before it leaves the device, and `user_context.user_email_hashed` is set `true`. NOTE: FNV-1a is\n * a lightweight NON-cryptographic fold (obfuscation, not a secure digest). For a cryptographic\n * hash, compute it host-side (e.g. SHA-256 via `expo-crypto`) and pass the digest as `userEmail`\n * with `hashEmail` falsy. Default: raw email is sent as-is (opt-in already gated it upstream).\n */\n hashEmail?: boolean;\n /**\n * Arbitrary host context (signup method, referral, plan tier…). Each value is coerced to a scalar\n * (`string | number | boolean`; non-scalars and non-finite numbers are DROPPED) and NAMESPACED\n * under a `custom.` key prefix in `user_context` (e.g. `user_context[\"custom.referral\"]`) so it can\n * never collide with a reserved key. No raw PII — use {@link userEmail} for email.\n */\n extra?: Record<string, string | number | boolean>;\n}\n\n/**\n * The wire-shaped result of {@link resolveUserContext}. `userContext` is the non-PII/opt-in-PII\n * bucket stamped onto the event; `userId` is the top-level opaque id; `appVersion`/`deviceKey` are\n * echoed for callers that also place them elsewhere (e.g. `device.appVersion`). Absent fields are\n * omitted so a caller can spread this without sending empties.\n */\nexport interface ResolvedUserContext {\n /** The opaque, sanitized user id → the event's top-level `user_id`. Omitted when unset/blank. */\n userId?: string;\n /** The stable device id → `user_context.device_key`. Omitted when unset. */\n deviceKey?: string;\n /** The effective app version (explicit > auto-detected) → `user_context.app_version`. */\n appVersion?: string;\n /** The `user_context` bucket (device_key, app_version, user_email[+ _hashed], custom.*). */\n userContext?: Record<string, string | number | boolean>;\n}\n\n/** The prefix applied to every host `extra` key so it can never collide with a reserved key. */\nexport const EXTRA_KEY_PREFIX = \"custom.\" as const;\n\n/** A finite scalar the wire accepts. Non-finite numbers (NaN/Infinity) are NOT scalars here. */\nexport const isWireScalar = (value: unknown): value is string | number | boolean => {\n const t = typeof value;\n if (t === \"string\" || t === \"boolean\") return true;\n if (t === \"number\") return Number.isFinite(value as number);\n return false;\n};\n\n/**\n * Fold an email to a stable, dependency-free 32-bit FNV-1a hex token (lowercased + trimmed first so\n * the same address always folds identically). This is OBFUSCATION, not a cryptographic digest — it\n * is not collision-resistant. For a real hash, pre-hash host-side and pass the digest as `userEmail`.\n */\nexport const hashEmailFnv1a = (email: string): string => {\n const normalized = email.trim().toLowerCase();\n let hash = 0x811c9dc5; // FNV offset basis (32-bit)\n for (let i = 0; i < normalized.length; i++) {\n hash ^= normalized.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193); // FNV prime (32-bit), kept in 32-bit via imul\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n};\n\n/** Trim a candidate string; return `undefined` for a non-string / blank so callers can `if`-gate.\n * Shared with `activation/wireActivation`, which carried a byte-identical private copy named `clean`.\n * Not re-exported from the package barrel — this is an internal helper, not public surface. */\nexport const cleanString = (value: unknown): string | undefined => {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n};\n\n/**\n * Coerce a host `extra` map into the namespaced, scalar-only bucket shape. Every kept value is\n * placed under `custom.<key>`; non-scalar values (objects, arrays, null, functions, NaN/Infinity)\n * are DROPPED. Returns an object (possibly empty).\n */\nexport const namespaceExtra = (\n extra: Record<string, unknown> | undefined,\n): Record<string, string | number | boolean> => {\n const out: Record<string, string | number | boolean> = {};\n if (!extra || typeof extra !== \"object\") return out;\n for (const [key, value] of Object.entries(extra)) {\n const cleanKey = cleanString(key);\n if (!cleanKey) continue;\n if (!isWireScalar(value)) continue; // drop anything that isn't a finite scalar\n out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;\n }\n return out;\n};\n\n/**\n * The storage key the analytics façade persists the bound opaque `user_id` under (namespaced per\n * `appId`, mirroring {@link deviceIdStorageKey}). Exported so a logout path can target it directly.\n */\nexport const analyticsUserIdStorageKey = (appId?: string): string =>\n `wireai:analytics:userId:${appId ?? \"default\"}`;\n\n/**\n * Return a COPY of a {@link WireUserContext} with every USER-scoped (PII / pseudonymous) field\n * removed — `userId`, `userEmail`, `hashEmail`, and `extra` — while KEEPING the non-PII device-scope\n * fields (`appVersion`, `deviceKey`). This is the in-memory half of logout: after it, the same\n * analytics instance keeps its stable `device_key` (which groups a DEVICE, not a user) but no longer\n * stamps the previous user's id/email onto events. Pure; never mutates the input.\n */\nexport const clearPiiFromContext = (ctx: WireUserContext = {}): WireUserContext => {\n const rest: WireUserContext = {};\n if (typeof ctx.appVersion === \"string\") rest.appVersion = ctx.appVersion;\n if (typeof ctx.deviceKey === \"string\") rest.deviceKey = ctx.deviceKey;\n return rest;\n};\n\n/** Options for {@link clearUserContext}. */\nexport interface ClearUserContextOptions {\n /** Host persistence (AsyncStorage subset) — the persisted bound `user_id` is removed from here. */\n storage?: WireOnboardingStorage;\n /** Tenant/app id — namespaces the persisted key (`wireai:analytics:userId:<appId>`). */\n appId?: string;\n}\n\n/**\n * LOGOUT primitive: purge the persisted, bound opaque `user_id` for an app so the NEXT user on a\n * shared device is not silently attributed to the previous one. Removes the\n * `wireai:analytics:userId:<appId>` key that the analytics façade persists and reuses across\n * launches. Fire-and-forget: a missing storage or a failing adapter resolves quietly.\n *\n * COVERAGE. The stateful `createAnalytics(...)` instance also exposes {@link Analytics.reset}, which\n * does this AND clears the in-memory binding + PII in one call — prefer it when you hold the\n * instance. This standalone helper covers the `createWireActivation` / `wire` path (whose config is\n * captured immutably, so it has no `reset`): call `clearUserContext({ storage, appId })` on logout,\n * and RECREATE the `wire` / analytics instance without the user's `userContext` (userId/userEmail)\n * so no further events carry the previous user's identity. The non-PII per-install `device_key`\n * (`wireai:analytics:deviceKey:<appId>`) is intentionally left in place — it groups a device, not a\n * person, and stays stable across users of the same install.\n */\nexport const clearUserContext = async (opts: ClearUserContextOptions = {}): Promise<void> => {\n const storage = opts.storage;\n if (!storage) return;\n try {\n await storage.removeItem(analyticsUserIdStorageKey(opts.appId));\n } catch {\n /* best-effort, swallow — logout must never throw into the UI */\n }\n};\n\n/**\n * The `userContext` value to hand `<WireOnboarding userContext={...} />` so an onboarding session\n * and the app's later events (purchases, actions, screens) share ONE join key.\n *\n * WHY it exists as a named function instead of an inline object literal: the wire key is\n * `device_key`, the prop-facing name is `deviceKey`, and the analytics surfaces auto-mint the value\n * for you. A host that hand-writes `userContext={{ deviceKey }}` produces a bucket the server's\n * device lookup does not read, and the resulting funnel is silently EMPTY rather than wrong. This is\n * the one place that spelling is decided.\n *\n * Pass the SAME `deviceKey` you gave `createAnalytics` / `createWireActivation`. `session_id` is not\n * a join key across those two families: an onboarding session id is the A2A `contextId` and an\n * app-event session id is the per-open id, so intersecting them returns nothing.\n */\nexport const activationJoinContext = (\n deviceKey: string,\n): Record<string, string | number | boolean> => resolveUserContext({ deviceKey }).userContext ?? {};\n\n/** Options for {@link resolveUserContext}. */\nexport interface ResolveUserContextOptions {\n /**\n * The kit's best-effort auto-detected app version (#42; from `detectAppVersion()`/the device\n * snapshot). Used ONLY when the explicit `WireUserContext.appVersion` is absent — explicit wins.\n */\n autoAppVersion?: string;\n}\n\n/**\n * Merge a {@link WireUserContext} into the wire shape with the precedence rule (explicit field >\n * auto-detected). Pure, never throws. Missing fields are omitted so the result can be spread onto an\n * event without sending empties.\n */\nexport const resolveUserContext = (\n ctx: WireUserContext = {},\n opts: ResolveUserContextOptions = {},\n): ResolvedUserContext => {\n const result: ResolvedUserContext = {};\n const bucket: Record<string, string | number | boolean> = {};\n\n // userId → top-level opaque id (NEVER the bucket). Sanitized + capped host-side.\n const userId = sanitizeUserId(ctx.userId);\n if (userId) result.userId = userId;\n\n // deviceKey → user_context.device_key (NOT session_id).\n const deviceKey = cleanString(ctx.deviceKey);\n if (deviceKey) {\n result.deviceKey = deviceKey;\n bucket.device_key = deviceKey;\n }\n\n // appVersion → explicit wins over auto-detected (#42); echoed for device.appVersion callers.\n const appVersion = cleanString(ctx.appVersion) ?? cleanString(opts.autoAppVersion);\n if (appVersion) {\n result.appVersion = appVersion;\n bucket.app_version = appVersion;\n }\n\n // userEmail → its OWN key. OPT-IN PII, optionally folded. NEVER touches userId.\n const email = cleanString(ctx.userEmail);\n if (email) {\n if (ctx.hashEmail) {\n bucket.user_email = hashEmailFnv1a(email);\n bucket.user_email_hashed = true;\n } else {\n bucket.user_email = email;\n }\n }\n\n // extra → namespaced + scalar-coerced.\n Object.assign(bucket, namespaceExtra(ctx.extra));\n\n if (Object.keys(bucket).length > 0) result.userContext = bucket;\n return result;\n};\n","/**\n * identityRecord — the ONE provenance-carrying shape for an id the kit puts on the wire.\n *\n * WHY IT EXISTS. `session_id` and `device_key` are bare `string`s minted independently by four\n * subsystems, and nothing anywhere recorded WHERE a given id came from. Every id-layer defect this\n * release fixes is a direct consequence of that one omission:\n *\n * • a rejecting storage adapter's in-memory id was indistinguishable from a persisted one, so the\n * kit injected a fresh per-launch join key on every launch — nothing carried `durable`.\n * • an app-OPEN session id could be posted into a field that means the ONBOARDING session, and the\n * caller was told `true` — nothing carried `space`.\n * • an auto-minted `wdev_*` could be injected beside a device id the host demonstrably owns on\n * another surface, silently — nothing carried `source`.\n *\n * WHAT THIS IS, AND WHAT IT DELIBERATELY IS NOT. It is a small record plus a process-wide registry of\n * the ids a HOST supplied. It is NOT a branded-type refactor (`OnboardingSessionId` / `AppSessionId` /\n * `DeviceKey` across every signature) — that is real value and it is deferred, because it touches\n * every file and is not what makes a number correct this week. Nothing here changes the wire.\n *\n * WHY A `Symbol.for` REGISTRY. Same reason as `analytics/currentSession` and `context/deviceId`: tsup\n * inlines a separate copy of a module into each bundle (`.` and `./analytics`), so a plain module\n * `let` would give every bundle its own registry and the cross-surface question this exists to answer\n * (\"did ANY surface in this process get a host-supplied device key?\") would read `no` from the wrong\n * copy. `Symbol.for` resolves to one slot on `globalThis` no matter how many copies exist.\n */\n\n/**\n * Which id space a value belongs to. These are NOT interchangeable, and the whole point of naming\n * them is that a value from one space must never be posted into a field that means another:\n * • `onboarding-session` — the A2A `contextId` for ONE onboarding run.\n * • `app-session` — the per-app-open session id (`app.session_started`).\n * • `device` — the per-install `device_key`; the only cross-family join key.\n */\nexport type IdentitySpace = \"onboarding-session\" | \"app-session\" | \"device\";\n\n/** Where the value came from: the host handed it over, or the kit minted it. */\nexport type IdentitySource = \"host\" | \"auto\";\n\n/** An id plus everything a consumer needs to decide whether it may use it. */\nexport type IdentityRecord = {\n /** The id itself, trimmed. Never empty (a blank input yields no record at all). */\n value: string;\n /** Which id space {@link value} belongs to. */\n space: IdentitySpace;\n /** `host` = the integrator supplied it; `auto` = the kit minted it. */\n source: IdentitySource;\n /**\n * Whether the value was actually PERSISTED (or adopted from persistence), as opposed to living\n * only in this process's memory. A non-durable auto id is a DIFFERENT id on the next launch, which\n * for a `device` value is worse than no value at all: the server counts `min_sessions` by distinct\n * opens grouped on `device_key`, so a per-launch key corrupts the counter rather than leaving it\n * empty. A host-supplied value is durable by definition — the host owns its lifetime.\n */\n durable: boolean;\n};\n\n/** Input to {@link resolveIdentity}. `value` is `unknown` so callers can pass a raw prop through. */\nexport type ResolveIdentityInput = {\n value: unknown;\n space: IdentitySpace;\n source: IdentitySource;\n /** Defaults to `true` for a host value (the host owns its lifetime) and `false` otherwise. */\n durable?: boolean;\n /** Tenant/app id — two tenants in one process never share a provenance entry. */\n scope?: string;\n};\n\n/**\n * Well-known key into the runtime-global symbol registry — one provenance registry per process.\n *\n * @globalSlot LATCH — the REGISTRY OBJECT is created once and its identity is then stable. A second\n * write drops every host id recorded so far, so `hostIdentity()` answers \"no host key in this\n * process\" for a process that demonstrably has one, and the kit injects its own `wdev_*` beside it\n * without warning. Its CONTENTS are live (`host` gains an entry on every host-sourced\n * `resolveIdentity`), so both accessors go through `provenanceRegistry()` on every call.\n */\nconst IDENTITY_PROVENANCE_SLOT: unique symbol = Symbol.for(\"@wireai/activation:identityProvenance\");\n\n/** `\"<space>:<scope>\"` → the HOST-supplied value seen for it. Auto values are never recorded. */\ntype ProvenanceRegistry = { host: Map<string, string> };\n\ntype GlobalWithProvenance = typeof globalThis & {\n [IDENTITY_PROVENANCE_SLOT]?: ProvenanceRegistry;\n};\n\nconst provenanceGlobal = globalThis as GlobalWithProvenance;\n\nconst provenanceRegistry = (): ProvenanceRegistry => {\n const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];\n if (existing) return existing;\n const created: ProvenanceRegistry = { host: new Map() };\n provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;\n return created;\n};\n\nconst provenanceKey = (space: IdentitySpace, scope?: string): string =>\n `${space}:${scope ?? \"default\"}`;\n\n/**\n * Build an {@link IdentityRecord} from a candidate value, or `undefined` when there is nothing usable\n * (a non-string, or blank after trimming) — so a caller can `if (record)`-gate instead of guessing\n * whether an empty string means \"none\" or \"not yet\".\n *\n * SIDE EFFECT, deliberate and the reason this is a function and not an object literal: a `host`-sourced\n * record is RECORDED on the process registry, so a later surface can ask {@link hostIdentity} whether\n * this process demonstrably owns a host id in that space. That is what turns \"the kit injected its own\n * key\" from a silent third id space into a warnable condition. Never throws.\n */\nexport const resolveIdentity = (input: ResolveIdentityInput): IdentityRecord | undefined => {\n if (typeof input.value !== \"string\") return undefined;\n const value = input.value.trim();\n if (!value) return undefined;\n const durable = input.durable ?? input.source === \"host\";\n if (input.source === \"host\") {\n provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);\n }\n return { value, space: input.space, source: input.source, durable };\n};\n\n/**\n * The HOST-supplied id this process has seen for a space, or `undefined` when every surface so far\n * let the kit mint its own. Answers the cross-surface question no single mount can answer alone:\n * \"does this app own a device id that this particular mount was not given?\"\n */\nexport const hostIdentity = (space: IdentitySpace, scope?: string): string | undefined =>\n provenanceRegistry().host.get(provenanceKey(space, scope));\n\n/** Test-only: forget every recorded host identity so a unit test starts from a clean registry. */\nexport const resetIdentityProvenance = (): void => {\n provenanceRegistry().host.clear();\n};\n","/**\n * deviceId — mint a stable, NON-PII, per-install device id the kit owns when the host supplies\n * none. This is the headline of \"device fully automatic\": the analytics façade auto-mints ONE id,\n * persists it via the host's `storage` abstraction, and reuses it on every subsequent open — so\n * `user_context.device_key` is ALWAYS present and the server's review/questionnaire gating +\n * A/B stickiness (both key on `device_key`) work out of the box, with zero host wiring.\n *\n * WHY it is NOT PII and adds NO dependency (the kit's hard rules):\n * The id is a random token generated from `Date.now()` + `Math.random()` — it carries NO hardware\n * identifier, NO IDFA/GAID, NO fingerprint. It is a first-party per-install correlation key, the\n * same privacy category as a first-party cookie: it groups a single install's sessions and cannot\n * identify a person or be joined across apps. There is NO `uuid` (or any) dependency — a\n * time+random scheme is sufficient because the id is minted ONCE and then persisted, so global\n * uniqueness across the fleet is not required (a per-install collision is astronomically unlikely\n * and inconsequential — worst case two installs share a bucket).\n *\n * A host that wants its OWN device id still wins: pass `WireUserContext.deviceKey` and the kit uses\n * that verbatim and never mints/persists an auto id.\n */\n\nimport { resolveIdentity, type IdentityRecord } from \"../identity/identityRecord\";\n\n/** Prefix so an auto-minted id is visibly the kit's (distinguishable from a host-supplied `deviceKey`). */\nexport const AUTO_DEVICE_ID_PREFIX = \"wdev_\";\n\n/** The storage key the façade persists the auto-minted id under (namespaced per `appId`). */\nexport const deviceIdStorageKey = (appId?: string): string =>\n `wireai:analytics:deviceKey:${appId ?? \"default\"}`;\n\n/** One 32-bit base-36 chunk of randomness. Two chunks are concatenated for a wider token. */\nconst randomChunk = (): string =>\n Math.floor(Math.random() * 0x100000000)\n .toString(36)\n .padStart(6, \"0\");\n\n/**\n * Mint a fresh per-install device id. Dependency-free (`Date.now()` + `Math.random()`), never\n * throws, and returns a NEW value on every call — the façade mints ONCE and persists, so this is\n * called at most once per install (then the persisted value is reused). Two random chunks plus the\n * timestamp keep the token wide enough that a per-install collision is not a practical concern.\n */\nexport const mintDeviceId = (): string => {\n const time = Date.now().toString(36);\n return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;\n};\n\n// ── The ONE auto device key per install ──────────────────────────────────────────────────────\n//\n// WHY A REGISTRY AND NOT A `let` PER FACTORY: `createAnalytics` and `createWireActivation` each\n// used to mint their OWN id synchronously and then race a storage read to overwrite it. A host that\n// creates BOTH (the documented wiring: a façade for `track`/`screen`, an activation instance for the\n// gate-firing `wire.track`) therefore had TWO auto ids for ONE install. Every event carried whichever\n// id its own surface minted, so the `device_key` the server groups a device's sessions under — the\n// key `min_sessions`, A/B arm stickiness, and the purchase↔onboarding join all read — SPLIT in two.\n// On a first run both also wrote their own id to the same storage slot, so which one survived was a\n// coin flip. Same failure class as joining two event families on disjoint id spaces: no error, just\n// halved counts and a join that misses.\n//\n// The fix is the pattern this repo already uses for `currentSession` and `activation revalidation`:\n// ONE value in a `globalThis` slot keyed by `Symbol.for(...)`, so every inlined copy of this module\n// (tsup duplicates modules across the `.` / `./analytics` bundles) addresses the SAME registry.\n// Keyed by `appId` so two tenants in one process never share an id.\n//\n// RESIDUAL WINDOW: the storage read is async, so an event emitted in the milliseconds before\n// hydration completes carries the freshly minted id rather than the persisted one. The registry makes\n// every surface agree on WHICH id that is; it does not make the read sync. A caller that can afford to\n// wait (`useLifecycleEvents` / `useSessionStart`, both app-open paths — see `hydrateDeviceIdentity`)\n// must await instead: their events are the ONLY ones the server counts `min_sessions` from, so a\n// per-launch id there is not a millisecond of noise, it is a counter that can never exceed 1. Waiting\n// is only half of it — the settled outcome can still be NON-DURABLE, and those callers refuse it.\n\n/**\n * Well-known key into the runtime-global symbol registry — one auto-id registry across every bundle.\n *\n * @globalSlot LATCH — the REGISTRY OBJECT is created once and its identity is then stable. A second\n * write empties `keys`/`hydrating`/`pending`, so the next surface mints a SECOND auto id for one\n * install and the `device_key` the server joins sessions on splits in two — the halved-counter\n * defect this registry exists to close. Its CONTENTS are live (the id per `appId` is replaced when\n * an async hydration adopts a persisted value), so callers must re-read through\n * `resolveAutoDeviceKey()` rather than hold the string a mount happened to see first.\n */\nconst AUTO_DEVICE_KEY_SLOT: unique symbol = Symbol.for(\"@wireai/activation:autoDeviceKeys\");\n\n/** What a hydration settled on: the id, and whether persistence actually CONFIRMED it.\n *\n * `durable: false` means the id lives only in this process's memory — the adapter rejected, threw,\n * or there was no adapter at all. That distinction is the whole point here: a string is a string, so\n * before 0.13.0 a caller could not tell a persisted id from a per-launch mint, and the auto-join\n * gate (`Boolean(storage)`) was reading the PRESENCE of the prop rather than the SUCCESS of the\n * write. See {@link hydrateDeviceIdentity}. */\ntype HydrationOutcome = { value: string; durable: boolean };\n\n/** The shared registry: the live id per `appId`, the set of appIds whose hydration already started,\n * and the in-flight (or settled) hydration promise per `appId` so a waiter can join it. */\ntype AutoDeviceKeyRegistry = {\n keys: Map<string, string>;\n hydrating: Set<string>;\n pending?: Map<string, Promise<HydrationOutcome>>;\n};\n\ntype GlobalWithDeviceKeys = typeof globalThis & {\n [AUTO_DEVICE_KEY_SLOT]?: AutoDeviceKeyRegistry;\n};\n\nconst deviceKeyGlobal = globalThis as GlobalWithDeviceKeys;\n\nconst autoDeviceKeyRegistry = (): AutoDeviceKeyRegistry => {\n const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];\n if (existing) return existing;\n const created: AutoDeviceKeyRegistry = { keys: new Map(), hydrating: new Set() };\n deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;\n return created;\n};\n\n/** The persistence subset {@link resolveAutoDeviceKey} needs (a strict subset of `WireOnboardingStorage`). */\nexport type DeviceKeyStorage = {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n};\n\n/** Options for {@link resolveAutoDeviceKey}. Omitting `storage` gives a PROCESS-scoped id, not a\n * per-install one — see the caller notes: a caller with no persistence must decide whether a\n * per-launch id is better or worse than no id for its metric. */\nexport interface ResolveAutoDeviceKeyOptions {\n /** Tenant/app id — namespaces both the registry entry and the storage slot. */\n appId?: string;\n /** Host persistence. Present → the id survives launches. Absent → process-scoped only. */\n storage?: DeviceKeyStorage;\n}\n\n/**\n * Start (or join) the SINGLE-FLIGHT storage read for `appId` and resolve to the {@link HydrationOutcome}\n * it settles on. The promise is parked on the registry so a later `hydrateAutoDeviceKey` awaits the\n * SAME read instead of starting a second one. Never rejects: any storage failure resolves to the live\n * id with `durable: false`.\n *\n * TWO OUTCOMES, NOT ONE STRING:\n * • ADOPTED (`durable: true`) — a persisted id was read back, or the freshly minted one was\n * written successfully. The next launch will see the same id.\n * • DEGRADED (`durable: false`) — the adapter rejected, threw, or returned nothing and then failed\n * the write. The id is real but PROCESS-scoped, so anything that\n * counts a device across launches must refuse it.\n *\n * A DEGRADED outcome also drops the registry latches so the NEXT caller starts a fresh read. A\n * cold-boot storage lock is transient; caching it as a verdict for the process lifetime turned a\n * one-second problem into a whole-launch one, and nothing ever retried.\n */\nconst startHydration = (\n registry: AutoDeviceKeyRegistry,\n appId: string,\n storage: DeviceKeyStorage,\n minted: string,\n): Promise<HydrationOutcome> => {\n if (!registry.pending) registry.pending = new Map();\n const existing = registry.pending.get(appId);\n if (existing) return existing;\n\n const slot = deviceIdStorageKey(appId);\n const degraded = (): HydrationOutcome => ({ value: registry.keys.get(appId) ?? minted, durable: false });\n const adopted = (value: string): HydrationOutcome => ({ value, durable: true });\n let run: Promise<HydrationOutcome>;\n try {\n run = Promise.resolve(storage.getItem(slot))\n .then((saved) => {\n const persisted = typeof saved === \"string\" && saved.trim() ? saved.trim() : undefined;\n if (persisted) {\n registry.keys.set(appId, persisted);\n return adopted(persisted);\n }\n // First run on this install: persist the id we just minted so the next launch adopts it.\n // ONLY a resolved write earns `durable` — a rejected one leaves the id in memory alone.\n return Promise.resolve(storage.setItem(slot, minted)).then(\n () => adopted(registry.keys.get(appId) ?? minted),\n degraded,\n );\n })\n .catch(degraded);\n } catch {\n // A storage adapter that throws synchronously — degrade to the in-memory id.\n run = Promise.resolve(degraded());\n }\n registry.pending.set(appId, run);\n // Retry-on-failure: release the latches once a degraded outcome settles, so a later caller\n // is not permanently bound to one bad read. Registered AFTER the `set` above, so the clean-up can\n // never race ahead of the entry it is clearing.\n void run.then((outcome) => {\n if (outcome.durable) return;\n registry.pending?.delete(appId);\n registry.hydrating.delete(appId);\n });\n return run;\n};\n\n/**\n * The ONE auto-minted `device_key` for an install, shared by every kit surface.\n *\n * SYNCHRONOUS by contract (a fire-and-forget event path cannot await): returns the current live id\n * immediately, minting one on first call. When `storage` is supplied it also kicks off a SINGLE\n * hydration per `appId` that adopts the persisted id (or persists the freshly minted one). Callers\n * should call this per EVENT rather than caching the return value, so an event built after hydration\n * carries the persisted id.\n *\n * A host-supplied `deviceKey` always wins — callers must short-circuit before reaching this.\n * Never throws: a missing, hung, or rejecting storage adapter degrades to the in-memory id.\n */\nexport const resolveAutoDeviceKey = (opts: ResolveAutoDeviceKeyOptions = {}): string => {\n const registry = autoDeviceKeyRegistry();\n const appId = opts.appId ?? \"default\";\n\n let id = registry.keys.get(appId);\n if (!id) {\n id = mintDeviceId();\n registry.keys.set(appId, id);\n }\n\n const storage = opts.storage;\n // Single-flight: the FIRST caller with storage owns hydration for this appId; later callers just\n // read whatever the registry currently holds.\n if (storage && !registry.hydrating.has(appId)) {\n registry.hydrating.add(appId);\n void startHydration(registry, appId, storage, id);\n }\n\n return registry.keys.get(appId) ?? id;\n};\n\n/**\n * The AWAITABLE sibling of {@link resolveAutoDeviceKey}: resolve to the auto `device_key` AFTER the\n * persisted id has been read back (or written, on a first run), so the caller stamps the id this\n * install will keep rather than the one that was minted a millisecond ago.\n *\n * WHY IT EXISTS: `resolveAutoDeviceKey` is synchronous by contract, so a caller firing at mount got\n * the freshly minted id and the persisted one landed milliseconds later. For most events that is\n * noise. For `app.session_started` it is the whole metric: the server computes `min_sessions` by\n * counting distinct opens grouped by `device_key`, so a per-launch key there makes the counter\n * structurally incapable of exceeding 1, and it splits `first_open` off from every event that\n * follows it. Only a caller that can afford one storage read should use this; the fire-and-forget\n * event paths must stay on the sync function.\n *\n * Never throws or rejects: a missing, hung, or rejecting adapter resolves to the in-memory id, and\n * with no `storage` it resolves immediately (there is nothing to hydrate from).\n *\n * ⚠️ IT RETURNS A BARE STRING, so it CANNOT say whether the id survives the launch — a degraded\n * adapter resolves to the in-memory mint and reads identically to a persisted one. No kit surface\n * uses it any more (0.14.0 moved the two lifecycle hooks off it): a caller writing a cross-launch\n * join key wants {@link hydrateDeviceIdentity} and its `durable` flag. Kept as public API for a host\n * that only wants \"the id\", never as the way to decide whether to stamp one.\n */\nexport const hydrateAutoDeviceKey = async (\n opts: ResolveAutoDeviceKeyOptions = {},\n): Promise<string> => (await hydrateDeviceIdentity(opts))?.value ?? resolveAutoDeviceKey(opts);\n\n/**\n * The PROVENANCE-CARRYING sibling of {@link hydrateAutoDeviceKey}: the same awaited read, but it\n * answers \"is this id one this install will KEEP?\" instead of only \"what is the id?\".\n *\n * WHY IT EXISTS. `<WireOnboarding>` gated auto-injection on `Boolean(storage)` — the presence of\n * the prop — because a string carries no provenance and there was nothing better to gate on. A\n * REJECTING adapter therefore injected a fresh `wdev_*` on every launch: strictly worse than\n * injecting nothing, since the server counts `min_sessions` by distinct opens grouped on `device_key`,\n * so a per-launch key corrupts that counter AND inflates distinct-device counts. Callers that write a\n * key onto the wire as a cross-launch join must read `durable` and refuse a `false`.\n *\n * Resolves `undefined` only when there is no usable id at all. With no `storage` it resolves\n * immediately with `durable: false` — a process-scoped id is exactly what \"no persistence\" means.\n * Never throws or rejects.\n */\nexport const hydrateDeviceIdentity = async (\n opts: ResolveAutoDeviceKeyOptions = {},\n): Promise<IdentityRecord | undefined> => {\n // Mint + register synchronously first, so a waiter and a concurrent sync caller share ONE id.\n const id = resolveAutoDeviceKey(opts);\n const appId = opts.appId ?? \"default\";\n const record = (value: string, durable: boolean): IdentityRecord | undefined =>\n resolveIdentity({ value, space: \"device\", source: \"auto\", durable, scope: appId });\n\n if (!opts.storage) return record(id, false);\n const registry = autoDeviceKeyRegistry();\n const pending = registry.pending?.get(appId);\n // No pending entry means a previous hydration already settled DEGRADED and released its latches\n // (see `startHydration`), so the live id is the in-memory one — real, but not durable.\n if (!pending) return record(registry.keys.get(appId) ?? id, false);\n const outcome = await pending;\n return record(outcome.value, outcome.durable);\n};\n\n/** Test-only: forget every auto id + hydration flag so a unit test starts from a clean registry. */\nexport const resetAutoDeviceKeys = (): void => {\n const registry = autoDeviceKeyRegistry();\n registry.keys.clear();\n registry.hydrating.clear();\n registry.pending?.clear();\n};\n","/**\n * analyticsFacade — a Segment/PostHog-shaped developer API (`track` / `screen` / `identify`)\n * over the kit's OWN offline-first event queue. One package, one key, one line:\n *\n * const analytics = createAnalytics({ serverUrl, apiKey, storage });\n * analytics.track(\"content_share\", { source: \"feed\" });\n * analytics.screen(\"Home\", { tab: \"explore\" });\n * analytics.identify(\"u_123\", { plan: \"pro\" });\n *\n * WHY a façade: the primitives already exist (`createEventQueue` for durable offline-first\n * transport, `buildContextEnvelope` for the non-PII device context, the `identify` client-event\n * contract for user binding), but a host had to wire them together by hand. This composes them\n * into the familiar analytics-SDK surface so a consumer gets the ergonomics with NO second\n * install and NO second key — the same `{ serverUrl, apiKey }` creds as onboarding.\n *\n * ROUTING: every call builds a `ClientEvent` and `enqueue`s it. The queue stamps the context\n * envelope (device + host scalars), persists offline, batches, retries, and dequeues on ack —\n * so `track`/`screen`/`identify` inherit offline-first durability for free. `screen` routes\n * through the SAME queue rather than the direct `reportAppEvent` POST (a deliberate upgrade:\n * screen views survive being offline too).\n *\n * DEPENDENCY-FREE + TREE-SHAKEABLE: this module imports ONLY the queue, the envelope builder,\n * the client-event types + session-id seed, and the identity sanitizer. It reaches NO\n * onboarding / showcase / review UI, so an analytics-only consumer bundles none of it. React is\n * absent here on purpose — the optional React glue is the thin `useAnalytics` hook.\n *\n * FIRE-AND-FORGET: no method throws into the UI or blocks — the queue already guarantees that.\n */\nimport { buildContextEnvelope, type ContextEnvelope } from \"./contextEnvelope\";\nimport { ensureCurrentSessionId } from \"./currentSession\";\nimport { createEventQueue, type EventQueue, type EventQueueOptions } from \"./eventQueue\";\nimport type { ClientEvent } from \"./reportClientEvent\";\nimport {\n analyticsUserIdStorageKey,\n clearPiiFromContext,\n resolveUserContext,\n type WireUserContext,\n} from \"../context/userContext\";\nimport { resolveAutoDeviceKey, type ResolveAutoDeviceKeyOptions } from \"../context/deviceId\";\nimport { detectAppVersion } from \"../device/appVersion\";\nimport { resolveIdentity } from \"../identity/identityRecord\";\nimport { looksLikeEmail, sanitizeUserId } from \"../identity/userIdentity\";\nimport { warnInDev } from \"../utils/warnInDev\";\n\n/** Arbitrary non-PII event properties. Serialized to the event's `meta` (a JSON string) on the wire. */\nexport type AnalyticsProps = Record<string, unknown>;\n\n/**\n * Tenant transport + context inputs for {@link createAnalytics}. `serverUrl`/`apiKey` are the\n * SAME creds as onboarding (never a second key). The rest feed the context envelope + the queue's\n * offline persistence — all optional.\n */\nexport type CreateAnalyticsConfig = {\n /** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */\n serverUrl: string;\n /** Tenant API key; sent as `Authorization: Bearer`. */\n apiKey: string;\n /**\n * ⚠️ OPT-OUT KNOB, not a default. Supplying a `sessionId` FREEZES the correlation id: every event\n * this instance ever sends (including `identify`) is pinned to that one id, and the instance stops\n * following the LIVE per-open session the server registered via `app.session_started`. Lifecycle\n * analytics then collapse onto a single device-scoped session — one \"first open\", forever.\n *\n * OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and\n * when no open has been registered yet it mints one AND registers it, so every later surface joins\n * the same session instead of each inventing its own. Pass one ONLY if your host runs its own\n * session lifecycle and owns the id the server should correlate on.\n */\n sessionId?: string;\n /** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */\n appId?: string;\n /**\n * Host persistence (AsyncStorage-compatible subset) for offline-first durability. When omitted,\n * the queue runs in the documented in-memory mode (survives re-renders, not app kills).\n */\n storage?: EventQueueOptions[\"storage\"];\n /** Host app version, e.g. \"1.4.2\" (host-injected; stamped onto every event's context). */\n appVersion?: string;\n /** Host native build number, e.g. \"412\" (host-injected). */\n appBuild?: string;\n /** Host connectivity signal, e.g. \"wifi\" | \"cellular\" — read fresh per event via the provider. */\n networkType?: string;\n /**\n * The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque\n * user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via\n * {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional.\n *\n * NOTE on `deviceKey`: you do NOT need to supply one. When omitted, the kit auto-mints a stable,\n * non-PII per-install `device_key`, persists it via `storage`, and reuses it every open (in-memory\n * fallback without storage) — so `user_context.device_key` is ALWAYS present for the server's\n * review/questionnaire gating + A/B stickiness. Supply `deviceKey` only to use your OWN id (it wins).\n */\n userContext?: WireUserContext;\n /**\n * ESCAPE HATCH for the email-shape guard. By default `identify(id)` and a `setUserContext({ userId })`\n * REFUSE to bind an id that looks like an email (`local@domain.tld`) and warn in dev — because a\n * raw email in the opaque `user_id` is a PII leak; an email belongs in the opt-in\n * `userContext.userEmail` field. Set `true` ONLY if your real internal user id genuinely IS an\n * email address and you accept it as the pseudonymous key. Default `false` (guard on).\n */\n allowEmailAsUserId?: boolean;\n};\n\n/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */\nexport type AnalyticsOptions = Partial<\n Pick<EventQueueOptions, \"maxSize\" | \"batchSize\" | \"baseBackoffMs\" | \"maxBackoffMs\" | \"maxRetries\">\n>;\n\n/**\n * The developer-facing analytics surface. `track`/`screen`/`identify` are the Segment/PostHog-shaped\n * API; `flush`/`notifyOnline`/`size` expose the underlying queue so a host can drive reconnect\n * draining (load-bearing for offline-first) and inspect the pending buffer.\n */\nexport type Analytics = {\n /** Record an in-app event: `event_type='app_event'`, `question_key=<event>`, `props`→`meta`. */\n track(event: string, props?: AnalyticsProps): void;\n /** Record a screen view: `event_type='app_event'`, `question_key='screen'`, `meta={ screen, ...props }`. */\n screen(name: string, props?: AnalyticsProps): void;\n /** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */\n identify(userId: string, traits?: AnalyticsProps): void;\n /**\n * Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow\n * merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also\n * binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.\n */\n setUserContext(partial: Partial<WireUserContext>): void;\n /**\n * LOGOUT: unbind the current user so a shared device never attributes user B's events to user A.\n * Clears the in-memory `boundUserId`, strips the PII / pseudonymous fields (`userId`, `userEmail`,\n * `extra`) from the bound {@link WireUserContext} (keeping the non-PII `device_key` + `appVersion`,\n * which group a DEVICE not a user), and removes the persisted `wireai:analytics:userId:<appId>`\n * key so it cannot be rehydrated on the next launch. Subsequent events are anonymous until the\n * next `identify` / `setUserContext`. Fire-and-forget; mirrors the `reset()` convention on the\n * screen tracker. The standalone `clearUserContext({ storage, appId })` covers the `wire` path.\n */\n reset(): void;\n /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */\n flush(): void;\n /** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */\n notifyOnline(): void;\n /** Current pending (in-memory) count. */\n size(): number;\n /**\n * Tear the instance's event queue down when its owner goes away (see {@link EventQueue.dispose}).\n * Idempotent, never throws. Call it if you build an instance per screen / per mount: the queue\n * claims a persisted storage slot, and a replacement built before the old one released it lands on\n * a rotated `…#2` key that no later launch ever reads. `useAnalytics` does this for you.\n */\n dispose(): void;\n};\n\n/**\n * Create a bound analytics instance. Seeds one correlation `sessionId`, builds an offline-first\n * queue over the tenant transport, and passes a fresh-per-event context envelope PROVIDER so the\n * connectivity type is read at enqueue time. The bound user id starts unset (see `identify`).\n */\nexport const createAnalytics = (\n config: CreateAnalyticsConfig,\n options: AnalyticsOptions = {},\n): Analytics => {\n // The session id every event correlates to. Precedence: an explicit `config.sessionId` freezes the\n // id (opt-out of the reuse); otherwise reuse the LIVE per-open session the server saw (via\n // `app.session_started`) so `identify`/app-events don't mint a fresh id the server back-fills into a\n // phantom session.\n //\n // `ensureCurrentSessionId` (not the bare `getCurrentSessionId`) is what closes the facade-first\n // ordering hole: a `track` that runs BEFORE the root lifecycle effect used to fall back to a\n // per-instance id this facade never REGISTERED, so the server saw a session it had no\n // `session_started` for and back-filled a phantom one. The two paths that already mint on the wire\n // (`wire.track`, `reportAppEvent`) both register; this one only read. Registering makes the\n // fallback id the id every LATER surface joins on, and when an open IS registered this is exactly\n // `getCurrentSessionId()` — so nothing changes for a host that mounts lifecycle first.\n const resolveSessionId = (): string => config.sessionId ?? ensureCurrentSessionId();\n\n // A frozen id is almost always a mistake (it silently flattens every open into ONE session), so\n // name it once at construction — same dev-only channel as the email-shape guard below.\n if (config.sessionId) {\n warnInDev(\n \"[wireai] createAnalytics({ sessionId }) PINS every event from this instance to that one \" +\n \"frozen id and opts out of the live per-open session (app.session_started) — lifecycle \" +\n \"analytics collapse onto a single device-scoped id. Remove it unless your host runs its \" +\n \"own session lifecycle.\",\n );\n }\n\n // The auto-detected host app version, read ONCE here (cheap, sync, never throws). It backs the\n // `user_context.app_version` fallback below: without it a host that passes no `config.appVersion`\n // got the detected version on `device.appVersion` only, leaving the user_context field absent.\n const detectedAppVersion = detectAppVersion();\n\n // The mutable rich user-context: seeded at init, updated via `setUserContext`. Resolved fresh on\n // every event so a post-mount update (login) takes effect immediately. Declared before the envelope\n // provider so the provider can read the current `userContext.appVersion` (see below).\n let userContext: WireUserContext = { ...(config.userContext ?? {}) };\n\n // Auto device id (the headline: \"device\" fully automatic). When the host supplies NO `deviceKey`,\n // the kit mints ONE stable, non-PII per-install id, PERSISTS it via the host `storage`, and reuses it\n // on every subsequent open — so `user_context.device_key` is ALWAYS present (the server's\n // review/questionnaire gating + A/B stickiness both key on it) with zero host wiring. A host-supplied\n // `deviceKey` still wins (see `applyContext`). Falls back to an in-memory id (stable for this\n // instance) when no storage is available.\n const hostDeviceKeyAtInit =\n typeof config.userContext?.deviceKey === \"string\" && config.userContext.deviceKey.trim()\n ? config.userContext.deviceKey.trim()\n : undefined;\n // Record a HOST-supplied key on the process provenance registry, so a `<WireOnboarding>` mount that\n // was NOT given one can tell \"this app owns no device id\" (fine, inject) from \"this app owns one\n // and forgot it here\" (the silent third id space). Recording only; nothing reads it here.\n resolveIdentity({\n value: hostDeviceKeyAtInit,\n space: \"device\",\n source: \"host\",\n scope: config.appId,\n });\n // The auto id comes from the ONE process-wide registry (`resolveAutoDeviceKey`), NOT a mint local to\n // this instance. A host that also builds a `createWireActivation` instance used to get a SECOND,\n // different auto id for the same install, splitting `device_key` across two id spaces — see the\n // registry note in context/deviceId.ts. Resolved lazily per event so hydration is picked up.\n const autoDeviceKeyOptions: ResolveAutoDeviceKeyOptions = {\n appId: config.appId,\n // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).\n storage: hostDeviceKeyAtInit ? undefined : config.storage,\n };\n // Start hydration AT CONSTRUCTION (not at the first event) so the persisted id is adopted as early\n // as it used to be. The return value is deliberately discarded — every event re-resolves.\n if (!hostDeviceKeyAtInit) resolveAutoDeviceKey(autoDeviceKeyOptions);\n\n // A provider (not a fixed value) so `networkType`, the current session id, AND the effective app\n // version are evaluated fresh on every enqueue. An explicit `WireUserContext.appVersion` (a host that\n // set the version ONLY inside `userContext`) now flows into `device.appVersion` too — not just\n // `user_context.app_version` — so the server's `by_app_version` breakdown (which reads\n // `device.appVersion`) agrees. Explicit wins over the auto-detected device version;\n // `buildContextEnvelope` keeps the auto value when neither is set.\n const envelope = (): ContextEnvelope =>\n buildContextEnvelope({\n sessionId: resolveSessionId(),\n appVersion: userContext.appVersion ?? config.appVersion,\n appBuild: config.appBuild,\n networkType: config.networkType,\n });\n\n const queue: EventQueue = createEventQueue({\n target: { serverUrl: config.serverUrl, apiKey: config.apiKey },\n storage: config.storage,\n appId: config.appId,\n envelope,\n ...options,\n });\n\n // Per-session, in-memory user binding. Seeded from the init context, then persisted across\n // launches when storage is provided.\n let boundUserId: string | undefined = sanitizeUserId(config.userContext?.userId);\n const storageKey = analyticsUserIdStorageKey(config.appId);\n\n // SUPERSESSION LATCH for the construction-time hydration below.\n //\n // The read's guard used to be `saved && !boundUserId` alone, which asks \"did something bind an id\n // while I was reading?\". A LOGOUT is the one case where the answer is a deliberate `undefined`, so\n // a read that landed after `reset()` sailed through the guard and re-bound the user who had just\n // logged out — onto every subsequent event, which is precisely what `reset()` promises cannot\n // happen. The window is the first storage read of a cold start, and a logout inside it is a real\n // sequence, not a contrived one. An empty binding must be able to mean \"cleared on purpose\".\n let hydrationSuperseded = false;\n\n if (config.storage) {\n void config.storage\n .getItem(storageKey)\n .then((saved) => {\n // A `reset()` that already ran wins, whatever this read says.\n if (hydrationSuperseded) return;\n // Don't clobber an explicit init-context user id with a stale persisted one.\n if (saved && !boundUserId) boundUserId = saved;\n })\n .catch(() => {});\n }\n\n // Stamp the resolved rich context onto an event: the `user_context` bucket (device_key, app_version,\n // opt-in user_email, namespaced `custom.*`) and the top-level opaque `user_id`. Never overwrites a\n // key the caller already set (so `identify`'s explicit `user_id` and any caller `user_context` win).\n const applyContext = (event: ClientEvent): void => {\n // Host `deviceKey` wins; otherwise the auto-minted/persisted per-install id fills it in so\n // `user_context.device_key` is always present.\n const hostDeviceKey =\n typeof userContext.deviceKey === \"string\" && userContext.deviceKey.trim()\n ? userContext.deviceKey\n : undefined;\n const resolved = resolveUserContext(\n // `??` is lazy on purpose: a host-supplied key must never even touch the auto registry.\n { ...userContext, deviceKey: hostDeviceKey ?? resolveAutoDeviceKey(autoDeviceKeyOptions) },\n { autoAppVersion: config.appVersion ?? detectedAppVersion },\n );\n if (resolved.userContext) {\n event.user_context = { ...resolved.userContext, ...(event.user_context ?? {}) };\n }\n if (boundUserId && !event.user_id) event.user_id = boundUserId;\n };\n\n // Email-shape guard for the OPAQUE user id. A raw email bound as `user_id` is a PII leak (it\n // belongs in the opt-in `user_context.user_email`), so refuse it and warn in dev — unless the host\n // opted in via `allowEmailAsUserId`. Returns the id to bind, or `undefined` to refuse.\n const guardUserId = (clean: string): string | undefined => {\n if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;\n warnInDev(\n \"[wireai] identify() was called with an email-shaped id. A raw email must NOT be the opaque \" +\n \"user_id (PII leak) — pass it as userContext.userEmail instead. Binding was skipped. Set \" +\n \"allowEmailAsUserId:true on createAnalytics if your user id genuinely is an email.\",\n );\n return undefined;\n };\n\n const setUserContext = (partial: Partial<WireUserContext>): void => {\n if (!partial || typeof partial !== \"object\") return;\n // Deep-merge `extra` so a partial update adds keys instead of replacing the whole map.\n const mergedExtra =\n partial.extra || userContext.extra\n ? { ...(userContext.extra ?? {}), ...(partial.extra ?? {}) }\n : undefined;\n userContext = { ...userContext, ...partial };\n if (mergedExtra) userContext.extra = mergedExtra;\n // A user id supplied here binds like `identify` so subsequent events carry `user_id` — through\n // the SAME email-shape guard (a raw email must not become the opaque user_id).\n const uid = sanitizeUserId(partial.userId);\n if (uid) {\n const bindable = guardUserId(uid);\n if (bindable) {\n boundUserId = bindable;\n if (config.storage) void config.storage.setItem(storageKey, bindable).catch(() => {});\n }\n }\n };\n\n const reset = (): void => {\n // Supersede any construction-time hydration still in flight, BEFORE clearing the binding: the\n // read cannot tell \"nobody bound anything yet\" from \"the user just logged out\", so the answer\n // has to come from here. Without this the read re-binds the logged-out user.\n hydrationSuperseded = true;\n // In-memory binding cleared: subsequent events carry no user_id until the next identify.\n boundUserId = undefined;\n // Strip PII from the rich context but keep the device-scope fields (device_key / app_version).\n userContext = clearPiiFromContext(userContext);\n // Remove the persisted binding so a relaunch can't rehydrate the previous user's id.\n if (config.storage) void config.storage.removeItem(storageKey).catch(() => {});\n };\n\n const track = (event: string, props?: AnalyticsProps): void => {\n if (!event) return;\n const clientEvent: ClientEvent = {\n event_type: \"app_event\",\n session_id: resolveSessionId(),\n question_key: event,\n };\n if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n const screen = (name: string, props?: AnalyticsProps): void => {\n if (!name) return;\n // Reuse the existing screen event shape (question_key='screen'); route through the queue for offline-first.\n const meta = { screen: name, ...(props ?? {}) };\n const clientEvent: ClientEvent = {\n event_type: \"app_event\",\n session_id: resolveSessionId(),\n question_key: \"screen\",\n meta: JSON.stringify(meta),\n };\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n const identify = (userId: string, traits?: AnalyticsProps): void => {\n const clean = sanitizeUserId(userId);\n // Blank / non-string → no binding, no event (sanitizeUserId returns undefined). >128 → truncated.\n if (!clean) return;\n // Email-shape guard: refuse to bind (and emit) a raw email as the opaque user_id unless opted in.\n const bindable = guardUserId(clean);\n if (!bindable) return;\n boundUserId = bindable;\n if (config.storage) {\n void config.storage.setItem(storageKey, bindable).catch(() => {});\n }\n const clientEvent: ClientEvent = {\n event_type: \"identify\",\n // Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to\n // the session it already saw instead of back-filling a phantom `session_started`.\n session_id: resolveSessionId(),\n user_id: bindable,\n };\n if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);\n applyContext(clientEvent);\n queue.enqueue(clientEvent);\n };\n\n return {\n track,\n screen,\n identify,\n setUserContext,\n reset,\n flush: queue.flush,\n notifyOnline: queue.notifyOnline,\n size: queue.size,\n dispose: queue.dispose,\n };\n};\n","/**\n * useAnalytics — a THIN optional React hook over the pure {@link createAnalytics} factory.\n *\n * It builds ONE analytics instance for the component's lifetime and returns it, so re-renders\n * never rebuild the queue or lose the in-memory user binding. React is a REQUIRED peer of the\n * kit, so importing it here is allowed; the hook adds NO other dependency. Mirrors the existing\n * `createScreenTracker` / `useScreenTracking` split — the factory stays React-free, this is the\n * glue.\n *\n * const analytics = useAnalytics({ serverUrl, apiKey, storage, appId });\n * analytics.track(\"content_share\", { source: \"feed\" });\n * // ...on reconnect: analytics.notifyOnline();\n */\nimport { useEffect, useRef } from \"react\";\n\nimport {\n createAnalytics,\n type Analytics,\n type AnalyticsOptions,\n type CreateAnalyticsConfig,\n} from \"./analyticsFacade\";\n\n/**\n * Build a per-mount analytics instance. `config`/`options` are read once at first render (the\n * instance is stable for the component's lifetime, held in a ref). Returns the {@link Analytics}\n * surface so the component can `track` / `screen` / `identify` and drive `notifyOnline` on reconnect.\n */\nexport const useAnalytics = (\n config: CreateAnalyticsConfig,\n options: AnalyticsOptions = {},\n): Analytics => {\n // One instance per mount; kept in a ref so re-renders never rebuild the queue or drop the binding.\n const ref = useRef<Analytics | undefined>(undefined);\n const prevKeys = useRef<string>(\"\");\n\n const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;\n if (!ref.current || prevKeys.current !== currentKeys) {\n prevKeys.current = currentKeys;\n // The instance being REPLACED (credentials changed, or an `appId` that arrived async) owns a\n // claimed storage slot and a live backoff timer. Hand them back before building the replacement,\n // or the new instance rotates onto a `…#2` key that no later launch reads, and the old one's\n // retry can still wake up and `removeItem` the slot the new one is using.\n ref.current?.dispose();\n ref.current = createAnalytics(config, options);\n }\n\n // A LATE-ARRIVING host device key must still reach the instance.\n //\n // `createAnalytics` copies `config.userContext` into a closure at construction and never re-reads\n // the prop, and `config.userContext.deviceKey` is deliberately NOT part of `currentKeys` (rebuilding\n // the instance would throw away the event queue's pending buffer). So a host that hydrates its\n // device id asynchronously — an AsyncStorage read that resolves after first render — used to be\n // stamped with the kit's auto-minted `wdev_*` id FOREVER, while a sibling `useWireActivation`\n // (which does key on it) rebuilt and used the real one. One install, two `device_key` values, in\n // the same app, on the key every gating rule and the purchase↔onboarding join reads.\n //\n // `setUserContext` is the non-destructive seam for exactly this: it updates the bound context in\n // place, so subsequent events carry the host key with no queue rebuild.\n const hostDeviceKey =\n typeof config.userContext?.deviceKey === \"string\" && config.userContext.deviceKey.trim()\n ? config.userContext.deviceKey.trim()\n : undefined;\n useEffect(() => {\n if (hostDeviceKey) ref.current?.setUserContext({ deviceKey: hostDeviceKey });\n }, [hostDeviceKey]);\n\n // UNMOUNT: give the storage claim and the retry timer back. A screen that mounts this hook is\n // built and torn down repeatedly (navigation, Fast Refresh, StrictMode's double-invoke), and each\n // rebuild used to leave the previous queue holding the appId's slot — so every remount after the\n // first persisted into `…#2`, `…#3`, and the next launch read none of them.\n useEffect(\n () => () => {\n ref.current?.dispose();\n ref.current = undefined;\n },\n [],\n );\n\n return ref.current;\n};\n"]}