mixpanel-react-native 3.2.0-beta.1 → 3.2.0-beta.3

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.
@@ -82,6 +82,14 @@ export default class MixpanelMain {
82
82
  await this.mixpanelPersistent.reset(token);
83
83
  }
84
84
 
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
+
85
93
  async track(token, eventName, properties) {
86
94
  if (this.mixpanelPersistent.getOptedOut(token)) {
87
95
  MixpanelLogger.log(
@@ -15,37 +15,77 @@ export const MixpanelNetwork = (() => {
15
15
  serverURL,
16
16
  useIPAddressForGeoLocation,
17
17
  retryCount = 0,
18
+ headers = {},
18
19
  }) => {
19
20
  retryCount = retryCount || 0;
20
- const url = `${serverURL}${endpoint}?ip=${+useIPAddressForGeoLocation}`;
21
+ // Use & if endpoint already has query params, otherwise use ?
22
+ const separator = endpoint.includes('?') ? '&' : '?';
23
+ const url = `${serverURL}${endpoint}${separator}ip=${+useIPAddressForGeoLocation}`;
21
24
  MixpanelLogger.log(token, `Sending request to: ${url}`);
22
25
 
23
26
  try {
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
- });
27
+ // Determine if this is a GET or POST request based on data presence
28
+ const isGetRequest = data === null || data === undefined;
31
29
 
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
- );
38
- }
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
+ }
39
67
 
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
- )}`;
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
+ )}`;
46
74
 
47
- MixpanelLogger.log(token, message);
75
+ MixpanelLogger.log(token, message);
76
+ return responseBody;
77
+ }
48
78
  } catch (error) {
79
+ // Determine if this is a GET or POST request
80
+ const isGetRequest = data === null || data === undefined;
81
+
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
+ }
87
+
88
+ // For POST requests or non-client errors, handle retries
49
89
  if (error.code === 400) {
50
90
  // This indicates that the data was invalid and we should not retry
51
91
  throw new MixpanelHttpError(
@@ -53,30 +93,35 @@ export const MixpanelNetwork = (() => {
53
93
  error.code
54
94
  );
55
95
  }
96
+
56
97
  MixpanelLogger.warn(
57
98
  token,
58
99
  `API request to ${url} has failed with reason: ${error.message}`
59
100
  );
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
- );
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
+ }
79
118
  }
119
+
120
+ MixpanelLogger.warn(token, `Request failed. Not retrying.`);
121
+ throw new MixpanelHttpError(
122
+ `HTTP error! status: ${error.code || 'unknown'}`,
123
+ error.code
124
+ );
80
125
  }
81
126
  };
82
127
 
@@ -14,6 +14,38 @@ import { AsyncStorageAdapter } from "./mixpanel-storage";
14
14
  import uuid 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
+
17
49
  export class MixpanelPersistent {
18
50
  static instance;
19
51
 
@@ -42,7 +74,7 @@ export class MixpanelPersistent {
42
74
  }
43
75
 
44
76
  async initializationCompletePromise(token) {
45
- Promise.all([
77
+ return Promise.all([
46
78
  this.loadIdentity(token),
47
79
  this.loadSuperProperties(token),
48
80
  this.loadTimeEvents(token),
@@ -67,8 +99,8 @@ export class MixpanelPersistent {
67
99
  this._identity[token].deviceId = storageToken;
68
100
 
69
101
  if (!this._identity[token].deviceId) {
70
- // Generate device ID using uuid.v4() with polyfilled crypto.getRandomValues
71
- this._identity[token].deviceId = uuid.v4();
102
+ // Generate device ID with cross-platform UUID generation
103
+ this._identity[token].deviceId = generateUUID();
72
104
  await this.storageAdapter.setItem(
73
105
  getDeviceIdKey(token),
74
106
  this._identity[token].deviceId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-react-native",
3
- "version": "3.2.0-beta.1",
3
+ "version": "3.2.0-beta.3",
4
4
  "description": "Official React Native Tracking Library for Mixpanel Analytics",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -38,8 +38,10 @@
38
38
  "eslint": "^7.11.0",
39
39
  "jest": "^26.6.3",
40
40
  "jest-fetch-mock": "^3.0.3",
41
+ "jsdoc": "^4.0.5",
41
42
  "metro-react-native-babel-preset": "^0.63.0",
42
43
  "react-native": "^0.63.3",
44
+ "react-native-dotenv": "^3.4.11",
43
45
  "react-test-renderer": "16.13.1"
44
46
  },
45
47
  "jest": {
@@ -59,7 +61,7 @@
59
61
  }
60
62
  },
61
63
  "dependencies": {
62
- "@react-native-async-storage/async-storage": "^1.21.0",
64
+ "@react-native-async-storage/async-storage": "^1.24.0",
63
65
  "react-native-get-random-values": "^1.9.0",
64
66
  "uuid": "3.3.2"
65
67
  }