From 48a39aaf52bdbff3c144d1d59553783fb82ee777 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 20:42:57 -0800 Subject: [PATCH 1/9] 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 --- .gitignore | 1 + FEATURE_FLAGS_JS_MODE_FINDINGS.md | 97 ++++++++++++ __tests__/flags-js-mode.test.js | 235 ++++++++++++++++++++++++++++++ javascript/mixpanel-network.js | 120 ++++++++++----- test-js-flags.js | 166 +++++++++++++++++++++ 5 files changed, 579 insertions(+), 40 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..ed1352d8 100644 --- a/javascript/mixpanel-network.js +++ b/javascript/mixpanel-network.js @@ -21,31 +21,66 @@ export const MixpanelNetwork = (() => { 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", + } + : { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + 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 +88,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 3624eed81e17adc3cc6721df45e112a7daef9966 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 21:01:34 -0800 Subject: [PATCH 2/9] 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. --- Samples/MixpanelExpo/App.js | 111 +++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/Samples/MixpanelExpo/App.js b/Samples/MixpanelExpo/App.js index d2c7566c..b6788681 100644 --- a/Samples/MixpanelExpo/App.js +++ b/Samples/MixpanelExpo/App.js @@ -18,9 +18,13 @@ const App = () => { trackAutomaticEvents, useNative ); - mixpanel.init(); + // Enable feature flags during initialization + mixpanel.init(false, {}, undefined, false, { enabled: true }); mixpanel.setLoggingEnabled(true); + // Test flag name - replace with your actual flag from Mixpanel + const testFlagName = "sample-bool-flag"; + const group = mixpanel.getGroup("company_id", 111); const track = async () => { await mixpanel.track("Track Event!"); @@ -197,6 +201,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 +391,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 }) => ( From c0185f94e6cce74792d96e9bfba9e91a2176e0b2 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 21:08:08 -0800 Subject: [PATCH 3/9] 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. --- Samples/MixpanelExpo/.env | 5 +++++ Samples/MixpanelExpo/babel.config.js | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 Samples/MixpanelExpo/.env diff --git a/Samples/MixpanelExpo/.env b/Samples/MixpanelExpo/.env new file mode 100644 index 00000000..3ad29669 --- /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=YOUR_MIXPANEL_TOKEN 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, + }], + ], }; }; From a11b6d8890c925931d15095b1aae510cf8824739 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 21:16:25 -0800 Subject: [PATCH 4/9] 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. --- Samples/MixpanelExpo/.env | 2 +- Samples/MixpanelExpo/App.js | 58 ++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Samples/MixpanelExpo/.env b/Samples/MixpanelExpo/.env index 3ad29669..7eff781c 100644 --- a/Samples/MixpanelExpo/.env +++ b/Samples/MixpanelExpo/.env @@ -2,4 +2,4 @@ MIXPANEL_ENABLE_JS_FLAGS=true # Replace with your actual Mixpanel project token -MIXPANEL_TOKEN=YOUR_MIXPANEL_TOKEN +MIXPANEL_TOKEN="metrics-1" diff --git a/Samples/MixpanelExpo/App.js b/Samples/MixpanelExpo/App.js index b6788681..67f6e7f8 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,25 +6,50 @@ import { Button, StyleSheet, SafeAreaView, + ActivityIndicator, } from "react-native"; import { Mixpanel } from "mixpanel-react-native"; +import { MIXPANEL_TOKEN } from "@env"; const App = () => { - const trackAutomaticEvents = false; - const useNative = false; - const mixpanel = new Mixpanel( - "YOUR_MIXPANEL_TOKEN", - trackAutomaticEvents, - useNative - ); - // Enable feature flags during initialization - mixpanel.init(false, {}, undefined, false, { enabled: true }); - 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; + const mp = new Mixpanel(MIXPANEL_TOKEN, trackAutomaticEvents, useNative); + + // 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 () => { await mixpanel.track("Track Event!"); @@ -433,6 +458,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", From 13b5cfd3f478e63b9963155a4bb0f0668bc39c84 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Thu, 4 Dec 2025 21:24:01 -0800 Subject: [PATCH 5/9] 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/App.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Samples/MixpanelExpo/App.js b/Samples/MixpanelExpo/App.js index 67f6e7f8..39981228 100644 --- a/Samples/MixpanelExpo/App.js +++ b/Samples/MixpanelExpo/App.js @@ -10,6 +10,7 @@ import { } 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 = () => { @@ -23,7 +24,8 @@ const App = () => { const initMixpanel = async () => { const trackAutomaticEvents = false; const useNative = false; - const mp = new Mixpanel(MIXPANEL_TOKEN, trackAutomaticEvents, useNative); + // 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 }); From 2ab9dc0f6ae2380c7949f2593e9092def023c783 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:13:34 -0800 Subject: [PATCH 6/9] 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. --- javascript/mixpanel-network.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/javascript/mixpanel-network.js b/javascript/mixpanel-network.js index ed1352d8..ecb18f71 100644 --- a/javascript/mixpanel-network.js +++ b/javascript/mixpanel-network.js @@ -15,9 +15,12 @@ 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 { @@ -27,11 +30,13 @@ export const MixpanelNetwork = (() => { const fetchOptions = isGetRequest ? { method: "GET", + headers: headers, } : { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", + ...headers, }, body: `data=${encodeURIComponent(JSON.stringify(data))}`, }; From 4bb0ab9ac005e3fc4e7eabd2aa2028d6ab10695e Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:19:47 -0800 Subject: [PATCH 7/9] 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 84b26abe..2d28bf65 100644 --- a/javascript/mixpanel-persistent.js +++ b/javascript/mixpanel-persistent.js @@ -42,7 +42,7 @@ export class MixpanelPersistent { } async initializationCompletePromise(token) { - Promise.all([ + return Promise.all([ this.loadIdentity(token), this.loadSuperProperties(token), this.loadTimeEvents(token), From 3e32750a14cb94208868b9bb17b286fd621f1bd2 Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:23:55 -0800 Subject: [PATCH 8/9] 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 2d28bf65..d9793723 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 674c005c364f05549bc9d4c413d2f055aab849ed Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Fri, 5 Dec 2025 08:31:13 -0800 Subject: [PATCH 9/9] 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 bd7a668b..5bec73b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.1.1", "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" }, @@ -23,6 +23,7 @@ "jest-fetch-mock": "^3.0.3", "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" } }, @@ -1199,6 +1200,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" } @@ -2042,9 +2044,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" }, @@ -5022,6 +5025,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", @@ -12634,6 +12650,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 ebfc2869..c23112be 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "jest-fetch-mock": "^3.0.3", "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": { @@ -59,7 +60,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" }