mixpanel-react-native 3.2.0-beta.3 → 3.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.
@@ -18,39 +18,16 @@ open class MixpanelReactNative: NSObject {
18
18
  properties: [String: Any],
19
19
  serverURL: String,
20
20
  useGzipCompression: Bool = false,
21
- featureFlagsOptions: [String: Any]?,
22
21
  resolver resolve: RCTPromiseResolveBlock,
23
22
  rejecter reject: RCTPromiseRejectBlock) -> Void {
24
23
  let autoProps = properties // copy
25
24
  AutomaticProperties.setAutomaticProperties(autoProps)
26
25
  let propsProcessed = MixpanelTypeHandler.processProperties(properties: autoProps)
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)
26
+ Mixpanel.initialize(token: token, trackAutomaticEvents: trackAutomaticEvents, flushInterval: Constants.DEFAULT_FLUSH_INTERVAL,
27
+ instanceName: token, optOutTrackingByDefault: optOutTrackingByDefault,
28
+ superProperties: propsProcessed,
29
+ serverURL: serverURL,
30
+ useGzipCompression: useGzipCompression)
54
31
  resolve(true)
55
32
  }
56
33
 
@@ -483,159 +460,4 @@ open class MixpanelReactNative: NSObject {
483
460
  return Mixpanel.getInstance(name: token)
484
461
  }
485
462
 
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
-
641
463
  }
@@ -21,17 +21,10 @@ export default class MixpanelMain {
21
21
  trackAutomaticEvents = false,
22
22
  optOutTrackingDefault = false,
23
23
  superProperties = null,
24
- serverURL = "https://api.mixpanel.com",
25
- useGzipCompression = false,
26
- featureFlagsOptions = {}
24
+ serverURL = "https://api.mixpanel.com"
27
25
  ) {
28
26
  MixpanelLogger.log(token, `Initializing Mixpanel`);
29
27
 
30
- // Store feature flags options for later use
31
- this.featureFlagsOptions = featureFlagsOptions;
32
- this.featureFlagsEnabled = featureFlagsOptions.enabled || false;
33
- this.featureFlagsContext = featureFlagsOptions.context || {};
34
-
35
28
  await this.mixpanelPersistent.initializationCompletePromise(token);
36
29
  if (optOutTrackingDefault) {
37
30
  await this.optOutTracking(token);
@@ -44,11 +37,6 @@ export default class MixpanelMain {
44
37
  await this.registerSuperProperties(token, {
45
38
  ...superProperties,
46
39
  });
47
-
48
- // Initialize feature flags if enabled
49
- if (this.featureFlagsEnabled) {
50
- MixpanelLogger.log(token, "Feature flags enabled during initialization");
51
- }
52
40
  }
53
41
 
54
42
  getMetaData() {
@@ -82,14 +70,6 @@ export default class MixpanelMain {
82
70
  await this.mixpanelPersistent.reset(token);
83
71
  }
84
72
 
85
- /**
86
- * Get the feature flags context that was provided during initialization
87
- * @returns {object} The feature flags context object
88
- */
89
- getFeatureFlagsContext() {
90
- return this.featureFlagsContext || {};
91
- }
92
-
93
73
  async track(token, eventName, properties) {
94
74
  if (this.mixpanelPersistent.getOptedOut(token)) {
95
75
  MixpanelLogger.log(
@@ -15,77 +15,37 @@ export const MixpanelNetwork = (() => {
15
15
  serverURL,
16
16
  useIPAddressForGeoLocation,
17
17
  retryCount = 0,
18
- headers = {},
19
18
  }) => {
20
19
  retryCount = retryCount || 0;
21
- // Use & if endpoint already has query params, otherwise use ?
22
- const separator = endpoint.includes('?') ? '&' : '?';
23
- const url = `${serverURL}${endpoint}${separator}ip=${+useIPAddressForGeoLocation}`;
20
+ const url = `${serverURL}${endpoint}?ip=${+useIPAddressForGeoLocation}`;
24
21
  MixpanelLogger.log(token, `Sending request to: ${url}`);
25
22
 
26
23
  try {
27
- // Determine if this is a GET or POST request based on data presence
28
- const isGetRequest = data === null || data === undefined;
24
+ const response = await fetch(url, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/x-www-form-urlencoded",
28
+ },
29
+ body: `data=${encodeURIComponent(JSON.stringify(data))}`,
30
+ });
29
31
 
30
- const fetchOptions = isGetRequest
31
- ? {
32
- method: "GET",
33
- headers: headers,
34
- }
35
- : {
36
- method: "POST",
37
- headers: {
38
- "Content-Type": "application/x-www-form-urlencoded",
39
- ...headers,
40
- },
41
- body: `data=${encodeURIComponent(JSON.stringify(data))}`,
42
- };
43
-
44
- const response = await fetch(url, fetchOptions);
45
-
46
- // Handle GET requests differently - they return the data directly
47
- if (isGetRequest) {
48
- if (response.status === 200) {
49
- const responseData = await response.json();
50
- MixpanelLogger.log(token, `GET request successful: ${endpoint}`);
51
- return responseData;
52
- } else {
53
- throw new MixpanelHttpError(
54
- `HTTP error! status: ${response.status}`,
55
- response.status
56
- );
57
- }
58
- } else {
59
- // Handle POST requests (existing logic)
60
- const responseBody = await response.json();
61
- if (response.status !== 200) {
62
- throw new MixpanelHttpError(
63
- `HTTP error! status: ${response.status}`,
64
- response.status
65
- );
66
- }
67
-
68
- const message =
69
- responseBody === 0
70
- ? `${url} api rejected some items`
71
- : `Mixpanel batch sent successfully, endpoint: ${endpoint}, data: ${JSON.stringify(
72
- data
73
- )}`;
74
-
75
- MixpanelLogger.log(token, message);
76
- return responseBody;
32
+ const responseBody = await response.json();
33
+ if (response.status !== 200) {
34
+ throw new MixpanelHttpError(
35
+ `HTTP error! status: ${response.status}`,
36
+ response.status
37
+ );
77
38
  }
78
- } catch (error) {
79
- // Determine if this is a GET or POST request
80
- const isGetRequest = data === null || data === undefined;
81
39
 
82
- // For GET requests (like flags), don't retry on 404 or other client errors
83
- if (isGetRequest && error.code >= 400 && error.code < 500) {
84
- MixpanelLogger.log(token, `GET request failed with status ${error.code}, not retrying`);
85
- throw error;
86
- }
40
+ const message =
41
+ responseBody === 0
42
+ ? `${url} api rejected some items`
43
+ : `Mixpanel batch sent successfully, endpoint: ${endpoint}, data: ${JSON.stringify(
44
+ data
45
+ )}`;
87
46
 
88
- // For POST requests or non-client errors, handle retries
47
+ MixpanelLogger.log(token, message);
48
+ } catch (error) {
89
49
  if (error.code === 400) {
90
50
  // This indicates that the data was invalid and we should not retry
91
51
  throw new MixpanelHttpError(
@@ -93,35 +53,30 @@ export const MixpanelNetwork = (() => {
93
53
  error.code
94
54
  );
95
55
  }
96
-
97
56
  MixpanelLogger.warn(
98
57
  token,
99
58
  `API request to ${url} has failed with reason: ${error.message}`
100
59
  );
101
-
102
- // Only retry for POST requests or server errors
103
- if (!isGetRequest || error.code >= 500) {
104
- const maxRetries = 5;
105
- const backoff = Math.min(2 ** retryCount * 2000, 60000); // Exponential backoff
106
- if (retryCount < maxRetries) {
107
- MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`);
108
- await new Promise((resolve) => setTimeout(resolve, backoff));
109
- return sendRequest({
110
- token,
111
- endpoint,
112
- data,
113
- serverURL,
114
- useIPAddressForGeoLocation,
115
- retryCount: retryCount + 1,
116
- });
117
- }
60
+ const maxRetries = 5;
61
+ const backoff = Math.min(2 ** retryCount * 2000, 60000); // Exponential backoff
62
+ if (retryCount < maxRetries) {
63
+ MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`);
64
+ await new Promise((resolve) => setTimeout(resolve, backoff));
65
+ return sendRequest({
66
+ token,
67
+ endpoint,
68
+ data,
69
+ serverURL,
70
+ useIPAddressForGeoLocation,
71
+ retryCount: retryCount + 1,
72
+ });
73
+ } else {
74
+ MixpanelLogger.warn(token, `Max retries reached. Giving up.`);
75
+ throw new MixpanelHttpError(
76
+ `HTTP error! status: ${error.code}`,
77
+ error.code
78
+ );
118
79
  }
119
-
120
- MixpanelLogger.warn(token, `Request failed. Not retrying.`);
121
- throw new MixpanelHttpError(
122
- `HTTP error! status: ${error.code || 'unknown'}`,
123
- error.code
124
- );
125
80
  }
126
81
  };
127
82
 
@@ -11,41 +11,9 @@ import {
11
11
 
12
12
  import "react-native-get-random-values"; // Polyfill for crypto.getRandomValues
13
13
  import { AsyncStorageAdapter } from "./mixpanel-storage";
14
- import uuid from "uuid";
14
+ import { v4 as uuidv4 } from "uuid";
15
15
  import { MixpanelLogger } from "mixpanel-react-native/javascript/mixpanel-logger";
16
16
 
17
- /**
18
- * Generate a UUID v4, with cross-platform fallbacks
19
- * Tries: uuid package → Web Crypto API → manual generation
20
- */
21
- function generateUUID() {
22
- // Try uuid package first (works in React Native with polyfill)
23
- try {
24
- const result = uuid.v4();
25
- if (result) return result;
26
- } catch (e) {
27
- // Fall through to alternatives
28
- }
29
-
30
- // Try Web Crypto API (modern browsers)
31
- const cryptoObj =
32
- (typeof globalThis !== "undefined" && globalThis.crypto) ||
33
- (typeof window !== "undefined" && window.crypto) ||
34
- (typeof crypto !== "undefined" && crypto);
35
-
36
- if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
37
- return cryptoObj.randomUUID();
38
- }
39
-
40
- // Last resort: manual UUID v4 generation using Math.random
41
- // Less secure but functional for device IDs
42
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
43
- const r = (Math.random() * 16) | 0;
44
- const v = c === "x" ? r : (r & 0x3) | 0x8;
45
- return v.toString(16);
46
- });
47
- }
48
-
49
17
  export class MixpanelPersistent {
50
18
  static instance;
51
19
 
@@ -74,7 +42,7 @@ export class MixpanelPersistent {
74
42
  }
75
43
 
76
44
  async initializationCompletePromise(token) {
77
- return Promise.all([
45
+ await Promise.all([
78
46
  this.loadIdentity(token),
79
47
  this.loadSuperProperties(token),
80
48
  this.loadTimeEvents(token),
@@ -99,8 +67,8 @@ export class MixpanelPersistent {
99
67
  this._identity[token].deviceId = storageToken;
100
68
 
101
69
  if (!this._identity[token].deviceId) {
102
- // Generate device ID with cross-platform UUID generation
103
- this._identity[token].deviceId = generateUUID();
70
+ // Generate device ID using uuidv4() with polyfilled crypto.getRandomValues
71
+ this._identity[token].deviceId = uuidv4();
104
72
  await this.storageAdapter.setItem(
105
73
  getDeviceIdKey(token),
106
74
  this._identity[token].deviceId
@@ -12,9 +12,9 @@ export class AsyncStorageAdapter {
12
12
  }
13
13
  } catch {
14
14
  console.error(
15
- "[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null. Please run 'npm install @react-native-async-storage/async-storage' or follow the Mixpanel guide to set up your own Storage class."
15
+ "[Mixpanel] AsyncStorage not available. Install @react-native-async-storage/async-storage (^1.15.0 or ^2.0.0), or provide a custom storage implementation. See: https://github.com/mixpanel/mixpanel-react-native#readme"
16
16
  );
17
- console.error("[Mixpanel] Falling back to in-memory storage");
17
+ console.error("[Mixpanel] Falling back to in-memory storage. Data will not persist across app restarts.");
18
18
  this.storage = new InMemoryStorage();
19
19
  }
20
20
  } else {
package/package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "name": "mixpanel-react-native",
3
- "version": "3.2.0-beta.3",
3
+ "version": "3.2.0",
4
4
  "description": "Official React Native Tracking Library for Mixpanel Analytics",
5
5
  "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "index.d.ts",
9
+ "javascript/",
10
+ "ios/",
11
+ "android/src/",
12
+ "android/build.gradle",
13
+ "MixpanelReactNative.podspec",
14
+ "react-native.config.js",
15
+ "README.md",
16
+ "LICENSE.md",
17
+ "CHANGELOG.md"
18
+ ],
6
19
  "scripts": {
7
20
  "test": "jest"
8
21
  },
@@ -31,18 +44,17 @@
31
44
  "mp_lib": "react-native"
32
45
  },
33
46
  "devDependencies": {
34
- "@babel/core": "^7.12.3",
35
- "@babel/runtime": "^7.12.1",
36
- "@react-native-community/eslint-config": "^2.0.0",
37
- "babel-jest": "^26.6.0",
38
- "eslint": "^7.11.0",
39
- "jest": "^26.6.3",
47
+ "@react-native-async-storage/async-storage": "^1.24.0",
48
+ "@babel/core": "^7.26.0",
49
+ "@babel/runtime": "^7.26.0",
50
+ "@react-native-community/eslint-config": "^3.2.0",
51
+ "babel-jest": "^29.7.0",
52
+ "eslint": "^8.57.0",
53
+ "jest": "^29.7.0",
40
54
  "jest-fetch-mock": "^3.0.3",
41
- "jsdoc": "^4.0.5",
42
- "metro-react-native-babel-preset": "^0.63.0",
43
- "react-native": "^0.63.3",
44
- "react-native-dotenv": "^3.4.11",
45
- "react-test-renderer": "16.13.1"
55
+ "metro-react-native-babel-preset": "^0.77.0",
56
+ "react-native": "^0.76.0",
57
+ "react-test-renderer": "^18.3.1"
46
58
  },
47
59
  "jest": {
48
60
  "modulePathIgnorePatterns": [
@@ -56,13 +68,21 @@
56
68
  ],
57
69
  "verbose": true,
58
70
  "preset": "react-native",
59
- "transform": {
60
- "^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js"
61
- }
71
+ "transformIgnorePatterns": [
72
+ "node_modules/(?!(@react-native|react-native|react-native-get-random-values|uuid)/)"
73
+ ],
74
+ "testEnvironment": "node"
62
75
  },
63
76
  "dependencies": {
64
- "@react-native-async-storage/async-storage": "^1.24.0",
65
77
  "react-native-get-random-values": "^1.9.0",
66
- "uuid": "3.3.2"
78
+ "uuid": "^9.0.1"
79
+ },
80
+ "peerDependencies": {
81
+ "@react-native-async-storage/async-storage": "^1.15.0 || ^2.0.0"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "@react-native-async-storage/async-storage": {
85
+ "optional": true
86
+ }
67
87
  }
68
- }
88
+ }
@@ -1,7 +0,0 @@
1
- # .github/dependabot.yml
2
- version: 2
3
- updates:
4
- - package-ecosystem: "npm" # Or "yarn"
5
- directory: "/" # Tells Dependabot to scan root directory only
6
- schedule:
7
- interval: "daily"