@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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/babel.js +101 -0
- package/instrumented/Pressable.d.ts +2 -0
- package/instrumented/Pressable.js +2 -0
- package/instrumented/ScrollView.d.ts +2 -0
- package/instrumented/ScrollView.js +2 -0
- package/instrumented/TextInput.d.ts +2 -0
- package/instrumented/TextInput.js +2 -0
- package/instrumented/TouchableOpacity.d.ts +2 -0
- package/instrumented/TouchableOpacity.js +2 -0
- package/lib/CXPinsight.d.ts +113 -0
- package/lib/CXPinsight.d.ts.map +1 -0
- package/lib/CXPinsight.js +831 -0
- package/lib/CXPinsight.js.map +1 -0
- package/lib/CXPinsightProvider.d.ts +19 -0
- package/lib/CXPinsightProvider.d.ts.map +1 -0
- package/lib/CXPinsightProvider.js +382 -0
- package/lib/CXPinsightProvider.js.map +1 -0
- package/lib/core/autoScreenTracker.d.ts +20 -0
- package/lib/core/autoScreenTracker.d.ts.map +1 -0
- package/lib/core/autoScreenTracker.js +66 -0
- package/lib/core/autoScreenTracker.js.map +1 -0
- package/lib/core/deepLinkTracker.d.ts +17 -0
- package/lib/core/deepLinkTracker.d.ts.map +1 -0
- package/lib/core/deepLinkTracker.js +81 -0
- package/lib/core/deepLinkTracker.js.map +1 -0
- package/lib/core/deviceInfo.d.ts +14 -0
- package/lib/core/deviceInfo.d.ts.map +1 -0
- package/lib/core/deviceInfo.js +166 -0
- package/lib/core/deviceInfo.js.map +1 -0
- package/lib/core/eventQueue.d.ts +23 -0
- package/lib/core/eventQueue.d.ts.map +1 -0
- package/lib/core/eventQueue.js +240 -0
- package/lib/core/eventQueue.js.map +1 -0
- package/lib/core/lifecycle.d.ts +18 -0
- package/lib/core/lifecycle.d.ts.map +1 -0
- package/lib/core/lifecycle.js +127 -0
- package/lib/core/lifecycle.js.map +1 -0
- package/lib/core/mobileRecorder.d.ts +15 -0
- package/lib/core/mobileRecorder.d.ts.map +1 -0
- package/lib/core/mobileRecorder.js +231 -0
- package/lib/core/mobileRecorder.js.map +1 -0
- package/lib/core/network.d.ts +10 -0
- package/lib/core/network.d.ts.map +1 -0
- package/lib/core/network.js +74 -0
- package/lib/core/network.js.map +1 -0
- package/lib/core/networkCapture.d.ts +22 -0
- package/lib/core/networkCapture.d.ts.map +1 -0
- package/lib/core/networkCapture.js +100 -0
- package/lib/core/networkCapture.js.map +1 -0
- package/lib/core/scrollCapture.d.ts +48 -0
- package/lib/core/scrollCapture.d.ts.map +1 -0
- package/lib/core/scrollCapture.js +221 -0
- package/lib/core/scrollCapture.js.map +1 -0
- package/lib/core/session.d.ts +15 -0
- package/lib/core/session.d.ts.map +1 -0
- package/lib/core/session.js +157 -0
- package/lib/core/session.js.map +1 -0
- package/lib/core/storage.d.ts +11 -0
- package/lib/core/storage.d.ts.map +1 -0
- package/lib/core/storage.js +59 -0
- package/lib/core/storage.js.map +1 -0
- package/lib/core/triggerEngine.d.ts +56 -0
- package/lib/core/triggerEngine.d.ts.map +1 -0
- package/lib/core/triggerEngine.js +432 -0
- package/lib/core/triggerEngine.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/lib/instrumented/Pressable.d.ts +9 -0
- package/lib/instrumented/Pressable.d.ts.map +1 -0
- package/lib/instrumented/Pressable.js +96 -0
- package/lib/instrumented/Pressable.js.map +1 -0
- package/lib/instrumented/ScrollView.d.ts +6 -0
- package/lib/instrumented/ScrollView.d.ts.map +1 -0
- package/lib/instrumented/ScrollView.js +101 -0
- package/lib/instrumented/ScrollView.js.map +1 -0
- package/lib/instrumented/TextInput.d.ts +9 -0
- package/lib/instrumented/TextInput.d.ts.map +1 -0
- package/lib/instrumented/TextInput.js +75 -0
- package/lib/instrumented/TextInput.js.map +1 -0
- package/lib/instrumented/TouchableOpacity.d.ts +11 -0
- package/lib/instrumented/TouchableOpacity.d.ts.map +1 -0
- package/lib/instrumented/TouchableOpacity.js +96 -0
- package/lib/instrumented/TouchableOpacity.js.map +1 -0
- package/lib/instrumented/index.d.ts +19 -0
- package/lib/instrumented/index.d.ts.map +1 -0
- package/lib/instrumented/index.js +28 -0
- package/lib/instrumented/index.js.map +1 -0
- package/lib/instrumented/interactionObserver.d.ts +29 -0
- package/lib/instrumented/interactionObserver.d.ts.map +1 -0
- package/lib/instrumented/interactionObserver.js +38 -0
- package/lib/instrumented/interactionObserver.js.map +1 -0
- package/lib/setup/AccessibilityScanner.d.ts +32 -0
- package/lib/setup/AccessibilityScanner.d.ts.map +1 -0
- package/lib/setup/AccessibilityScanner.js +127 -0
- package/lib/setup/AccessibilityScanner.js.map +1 -0
- package/lib/setup/ElementIdentifier.d.ts +65 -0
- package/lib/setup/ElementIdentifier.d.ts.map +1 -0
- package/lib/setup/ElementIdentifier.js +114 -0
- package/lib/setup/ElementIdentifier.js.map +1 -0
- package/lib/setup/ElementRegistry.d.ts +22 -0
- package/lib/setup/ElementRegistry.d.ts.map +1 -0
- package/lib/setup/ElementRegistry.js +47 -0
- package/lib/setup/ElementRegistry.js.map +1 -0
- package/lib/setup/SetupCodeEntry.d.ts +15 -0
- package/lib/setup/SetupCodeEntry.d.ts.map +1 -0
- package/lib/setup/SetupCodeEntry.js +198 -0
- package/lib/setup/SetupCodeEntry.js.map +1 -0
- package/lib/setup/SetupController.d.ts +29 -0
- package/lib/setup/SetupController.d.ts.map +1 -0
- package/lib/setup/SetupController.js +189 -0
- package/lib/setup/SetupController.js.map +1 -0
- package/lib/setup/SetupOverlay.d.ts +10 -0
- package/lib/setup/SetupOverlay.d.ts.map +1 -0
- package/lib/setup/SetupOverlay.js +408 -0
- package/lib/setup/SetupOverlay.js.map +1 -0
- package/lib/setup/TapInterceptor.d.ts +19 -0
- package/lib/setup/TapInterceptor.d.ts.map +1 -0
- package/lib/setup/TapInterceptor.js +98 -0
- package/lib/setup/TapInterceptor.js.map +1 -0
- package/lib/setup/TouchObserver.d.ts +25 -0
- package/lib/setup/TouchObserver.d.ts.map +1 -0
- package/lib/setup/TouchObserver.js +88 -0
- package/lib/setup/TouchObserver.js.map +1 -0
- package/lib/setup/TouchPatcher.d.ts +27 -0
- package/lib/setup/TouchPatcher.d.ts.map +1 -0
- package/lib/setup/TouchPatcher.js +182 -0
- package/lib/setup/TouchPatcher.js.map +1 -0
- package/lib/setup/index.d.ts +13 -0
- package/lib/setup/index.d.ts.map +1 -0
- package/lib/setup/index.js +45 -0
- package/lib/setup/index.js.map +1 -0
- package/lib/setup/useCXPElement.d.ts +25 -0
- package/lib/setup/useCXPElement.d.ts.map +1 -0
- package/lib/setup/useCXPElement.js +33 -0
- package/lib/setup/useCXPElement.js.map +1 -0
- package/lib/survey/SurveyModal.d.ts +10 -0
- package/lib/survey/SurveyModal.d.ts.map +1 -0
- package/lib/survey/SurveyModal.js +1650 -0
- package/lib/survey/SurveyModal.js.map +1 -0
- package/lib/survey/expressionEval.d.ts +10 -0
- package/lib/survey/expressionEval.d.ts.map +1 -0
- package/lib/survey/expressionEval.js +287 -0
- package/lib/survey/expressionEval.js.map +1 -0
- package/lib/survey/frequencyCap.d.ts +15 -0
- package/lib/survey/frequencyCap.d.ts.map +1 -0
- package/lib/survey/frequencyCap.js +116 -0
- package/lib/survey/frequencyCap.js.map +1 -0
- package/lib/survey/handsFreeVoice.d.ts +30 -0
- package/lib/survey/handsFreeVoice.d.ts.map +1 -0
- package/lib/survey/handsFreeVoice.js +312 -0
- package/lib/survey/handsFreeVoice.js.map +1 -0
- package/lib/survey/interpolate.d.ts +6 -0
- package/lib/survey/interpolate.d.ts.map +1 -0
- package/lib/survey/interpolate.js +61 -0
- package/lib/survey/interpolate.js.map +1 -0
- package/lib/survey/pagesFromV1.d.ts +6 -0
- package/lib/survey/pagesFromV1.d.ts.map +1 -0
- package/lib/survey/pagesFromV1.js +83 -0
- package/lib/survey/pagesFromV1.js.map +1 -0
- package/lib/survey/questions/CSATQuestion.d.ts +16 -0
- package/lib/survey/questions/CSATQuestion.d.ts.map +1 -0
- package/lib/survey/questions/CSATQuestion.js +37 -0
- package/lib/survey/questions/CSATQuestion.js.map +1 -0
- package/lib/survey/questions/DateQuestion.d.ts +20 -0
- package/lib/survey/questions/DateQuestion.d.ts.map +1 -0
- package/lib/survey/questions/DateQuestion.js +103 -0
- package/lib/survey/questions/DateQuestion.js.map +1 -0
- package/lib/survey/questions/ImagePickerQuestion.d.ts +33 -0
- package/lib/survey/questions/ImagePickerQuestion.d.ts.map +1 -0
- package/lib/survey/questions/ImagePickerQuestion.js +68 -0
- package/lib/survey/questions/ImagePickerQuestion.js.map +1 -0
- package/lib/survey/questions/ImageQuestion.d.ts +26 -0
- package/lib/survey/questions/ImageQuestion.d.ts.map +1 -0
- package/lib/survey/questions/ImageQuestion.js +24 -0
- package/lib/survey/questions/ImageQuestion.js.map +1 -0
- package/lib/survey/questions/MatrixCell.d.ts +45 -0
- package/lib/survey/questions/MatrixCell.d.ts.map +1 -0
- package/lib/survey/questions/MatrixCell.js +54 -0
- package/lib/survey/questions/MatrixCell.js.map +1 -0
- package/lib/survey/questions/MatrixDropdownQuestion.d.ts +29 -0
- package/lib/survey/questions/MatrixDropdownQuestion.d.ts.map +1 -0
- package/lib/survey/questions/MatrixDropdownQuestion.js +42 -0
- package/lib/survey/questions/MatrixDropdownQuestion.js.map +1 -0
- package/lib/survey/questions/MatrixDynamicQuestion.d.ts +26 -0
- package/lib/survey/questions/MatrixDynamicQuestion.d.ts.map +1 -0
- package/lib/survey/questions/MatrixDynamicQuestion.js +52 -0
- package/lib/survey/questions/MatrixDynamicQuestion.js.map +1 -0
- package/lib/survey/questions/MatrixQuestion.d.ts +38 -0
- package/lib/survey/questions/MatrixQuestion.d.ts.map +1 -0
- package/lib/survey/questions/MatrixQuestion.js +64 -0
- package/lib/survey/questions/MatrixQuestion.js.map +1 -0
- package/lib/survey/questions/MultiTextQuestion.d.ts +19 -0
- package/lib/survey/questions/MultiTextQuestion.d.ts.map +1 -0
- package/lib/survey/questions/MultiTextQuestion.js +64 -0
- package/lib/survey/questions/MultiTextQuestion.js.map +1 -0
- package/lib/survey/questions/MultipleChoiceQuestion.d.ts +25 -0
- package/lib/survey/questions/MultipleChoiceQuestion.d.ts.map +1 -0
- package/lib/survey/questions/MultipleChoiceQuestion.js +111 -0
- package/lib/survey/questions/MultipleChoiceQuestion.js.map +1 -0
- package/lib/survey/questions/NPSQuestion.d.ts +18 -0
- package/lib/survey/questions/NPSQuestion.d.ts.map +1 -0
- package/lib/survey/questions/NPSQuestion.js +62 -0
- package/lib/survey/questions/NPSQuestion.js.map +1 -0
- package/lib/survey/questions/PanelDynamicQuestion.d.ts +50 -0
- package/lib/survey/questions/PanelDynamicQuestion.d.ts.map +1 -0
- package/lib/survey/questions/PanelDynamicQuestion.js +62 -0
- package/lib/survey/questions/PanelDynamicQuestion.js.map +1 -0
- package/lib/survey/questions/RankingQuestion.d.ts +34 -0
- package/lib/survey/questions/RankingQuestion.d.ts.map +1 -0
- package/lib/survey/questions/RankingQuestion.js +85 -0
- package/lib/survey/questions/RankingQuestion.js.map +1 -0
- package/lib/survey/questions/RatingQuestion.d.ts +20 -0
- package/lib/survey/questions/RatingQuestion.d.ts.map +1 -0
- package/lib/survey/questions/RatingQuestion.js +53 -0
- package/lib/survey/questions/RatingQuestion.js.map +1 -0
- package/lib/survey/questions/TextQuestion.d.ts +17 -0
- package/lib/survey/questions/TextQuestion.d.ts.map +1 -0
- package/lib/survey/questions/TextQuestion.js +46 -0
- package/lib/survey/questions/TextQuestion.js.map +1 -0
- package/lib/survey/styleContract.d.ts +230 -0
- package/lib/survey/styleContract.d.ts.map +1 -0
- package/lib/survey/styleContract.js +365 -0
- package/lib/survey/styleContract.js.map +1 -0
- package/lib/survey/themePalette.d.ts +56 -0
- package/lib/survey/themePalette.d.ts.map +1 -0
- package/lib/survey/themePalette.js +63 -0
- package/lib/survey/themePalette.js.map +1 -0
- package/lib/survey/translationLookup.d.ts +4 -0
- package/lib/survey/translationLookup.d.ts.map +1 -0
- package/lib/survey/translationLookup.js +62 -0
- package/lib/survey/translationLookup.js.map +1 -0
- package/lib/survey/validate.d.ts +3 -0
- package/lib/survey/validate.d.ts.map +1 -0
- package/lib/survey/validate.js +159 -0
- package/lib/survey/validate.js.map +1 -0
- package/lib/survey/variablesResolver.d.ts +10 -0
- package/lib/survey/variablesResolver.d.ts.map +1 -0
- package/lib/survey/variablesResolver.js +107 -0
- package/lib/survey/variablesResolver.js.map +1 -0
- package/lib/survey/voiceParsers.d.ts +47 -0
- package/lib/survey/voiceParsers.d.ts.map +1 -0
- package/lib/survey/voiceParsers.js +310 -0
- package/lib/survey/voiceParsers.js.map +1 -0
- package/lib/types.d.ts +349 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +20 -0
- package/lib/types.js.map +1 -0
- package/metro.js +55 -0
- 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
|