diff --git a/CHANGELOG.md b/CHANGELOG.md index 785e449f14..891beef274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - Update type definitions for `Bugsnag.notify()` [#1743](https://github.com/bugsnag/bugsnag-js/pull/1743) +### Fixed + +- (react-native) Fixed Kotlin related [version conflict](https://github.com/bugsnag/bugsnag-js/issues/1734) with Android Gradle Plugin [#1750](https://github.com/bugsnag/bugsnag-js/pull/1750) + ## v7.16.6 (2022-05-25) ### Changed diff --git a/packages/react-native/android/build.gradle b/packages/react-native/android/build.gradle index fac8268951..94615fb597 100644 --- a/packages/react-native/android/build.gradle +++ b/packages/react-native/android/build.gradle @@ -1,4 +1,7 @@ buildscript { + ext { + androidToolsVersion = '3.5.3' + } repositories { google() mavenCentral() @@ -7,13 +10,11 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61" + classpath "com.android.tools.build:gradle:${project.ext.androidToolsVersion}" } } apply plugin: 'com.android.library' -apply plugin: "kotlin-android" def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback diff --git a/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.java b/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.java new file mode 100644 index 0000000000..149371bd39 --- /dev/null +++ b/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.java @@ -0,0 +1,262 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +import java.util.Map; + +import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + +public class BugsnagReactNative extends ReactContextBaseJavaModule { + + private static final String UPDATE_CONTEXT = "ContextUpdate"; + private static final String UPDATE_USER = "UserUpdate"; + private static final String UPDATE_METADATA = "MetadataUpdate"; + private static final String ADD_FEATURE_FLAG = "AddFeatureFlag"; + private static final String CLEAR_FEATURE_FLAG = "ClearFeatureFlag"; + private static final String SYNC_KEY = "bugsnag::sync"; + private static final String DATA_KEY = "data"; + + private final ReactApplicationContext reactContext; + + private RCTDeviceEventEmitter bridge; + private BugsnagReactNativePlugin plugin; + private Logger logger; + + public BugsnagReactNative(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + private void logFailure(String msg, Throwable exc) { + logger.e("Failed to call " + msg + " on bugsnag-plugin-react-native, continuing", exc); + } + + @Override + public String getName() { + return "BugsnagReactNative"; + } + + @ReactMethod + public void configureAsync(ReadableMap env, Promise promise) { + promise.resolve(configure(env)); + } + + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap configure(ReadableMap env) { + Client client; + try { + client = Bugsnag.getClient(); + } catch (IllegalStateException ise) { + throw new IllegalStateException("Failed to initialise the native Bugsnag Android client, please check you have " + + "added Bugsnag.start() in the onCreate() method of your Application subclass"); + } + + try { + bridge = reactContext.getJSModule(RCTDeviceEventEmitter.class); + logger = client.getLogger(); + plugin = (BugsnagReactNativePlugin) client.getPlugin(BugsnagReactNativePlugin.class); + plugin.registerForMessageEvents(new Function1() { + @Override + public Unit invoke(MessageEvent messageEvent) { + emitEvent(messageEvent); + return Unit.INSTANCE; + } + }); + + return ReactNativeCompat.toWritableMap(plugin.configure(env.toHashMap())); + } catch (Throwable error) { + logFailure("configure", error); + return new WritableNativeMap(); + } + } + + @SuppressWarnings("unchecked") + void emitEvent(MessageEvent event) { + logger.d("Received MessageEvent: " + event.getType()); + + WritableMap map = Arguments.createMap(); + map.putString("type", event.getType()); + + switch (event.getType()) { + case UPDATE_CONTEXT: + map.putString(DATA_KEY, (String) event.getData()); + break; + case UPDATE_USER: + case UPDATE_METADATA: + case ADD_FEATURE_FLAG: + case CLEAR_FEATURE_FLAG: + map.putMap(DATA_KEY, event.getData() != null + ? Arguments.makeNativeMap((Map) event.getData()) + : null); + break; + default: + logger.w("Received unknown message event " + event.getType() + ", ignoring"); + } + + bridge.emit(SYNC_KEY, map); + } + + @ReactMethod + void updateCodeBundleId(@Nullable String id) { + try { + plugin.updateCodeBundleId(id); + } catch (Throwable exc) { + logFailure("updateCodeBundleId", exc); + } + } + + @ReactMethod + void leaveBreadcrumb(@NonNull ReadableMap map) { + try { + plugin.leaveBreadcrumb(map.toHashMap()); + } catch (Throwable exc) { + logFailure("leaveBreadcrumb", exc); + } + } + + @ReactMethod + void startSession() { + try { + plugin.startSession(); + } catch (Throwable exc) { + logFailure("startSession", exc); + } + } + + @ReactMethod + void pauseSession() { + try { + plugin.pauseSession(); + } catch (Throwable exc) { + logFailure("pauseSession", exc); + } + } + + @ReactMethod + void resumeSession() { + try { + plugin.resumeSession(); + } catch (Throwable exc) { + logFailure("resumeSession", exc); + } + } + + @ReactMethod + void updateContext(@Nullable String context) { + try { + plugin.updateContext(context); + } catch (Throwable exc) { + logFailure("updateContext", exc); + } + } + + @ReactMethod + void addMetadata(@NonNull String section, @Nullable ReadableMap data) { + try { + plugin.addMetadata(section, data != null ? data.toHashMap() : null); + } catch (Throwable exc) { + logFailure("addMetadata", exc); + } + } + + @ReactMethod + void clearMetadata(@NonNull String section, @Nullable String key) { + try { + plugin.clearMetadata(section, key); + } catch (Throwable exc) { + logFailure("clearMetadata", exc); + } + } + + @ReactMethod + void updateUser(@Nullable String id, @Nullable String email, @Nullable String name) { + try { + plugin.updateUser(id, email, name); + } catch (Throwable exc) { + logFailure("updateUser", exc); + } + } + + @ReactMethod + void dispatch(@NonNull ReadableMap payload, @NonNull Promise promise) { + try { + plugin.dispatch(payload.toHashMap()); + promise.resolve(true); + } catch (Throwable exc) { + logFailure("dispatch", exc); + promise.resolve(false); + } + } + + @ReactMethod + void getPayloadInfo(@NonNull ReadableMap payload, @NonNull Promise promise) { + try { + boolean unhandled = payload.getBoolean("unhandled"); + Map info = plugin.getPayloadInfo(unhandled); + promise.resolve(ReactNativeCompat.toWritableMap(info)); + } catch (Throwable exc) { + logFailure("dispatch", exc); + promise.resolve(null); + } + } + + @ReactMethod + void addFeatureFlag(@NonNull String name, @Nullable String variant) { + try { + plugin.addFeatureFlag(name, variant); + } catch (Throwable exc) { + logFailure("addFeatureFlag", exc); + } + } + + @ReactMethod + void addFeatureFlags(@NonNull ReadableArray flags) { + try { + final int flagCount = flags.size(); + for (int index = 0; index < flagCount; index++) { + ReadableMap flag = flags.getMap(index); + String name = safeGetString(flag, "name"); + + if (name != null) { + plugin.addFeatureFlag(name, safeGetString(flag, "variant")); + } + } + } catch (Throwable exc) { + logFailure("addFeatureFlags", exc); + } + } + + @ReactMethod + void clearFeatureFlag(@NonNull String name) { + try { + plugin.clearFeatureFlag(name); + } catch (Throwable exc) { + logFailure("clearFeatureFlag", exc); + } + } + + @ReactMethod + void clearFeatureFlags() { + try { + plugin.clearFeatureFlags(); + } catch (Throwable exc) { + logFailure("clearFeatureFlags", exc); + } + } + + private String safeGetString(@NonNull ReadableMap map, @NonNull String key) { + return map.hasKey(key) ? map.getString(key) : null; + } +} diff --git a/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.kt b/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.kt deleted file mode 100644 index c13d227aad..0000000000 --- a/packages/react-native/android/src/main/java/com/bugsnag/android/BugsnagReactNative.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.bugsnag.android - -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter - -class BugsnagReactNative(private val reactContext: ReactApplicationContext) : - ReactContextBaseJavaModule(reactContext) { - - internal companion object { - private const val UPDATE_CONTEXT = "ContextUpdate" - private const val UPDATE_USER = "UserUpdate" - private const val UPDATE_METADATA = "MetadataUpdate" - private const val ADD_FEATURE_FLAG = "AddFeatureFlag" - private const val CLEAR_FEATURE_FLAG = "ClearFeatureFlag" - private const val SYNC_KEY = "bugsnag::sync" - private const val DATA_KEY = "data" - } - - lateinit var bridge: RCTDeviceEventEmitter - lateinit var plugin: BugsnagReactNativePlugin - lateinit var logger: Logger - - override fun getName(): String = "BugsnagReactNative" - - fun logFailure(msg: String, exc: Throwable) { - logger.e("Failed to call $msg on bugsnag-plugin-react-native, continuing", exc) - } - - /* - * This method exists for when the app is run in a remote debugger, - * where synchronous methods are not allowed. It should not ordinarily - * be used. - */ - @ReactMethod - fun configureAsync(env: ReadableMap, promise: Promise) { - promise.resolve(configure(env)) - } - - @ReactMethod(isBlockingSynchronousMethod = true) - fun configure(env: ReadableMap): WritableMap { - val client = try { - Bugsnag.getClient() - } catch (exc: IllegalStateException) { - throw IllegalStateException("Failed to initialise the native Bugsnag Android client, please check you have " + - "added Bugsnag.start() in the onCreate() method of your Application subclass") - } - return try { - bridge = reactContext.getJSModule(RCTDeviceEventEmitter::class.java) - logger = client.logger - plugin = client.getPlugin(BugsnagReactNativePlugin::class.java) as BugsnagReactNativePlugin - plugin.registerForMessageEvents { emitEvent(it) } - plugin.configure(env.toHashMap()).toWritableMap() - } catch (exc: Throwable) { - logFailure("configure", exc) - WritableNativeMap() - } - } - - /** - * Serializes a MessageEvent into a WritableMap and sends it across the React Bridge - */ - @Suppress("UNCHECKED_CAST") - fun emitEvent(event: MessageEvent) { - logger.d("Received MessageEvent: ${event.type}") - - val map = Arguments.createMap() - map.putString("type", event.type) - - when (event.type) { - UPDATE_CONTEXT -> map.putString(DATA_KEY, event.data as String?) - UPDATE_USER -> map.putMap(DATA_KEY, Arguments.makeNativeMap(event.data as Map?)) - UPDATE_METADATA -> map.putMap(DATA_KEY, Arguments.makeNativeMap(event.data as Map?)) - ADD_FEATURE_FLAG -> map.putMap(DATA_KEY, Arguments.makeNativeMap(event.data as Map?)) - CLEAR_FEATURE_FLAG -> map.putMap(DATA_KEY, Arguments.makeNativeMap(event.data as Map?)) - else -> logger.w("Received unknown message event ${event.type}, ignoring") - } - bridge.emit(SYNC_KEY, map) - } - - @ReactMethod - fun updateCodeBundleId(id: String?) { - try { - plugin.updateCodeBundleId(id) - } catch (exc: Throwable) { - logFailure("updateCodeBundleId", exc) - } - } - - @ReactMethod - fun leaveBreadcrumb(map: ReadableMap) { - try { - plugin.leaveBreadcrumb(map.toHashMap()) - } catch (exc: Throwable) { - logFailure("leaveBreadcrumb", exc) - } - } - - @ReactMethod - fun startSession() { - try { - plugin.startSession() - } catch (exc: Throwable) { - logFailure("startSession", exc) - } - } - - @ReactMethod - fun pauseSession() { - try { - plugin.pauseSession() - } catch (exc: Throwable) { - logFailure("pauseSession", exc) - } - } - - @ReactMethod - fun resumeSession() { - try { - plugin.resumeSession() - } catch (exc: Throwable) { - logFailure("resumeSession", exc) - } - } - - @ReactMethod - fun updateContext(context: String?) { - try { - plugin.updateContext(context) - } catch (exc: Throwable) { - logFailure("updateContext", exc) - } - } - - @ReactMethod - fun addMetadata(section: String, data: ReadableMap?) { - try { - plugin.addMetadata(section, data?.toHashMap() as Map?) - } catch (exc: Throwable) { - logFailure("addMetadata", exc) - } - } - - @ReactMethod - fun clearMetadata(section: String, key: String?) { - try { - plugin.clearMetadata(section, key) - } catch (exc: Throwable) { - logFailure("clearMetadata", exc) - } - } - - @ReactMethod - fun updateUser(id: String?, email: String?, name: String?) { - try { - plugin.updateUser(id, email, name) - } catch (exc: Throwable) { - logFailure("updateUser", exc) - } - } - - @ReactMethod - fun dispatch(payload: ReadableMap, promise: Promise) { - try { - plugin.dispatch(payload.toHashMap()) - promise.resolve(true) - } catch (exc: Throwable) { - logFailure("dispatch", exc) - promise.resolve(false) - } - } - - @ReactMethod - fun getPayloadInfo(payload: ReadableMap, promise: Promise) { - try { - val unhandled = payload.getBoolean("unhandled") - val info = plugin.getPayloadInfo(unhandled) - promise.resolve(Arguments.makeNativeMap(info)) - } catch (exc: Throwable) { - logFailure("getPayloadInfo", exc) - } - } - - @ReactMethod - fun addFeatureFlag(name: String, variant: String?) { - try { - plugin.addFeatureFlag(name, variant) - } catch (exc: Throwable) { - logFailure("addFeatureFlag", exc) - } - } - - @ReactMethod - fun addFeatureFlags(flags: ReadableArray) { - try { - for (index in 0 until flags.size()) { - val flag = flags.getMap(index) ?: continue - val name = flag.safeString("name") ?: continue - - plugin.addFeatureFlag(name, flag.safeString("variant")) - } - } catch (exc: Throwable) { - logFailure("addFeatureFlags", exc) - } - } - - @ReactMethod - fun clearFeatureFlag(name: String) { - try { - plugin.clearFeatureFlag(name) - } catch (exc: Throwable) { - logFailure("clearFeatureFlag", exc) - } - } - - @ReactMethod - fun clearFeatureFlags() { - try { - plugin.clearFeatureFlags() - } catch (exc: Throwable) { - logFailure("clearFeatureFlags", exc) - } - } - - private fun ReadableMap.safeString(key: String): String? { - return if (hasKey(key)) getString(key) else null - } -} diff --git a/packages/react-native/android/src/main/java/com/bugsnag/android/MapExtensions.kt b/packages/react-native/android/src/main/java/com/bugsnag/android/MapExtensions.kt deleted file mode 100644 index 4c0ea4b26e..0000000000 --- a/packages/react-native/android/src/main/java/com/bugsnag/android/MapExtensions.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.bugsnag.android - -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.WritableNativeMap - -@Suppress("UNCHECKED_CAST") -internal fun Map.toWritableMap(): WritableMap { - val nativeMap = WritableNativeMap() - - entries.forEach { - val key = it.key - when (val obj = it.value) { - null -> nativeMap.putNull(key) - is Boolean -> nativeMap.putBoolean(key, obj) - is Int -> nativeMap.putInt(key, obj) - is Long -> nativeMap.putDouble(key, obj.toDouble()) - is Number -> nativeMap.putDouble(key, obj.toDouble()) - is String -> nativeMap.putString(key, obj) - is Map<*, *> -> nativeMap.putMap(key, Arguments.makeNativeMap(obj as MutableMap)) - is Collection<*> -> nativeMap.putArray(key, Arguments.makeNativeArray(obj.toTypedArray())) - else -> throw IllegalArgumentException("Could not convert $obj to native map") - } - } - return nativeMap -} diff --git a/packages/react-native/android/src/main/java/com/bugsnag/android/ReactNativeCompat.java b/packages/react-native/android/src/main/java/com/bugsnag/android/ReactNativeCompat.java new file mode 100644 index 0000000000..9b6f080871 --- /dev/null +++ b/packages/react-native/android/src/main/java/com/bugsnag/android/ReactNativeCompat.java @@ -0,0 +1,79 @@ +package com.bugsnag.android; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class ReactNativeCompat { + private ReactNativeCompat() { + } + + @SuppressWarnings("unchecked") + static WritableMap toWritableMap(Map javaMap) { + WritableMap writableMap = Arguments.createMap(); + + if (javaMap == null) { + return writableMap; + } + + for (Map.Entry entry : javaMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + writableMap.putString(key, (String) value); + } else if (value instanceof Integer) { + writableMap.putInt(key, (Integer) value); + } else if (value instanceof Number) { + writableMap.putDouble(key, ((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + writableMap.putBoolean(key, (Boolean) value); + } else if (value instanceof Map) { + writableMap.putMap(key, toWritableMap((Map) value)); + } else if (value instanceof Collection) { + writableMap.putArray(key, toWritableArray((Collection) value)); + } else if (value == null) { + writableMap.putNull(key); + } + } + + return writableMap; + } + + static WritableArray toWritableArray(Collection collection) { + WritableArray writableArray = Arguments.createArray(); + + if (collection == null) { + return writableArray; + } + + for (Object value : collection) { + if (value instanceof String) { + writableArray.pushString((String) value); + } else if (value instanceof Integer) { + writableArray.pushInt((Integer) value); + } else if (value instanceof Number) { + writableArray.pushDouble(((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + writableArray.pushBoolean((Boolean) value); + } else if (value instanceof Map) { + writableArray.pushMap(toWritableMap((Map) value)); + } else if (value instanceof Collection) { + writableArray.pushArray(toWritableArray((Collection) value)); + } else if (value == null) { + writableArray.pushNull(); + } + } + + return writableArray; + } +} diff --git a/packages/react-native/android/src/test/java/com/bugsnag/android/BugsnagReactNativeTest.kt b/packages/react-native/android/src/test/java/com/bugsnag/android/BugsnagReactNativeTest.kt deleted file mode 100644 index b564671c5f..0000000000 --- a/packages/react-native/android/src/test/java/com/bugsnag/android/BugsnagReactNativeTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.bugsnag.android - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.ReadableArray -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.any -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnitRunner - -@RunWith(MockitoJUnitRunner::class) -class BugsnagReactNativeTest { - - @Mock - lateinit var ctx: ReactApplicationContext - - @Mock - lateinit var plugin: BugsnagReactNativePlugin - - @Mock - lateinit var map: ReadableMap - - @Mock - lateinit var array: ReadableArray - - @Mock - lateinit var promise: Promise - - private lateinit var brn: BugsnagReactNative - - @Before - fun setUp() { - brn = BugsnagReactNative(ctx) - brn.plugin = plugin - brn.logger = object: Logger {} - `when`(map.toHashMap()).thenReturn(HashMap()) - } - - @Test - fun getName() { - assertEquals("BugsnagReactNative", brn.name) - } - - @Test - fun leaveBreadcrumb() { - brn.leaveBreadcrumb(map) - verify(plugin, times(1)).leaveBreadcrumb(any()) - } - - @Test - fun startSession() { - brn.startSession() - verify(plugin, times(1)).startSession() - } - - @Test - fun pauseSession() { - brn.pauseSession() - verify(plugin, times(1)).pauseSession() - } - - @Test - fun resumeSession() { - brn.resumeSession() - verify(plugin, times(1)).resumeSession() - } - - @Test - fun addFeatureFlag() { - brn.addFeatureFlag("feature flag", "abc123") - verify(plugin, times(1)).addFeatureFlag("feature flag", "abc123") - } - - @Test - fun addFeatureFlags() { - `when`(array.size()).thenReturn(1) - `when`(array.getMap(eq(0))).thenReturn(map) - `when`(map.getString(eq("name"))).thenReturn("feature flag") - `when`(map.getString(eq("variant"))).thenReturn("abc123") - brn.addFeatureFlags(array) - verify(plugin, times(1)).addFeatureFlag("feature flag", "abc123") - } - - @Test - fun clearFeatureFlag() { - brn.clearFeatureFlag("feature flag") - verify(plugin, times(1)).clearFeatureFlag("feature flag") - } - - @Test - fun clearFeatureFlags() { - brn.clearFeatureFlags() - verify(plugin, times(1)).clearFeatureFlags() - } - - @Test - fun updateContext() { - brn.updateContext("Foo") - verify(plugin, times(1)).updateContext("Foo") - } - - @Test - fun updateUser() { - brn.updateUser("123", "joe@example.com", "Joe") - verify(plugin, times(1)).updateUser("123", "joe@example.com", "Joe") - } - - @Test - fun dispatch() { - brn.dispatch(map, promise) - verify(plugin, times(1)).dispatch(any()) - } - - @Test - fun getPayloadInfo() { - brn.getPayloadInfo(map, promise) - verify(plugin, times(1)).getPayloadInfo(false) - } -}