From 53e02c1d1937585dc49822a4d8df492af2817d42 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:31:13 -0800 Subject: [PATCH 1/5] deps: Upgrade dependencies and add react-native-dotenv Upgrade AsyncStorage to latest version, add react-native-dotenv for environment variable management, and update package-lock.json with corresponding dependency changes. --- Samples/MixpanelExpo/package-lock.json | 77 ++++++++++++++++++-------- Samples/MixpanelExpo/yarn.lock | 27 +++++---- package-lock.json | 37 +++++++++++-- package.json | 3 +- 4 files changed, 104 insertions(+), 40 deletions(-) diff --git a/Samples/MixpanelExpo/package-lock.json b/Samples/MixpanelExpo/package-lock.json index 86475676..df724521 100644 --- a/Samples/MixpanelExpo/package-lock.json +++ b/Samples/MixpanelExpo/package-lock.json @@ -44,11 +44,11 @@ } }, ".yalc/mixpanel-react-native": { - "version": "3.0.9", + "version": "3.2.0-beta.2", "license": "Apache-2.0", "dependencies": { "@react-native-async-storage/async-storage": "^1.21.0", - "expo-crypto": "~13.0.2", + "react-native-get-random-values": "^1.9.0", "uuid": "3.3.2" } }, @@ -98,6 +98,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -1816,6 +1817,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "peer": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", @@ -5513,6 +5515,7 @@ "version": "0.73.21", "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.73.21.tgz", "integrity": "sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA==", + "peer": true, "dependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", @@ -6547,6 +6550,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -7542,6 +7546,7 @@ "version": "50.0.8", "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.8.tgz", "integrity": "sha512-8yXsoMbFRjWyEDNuFRtH0vTFvEjFnnwP+LceS6xmXGp+IW1hKdN1X6Bj1EUocFtepH0ruHDPCof1KvPoWfUWkw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.17.6", @@ -7587,18 +7592,6 @@ "expo": "*" } }, - "node_modules/expo-crypto": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-13.0.2.tgz", - "integrity": "sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.0" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-file-system": { "version": "16.0.7", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz", @@ -7630,6 +7623,7 @@ "version": "1.10.3", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.10.3.tgz", "integrity": "sha512-pn4n2Dl4iRh/zUeiChjRIe1C7EqOw1qhccr85viQV7W6l5vgRpY0osE51ij5LKg/kJmGRcJfs12+PwbdTplbKw==", + "peer": true, "dependencies": { "@expo/config": "~8.5.0", "chalk": "^4.1.0", @@ -7752,6 +7746,12 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.11.1.tgz", "integrity": "sha512-ddQEtCOgYHTLlFUe/yH67dDBIoct5VIULthyT3LRJbEwdpzAgueKsX2FYK02ldh440V87PWKCamh7R9evk1rrg==" }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8124,6 +8124,7 @@ "version": "15.8.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "peer": true, "engines": { "node": ">= 10.x" } @@ -11353,6 +11354,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -11370,6 +11372,7 @@ "version": "0.73.4", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz", "integrity": "sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg==", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "12.3.2", @@ -11420,6 +11423,18 @@ "react": "18.2.0" } }, + "node_modules/react-native-get-random-values": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz", + "integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==", + "license": "MIT", + "dependencies": { + "fast-base64-decode": "^1.0.0" + }, + "peerDependencies": { + "react-native": ">=0.56" + } + }, "node_modules/react-native-web": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.10.tgz", @@ -13135,6 +13150,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -14219,6 +14235,7 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "peer": true, "requires": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", @@ -16959,6 +16976,7 @@ "version": "0.73.21", "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.73.21.tgz", "integrity": "sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA==", + "peer": true, "requires": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", @@ -17761,6 +17779,7 @@ "version": "4.23.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "peer": true, "requires": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -18486,6 +18505,7 @@ "version": "50.0.8", "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.8.tgz", "integrity": "sha512-8yXsoMbFRjWyEDNuFRtH0vTFvEjFnnwP+LceS6xmXGp+IW1hKdN1X6Bj1EUocFtepH0ruHDPCof1KvPoWfUWkw==", + "peer": true, "requires": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.17.6", @@ -18525,14 +18545,6 @@ "@expo/config": "~8.5.0" } }, - "expo-crypto": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-13.0.2.tgz", - "integrity": "sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA==", - "requires": { - "base64-js": "^1.3.0" - } - }, "expo-file-system": { "version": "16.0.7", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz", @@ -18557,6 +18569,7 @@ "version": "1.10.3", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.10.3.tgz", "integrity": "sha512-pn4n2Dl4iRh/zUeiChjRIe1C7EqOw1qhccr85viQV7W6l5vgRpY0osE51ij5LKg/kJmGRcJfs12+PwbdTplbKw==", + "peer": true, "requires": { "@expo/config": "~8.5.0", "chalk": "^4.1.0", @@ -18649,6 +18662,11 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.11.1.tgz", "integrity": "sha512-ddQEtCOgYHTLlFUe/yH67dDBIoct5VIULthyT3LRJbEwdpzAgueKsX2FYK02ldh440V87PWKCamh7R9evk1rrg==" }, + "fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==" + }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -18930,7 +18948,8 @@ "graphql": { "version": "15.8.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", - "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==" + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "peer": true }, "graphql-tag": { "version": "2.12.6", @@ -20563,7 +20582,7 @@ "version": "file:.yalc/mixpanel-react-native", "requires": { "@react-native-async-storage/async-storage": "^1.21.0", - "expo-crypto": "~13.0.2", + "react-native-get-random-values": "^1.9.0", "uuid": "3.3.2" }, "dependencies": { @@ -21271,6 +21290,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -21285,6 +21305,7 @@ "version": "0.73.4", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz", "integrity": "sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg==", + "peer": true, "requires": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "12.3.2", @@ -21400,6 +21421,14 @@ } } }, + "react-native-get-random-values": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz", + "integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==", + "requires": { + "fast-base64-decode": "^1.0.0" + } + }, "react-native-web": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.10.tgz", diff --git a/Samples/MixpanelExpo/yarn.lock b/Samples/MixpanelExpo/yarn.lock index 83a1159f..f86e4227 100644 --- a/Samples/MixpanelExpo/yarn.lock +++ b/Samples/MixpanelExpo/yarn.lock @@ -2402,7 +2402,7 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3216,13 +3216,6 @@ expo-constants@~15.4.0: dependencies: "@expo/config" "~8.5.0" -expo-crypto@~13.0.2: - version "13.0.2" - resolved "https://registry.npmjs.org/expo-crypto/-/expo-crypto-13.0.2.tgz" - integrity sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA== - dependencies: - base64-js "^1.3.0" - expo-file-system@~16.0.0, expo-file-system@~16.0.7: version "16.0.7" resolved "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz" @@ -3285,6 +3278,11 @@ expo@*, expo@~50.0.8: fbemitter "^3.0.0" whatwg-url-without-unicode "8.0.0-3" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-glob@^3.2.5, fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" @@ -4635,11 +4633,11 @@ minizlib@^2.1.1: yallist "^4.0.0" "mixpanel-react-native@file:.yalc/mixpanel-react-native": - version "3.0.9" + version "3.2.0-beta.2" resolved "file:.yalc/mixpanel-react-native" dependencies: "@react-native-async-storage/async-storage" "^1.21.0" - expo-crypto "~13.0.2" + react-native-get-random-values "^1.9.0" uuid "3.3.2" mkdirp@^0.5.1, mkdirp@~0.5.1: @@ -5257,6 +5255,13 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-native-get-random-values@^1.9.0: + version "1.11.0" + resolved "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz" + integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== + dependencies: + fast-base64-decode "^1.0.0" + react-native-web@~0.19.6: version "0.19.10" resolved "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.10.tgz" @@ -5271,7 +5276,7 @@ react-native-web@~0.19.6: postcss-value-parser "^4.2.0" styleq "^0.1.3" -react-native@*, "react-native@^0.0.0-0 || >=0.60 <1.0", react-native@0.73.4: +react-native@*, "react-native@^0.0.0-0 || >=0.60 <1.0", react-native@>=0.56, react-native@0.73.4: version "0.73.4" resolved "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz" integrity sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg== diff --git a/package-lock.json b/package-lock.json index 55baafbf..b5bb448f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.2.0-beta.2", "license": "Apache-2.0", "dependencies": { - "@react-native-async-storage/async-storage": "^1.21.0", + "@react-native-async-storage/async-storage": "^1.24.0", "react-native-get-random-values": "^1.9.0", "uuid": "3.3.2" }, @@ -24,6 +24,7 @@ "jsdoc": "^4.0.5", "metro-react-native-babel-preset": "^0.63.0", "react-native": "^0.63.3", + "react-native-dotenv": "^3.4.11", "react-test-renderer": "16.13.1" } }, @@ -1201,6 +1202,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -2057,9 +2059,10 @@ } }, "node_modules/@react-native-async-storage/async-storage": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz", - "integrity": "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", "dependencies": { "merge-options": "^3.0.4" }, @@ -5086,6 +5089,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12864,6 +12880,19 @@ "react": "16.13.1" } }, + "node_modules/react-native-dotenv": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz", + "integrity": "sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dotenv": "^16.4.5" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/react-native-get-random-values": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz", diff --git a/package.json b/package.json index 056ae133..84afb0d5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "jsdoc": "^4.0.5", "metro-react-native-babel-preset": "^0.63.0", "react-native": "^0.63.3", + "react-native-dotenv": "^3.4.11", "react-test-renderer": "16.13.1" }, "jest": { @@ -60,7 +61,7 @@ } }, "dependencies": { - "@react-native-async-storage/async-storage": "^1.21.0", + "@react-native-async-storage/async-storage": "^1.24.0", "react-native-get-random-values": "^1.9.0", "uuid": "3.3.2" } From fbd4aa0b2e618cf4e7772b196b1402bab3c51996 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:23:55 -0800 Subject: [PATCH 2/5] Fallback UUID generation for MixpanelPersistent Provide a cross-platform UUID v4 generator to avoid requiring the uuid module in environments where it may be unavailable (fixes "Could not generate traceparent (UUID unavailable): Requiring unknown module \"undefined\"" errors). The change adds generateUUID() which tries the uuid package, then the Web Crypto API (crypto.randomUUID), and finally a Math.random-based fallback, and uses it when creating device IDs in MixpanelPersistent. --- javascript/mixpanel-persistent.js | 36 +++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/javascript/mixpanel-persistent.js b/javascript/mixpanel-persistent.js index 84b26abe..d0ad6765 100644 --- a/javascript/mixpanel-persistent.js +++ b/javascript/mixpanel-persistent.js @@ -14,6 +14,38 @@ import { AsyncStorageAdapter } from "./mixpanel-storage"; import uuid from "uuid"; import { MixpanelLogger } from "mixpanel-react-native/javascript/mixpanel-logger"; +/** + * Generate a UUID v4, with cross-platform fallbacks + * Tries: uuid package → Web Crypto API → manual generation + */ +function generateUUID() { + // Try uuid package first (works in React Native with polyfill) + try { + const result = uuid.v4(); + if (result) return result; + } catch (e) { + // Fall through to alternatives + } + + // Try Web Crypto API (modern browsers) + const cryptoObj = + (typeof globalThis !== "undefined" && globalThis.crypto) || + (typeof window !== "undefined" && window.crypto) || + (typeof crypto !== "undefined" && crypto); + + if (cryptoObj && typeof cryptoObj.randomUUID === "function") { + return cryptoObj.randomUUID(); + } + + // Last resort: manual UUID v4 generation using Math.random + // Less secure but functional for device IDs + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + export class MixpanelPersistent { static instance; @@ -67,8 +99,8 @@ export class MixpanelPersistent { this._identity[token].deviceId = storageToken; if (!this._identity[token].deviceId) { - // Generate device ID using uuid.v4() with polyfilled crypto.getRandomValues - this._identity[token].deviceId = uuid.v4(); + // Generate device ID with cross-platform UUID generation + this._identity[token].deviceId = generateUUID(); await this.storageAdapter.setItem( getDeviceIdKey(token), this._identity[token].deviceId From e064dd2b2ca3e173fcdd5150b837b64fe7aaaf9f Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:19:47 -0800 Subject: [PATCH 3/5] Return initialization promise from initializationCompletePromise Return the Promise from initializationCompletePromise so callers can await initialization. Previously the function created a Promise.all but did not return it, meaning callers couldn't know when identity, super properties and time events had finished loading. This change fixes that by returning Promise.all([...]) so the initialization flow can be awaited and errors propagated. --- javascript/mixpanel-persistent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/mixpanel-persistent.js b/javascript/mixpanel-persistent.js index d0ad6765..d9793723 100644 --- a/javascript/mixpanel-persistent.js +++ b/javascript/mixpanel-persistent.js @@ -74,7 +74,7 @@ export class MixpanelPersistent { } async initializationCompletePromise(token) { - Promise.all([ + return Promise.all([ this.loadIdentity(token), this.loadSuperProperties(token), this.loadTimeEvents(token), From 68d4bd5f36175cde4adfdccf31309c74a077872e Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 20:42:57 -0800 Subject: [PATCH 4/5] feat(mixpanel): Enhance request handling with GET/POST support Improve Mixpanel network request handling by: - Adding support for GET and POST requests - Implementing different error handling for GET/POST - Refining retry logic based on request type - Improving logging and error reporting - Ensuring more robust network communication Include headers and proper query separator in network requests Allow custom headers and correct URL query concatenation in network layer to support authenticated flag requests and avoid malformed URLs. This adds an optional headers parameter to sendRequest, merges headers into POST requests and includes them for GET requests, and chooses '?' or '&' depending on existing query params. These changes enable sending Authorization (Basic) headers for the /flags endpoint (fixing CORS/401 issues) and ensure ip query is appended correctly. --- .gitignore | 1 + FEATURE_FLAGS_JS_MODE_FINDINGS.md | 97 ++++++++++++ __tests__/flags-js-mode.test.js | 235 ++++++++++++++++++++++++++++++ javascript/mixpanel-network.js | 127 ++++++++++------ test-js-flags.js | 166 +++++++++++++++++++++ 5 files changed, 585 insertions(+), 41 deletions(-) create mode 100644 FEATURE_FLAGS_JS_MODE_FINDINGS.md create mode 100644 __tests__/flags-js-mode.test.js create mode 100644 test-js-flags.js diff --git a/.gitignore b/.gitignore index 50185058..6aac016b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ claude/ .github/copilot-* .github/instructions/ .github/prompts/ +WARP.md diff --git a/FEATURE_FLAGS_JS_MODE_FINDINGS.md b/FEATURE_FLAGS_JS_MODE_FINDINGS.md new file mode 100644 index 00000000..e2725f3b --- /dev/null +++ b/FEATURE_FLAGS_JS_MODE_FINDINGS.md @@ -0,0 +1,97 @@ +# Feature Flags JavaScript Mode - Test Results & Findings + +## Summary +JavaScript mode for feature flags has been successfully enabled for testing via environment variable. The implementation is mostly working but has some async operation issues that need resolution. + +## What's Working ✅ +1. **Environment Variable Control**: `MIXPANEL_ENABLE_JS_FLAGS=true` successfully enables JavaScript mode +2. **Basic Initialization**: Mixpanel instance creates correctly in JavaScript mode +3. **Synchronous Methods**: All sync methods work as expected: + - `areFlagsReady()` + - `getVariantSync()` + - `getVariantValueSync()` + - `isEnabledSync()` +4. **Snake-case Aliases**: API compatibility methods working +5. **Error Handling**: Gracefully handles null feature names + +## Issues Found & Fixed ✅ + +### 1. Async Methods Timeout (FIXED) +The following async methods were hanging indefinitely (5+ second timeout): +- `loadFlags()` +- `getVariant()` (async version) +- `getVariantValue()` (async version) +- `isEnabled()` (async version) +- `updateContext()` + +**Root Cause**: The MixpanelNetwork.sendRequest method was: +1. Always sending POST requests, even for the flags endpoint (which should be GET) +2. Retrying all failed requests with exponential backoff (up to 5 retries) +3. For GET requests returning 404, this caused 5+ seconds of retry delays + +**Solution**: Modified `javascript/mixpanel-network.js`: +- Detect GET requests (when data is null/undefined) +- Send proper GET requests without body for flags endpoint +- Don't retry GET requests on client errors (4xx status codes) +- Only retry POST requests or server errors (5xx) + +### 2. Test Suite Hanging (RESOLVED) +- **Initial Issue**: Tests would not exit after completion +- **Cause**: Recurring intervals from `mixpanel-core.js` queue processing +- **Solution**: Removed fake timers and added proper cleanup in `afterEach` + +## Code Changes Made + +### 1. index.js (Lines 89-95) +```javascript +// Enable JS flags for testing via environment variable +const jsFlagesEnabled = process.env.MIXPANEL_ENABLE_JS_FLAGS === 'true' || + process.env.NODE_ENV === 'test'; + +// Short circuit for JavaScript mode unless explicitly enabled +if (this.mixpanelImpl !== MixpanelReactNative && !jsFlagesEnabled) { + throw new Error( + "Feature flags are only available in native mode. " + + "JavaScript mode support is coming in a future release." + ); +} +``` + +### 2. Test File Created +- Created `__tests__/flags-js-mode.test.js` with comprehensive JavaScript mode tests +- Tests pass AsyncStorage mock as 4th parameter to Mixpanel constructor +- Proper cleanup to prevent hanging + +## Next Steps + +### Immediate (Before Beta Release) +1. ✅ **Fix Async Methods**: COMPLETE - Fixed network layer to handle GET requests properly +2. **Test in Real Expo App**: Run in actual Expo environment (not just unit tests) +3. **Performance Testing**: Verify AsyncStorage performance with large flag sets + +### Future Enhancements +1. **Remove Blocking Check**: Once stable, remove environment variable requirement +2. **Documentation**: Update FEATURE_FLAGS_QUICKSTART.md with JS mode examples +3. **Migration Guide**: Document differences between native and JS modes + +## Testing Commands + +```bash +# Run JavaScript mode tests +MIXPANEL_ENABLE_JS_FLAGS=true npm test -- --testPathPattern=flags-js-mode + +# Run in Expo app +cd Samples/MixpanelExpo +MIXPANEL_ENABLE_JS_FLAGS=true npm start +``` + +## Risk Assessment +- **Low Risk**: Core functionality works, follows established patterns +- **Low Risk**: All async operations now working correctly +- **Mitigation**: Keep behind environment variable until Expo testing complete + +## Recommendations +1. ✅ Async methods fixed - ready for beta testing +2. Test in real Expo environment before removing environment variable guard +3. Consider adding a `jsMode` flag to initialization options for cleaner API +4. Monitor network performance with real API endpoints \ No newline at end of file diff --git a/__tests__/flags-js-mode.test.js b/__tests__/flags-js-mode.test.js new file mode 100644 index 00000000..e43638a5 --- /dev/null +++ b/__tests__/flags-js-mode.test.js @@ -0,0 +1,235 @@ +/** + * Tests for JavaScript mode feature flags functionality + */ + +// Enable JavaScript mode via environment variable +process.env.MIXPANEL_ENABLE_JS_FLAGS = 'true'; + +// Mock React Native to simulate JavaScript mode (no native modules) +jest.mock('react-native', () => ({ + NativeModules: {}, // Empty to simulate no native modules + Platform: { OS: 'ios' }, + NativeEventEmitter: jest.fn() +})); + +// Mock AsyncStorage +const mockAsyncStorage = { + getItem: jest.fn(() => Promise.resolve(null)), + setItem: jest.fn(() => Promise.resolve()), + removeItem: jest.fn(() => Promise.resolve()), + getAllKeys: jest.fn(() => Promise.resolve([])), + multiGet: jest.fn(() => Promise.resolve([])), + multiSet: jest.fn(() => Promise.resolve()), + multiRemove: jest.fn(() => Promise.resolve()) +}; + +jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); + +// Mock fetch for network requests +global.fetch = jest.fn(() => + Promise.resolve({ + ok: false, + status: 404, + json: () => Promise.resolve({ error: 'Not found' }) + }) +); + +// Don't use fake timers - we'll handle cleanup manually + +const { Mixpanel } = require('../index'); + +describe('Feature Flags - JavaScript Mode', () => { + let mixpanel; + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Clean up to prevent hanging + if (mixpanel) { + // Call reset to clean up any pending operations + if (mixpanel.mixpanelImpl && mixpanel.mixpanelImpl.reset) { + mixpanel.mixpanelImpl.reset(mixpanel.token); + } + } + mixpanel = null; + }); + + describe('JavaScript Mode Initialization', () => { + it('should create Mixpanel instance in JavaScript mode', () => { + // Pass AsyncStorage as the 4th parameter for JavaScript mode + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + + // Verify we're NOT using native module + expect(mixpanel.mixpanelImpl.constructor.name).not.toBe('MixpanelReactNative'); + }); + + it('should initialize with feature flags enabled', async () => { + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + + // init doesn't return a value, just await it + await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, { + enabled: true, + context: { + user_type: 'tester' + } + }); + + // Check that flags property is accessible + expect(mixpanel.flags).toBeDefined(); + }); + + it('should access flags property without error in JavaScript mode', async () => { + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + + await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, { + enabled: true + }); + + // This should not throw an error with JavaScript mode enabled + expect(() => mixpanel.flags).not.toThrow(); + }); + }); + + describe('JavaScript Mode Flag Methods', () => { + beforeEach(async () => { + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, { + enabled: true + }); + }); + + describe('Synchronous Methods', () => { + it('should handle areFlagsReady', () => { + const ready = mixpanel.flags.areFlagsReady(); + expect(typeof ready).toBe('boolean'); + }); + + it('should return fallback from getVariantSync', () => { + const variant = mixpanel.flags.getVariantSync('test-flag', 'fallback-value'); + expect(variant).toBe('fallback-value'); + }); + + it('should return fallback from getVariantValueSync', () => { + const value = mixpanel.flags.getVariantValueSync('button-color', 'blue'); + expect(value).toBe('blue'); + }); + + it('should return fallback from isEnabledSync', () => { + const enabled = mixpanel.flags.isEnabledSync('new-feature', false); + expect(enabled).toBe(false); + }); + }); + + describe('Asynchronous Methods', () => { + it('should handle loadFlags gracefully', async () => { + // loadFlags will fail in test environment (no real API) + // but the method should exist and be callable + expect(typeof mixpanel.flags.loadFlags).toBe('function'); + + // Call it and let it fail gracefully (network error is expected) + try { + await mixpanel.flags.loadFlags(); + } catch (error) { + // This is expected in test environment + expect(error).toBeDefined(); + } + }); + + it('should return fallback from getVariant', async () => { + const variant = await mixpanel.flags.getVariant('async-test', 'async-fallback'); + expect(variant).toBe('async-fallback'); + }); + + it('should return fallback from getVariantValue', async () => { + const value = await mixpanel.flags.getVariantValue('async-color', 'red'); + expect(value).toBe('red'); + }); + + it('should return fallback from isEnabled', async () => { + const enabled = await mixpanel.flags.isEnabled('async-feature', true); + expect(enabled).toBe(true); + }); + + it('should support callback pattern', (done) => { + mixpanel.flags.getVariant('callback-test', 'callback-fallback', (variant) => { + expect(variant).toBe('callback-fallback'); + done(); + }); + }); + }); + + describe('JavaScript-Specific Features', () => { + it('should support updateContext method', async () => { + // updateContext is JavaScript mode only + expect(typeof mixpanel.flags.updateContext).toBe('function'); + + // Call it - it should work in JS mode + await mixpanel.flags.updateContext({ + user_type: 'premium', + plan: 'enterprise' + }); + + // Verify the context was updated + expect(mixpanel.flags.jsFlags.context).toEqual({ + user_type: 'premium', + plan: 'enterprise' + }); + }); + + it('should support snake_case aliases', () => { + expect(typeof mixpanel.flags.are_flags_ready).toBe('function'); + expect(typeof mixpanel.flags.get_variant_sync).toBe('function'); + expect(typeof mixpanel.flags.get_variant_value_sync).toBe('function'); + expect(typeof mixpanel.flags.is_enabled_sync).toBe('function'); + }); + }); + }); + + describe('Error Handling', () => { + beforeEach(async () => { + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, { + enabled: true + }); + }); + + it('should handle null feature names gracefully', () => { + expect(() => mixpanel.flags.getVariantSync(null, 'fallback')).not.toThrow(); + const result = mixpanel.flags.getVariantSync(null, 'fallback'); + expect(result).toBe('fallback'); + }); + + it('should handle undefined callbacks', async () => { + await expect( + mixpanel.flags.getVariant('test', 'fallback', undefined) + ).resolves.not.toThrow(); + }); + }); + + describe('Type Preservation', () => { + beforeEach(async () => { + mixpanel = new Mixpanel('js-test-token', false, false, mockAsyncStorage); + await mixpanel.init(false, {}, 'https://api.mixpanel.com', false, { + enabled: true + }); + }); + + it('should preserve boolean types', async () => { + const boolValue = await mixpanel.flags.getVariantValue('bool-flag', true); + expect(typeof boolValue).toBe('boolean'); + }); + + it('should preserve number types', async () => { + const numValue = await mixpanel.flags.getVariantValue('num-flag', 42); + expect(typeof numValue).toBe('number'); + }); + + it('should preserve object types', async () => { + const objValue = await mixpanel.flags.getVariantValue('obj-flag', { key: 'value' }); + expect(typeof objValue).toBe('object'); + }); + }); +}); \ No newline at end of file diff --git a/javascript/mixpanel-network.js b/javascript/mixpanel-network.js index d2abbd99..ecb18f71 100644 --- a/javascript/mixpanel-network.js +++ b/javascript/mixpanel-network.js @@ -15,37 +15,77 @@ export const MixpanelNetwork = (() => { serverURL, useIPAddressForGeoLocation, retryCount = 0, + headers = {}, }) => { retryCount = retryCount || 0; - const url = `${serverURL}${endpoint}?ip=${+useIPAddressForGeoLocation}`; + // Use & if endpoint already has query params, otherwise use ? + const separator = endpoint.includes('?') ? '&' : '?'; + const url = `${serverURL}${endpoint}${separator}ip=${+useIPAddressForGeoLocation}`; MixpanelLogger.log(token, `Sending request to: ${url}`); try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: `data=${encodeURIComponent(JSON.stringify(data))}`, - }); + // Determine if this is a GET or POST request based on data presence + const isGetRequest = data === null || data === undefined; - const responseBody = await response.json(); - if (response.status !== 200) { - throw new MixpanelHttpError( - `HTTP error! status: ${response.status}`, - response.status - ); - } + const fetchOptions = isGetRequest + ? { + method: "GET", + headers: headers, + } + : { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + ...headers, + }, + body: `data=${encodeURIComponent(JSON.stringify(data))}`, + }; + + const response = await fetch(url, fetchOptions); + + // Handle GET requests differently - they return the data directly + if (isGetRequest) { + if (response.status === 200) { + const responseData = await response.json(); + MixpanelLogger.log(token, `GET request successful: ${endpoint}`); + return responseData; + } else { + throw new MixpanelHttpError( + `HTTP error! status: ${response.status}`, + response.status + ); + } + } else { + // Handle POST requests (existing logic) + const responseBody = await response.json(); + if (response.status !== 200) { + throw new MixpanelHttpError( + `HTTP error! status: ${response.status}`, + response.status + ); + } - const message = - responseBody === 0 - ? `${url} api rejected some items` - : `Mixpanel batch sent successfully, endpoint: ${endpoint}, data: ${JSON.stringify( - data - )}`; + const message = + responseBody === 0 + ? `${url} api rejected some items` + : `Mixpanel batch sent successfully, endpoint: ${endpoint}, data: ${JSON.stringify( + data + )}`; - MixpanelLogger.log(token, message); + MixpanelLogger.log(token, message); + return responseBody; + } } catch (error) { + // Determine if this is a GET or POST request + const isGetRequest = data === null || data === undefined; + + // For GET requests (like flags), don't retry on 404 or other client errors + if (isGetRequest && error.code >= 400 && error.code < 500) { + MixpanelLogger.log(token, `GET request failed with status ${error.code}, not retrying`); + throw error; + } + + // For POST requests or non-client errors, handle retries if (error.code === 400) { // This indicates that the data was invalid and we should not retry throw new MixpanelHttpError( @@ -53,30 +93,35 @@ export const MixpanelNetwork = (() => { error.code ); } + MixpanelLogger.warn( token, `API request to ${url} has failed with reason: ${error.message}` ); - const maxRetries = 5; - const backoff = Math.min(2 ** retryCount * 2000, 60000); // Exponential backoff - if (retryCount < maxRetries) { - MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`); - await new Promise((resolve) => setTimeout(resolve, backoff)); - return sendRequest({ - token, - endpoint, - data, - serverURL, - useIPAddressForGeoLocation, - retryCount: retryCount + 1, - }); - } else { - MixpanelLogger.warn(token, `Max retries reached. Giving up.`); - throw new MixpanelHttpError( - `HTTP error! status: ${error.code}`, - error.code - ); + + // Only retry for POST requests or server errors + if (!isGetRequest || error.code >= 500) { + const maxRetries = 5; + const backoff = Math.min(2 ** retryCount * 2000, 60000); // Exponential backoff + if (retryCount < maxRetries) { + MixpanelLogger.log(token, `Retrying in ${backoff / 1000} seconds...`); + await new Promise((resolve) => setTimeout(resolve, backoff)); + return sendRequest({ + token, + endpoint, + data, + serverURL, + useIPAddressForGeoLocation, + retryCount: retryCount + 1, + }); + } } + + MixpanelLogger.warn(token, `Request failed. Not retrying.`); + throw new MixpanelHttpError( + `HTTP error! status: ${error.code || 'unknown'}`, + error.code + ); } }; diff --git a/test-js-flags.js b/test-js-flags.js new file mode 100644 index 00000000..f149fc38 --- /dev/null +++ b/test-js-flags.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +/** + * Test script to verify JavaScript mode feature flags functionality + * This script simulates an environment where native modules are not available + */ + +// Enable JavaScript mode for testing +process.env.MIXPANEL_ENABLE_JS_FLAGS = 'true'; + +// Mock React Native completely before any imports +const Module = require('module'); +const originalRequire = Module.prototype.require; + +Module.prototype.require = function(id) { + if (id === 'react-native') { + return { + NativeModules: {}, + Platform: { + OS: 'ios', + select: (obj) => obj.ios || obj.default + }, + NativeEventEmitter: class NativeEventEmitter {} + }; + } + // Mock expo-crypto as unavailable + if (id === 'expo-crypto') { + throw new Error('Module not found'); + } + // Let uuid work normally + return originalRequire.apply(this, arguments); +}; + +// Mock React Native modules +global.NativeModules = {}; + +// Mock AsyncStorage +const storage = new Map(); +global.AsyncStorage = { + getItem: async (key) => storage.get(key) || null, + setItem: async (key, value) => { + storage.set(key, value); + return Promise.resolve(); + }, + removeItem: async (key) => { + storage.delete(key); + return Promise.resolve(); + }, + getAllKeys: async () => Array.from(storage.keys()), + multiGet: async (keys) => keys.map(key => [key, storage.get(key) || null]), + multiSet: async (keyValuePairs) => { + keyValuePairs.forEach(([key, value]) => storage.set(key, value)); + return Promise.resolve(); + }, + multiRemove: async (keys) => { + keys.forEach(key => storage.delete(key)); + return Promise.resolve(); + } +}; + +// Import Mixpanel +const { Mixpanel } = require('./index.js'); + +async function testJavaScriptModeFlags() { + console.log('🧪 Testing JavaScript Mode Feature Flags\n'); + console.log('===================================\n'); + + try { + // Create Mixpanel instance in JavaScript mode + const mixpanel = new Mixpanel('test-token-123', false, false); + console.log('✅ Created Mixpanel instance in JavaScript mode\n'); + + // Verify we're in JavaScript mode + const isNativeMode = mixpanel.mixpanelImpl.constructor.name === 'MixpanelReactNative'; + console.log(`Mode: ${isNativeMode ? 'Native' : 'JavaScript'} ✅\n`); + + // Initialize with feature flags enabled + const success = await mixpanel.init(false, { + featureFlagsOptions: { + enabled: true, + context: { + user_type: 'tester', + environment: 'development' + } + } + }); + console.log(`Initialized: ${success ? '✅' : '❌'}\n`); + + // Access feature flags + const flags = mixpanel.flags; + console.log('✅ Accessed flags property without error\n'); + + // Test synchronous methods + console.log('Testing Synchronous Methods:'); + console.log('----------------------------'); + + const ready = flags.areFlagsReady(); + console.log(`areFlagsReady(): ${ready}`); + + const variant = flags.getVariantSync('test-flag', 'fallback'); + console.log(`getVariantSync('test-flag'): ${variant}`); + + const value = flags.getVariantValueSync('button-color', 'blue'); + console.log(`getVariantValueSync('button-color'): ${value}`); + + const enabled = flags.isEnabledSync('new-feature', false); + console.log(`isEnabledSync('new-feature'): ${enabled}\n`); + + // Test asynchronous methods + console.log('Testing Asynchronous Methods:'); + console.log('-----------------------------'); + + // Load flags (this will fail in test environment but should handle gracefully) + try { + await flags.loadFlags(); + console.log('loadFlags(): Success (unexpected)'); + } catch (error) { + console.log('loadFlags(): Failed gracefully (expected in test) ✅'); + } + + // Test async variants with promises + const asyncVariant = await flags.getVariant('async-test', 'async-fallback'); + console.log(`getVariant('async-test'): ${asyncVariant}`); + + const asyncValue = await flags.getVariantValue('async-color', 'red'); + console.log(`getVariantValue('async-color'): ${asyncValue}`); + + const asyncEnabled = await flags.isEnabled('async-feature', true); + console.log(`isEnabled('async-feature'): ${asyncEnabled}\n`); + + // Test JavaScript-specific features + console.log('Testing JavaScript-Specific Features:'); + console.log('------------------------------------'); + + // Test updateContext (JavaScript mode only) + try { + await flags.updateContext({ + user_type: 'premium', + plan: 'enterprise' + }); + console.log('updateContext(): Success ✅'); + } catch (error) { + console.log(`updateContext(): ${error.message}`); + } + + // Test snake_case aliases + console.log('\nTesting snake_case Aliases:'); + console.log('---------------------------'); + + const snakeReady = flags.are_flags_ready(); + console.log(`are_flags_ready(): ${snakeReady}`); + + const snakeVariant = flags.get_variant_sync('snake-test', 'snake-fallback'); + console.log(`get_variant_sync(): ${snakeVariant}`); + + console.log('\n✅ All JavaScript mode feature flag tests completed successfully!'); + + } catch (error) { + console.error('❌ Test failed:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +// Run the tests +testJavaScriptModeFlags().catch(console.error); \ No newline at end of file From 75804b6405892a5142b379d404be13075bd12f19 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 21:01:34 -0800 Subject: [PATCH 5/5] Add Feature Flags UI and API calls to MixpanelExpo sample Enable Feature Flags during Mixpanel initialization and add a new "Feature Flags" section with buttons that exercise the Flags API (loadFlags, check ready, sync/async getVariant, getVariantValue, isEnabled). This brings the MixpanelExpo sample in line with the MixpanelStarter app so QA can manually test Feature Flags behavior in the Expo sample app. Enable JS feature flags via env and dotenv plugin Allow enabling Mixpanel JavaScript-mode feature flags through an environment variable to avoid the runtime error about flags only being available in native mode. Add a .env file with MIXPANEL_ENABLE_JS_FLAGS and MIXPANEL_TOKEN for local testing, and configure babel to load .env values via react-native-dotenv so the env variables are available in JS. Initialize Mixpanel asynchronously and load token from env Initialize Mixpanel in a useEffect with the MIXPANEL_TOKEN from .env and show a loading state until initialization completes. This fixes the "Failed to load flags: Feature flags are not initialized" error by ensuring feature flags are enabled only after the SDK has been initialized. Also switch to a ref-based Mixpanel instance, enable logging, and add a simple ActivityIndicator UI while initializing. Changes: - Load MIXPANEL_TOKEN from .env and update .env sample. - Initialize Mixpanel asynchronously in useEffect and enable feature flags. - Store Mixpanel instance in useRef and gate UI on initialization with ActivityIndicator. - Minor style updates for loading layout. Pass AsyncStorage to Mixpanel for feature flags Fix initialization error "Feature flags are not initialized" when clicking "Load Flags" by supplying AsyncStorage to the Mixpanel constructor. This enables JavaScript-mode feature flags support so flags are properly initialized and can be loaded. Changes: - Import AsyncStorage from @react-native-async-storage/async-storage. - Pass AsyncStorage as the fourth argument to new Mixpanel(...) when creating the instance. - Leave feature-flag enabling call (mp.init) intact; this change provides the storage dependency needed for it to work. --- Samples/MixpanelExpo/.env | 5 + Samples/MixpanelExpo/App.js | 167 +++++++++++++++++++++++++-- Samples/MixpanelExpo/babel.config.js | 8 ++ 3 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 Samples/MixpanelExpo/.env diff --git a/Samples/MixpanelExpo/.env b/Samples/MixpanelExpo/.env new file mode 100644 index 00000000..7eff781c --- /dev/null +++ b/Samples/MixpanelExpo/.env @@ -0,0 +1,5 @@ +# Enable JavaScript mode feature flags for testing +MIXPANEL_ENABLE_JS_FLAGS=true + +# Replace with your actual Mixpanel project token +MIXPANEL_TOKEN="metrics-1" diff --git a/Samples/MixpanelExpo/App.js b/Samples/MixpanelExpo/App.js index d2c7566c..39981228 100644 --- a/Samples/MixpanelExpo/App.js +++ b/Samples/MixpanelExpo/App.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; import { SectionList, Text, @@ -6,20 +6,51 @@ import { Button, StyleSheet, SafeAreaView, + ActivityIndicator, } from "react-native"; import { Mixpanel } from "mixpanel-react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { MIXPANEL_TOKEN } from "@env"; const App = () => { - const trackAutomaticEvents = false; - const useNative = false; - const mixpanel = new Mixpanel( - "YOUR_MIXPANEL_TOKEN", - trackAutomaticEvents, - useNative - ); - mixpanel.init(); - mixpanel.setLoggingEnabled(true); + const [isInitialized, setIsInitialized] = useState(false); + const mixpanelRef = useRef(null); + + // Test flag name - replace with your actual flag from Mixpanel + const testFlagName = "sample-bool-flag"; + + useEffect(() => { + const initMixpanel = async () => { + const trackAutomaticEvents = false; + const useNative = false; + // Pass AsyncStorage for JavaScript mode feature flags support + const mp = new Mixpanel(MIXPANEL_TOKEN, trackAutomaticEvents, useNative, AsyncStorage); + + // Enable feature flags during initialization + await mp.init(false, {}, undefined, false, { enabled: true }); + mp.setLoggingEnabled(true); + + mixpanelRef.current = mp; + setIsInitialized(true); + console.log("[Mixpanel] Initialized with token:", MIXPANEL_TOKEN); + }; + + initMixpanel(); + }, []); + + // Helper to get mixpanel instance + const mixpanel = mixpanelRef.current; + + // Show loading while initializing + if (!isInitialized || !mixpanel) { + return ( + + + Initializing Mixpanel... + + ); + } const group = mixpanel.getGroup("company_id", 111); const track = async () => { @@ -197,6 +228,97 @@ const App = () => { ); }; + // ----------------- Feature Flags API ----------------- + const loadFlags = async () => { + try { + await mixpanel.flags.loadFlags(); + alert("Flags loaded successfully!"); + } catch (error) { + alert(`Failed to load flags: ${error.message}`); + } + }; + + const checkFlagsReady = () => { + const ready = mixpanel.flags.areFlagsReady(); + alert(`Flags ready: ${ready}`); + }; + + const getVariantSync = () => { + const fallback = { key: "fallback", value: null }; + try { + const result = mixpanel.flags.getVariantSync(testFlagName, fallback); + alert( + `getVariantSync('${testFlagName}'):\n` + + `Key: ${result.key}\n` + + `Value: ${JSON.stringify(result.value)}\n` + + `Experiment ID: ${result.experiment_id || "N/A"}` + ); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + + const getVariantValueSync = () => { + const fallback = "default-value"; + try { + const result = mixpanel.flags.getVariantValueSync(testFlagName, fallback); + alert( + `getVariantValueSync('${testFlagName}'):\n` + + `Value: ${JSON.stringify(result)}\n` + + `Type: ${typeof result}` + ); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + + const isEnabledSync = () => { + try { + const result = mixpanel.flags.isEnabledSync(testFlagName, false); + alert(`isEnabledSync('${testFlagName}'): ${result}`); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + + const getVariantAsync = async () => { + const fallback = { key: "fallback", value: null }; + try { + const result = await mixpanel.flags.getVariant(testFlagName, fallback); + alert( + `getVariant('${testFlagName}') [async]:\n` + + `Key: ${result.key}\n` + + `Value: ${JSON.stringify(result.value)}\n` + + `Experiment ID: ${result.experiment_id || "N/A"}` + ); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + + const getVariantValueAsync = async () => { + const fallback = "default-value"; + try { + const result = await mixpanel.flags.getVariantValue(testFlagName, fallback); + alert( + `getVariantValue('${testFlagName}') [async]:\n` + + `Value: ${JSON.stringify(result)}\n` + + `Type: ${typeof result}` + ); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + + const isEnabledAsync = async () => { + try { + const result = await mixpanel.flags.isEnabled(testFlagName, false); + alert(`isEnabled('${testFlagName}') [async]: ${result}`); + } catch (error) { + alert(`Error: ${error.message}`); + } + }; + const DATA = [ { title: "Events and Properties", @@ -296,6 +418,20 @@ const App = () => { { id: "11", label: "Flush", onPress: flush }, ], }, + { + title: "Feature Flags", + data: [ + { id: "1", label: "Load Flags", onPress: loadFlags }, + { id: "2", label: "Check Flags Ready", onPress: checkFlagsReady }, + { id: "3", label: "getVariantSync()", onPress: getVariantSync }, + { id: "4", label: "getVariantValueSync()", onPress: getVariantValueSync }, + { id: "5", label: "isEnabledSync()", onPress: isEnabledSync }, + { id: "6", label: "getVariant() [async]", onPress: getVariantAsync }, + { id: "7", label: "getVariantValue() [async]", onPress: getVariantValueAsync }, + { id: "8", label: "isEnabled() [async]", onPress: isEnabledAsync }, + { id: "9", label: "Flush", onPress: flush }, + ], + }, ]; const renderItem = ({ item }) => ( @@ -324,6 +460,17 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#fff", + }, + loadingText: { + marginTop: 16, + fontSize: 16, + color: "#666", + }, header: { fontSize: 20, backgroundColor: "#f4f4f4", diff --git a/Samples/MixpanelExpo/babel.config.js b/Samples/MixpanelExpo/babel.config.js index 2900afe9..cd17a170 100644 --- a/Samples/MixpanelExpo/babel.config.js +++ b/Samples/MixpanelExpo/babel.config.js @@ -2,5 +2,13 @@ module.exports = function(api) { api.cache(true); return { presets: ['babel-preset-expo'], + plugins: [ + ['module:react-native-dotenv', { + moduleName: '@env', + path: '.env', + safe: false, + allowUndefined: true, + }], + ], }; };