mixpanel-react-native 3.2.0-beta.3 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -13
- package/README.md +10 -31
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java +2 -272
- package/index.d.ts +1 -64
- package/index.js +8 -103
- package/ios/MixpanelReactNative.m +1 -19
- package/ios/MixpanelReactNative.swift +5 -183
- package/javascript/mixpanel-main.js +1 -21
- package/javascript/mixpanel-network.js +41 -86
- package/javascript/mixpanel-persistent.js +4 -36
- package/javascript/mixpanel-storage.js +2 -2
- package/package.json +38 -18
- package/.github/dependabot.yml +0 -7
- package/FEATURE_FLAGS_JS_MODE_FINDINGS.md +0 -119
- package/FEATURE_FLAGS_QUICKSTART.md +0 -399
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +0 -6
- package/android/gradlew +0 -172
- package/android/gradlew.bat +0 -84
- package/javascript/mixpanel-flags-js.js +0 -465
- package/javascript/mixpanel-flags.js +0 -710
package/android/gradlew.bat
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
@if "%DEBUG%" == "" @echo off
|
|
2
|
-
@rem ##########################################################################
|
|
3
|
-
@rem
|
|
4
|
-
@rem Gradle startup script for Windows
|
|
5
|
-
@rem
|
|
6
|
-
@rem ##########################################################################
|
|
7
|
-
|
|
8
|
-
@rem Set local scope for the variables with windows NT shell
|
|
9
|
-
if "%OS%"=="Windows_NT" setlocal
|
|
10
|
-
|
|
11
|
-
set DIRNAME=%~dp0
|
|
12
|
-
if "%DIRNAME%" == "" set DIRNAME=.
|
|
13
|
-
set APP_BASE_NAME=%~n0
|
|
14
|
-
set APP_HOME=%DIRNAME%
|
|
15
|
-
|
|
16
|
-
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
17
|
-
set DEFAULT_JVM_OPTS=
|
|
18
|
-
|
|
19
|
-
@rem Find java.exe
|
|
20
|
-
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
21
|
-
|
|
22
|
-
set JAVA_EXE=java.exe
|
|
23
|
-
%JAVA_EXE% -version >NUL 2>&1
|
|
24
|
-
if "%ERRORLEVEL%" == "0" goto init
|
|
25
|
-
|
|
26
|
-
echo.
|
|
27
|
-
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
28
|
-
echo.
|
|
29
|
-
echo Please set the JAVA_HOME variable in your environment to match the
|
|
30
|
-
echo location of your Java installation.
|
|
31
|
-
|
|
32
|
-
goto fail
|
|
33
|
-
|
|
34
|
-
:findJavaFromJavaHome
|
|
35
|
-
set JAVA_HOME=%JAVA_HOME:"=%
|
|
36
|
-
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
37
|
-
|
|
38
|
-
if exist "%JAVA_EXE%" goto init
|
|
39
|
-
|
|
40
|
-
echo.
|
|
41
|
-
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
42
|
-
echo.
|
|
43
|
-
echo Please set the JAVA_HOME variable in your environment to match the
|
|
44
|
-
echo location of your Java installation.
|
|
45
|
-
|
|
46
|
-
goto fail
|
|
47
|
-
|
|
48
|
-
:init
|
|
49
|
-
@rem Get command-line arguments, handling Windows variants
|
|
50
|
-
|
|
51
|
-
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
52
|
-
|
|
53
|
-
:win9xME_args
|
|
54
|
-
@rem Slurp the command line arguments.
|
|
55
|
-
set CMD_LINE_ARGS=
|
|
56
|
-
set _SKIP=2
|
|
57
|
-
|
|
58
|
-
:win9xME_args_slurp
|
|
59
|
-
if "x%~1" == "x" goto execute
|
|
60
|
-
|
|
61
|
-
set CMD_LINE_ARGS=%*
|
|
62
|
-
|
|
63
|
-
:execute
|
|
64
|
-
@rem Setup the command line
|
|
65
|
-
|
|
66
|
-
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
67
|
-
|
|
68
|
-
@rem Execute Gradle
|
|
69
|
-
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
|
70
|
-
|
|
71
|
-
:end
|
|
72
|
-
@rem End local scope for the variables with windows NT shell
|
|
73
|
-
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
74
|
-
|
|
75
|
-
:fail
|
|
76
|
-
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
77
|
-
rem the _cmd.exe /c_ return code!
|
|
78
|
-
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
79
|
-
exit /b 1
|
|
80
|
-
|
|
81
|
-
:mainEnd
|
|
82
|
-
if "%OS%"=="Windows_NT" endlocal
|
|
83
|
-
|
|
84
|
-
:omega
|
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
import { MixpanelLogger } from "./mixpanel-logger";
|
|
2
|
-
import { MixpanelNetwork } from "./mixpanel-network";
|
|
3
|
-
import { MixpanelPersistent } from "./mixpanel-persistent";
|
|
4
|
-
import packageJson from "mixpanel-react-native/package.json";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* JavaScript implementation of Feature Flags for React Native
|
|
8
|
-
* This is used when native modules are not available (Expo, React Native Web)
|
|
9
|
-
* Aligned with mixpanel-js reference implementation
|
|
10
|
-
*/
|
|
11
|
-
export class MixpanelFlagsJS {
|
|
12
|
-
constructor(token, mixpanelImpl, storage, initialContext = {}) {
|
|
13
|
-
this.token = token;
|
|
14
|
-
this.mixpanelImpl = mixpanelImpl;
|
|
15
|
-
this.storage = storage;
|
|
16
|
-
this.flags = new Map(); // Use Map like mixpanel-js
|
|
17
|
-
this.flagsReady = false;
|
|
18
|
-
this.experimentTracked = new Set();
|
|
19
|
-
this.context = initialContext; // Initialize with provided context
|
|
20
|
-
this.flagsCacheKey = `MIXPANEL_${token}_FLAGS_CACHE`;
|
|
21
|
-
this.flagsReadyKey = `MIXPANEL_${token}_FLAGS_READY`;
|
|
22
|
-
this.mixpanelPersistent = MixpanelPersistent.getInstance(storage, token);
|
|
23
|
-
|
|
24
|
-
// Performance tracking (mixpanel-js alignment)
|
|
25
|
-
this._fetchStartTime = null;
|
|
26
|
-
this._fetchCompleteTime = null;
|
|
27
|
-
this._fetchLatency = null;
|
|
28
|
-
this._traceparent = null;
|
|
29
|
-
|
|
30
|
-
// Load cached flags on initialization (fire and forget - loads in background)
|
|
31
|
-
// This is async but intentionally not awaited to avoid blocking constructor
|
|
32
|
-
// Flags will be available once cache loads or after explicit loadFlags() call
|
|
33
|
-
this.loadCachedFlags().catch(error => {
|
|
34
|
-
MixpanelLogger.log(this.token, "Failed to load cached flags in constructor:", error);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Load cached flags from storage
|
|
40
|
-
*/
|
|
41
|
-
async loadCachedFlags() {
|
|
42
|
-
try {
|
|
43
|
-
const cachedFlags = await this.storage.getItem(this.flagsCacheKey);
|
|
44
|
-
if (cachedFlags) {
|
|
45
|
-
const parsed = JSON.parse(cachedFlags);
|
|
46
|
-
// Convert array back to Map for consistency
|
|
47
|
-
this.flags = new Map(parsed);
|
|
48
|
-
this.flagsReady = true;
|
|
49
|
-
MixpanelLogger.log(this.token, "Loaded cached feature flags");
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
MixpanelLogger.log(this.token, "Error loading cached flags:", error);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Cache flags to storage
|
|
58
|
-
*/
|
|
59
|
-
async cacheFlags() {
|
|
60
|
-
try {
|
|
61
|
-
// Convert Map to array for JSON serialization
|
|
62
|
-
const flagsArray = Array.from(this.flags.entries());
|
|
63
|
-
await this.storage.setItem(
|
|
64
|
-
this.flagsCacheKey,
|
|
65
|
-
JSON.stringify(flagsArray)
|
|
66
|
-
);
|
|
67
|
-
await this.storage.setItem(this.flagsReadyKey, "true");
|
|
68
|
-
} catch (error) {
|
|
69
|
-
MixpanelLogger.log(this.token, "Error caching flags:", error);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Generate W3C traceparent header
|
|
75
|
-
* Format: 00-{traceID}-{parentID}-{flags}
|
|
76
|
-
* Returns null if UUID generation fails (graceful degradation)
|
|
77
|
-
*/
|
|
78
|
-
generateTraceparent() {
|
|
79
|
-
try {
|
|
80
|
-
// Try expo-crypto first
|
|
81
|
-
const crypto = require("expo-crypto");
|
|
82
|
-
const traceID = crypto.randomUUID().replace(/-/g, "");
|
|
83
|
-
const parentID = crypto.randomUUID().replace(/-/g, "").substring(0, 16);
|
|
84
|
-
return `00-${traceID}-${parentID}-01`;
|
|
85
|
-
} catch (expoCryptoError) {
|
|
86
|
-
try {
|
|
87
|
-
// Fallback to uuid (import the v4 function directly)
|
|
88
|
-
const { v4: uuidv4 } = require("uuid");
|
|
89
|
-
const traceID = uuidv4().replace(/-/g, "");
|
|
90
|
-
const parentID = uuidv4().replace(/-/g, "").substring(0, 16);
|
|
91
|
-
return `00-${traceID}-${parentID}-01`;
|
|
92
|
-
} catch (uuidError) {
|
|
93
|
-
// Graceful degradation: traceparent is optional for observability
|
|
94
|
-
// Don't block flag loading if UUID generation fails
|
|
95
|
-
MixpanelLogger.log(
|
|
96
|
-
this.token,
|
|
97
|
-
"Could not generate traceparent (UUID unavailable):",
|
|
98
|
-
uuidError
|
|
99
|
-
);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Mark fetch operation complete and calculate latency
|
|
107
|
-
*/
|
|
108
|
-
markFetchComplete() {
|
|
109
|
-
if (!this._fetchStartTime) {
|
|
110
|
-
MixpanelLogger.error(
|
|
111
|
-
this.token,
|
|
112
|
-
"Fetch start time not set, cannot mark fetch complete"
|
|
113
|
-
);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
this._fetchCompleteTime = Date.now();
|
|
117
|
-
this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
|
|
118
|
-
this._fetchStartTime = null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Fetch feature flags from Mixpanel API
|
|
123
|
-
*/
|
|
124
|
-
async loadFlags() {
|
|
125
|
-
this._fetchStartTime = Date.now();
|
|
126
|
-
|
|
127
|
-
// Generate traceparent if possible (graceful degradation if UUID unavailable)
|
|
128
|
-
try {
|
|
129
|
-
this._traceparent = this.generateTraceparent();
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// Silently skip traceparent if generation fails
|
|
132
|
-
this._traceparent = null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const distinctId = this.mixpanelPersistent.getDistinctId(this.token);
|
|
137
|
-
const deviceId = this.mixpanelPersistent.getDeviceId(this.token);
|
|
138
|
-
|
|
139
|
-
// Build context object (mixpanel-js format)
|
|
140
|
-
const context = {
|
|
141
|
-
distinct_id: distinctId,
|
|
142
|
-
device_id: deviceId,
|
|
143
|
-
...this.context,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Build query parameters (mixpanel-js format)
|
|
147
|
-
const queryParams = new URLSearchParams();
|
|
148
|
-
queryParams.set('context', JSON.stringify(context));
|
|
149
|
-
queryParams.set('token', this.token);
|
|
150
|
-
queryParams.set('mp_lib', 'react-native');
|
|
151
|
-
queryParams.set('$lib_version', packageJson.version);
|
|
152
|
-
|
|
153
|
-
MixpanelLogger.log(
|
|
154
|
-
this.token,
|
|
155
|
-
"Fetching feature flags with context:",
|
|
156
|
-
context
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const serverURL =
|
|
160
|
-
this.mixpanelImpl.config?.getServerURL?.(this.token) ||
|
|
161
|
-
"https://api.mixpanel.com";
|
|
162
|
-
|
|
163
|
-
// Use /flags endpoint with query parameters (mixpanel-js format)
|
|
164
|
-
const endpoint = `/flags?${queryParams.toString()}`;
|
|
165
|
-
|
|
166
|
-
const response = await MixpanelNetwork.sendRequest({
|
|
167
|
-
token: this.token,
|
|
168
|
-
endpoint: endpoint,
|
|
169
|
-
data: null, // Data is in query params for flags endpoint
|
|
170
|
-
serverURL: serverURL,
|
|
171
|
-
useIPAddressForGeoLocation: true,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
this.markFetchComplete();
|
|
175
|
-
|
|
176
|
-
// Support both response formats for backwards compatibility
|
|
177
|
-
if (response && response.flags) {
|
|
178
|
-
// New format (mixpanel-js compatible): {flags: {key: {variant_key, variant_value, ...}}}
|
|
179
|
-
this.flags = new Map();
|
|
180
|
-
for (const [key, data] of Object.entries(response.flags)) {
|
|
181
|
-
this.flags.set(key, {
|
|
182
|
-
key: data.variant_key,
|
|
183
|
-
value: data.variant_value,
|
|
184
|
-
experiment_id: data.experiment_id,
|
|
185
|
-
is_experiment_active: data.is_experiment_active,
|
|
186
|
-
is_qa_tester: data.is_qa_tester,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
this.flagsReady = true;
|
|
190
|
-
await this.cacheFlags();
|
|
191
|
-
MixpanelLogger.log(this.token, "Feature flags loaded successfully");
|
|
192
|
-
} else if (response && response.featureFlags) {
|
|
193
|
-
// Legacy format: {featureFlags: [{key, value, experimentID, ...}]}
|
|
194
|
-
this.flags = new Map();
|
|
195
|
-
for (const flag of response.featureFlags) {
|
|
196
|
-
this.flags.set(flag.key, {
|
|
197
|
-
key: flag.key,
|
|
198
|
-
value: flag.value,
|
|
199
|
-
experiment_id: flag.experimentID,
|
|
200
|
-
is_experiment_active: flag.isExperimentActive,
|
|
201
|
-
is_qa_tester: flag.isQATester,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
this.flagsReady = true;
|
|
205
|
-
await this.cacheFlags();
|
|
206
|
-
MixpanelLogger.warn(
|
|
207
|
-
this.token,
|
|
208
|
-
'Received legacy featureFlags format. Please update backend to use "flags" format.'
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
this.markFetchComplete();
|
|
213
|
-
MixpanelLogger.log(this.token, "Error loading feature flags:", error);
|
|
214
|
-
// Keep using cached flags if available
|
|
215
|
-
if (this.flags.size > 0) {
|
|
216
|
-
this.flagsReady = true;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Check if flags are ready to use
|
|
223
|
-
*/
|
|
224
|
-
areFlagsReady() {
|
|
225
|
-
return this.flagsReady;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Track experiment started event
|
|
230
|
-
* Aligned with mixpanel-js tracking properties
|
|
231
|
-
*/
|
|
232
|
-
async trackExperimentStarted(featureName, variant) {
|
|
233
|
-
if (this.experimentTracked.has(featureName)) {
|
|
234
|
-
return; // Already tracked
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const properties = {
|
|
239
|
-
"Experiment name": featureName, // Human-readable (mixpanel-js format)
|
|
240
|
-
"Variant name": variant.key, // Human-readable (mixpanel-js format)
|
|
241
|
-
$experiment_type: "feature_flag", // Added to match mixpanel-js
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// Add performance metrics if available
|
|
245
|
-
if (this._fetchCompleteTime) {
|
|
246
|
-
const fetchStartTime =
|
|
247
|
-
this._fetchCompleteTime - (this._fetchLatency || 0);
|
|
248
|
-
properties["Variant fetch start time"] = new Date(
|
|
249
|
-
fetchStartTime
|
|
250
|
-
).toISOString();
|
|
251
|
-
properties["Variant fetch complete time"] = new Date(
|
|
252
|
-
this._fetchCompleteTime
|
|
253
|
-
).toISOString();
|
|
254
|
-
properties["Variant fetch latency (ms)"] = this._fetchLatency || 0;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Add traceparent if available
|
|
258
|
-
if (this._traceparent) {
|
|
259
|
-
properties["Variant fetch traceparent"] = this._traceparent;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Add experiment metadata (system properties)
|
|
263
|
-
if (
|
|
264
|
-
variant.experiment_id !== undefined &&
|
|
265
|
-
variant.experiment_id !== null
|
|
266
|
-
) {
|
|
267
|
-
properties["$experiment_id"] = variant.experiment_id;
|
|
268
|
-
}
|
|
269
|
-
if (variant.is_experiment_active !== undefined) {
|
|
270
|
-
properties["$is_experiment_active"] = variant.is_experiment_active;
|
|
271
|
-
}
|
|
272
|
-
if (variant.is_qa_tester !== undefined) {
|
|
273
|
-
properties["$is_qa_tester"] = variant.is_qa_tester;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Track the experiment started event
|
|
277
|
-
await this.mixpanelImpl.track(
|
|
278
|
-
this.token,
|
|
279
|
-
"$experiment_started",
|
|
280
|
-
properties
|
|
281
|
-
);
|
|
282
|
-
this.experimentTracked.add(featureName);
|
|
283
|
-
|
|
284
|
-
MixpanelLogger.log(
|
|
285
|
-
this.token,
|
|
286
|
-
`Tracked experiment started for ${featureName}`
|
|
287
|
-
);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
MixpanelLogger.log(this.token, "Error tracking experiment:", error);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Get variant synchronously (only works when flags are ready)
|
|
295
|
-
*/
|
|
296
|
-
getVariantSync(featureName, fallback) {
|
|
297
|
-
if (!this.flagsReady || !this.flags.has(featureName)) {
|
|
298
|
-
return fallback;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const variant = this.flags.get(featureName);
|
|
302
|
-
|
|
303
|
-
// Track experiment on first access (fire and forget)
|
|
304
|
-
if (!this.experimentTracked.has(featureName)) {
|
|
305
|
-
this.trackExperimentStarted(featureName, variant).catch(error => {
|
|
306
|
-
MixpanelLogger.warn(this.token, `Failed to track experiment for ${featureName}:`, error);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return variant;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Get variant value synchronously
|
|
315
|
-
*/
|
|
316
|
-
getVariantValueSync(featureName, fallbackValue) {
|
|
317
|
-
const variant = this.getVariantSync(featureName, {
|
|
318
|
-
key: featureName,
|
|
319
|
-
value: fallbackValue,
|
|
320
|
-
});
|
|
321
|
-
return variant.value;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Check if feature is enabled synchronously
|
|
326
|
-
* Enhanced with boolean validation like mixpanel-js
|
|
327
|
-
*/
|
|
328
|
-
isEnabledSync(featureName, fallbackValue = false) {
|
|
329
|
-
const value = this.getVariantValueSync(featureName, fallbackValue);
|
|
330
|
-
|
|
331
|
-
// Validate boolean type (mixpanel-js pattern)
|
|
332
|
-
if (value !== true && value !== false) {
|
|
333
|
-
MixpanelLogger.error(
|
|
334
|
-
this.token,
|
|
335
|
-
`Feature flag "${featureName}" value: ${value} is not a boolean; returning fallback value: ${fallbackValue}`
|
|
336
|
-
);
|
|
337
|
-
return fallbackValue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return value;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Get variant asynchronously
|
|
345
|
-
*/
|
|
346
|
-
async getVariant(featureName, fallback) {
|
|
347
|
-
// If flags not ready, try to load them
|
|
348
|
-
if (!this.flagsReady) {
|
|
349
|
-
await this.loadFlags();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!this.flags.has(featureName)) {
|
|
353
|
-
return fallback;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const variant = this.flags.get(featureName);
|
|
357
|
-
|
|
358
|
-
// Track experiment on first access
|
|
359
|
-
if (!this.experimentTracked.has(featureName)) {
|
|
360
|
-
await this.trackExperimentStarted(featureName, variant);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return variant;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Get variant value asynchronously
|
|
368
|
-
*/
|
|
369
|
-
async getVariantValue(featureName, fallbackValue) {
|
|
370
|
-
const variant = await this.getVariant(featureName, {
|
|
371
|
-
key: featureName,
|
|
372
|
-
value: fallbackValue,
|
|
373
|
-
});
|
|
374
|
-
return variant.value;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Check if feature is enabled asynchronously
|
|
379
|
-
*/
|
|
380
|
-
async isEnabled(featureName, fallbackValue = false) {
|
|
381
|
-
const value = await this.getVariantValue(featureName, fallbackValue);
|
|
382
|
-
if (typeof value === "boolean") {
|
|
383
|
-
return value;
|
|
384
|
-
} else {
|
|
385
|
-
MixpanelLogger.log(this.token, `Flag "${featureName}" value is not boolean:`, value);
|
|
386
|
-
return fallbackValue;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Update context and reload flags
|
|
392
|
-
* Aligned with mixpanel-js API signature
|
|
393
|
-
* @param {object} newContext - New context properties to add/update
|
|
394
|
-
* @param {object} options - Options object
|
|
395
|
-
* @param {boolean} options.replace - If true, replace entire context instead of merging
|
|
396
|
-
*/
|
|
397
|
-
async updateContext(newContext, options = {}) {
|
|
398
|
-
if (options.replace) {
|
|
399
|
-
// Replace entire context
|
|
400
|
-
this.context = { ...newContext };
|
|
401
|
-
} else {
|
|
402
|
-
// Merge with existing context (default)
|
|
403
|
-
this.context = {
|
|
404
|
-
...this.context,
|
|
405
|
-
...newContext,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Clear experiment tracking since context changed
|
|
410
|
-
this.experimentTracked.clear();
|
|
411
|
-
|
|
412
|
-
// Reload flags with new context
|
|
413
|
-
await this.loadFlags();
|
|
414
|
-
|
|
415
|
-
MixpanelLogger.log(this.token, "Context updated, flags reloaded");
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Clear cached flags
|
|
420
|
-
*/
|
|
421
|
-
async clearCache() {
|
|
422
|
-
try {
|
|
423
|
-
await this.storage.removeItem(this.flagsCacheKey);
|
|
424
|
-
await this.storage.removeItem(this.flagsReadyKey);
|
|
425
|
-
this.flags = new Map();
|
|
426
|
-
this.flagsReady = false;
|
|
427
|
-
this.experimentTracked.clear();
|
|
428
|
-
} catch (error) {
|
|
429
|
-
MixpanelLogger.log(this.token, "Error clearing flag cache:", error);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// snake_case aliases for API consistency with mixpanel-js
|
|
434
|
-
are_flags_ready() {
|
|
435
|
-
return this.areFlagsReady();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
get_variant(featureName, fallback) {
|
|
439
|
-
return this.getVariant(featureName, fallback);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
get_variant_sync(featureName, fallback) {
|
|
443
|
-
return this.getVariantSync(featureName, fallback);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
get_variant_value(featureName, fallbackValue) {
|
|
447
|
-
return this.getVariantValue(featureName, fallbackValue);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
get_variant_value_sync(featureName, fallbackValue) {
|
|
451
|
-
return this.getVariantValueSync(featureName, fallbackValue);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
is_enabled(featureName, fallbackValue = false) {
|
|
455
|
-
return this.isEnabled(featureName, fallbackValue);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
is_enabled_sync(featureName, fallbackValue = false) {
|
|
459
|
-
return this.isEnabledSync(featureName, fallbackValue);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
update_context(newContext, options) {
|
|
463
|
-
return this.updateContext(newContext, options);
|
|
464
|
-
}
|
|
465
|
-
}
|