@wireai/activation 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +82 -40
  2. package/CHANGELOG.md +17 -2
  3. package/INTEGRATION_PROMPT.md +8 -8
  4. package/README.md +38 -38
  5. package/dist/analytics/index.d.mts +92 -0
  6. package/dist/analytics/index.d.ts +92 -0
  7. package/dist/analytics/index.js +150 -0
  8. package/dist/analytics/index.js.map +1 -0
  9. package/dist/analytics/index.mjs +139 -0
  10. package/dist/analytics/index.mjs.map +1 -0
  11. package/dist/analyticsEvent-B8v3BZjM.d.mts +419 -0
  12. package/dist/analyticsEvent-DvjB92kK.d.ts +419 -0
  13. package/dist/coachmarks/index.js.map +1 -1
  14. package/dist/coachmarks/index.mjs.map +1 -1
  15. package/dist/decision-BzbiKwk3.d.mts +79 -0
  16. package/dist/decision-plDEOCkt.d.ts +79 -0
  17. package/dist/index.d.mts +8 -418
  18. package/dist/index.d.ts +8 -418
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/questionnaire/index.d.mts +2 -1
  22. package/dist/questionnaire/index.d.ts +2 -1
  23. package/dist/questionnaire/index.js.map +1 -1
  24. package/dist/questionnaire/index.mjs.map +1 -1
  25. package/dist/reviews/index.d.mts +5 -40
  26. package/dist/reviews/index.d.ts +5 -40
  27. package/dist/reviews/index.js.map +1 -1
  28. package/dist/reviews/index.mjs.map +1 -1
  29. package/dist/showcase/index.js.map +1 -1
  30. package/dist/showcase/index.mjs.map +1 -1
  31. package/dist/transport-BeO_Brcu.d.mts +40 -0
  32. package/dist/transport-DLpd1v5_.d.ts +40 -0
  33. package/dist/{decision-Cl8OFYzu.d.mts → types-A6pTxIZV.d.mts} +1 -77
  34. package/dist/{decision-CFvGY6nP.d.ts → types-BhpXJGlg.d.ts} +1 -77
  35. package/llms.txt +7 -7
  36. package/metro/index.d.ts +3 -3
  37. package/metro/index.js +3 -3
  38. package/package.json +15 -1
  39. package/src/analytics/index.ts +39 -0
  40. package/src/analytics/screenTracking.ts +122 -0
  41. package/src/analytics/useScreenTracking.ts +48 -0
  42. package/src/coachmarks/index.ts +2 -2
  43. package/src/features/WireFeaturesProvider.tsx +1 -1
  44. package/src/features/index.ts +2 -2
  45. package/src/index.ts +1 -1
  46. package/src/questionnaire/index.ts +2 -2
  47. package/src/reviews/index.ts +2 -2
  48. package/src/showcase/index.ts +2 -2
  49. package/src/types.ts +1 -1
@@ -0,0 +1,92 @@
1
+ export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DLpd1v5_.js';
2
+ export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, W as WIRE_ONBOARDING_EVENTS, c as WireOnboardingEventName, m as makeSessionId, r as reportClientEvent, d as reportClientEvents, t as toAnalyticsEvent } from '../analyticsEvent-DvjB92kK.js';
3
+ import '../types-BhpXJGlg.js';
4
+ import '../types-BKfpdZzX.js';
5
+ import '../types-GL_hQ0TN.js';
6
+ import '../types-CMuOexw0.js';
7
+ import 'react-native';
8
+ import 'react';
9
+ import 'wireai-rn';
10
+
11
+ /** A single route inside a React-Navigation-shaped state (structural — no `@react-navigation`). */
12
+ interface NavigationRouteLike {
13
+ name: string;
14
+ /** A nested navigator's own state, when this route hosts one. */
15
+ state?: NavigationStateLike;
16
+ /** Route params are intentionally left `unknown` — this helper never reads them. */
17
+ params?: unknown;
18
+ }
19
+ /** A React-Navigation-shaped navigator state (structural type; no library import). */
20
+ interface NavigationStateLike {
21
+ /** Index of the active route within `routes`. */
22
+ index?: number;
23
+ routes?: NavigationRouteLike[];
24
+ }
25
+ /** Options for {@link createScreenTracker}. */
26
+ interface ScreenTrackerOptions {
27
+ /**
28
+ * Where to POST. `{ serverUrl, apiKey }` — same shape the kit's review/analytics config
29
+ * exposes. When omitted, the tracker still de-dups and fires `onScreen`, but sends nothing.
30
+ */
31
+ target?: {
32
+ serverUrl: string;
33
+ apiKey: string;
34
+ };
35
+ /** The onboarding/session id to correlate screen views with, when known. */
36
+ sessionId?: string;
37
+ /** A stable, non-PII device id — groups a device's sessions server-side. */
38
+ deviceKey?: string;
39
+ /** Called on every REAL screen change (after de-dup), before the network emit. */
40
+ onScreen?: (screen: string) => void;
41
+ /**
42
+ * Per-screen filter. Return `false` to skip the NETWORK emit for a screen (last-screen memory
43
+ * is still advanced + `onScreen` still fires) — e.g. to keep a sensitive route out of analytics.
44
+ */
45
+ shouldTrack?: (screen: string) => boolean;
46
+ }
47
+ /** The screen tracker returned by {@link createScreenTracker}. */
48
+ interface ScreenTracker {
49
+ /** Report the active screen. Ignores `undefined`/empty and de-dups repeats of the last screen. */
50
+ track: (screen: string | undefined) => void;
51
+ /** Clear the last-screen memory (e.g. on logout) so the next `track` always emits. */
52
+ reset: () => void;
53
+ }
54
+ /**
55
+ * Walk a React-Navigation-shaped state to the DEEPEST active route and return its NAME (never
56
+ * its params). Recurses `routes[index]` while a nested `.state` exists. Returns `undefined` for
57
+ * a missing/empty/malformed state — the caller treats that as "nothing to report".
58
+ */
59
+ declare const getActiveRouteName: (state: NavigationStateLike | undefined) => string | undefined;
60
+ /**
61
+ * Build a stateful screen tracker. `track` de-dups against the last reported screen so only a
62
+ * REAL change emits; `reset` clears that memory. Fire-and-forget throughout — a missing `target`
63
+ * skips the network but keeps the de-dup + `onScreen` behaviour intact.
64
+ */
65
+ declare const createScreenTracker: (options?: ScreenTrackerOptions) => ScreenTracker;
66
+ /**
67
+ * Adapt a tracker into a React-Navigation `onStateChange` handler — the one-place wiring:
68
+ *
69
+ * <NavigationContainer onStateChange={screenTrackingHandler(tracker)}>
70
+ *
71
+ * It resolves the deepest active route name and hands it to `tracker.track` (which de-dups).
72
+ */
73
+ declare const screenTrackingHandler: (tracker: ScreenTracker) => (state: NavigationStateLike | undefined) => void;
74
+
75
+ /**
76
+ * The structural slice of a React-Navigation container ref this hook needs — no
77
+ * `@react-navigation` import. `getCurrentRoute` yields the active route; `addListener("state", …)`
78
+ * fires on every navigation state change and returns its own unsubscribe.
79
+ */
80
+ interface NavigationRefLike {
81
+ getCurrentRoute?: () => {
82
+ name?: string;
83
+ } | undefined;
84
+ addListener?: (type: "state", callback: () => void) => () => void;
85
+ }
86
+ /**
87
+ * Subscribe screen tracking to a host navigation ref. Safe to call with a not-yet-ready ref
88
+ * (the effect no-ops until `addListener` exists). Returns nothing — it wires side effects only.
89
+ */
90
+ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined, options?: ScreenTrackerOptions) => void;
91
+
92
+ export { type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, createScreenTracker, getActiveRouteName, screenTrackingHandler, useScreenTracking };
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/reviews/transport.ts
6
+ var reportAppEvent = (target, name, options = {}) => {
7
+ if (!(target == null ? void 0 : target.serverUrl) || !name) return;
8
+ try {
9
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
10
+ const headers = { "Content-Type": "application/json" };
11
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
12
+ const event = {
13
+ event_type: "app_event",
14
+ question_key: name
15
+ };
16
+ if (options.sessionId) event.session_id = options.sessionId;
17
+ if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
18
+ if (options.meta && Object.keys(options.meta).length > 0) {
19
+ event.meta = JSON.stringify(options.meta);
20
+ }
21
+ void fetch(url, {
22
+ method: "POST",
23
+ headers,
24
+ body: JSON.stringify({ events: [event] })
25
+ }).catch(() => {
26
+ });
27
+ } catch {
28
+ }
29
+ };
30
+
31
+ // src/analytics/screenTracking.ts
32
+ var getActiveRouteName = (state) => {
33
+ let current = state;
34
+ let name;
35
+ while (current && Array.isArray(current.routes) && current.routes.length > 0) {
36
+ const index = typeof current.index === "number" ? current.index : 0;
37
+ const route = current.routes[index];
38
+ if (!route) break;
39
+ name = route.name;
40
+ current = route.state;
41
+ }
42
+ return name;
43
+ };
44
+ var createScreenTracker = (options = {}) => {
45
+ let lastScreen;
46
+ return {
47
+ track: (screen) => {
48
+ var _a;
49
+ if (!screen || screen === lastScreen) return;
50
+ lastScreen = screen;
51
+ (_a = options.onScreen) == null ? void 0 : _a.call(options, screen);
52
+ if (options.shouldTrack && !options.shouldTrack(screen)) return;
53
+ reportAppEvent(options.target, "screen", {
54
+ sessionId: options.sessionId,
55
+ deviceKey: options.deviceKey,
56
+ meta: { screen }
57
+ });
58
+ },
59
+ reset: () => {
60
+ lastScreen = void 0;
61
+ }
62
+ };
63
+ };
64
+ var screenTrackingHandler = (tracker) => (state) => tracker.track(getActiveRouteName(state));
65
+ var useScreenTracking = (navigationRef, options = {}) => {
66
+ const trackerRef = react.useRef(void 0);
67
+ if (!trackerRef.current) trackerRef.current = createScreenTracker(options);
68
+ react.useEffect(() => {
69
+ const tracker = trackerRef.current;
70
+ if (!tracker || !(navigationRef == null ? void 0 : navigationRef.addListener)) return;
71
+ const report = () => {
72
+ var _a, _b;
73
+ return tracker.track((_b = (_a = navigationRef.getCurrentRoute) == null ? void 0 : _a.call(navigationRef)) == null ? void 0 : _b.name);
74
+ };
75
+ report();
76
+ const unsubscribe = navigationRef.addListener("state", report);
77
+ return unsubscribe;
78
+ }, [navigationRef]);
79
+ };
80
+
81
+ // src/analytics/reportClientEvent.ts
82
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
83
+ var reportClientEvents = (target, events) => {
84
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return;
85
+ try {
86
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
87
+ const headers = { "Content-Type": "application/json" };
88
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
89
+ void fetch(url, {
90
+ method: "POST",
91
+ headers,
92
+ body: JSON.stringify({ events })
93
+ }).catch(() => {
94
+ });
95
+ } catch {
96
+ }
97
+ };
98
+ var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
99
+
100
+ // src/analytics/analyticsEvent.ts
101
+ var WIRE_ONBOARDING_EVENTS = {
102
+ started: "wire_onboarding_started",
103
+ /** A persisted session was restored after an app kill (fires instead of `started`). */
104
+ resumed: "wire_onboarding_resumed",
105
+ turn: "wire_onboarding_turn",
106
+ error: "wire_onboarding_error",
107
+ retry: "wire_onboarding_retry",
108
+ fallback: "wire_onboarding_fallback",
109
+ /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */
110
+ completed: "wire_onboarding_completed"
111
+ };
112
+ var toAnalyticsEvent = (event) => {
113
+ switch (event.type) {
114
+ case "started":
115
+ return { name: WIRE_ONBOARDING_EVENTS.started };
116
+ case "resumed":
117
+ return { name: WIRE_ONBOARDING_EVENTS.resumed };
118
+ case "turn":
119
+ return {
120
+ name: WIRE_ONBOARDING_EVENTS.turn,
121
+ params: { step: event.step, component: event.component }
122
+ };
123
+ case "error":
124
+ return { name: WIRE_ONBOARDING_EVENTS.error, params: { reason: event.reason } };
125
+ case "retry":
126
+ return {
127
+ name: WIRE_ONBOARDING_EVENTS.retry,
128
+ params: { reason: event.reason, attempt: event.attempt }
129
+ };
130
+ case "fallback":
131
+ return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
132
+ default: {
133
+ const _exhaustive = event;
134
+ return _exhaustive;
135
+ }
136
+ }
137
+ };
138
+
139
+ exports.WIRE_ONBOARDING_EVENTS = WIRE_ONBOARDING_EVENTS;
140
+ exports.createScreenTracker = createScreenTracker;
141
+ exports.getActiveRouteName = getActiveRouteName;
142
+ exports.makeSessionId = makeSessionId;
143
+ exports.reportAppEvent = reportAppEvent;
144
+ exports.reportClientEvent = reportClientEvent;
145
+ exports.reportClientEvents = reportClientEvents;
146
+ exports.screenTrackingHandler = screenTrackingHandler;
147
+ exports.toAnalyticsEvent = toAnalyticsEvent;
148
+ exports.useScreenTracking = useScreenTracking;
149
+ //# sourceMappingURL=index.js.map
150
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/reviews/transport.ts","../../src/analytics/screenTracking.ts","../../src/analytics/useScreenTracking.ts","../../src/analytics/reportClientEvent.ts","../../src/analytics/analyticsEvent.ts"],"names":["useRef","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * transport.ts — kit → Wire server requests for the review module, all fire-and-forget\n * (analytics/reviews must never break the app). Mirrors analytics/reportClientEvent: a\n * thin fetch wrapper, Bearer tenant key, swallow every error.\n *\n * • submitReview → POST {serverUrl}/v1/reviews (the 1-4 feedback body)\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 type { ReviewSubmission, ReviewTarget } from \"./types\";\n\n/**\n * POST a review (the 1-4 feedback path). Fire-and-forget: a missing target, a build error,\n * a missing `fetch`, or a network failure is swallowed and the call returns immediately.\n */\nexport const submitReview = (\n target: ReviewTarget | undefined,\n review: ReviewSubmission,\n): void => {\n if (!target?.serverUrl) return;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(review),\n }).catch(() => {\n /* best-effort, swallow */\n });\n } catch {\n /* URL/JSON/missing-fetch — swallow */\n }\n};\n\n/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */\nexport interface ReportAppEventOptions {\n /** The onboarding/session id to correlate with, when known. */\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 */\nexport const reportAppEvent = (\n target: ReviewTarget | undefined,\n name: string,\n options: ReportAppEventOptions = {},\n): void => {\n if (!target?.serverUrl || !name) return;\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 const event: Record<string, unknown> = {\n event_type: \"app_event\",\n question_key: name,\n };\n if (options.sessionId) event.session_id = options.sessionId;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ events: [event] }),\n }).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 /** The onboarding/session id to correlate screen views with, when known. */\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 // 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) trackerRef.current = createScreenTracker(options);\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 * 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. */\nexport type ClientEventType = \"screen_skipped\" | \"dropped\" | \"client_fallback\" | \"identify\";\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\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 * 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 if (!target?.serverUrl || events.length === 0) return;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ events }),\n }).catch(() => {\n // Network/transport error — analytics is best-effort, swallow.\n });\n } catch {\n // URL construction, JSON serialization, or 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 * 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 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 name: WireOnboardingEventName;\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 default: {\n const _exhaustive: never = event;\n return _exhaustive;\n }\n }\n};\n"]}
@@ -0,0 +1,139 @@
1
+ import { useRef, useEffect } from 'react';
2
+
3
+ // src/reviews/transport.ts
4
+ var reportAppEvent = (target, name, options = {}) => {
5
+ if (!(target == null ? void 0 : target.serverUrl) || !name) return;
6
+ try {
7
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
8
+ const headers = { "Content-Type": "application/json" };
9
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
10
+ const event = {
11
+ event_type: "app_event",
12
+ question_key: name
13
+ };
14
+ if (options.sessionId) event.session_id = options.sessionId;
15
+ if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
16
+ if (options.meta && Object.keys(options.meta).length > 0) {
17
+ event.meta = JSON.stringify(options.meta);
18
+ }
19
+ void fetch(url, {
20
+ method: "POST",
21
+ headers,
22
+ body: JSON.stringify({ events: [event] })
23
+ }).catch(() => {
24
+ });
25
+ } catch {
26
+ }
27
+ };
28
+
29
+ // src/analytics/screenTracking.ts
30
+ var getActiveRouteName = (state) => {
31
+ let current = state;
32
+ let name;
33
+ while (current && Array.isArray(current.routes) && current.routes.length > 0) {
34
+ const index = typeof current.index === "number" ? current.index : 0;
35
+ const route = current.routes[index];
36
+ if (!route) break;
37
+ name = route.name;
38
+ current = route.state;
39
+ }
40
+ return name;
41
+ };
42
+ var createScreenTracker = (options = {}) => {
43
+ let lastScreen;
44
+ return {
45
+ track: (screen) => {
46
+ var _a;
47
+ if (!screen || screen === lastScreen) return;
48
+ lastScreen = screen;
49
+ (_a = options.onScreen) == null ? void 0 : _a.call(options, screen);
50
+ if (options.shouldTrack && !options.shouldTrack(screen)) return;
51
+ reportAppEvent(options.target, "screen", {
52
+ sessionId: options.sessionId,
53
+ deviceKey: options.deviceKey,
54
+ meta: { screen }
55
+ });
56
+ },
57
+ reset: () => {
58
+ lastScreen = void 0;
59
+ }
60
+ };
61
+ };
62
+ var screenTrackingHandler = (tracker) => (state) => tracker.track(getActiveRouteName(state));
63
+ var useScreenTracking = (navigationRef, options = {}) => {
64
+ const trackerRef = useRef(void 0);
65
+ if (!trackerRef.current) trackerRef.current = createScreenTracker(options);
66
+ useEffect(() => {
67
+ const tracker = trackerRef.current;
68
+ if (!tracker || !(navigationRef == null ? void 0 : navigationRef.addListener)) return;
69
+ const report = () => {
70
+ var _a, _b;
71
+ return tracker.track((_b = (_a = navigationRef.getCurrentRoute) == null ? void 0 : _a.call(navigationRef)) == null ? void 0 : _b.name);
72
+ };
73
+ report();
74
+ const unsubscribe = navigationRef.addListener("state", report);
75
+ return unsubscribe;
76
+ }, [navigationRef]);
77
+ };
78
+
79
+ // src/analytics/reportClientEvent.ts
80
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
81
+ var reportClientEvents = (target, events) => {
82
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return;
83
+ try {
84
+ const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
85
+ const headers = { "Content-Type": "application/json" };
86
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
87
+ void fetch(url, {
88
+ method: "POST",
89
+ headers,
90
+ body: JSON.stringify({ events })
91
+ }).catch(() => {
92
+ });
93
+ } catch {
94
+ }
95
+ };
96
+ var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
97
+
98
+ // src/analytics/analyticsEvent.ts
99
+ var WIRE_ONBOARDING_EVENTS = {
100
+ started: "wire_onboarding_started",
101
+ /** A persisted session was restored after an app kill (fires instead of `started`). */
102
+ resumed: "wire_onboarding_resumed",
103
+ turn: "wire_onboarding_turn",
104
+ error: "wire_onboarding_error",
105
+ retry: "wire_onboarding_retry",
106
+ fallback: "wire_onboarding_fallback",
107
+ /** Logged by the host on `onComplete` (no matching kit `OnboardingEvent`). */
108
+ completed: "wire_onboarding_completed"
109
+ };
110
+ var toAnalyticsEvent = (event) => {
111
+ switch (event.type) {
112
+ case "started":
113
+ return { name: WIRE_ONBOARDING_EVENTS.started };
114
+ case "resumed":
115
+ return { name: WIRE_ONBOARDING_EVENTS.resumed };
116
+ case "turn":
117
+ return {
118
+ name: WIRE_ONBOARDING_EVENTS.turn,
119
+ params: { step: event.step, component: event.component }
120
+ };
121
+ case "error":
122
+ return { name: WIRE_ONBOARDING_EVENTS.error, params: { reason: event.reason } };
123
+ case "retry":
124
+ return {
125
+ name: WIRE_ONBOARDING_EVENTS.retry,
126
+ params: { reason: event.reason, attempt: event.attempt }
127
+ };
128
+ case "fallback":
129
+ return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
130
+ default: {
131
+ const _exhaustive = event;
132
+ return _exhaustive;
133
+ }
134
+ }
135
+ };
136
+
137
+ export { WIRE_ONBOARDING_EVENTS, createScreenTracker, getActiveRouteName, makeSessionId, reportAppEvent, reportClientEvent, reportClientEvents, screenTrackingHandler, toAnalyticsEvent, useScreenTracking };
138
+ //# sourceMappingURL=index.mjs.map
139
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/reviews/transport.ts","../../src/analytics/screenTracking.ts","../../src/analytics/useScreenTracking.ts","../../src/analytics/reportClientEvent.ts","../../src/analytics/analyticsEvent.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * transport.ts — kit → Wire server requests for the review module, all fire-and-forget\n * (analytics/reviews must never break the app). Mirrors analytics/reportClientEvent: a\n * thin fetch wrapper, Bearer tenant key, swallow every error.\n *\n * • submitReview → POST {serverUrl}/v1/reviews (the 1-4 feedback body)\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 type { ReviewSubmission, ReviewTarget } from \"./types\";\n\n/**\n * POST a review (the 1-4 feedback path). Fire-and-forget: a missing target, a build error,\n * a missing `fetch`, or a network failure is swallowed and the call returns immediately.\n */\nexport const submitReview = (\n target: ReviewTarget | undefined,\n review: ReviewSubmission,\n): void => {\n if (!target?.serverUrl) return;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(review),\n }).catch(() => {\n /* best-effort, swallow */\n });\n } catch {\n /* URL/JSON/missing-fetch — swallow */\n }\n};\n\n/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */\nexport interface ReportAppEventOptions {\n /** The onboarding/session id to correlate with, when known. */\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 */\nexport const reportAppEvent = (\n target: ReviewTarget | undefined,\n name: string,\n options: ReportAppEventOptions = {},\n): void => {\n if (!target?.serverUrl || !name) return;\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 const event: Record<string, unknown> = {\n event_type: \"app_event\",\n question_key: name,\n };\n if (options.sessionId) event.session_id = options.sessionId;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ events: [event] }),\n }).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 /** The onboarding/session id to correlate screen views with, when known. */\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 // 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) trackerRef.current = createScreenTracker(options);\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 * 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. */\nexport type ClientEventType = \"screen_skipped\" | \"dropped\" | \"client_fallback\" | \"identify\";\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\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 * 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 if (!target?.serverUrl || events.length === 0) return;\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 void fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ events }),\n }).catch(() => {\n // Network/transport error — analytics is best-effort, swallow.\n });\n } catch {\n // URL construction, JSON serialization, or 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 * 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 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 name: WireOnboardingEventName;\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 default: {\n const _exhaustive: never = event;\n return _exhaustive;\n }\n }\n};\n"]}