@cxpinsight/react-native 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/babel.js +101 -0
  4. package/instrumented/Pressable.d.ts +2 -0
  5. package/instrumented/Pressable.js +2 -0
  6. package/instrumented/ScrollView.d.ts +2 -0
  7. package/instrumented/ScrollView.js +2 -0
  8. package/instrumented/TextInput.d.ts +2 -0
  9. package/instrumented/TextInput.js +2 -0
  10. package/instrumented/TouchableOpacity.d.ts +2 -0
  11. package/instrumented/TouchableOpacity.js +2 -0
  12. package/lib/CXPinsight.d.ts +113 -0
  13. package/lib/CXPinsight.d.ts.map +1 -0
  14. package/lib/CXPinsight.js +831 -0
  15. package/lib/CXPinsight.js.map +1 -0
  16. package/lib/CXPinsightProvider.d.ts +19 -0
  17. package/lib/CXPinsightProvider.d.ts.map +1 -0
  18. package/lib/CXPinsightProvider.js +382 -0
  19. package/lib/CXPinsightProvider.js.map +1 -0
  20. package/lib/core/autoScreenTracker.d.ts +20 -0
  21. package/lib/core/autoScreenTracker.d.ts.map +1 -0
  22. package/lib/core/autoScreenTracker.js +66 -0
  23. package/lib/core/autoScreenTracker.js.map +1 -0
  24. package/lib/core/deepLinkTracker.d.ts +17 -0
  25. package/lib/core/deepLinkTracker.d.ts.map +1 -0
  26. package/lib/core/deepLinkTracker.js +81 -0
  27. package/lib/core/deepLinkTracker.js.map +1 -0
  28. package/lib/core/deviceInfo.d.ts +14 -0
  29. package/lib/core/deviceInfo.d.ts.map +1 -0
  30. package/lib/core/deviceInfo.js +166 -0
  31. package/lib/core/deviceInfo.js.map +1 -0
  32. package/lib/core/eventQueue.d.ts +23 -0
  33. package/lib/core/eventQueue.d.ts.map +1 -0
  34. package/lib/core/eventQueue.js +240 -0
  35. package/lib/core/eventQueue.js.map +1 -0
  36. package/lib/core/lifecycle.d.ts +18 -0
  37. package/lib/core/lifecycle.d.ts.map +1 -0
  38. package/lib/core/lifecycle.js +127 -0
  39. package/lib/core/lifecycle.js.map +1 -0
  40. package/lib/core/mobileRecorder.d.ts +15 -0
  41. package/lib/core/mobileRecorder.d.ts.map +1 -0
  42. package/lib/core/mobileRecorder.js +231 -0
  43. package/lib/core/mobileRecorder.js.map +1 -0
  44. package/lib/core/network.d.ts +10 -0
  45. package/lib/core/network.d.ts.map +1 -0
  46. package/lib/core/network.js +74 -0
  47. package/lib/core/network.js.map +1 -0
  48. package/lib/core/networkCapture.d.ts +22 -0
  49. package/lib/core/networkCapture.d.ts.map +1 -0
  50. package/lib/core/networkCapture.js +100 -0
  51. package/lib/core/networkCapture.js.map +1 -0
  52. package/lib/core/scrollCapture.d.ts +48 -0
  53. package/lib/core/scrollCapture.d.ts.map +1 -0
  54. package/lib/core/scrollCapture.js +221 -0
  55. package/lib/core/scrollCapture.js.map +1 -0
  56. package/lib/core/session.d.ts +15 -0
  57. package/lib/core/session.d.ts.map +1 -0
  58. package/lib/core/session.js +157 -0
  59. package/lib/core/session.js.map +1 -0
  60. package/lib/core/storage.d.ts +11 -0
  61. package/lib/core/storage.d.ts.map +1 -0
  62. package/lib/core/storage.js +59 -0
  63. package/lib/core/storage.js.map +1 -0
  64. package/lib/core/triggerEngine.d.ts +56 -0
  65. package/lib/core/triggerEngine.d.ts.map +1 -0
  66. package/lib/core/triggerEngine.js +432 -0
  67. package/lib/core/triggerEngine.js.map +1 -0
  68. package/lib/index.d.ts +7 -0
  69. package/lib/index.d.ts.map +1 -0
  70. package/lib/index.js +11 -0
  71. package/lib/index.js.map +1 -0
  72. package/lib/instrumented/Pressable.d.ts +9 -0
  73. package/lib/instrumented/Pressable.d.ts.map +1 -0
  74. package/lib/instrumented/Pressable.js +96 -0
  75. package/lib/instrumented/Pressable.js.map +1 -0
  76. package/lib/instrumented/ScrollView.d.ts +6 -0
  77. package/lib/instrumented/ScrollView.d.ts.map +1 -0
  78. package/lib/instrumented/ScrollView.js +101 -0
  79. package/lib/instrumented/ScrollView.js.map +1 -0
  80. package/lib/instrumented/TextInput.d.ts +9 -0
  81. package/lib/instrumented/TextInput.d.ts.map +1 -0
  82. package/lib/instrumented/TextInput.js +75 -0
  83. package/lib/instrumented/TextInput.js.map +1 -0
  84. package/lib/instrumented/TouchableOpacity.d.ts +11 -0
  85. package/lib/instrumented/TouchableOpacity.d.ts.map +1 -0
  86. package/lib/instrumented/TouchableOpacity.js +96 -0
  87. package/lib/instrumented/TouchableOpacity.js.map +1 -0
  88. package/lib/instrumented/index.d.ts +19 -0
  89. package/lib/instrumented/index.d.ts.map +1 -0
  90. package/lib/instrumented/index.js +28 -0
  91. package/lib/instrumented/index.js.map +1 -0
  92. package/lib/instrumented/interactionObserver.d.ts +29 -0
  93. package/lib/instrumented/interactionObserver.d.ts.map +1 -0
  94. package/lib/instrumented/interactionObserver.js +38 -0
  95. package/lib/instrumented/interactionObserver.js.map +1 -0
  96. package/lib/setup/AccessibilityScanner.d.ts +32 -0
  97. package/lib/setup/AccessibilityScanner.d.ts.map +1 -0
  98. package/lib/setup/AccessibilityScanner.js +127 -0
  99. package/lib/setup/AccessibilityScanner.js.map +1 -0
  100. package/lib/setup/ElementIdentifier.d.ts +65 -0
  101. package/lib/setup/ElementIdentifier.d.ts.map +1 -0
  102. package/lib/setup/ElementIdentifier.js +114 -0
  103. package/lib/setup/ElementIdentifier.js.map +1 -0
  104. package/lib/setup/ElementRegistry.d.ts +22 -0
  105. package/lib/setup/ElementRegistry.d.ts.map +1 -0
  106. package/lib/setup/ElementRegistry.js +47 -0
  107. package/lib/setup/ElementRegistry.js.map +1 -0
  108. package/lib/setup/SetupCodeEntry.d.ts +15 -0
  109. package/lib/setup/SetupCodeEntry.d.ts.map +1 -0
  110. package/lib/setup/SetupCodeEntry.js +198 -0
  111. package/lib/setup/SetupCodeEntry.js.map +1 -0
  112. package/lib/setup/SetupController.d.ts +29 -0
  113. package/lib/setup/SetupController.d.ts.map +1 -0
  114. package/lib/setup/SetupController.js +189 -0
  115. package/lib/setup/SetupController.js.map +1 -0
  116. package/lib/setup/SetupOverlay.d.ts +10 -0
  117. package/lib/setup/SetupOverlay.d.ts.map +1 -0
  118. package/lib/setup/SetupOverlay.js +408 -0
  119. package/lib/setup/SetupOverlay.js.map +1 -0
  120. package/lib/setup/TapInterceptor.d.ts +19 -0
  121. package/lib/setup/TapInterceptor.d.ts.map +1 -0
  122. package/lib/setup/TapInterceptor.js +98 -0
  123. package/lib/setup/TapInterceptor.js.map +1 -0
  124. package/lib/setup/TouchObserver.d.ts +25 -0
  125. package/lib/setup/TouchObserver.d.ts.map +1 -0
  126. package/lib/setup/TouchObserver.js +88 -0
  127. package/lib/setup/TouchObserver.js.map +1 -0
  128. package/lib/setup/TouchPatcher.d.ts +27 -0
  129. package/lib/setup/TouchPatcher.d.ts.map +1 -0
  130. package/lib/setup/TouchPatcher.js +182 -0
  131. package/lib/setup/TouchPatcher.js.map +1 -0
  132. package/lib/setup/index.d.ts +13 -0
  133. package/lib/setup/index.d.ts.map +1 -0
  134. package/lib/setup/index.js +45 -0
  135. package/lib/setup/index.js.map +1 -0
  136. package/lib/setup/useCXPElement.d.ts +25 -0
  137. package/lib/setup/useCXPElement.d.ts.map +1 -0
  138. package/lib/setup/useCXPElement.js +33 -0
  139. package/lib/setup/useCXPElement.js.map +1 -0
  140. package/lib/survey/SurveyModal.d.ts +10 -0
  141. package/lib/survey/SurveyModal.d.ts.map +1 -0
  142. package/lib/survey/SurveyModal.js +1650 -0
  143. package/lib/survey/SurveyModal.js.map +1 -0
  144. package/lib/survey/expressionEval.d.ts +10 -0
  145. package/lib/survey/expressionEval.d.ts.map +1 -0
  146. package/lib/survey/expressionEval.js +287 -0
  147. package/lib/survey/expressionEval.js.map +1 -0
  148. package/lib/survey/frequencyCap.d.ts +15 -0
  149. package/lib/survey/frequencyCap.d.ts.map +1 -0
  150. package/lib/survey/frequencyCap.js +116 -0
  151. package/lib/survey/frequencyCap.js.map +1 -0
  152. package/lib/survey/handsFreeVoice.d.ts +30 -0
  153. package/lib/survey/handsFreeVoice.d.ts.map +1 -0
  154. package/lib/survey/handsFreeVoice.js +312 -0
  155. package/lib/survey/handsFreeVoice.js.map +1 -0
  156. package/lib/survey/interpolate.d.ts +6 -0
  157. package/lib/survey/interpolate.d.ts.map +1 -0
  158. package/lib/survey/interpolate.js +61 -0
  159. package/lib/survey/interpolate.js.map +1 -0
  160. package/lib/survey/pagesFromV1.d.ts +6 -0
  161. package/lib/survey/pagesFromV1.d.ts.map +1 -0
  162. package/lib/survey/pagesFromV1.js +83 -0
  163. package/lib/survey/pagesFromV1.js.map +1 -0
  164. package/lib/survey/questions/CSATQuestion.d.ts +16 -0
  165. package/lib/survey/questions/CSATQuestion.d.ts.map +1 -0
  166. package/lib/survey/questions/CSATQuestion.js +37 -0
  167. package/lib/survey/questions/CSATQuestion.js.map +1 -0
  168. package/lib/survey/questions/DateQuestion.d.ts +20 -0
  169. package/lib/survey/questions/DateQuestion.d.ts.map +1 -0
  170. package/lib/survey/questions/DateQuestion.js +103 -0
  171. package/lib/survey/questions/DateQuestion.js.map +1 -0
  172. package/lib/survey/questions/ImagePickerQuestion.d.ts +33 -0
  173. package/lib/survey/questions/ImagePickerQuestion.d.ts.map +1 -0
  174. package/lib/survey/questions/ImagePickerQuestion.js +68 -0
  175. package/lib/survey/questions/ImagePickerQuestion.js.map +1 -0
  176. package/lib/survey/questions/ImageQuestion.d.ts +26 -0
  177. package/lib/survey/questions/ImageQuestion.d.ts.map +1 -0
  178. package/lib/survey/questions/ImageQuestion.js +24 -0
  179. package/lib/survey/questions/ImageQuestion.js.map +1 -0
  180. package/lib/survey/questions/MatrixCell.d.ts +45 -0
  181. package/lib/survey/questions/MatrixCell.d.ts.map +1 -0
  182. package/lib/survey/questions/MatrixCell.js +54 -0
  183. package/lib/survey/questions/MatrixCell.js.map +1 -0
  184. package/lib/survey/questions/MatrixDropdownQuestion.d.ts +29 -0
  185. package/lib/survey/questions/MatrixDropdownQuestion.d.ts.map +1 -0
  186. package/lib/survey/questions/MatrixDropdownQuestion.js +42 -0
  187. package/lib/survey/questions/MatrixDropdownQuestion.js.map +1 -0
  188. package/lib/survey/questions/MatrixDynamicQuestion.d.ts +26 -0
  189. package/lib/survey/questions/MatrixDynamicQuestion.d.ts.map +1 -0
  190. package/lib/survey/questions/MatrixDynamicQuestion.js +52 -0
  191. package/lib/survey/questions/MatrixDynamicQuestion.js.map +1 -0
  192. package/lib/survey/questions/MatrixQuestion.d.ts +38 -0
  193. package/lib/survey/questions/MatrixQuestion.d.ts.map +1 -0
  194. package/lib/survey/questions/MatrixQuestion.js +64 -0
  195. package/lib/survey/questions/MatrixQuestion.js.map +1 -0
  196. package/lib/survey/questions/MultiTextQuestion.d.ts +19 -0
  197. package/lib/survey/questions/MultiTextQuestion.d.ts.map +1 -0
  198. package/lib/survey/questions/MultiTextQuestion.js +64 -0
  199. package/lib/survey/questions/MultiTextQuestion.js.map +1 -0
  200. package/lib/survey/questions/MultipleChoiceQuestion.d.ts +25 -0
  201. package/lib/survey/questions/MultipleChoiceQuestion.d.ts.map +1 -0
  202. package/lib/survey/questions/MultipleChoiceQuestion.js +111 -0
  203. package/lib/survey/questions/MultipleChoiceQuestion.js.map +1 -0
  204. package/lib/survey/questions/NPSQuestion.d.ts +18 -0
  205. package/lib/survey/questions/NPSQuestion.d.ts.map +1 -0
  206. package/lib/survey/questions/NPSQuestion.js +62 -0
  207. package/lib/survey/questions/NPSQuestion.js.map +1 -0
  208. package/lib/survey/questions/PanelDynamicQuestion.d.ts +50 -0
  209. package/lib/survey/questions/PanelDynamicQuestion.d.ts.map +1 -0
  210. package/lib/survey/questions/PanelDynamicQuestion.js +62 -0
  211. package/lib/survey/questions/PanelDynamicQuestion.js.map +1 -0
  212. package/lib/survey/questions/RankingQuestion.d.ts +34 -0
  213. package/lib/survey/questions/RankingQuestion.d.ts.map +1 -0
  214. package/lib/survey/questions/RankingQuestion.js +85 -0
  215. package/lib/survey/questions/RankingQuestion.js.map +1 -0
  216. package/lib/survey/questions/RatingQuestion.d.ts +20 -0
  217. package/lib/survey/questions/RatingQuestion.d.ts.map +1 -0
  218. package/lib/survey/questions/RatingQuestion.js +53 -0
  219. package/lib/survey/questions/RatingQuestion.js.map +1 -0
  220. package/lib/survey/questions/TextQuestion.d.ts +17 -0
  221. package/lib/survey/questions/TextQuestion.d.ts.map +1 -0
  222. package/lib/survey/questions/TextQuestion.js +46 -0
  223. package/lib/survey/questions/TextQuestion.js.map +1 -0
  224. package/lib/survey/styleContract.d.ts +230 -0
  225. package/lib/survey/styleContract.d.ts.map +1 -0
  226. package/lib/survey/styleContract.js +365 -0
  227. package/lib/survey/styleContract.js.map +1 -0
  228. package/lib/survey/themePalette.d.ts +56 -0
  229. package/lib/survey/themePalette.d.ts.map +1 -0
  230. package/lib/survey/themePalette.js +63 -0
  231. package/lib/survey/themePalette.js.map +1 -0
  232. package/lib/survey/translationLookup.d.ts +4 -0
  233. package/lib/survey/translationLookup.d.ts.map +1 -0
  234. package/lib/survey/translationLookup.js +62 -0
  235. package/lib/survey/translationLookup.js.map +1 -0
  236. package/lib/survey/validate.d.ts +3 -0
  237. package/lib/survey/validate.d.ts.map +1 -0
  238. package/lib/survey/validate.js +159 -0
  239. package/lib/survey/validate.js.map +1 -0
  240. package/lib/survey/variablesResolver.d.ts +10 -0
  241. package/lib/survey/variablesResolver.d.ts.map +1 -0
  242. package/lib/survey/variablesResolver.js +107 -0
  243. package/lib/survey/variablesResolver.js.map +1 -0
  244. package/lib/survey/voiceParsers.d.ts +47 -0
  245. package/lib/survey/voiceParsers.d.ts.map +1 -0
  246. package/lib/survey/voiceParsers.js +310 -0
  247. package/lib/survey/voiceParsers.js.map +1 -0
  248. package/lib/types.d.ts +349 -0
  249. package/lib/types.d.ts.map +1 -0
  250. package/lib/types.js +20 -0
  251. package/lib/types.js.map +1 -0
  252. package/metro.js +55 -0
  253. package/package.json +78 -0
@@ -0,0 +1,831 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CXPinsight = void 0;
37
+ const types_1 = require("./types");
38
+ const storage = __importStar(require("./core/storage"));
39
+ const network_1 = require("./core/network");
40
+ const deviceInfo_1 = require("./core/deviceInfo");
41
+ const session_1 = require("./core/session");
42
+ const eventQueue_1 = require("./core/eventQueue");
43
+ const frequencyCap_1 = require("./survey/frequencyCap");
44
+ const variablesResolver_1 = require("./survey/variablesResolver");
45
+ const triggerEngine_1 = require("./core/triggerEngine");
46
+ const SetupController_1 = require("./setup/SetupController");
47
+ const recorder = __importStar(require("./core/mobileRecorder"));
48
+ const interactionObserver_1 = require("./instrumented/interactionObserver");
49
+ const lifecycle_1 = require("./core/lifecycle");
50
+ const networkCapture_1 = require("./core/networkCapture");
51
+ const scrollCapture_1 = require("./core/scrollCapture");
52
+ // Canonical eventType per event name — keeps mobile events aligned with the shared
53
+ // engagement taxonomy (docs/engagement-vocabulary-v1.md) instead of everything being
54
+ // 'user_action'. Unknown names fall back to 'user_action'.
55
+ const EVENT_TYPE_BY_NAME = {
56
+ screen_view: 'page_view', page_view: 'page_view',
57
+ click: 'click', button_click: 'click', link_click: 'click', element_click: 'click',
58
+ scroll: 'engagement', swipe: 'engagement', engagement: 'engagement',
59
+ form_start: 'form', form_submit: 'form', form_field: 'form', form_abandon: 'form',
60
+ app_opened: 'navigation', app_foregrounded: 'navigation', app_backgrounded: 'navigation',
61
+ app_installed: 'navigation', app_updated: 'navigation', session_start: 'navigation', first_open: 'navigation',
62
+ error: 'error', crash: 'error',
63
+ search: 'search', file_download: 'file', purchase: 'ecommerce', add_to_cart: 'ecommerce',
64
+ };
65
+ class CXPinsightSDK {
66
+ constructor() {
67
+ this.initialized = false;
68
+ this.config = null;
69
+ this.callbacks = {};
70
+ this.userId = '';
71
+ // In-memory mirror of the last identify() call. Storage layer is
72
+ // AsyncStorage (async I/O) so a synchronous accessor is needed for
73
+ // the variables resolver — which runs on every render tick.
74
+ // Rehydrated from storage on init() below.
75
+ this.userIdentity = null;
76
+ this.surveyPresenter = null;
77
+ this.setupPresenter = null;
78
+ // Unsubscribe for the auto-event interaction listener (taps/scroll/focus → events).
79
+ this.autoEventUnsub = null;
80
+ // Current + previous screen, so click/scroll/screen_view events carry screenName.
81
+ this.currentScreen = null;
82
+ this.previousScreen = null;
83
+ // Engagement clock — active foreground time this session (paused while backgrounded),
84
+ // attached to every event as engagementTimeMs (parity with web's time-on-page).
85
+ this.engagementStartTs = 0;
86
+ this.engagementAccumMs = 0;
87
+ this.engagementActive = false;
88
+ }
89
+ startEngagementClock() { this.engagementStartTs = Date.now(); this.engagementAccumMs = 0; this.engagementActive = true; }
90
+ pauseEngagementClock() { if (this.engagementActive) {
91
+ this.engagementAccumMs += Date.now() - this.engagementStartTs;
92
+ this.engagementActive = false;
93
+ } }
94
+ resumeEngagementClock() { if (!this.engagementActive) {
95
+ this.engagementStartTs = Date.now();
96
+ this.engagementActive = true;
97
+ } }
98
+ getEngagementMs() { return this.engagementAccumMs + (this.engagementActive ? Date.now() - this.engagementStartTs : 0); }
99
+ /** Initialize the SDK. Call once at app startup. */
100
+ async init(config, callbacks) {
101
+ if (this.initialized) {
102
+ this.log('Already initialized');
103
+ return;
104
+ }
105
+ if (!config.applicationId) {
106
+ throw new Error('[CXPinsight] init requires config.applicationId');
107
+ }
108
+ this.config = config;
109
+ (0, deviceInfo_1.setDeviceTheme)(config.theme); // host user's theme preference → deviceInfo.theme → device.theme
110
+ (0, deviceInfo_1.setDeviceBrandingPalette)(config.brandingPalette, config.primaryColor); // app palette + accent → deviceInfo.themePalette → reported-themes panel
111
+ this.callbacks = callbacks ?? {};
112
+ // Configure network layer
113
+ (0, network_1.configureNetwork)(config);
114
+ // P1 — server-driven SDK behavior. Fetch the Site's SdkConfig and fill any
115
+ // capture/session/batching setting the app didn't explicitly pass to init(),
116
+ // so behavior can change from the dashboard without an app redeploy. Client
117
+ // init() flags always win (only undefined fields are filled). Bounded + fail-
118
+ // safe: a slow/failed fetch leaves config untouched so init never blocks.
119
+ // Must run BEFORE initSession/startQueue/recording below consume `config`.
120
+ await this.applyServerSdkConfig(config);
121
+ // Restore or generate userId
122
+ const savedUserId = await storage.get(types_1.STORAGE_KEYS.USER_ID);
123
+ if (savedUserId) {
124
+ this.userId = savedUserId;
125
+ }
126
+ else {
127
+ this.userId = `cxp_user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
128
+ await storage.set(types_1.STORAGE_KEYS.USER_ID, this.userId);
129
+ }
130
+ // Rehydrate the identity blob so {{variable}} refs sourced from
131
+ // SDK identify() work on the first survey after a cold start —
132
+ // otherwise the user has to call identify() every launch for
133
+ // personalization to hold.
134
+ const savedIdentity = await storage.get(types_1.STORAGE_KEYS.USER_IDENTITY);
135
+ if (savedIdentity)
136
+ this.userIdentity = savedIdentity;
137
+ // Start session
138
+ await (0, session_1.initSession)(config.sessionTimeout);
139
+ // Start event queue
140
+ await (0, eventQueue_1.startQueue)({
141
+ flushInterval: config.flushInterval,
142
+ flushThreshold: config.flushThreshold,
143
+ debug: config.debug,
144
+ });
145
+ this.initialized = true;
146
+ this.log('Initialized', { applicationId: config.applicationId, userId: this.userId });
147
+ // Non-blocking connectivity self-check — surfaces a precise message on a bad apiBase
148
+ // (wrong port, a service instead of the gateway, or localhost on a physical device)
149
+ // instead of a generic "could not connect to server" later.
150
+ void this.verifyConnectivity(config.apiBase);
151
+ // Start mobile session (timeline) recording when opted in via config. Hosts
152
+ // that gate on their own consent UI should omit `record` and call
153
+ // CXPinsight.startRecording() after consent instead.
154
+ if (config.record) {
155
+ recorder.startRecording({
156
+ sessionId: (0, session_1.getSessionId)(),
157
+ userId: this.userId,
158
+ applicationId: config.applicationId,
159
+ sampleRate: config.recordingSampleRate,
160
+ maskInputs: config.recordMaskInputs,
161
+ captureScreen: config.captureScreen,
162
+ }).catch(err => this.log('Failed to start recording:', err));
163
+ }
164
+ // Load remote trigger configs (non-blocking).
165
+ (0, triggerEngine_1.loadTriggerConfigs)(config.applicationId, config.debug).catch(err => {
166
+ this.log('Failed to load trigger configs:', err);
167
+ });
168
+ // Restore setup mode if it was active when app was killed (non-blocking)
169
+ (0, SetupController_1.restoreSetupState)(config.debug).catch(err => {
170
+ this.log('Failed to restore setup state:', err);
171
+ });
172
+ // Auto-emit typed engagement events from native signals so a mobile session
173
+ // produces the same event vocabulary as web (docs/engagement-vocabulary-v1.md).
174
+ // No app code beyond the instrumented components + provider screenName.
175
+ this.startAutoEvents();
176
+ // Start the engagement clock and emit session_start for a genuinely new session
177
+ // (navigation, parity with web) — not on a restored/continued session.
178
+ this.startEngagementClock();
179
+ if ((0, session_1.wasSessionNew)()) {
180
+ // Cross-platform parity — first_visit (mobile equivalent of the web SDK's
181
+ // one). Absence of RETURNING_USER = brand-new install; emit first_visit
182
+ // BEFORE session_start (so downstream reads see the first-visit marker
183
+ // as part of the same session's initial event stream, matching web).
184
+ let isFirstVisit = false;
185
+ try {
186
+ const flag = await storage.get(types_1.STORAGE_KEYS.RETURNING_USER);
187
+ isFirstVisit = !flag;
188
+ if (isFirstVisit) {
189
+ this.track('first_visit', {}, 'navigation', false);
190
+ await storage.set(types_1.STORAGE_KEYS.RETURNING_USER, '1');
191
+ }
192
+ }
193
+ catch (e) { /* AsyncStorage failure — non-fatal, skip first_visit */ }
194
+ this.track('session_start', {
195
+ sessionId: (0, session_1.getSessionId)(),
196
+ is_first_visit: isFirstVisit,
197
+ }, 'navigation', false);
198
+ }
199
+ // When an inactive session rolls over to a new one, start a fresh recording so a
200
+ // session maps to one recording (prevents an ever-growing single recording).
201
+ (0, session_1.setRolloverHandler)((newSessionId, prevSessionId) => {
202
+ void this.onSessionRollover(newSessionId, prevSessionId);
203
+ });
204
+ }
205
+ /**
206
+ * P1 — merge the server's canonical SdkConfig into `config`, filling only the
207
+ * flat init() fields the app left undefined (client always wins). Bounded to a
208
+ * short timeout so a slow network never stalls init; any failure is swallowed
209
+ * and the app's config/defaults stand. Runs before the capture/session/queue
210
+ * consumers in init().
211
+ */
212
+ async applyServerSdkConfig(config) {
213
+ if (!config.applicationId)
214
+ return;
215
+ const scopeQs = `applicationId=${encodeURIComponent(config.applicationId)}`;
216
+ const timeout = new Promise((resolve) => setTimeout(() => resolve(null), 2500));
217
+ try {
218
+ const res = await Promise.race([
219
+ (0, network_1.sdkFetch)('GET', `/api/engagement/setup/sdk-config?${scopeQs}`),
220
+ timeout,
221
+ ]);
222
+ // sdkFetch already unwraps the HTTP layer into { success, data: <body> }, and
223
+ // the body is itself the API envelope { success, data: SrvCfg } — so the config
224
+ // sits one level deeper than `res.data`. Reading `res.data` directly (the prior
225
+ // behaviour) always yielded the envelope, so every field below resolved to
226
+ // undefined and no dashboard setting ever reached a mobile app.
227
+ // Accept either shape: prefer the nested envelope, fall back to a flat body so a
228
+ // future unwrapped endpoint keeps working.
229
+ const body = res && res.success ? res.data : null;
230
+ if (!body || typeof body !== 'object')
231
+ return;
232
+ const s = (body.data && typeof body.data === 'object')
233
+ ? body.data
234
+ : body;
235
+ if (!s || typeof s !== 'object')
236
+ return;
237
+ // Only fill fields the app didn't set — init() flags take precedence.
238
+ if (config.record === undefined && typeof s.recording?.enabled === 'boolean')
239
+ config.record = s.recording.enabled;
240
+ if (config.recordMaskInputs === undefined && typeof s.recording?.maskInputs === 'boolean')
241
+ config.recordMaskInputs = s.recording.maskInputs;
242
+ // Recording sample rate — always take the server's value if present. Unlike
243
+ // enabled/maskInputs there's no init() equivalent (sampling is a dashboard-side
244
+ // control), so this is the only source. Stashed on `config` so both the initial
245
+ // startRecording and the rollover path can read it without another fetch.
246
+ if (typeof s.recording?.sampleRate === 'number')
247
+ config.recordingSampleRate = s.recording.sampleRate;
248
+ if (config.captureNetwork === undefined && typeof s.capture?.network === 'boolean')
249
+ config.captureNetwork = s.capture.network;
250
+ if (config.sessionTimeout === undefined && typeof s.session?.timeoutMs === 'number')
251
+ config.sessionTimeout = s.session.timeoutMs;
252
+ if (config.flushInterval === undefined && typeof s.batching?.flushIntervalMs === 'number')
253
+ config.flushInterval = s.batching.flushIntervalMs;
254
+ if (config.flushThreshold === undefined && typeof s.batching?.flushThreshold === 'number')
255
+ config.flushThreshold = s.batching.flushThreshold;
256
+ this.log('Applied server SdkConfig');
257
+ }
258
+ catch (err) {
259
+ this.log('Server SdkConfig fetch failed — using init/defaults:', err);
260
+ }
261
+ }
262
+ /** Roll the recording + engagement clock onto a freshly-started session. */
263
+ async onSessionRollover(newSessionId, prevSessionId) {
264
+ this.log('Session rolled over (inactivity):', prevSessionId, '->', newSessionId);
265
+ if (this.config?.record) {
266
+ try {
267
+ await recorder.stopRecording(); // finalize the previous recording
268
+ await recorder.startRecording({
269
+ sessionId: newSessionId,
270
+ userId: this.userId,
271
+ applicationId: this.config.applicationId,
272
+ sampleRate: this.config.recordingSampleRate,
273
+ maskInputs: this.config.recordMaskInputs,
274
+ captureScreen: this.config.captureScreen,
275
+ });
276
+ }
277
+ catch (e) {
278
+ this.log('recording rollover failed', e);
279
+ }
280
+ }
281
+ this.startEngagementClock(); // reset engagement time for the new session
282
+ this.track('session_start', { sessionId: newSessionId }, 'navigation', false);
283
+ }
284
+ /** Wire native signals → typed analytics events (shared taxonomy). */
285
+ startAutoEvents() {
286
+ // Scroll depth from the app's ORGANIC <ScrollView>s (and FlatList /
287
+ // SectionList, which are built on one). Previously this required the app to
288
+ // import the SDK's instrumented ScrollView instead of React Native's —
289
+ // app-code restructuring the install snippet shouldn't demand. Without it
290
+ // `percent_scrolled` was always 0, so scroll-depth definitions never fired.
291
+ // Returns false if RN internals have moved; capture is simply off then.
292
+ if (!(0, scrollCapture_1.installScrollCapture)()) {
293
+ this.log('Scroll capture unavailable on this React Native version');
294
+ }
295
+ // Interactions → click / engagement / form events. tee=false: these are already
296
+ // in the recording as interaction frames, so don't duplicate them there.
297
+ this.autoEventUnsub = (0, interactionObserver_1.setInteractionListener)((e) => {
298
+ if ((0, SetupController_1.isSetupActive)())
299
+ return; // don't emit during element-mapping/setup
300
+ const r = e;
301
+ // Prefer the human-readable label for `element_text`; fall back to testID/
302
+ // text so at least SOMETHING is bindable even without an accessibilityLabel.
303
+ const label = (r.label ?? r.accessibilityLabel ?? r.text);
304
+ // Cross-platform normalization: emit the SAME wire shape the web SDK uses so
305
+ // one signature collapses web + mobile fires of the same interaction into a
306
+ // single observed-event row. Field naming matches services/unified-sdk/src/
307
+ // engagement.js:973-984 (element_text / element_tag / element_role / etc.).
308
+ const page = this.currentScreen ?? undefined;
309
+ switch (e.interactionType) {
310
+ case 'tap':
311
+ case 'long_press':
312
+ // Web click carries: element_text, element_tag, element_role,
313
+ // element_testid, element_id, element_selector, element_classes, link_url,
314
+ // link_domain, is_outbound. Mobile has direct analogues for the first
315
+ // five — populate them so a "element_testid = 'signup_btn'" condition
316
+ // matches on either channel.
317
+ this.track('click', {
318
+ element_text: label,
319
+ element_tag: r.elementType, // 'button' | 'text_input' | …
320
+ element_role: r.accessibilityRole, // 'button' | 'link' | …
321
+ element_testid: r.testID, // stable dev-assigned id
322
+ element_id: r.nativeID, // native id fallback
323
+ element_selector: r.hierarchyPath, // last-resort path (mobile analogue of CSS selector)
324
+ interaction_type: e.interactionType, // 'tap' | 'long_press' — internal detail
325
+ page,
326
+ }, 'click', false);
327
+ break;
328
+ case 'scroll':
329
+ case 'swipe': {
330
+ // ScrollView emits "<pct>%" in text. Field name matches web's
331
+ // `percent_scrolled` (engagement.js:1030) so a scroll-threshold
332
+ // condition works on either channel.
333
+ const pct = parseInt(String(r.text ?? '0'), 10);
334
+ this.track(e.interactionType, {
335
+ percent_scrolled: Number.isFinite(pct) ? pct : 0,
336
+ page,
337
+ }, 'engagement', false);
338
+ break;
339
+ }
340
+ case 'focus':
341
+ // Web's `form_start` fires only inside a real <form> element. Mobile
342
+ // has no <form> concept — any input focus was firing this event, which
343
+ // was semantically wrong and collided with web on the signature. Rename
344
+ // to `input_focus` (eventType `engagement`) so the two don't false-match.
345
+ this.track('input_focus', {
346
+ element_text: label,
347
+ element_testid: r.testID,
348
+ element_role: r.accessibilityRole,
349
+ page,
350
+ }, 'engagement', false);
351
+ break;
352
+ // value_change / blur → captured in the recording only, no analytics event
353
+ }
354
+ });
355
+ // App lifecycle → navigation events (app_opened / foregrounded / backgrounded).
356
+ // Also pause/resume the engagement clock so time-on-app excludes backgrounded time.
357
+ (0, lifecycle_1.startLifecycleTracking)((evt) => {
358
+ if (evt.type === 'app_backgrounded')
359
+ this.pauseEngagementClock();
360
+ else if (evt.type === 'app_foregrounded')
361
+ this.resumeEngagementClock();
362
+ this.track(evt.type, evt.data, 'navigation', false);
363
+ });
364
+ // Network capture → sanitized api_call events (method/host/path/status/duration).
365
+ // On by default; set captureNetwork:false to disable.
366
+ if (this.config?.captureNetwork !== false) {
367
+ this.log('Network capture enabled');
368
+ (0, networkCapture_1.startNetworkCapture)(this.config.apiBase, (call) => {
369
+ this.log('api_call captured:', call.method, call.path, call.status);
370
+ // Static eventName `api_call` (was `"${METHOD} ${PATH}"` — dynamic per URL,
371
+ // which exploded the observed-events catalog into thousands of unique
372
+ // rows). Method/host/path stay in eventData so conditions like
373
+ // `method = POST AND path contains /checkout` still work.
374
+ this.track('api_call', {
375
+ method: call.method, host: call.host, path: call.path,
376
+ status: call.status, durationMs: call.durationMs, success: call.success,
377
+ }, 'api_call', false);
378
+ });
379
+ }
380
+ // Uncaught JS errors → error events. Install once; chain the previous handler.
381
+ if (!CXPinsightSDK.errorHandlerInstalled) {
382
+ try {
383
+ const EU = globalThis.ErrorUtils;
384
+ if (EU?.setGlobalHandler) {
385
+ const prev = EU.getGlobalHandler?.();
386
+ EU.setGlobalHandler((error, isFatal) => {
387
+ try {
388
+ const err = error;
389
+ this.track('error', { message: String(err?.message ?? error), fatal: !!isFatal, stack: String(err?.stack ?? '').slice(0, 800) }, 'error', false);
390
+ }
391
+ catch { /* never let telemetry mask the real error */ }
392
+ prev?.(error, isFatal);
393
+ });
394
+ CXPinsightSDK.errorHandlerInstalled = true;
395
+ }
396
+ }
397
+ catch { /* ErrorUtils not available on this platform */ }
398
+ }
399
+ }
400
+ /** Track a custom event */
401
+ /**
402
+ * One-shot reachability check of `apiBase` at init. Logs a precise, actionable
403
+ * warning for the common misconfigurations (frontend port, a service instead of the
404
+ * gateway, or localhost on a physical device). Never throws or blocks init.
405
+ */
406
+ async verifyConnectivity(apiBase) {
407
+ const base = (apiBase || '').replace(/\/+$/, '');
408
+ if (!base) {
409
+ console.warn('[CXPinsight] apiBase is not set — point it at your CXPinsight gateway URL (e.g. http://<your-LAN-IP>:3000). No data will be sent.');
410
+ return;
411
+ }
412
+ if (/\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
413
+ console.warn(`[CXPinsight] apiBase is "${base}". In a native app "localhost" is the device itself — on a physical device use your machine's LAN IP instead (e.g. http://192.168.x.x:3000).`);
414
+ }
415
+ try {
416
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
417
+ const timer = controller ? setTimeout(() => controller.abort(), 5000) : undefined;
418
+ const res = await fetch(`${base}/health`, { method: 'GET', signal: controller?.signal });
419
+ if (timer)
420
+ clearTimeout(timer);
421
+ const type = res.headers.get('content-type') || '';
422
+ if (!type.includes('application/json')) {
423
+ console.warn(`[CXPinsight] apiBase "${base}" returned ${type || 'a non-JSON response'} from /health — this looks like a frontend/dev server, not the CXPinsight gateway. Point apiBase at the gateway (usually :3000).`);
424
+ return;
425
+ }
426
+ const body = await res.json().catch(() => null);
427
+ if (body && body.service && body.service !== 'gateway') {
428
+ console.warn(`[CXPinsight] apiBase "${base}" points at the "${body.service}" service, not the gateway. Point apiBase at your CXPinsight gateway (usually :3000) so /api/engagement routes resolve.`);
429
+ return;
430
+ }
431
+ this.log('Connectivity OK →', base);
432
+ }
433
+ catch (err) {
434
+ const msg = err?.message || String(err);
435
+ console.warn(`[CXPinsight] Could not reach apiBase "${base}" (${msg}). It must be your CXPinsight gateway (usually :3000), reachable from the device — on a physical device use your machine's LAN IP, not localhost.`);
436
+ }
437
+ }
438
+ track(eventName, data, eventType, teeToRecording = true) {
439
+ if (!this.initialized || !this.config) {
440
+ console.warn('[CXPinsight] track() called but SDK not initialized — call ignored. Event:', eventName);
441
+ return;
442
+ }
443
+ (0, session_1.touchSession)();
444
+ const device = (0, deviceInfo_1.getDeviceInfo)();
445
+ const now = new Date().toISOString();
446
+ // Explicit type wins; otherwise derive from the shared taxonomy by name.
447
+ const resolvedType = eventType || EVENT_TYPE_BY_NAME[eventName] || 'user_action';
448
+ // Identified user traits (from identify()) to stamp on EVERY event — including the
449
+ // auto-captured taps/scrolls from the instrumented setup, which route through track().
450
+ // Excludes `id` (that's the userId). Omitted entirely until identify() is called.
451
+ let userProperties;
452
+ if (this.userIdentity) {
453
+ const { id: _id, ...traits } = this.userIdentity;
454
+ if (Object.keys(traits).length)
455
+ userProperties = traits;
456
+ }
457
+ const event = {
458
+ userId: this.userId,
459
+ sessionId: (0, session_1.getSessionId)(),
460
+ applicationId: this.config.applicationId,
461
+ eventType: resolvedType,
462
+ eventName,
463
+ // No redundant `context` blob — device/os/screen/etc. are on `deviceInfo`
464
+ // (canonical shape), so eventData carries only event-specific attributes.
465
+ eventData: {
466
+ source: 'cxpinsight_mobile_sdk',
467
+ ...data,
468
+ },
469
+ deviceInfo: device,
470
+ timestamp: now,
471
+ // Active foreground time this session (parity with web's engagementTimeMs).
472
+ engagementTimeMs: this.getEngagementMs(),
473
+ ...(userProperties ? { userProperties } : {}),
474
+ };
475
+ (0, eventQueue_1.enqueue)(event);
476
+ this.callbacks.onEventTracked?.(eventName);
477
+ // Tee into the session recording timeline as an 'event' frame — but only for
478
+ // custom business events. Auto-events (click/scroll/screen_view) are already in
479
+ // the recording as interaction/screen frames, so teeing them would duplicate.
480
+ if (teeToRecording && recorder.isRecording() && eventName !== 'screen_view') {
481
+ recorder.recordFrame('event', { name: eventName });
482
+ }
483
+ // P2c — evaluate targeting conditions against the full ATTRIBUTE CONTEXT, not
484
+ // just event data: bare event keys (legacy conditions like scrollPercent) PLUS
485
+ // the namespaced bag (event.* / device.* / user.* / session.*) so a rule can
486
+ // target `user.plan == 'pro'` or `device.os == 'iOS'`. geo.* is server-only and
487
+ // absent here — the trigger engine skips those (finalized server-side in P3).
488
+ const targetingContext = {
489
+ ...event.eventData,
490
+ ...(0, variablesResolver_1.buildAttributeContext)({
491
+ event: event.eventData,
492
+ // Canonical device.* keys (type/model/brand, lowercased) so the client-side
493
+ // targeting context matches the server's eventAttributes() vocabulary — a
494
+ // rule authored as `device.type == 'mobile'` then resolves in both tiers.
495
+ device: (0, deviceInfo_1.canonicalDeviceContext)(device),
496
+ user: userProperties,
497
+ session: { engagementMs: event.engagementTimeMs, id: event.sessionId },
498
+ }),
499
+ };
500
+ (0, triggerEngine_1.onEventTracked)(eventName, targetingContext);
501
+ }
502
+ /** Track a screen view (triggers remote page_view rules) */
503
+ trackScreen(screenName, data) {
504
+ // Roll current → previous so page_view (and downstream click/scroll) carry both.
505
+ this.previousScreen = this.currentScreen;
506
+ this.currentScreen = screenName;
507
+ if (recorder.isRecording())
508
+ recorder.recordScreen(screenName, this.previousScreen);
509
+ // Cross-platform normalization: emit as `page_view` (web's canonical navigation
510
+ // event name) so the eventSignature (eventType|page|elementKey) collapses web
511
+ // and mobile navigations to the same key when they share a page identifier.
512
+ // `page` is the canonical field the backend's eventPage() reads for the
513
+ // signature; screen_name is preserved as a mobile-side alias.
514
+ this.track('page_view', {
515
+ page: screenName,
516
+ screen_name: screenName,
517
+ previous_page: this.previousScreen ?? undefined,
518
+ ...data,
519
+ }, 'page_view', false);
520
+ (0, triggerEngine_1.onScreenView)(screenName).catch(err => {
521
+ console.error('[CXPinsight] onScreenView error:', err);
522
+ });
523
+ }
524
+ /** Identify the current user */
525
+ async identify(user) {
526
+ const prevId = this.userId;
527
+ // If we were anonymous, merge the pre-login activity into this user. Without this,
528
+ // events sent before identify() (session_start, first screen views, etc.) stay under
529
+ // the anonymous id and the visitor shows up as TWO customers with split event counts.
530
+ if (prevId && prevId !== user.id && prevId.startsWith('cxp_user_')) {
531
+ try {
532
+ // Flush queued anonymous events first so the backend has them to re-attribute.
533
+ await (0, eventQueue_1.flush)();
534
+ await (0, network_1.sdkFetch)('POST', '/api/engagement/events/alias', {
535
+ anonymousId: prevId,
536
+ userId: user.id,
537
+ });
538
+ this.log('Aliased anonymous id -> user', prevId, user.id);
539
+ }
540
+ catch (e) {
541
+ this.log('alias failed', e);
542
+ }
543
+ }
544
+ this.userId = user.id;
545
+ this.userIdentity = user;
546
+ await storage.set(types_1.STORAGE_KEYS.USER_ID, user.id);
547
+ await storage.set(types_1.STORAGE_KEYS.USER_IDENTITY, user);
548
+ this.log('User identified', user.id);
549
+ }
550
+ /** Track a funnel step */
551
+ trackFunnelStep(funnelId, stepNumber, stepName, options) {
552
+ if (!this.initialized || !this.config) {
553
+ console.warn('[CXPinsight] trackFunnelStep() called but SDK not initialized');
554
+ return;
555
+ }
556
+ (0, session_1.touchSession)();
557
+ const device = (0, deviceInfo_1.getDeviceInfo)();
558
+ const now = new Date().toISOString();
559
+ const opts = options ?? {};
560
+ const event = {
561
+ userId: this.userId,
562
+ sessionId: (0, session_1.getSessionId)(),
563
+ applicationId: this.config.applicationId,
564
+ eventType: 'conversion',
565
+ eventName: 'funnel_step',
566
+ eventData: {
567
+ source: 'cxpinsight_mobile_sdk',
568
+ },
569
+ deviceInfo: device,
570
+ timestamp: now,
571
+ eventTemplate: 'funnel_step',
572
+ funnel: {
573
+ funnel_id: funnelId,
574
+ funnel_name: opts.funnelName ?? funnelId,
575
+ step_number: stepNumber,
576
+ step_name: stepName ?? `Step ${stepNumber}`,
577
+ is_conversion: opts.isConversion,
578
+ },
579
+ value: opts.value,
580
+ currency: opts.currency,
581
+ };
582
+ (0, eventQueue_1.enqueue)(event);
583
+ this.callbacks.onEventTracked?.('funnel_step');
584
+ this.log('Funnel step tracked:', funnelId, stepNumber);
585
+ }
586
+ /** Track an experiment impression */
587
+ trackExperiment(experimentId, variantId, options) {
588
+ if (!this.initialized || !this.config) {
589
+ console.warn('[CXPinsight] trackExperiment() called but SDK not initialized');
590
+ return;
591
+ }
592
+ (0, session_1.touchSession)();
593
+ const device = (0, deviceInfo_1.getDeviceInfo)();
594
+ const now = new Date().toISOString();
595
+ const opts = options ?? {};
596
+ const event = {
597
+ userId: this.userId,
598
+ sessionId: (0, session_1.getSessionId)(),
599
+ applicationId: this.config.applicationId,
600
+ eventType: 'experiment',
601
+ eventName: 'experiment_impression',
602
+ eventData: {
603
+ source: 'cxpinsight_mobile_sdk',
604
+ },
605
+ deviceInfo: device,
606
+ timestamp: now,
607
+ eventTemplate: 'experiment_impression',
608
+ experiment: {
609
+ experiment_id: experimentId,
610
+ experiment_name: opts.experimentName ?? experimentId,
611
+ variant_id: variantId,
612
+ variant_name: opts.variantName ?? variantId,
613
+ assignment_method: opts.assignmentMethod ?? 'random',
614
+ },
615
+ };
616
+ (0, eventQueue_1.enqueue)(event);
617
+ this.callbacks.onEventTracked?.('experiment_impression');
618
+ this.log('Experiment tracked:', experimentId, variantId);
619
+ }
620
+ /** Track a conversion event */
621
+ trackConversion(eventName, options) {
622
+ if (!this.initialized || !this.config) {
623
+ console.warn('[CXPinsight] trackConversion() called but SDK not initialized');
624
+ return;
625
+ }
626
+ (0, session_1.touchSession)();
627
+ const device = (0, deviceInfo_1.getDeviceInfo)();
628
+ const now = new Date().toISOString();
629
+ const opts = options ?? {};
630
+ const event = {
631
+ userId: this.userId,
632
+ sessionId: (0, session_1.getSessionId)(),
633
+ applicationId: this.config.applicationId,
634
+ eventType: 'conversion',
635
+ eventName,
636
+ eventData: {
637
+ source: 'cxpinsight_mobile_sdk',
638
+ },
639
+ deviceInfo: device,
640
+ timestamp: now,
641
+ value: opts.value,
642
+ currency: opts.currency,
643
+ };
644
+ // Attach experiment context if provided
645
+ if (opts.experimentId) {
646
+ event.experiment = {
647
+ experiment_id: opts.experimentId,
648
+ experiment_name: opts.experimentName ?? opts.experimentId,
649
+ variant_id: opts.variantId ?? '',
650
+ variant_name: opts.variantName ?? '',
651
+ };
652
+ }
653
+ // Attach funnel context if provided
654
+ if (opts.funnelId) {
655
+ event.funnel = {
656
+ funnel_id: opts.funnelId,
657
+ funnel_name: opts.funnelName ?? opts.funnelId,
658
+ step_number: opts.stepNumber ?? 1,
659
+ step_name: opts.stepName ?? 'Conversion',
660
+ is_conversion: true,
661
+ };
662
+ }
663
+ (0, eventQueue_1.enqueue)(event);
664
+ this.callbacks.onEventTracked?.(eventName);
665
+ this.log('Conversion tracked:', eventName, opts);
666
+ }
667
+ // ── Setup Mode API ──
668
+ /** Enter setup mode. Shows the setup code entry UI. */
669
+ enterSetupMode() {
670
+ if (!this.initialized || !this.config) {
671
+ console.warn('[CXPinsight] enterSetupMode() called but SDK not initialized');
672
+ return;
673
+ }
674
+ if (this.setupPresenter) {
675
+ this.setupPresenter(true);
676
+ }
677
+ else {
678
+ this.log('No setup presenter registered. Wrap your app with <CXPinsightProvider>.');
679
+ }
680
+ }
681
+ /** Exit setup mode and clean up. */
682
+ async exitSetupMode() {
683
+ await (0, SetupController_1.endSetup)();
684
+ if (this.setupPresenter) {
685
+ this.setupPresenter(false);
686
+ }
687
+ }
688
+ /** Check if setup mode is currently active. */
689
+ isInSetupMode() {
690
+ return (0, SetupController_1.isSetupActive)();
691
+ }
692
+ /** Get the current setup session state. */
693
+ getSetupState() {
694
+ return (0, SetupController_1.getSetupState)();
695
+ }
696
+ /** Get the stable device ID (generates one if first time). */
697
+ async getDeviceId() {
698
+ return (0, SetupController_1.getDeviceId)();
699
+ }
700
+ /** Programmatically show a survey */
701
+ showSurvey(surveyId) {
702
+ if (!this.surveyPresenter) {
703
+ this.log('No survey presenter registered. Wrap your app with <CXPinsightProvider>.');
704
+ return;
705
+ }
706
+ this.surveyPresenter(surveyId);
707
+ }
708
+ /**
709
+ * Start mobile session (timeline) recording. Use this when gating on your own
710
+ * consent UI: show your banner, then call after the user accepts. No-op if
711
+ * already recording or the SDK isn't initialized.
712
+ */
713
+ async startRecording() {
714
+ if (!this.initialized || !this.config) {
715
+ console.warn('[CXPinsight] startRecording() called but SDK not initialized — ignored');
716
+ return;
717
+ }
718
+ await recorder.startRecording({
719
+ sessionId: (0, session_1.getSessionId)(),
720
+ userId: this.userId,
721
+ applicationId: this.config.applicationId,
722
+ sampleRate: this.config.recordingSampleRate,
723
+ maskInputs: this.config.recordMaskInputs,
724
+ captureScreen: this.config.captureScreen,
725
+ });
726
+ }
727
+ /** Stop mobile session recording and flush the final chunk. */
728
+ async stopRecording() {
729
+ await recorder.stopRecording();
730
+ }
731
+ /** Reset all SDK state (call on logout) */
732
+ async reset() {
733
+ this.log('Resetting SDK state');
734
+ if (this.autoEventUnsub) {
735
+ this.autoEventUnsub();
736
+ this.autoEventUnsub = null;
737
+ }
738
+ (0, session_1.setRolloverHandler)(null);
739
+ (0, lifecycle_1.stopLifecycleTracking)();
740
+ (0, networkCapture_1.stopNetworkCapture)();
741
+ this.engagementActive = false;
742
+ this.engagementAccumMs = 0;
743
+ this.currentScreen = null;
744
+ this.previousScreen = null;
745
+ await recorder.stopRecording();
746
+ await (0, eventQueue_1.flush)();
747
+ await (0, eventQueue_1.stopQueue)();
748
+ await (0, session_1.destroySession)();
749
+ await (0, frequencyCap_1.clearAllCaps)();
750
+ await (0, triggerEngine_1.clearTriggerState)();
751
+ await (0, SetupController_1.clearSetupState)();
752
+ await storage.multiRemove([
753
+ types_1.STORAGE_KEYS.USER_ID,
754
+ types_1.STORAGE_KEYS.USER_IDENTITY,
755
+ types_1.STORAGE_KEYS.SESSION,
756
+ types_1.STORAGE_KEYS.EVENT_QUEUE,
757
+ ]);
758
+ (0, deviceInfo_1.clearDeviceInfoCache)();
759
+ (0, network_1.resetNetwork)();
760
+ this.userId = '';
761
+ this.initialized = false;
762
+ this.config = null;
763
+ this.callbacks = {};
764
+ }
765
+ /** Manual flush for testing */
766
+ async flush() {
767
+ await (0, eventQueue_1.flush)();
768
+ }
769
+ /** @internal Register the survey presenter from CXPinsightProvider */
770
+ _registerSurveyPresenter(presenter) {
771
+ this.surveyPresenter = presenter;
772
+ }
773
+ /** @internal Unregister the survey presenter */
774
+ _unregisterSurveyPresenter() {
775
+ this.surveyPresenter = null;
776
+ }
777
+ /** @internal Register the setup presenter from CXPinsightProvider */
778
+ _registerSetupPresenter(presenter) {
779
+ this.setupPresenter = presenter;
780
+ }
781
+ /** @internal Unregister the setup presenter */
782
+ _unregisterSetupPresenter() {
783
+ this.setupPresenter = null;
784
+ }
785
+ /** @internal Get callbacks */
786
+ _getCallbacks() {
787
+ return this.callbacks;
788
+ }
789
+ /** @internal Get userId */
790
+ _getUserId() {
791
+ return this.userId;
792
+ }
793
+ /** @internal Get the full identity blob (or null if identify() was
794
+ * never called). Consumed by the variables resolver so
795
+ * {source: 'sdk'} v1.1 variables interpolate to identify()-supplied
796
+ * values. Return type kept broad ({[k]:unknown}) because the caller
797
+ * looks up variable-name keys against it, not typed properties. */
798
+ _getUserIdentity() {
799
+ return this.userIdentity;
800
+ }
801
+ /** @internal Get config */
802
+ _getConfig() {
803
+ return this.config;
804
+ }
805
+ /** @internal Host-supplied palette override from CXPinsight.init({ brandingPalette }).
806
+ * Read by SurveyModal to layer over survey.branding.palette + defaults.
807
+ * Returns null when the app didn't pass one — palette resolver falls back. */
808
+ _getInitBrandingPalette() {
809
+ const bp = this.config?.brandingPalette;
810
+ if (!bp || typeof bp !== 'object')
811
+ return null;
812
+ // Cast: types.ts declares this loosely (Partial<Record<string,string>>),
813
+ // but SurveyModal expects a concrete object shape. Runtime values are
814
+ // string-keyed maps of hex/rgba color strings.
815
+ return bp;
816
+ }
817
+ /** @internal Check initialization */
818
+ _isInitialized() {
819
+ return this.initialized;
820
+ }
821
+ log(...args) {
822
+ if (this.config?.debug) {
823
+ console.log('[CXPinsight]', ...args);
824
+ }
825
+ }
826
+ }
827
+ // The global JS error handler is installed once for the process lifetime.
828
+ CXPinsightSDK.errorHandlerInstalled = false;
829
+ /** Singleton SDK instance */
830
+ exports.CXPinsight = new CXPinsightSDK();
831
+ //# sourceMappingURL=CXPinsight.js.map