noboarding 1.0.2-beta → 1.0.3-beta
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/lib/OnboardingFlow.js +88 -13
- package/lib/analytics.d.ts +6 -0
- package/lib/analytics.js +18 -1
- package/lib/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/OnboardingFlow.tsx +97 -14
- package/src/analytics.ts +23 -1
- package/src/types.ts +1 -0
package/lib/OnboardingFlow.js
CHANGED
|
@@ -32,18 +32,39 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.OnboardingFlow = void 0;
|
|
37
40
|
const react_1 = __importStar(require("react"));
|
|
38
41
|
const react_native_1 = require("react-native");
|
|
42
|
+
const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
|
|
39
43
|
const api_1 = require("./api");
|
|
40
44
|
const analytics_1 = require("./analytics");
|
|
41
45
|
const variableUtils_1 = require("./variableUtils");
|
|
42
46
|
const ElementRenderer_1 = require("./components/ElementRenderer");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const USER_ID_STORAGE_KEY = '@noboarding_user_id';
|
|
48
|
+
// Get or create persistent user ID
|
|
49
|
+
const getPersistentUserId = async () => {
|
|
50
|
+
try {
|
|
51
|
+
// Try to get existing user ID from storage
|
|
52
|
+
const existingUserId = await async_storage_1.default.getItem(USER_ID_STORAGE_KEY);
|
|
53
|
+
if (existingUserId) {
|
|
54
|
+
return existingUserId;
|
|
55
|
+
}
|
|
56
|
+
// Generate new user ID if none exists
|
|
57
|
+
const newUserId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
58
|
+
await async_storage_1.default.setItem(USER_ID_STORAGE_KEY, newUserId);
|
|
59
|
+
return newUserId;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('Failed to get/set user ID from storage:', error);
|
|
63
|
+
// Fallback to generating a non-persistent ID
|
|
64
|
+
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
65
|
+
}
|
|
46
66
|
};
|
|
67
|
+
// Generate session ID (always new per session)
|
|
47
68
|
const generateSessionId = () => {
|
|
48
69
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
49
70
|
};
|
|
@@ -80,14 +101,22 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
|
|
|
80
101
|
const [variables, setVariables] = (0, react_1.useState)(initialVariables || {});
|
|
81
102
|
const apiRef = (0, react_1.useRef)(null);
|
|
82
103
|
const analyticsRef = (0, react_1.useRef)(null);
|
|
83
|
-
const userIdRef = (0, react_1.useRef)(
|
|
104
|
+
const userIdRef = (0, react_1.useRef)(null);
|
|
84
105
|
const sessionIdRef = (0, react_1.useRef)(generateSessionId());
|
|
106
|
+
const flowIdRef = (0, react_1.useRef)(null);
|
|
85
107
|
(0, react_1.useEffect)(() => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const initialize = async () => {
|
|
109
|
+
// Get or create persistent user ID
|
|
110
|
+
const userId = await getPersistentUserId();
|
|
111
|
+
userIdRef.current = userId;
|
|
112
|
+
// Notify parent of the user ID
|
|
113
|
+
if (onUserIdGenerated) {
|
|
114
|
+
onUserIdGenerated(userId);
|
|
115
|
+
}
|
|
116
|
+
// Initialize the flow
|
|
117
|
+
await initializeFlow();
|
|
118
|
+
};
|
|
119
|
+
initialize();
|
|
91
120
|
return () => {
|
|
92
121
|
// Cleanup: flush remaining analytics
|
|
93
122
|
if (analyticsRef.current) {
|
|
@@ -95,26 +124,65 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
|
|
|
95
124
|
}
|
|
96
125
|
};
|
|
97
126
|
}, []);
|
|
127
|
+
// Track screen views whenever user navigates to a different screen
|
|
128
|
+
(0, react_1.useEffect)(() => {
|
|
129
|
+
if (screens.length > 0 && analyticsRef.current && currentIndex > 0) {
|
|
130
|
+
const currentScreen = screens[currentIndex];
|
|
131
|
+
analyticsRef.current.track('screen_viewed', {
|
|
132
|
+
flow_id: flowIdRef.current,
|
|
133
|
+
screen_id: currentScreen.id,
|
|
134
|
+
screen_index: currentIndex,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}, [currentIndex, screens]);
|
|
98
138
|
const initializeFlow = async () => {
|
|
139
|
+
var _a, _b, _c;
|
|
99
140
|
try {
|
|
100
141
|
setLoading(true);
|
|
101
142
|
setError(null);
|
|
143
|
+
// Ensure user ID is set
|
|
144
|
+
if (!userIdRef.current) {
|
|
145
|
+
throw new Error('User ID not initialized');
|
|
146
|
+
}
|
|
102
147
|
// Initialize API client with detected API key
|
|
103
148
|
const api = new api_1.API(activeApiKey, baseUrl);
|
|
104
149
|
apiRef.current = api;
|
|
105
150
|
// Initialize analytics
|
|
106
151
|
const analytics = new analytics_1.AnalyticsManager(api, userIdRef.current, sessionIdRef.current);
|
|
107
152
|
analyticsRef.current = analytics;
|
|
108
|
-
// Track onboarding start
|
|
109
|
-
analytics.track('onboarding_started');
|
|
110
153
|
// Fetch configuration
|
|
111
154
|
const configResponse = await api.getConfig();
|
|
155
|
+
// Store flow_id for analytics
|
|
156
|
+
flowIdRef.current = configResponse.config_id;
|
|
157
|
+
// Handle A/B test experiment assignment
|
|
158
|
+
let screensToUse = configResponse.config.screens;
|
|
159
|
+
if (configResponse.experiments && configResponse.experiments.length > 0) {
|
|
160
|
+
// Assign user to the first active experiment
|
|
161
|
+
const experiment = configResponse.experiments[0];
|
|
162
|
+
try {
|
|
163
|
+
const assignment = await api.assignVariant(experiment.id, userIdRef.current);
|
|
164
|
+
// Set experiment context so all events get tagged
|
|
165
|
+
analytics.setExperimentContext(experiment.id, assignment.variant_id);
|
|
166
|
+
// Use variant screens if available
|
|
167
|
+
if (((_b = (_a = assignment.variant_config) === null || _a === void 0 ? void 0 : _a.screens) === null || _b === void 0 ? void 0 : _b.length) > 0) {
|
|
168
|
+
screensToUse = assignment.variant_config.screens;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.warn('Failed to assign experiment variant, using default flow:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
112
175
|
// Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
|
|
113
|
-
const normalizedScreens =
|
|
176
|
+
const normalizedScreens = screensToUse
|
|
114
177
|
.map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })))
|
|
115
178
|
// Filter out hidden screens (dashboard show/hide feature)
|
|
116
179
|
.filter(s => !s.hidden);
|
|
117
180
|
setScreens(normalizedScreens);
|
|
181
|
+
// Track onboarding start with first screen
|
|
182
|
+
analytics.track('onboarding_started', {
|
|
183
|
+
flow_id: flowIdRef.current,
|
|
184
|
+
screen_id: (_c = normalizedScreens[0]) === null || _c === void 0 ? void 0 : _c.id,
|
|
185
|
+
});
|
|
118
186
|
setLoading(false);
|
|
119
187
|
}
|
|
120
188
|
catch (err) {
|
|
@@ -155,18 +223,25 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
|
|
|
155
223
|
setVariables(prev => (Object.assign(Object.assign({}, prev), { [name]: value })));
|
|
156
224
|
}, []);
|
|
157
225
|
const handleComplete = async (lastScreenData) => {
|
|
226
|
+
var _a;
|
|
158
227
|
const finalData = Object.assign(Object.assign(Object.assign({}, collectedData), (lastScreenData || {})), { _variables: variables });
|
|
159
228
|
// Track completion
|
|
160
229
|
if (analyticsRef.current) {
|
|
161
|
-
analyticsRef.current.track('onboarding_completed'
|
|
230
|
+
analyticsRef.current.track('onboarding_completed', {
|
|
231
|
+
flow_id: flowIdRef.current,
|
|
232
|
+
screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
|
|
233
|
+
});
|
|
162
234
|
await analyticsRef.current.flush();
|
|
163
235
|
}
|
|
164
236
|
// Call developer's completion callback with collected data
|
|
165
237
|
onComplete(finalData);
|
|
166
238
|
};
|
|
167
239
|
const handleSkipAll = async () => {
|
|
240
|
+
var _a;
|
|
168
241
|
if (analyticsRef.current) {
|
|
169
242
|
analyticsRef.current.track('onboarding_abandoned', {
|
|
243
|
+
flow_id: flowIdRef.current,
|
|
244
|
+
screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
|
|
170
245
|
current_screen_index: currentIndex,
|
|
171
246
|
});
|
|
172
247
|
await analyticsRef.current.flush();
|
package/lib/analytics.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export declare class AnalyticsManager {
|
|
|
5
5
|
private userId;
|
|
6
6
|
private sessionId;
|
|
7
7
|
private flushTimer;
|
|
8
|
+
private experimentId;
|
|
9
|
+
private variantId;
|
|
8
10
|
constructor(api: API, userId: string, sessionId: string);
|
|
11
|
+
/**
|
|
12
|
+
* Set experiment context — all subsequent events will be tagged
|
|
13
|
+
*/
|
|
14
|
+
setExperimentContext(experimentId: string, variantId: string): void;
|
|
9
15
|
/**
|
|
10
16
|
* Track an analytics event
|
|
11
17
|
*/
|
package/lib/analytics.js
CHANGED
|
@@ -7,22 +7,39 @@ class AnalyticsManager {
|
|
|
7
7
|
constructor(api, userId, sessionId) {
|
|
8
8
|
this.events = [];
|
|
9
9
|
this.flushTimer = null;
|
|
10
|
+
this.experimentId = null;
|
|
11
|
+
this.variantId = null;
|
|
10
12
|
this.api = api;
|
|
11
13
|
this.userId = userId;
|
|
12
14
|
this.sessionId = sessionId;
|
|
13
15
|
// Start auto-flush timer
|
|
14
16
|
this.startFlushTimer();
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Set experiment context — all subsequent events will be tagged
|
|
20
|
+
*/
|
|
21
|
+
setExperimentContext(experimentId, variantId) {
|
|
22
|
+
this.experimentId = experimentId;
|
|
23
|
+
this.variantId = variantId;
|
|
24
|
+
}
|
|
16
25
|
/**
|
|
17
26
|
* Track an analytics event
|
|
18
27
|
*/
|
|
19
28
|
track(eventName, properties) {
|
|
29
|
+
const mergedProperties = Object.assign({}, (properties || {}));
|
|
30
|
+
// Auto-inject experiment context if set
|
|
31
|
+
if (this.experimentId) {
|
|
32
|
+
mergedProperties.experiment_id = this.experimentId;
|
|
33
|
+
}
|
|
34
|
+
if (this.variantId) {
|
|
35
|
+
mergedProperties.variant_id = this.variantId;
|
|
36
|
+
}
|
|
20
37
|
const event = {
|
|
21
38
|
event: eventName,
|
|
22
39
|
user_id: this.userId,
|
|
23
40
|
session_id: this.sessionId,
|
|
24
41
|
timestamp: Date.now(),
|
|
25
|
-
properties:
|
|
42
|
+
properties: mergedProperties,
|
|
26
43
|
};
|
|
27
44
|
this.events.push(event);
|
|
28
45
|
// Auto-flush if batch size reached
|
package/lib/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/OnboardingFlow.tsx
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { View, ActivityIndicator, StyleSheet, Text, TouchableOpacity } from 'react-native';
|
|
3
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
4
|
import { API } from './api';
|
|
4
5
|
import { AnalyticsManager } from './analytics';
|
|
5
6
|
import { OnboardingFlowProps, ScreenConfig, ConditionalDestination, ConditionalRoutes } from './types';
|
|
6
7
|
import { resolveDestination } from './variableUtils';
|
|
7
8
|
import { ElementRenderer } from './components/ElementRenderer';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const USER_ID_STORAGE_KEY = '@noboarding_user_id';
|
|
11
|
+
|
|
12
|
+
// Get or create persistent user ID
|
|
13
|
+
const getPersistentUserId = async (): Promise<string> => {
|
|
14
|
+
try {
|
|
15
|
+
// Try to get existing user ID from storage
|
|
16
|
+
const existingUserId = await AsyncStorage.getItem(USER_ID_STORAGE_KEY);
|
|
17
|
+
|
|
18
|
+
if (existingUserId) {
|
|
19
|
+
return existingUserId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Generate new user ID if none exists
|
|
23
|
+
const newUserId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
24
|
+
await AsyncStorage.setItem(USER_ID_STORAGE_KEY, newUserId);
|
|
25
|
+
return newUserId;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to get/set user ID from storage:', error);
|
|
28
|
+
// Fallback to generating a non-persistent ID
|
|
29
|
+
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
30
|
+
}
|
|
12
31
|
};
|
|
13
32
|
|
|
33
|
+
// Generate session ID (always new per session)
|
|
14
34
|
const generateSessionId = (): string => {
|
|
15
35
|
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
16
36
|
};
|
|
@@ -60,16 +80,26 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
60
80
|
|
|
61
81
|
const apiRef = useRef<API | null>(null);
|
|
62
82
|
const analyticsRef = useRef<AnalyticsManager | null>(null);
|
|
63
|
-
const userIdRef = useRef<string>(
|
|
83
|
+
const userIdRef = useRef<string | null>(null);
|
|
64
84
|
const sessionIdRef = useRef<string>(generateSessionId());
|
|
85
|
+
const flowIdRef = useRef<string | null>(null);
|
|
65
86
|
|
|
66
87
|
useEffect(() => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
const initialize = async () => {
|
|
89
|
+
// Get or create persistent user ID
|
|
90
|
+
const userId = await getPersistentUserId();
|
|
91
|
+
userIdRef.current = userId;
|
|
92
|
+
|
|
93
|
+
// Notify parent of the user ID
|
|
94
|
+
if (onUserIdGenerated) {
|
|
95
|
+
onUserIdGenerated(userId);
|
|
96
|
+
}
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
// Initialize the flow
|
|
99
|
+
await initializeFlow();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
initialize();
|
|
73
103
|
|
|
74
104
|
return () => {
|
|
75
105
|
// Cleanup: flush remaining analytics
|
|
@@ -79,11 +109,28 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
79
109
|
};
|
|
80
110
|
}, []);
|
|
81
111
|
|
|
112
|
+
// Track screen views whenever user navigates to a different screen
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (screens.length > 0 && analyticsRef.current && currentIndex > 0) {
|
|
115
|
+
const currentScreen = screens[currentIndex];
|
|
116
|
+
analyticsRef.current.track('screen_viewed', {
|
|
117
|
+
flow_id: flowIdRef.current,
|
|
118
|
+
screen_id: currentScreen.id,
|
|
119
|
+
screen_index: currentIndex,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}, [currentIndex, screens]);
|
|
123
|
+
|
|
82
124
|
const initializeFlow = async () => {
|
|
83
125
|
try {
|
|
84
126
|
setLoading(true);
|
|
85
127
|
setError(null);
|
|
86
128
|
|
|
129
|
+
// Ensure user ID is set
|
|
130
|
+
if (!userIdRef.current) {
|
|
131
|
+
throw new Error('User ID not initialized');
|
|
132
|
+
}
|
|
133
|
+
|
|
87
134
|
// Initialize API client with detected API key
|
|
88
135
|
const api = new API(activeApiKey, baseUrl);
|
|
89
136
|
apiRef.current = api;
|
|
@@ -96,13 +143,38 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
96
143
|
);
|
|
97
144
|
analyticsRef.current = analytics;
|
|
98
145
|
|
|
99
|
-
// Track onboarding start
|
|
100
|
-
analytics.track('onboarding_started');
|
|
101
|
-
|
|
102
146
|
// Fetch configuration
|
|
103
147
|
const configResponse = await api.getConfig();
|
|
148
|
+
|
|
149
|
+
// Store flow_id for analytics
|
|
150
|
+
flowIdRef.current = configResponse.config_id;
|
|
151
|
+
|
|
152
|
+
// Handle A/B test experiment assignment
|
|
153
|
+
let screensToUse = configResponse.config.screens;
|
|
154
|
+
|
|
155
|
+
if (configResponse.experiments && configResponse.experiments.length > 0) {
|
|
156
|
+
// Assign user to the first active experiment
|
|
157
|
+
const experiment = configResponse.experiments[0];
|
|
158
|
+
try {
|
|
159
|
+
const assignment = await api.assignVariant(
|
|
160
|
+
experiment.id,
|
|
161
|
+
userIdRef.current!
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Set experiment context so all events get tagged
|
|
165
|
+
analytics.setExperimentContext(experiment.id, assignment.variant_id);
|
|
166
|
+
|
|
167
|
+
// Use variant screens if available
|
|
168
|
+
if (assignment.variant_config?.screens?.length > 0) {
|
|
169
|
+
screensToUse = assignment.variant_config.screens;
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn('Failed to assign experiment variant, using default flow:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
104
176
|
// Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
|
|
105
|
-
const normalizedScreens =
|
|
177
|
+
const normalizedScreens = screensToUse
|
|
106
178
|
.map(s => ({
|
|
107
179
|
...s,
|
|
108
180
|
type: (s.type === ('custom_screen' as any) && s.elements) ? 'noboard_screen' as ScreenConfig['type'] : s.type,
|
|
@@ -111,6 +183,12 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
111
183
|
.filter(s => !s.hidden);
|
|
112
184
|
setScreens(normalizedScreens);
|
|
113
185
|
|
|
186
|
+
// Track onboarding start with first screen
|
|
187
|
+
analytics.track('onboarding_started', {
|
|
188
|
+
flow_id: flowIdRef.current,
|
|
189
|
+
screen_id: normalizedScreens[0]?.id,
|
|
190
|
+
});
|
|
191
|
+
|
|
114
192
|
setLoading(false);
|
|
115
193
|
} catch (err) {
|
|
116
194
|
console.error('Failed to initialize onboarding flow:', err);
|
|
@@ -162,7 +240,10 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
162
240
|
|
|
163
241
|
// Track completion
|
|
164
242
|
if (analyticsRef.current) {
|
|
165
|
-
analyticsRef.current.track('onboarding_completed'
|
|
243
|
+
analyticsRef.current.track('onboarding_completed', {
|
|
244
|
+
flow_id: flowIdRef.current,
|
|
245
|
+
screen_id: screens[currentIndex]?.id,
|
|
246
|
+
});
|
|
166
247
|
await analyticsRef.current.flush();
|
|
167
248
|
}
|
|
168
249
|
|
|
@@ -173,6 +254,8 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
|
|
|
173
254
|
const handleSkipAll = async () => {
|
|
174
255
|
if (analyticsRef.current) {
|
|
175
256
|
analyticsRef.current.track('onboarding_abandoned', {
|
|
257
|
+
flow_id: flowIdRef.current,
|
|
258
|
+
screen_id: screens[currentIndex]?.id,
|
|
176
259
|
current_screen_index: currentIndex,
|
|
177
260
|
});
|
|
178
261
|
await analyticsRef.current.flush();
|
package/src/analytics.ts
CHANGED
|
@@ -10,6 +10,8 @@ export class AnalyticsManager {
|
|
|
10
10
|
private userId: string;
|
|
11
11
|
private sessionId: string;
|
|
12
12
|
private flushTimer: NodeJS.Timeout | null = null;
|
|
13
|
+
private experimentId: string | null = null;
|
|
14
|
+
private variantId: string | null = null;
|
|
13
15
|
|
|
14
16
|
constructor(api: API, userId: string, sessionId: string) {
|
|
15
17
|
this.api = api;
|
|
@@ -20,16 +22,36 @@ export class AnalyticsManager {
|
|
|
20
22
|
this.startFlushTimer();
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Set experiment context — all subsequent events will be tagged
|
|
27
|
+
*/
|
|
28
|
+
setExperimentContext(experimentId: string, variantId: string): void {
|
|
29
|
+
this.experimentId = experimentId;
|
|
30
|
+
this.variantId = variantId;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Track an analytics event
|
|
25
35
|
*/
|
|
26
36
|
track(eventName: string, properties?: Record<string, any>): void {
|
|
37
|
+
const mergedProperties: Record<string, any> = {
|
|
38
|
+
...(properties || {}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Auto-inject experiment context if set
|
|
42
|
+
if (this.experimentId) {
|
|
43
|
+
mergedProperties.experiment_id = this.experimentId;
|
|
44
|
+
}
|
|
45
|
+
if (this.variantId) {
|
|
46
|
+
mergedProperties.variant_id = this.variantId;
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
const event: AnalyticsEvent = {
|
|
28
50
|
event: eventName,
|
|
29
51
|
user_id: this.userId,
|
|
30
52
|
session_id: this.sessionId,
|
|
31
53
|
timestamp: Date.now(),
|
|
32
|
-
properties:
|
|
54
|
+
properties: mergedProperties,
|
|
33
55
|
};
|
|
34
56
|
|
|
35
57
|
this.events.push(event);
|