mixpanel-react-native 3.1.2 → 3.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -1
- package/.github/dependabot.yml +7 -0
- package/.github/workflows/node.js.yml +24 -1
- package/.vscode/settings.json +2 -1
- package/MixpanelReactNative.podspec +1 -1
- package/Samples/MixpanelExample/ios/MixpanelExample.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelExample/ios/Podfile.lock +1996 -0
- package/Samples/MixpanelStarter/.bundle/config +2 -0
- package/Samples/MixpanelStarter/.env.example +4 -0
- package/Samples/MixpanelStarter/.eslintrc.js +4 -0
- package/Samples/MixpanelStarter/.prettierrc.js +5 -0
- package/Samples/MixpanelStarter/.watchmanconfig +1 -0
- package/Samples/MixpanelStarter/App.tsx +10 -0
- package/Samples/MixpanelStarter/CLAUDE.md +538 -0
- package/Samples/MixpanelStarter/Gemfile +16 -0
- package/Samples/MixpanelStarter/INTEGRATION_GUIDE.md +606 -0
- package/Samples/MixpanelStarter/README.md +406 -0
- package/Samples/MixpanelStarter/__tests__/MixpanelContext.test.tsx +63 -0
- package/Samples/MixpanelStarter/android/app/build.gradle +119 -0
- package/Samples/MixpanelStarter/android/app/debug.keystore +0 -0
- package/Samples/MixpanelStarter/android/app/proguard-rules.pro +10 -0
- package/Samples/MixpanelStarter/android/app/src/main/AndroidManifest.xml +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainActivity.kt +22 -0
- package/Samples/MixpanelStarter/android/app/src/main/java/com/mixpanelstarter/MainApplication.kt +27 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/strings.xml +3 -0
- package/Samples/MixpanelStarter/android/app/src/main/res/values/styles.xml +9 -0
- package/Samples/MixpanelStarter/android/build.gradle +21 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/Samples/MixpanelStarter/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/Samples/MixpanelStarter/android/gradle.properties +44 -0
- package/Samples/MixpanelStarter/android/gradlew +251 -0
- package/Samples/MixpanelStarter/android/gradlew.bat +99 -0
- package/Samples/MixpanelStarter/android/settings.gradle +6 -0
- package/Samples/MixpanelStarter/app.json +4 -0
- package/Samples/MixpanelStarter/babel.config.js +14 -0
- package/Samples/MixpanelStarter/index.js +9 -0
- package/Samples/MixpanelStarter/ios/.xcode.env +11 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/AppDelegate.swift +48 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Images.xcassets/Contents.json +6 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/Info.plist +55 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/LaunchScreen.storyboard +47 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter/PrivacyInfo.xcprivacy +38 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/project.pbxproj +482 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcodeproj/xcshareddata/xcschemes/MixpanelStarter.xcscheme +88 -0
- package/Samples/MixpanelStarter/ios/MixpanelStarter.xcworkspace/contents.xcworkspacedata +10 -0
- package/Samples/MixpanelStarter/ios/Podfile +34 -0
- package/Samples/MixpanelStarter/ios/Podfile.lock +2839 -0
- package/Samples/MixpanelStarter/jest.config.js +3 -0
- package/Samples/MixpanelStarter/metro.config.js +42 -0
- package/Samples/MixpanelStarter/package-lock.json +12141 -0
- package/Samples/MixpanelStarter/package.json +51 -0
- package/Samples/MixpanelStarter/src/@types/env.d.ts +3 -0
- package/Samples/MixpanelStarter/src/App.tsx +83 -0
- package/Samples/MixpanelStarter/src/components/ActionButton.tsx +92 -0
- package/Samples/MixpanelStarter/src/components/ErrorBoundary.tsx +81 -0
- package/Samples/MixpanelStarter/src/components/EventTrackingLog.tsx +163 -0
- package/Samples/MixpanelStarter/src/components/FlagCard.tsx +199 -0
- package/Samples/MixpanelStarter/src/components/InfoCard.tsx +77 -0
- package/Samples/MixpanelStarter/src/components/TestResultDisplay.tsx +181 -0
- package/Samples/MixpanelStarter/src/constants/tracking.ts +77 -0
- package/Samples/MixpanelStarter/src/contexts/MixpanelContext.tsx +159 -0
- package/Samples/MixpanelStarter/src/screens/FeatureFlagsScreen.tsx +1011 -0
- package/Samples/MixpanelStarter/src/screens/HomeScreen.tsx +307 -0
- package/Samples/MixpanelStarter/src/screens/OnboardingScreen.tsx +253 -0
- package/Samples/MixpanelStarter/src/screens/SettingsScreen.tsx +316 -0
- package/Samples/MixpanelStarter/src/types/flags.types.ts +42 -0
- package/Samples/MixpanelStarter/src/types/mixpanel.types.ts +26 -0
- package/Samples/MixpanelStarter/tsconfig.json +13 -0
- package/__tests__/flags.test.js +730 -0
- package/__tests__/index.test.js +7 -3
- package/__tests__/jest_setup.js +18 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/mixpanel/reactnative/MixpanelReactNativeModule.java +272 -2
- package/index.d.ts +64 -1
- package/index.js +42 -3
- package/ios/MixpanelReactNative.m +19 -1
- package/ios/MixpanelReactNative.swift +183 -5
- package/javascript/mixpanel-flags-js.js +463 -0
- package/javascript/mixpanel-flags.js +290 -0
- package/javascript/mixpanel-main.js +13 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import React, {useState, useEffect, useCallback} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
ScrollView,
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
Alert,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import {useMixpanel} from '../contexts/MixpanelContext';
|
|
12
|
+
import {ActionButton} from '../components/ActionButton';
|
|
13
|
+
import {InfoCard} from '../components/InfoCard';
|
|
14
|
+
import {FlagCard} from '../components/FlagCard';
|
|
15
|
+
import {TestResultDisplay} from '../components/TestResultDisplay';
|
|
16
|
+
import {EventTrackingLog} from '../components/EventTrackingLog';
|
|
17
|
+
import {Events, Properties} from '../constants/tracking';
|
|
18
|
+
import {
|
|
19
|
+
FlagInfo,
|
|
20
|
+
TestResult,
|
|
21
|
+
TrackedEvent,
|
|
22
|
+
TestMode,
|
|
23
|
+
ValueType,
|
|
24
|
+
} from '../types/flags.types';
|
|
25
|
+
|
|
26
|
+
// Existing flags from your Mixpanel project
|
|
27
|
+
// Selected to demonstrate different flag types and scenarios
|
|
28
|
+
const RECOMMENDED_FLAGS = {
|
|
29
|
+
// Boolean FeatureGate flags (value: true/false)
|
|
30
|
+
'sample-bool-flag': 'boolean',
|
|
31
|
+
'hash-slinging-slasher': 'boolean',
|
|
32
|
+
'mike-test': 'boolean',
|
|
33
|
+
|
|
34
|
+
// String Experiment flags (custom variants)
|
|
35
|
+
'sample-exp-testing': 'string-experiment',
|
|
36
|
+
'new_feature_flag_v2': 'string-variant',
|
|
37
|
+
'mojojojo': 'string-variant',
|
|
38
|
+
'af_ff_music_finder_test': 'string-variant',
|
|
39
|
+
|
|
40
|
+
// Experiment with active tracking
|
|
41
|
+
'general-replay-events-query-improvement': 'active-experiment',
|
|
42
|
+
'test-active-flag': 'active-experiment',
|
|
43
|
+
|
|
44
|
+
// Dynamic Config (object values)
|
|
45
|
+
'matthew-dynamic-7': 'object-config',
|
|
46
|
+
'mike-dynamic-config-4': 'complex-object',
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
const getValueType = (value: any): ValueType => {
|
|
50
|
+
if (value === null) return 'null';
|
|
51
|
+
if (value === undefined) return 'undefined';
|
|
52
|
+
if (Array.isArray(value)) return 'array';
|
|
53
|
+
return typeof value as ValueType;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const FeatureFlagsScreen: React.FC = () => {
|
|
57
|
+
const {mixpanel, isInitialized, track} = useMixpanel();
|
|
58
|
+
|
|
59
|
+
// State
|
|
60
|
+
const [flagsReady, setFlagsReady] = useState(false);
|
|
61
|
+
const [isLoadingFlags, setIsLoadingFlags] = useState(false);
|
|
62
|
+
const [allFlags, setAllFlags] = useState<Record<string, FlagInfo>>({});
|
|
63
|
+
const [selectedFlag, setSelectedFlag] = useState<string>('react-native');
|
|
64
|
+
const [testMode, setTestMode] = useState<TestMode>('sync');
|
|
65
|
+
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
66
|
+
const [trackedEvents, setTrackedEvents] = useState<TrackedEvent[]>([]);
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// Track screen view
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isInitialized) {
|
|
72
|
+
track(Events.SCREEN_VIEWED, {
|
|
73
|
+
[Properties.SCREEN_NAME]: 'Feature Flags',
|
|
74
|
+
[Properties.TIMESTAMP]: new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
console.log('Tracked SCREEN_VIEWED for Feature Flags screen');
|
|
77
|
+
}
|
|
78
|
+
}, [isInitialized, track]);
|
|
79
|
+
|
|
80
|
+
// Check if flags are ready on mount and after load
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (mixpanel && isInitialized) {
|
|
83
|
+
const ready = mixpanel.flags.areFlagsReady();
|
|
84
|
+
setFlagsReady(ready);
|
|
85
|
+
|
|
86
|
+
if (ready) {
|
|
87
|
+
refreshAllFlags();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [mixpanel, isInitialized]);
|
|
91
|
+
|
|
92
|
+
// Intercept track calls to log them
|
|
93
|
+
const trackWithLog = useCallback(
|
|
94
|
+
(eventName: string, properties?: Record<string, any>) => {
|
|
95
|
+
const event: TrackedEvent = {
|
|
96
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
97
|
+
timestamp: new Date(),
|
|
98
|
+
eventName,
|
|
99
|
+
properties: properties || {},
|
|
100
|
+
};
|
|
101
|
+
setTrackedEvents(prev => [event, ...prev].slice(0, 20));
|
|
102
|
+
track(eventName, properties);
|
|
103
|
+
},
|
|
104
|
+
[track],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Refresh all flags from Mixpanel
|
|
108
|
+
const refreshAllFlags = useCallback(() => {
|
|
109
|
+
if (!mixpanel || !flagsReady) return;
|
|
110
|
+
|
|
111
|
+
const flags: Record<string, FlagInfo> = {};
|
|
112
|
+
|
|
113
|
+
// Get all recommended flags
|
|
114
|
+
Object.keys(RECOMMENDED_FLAGS).forEach(flagKey => {
|
|
115
|
+
try {
|
|
116
|
+
const variant = mixpanel.flags.getVariantSync(flagKey, {
|
|
117
|
+
key: 'fallback',
|
|
118
|
+
value: null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let displayValue = variant.value;
|
|
122
|
+
|
|
123
|
+
// Handle dynamic config flags that return JSON strings
|
|
124
|
+
if (variant.key === '$dynamic_config' && typeof variant.value === 'string') {
|
|
125
|
+
try {
|
|
126
|
+
displayValue = JSON.parse(variant.value);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// If parsing fails, keep as string
|
|
129
|
+
displayValue = variant.value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
flags[flagKey] = {
|
|
134
|
+
key: flagKey,
|
|
135
|
+
value: displayValue,
|
|
136
|
+
valueType: getValueType(displayValue),
|
|
137
|
+
variantKey: variant.key,
|
|
138
|
+
experimentID: variant.experimentID,
|
|
139
|
+
isExperimentActive: variant.isExperimentActive,
|
|
140
|
+
isQATester: variant.isQATester,
|
|
141
|
+
lastAccessed: new Date(),
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`Failed to get flag ${flagKey}:`, error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
setAllFlags(flags);
|
|
149
|
+
}, [mixpanel, flagsReady]);
|
|
150
|
+
|
|
151
|
+
// Load flags from Mixpanel
|
|
152
|
+
const handleLoadFlags = async () => {
|
|
153
|
+
if (!mixpanel) {
|
|
154
|
+
Alert.alert('Error', 'Mixpanel not initialized');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
setIsLoadingFlags(true);
|
|
160
|
+
|
|
161
|
+
await mixpanel.flags.loadFlags();
|
|
162
|
+
|
|
163
|
+
setFlagsReady(true);
|
|
164
|
+
|
|
165
|
+
trackWithLog(Events.FLAGS_LOADED, {
|
|
166
|
+
[Properties.TIMESTAMP]: new Date().toISOString(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
refreshAllFlags();
|
|
170
|
+
|
|
171
|
+
Alert.alert(
|
|
172
|
+
'Flags Loaded',
|
|
173
|
+
`Successfully fetched ${Object.keys(allFlags).length} feature flags!`,
|
|
174
|
+
);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Failed to load flags:', error);
|
|
177
|
+
Alert.alert(
|
|
178
|
+
'Load Failed',
|
|
179
|
+
`Failed to load feature flags: ${error instanceof Error ? error.message : String(error)}`,
|
|
180
|
+
);
|
|
181
|
+
} finally {
|
|
182
|
+
setIsLoadingFlags(false);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Helper to create test result
|
|
187
|
+
const createTestResult = (
|
|
188
|
+
method: string,
|
|
189
|
+
flagName: string,
|
|
190
|
+
fallback: any,
|
|
191
|
+
result: any,
|
|
192
|
+
startTime: number,
|
|
193
|
+
error?: string,
|
|
194
|
+
): TestResult => {
|
|
195
|
+
const executionTime = Date.now() - startTime;
|
|
196
|
+
const resultType = getValueType(result);
|
|
197
|
+
const usedFallback =
|
|
198
|
+
JSON.stringify(result) === JSON.stringify(fallback) || error !== undefined;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
202
|
+
timestamp: new Date(),
|
|
203
|
+
method,
|
|
204
|
+
flagName,
|
|
205
|
+
fallback,
|
|
206
|
+
result,
|
|
207
|
+
resultType,
|
|
208
|
+
executionTime,
|
|
209
|
+
usedFallback,
|
|
210
|
+
error,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Sync Method Tests
|
|
215
|
+
const testGetVariantSync = () => {
|
|
216
|
+
if (!mixpanel) return;
|
|
217
|
+
|
|
218
|
+
const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
|
|
219
|
+
const startTime = Date.now();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const result = mixpanel.flags.getVariantSync(selectedFlag, fallback);
|
|
223
|
+
const testResult = createTestResult(
|
|
224
|
+
`getVariantSync('${selectedFlag}', fallback)`,
|
|
225
|
+
selectedFlag,
|
|
226
|
+
fallback,
|
|
227
|
+
result,
|
|
228
|
+
startTime,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
setTestResult(testResult);
|
|
232
|
+
trackWithLog(Events.FLAG_TEST_SYNC, {
|
|
233
|
+
[Properties.FLAG_METHOD]: 'getVariantSync',
|
|
234
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
235
|
+
[Properties.FLAG_VALUE]: result.value,
|
|
236
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const testResult = createTestResult(
|
|
240
|
+
`getVariantSync('${selectedFlag}', fallback)`,
|
|
241
|
+
selectedFlag,
|
|
242
|
+
fallback,
|
|
243
|
+
fallback,
|
|
244
|
+
startTime,
|
|
245
|
+
error instanceof Error ? error.message : String(error),
|
|
246
|
+
);
|
|
247
|
+
setTestResult(testResult);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const testGetVariantValueSync = () => {
|
|
252
|
+
if (!mixpanel) return;
|
|
253
|
+
|
|
254
|
+
const fallback = JSON.parse(customFallback);
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const result = mixpanel.flags.getVariantValueSync(selectedFlag, fallback);
|
|
259
|
+
const testResult = createTestResult(
|
|
260
|
+
`getVariantValueSync('${selectedFlag}', ${customFallback})`,
|
|
261
|
+
selectedFlag,
|
|
262
|
+
fallback,
|
|
263
|
+
result,
|
|
264
|
+
startTime,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
setTestResult(testResult);
|
|
268
|
+
trackWithLog(Events.FLAG_TEST_SYNC, {
|
|
269
|
+
[Properties.FLAG_METHOD]: 'getVariantValueSync',
|
|
270
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
271
|
+
[Properties.FLAG_VALUE]: result,
|
|
272
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const testResult = createTestResult(
|
|
276
|
+
`getVariantValueSync('${selectedFlag}', ${customFallback})`,
|
|
277
|
+
selectedFlag,
|
|
278
|
+
fallback,
|
|
279
|
+
fallback,
|
|
280
|
+
startTime,
|
|
281
|
+
error instanceof Error ? error.message : String(error),
|
|
282
|
+
);
|
|
283
|
+
setTestResult(testResult);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const testIsEnabledSync = () => {
|
|
288
|
+
if (!mixpanel) return;
|
|
289
|
+
|
|
290
|
+
const fallback = false;
|
|
291
|
+
const startTime = Date.now();
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const result = mixpanel.flags.isEnabledSync(selectedFlag, fallback);
|
|
295
|
+
const testResult = createTestResult(
|
|
296
|
+
`isEnabledSync('${selectedFlag}', false)`,
|
|
297
|
+
selectedFlag,
|
|
298
|
+
fallback,
|
|
299
|
+
result,
|
|
300
|
+
startTime,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
setTestResult(testResult);
|
|
304
|
+
trackWithLog(Events.FLAG_TEST_SYNC, {
|
|
305
|
+
[Properties.FLAG_METHOD]: 'isEnabledSync',
|
|
306
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
307
|
+
[Properties.FLAG_ENABLED]: result,
|
|
308
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const testResult = createTestResult(
|
|
312
|
+
`isEnabledSync('${selectedFlag}', false)`,
|
|
313
|
+
selectedFlag,
|
|
314
|
+
fallback,
|
|
315
|
+
fallback,
|
|
316
|
+
startTime,
|
|
317
|
+
error instanceof Error ? error.message : String(error),
|
|
318
|
+
);
|
|
319
|
+
setTestResult(testResult);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Async Method Tests (Promise)
|
|
324
|
+
const testGetVariantAsync = async () => {
|
|
325
|
+
if (!mixpanel) return;
|
|
326
|
+
|
|
327
|
+
const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
|
|
328
|
+
const startTime = Date.now();
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const result = await mixpanel.flags.getVariant(selectedFlag, fallback);
|
|
332
|
+
const testResult = createTestResult(
|
|
333
|
+
`await getVariant('${selectedFlag}', fallback)`,
|
|
334
|
+
selectedFlag,
|
|
335
|
+
fallback,
|
|
336
|
+
result,
|
|
337
|
+
startTime,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
setTestResult(testResult);
|
|
341
|
+
trackWithLog(Events.FLAG_TEST_ASYNC, {
|
|
342
|
+
[Properties.FLAG_METHOD]: 'getVariant',
|
|
343
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
344
|
+
[Properties.FLAG_VALUE]: result.value,
|
|
345
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const testResult = createTestResult(
|
|
349
|
+
`await getVariant('${selectedFlag}', fallback)`,
|
|
350
|
+
selectedFlag,
|
|
351
|
+
fallback,
|
|
352
|
+
fallback,
|
|
353
|
+
startTime,
|
|
354
|
+
error instanceof Error ? error.message : String(error),
|
|
355
|
+
);
|
|
356
|
+
setTestResult(testResult);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const testGetVariantValueAsync = async () => {
|
|
361
|
+
if (!mixpanel) return;
|
|
362
|
+
|
|
363
|
+
const fallback = JSON.parse(customFallback);
|
|
364
|
+
const startTime = Date.now();
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const result = await mixpanel.flags.getVariantValue(
|
|
368
|
+
selectedFlag,
|
|
369
|
+
fallback,
|
|
370
|
+
);
|
|
371
|
+
const testResult = createTestResult(
|
|
372
|
+
`await getVariantValue('${selectedFlag}', ${customFallback})`,
|
|
373
|
+
selectedFlag,
|
|
374
|
+
fallback,
|
|
375
|
+
result,
|
|
376
|
+
startTime,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
setTestResult(testResult);
|
|
380
|
+
trackWithLog(Events.FLAG_TEST_ASYNC, {
|
|
381
|
+
[Properties.FLAG_METHOD]: 'getVariantValue',
|
|
382
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
383
|
+
[Properties.FLAG_VALUE]: result,
|
|
384
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const testResult = createTestResult(
|
|
388
|
+
`await getVariantValue('${selectedFlag}', ${customFallback})`,
|
|
389
|
+
selectedFlag,
|
|
390
|
+
fallback,
|
|
391
|
+
fallback,
|
|
392
|
+
startTime,
|
|
393
|
+
error instanceof Error ? error.message : String(error),
|
|
394
|
+
);
|
|
395
|
+
setTestResult(testResult);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const testIsEnabledAsync = async () => {
|
|
400
|
+
if (!mixpanel) return;
|
|
401
|
+
|
|
402
|
+
const fallback = false;
|
|
403
|
+
const startTime = Date.now();
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const result = await mixpanel.flags.isEnabled(selectedFlag, fallback);
|
|
407
|
+
const testResult = createTestResult(
|
|
408
|
+
`await isEnabled('${selectedFlag}', false)`,
|
|
409
|
+
selectedFlag,
|
|
410
|
+
fallback,
|
|
411
|
+
result,
|
|
412
|
+
startTime,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
setTestResult(testResult);
|
|
416
|
+
trackWithLog(Events.FLAG_TEST_ASYNC, {
|
|
417
|
+
[Properties.FLAG_METHOD]: 'isEnabled',
|
|
418
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
419
|
+
[Properties.FLAG_ENABLED]: result,
|
|
420
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const testResult = createTestResult(
|
|
424
|
+
`await isEnabled('${selectedFlag}', false)`,
|
|
425
|
+
selectedFlag,
|
|
426
|
+
fallback,
|
|
427
|
+
fallback,
|
|
428
|
+
startTime,
|
|
429
|
+
error instanceof Error ? error.message : String(error),
|
|
430
|
+
);
|
|
431
|
+
setTestResult(testResult);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// Callback Pattern Test
|
|
436
|
+
const testGetVariantCallback = () => {
|
|
437
|
+
if (!mixpanel) return;
|
|
438
|
+
|
|
439
|
+
const fallback = {key: 'fallback', value: JSON.parse(customFallback)};
|
|
440
|
+
const startTime = Date.now();
|
|
441
|
+
|
|
442
|
+
mixpanel.flags.getVariant(selectedFlag, fallback, result => {
|
|
443
|
+
const testResult = createTestResult(
|
|
444
|
+
`getVariant('${selectedFlag}', fallback, callback)`,
|
|
445
|
+
selectedFlag,
|
|
446
|
+
fallback,
|
|
447
|
+
result,
|
|
448
|
+
startTime,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
setTestResult(testResult);
|
|
452
|
+
trackWithLog(Events.FLAG_TEST_CALLBACK, {
|
|
453
|
+
[Properties.FLAG_METHOD]: 'getVariant (callback)',
|
|
454
|
+
[Properties.FLAG_KEY]: selectedFlag,
|
|
455
|
+
[Properties.FLAG_VALUE]: result.value,
|
|
456
|
+
[Properties.FLAG_EXECUTION_TIME]: testResult.executionTime,
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Edge Case Tests
|
|
462
|
+
const testNonExistentFlag = () => {
|
|
463
|
+
if (!mixpanel) return;
|
|
464
|
+
|
|
465
|
+
const fakeFlag = 'non-existent-flag-12345';
|
|
466
|
+
const fallback = {key: 'fallback', value: 'NOT_FOUND'};
|
|
467
|
+
const startTime = Date.now();
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const result = mixpanel.flags.getVariantSync(fakeFlag, fallback);
|
|
471
|
+
const testResult = createTestResult(
|
|
472
|
+
`getVariantSync('${fakeFlag}', fallback) [non-existent]`,
|
|
473
|
+
fakeFlag,
|
|
474
|
+
fallback,
|
|
475
|
+
result,
|
|
476
|
+
startTime,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
setTestResult(testResult);
|
|
480
|
+
trackWithLog(Events.FLAG_TEST_EDGE_CASE, {
|
|
481
|
+
[Properties.FLAG_METHOD]: 'non-existent flag',
|
|
482
|
+
[Properties.FLAG_KEY]: fakeFlag,
|
|
483
|
+
[Properties.FLAG_USED_FALLBACK]: testResult.usedFallback,
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const testResult = createTestResult(
|
|
487
|
+
`getVariantSync('${fakeFlag}', fallback) [non-existent]`,
|
|
488
|
+
fakeFlag,
|
|
489
|
+
fallback,
|
|
490
|
+
fallback,
|
|
491
|
+
startTime,
|
|
492
|
+
error instanceof Error ? error.message : String(error),
|
|
493
|
+
);
|
|
494
|
+
setTestResult(testResult);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const testTypeCoercion = (value: any, type: string) => {
|
|
499
|
+
if (!mixpanel) return;
|
|
500
|
+
|
|
501
|
+
// Create a mock flag scenario for type coercion testing
|
|
502
|
+
const fallback = value;
|
|
503
|
+
const startTime = Date.now();
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
// For demo purposes, we'll use isEnabledSync with different values
|
|
507
|
+
const result = Boolean(value);
|
|
508
|
+
const testResult = createTestResult(
|
|
509
|
+
`Boolean(${JSON.stringify(value)}) [${type}]`,
|
|
510
|
+
'type-coercion-test',
|
|
511
|
+
fallback,
|
|
512
|
+
result,
|
|
513
|
+
startTime,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
setTestResult(testResult);
|
|
517
|
+
trackWithLog(Events.FLAG_TEST_COERCION, {
|
|
518
|
+
[Properties.FLAG_METHOD]: 'type coercion',
|
|
519
|
+
[Properties.FLAG_VALUE]: value,
|
|
520
|
+
[Properties.FLAG_RESULT_TYPE]: type,
|
|
521
|
+
[Properties.FLAG_ENABLED]: result,
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const testResult = createTestResult(
|
|
525
|
+
`Boolean(${JSON.stringify(value)}) [${type}]`,
|
|
526
|
+
'type-coercion-test',
|
|
527
|
+
fallback,
|
|
528
|
+
fallback,
|
|
529
|
+
startTime,
|
|
530
|
+
error instanceof Error ? error.message : String(error),
|
|
531
|
+
);
|
|
532
|
+
setTestResult(testResult);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
538
|
+
<View style={styles.header}>
|
|
539
|
+
<Text style={styles.title}>Feature Flags Testing</Text>
|
|
540
|
+
<Text style={styles.subtitle}>
|
|
541
|
+
Integration test bed for comprehensive API coverage
|
|
542
|
+
</Text>
|
|
543
|
+
</View>
|
|
544
|
+
|
|
545
|
+
{/* Status Bar */}
|
|
546
|
+
<View style={styles.statusBar}>
|
|
547
|
+
<View style={styles.statusItem}>
|
|
548
|
+
<Text style={styles.statusLabel}>Status:</Text>
|
|
549
|
+
<Text style={[styles.statusValue, flagsReady && styles.statusReady]}>
|
|
550
|
+
{flagsReady ? '✅ Ready' : '⏳ Not Ready'}
|
|
551
|
+
</Text>
|
|
552
|
+
</View>
|
|
553
|
+
<View style={styles.statusItem}>
|
|
554
|
+
<Text style={styles.statusLabel}>Flags:</Text>
|
|
555
|
+
<Text style={styles.statusValue}>{Object.keys(allFlags).length}</Text>
|
|
556
|
+
</View>
|
|
557
|
+
</View>
|
|
558
|
+
|
|
559
|
+
{isLoadingFlags && (
|
|
560
|
+
<View style={styles.loadingContainer}>
|
|
561
|
+
<ActivityIndicator size="large" color="#007AFF" />
|
|
562
|
+
<Text style={styles.loadingText}>Loading feature flags...</Text>
|
|
563
|
+
</View>
|
|
564
|
+
)}
|
|
565
|
+
|
|
566
|
+
{/* Lifecycle Controls */}
|
|
567
|
+
<View style={styles.section}>
|
|
568
|
+
<Text style={styles.sectionTitle}>🔄 Lifecycle Controls</Text>
|
|
569
|
+
<View style={styles.buttonRow}>
|
|
570
|
+
<ActionButton
|
|
571
|
+
title={flagsReady ? 'Reload Flags' : 'Load Flags'}
|
|
572
|
+
onPress={handleLoadFlags}
|
|
573
|
+
disabled={!isInitialized || isLoadingFlags}
|
|
574
|
+
style={styles.halfButton}
|
|
575
|
+
/>
|
|
576
|
+
<ActionButton
|
|
577
|
+
title="Refresh Display"
|
|
578
|
+
onPress={refreshAllFlags}
|
|
579
|
+
disabled={!flagsReady}
|
|
580
|
+
variant="secondary"
|
|
581
|
+
style={styles.halfButton}
|
|
582
|
+
/>
|
|
583
|
+
</View>
|
|
584
|
+
</View>
|
|
585
|
+
|
|
586
|
+
{/* All Flags Display */}
|
|
587
|
+
{Object.keys(allFlags).length > 0 && (
|
|
588
|
+
<View style={styles.section}>
|
|
589
|
+
<Text style={styles.sectionTitle}>
|
|
590
|
+
📋 All Flags ({Object.keys(allFlags).length})
|
|
591
|
+
</Text>
|
|
592
|
+
{Object.values(allFlags).map(flag => (
|
|
593
|
+
<FlagCard
|
|
594
|
+
key={flag.key}
|
|
595
|
+
flag={flag}
|
|
596
|
+
onTest={key => setSelectedFlag(key)}
|
|
597
|
+
/>
|
|
598
|
+
))}
|
|
599
|
+
</View>
|
|
600
|
+
)}
|
|
601
|
+
|
|
602
|
+
{/* Test Mode Tabs */}
|
|
603
|
+
{flagsReady && (
|
|
604
|
+
<View style={styles.section}>
|
|
605
|
+
<Text style={styles.sectionTitle}>🧪 Test Controls</Text>
|
|
606
|
+
<View style={styles.tabs}>
|
|
607
|
+
<TouchableOpacity
|
|
608
|
+
style={[styles.tab, testMode === 'sync' && styles.activeTab]}
|
|
609
|
+
onPress={() => setTestMode('sync')}>
|
|
610
|
+
<Text
|
|
611
|
+
style={[
|
|
612
|
+
styles.tabText,
|
|
613
|
+
testMode === 'sync' && styles.activeTabText,
|
|
614
|
+
]}>
|
|
615
|
+
Sync
|
|
616
|
+
</Text>
|
|
617
|
+
</TouchableOpacity>
|
|
618
|
+
<TouchableOpacity
|
|
619
|
+
style={[styles.tab, testMode === 'async' && styles.activeTab]}
|
|
620
|
+
onPress={() => setTestMode('async')}>
|
|
621
|
+
<Text
|
|
622
|
+
style={[
|
|
623
|
+
styles.tabText,
|
|
624
|
+
testMode === 'async' && styles.activeTabText,
|
|
625
|
+
]}>
|
|
626
|
+
Async
|
|
627
|
+
</Text>
|
|
628
|
+
</TouchableOpacity>
|
|
629
|
+
<TouchableOpacity
|
|
630
|
+
style={[styles.tab, testMode === 'edge' && styles.activeTab]}
|
|
631
|
+
onPress={() => setTestMode('edge')}>
|
|
632
|
+
<Text
|
|
633
|
+
style={[
|
|
634
|
+
styles.tabText,
|
|
635
|
+
testMode === 'edge' && styles.activeTabText,
|
|
636
|
+
]}>
|
|
637
|
+
Edge Cases
|
|
638
|
+
</Text>
|
|
639
|
+
</TouchableOpacity>
|
|
640
|
+
<TouchableOpacity
|
|
641
|
+
style={[styles.tab, testMode === 'coercion' && styles.activeTab]}
|
|
642
|
+
onPress={() => setTestMode('coercion')}>
|
|
643
|
+
<Text
|
|
644
|
+
style={[
|
|
645
|
+
styles.tabText,
|
|
646
|
+
testMode === 'coercion' && styles.activeTabText,
|
|
647
|
+
]}>
|
|
648
|
+
Coercion
|
|
649
|
+
</Text>
|
|
650
|
+
</TouchableOpacity>
|
|
651
|
+
</View>
|
|
652
|
+
|
|
653
|
+
{/* Flag Selector */}
|
|
654
|
+
<View style={styles.flagSelector}>
|
|
655
|
+
<Text style={styles.label}>Test Flag:</Text>
|
|
656
|
+
<View style={styles.flagButtons}>
|
|
657
|
+
{Object.keys(RECOMMENDED_FLAGS).map(flagKey => (
|
|
658
|
+
<TouchableOpacity
|
|
659
|
+
key={flagKey}
|
|
660
|
+
style={[
|
|
661
|
+
styles.flagButton,
|
|
662
|
+
selectedFlag === flagKey && styles.selectedFlagButton,
|
|
663
|
+
]}
|
|
664
|
+
onPress={() => setSelectedFlag(flagKey)}>
|
|
665
|
+
<Text
|
|
666
|
+
style={[
|
|
667
|
+
styles.flagButtonText,
|
|
668
|
+
selectedFlag === flagKey && styles.selectedFlagButtonText,
|
|
669
|
+
]}>
|
|
670
|
+
{flagKey}
|
|
671
|
+
</Text>
|
|
672
|
+
</TouchableOpacity>
|
|
673
|
+
))}
|
|
674
|
+
</View>
|
|
675
|
+
</View>
|
|
676
|
+
|
|
677
|
+
{/* Sync Test Panel */}
|
|
678
|
+
{testMode === 'sync' && (
|
|
679
|
+
<View style={styles.testPanel}>
|
|
680
|
+
<Text style={styles.helper}>
|
|
681
|
+
Synchronous methods use cached values (fast, no network delay):
|
|
682
|
+
</Text>
|
|
683
|
+
<ActionButton
|
|
684
|
+
title="getVariantSync()"
|
|
685
|
+
onPress={testGetVariantSync}
|
|
686
|
+
style={styles.testButton}
|
|
687
|
+
/>
|
|
688
|
+
<ActionButton
|
|
689
|
+
title="getVariantValueSync()"
|
|
690
|
+
onPress={testGetVariantValueSync}
|
|
691
|
+
variant="secondary"
|
|
692
|
+
style={styles.testButton}
|
|
693
|
+
/>
|
|
694
|
+
<ActionButton
|
|
695
|
+
title="isEnabledSync()"
|
|
696
|
+
onPress={testIsEnabledSync}
|
|
697
|
+
variant="secondary"
|
|
698
|
+
style={styles.testButton}
|
|
699
|
+
/>
|
|
700
|
+
<Text style={styles.helperSmall}>
|
|
701
|
+
💡 Note: isEnabledSync() only returns true for boolean-valued flags (FeatureGates).
|
|
702
|
+
For string experiments, use getVariantValueSync() instead.
|
|
703
|
+
</Text>
|
|
704
|
+
</View>
|
|
705
|
+
)}
|
|
706
|
+
|
|
707
|
+
{/* Async Test Panel */}
|
|
708
|
+
{testMode === 'async' && (
|
|
709
|
+
<View style={styles.testPanel}>
|
|
710
|
+
<ActionButton
|
|
711
|
+
title="getVariant() - Promise"
|
|
712
|
+
onPress={testGetVariantAsync}
|
|
713
|
+
style={styles.testButton}
|
|
714
|
+
/>
|
|
715
|
+
<ActionButton
|
|
716
|
+
title="getVariantValue() - Promise"
|
|
717
|
+
onPress={testGetVariantValueAsync}
|
|
718
|
+
variant="secondary"
|
|
719
|
+
style={styles.testButton}
|
|
720
|
+
/>
|
|
721
|
+
<ActionButton
|
|
722
|
+
title="isEnabled() - Promise"
|
|
723
|
+
onPress={testIsEnabledAsync}
|
|
724
|
+
variant="secondary"
|
|
725
|
+
style={styles.testButton}
|
|
726
|
+
/>
|
|
727
|
+
<ActionButton
|
|
728
|
+
title="getVariant() - Callback"
|
|
729
|
+
onPress={testGetVariantCallback}
|
|
730
|
+
variant="secondary"
|
|
731
|
+
style={styles.testButton}
|
|
732
|
+
/>
|
|
733
|
+
</View>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
{/* Edge Case Test Panel */}
|
|
737
|
+
{testMode === 'edge' && (
|
|
738
|
+
<View style={styles.testPanel}>
|
|
739
|
+
<ActionButton
|
|
740
|
+
title="Test Non-Existent Flag"
|
|
741
|
+
onPress={testNonExistentFlag}
|
|
742
|
+
style={styles.testButton}
|
|
743
|
+
/>
|
|
744
|
+
<Text style={styles.helper}>
|
|
745
|
+
Tests fallback behavior when flag doesn't exist
|
|
746
|
+
</Text>
|
|
747
|
+
</View>
|
|
748
|
+
)}
|
|
749
|
+
|
|
750
|
+
{/* Type Coercion Test Panel */}
|
|
751
|
+
{testMode === 'coercion' && (
|
|
752
|
+
<View style={styles.testPanel}>
|
|
753
|
+
<Text style={styles.helper}>
|
|
754
|
+
Tests Boolean() coercion for isEnabled():
|
|
755
|
+
</Text>
|
|
756
|
+
<View style={styles.coercionGrid}>
|
|
757
|
+
<ActionButton
|
|
758
|
+
title="0 → false"
|
|
759
|
+
onPress={() => testTypeCoercion(0, 'zero')}
|
|
760
|
+
variant="secondary"
|
|
761
|
+
style={styles.coercionButton}
|
|
762
|
+
/>
|
|
763
|
+
<ActionButton
|
|
764
|
+
title="1 → true"
|
|
765
|
+
onPress={() => testTypeCoercion(1, 'one')}
|
|
766
|
+
variant="secondary"
|
|
767
|
+
style={styles.coercionButton}
|
|
768
|
+
/>
|
|
769
|
+
<ActionButton
|
|
770
|
+
title='"" → false'
|
|
771
|
+
onPress={() => testTypeCoercion('', 'empty string')}
|
|
772
|
+
variant="secondary"
|
|
773
|
+
style={styles.coercionButton}
|
|
774
|
+
/>
|
|
775
|
+
<ActionButton
|
|
776
|
+
title='"text" → true'
|
|
777
|
+
onPress={() => testTypeCoercion('text', 'string')}
|
|
778
|
+
variant="secondary"
|
|
779
|
+
style={styles.coercionButton}
|
|
780
|
+
/>
|
|
781
|
+
<ActionButton
|
|
782
|
+
title="null → false"
|
|
783
|
+
onPress={() => testTypeCoercion(null, 'null')}
|
|
784
|
+
variant="secondary"
|
|
785
|
+
style={styles.coercionButton}
|
|
786
|
+
/>
|
|
787
|
+
<ActionButton
|
|
788
|
+
title="{} → true"
|
|
789
|
+
onPress={() => testTypeCoercion({}, 'object')}
|
|
790
|
+
variant="secondary"
|
|
791
|
+
style={styles.coercionButton}
|
|
792
|
+
/>
|
|
793
|
+
</View>
|
|
794
|
+
</View>
|
|
795
|
+
)}
|
|
796
|
+
</View>
|
|
797
|
+
)}
|
|
798
|
+
|
|
799
|
+
{/* Test Results */}
|
|
800
|
+
{testResult && (
|
|
801
|
+
<View style={styles.section}>
|
|
802
|
+
<Text style={styles.sectionTitle}>📊 Test Results</Text>
|
|
803
|
+
<TestResultDisplay result={testResult} />
|
|
804
|
+
</View>
|
|
805
|
+
)}
|
|
806
|
+
|
|
807
|
+
{/* Event Tracking Log */}
|
|
808
|
+
{trackedEvents.length > 0 && (
|
|
809
|
+
<View style={styles.section}>
|
|
810
|
+
<Text style={styles.sectionTitle}>
|
|
811
|
+
📈 Recent Events ({trackedEvents.length})
|
|
812
|
+
</Text>
|
|
813
|
+
<EventTrackingLog events={trackedEvents} maxEvents={5} />
|
|
814
|
+
</View>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{/* Info Card */}
|
|
818
|
+
<InfoCard
|
|
819
|
+
title="Test Flag Categories"
|
|
820
|
+
content={`This screen uses existing flags from your project:
|
|
821
|
+
|
|
822
|
+
BOOLEAN FLAGS (FeatureGate):
|
|
823
|
+
• sample-bool-flag, hash-slinging-slasher, mike-test
|
|
824
|
+
Variants: "On" (true) / "Off" (false)
|
|
825
|
+
Test with: isEnabled() or getVariantValue()
|
|
826
|
+
|
|
827
|
+
STRING EXPERIMENT FLAGS:
|
|
828
|
+
• sample-exp-testing, new_feature_flag_v2, mojojojo
|
|
829
|
+
Custom string variants (e.g., "control", "treatment")
|
|
830
|
+
Test with: getVariant() to see variant key
|
|
831
|
+
|
|
832
|
+
ACTIVE EXPERIMENTS (with experimentID):
|
|
833
|
+
• general-replay-events-query-improvement
|
|
834
|
+
• test-active-flag
|
|
835
|
+
Watch for $experiment_started events!
|
|
836
|
+
|
|
837
|
+
DYNAMIC CONFIG (Object values):
|
|
838
|
+
• matthew-dynamic-7 - Simple object
|
|
839
|
+
• mike-dynamic-config-4 - Complex nested object
|
|
840
|
+
Test with: getVariantValue() for JSON objects
|
|
841
|
+
|
|
842
|
+
Use the tabs to test different API methods (Sync/Async/Edge Cases/Coercion) with these flags.`}
|
|
843
|
+
style={styles.infoCard}
|
|
844
|
+
/>
|
|
845
|
+
</ScrollView>
|
|
846
|
+
);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const styles = StyleSheet.create({
|
|
850
|
+
container: {
|
|
851
|
+
flex: 1,
|
|
852
|
+
backgroundColor: '#fff',
|
|
853
|
+
},
|
|
854
|
+
content: {
|
|
855
|
+
padding: 20,
|
|
856
|
+
},
|
|
857
|
+
header: {
|
|
858
|
+
marginBottom: 20,
|
|
859
|
+
},
|
|
860
|
+
title: {
|
|
861
|
+
fontSize: 24,
|
|
862
|
+
fontWeight: 'bold',
|
|
863
|
+
color: '#333',
|
|
864
|
+
marginBottom: 8,
|
|
865
|
+
},
|
|
866
|
+
subtitle: {
|
|
867
|
+
fontSize: 14,
|
|
868
|
+
color: '#666',
|
|
869
|
+
},
|
|
870
|
+
statusBar: {
|
|
871
|
+
flexDirection: 'row',
|
|
872
|
+
backgroundColor: '#f8f9fa',
|
|
873
|
+
padding: 12,
|
|
874
|
+
borderRadius: 8,
|
|
875
|
+
marginBottom: 20,
|
|
876
|
+
},
|
|
877
|
+
statusItem: {
|
|
878
|
+
flexDirection: 'row',
|
|
879
|
+
alignItems: 'center',
|
|
880
|
+
marginRight: 20,
|
|
881
|
+
},
|
|
882
|
+
statusLabel: {
|
|
883
|
+
fontSize: 13,
|
|
884
|
+
color: '#666',
|
|
885
|
+
marginRight: 6,
|
|
886
|
+
},
|
|
887
|
+
statusValue: {
|
|
888
|
+
fontSize: 13,
|
|
889
|
+
fontWeight: '600',
|
|
890
|
+
color: '#333',
|
|
891
|
+
},
|
|
892
|
+
statusReady: {
|
|
893
|
+
color: '#4caf50',
|
|
894
|
+
},
|
|
895
|
+
section: {
|
|
896
|
+
marginBottom: 24,
|
|
897
|
+
},
|
|
898
|
+
sectionTitle: {
|
|
899
|
+
fontSize: 18,
|
|
900
|
+
fontWeight: '600',
|
|
901
|
+
color: '#333',
|
|
902
|
+
marginBottom: 12,
|
|
903
|
+
},
|
|
904
|
+
buttonRow: {
|
|
905
|
+
flexDirection: 'row',
|
|
906
|
+
gap: 12,
|
|
907
|
+
},
|
|
908
|
+
halfButton: {
|
|
909
|
+
flex: 1,
|
|
910
|
+
},
|
|
911
|
+
loadingContainer: {
|
|
912
|
+
alignItems: 'center',
|
|
913
|
+
paddingVertical: 20,
|
|
914
|
+
marginBottom: 20,
|
|
915
|
+
},
|
|
916
|
+
loadingText: {
|
|
917
|
+
marginTop: 10,
|
|
918
|
+
fontSize: 14,
|
|
919
|
+
color: '#666',
|
|
920
|
+
},
|
|
921
|
+
tabs: {
|
|
922
|
+
flexDirection: 'row',
|
|
923
|
+
backgroundColor: '#f0f0f0',
|
|
924
|
+
borderRadius: 8,
|
|
925
|
+
padding: 4,
|
|
926
|
+
marginBottom: 16,
|
|
927
|
+
},
|
|
928
|
+
tab: {
|
|
929
|
+
flex: 1,
|
|
930
|
+
paddingVertical: 8,
|
|
931
|
+
alignItems: 'center',
|
|
932
|
+
borderRadius: 6,
|
|
933
|
+
},
|
|
934
|
+
activeTab: {
|
|
935
|
+
backgroundColor: '#007AFF',
|
|
936
|
+
},
|
|
937
|
+
tabText: {
|
|
938
|
+
fontSize: 14,
|
|
939
|
+
fontWeight: '500',
|
|
940
|
+
color: '#666',
|
|
941
|
+
},
|
|
942
|
+
activeTabText: {
|
|
943
|
+
color: '#fff',
|
|
944
|
+
},
|
|
945
|
+
flagSelector: {
|
|
946
|
+
marginBottom: 16,
|
|
947
|
+
},
|
|
948
|
+
label: {
|
|
949
|
+
fontSize: 14,
|
|
950
|
+
fontWeight: '600',
|
|
951
|
+
color: '#666',
|
|
952
|
+
marginBottom: 8,
|
|
953
|
+
},
|
|
954
|
+
flagButtons: {
|
|
955
|
+
flexDirection: 'row',
|
|
956
|
+
flexWrap: 'wrap',
|
|
957
|
+
gap: 8,
|
|
958
|
+
},
|
|
959
|
+
flagButton: {
|
|
960
|
+
paddingHorizontal: 12,
|
|
961
|
+
paddingVertical: 8,
|
|
962
|
+
backgroundColor: '#f0f0f0',
|
|
963
|
+
borderRadius: 6,
|
|
964
|
+
borderWidth: 1,
|
|
965
|
+
borderColor: '#e0e0e0',
|
|
966
|
+
},
|
|
967
|
+
selectedFlagButton: {
|
|
968
|
+
backgroundColor: '#007AFF',
|
|
969
|
+
borderColor: '#007AFF',
|
|
970
|
+
},
|
|
971
|
+
flagButtonText: {
|
|
972
|
+
fontSize: 12,
|
|
973
|
+
color: '#666',
|
|
974
|
+
},
|
|
975
|
+
selectedFlagButtonText: {
|
|
976
|
+
color: '#fff',
|
|
977
|
+
fontWeight: '600',
|
|
978
|
+
},
|
|
979
|
+
testPanel: {
|
|
980
|
+
marginTop: 12,
|
|
981
|
+
},
|
|
982
|
+
testButton: {
|
|
983
|
+
marginBottom: 10,
|
|
984
|
+
},
|
|
985
|
+
helper: {
|
|
986
|
+
fontSize: 13,
|
|
987
|
+
color: '#666',
|
|
988
|
+
marginBottom: 12,
|
|
989
|
+
lineHeight: 18,
|
|
990
|
+
},
|
|
991
|
+
helperSmall: {
|
|
992
|
+
fontSize: 12,
|
|
993
|
+
color: '#999',
|
|
994
|
+
marginTop: 12,
|
|
995
|
+
lineHeight: 16,
|
|
996
|
+
fontStyle: 'italic',
|
|
997
|
+
},
|
|
998
|
+
coercionGrid: {
|
|
999
|
+
flexDirection: 'row',
|
|
1000
|
+
flexWrap: 'wrap',
|
|
1001
|
+
gap: 8,
|
|
1002
|
+
marginTop: 8,
|
|
1003
|
+
},
|
|
1004
|
+
coercionButton: {
|
|
1005
|
+
flex: 0,
|
|
1006
|
+
minWidth: '47%',
|
|
1007
|
+
},
|
|
1008
|
+
infoCard: {
|
|
1009
|
+
marginTop: 10,
|
|
1010
|
+
},
|
|
1011
|
+
});
|