mixpanel-react-native 3.1.2 → 3.2.0-beta.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/.claude/settings.local.json +12 -1
- package/.github/dependabot.yml +7 -0
- package/.github/workflows/node.js.yml +24 -1
- package/.vscode/settings.json +2 -1
- package/MixpanelReactNative.podspec +1 -1
- package/Samples/MixpanelExample/ios/MixpanelExample.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelExample/ios/Podfile.lock +1996 -0
- package/Samples/MixpanelStarter/.bundle/config +2 -0
- package/Samples/MixpanelStarter/.env.example +4 -0
- package/Samples/MixpanelStarter/.eslintrc.js +4 -0
- package/Samples/MixpanelStarter/.prettierrc.js +5 -0
- package/Samples/MixpanelStarter/.watchmanconfig +1 -0
- package/Samples/MixpanelStarter/App.tsx +10 -0
- package/Samples/MixpanelStarter/CLAUDE.md +538 -0
- package/Samples/MixpanelStarter/Gemfile +16 -0
- package/Samples/MixpanelStarter/INTEGRATION_GUIDE.md +606 -0
- package/Samples/MixpanelStarter/README.md +406 -0
- package/Samples/MixpanelStarter/__tests__/MixpanelContext.test.tsx +63 -0
- package/Samples/MixpanelStarter/android/app/build.gradle +119 -0
- package/Samples/MixpanelStarter/android/app/debug.keystore +0 -0
- package/Samples/MixpanelStarter/android/app/proguard-rules.pro +10 -0
- package/Samples/MixpanelStarter/android/app/src/main/AndroidManifest.xml +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainActivity.kt +22 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainApplication.kt +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/strings.xml +3 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/styles.xml +9 -0
- package/Samples/MixpanelStarter/android/build.gradle +21 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/Samples/MixpanelStarter/android/gradle.properties +44 -0
- package/Samples/MixpanelStarter/android/gradlew +251 -0
- package/Samples/MixpanelStarter/android/gradlew.bat +99 -0
- package/Samples/MixpanelStarter/android/settings.gradle +6 -0
- package/Samples/MixpanelStarter/app.json +4 -0
- package/Samples/MixpanelStarter/babel.config.js +14 -0
- package/Samples/MixpanelStarter/index.js +9 -0
- package/Samples/MixpanelStarter/ios/.xcode.env +11 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/AppDelegate.swift +48 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/Contents.json +6 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Info.plist +55 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/LaunchScreen.storyboard +47 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/PrivacyInfo.xcprivacy +38 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/project.pbxproj +482 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/xcshareddata/xcschemes/MixpanelStarter.xcscheme +88 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelStarter/ios/Podfile +34 -0
- package/Samples/MixpanelStarter/ios/Podfile.lock +2839 -0
- package/Samples/MixpanelStarter/jest.config.js +3 -0
- package/Samples/MixpanelStarter/metro.config.js +42 -0
- package/Samples/MixpanelStarter/package-lock.json +12141 -0
- package/Samples/MixpanelStarter/package.json +51 -0
- package/Samples/MixpanelStarter/src/@types/env.d.ts +3 -0
- package/Samples/MixpanelStarter/src/App.tsx +83 -0
- package/Samples/MixpanelStarter/src/components/ActionButton.tsx +92 -0
- package/Samples/MixpanelStarter/src/components/ErrorBoundary.tsx +81 -0
- package/Samples/MixpanelStarter/src/components/EventTrackingLog.tsx +163 -0
- package/Samples/MixpanelStarter/src/components/FlagCard.tsx +199 -0
- package/Samples/MixpanelStarter/src/components/InfoCard.tsx +77 -0
- package/Samples/MixpanelStarter/src/components/TestResultDisplay.tsx +181 -0
- package/Samples/MixpanelStarter/src/constants/tracking.ts +77 -0
- package/Samples/MixpanelStarter/src/contexts/MixpanelContext.tsx +159 -0
- package/Samples/MixpanelStarter/src/screens/FeatureFlagsScreen.tsx +1011 -0
- package/Samples/MixpanelStarter/src/screens/HomeScreen.tsx +307 -0
- package/Samples/MixpanelStarter/src/screens/OnboardingScreen.tsx +253 -0
- package/Samples/MixpanelStarter/src/screens/SettingsScreen.tsx +316 -0
- package/Samples/MixpanelStarter/src/types/flags.types.ts +42 -0
- package/Samples/MixpanelStarter/src/types/mixpanel.types.ts +26 -0
- package/Samples/MixpanelStarter/tsconfig.json +13 -0
- package/__tests__/flags.test.js +730 -0
- package/__tests__/index.test.js +7 -3
- package/__tests__/jest_setup.js +18 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java +272 -2
- package/index.d.ts +64 -1
- package/index.js +42 -3
- package/ios/MixpanelReactNative.m +19 -1
- package/ios/MixpanelReactNative.swift +183 -5
- package/javascript/mixpanel-flags-js.js +463 -0
- package/javascript/mixpanel-flags.js +290 -0
- package/javascript/mixpanel-main.js +13 -1
- package/package.json +2 -2
|
@@ -18,16 +18,39 @@ open class MixpanelReactNative: NSObject {
|
|
|
18
18
|
properties: [String: Any],
|
|
19
19
|
serverURL: String,
|
|
20
20
|
useGzipCompression: Bool = false,
|
|
21
|
+
featureFlagsOptions: [String: Any]?,
|
|
21
22
|
resolver resolve: RCTPromiseResolveBlock,
|
|
22
23
|
rejecter reject: RCTPromiseRejectBlock) -> Void {
|
|
23
24
|
let autoProps = properties // copy
|
|
24
25
|
AutomaticProperties.setAutomaticProperties(autoProps)
|
|
25
26
|
let propsProcessed = MixpanelTypeHandler.processProperties(properties: autoProps)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
|
|
28
|
+
// Handle feature flags options
|
|
29
|
+
var featureFlagsEnabled = false
|
|
30
|
+
var featureFlagsContext: [String: Any]? = nil
|
|
31
|
+
|
|
32
|
+
if let flagsOptions = featureFlagsOptions {
|
|
33
|
+
featureFlagsEnabled = flagsOptions["enabled"] as? Bool ?? false
|
|
34
|
+
featureFlagsContext = flagsOptions["context"] as? [String: Any]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create MixpanelOptions with all configuration including feature flags
|
|
38
|
+
let options = MixpanelOptions(
|
|
39
|
+
token: token,
|
|
40
|
+
flushInterval: Constants.DEFAULT_FLUSH_INTERVAL,
|
|
41
|
+
instanceName: token,
|
|
42
|
+
trackAutomaticEvents: trackAutomaticEvents,
|
|
43
|
+
optOutTrackingByDefault: optOutTrackingByDefault,
|
|
44
|
+
useUniqueDistinctId: false,
|
|
45
|
+
superProperties: propsProcessed,
|
|
46
|
+
serverURL: serverURL,
|
|
47
|
+
proxyServerConfig: nil,
|
|
48
|
+
useGzipCompression: useGzipCompression,
|
|
49
|
+
featureFlagsEnabled: featureFlagsEnabled,
|
|
50
|
+
featureFlagsContext: featureFlagsContext ?? [:]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Mixpanel.initialize(options: options)
|
|
31
54
|
resolve(true)
|
|
32
55
|
}
|
|
33
56
|
|
|
@@ -460,4 +483,159 @@ open class MixpanelReactNative: NSObject {
|
|
|
460
483
|
return Mixpanel.getInstance(name: token)
|
|
461
484
|
}
|
|
462
485
|
|
|
486
|
+
// MARK: - Feature Flags
|
|
487
|
+
|
|
488
|
+
@objc
|
|
489
|
+
func loadFlags(_ token: String,
|
|
490
|
+
resolver resolve: RCTPromiseResolveBlock,
|
|
491
|
+
rejecter reject: RCTPromiseRejectBlock) -> Void {
|
|
492
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
493
|
+
let flags = instance.flags else {
|
|
494
|
+
resolve(nil)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
flags.loadFlags()
|
|
498
|
+
resolve(nil)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
@objc
|
|
502
|
+
func areFlagsReadySync(_ token: String) -> NSNumber {
|
|
503
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token) else {
|
|
504
|
+
NSLog("[Mixpanel - areFlagsReadySync: instance is nil for token: \(token)]")
|
|
505
|
+
return NSNumber(value: false)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
guard let flags = instance.flags else {
|
|
509
|
+
NSLog("[Mixpanel - areFlagsReadySync: flags is nil")
|
|
510
|
+
return NSNumber(value: false)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let ready = flags.areFlagsReady()
|
|
514
|
+
NSLog("[Mixpanel - areFlagsReadySync: flags ready = \(ready)")
|
|
515
|
+
return NSNumber(value: ready)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@objc
|
|
519
|
+
func getVariantSync(_ token: String,
|
|
520
|
+
featureName: String,
|
|
521
|
+
fallback: [String: Any]) -> [String: Any] {
|
|
522
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
523
|
+
let flags = instance.flags else {
|
|
524
|
+
return fallback
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let fallbackVariant = convertDictToVariant(fallback)
|
|
528
|
+
let variant = flags.getVariantSync(featureName, fallback: fallbackVariant)
|
|
529
|
+
return convertVariantToDict(variant)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@objc
|
|
533
|
+
func getVariantValueSync(_ token: String,
|
|
534
|
+
featureName: String,
|
|
535
|
+
fallbackValue: Any) -> Any {
|
|
536
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
537
|
+
let flags = instance.flags else {
|
|
538
|
+
return fallbackValue
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return flags.getVariantValueSync(featureName, fallbackValue: fallbackValue) ?? fallbackValue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@objc
|
|
545
|
+
func isEnabledSync(_ token: String,
|
|
546
|
+
featureName: String,
|
|
547
|
+
fallbackValue: Bool) -> NSNumber {
|
|
548
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
549
|
+
let flags = instance.flags else {
|
|
550
|
+
return NSNumber(value: fallbackValue)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let enabled = flags.isEnabledSync(featureName, fallbackValue: fallbackValue)
|
|
554
|
+
return NSNumber(value: enabled)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@objc
|
|
558
|
+
func getVariant(_ token: String,
|
|
559
|
+
featureName: String,
|
|
560
|
+
fallback: [String: Any],
|
|
561
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
562
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
563
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
564
|
+
let flags = instance.flags else {
|
|
565
|
+
resolve(fallback)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let fallbackVariant = convertDictToVariant(fallback)
|
|
570
|
+
flags.getVariant(featureName, fallback: fallbackVariant) { variant in
|
|
571
|
+
resolve(self.convertVariantToDict(variant))
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
@objc
|
|
576
|
+
func getVariantValue(_ token: String,
|
|
577
|
+
featureName: String,
|
|
578
|
+
fallbackValue: Any,
|
|
579
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
580
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
581
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
582
|
+
let flags = instance.flags else {
|
|
583
|
+
resolve(fallbackValue)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
flags.getVariantValue(featureName, fallbackValue: fallbackValue) { value in
|
|
588
|
+
resolve(value)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@objc
|
|
593
|
+
func isEnabled(_ token: String,
|
|
594
|
+
featureName: String,
|
|
595
|
+
fallbackValue: Bool,
|
|
596
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
597
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
598
|
+
guard let instance = MixpanelReactNative.getMixpanelInstance(token),
|
|
599
|
+
let flags = instance.flags else {
|
|
600
|
+
resolve(fallbackValue)
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
flags.isEnabled(featureName, fallbackValue: fallbackValue) { isEnabled in
|
|
605
|
+
resolve(isEnabled)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Helper methods for variant conversion
|
|
610
|
+
private func convertDictToVariant(_ dict: [String: Any]) -> MixpanelFlagVariant {
|
|
611
|
+
let key = dict["key"] as? String ?? ""
|
|
612
|
+
let value = dict["value"] ?? NSNull()
|
|
613
|
+
let experimentID = dict["experimentID"] as? String
|
|
614
|
+
let isExperimentActive = dict["isExperimentActive"] as? Bool
|
|
615
|
+
let isQATester = dict["isQATester"] as? Bool
|
|
616
|
+
|
|
617
|
+
return MixpanelFlagVariant(
|
|
618
|
+
key: key,
|
|
619
|
+
value: value,
|
|
620
|
+
isExperimentActive: isExperimentActive,
|
|
621
|
+
isQATester: isQATester,
|
|
622
|
+
experimentID: experimentID
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private func convertVariantToDict(_ variant: MixpanelFlagVariant) -> [String: Any] {
|
|
627
|
+
var dict: [String: Any] = [
|
|
628
|
+
"key": variant.key,
|
|
629
|
+
"value": variant.value ?? NSNull()
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
if let experimentID = variant.experimentID {
|
|
633
|
+
dict["experimentID"] = experimentID
|
|
634
|
+
}
|
|
635
|
+
dict["isExperimentActive"] = variant.isExperimentActive
|
|
636
|
+
dict["isQATester"] = variant.isQATester
|
|
637
|
+
|
|
638
|
+
return dict
|
|
639
|
+
}
|
|
640
|
+
|
|
463
641
|
}
|
|
@@ -0,0 +1,463 @@
|
|
|
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) {
|
|
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 = {};
|
|
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(() => {});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return variant;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get variant value synchronously
|
|
313
|
+
*/
|
|
314
|
+
getVariantValueSync(featureName, fallbackValue) {
|
|
315
|
+
const variant = this.getVariantSync(featureName, {
|
|
316
|
+
key: featureName,
|
|
317
|
+
value: fallbackValue,
|
|
318
|
+
});
|
|
319
|
+
return variant.value;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if feature is enabled synchronously
|
|
324
|
+
* Enhanced with boolean validation like mixpanel-js
|
|
325
|
+
*/
|
|
326
|
+
isEnabledSync(featureName, fallbackValue = false) {
|
|
327
|
+
const value = this.getVariantValueSync(featureName, fallbackValue);
|
|
328
|
+
|
|
329
|
+
// Validate boolean type (mixpanel-js pattern)
|
|
330
|
+
if (value !== true && value !== false) {
|
|
331
|
+
MixpanelLogger.error(
|
|
332
|
+
this.token,
|
|
333
|
+
`Feature flag "${featureName}" value: ${value} is not a boolean; returning fallback value: ${fallbackValue}`
|
|
334
|
+
);
|
|
335
|
+
return fallbackValue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get variant asynchronously
|
|
343
|
+
*/
|
|
344
|
+
async getVariant(featureName, fallback) {
|
|
345
|
+
// If flags not ready, try to load them
|
|
346
|
+
if (!this.flagsReady) {
|
|
347
|
+
await this.loadFlags();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!this.flags.has(featureName)) {
|
|
351
|
+
return fallback;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const variant = this.flags.get(featureName);
|
|
355
|
+
|
|
356
|
+
// Track experiment on first access
|
|
357
|
+
if (!this.experimentTracked.has(featureName)) {
|
|
358
|
+
await this.trackExperimentStarted(featureName, variant);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return variant;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get variant value asynchronously
|
|
366
|
+
*/
|
|
367
|
+
async getVariantValue(featureName, fallbackValue) {
|
|
368
|
+
const variant = await this.getVariant(featureName, {
|
|
369
|
+
key: featureName,
|
|
370
|
+
value: fallbackValue,
|
|
371
|
+
});
|
|
372
|
+
return variant.value;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if feature is enabled asynchronously
|
|
377
|
+
*/
|
|
378
|
+
async isEnabled(featureName, fallbackValue = false) {
|
|
379
|
+
const value = await this.getVariantValue(featureName, fallbackValue);
|
|
380
|
+
if (typeof value === "boolean") {
|
|
381
|
+
return value;
|
|
382
|
+
} else {
|
|
383
|
+
MixpanelLogger.log(this.token, `Flag "${featureName}" value is not boolean:`, value);
|
|
384
|
+
return fallbackValue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Update context and reload flags
|
|
390
|
+
* Aligned with mixpanel-js API signature
|
|
391
|
+
* @param {object} newContext - New context properties to add/update
|
|
392
|
+
* @param {object} options - Options object
|
|
393
|
+
* @param {boolean} options.replace - If true, replace entire context instead of merging
|
|
394
|
+
*/
|
|
395
|
+
async updateContext(newContext, options = {}) {
|
|
396
|
+
if (options.replace) {
|
|
397
|
+
// Replace entire context
|
|
398
|
+
this.context = { ...newContext };
|
|
399
|
+
} else {
|
|
400
|
+
// Merge with existing context (default)
|
|
401
|
+
this.context = {
|
|
402
|
+
...this.context,
|
|
403
|
+
...newContext,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Clear experiment tracking since context changed
|
|
408
|
+
this.experimentTracked.clear();
|
|
409
|
+
|
|
410
|
+
// Reload flags with new context
|
|
411
|
+
await this.loadFlags();
|
|
412
|
+
|
|
413
|
+
MixpanelLogger.log(this.token, "Context updated, flags reloaded");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Clear cached flags
|
|
418
|
+
*/
|
|
419
|
+
async clearCache() {
|
|
420
|
+
try {
|
|
421
|
+
await this.storage.removeItem(this.flagsCacheKey);
|
|
422
|
+
await this.storage.removeItem(this.flagsReadyKey);
|
|
423
|
+
this.flags = new Map();
|
|
424
|
+
this.flagsReady = false;
|
|
425
|
+
this.experimentTracked.clear();
|
|
426
|
+
} catch (error) {
|
|
427
|
+
MixpanelLogger.log(this.token, "Error clearing flag cache:", error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// snake_case aliases for API consistency with mixpanel-js
|
|
432
|
+
are_flags_ready() {
|
|
433
|
+
return this.areFlagsReady();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
get_variant(featureName, fallback) {
|
|
437
|
+
return this.getVariant(featureName, fallback);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
get_variant_sync(featureName, fallback) {
|
|
441
|
+
return this.getVariantSync(featureName, fallback);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
get_variant_value(featureName, fallbackValue) {
|
|
445
|
+
return this.getVariantValue(featureName, fallbackValue);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
get_variant_value_sync(featureName, fallbackValue) {
|
|
449
|
+
return this.getVariantValueSync(featureName, fallbackValue);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
is_enabled(featureName, fallbackValue = false) {
|
|
453
|
+
return this.isEnabled(featureName, fallbackValue);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
is_enabled_sync(featureName, fallbackValue = false) {
|
|
457
|
+
return this.isEnabledSync(featureName, fallbackValue);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
update_context(newContext, options) {
|
|
461
|
+
return this.updateContext(newContext, options);
|
|
462
|
+
}
|
|
463
|
+
}
|