diff --git a/CHANGELOG.md b/CHANGELOG.md index 1035a998d..f277b2607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,13 @@ ### Added +- Add support for Feature Flags APIs `Instabug.addFeatureFlags`, `Instabug.removeFeatureFlags` and `Instabug.clearAllFeatureFlags` ([#1230](https://github.com/Instabug/Instabug-React-Native/pull/1230)). - Export `uploadSourcemaps` and `uploadSoFiles` utilities in the `instabug-reactnative/upload` sub-package for usage in custom Node.js upload scripts ([#1252](https://github.com/Instabug/Instabug-React-Native/pull/1252)). +### Deprecated + +- Deprecate Experiments APIs `Instabug.addExperiments`, `Instabug.removeExperiments` and `Instabug.clearAllExperiments` in favor of the new Feature Flags APIs ([#1230](https://github.com/Instabug/Instabug-React-Native/pull/1230)). + ### Fixed - Fix APM network logging on Android ([#1253](https://github.com/Instabug/Instabug-React-Native/pull/1253)). diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index 7d1465608..f936eaa12 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -16,6 +16,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; @@ -29,6 +30,7 @@ import com.instabug.library.LogLevel; import com.instabug.library.ReproConfigurations; import com.instabug.library.core.InstabugCore; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.logging.InstabugLog; @@ -41,6 +43,7 @@ import com.instabug.reactlibrary.utils.MainThreadHandler; import com.instabug.reactlibrary.utils.RNTouchedViewExtractor; + import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; @@ -50,6 +53,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -1023,6 +1027,9 @@ public void run() { }); } + /** + * @deprecated see {@link #addFeatureFlags(ReadableArray)} + */ @ReactMethod public void addExperiments(final ReadableArray experiments) { MainThreadHandler.runOnMainThread(new Runnable() { @@ -1039,6 +1046,9 @@ public void run() { }); } + /** + * @deprecated see {@link #removeFeatureFlags(ReadableArray)} + */ @ReactMethod public void removeExperiments(final ReadableArray experiments) { MainThreadHandler.runOnMainThread(new Runnable() { @@ -1055,6 +1065,9 @@ public void run() { }); } + /** + * @deprecated see {@link #removeAllFeatureFlags()} + */ @ReactMethod public void clearAllExperiments() { MainThreadHandler.runOnMainThread(new Runnable() { @@ -1069,6 +1082,59 @@ public void run() { }); } + @ReactMethod + public void addFeatureFlags(final ReadableMap featureFlagsMap) { + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + Iterator> iterator = featureFlagsMap.getEntryIterator(); + ArrayList featureFlags = new ArrayList<>(); + while (iterator.hasNext()) { + Map.Entry item = iterator.next(); + String variant = (String) item.getValue(); + String name = item.getKey(); + featureFlags.add(new IBGFeatureFlag(name, variant.isEmpty() ? null : variant)); + } + if (!featureFlags.isEmpty()) { + Instabug.addFeatureFlags(featureFlags); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + @ReactMethod + public void removeFeatureFlags(final ReadableArray featureFlags) { + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + ArrayList stringArray = ArrayUtil.parseReadableArrayOfStrings(featureFlags); + Instabug.removeFeatureFlag(stringArray); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + @ReactMethod + public void removeAllFeatureFlags() { + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + Instabug.removeAllFeatureFlags(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + @ReactMethod public void willRedirectToStore() { MainThreadHandler.runOnMainThread(new Runnable() { diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java index beee65c4b..b9bf2308c 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java @@ -18,6 +18,7 @@ import com.instabug.library.IssueType; import com.instabug.library.ReproConfigurations; import com.instabug.library.ReproMode; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.ui.onboarding.WelcomeMessage; import com.instabug.reactlibrary.utils.MainThreadHandler; @@ -38,6 +39,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -567,8 +569,6 @@ public void testIdentifyUserWithId() { @Test public void given$clearAllExperiments_whenQuery_thenShouldCallNativeApi() { - // given - // when rnModule.clearAllExperiments(); @@ -577,6 +577,56 @@ public void testIdentifyUserWithId() { Instabug.clearAllExperiments(); } + @Test + public void testAddFeatureFlags() { + // given + JavaOnlyMap map = new JavaOnlyMap(); + map.putString("key1", "value1"); + map.putString("key2", "value2"); + + // when + rnModule.addFeatureFlags(map); + + // then + Iterator> iterator = map.getEntryIterator(); + ArrayList featureFlags = new ArrayList<>(); + while (iterator.hasNext()) { + Map.Entry item = iterator.next(); + featureFlags.add(new IBGFeatureFlag(item.getKey(), (String) item.getValue())); + } + + mockInstabug.verify(() -> Instabug.addFeatureFlags(featureFlags)); + + } + + @Test + public void testRemoveFeatureFlags() { + // given + JavaOnlyArray array = new JavaOnlyArray(); + array.pushString("exp1"); + array.pushString("exp2"); + + // when + rnModule.removeFeatureFlags(array); + + // then + List expectedList = new ArrayList(); + for (Object o : array.toArrayList()) { + expectedList.add((String) o); + } + mockInstabug.verify(() -> Instabug.removeFeatureFlag(expectedList)); + + } + + @Test + public void testRemoveAllFeatureFlags() { + // when + rnModule.removeAllFeatureFlags(); + + // then + mockInstabug.verify(() -> Instabug.removeAllFeatureFlags()); + } + @Test public void testWillRedirectToStore() { // when diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m index 37990b450..565a21b1a 100644 --- a/examples/default/ios/InstabugTests/InstabugSampleTests.m +++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m @@ -73,7 +73,7 @@ - (void)testInit { NSArray *invocationEvents = [NSArray arrayWithObjects:[NSNumber numberWithInteger:floatingButtonInvocationEvent], nil]; BOOL useNativeNetworkInterception = YES; IBGSDKDebugLogsLevel sdkDebugLogsLevel = IBGSDKDebugLogsLevelDebug; - + OCMStub([mock setCodePushVersion:codePushVersion]); [self.instabugBridge init:appToken invocationEvents:invocationEvents debugLogsLevel:sdkDebugLogsLevel useNativeNetworkInterception:useNativeNetworkInterception codePushVersion:codePushVersion]; @@ -85,9 +85,9 @@ - (void)testInit { - (void)testSetCodePushVersion { id mock = OCMClassMock([Instabug class]); NSString *codePushVersion = @"123"; - + [self.instabugBridge setCodePushVersion:codePushVersion]; - + OCMVerify([mock setCodePushVersion:codePushVersion]); } @@ -498,4 +498,43 @@ - (void)testClearAllExperiments { OCMVerify([mock clearAllExperiments]); } +- (void)testAddFeatureFlags { + id mock = OCMClassMock([Instabug class]); + NSDictionary *featureFlagsMap = @{ @"key13" : @"value1", @"key2" : @"value2"}; + + OCMStub([mock addFeatureFlags :[OCMArg any]]); + [self.instabugBridge addFeatureFlags:featureFlagsMap]; + OCMVerify([mock addFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlags = value; + NSString* firstFeatureFlagName = [featureFlags objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [[featureFlagsMap allKeys] objectAtIndex:0] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]); +} + +- (void)testRemoveFeatureFlags { + id mock = OCMClassMock([Instabug class]); + NSArray *featureFlags = @[@"exp1", @"exp2"]; + [self.instabugBridge removeFeatureFlags:featureFlags]; + OCMVerify([mock removeFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlagsObJ = value; + NSString* firstFeatureFlagName = [featureFlagsObJ objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [featureFlags firstObject] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]); +} + +- (void)testRemoveAllFeatureFlags { + id mock = OCMClassMock([Instabug class]); + OCMStub([mock removeAllFeatureFlags]); + [self.instabugBridge removeAllFeatureFlags]; + OCMVerify([mock removeAllFeatureFlags]); +} + @end diff --git a/examples/default/src/screens/SettingsScreen.tsx b/examples/default/src/screens/SettingsScreen.tsx index 618de54a2..6390bb7a0 100644 --- a/examples/default/src/screens/SettingsScreen.tsx +++ b/examples/default/src/screens/SettingsScreen.tsx @@ -17,8 +17,12 @@ export const SettingsScreen: React.FC = () => { const [userID, setUserID] = useState(''); const [userAttributeKey, setUserAttributeKey] = useState(''); const [userAttributeValue, setUserAttributeValue] = useState(''); + const [featureFlagName, setFeatureFlagName] = useState(''); + const [featureFlagVariant, setfeatureFlagVariant] = useState(''); + const toast = useToast(); const [userAttributesFormError, setUserAttributesFormError] = useState({}); + const [featureFlagFormError, setFeatureFlagFormError] = useState({}); const validateUserAttributeForm = () => { const errors: any = {}; @@ -31,6 +35,15 @@ export const SettingsScreen: React.FC = () => { setUserAttributesFormError(errors); return Object.keys(errors).length === 0; }; + const validateFeatureFlagForm = () => { + const errors: any = {}; + if (featureFlagName.length === 0) { + errors.featureFlagName = 'Value is required'; + } + setFeatureFlagFormError(errors); + return Object.keys(errors).length === 0; + }; + const styles = StyleSheet.create({ inputWrapper: { padding: 4, @@ -60,6 +73,37 @@ export const SettingsScreen: React.FC = () => { setUserAttributeValue(''); } }; + const saveFeatureFlags = () => { + if (validateFeatureFlagForm()) { + Instabug.addFeatureFlag({ + name: featureFlagName, + variant: featureFlagVariant, + }); + toast.show({ + description: 'Feature Flag added successfully', + }); + setFeatureFlagName(''); + setfeatureFlagVariant(''); + } + }; + const removeFeatureFlags = () => { + if (validateFeatureFlagForm()) { + Instabug.removeFeatureFlag(featureFlagName); + toast.show({ + description: 'Feature Flag removed successfully', + }); + setFeatureFlagName(''); + setfeatureFlagVariant(''); + } + }; + const removeAllFeatureFlags = () => { + Instabug.removeAllFeatureFlags(); + toast.show({ + description: 'Feature Flags removed successfully', + }); + setFeatureFlagName(''); + setfeatureFlagVariant(''); + }; const logout = () => { Instabug.logOut(); @@ -215,6 +259,38 @@ export const SettingsScreen: React.FC = () => { + + + + + setFeatureFlagName(key)} + value={featureFlagName} + errorText={featureFlagFormError.featureFlagName} + /> + + + setfeatureFlagVariant(value)} + value={featureFlagVariant} + /> + + + + + + + + + ); diff --git a/ios/RNInstabug/InstabugReactBridge.h b/ios/RNInstabug/InstabugReactBridge.h index 5da9b9cc7..c2accc995 100644 --- a/ios/RNInstabug/InstabugReactBridge.h +++ b/ios/RNInstabug/InstabugReactBridge.h @@ -133,5 +133,7 @@ - (void)addExperiments:(NSArray *)experiments; - (void)removeExperiments:(NSArray *)experiments; - (void)clearAllExperiments; - +- (void)addFeatureFlags:(NSDictionary *)featureFlagsMap; +- (void)removeFeatureFlags:(NSArray *)featureFlags; +- (void)removeAllFeatureFlags; @end diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index 2f2b24f27..a6f56109c 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -355,6 +355,38 @@ - (dispatch_queue_t)methodQueue { [Instabug clearAllExperiments]; } +RCT_EXPORT_METHOD(addFeatureFlags:(NSDictionary *)featureFlagsMap) { + NSMutableArray *featureFlags = [NSMutableArray array]; + for(id key in featureFlagsMap){ + NSString* variant =[featureFlagsMap objectForKey:key]; + if ([variant length]==0) { + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key]]; + } else{ + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; + } + } + + [Instabug addFeatureFlags:featureFlags]; +} + +RCT_EXPORT_METHOD(removeFeatureFlags:(NSArray *)featureFlags) { + NSMutableArray *features = [NSMutableArray array]; + for(id item in featureFlags){ + [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; + } + + @try { + [Instabug removeFeatureFlags:features]; + } + @catch (NSException *exception) { + NSLog(@"%@", exception); + } +} + +RCT_EXPORT_METHOD(removeAllFeatureFlags) { + [Instabug removeAllFeatureFlags]; +} + RCT_EXPORT_METHOD(willRedirectToStore){ [Instabug willRedirectToAppStore]; } diff --git a/src/models/FeatureFlag.ts b/src/models/FeatureFlag.ts new file mode 100644 index 000000000..d2fd58e0f --- /dev/null +++ b/src/models/FeatureFlag.ts @@ -0,0 +1,12 @@ +export interface FeatureFlag { + /** + * the name of feature flag + */ + name: string; + + /** + * The variant of the feature flag. + * Leave it `undefined` for boolean (kill switch) feature flags. + */ + variant?: string; +} diff --git a/src/modules/Instabug.ts b/src/modules/Instabug.ts index 07b3c079d..baec7660a 100644 --- a/src/modules/Instabug.ts +++ b/src/modules/Instabug.ts @@ -21,6 +21,7 @@ import InstabugUtils, { stringifyIfNotString } from '../utils/InstabugUtils'; import * as NetworkLogger from './NetworkLogger'; import { captureUnhandledRejections } from '../utils/UnhandledRejectionTracking'; import type { ReproConfig } from '../models/ReproConfig'; +import type { FeatureFlag } from '../models/FeatureFlag'; let _currentScreen: string | null = null; let _lastScreen: string | null = null; @@ -539,6 +540,8 @@ export const reportScreenChange = (screenName: string) => { /** * Add experiments to next report. * @param experiments An array of experiments to add to the next report. + * + * @deprecated Please migrate to the new Feature Flags APIs: {@link addFeatureFlags}. */ export const addExperiments = (experiments: string[]) => { NativeInstabug.addExperiments(experiments); @@ -547,6 +550,8 @@ export const addExperiments = (experiments: string[]) => { /** * Remove experiments from next report. * @param experiments An array of experiments to remove from the next report. + * + * @deprecated Please migrate to the new Feature Flags APIs: {@link removeFeatureFlags}. */ export const removeExperiments = (experiments: string[]) => { NativeInstabug.removeExperiments(experiments); @@ -554,11 +559,53 @@ export const removeExperiments = (experiments: string[]) => { /** * Clear all experiments + * + * @deprecated Please migrate to the new Feature Flags APIs: {@link removeAllFeatureFlags}. */ export const clearAllExperiments = () => { NativeInstabug.clearAllExperiments(); }; +/** + * Add feature flags to the next report. + * @param featureFlags An array of feature flags to add to the next report. + */ +export const addFeatureFlags = (featureFlags: FeatureFlag[]) => { + const entries = featureFlags.map((item) => [item.name, item.variant || '']); + const flags = Object.fromEntries(entries); + NativeInstabug.addFeatureFlags(flags); +}; + +/** + * Add a feature flag to the to next report. + */ +export const addFeatureFlag = (featureFlag: FeatureFlag) => { + addFeatureFlags([featureFlag]); +}; + +/** + * Remove feature flags from the next report. + * @param featureFlags An array of feature flags to remove from the next report. + */ +export const removeFeatureFlags = (featureFlags: string[]) => { + NativeInstabug.removeFeatureFlags(featureFlags); +}; + +/** + * Remove a feature flag from the next report. + * @param name the name of the feature flag to remove from the next report. + */ +export const removeFeatureFlag = (name: string) => { + removeFeatureFlags([name]); +}; + +/** + * Clear all feature flags + */ +export const removeAllFeatureFlags = () => { + NativeInstabug.removeAllFeatureFlags(); +}; + /** * This API has to be call when using custom app rating prompt */ diff --git a/src/native/NativeInstabug.ts b/src/native/NativeInstabug.ts index 3990da4c8..3b72f5951 100644 --- a/src/native/NativeInstabug.ts +++ b/src/native/NativeInstabug.ts @@ -118,6 +118,12 @@ export interface InstabugNativeModule extends NativeModule { removeExperiments(experiments: string[]): void; clearAllExperiments(): void; + addFeatureFlags(featureFlags: Record): void; + + removeFeatureFlags(featureFlags: string[]): void; + + removeAllFeatureFlags(): void; + // Files APIs // setFileAttachment(filePath: string, fileName?: string): void; diff --git a/test/mocks/mockInstabug.ts b/test/mocks/mockInstabug.ts index c2dac12b5..5139afcde 100644 --- a/test/mocks/mockInstabug.ts +++ b/test/mocks/mockInstabug.ts @@ -54,6 +54,9 @@ const mockInstabug: InstabugNativeModule = { clearAllExperiments: jest.fn(), networkLogIOS: jest.fn(), networkLogAndroid: jest.fn(), + addFeatureFlags: jest.fn(), + removeFeatureFlags: jest.fn(), + removeAllFeatureFlags: jest.fn(), appendTagToReport: jest.fn(), appendConsoleLogToReport: jest.fn(), setUserAttributeToReport: jest.fn(), diff --git a/test/modules/Instabug.spec.ts b/test/modules/Instabug.spec.ts index f2e52099f..fdbdcae64 100644 --- a/test/modules/Instabug.spec.ts +++ b/test/modules/Instabug.spec.ts @@ -21,6 +21,7 @@ import { WelcomeMessageMode, } from '../../src/utils/Enums'; import InstabugUtils from '../../src/utils/InstabugUtils'; +import type { FeatureFlag } from '../../src/models/FeatureFlag'; describe('Instabug Module', () => { beforeEach(() => { @@ -770,6 +771,57 @@ describe('Instabug Module', () => { expect(NativeInstabug.clearAllExperiments).toBeCalledTimes(1); }); + it('should call native addFeatureFlags method', () => { + const featureFlags: Array = [ + { + name: 'key1', + variant: 'variant1', + }, + { + name: 'key2', + variant: 'variant2', + }, + ]; + const expected: Record = {}; + expected.key1 = 'variant1'; + expected.key2 = 'variant2'; + + Instabug.addFeatureFlags(featureFlags); + expect(NativeInstabug.addFeatureFlags).toBeCalledTimes(1); + expect(NativeInstabug.addFeatureFlags).toBeCalledWith(expected); + }); + + it('should call native addFeatureFlag method', () => { + const featureFlag: FeatureFlag = { + name: 'key1', + variant: 'variant2', + }; + const expected: Record = {}; + expected.key1 = 'variant2'; + + Instabug.addFeatureFlag(featureFlag); + expect(NativeInstabug.addFeatureFlags).toBeCalledTimes(1); + expect(NativeInstabug.addFeatureFlags).toBeCalledWith(expected); + }); + it('should call native removeFeatureFlags method', () => { + const featureFlags = ['exp1', 'exp2']; + Instabug.removeFeatureFlags(featureFlags); + expect(NativeInstabug.removeFeatureFlags).toBeCalledTimes(1); + expect(NativeInstabug.removeFeatureFlags).toBeCalledWith(featureFlags); + }); + + it('should call native removeFeatureFlag method', () => { + const featureFlag = 'exp1'; + Instabug.removeFeatureFlag(featureFlag); + expect(NativeInstabug.removeFeatureFlags).toBeCalledTimes(1); + expect(NativeInstabug.removeFeatureFlags).toBeCalledWith([featureFlag]); + }); + + it('should call native removeAllFeatureFlags method', () => { + Instabug.removeAllFeatureFlags(); + expect(NativeInstabug.removeAllFeatureFlags).toBeCalledTimes(1); + }); + it('should call the native willRedirectToStore method', () => { Instabug.willRedirectToStore(); expect(NativeInstabug.willRedirectToStore).toBeCalledTimes(1);