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.
- package/CHANGELOG.md +33 -0
- package/FEATURE_FLAGS_JS_MODE_FINDINGS.md +119 -0
- package/FEATURE_FLAGS_QUICKSTART.md +399 -0
- package/MixpanelReactNative.podspec +1 -1
- package/README.md +29 -0
- package/index.js +74 -18
- package/javascript/mixpanel-config.js +9 -5
- package/javascript/mixpanel-flags-js.js +5 -3
- package/javascript/mixpanel-flags.js +475 -55
- package/javascript/mixpanel-main.js +8 -0
- package/javascript/mixpanel-network.js +86 -41
- package/javascript/mixpanel-persistent.js +35 -3
- package/package.json +4 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
token,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
71
|
-
this._identity[token].deviceId =
|
|
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.
|
|
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.
|
|
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
|
}
|