Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ claude/
.github/copilot-*
.github/instructions/
.github/prompts/
WARP.md
97 changes: 97 additions & 0 deletions FEATURE_FLAGS_JS_MODE_FINDINGS.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions Samples/MixpanelExpo/.env
Original file line number Diff line number Diff line change
@@ -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"
167 changes: 157 additions & 10 deletions Samples/MixpanelExpo/App.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
import React from "react";
import React, { useState, useEffect, useRef } from "react";
import {
SectionList,
Text,
View,
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 (
<SafeAreaView style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#8A2BE2" />
<Text style={styles.loadingText}>Initializing Mixpanel...</Text>
</SafeAreaView>
);
}

const group = mixpanel.getGroup("company_id", 111);
const track = async () => {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 }) => (
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions Samples/MixpanelExpo/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}],
],
};
};
Loading