diff --git a/CHANGELOG.md b/CHANGELOG.md index 9461434007..1e4ff78f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 5.26.0 (2022-08-18) + +### Enhancements + +* Introduced `bugsnag_refresh_symbol_table` and `BugsnagNDK.refreshSymbolTable` to allow NDK apps to force a refresh of cached + debug information used during a native crash. This new API is only applicable if you are using `dlopen` or `System.loadLibrary` + after startup, and experiencing native crashes with missing symbols. + [#1731](https://github.com/bugsnag/bugsnag-android/pull/1731) + +### Bug fixes + +* Non-List Collections are now correctly handled as OPAQUE values for NDK metadata + [#1728](https://github.com/bugsnag/bugsnag-android/pull/1728) + ## 5.25.0 (2022-07-19) ### Enhancements diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventStoreConfinementTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventStoreConfinementTest.kt index 029199b6cb..01cafa6ea8 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventStoreConfinementTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventStoreConfinementTest.kt @@ -2,6 +2,7 @@ package com.bugsnag.android import androidx.test.core.app.ApplicationProvider import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.io.File @@ -74,9 +75,13 @@ internal class EventStoreConfinementTest { assertEquals(EVENT_CONFINEMENT_ATTEMPTS, filenames.size) assertEquals(EVENT_CONFINEMENT_ATTEMPTS, filenames.toSet().size) + val remainingExpectedApiKeys = filenames.indices.mapTo(hashSetOf()) { "$it" } retainingDelivery.files.forEachIndexed { index, file -> val eventInfo = EventFilenameInfo.fromFile(file, client.immutableConfig) - assertEquals("$index", eventInfo.apiKey) + assertTrue( + "unexpected file: $file ($index), expected one of $remainingExpectedApiKeys", + remainingExpectedApiKeys.remove(eventInfo.apiKey) + ) } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java index 2416a1fa8b..4bcb0a2358 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java @@ -3,14 +3,18 @@ import com.bugsnag.android.internal.ImmutableConfig; import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -38,6 +42,26 @@ private static Client getClient() { } } + /** + * Create an empty Event for a "handled exception" report. The returned Event will have + * no Error objects, metadata, breadcrumbs, or feature flags. It's indented that the caller + * will populate the Error and then pass the Event object to + * {@link Client#populateAndNotifyAndroidEvent(Event, OnErrorCallback)}. + */ + private static Event createEmptyEvent() { + Client client = getClient(); + + return new Event( + new EventInternal( + (Throwable) null, + client.getConfig(), + SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), + client.getMetadataState().getMetadata().copy() + ), + client.getLogger() + ); + } + /** * Caches a client instance for responding to future events */ @@ -65,7 +89,7 @@ public static String getNativeReportPath() { */ @NonNull @SuppressWarnings("unused") - public static Map getUser() { + public static Map getUser() { HashMap userData = new HashMap<>(); User user = getClient().getUser(); userData.put("id", user.getId()); @@ -79,8 +103,8 @@ public static Map getUser() { */ @NonNull @SuppressWarnings("unused") - public static Map getApp() { - HashMap data = new HashMap<>(); + public static Map getApp() { + HashMap data = new HashMap<>(); AppDataCollector source = getClient().getAppDataCollector(); AppWithState app = source.generateAppWithState(); data.put("version", app.getVersion()); @@ -103,7 +127,7 @@ public static Map getApp() { */ @NonNull @SuppressWarnings("unused") - public static Map getDevice() { + public static Map getDevice() { DeviceDataCollector source = getClient().getDeviceDataCollector(); HashMap deviceData = new HashMap<>(source.getDeviceMetadata()); @@ -152,9 +176,9 @@ public static List getBreadcrumbs() { /** * Sets the user * - * @param id id + * @param id id * @param email email - * @param name name + * @param name name */ @SuppressWarnings("unused") public static void setUser(@Nullable final String id, @@ -167,9 +191,9 @@ public static void setUser(@Nullable final String id, /** * Sets the user * - * @param idBytes id + * @param idBytes id * @param emailBytes email - * @param nameBytes name + * @param nameBytes name */ @SuppressWarnings("unused") public static void setUser(@Nullable final byte[] idBytes, @@ -319,9 +343,9 @@ public static boolean isDiscardErrorClass(@NonNull String name) { * captured. Used to determine whether the report * should be discarded, based on configured release * stages - * @param payloadBytes The raw JSON payload of the event - * @param apiKey The apiKey for the event - * @param isLaunching whether the crash occurred when the app was launching + * @param payloadBytes The raw JSON payload of the event + * @param apiKey The apiKey for the event + * @param isLaunching whether the crash occurred when the app was launching */ @SuppressWarnings("unused") public static void deliverReport(@Nullable byte[] releaseStageBytes, @@ -353,10 +377,10 @@ public static void deliverReport(@Nullable byte[] releaseStageBytes, /** * Notifies using the Android SDK * - * @param nameBytes the error name + * @param nameBytes the error name * @param messageBytes the error message - * @param severity the error severity - * @param stacktrace a stacktrace + * @param severity the error severity + * @param stacktrace a stacktrace */ public static void notify(@NonNull final byte[] nameBytes, @NonNull final byte[] messageBytes, @@ -373,9 +397,9 @@ public static void notify(@NonNull final byte[] nameBytes, /** * Notifies using the Android SDK * - * @param name the error name - * @param message the error message - * @param severity the error severity + * @param name the error name + * @param message the error message + * @param severity the error severity * @param stacktrace a stacktrace */ public static void notify(@NonNull final String name, @@ -409,11 +433,65 @@ public boolean onError(@NonNull Event event) { }); } + /** + * Notifies using the Android SDK + * + * @param nameBytes the error name + * @param messageBytes the error message + * @param severity the error severity + * @param stacktrace a stacktrace + */ + public static void notify(@NonNull final byte[] nameBytes, + @NonNull final byte[] messageBytes, + @NonNull final Severity severity, + @NonNull final NativeStackframe[] stacktrace) { + + if (nameBytes == null || messageBytes == null || stacktrace == null) { + return; + } + String name = new String(nameBytes, UTF8Charset); + String message = new String(messageBytes, UTF8Charset); + notify(name, message, severity, stacktrace); + } + + /** + * Notifies using the Android SDK + * + * @param name the error name + * @param message the error message + * @param severity the error severity + * @param stacktrace a stacktrace + */ + public static void notify(@NonNull final String name, + @NonNull final String message, + @NonNull final Severity severity, + @NonNull final NativeStackframe[] stacktrace) { + Client client = getClient(); + + if (client.getConfig().shouldDiscardError(name)) { + return; + } + + Event event = createEmptyEvent(); + event.updateSeverityInternal(severity); + + List stackframes = new ArrayList<>(stacktrace.length); + for (NativeStackframe nativeStackframe : stacktrace) { + stackframes.add(new Stackframe(nativeStackframe)); + } + event.getErrors().add(new Error( + new ErrorInternal(name, message, new Stacktrace(stackframes), ErrorType.C), + client.getLogger() + )); + + getClient().populateAndNotifyAndroidEvent(event, null); + } + /** * Create an {@code Event} object * - * @param exc the Throwable object that caused the event - * @param client the Client object that the event is associated with + * @param exc the Throwable object that caused the event + * @param client the Client object that the event is associated with * @param severityReason the severity of the Event * @return a new {@code Event} object */ diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index c3da369f8f..01d9758392 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.25.0", + var version: String = "5.26.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt index acef665148..c50742ed0d 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt @@ -6,13 +6,16 @@ import com.bugsnag.android.BugsnagTestUtils.generateDeviceWithState import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -48,22 +51,30 @@ internal class NativeInterfaceApiTest { @Mock lateinit var eventStore: EventStore + @Captor + lateinit var eventCapture: ArgumentCaptor + @Before fun setUp() { NativeInterface.setClient(client) `when`(client.config).thenReturn(immutableConfig) `when`(client.getSessionTracker()).thenReturn(sessionTracker) `when`(client.getEventStore()).thenReturn(eventStore) + `when`(immutableConfig.apiKey).thenReturn("test-api-key") `when`(immutableConfig.endpoints).thenReturn( EndpointConfiguration( "http://notify.bugsnag.com", "http://sessions.bugsnag.com" ) ) + `when`(immutableConfig.redactedKeys).thenReturn(emptySet()) + `when`(immutableConfig.sendThreads).thenReturn(ThreadSendPolicy.NEVER) + `when`(immutableConfig.logger).thenReturn(NoopLogger) `when`(client.getAppDataCollector()).thenReturn(appDataCollector) `when`(client.getDeviceDataCollector()).thenReturn(deviceDataCollector) `when`(client.getUser()).thenReturn(User("123", "tod@example.com", "Tod")) + `when`(client.getMetadataState()).thenReturn(MetadataState()) } @Test @@ -89,14 +100,24 @@ internal class NativeInterfaceApiTest { @Test fun getAppData() { `when`(appDataCollector.generateAppWithState()).thenReturn(generateAppWithState()) - `when`(appDataCollector.getAppDataMetadata()).thenReturn(mutableMapOf(Pair("metadata", true))) - val expected = mapOf(Pair("metadata", true), Pair("type", "android"), Pair("versionCode", 0)) + `when`(appDataCollector.getAppDataMetadata()).thenReturn( + mutableMapOf( + Pair( + "metadata", + true + ) + ) + ) + val expected = + mapOf(Pair("metadata", true), Pair("type", "android"), Pair("versionCode", 0)) assertEquals(expected, NativeInterface.getApp().filter { it.value != null }) } @Test fun getDeviceData() { - `when`(deviceDataCollector.generateDeviceWithState(anyLong())).thenReturn(generateDeviceWithState()) + `when`(deviceDataCollector.generateDeviceWithState(anyLong())).thenReturn( + generateDeviceWithState() + ) `when`(deviceDataCollector.getDeviceMetadata()).thenReturn(mapOf(Pair("metadata", true))) assertTrue(NativeInterface.getDevice()["metadata"] as Boolean) } @@ -220,11 +241,50 @@ internal class NativeInterfaceApiTest { } @Test - fun notifyCall() { - NativeInterface.notify("SIGPIPE", "SIGSEGV 11", Severity.ERROR, arrayOf()) + fun notifyJVMStackTraceCall() { + NativeInterface.notify( + "SIGPIPE", + "SIGSEGV 11", + Severity.ERROR, + arrayOf() + ) verify(client, times(1)).notify(any(), any()) } + @Test + fun notifyNativeTraceCall() { + val nativeStackframe = NativeStackframe( + "someMethod", + "libtest.so", + null, + 98765, + null, + null, + false, + ErrorType.C, + "no identifying characteristics" + ) + + NativeInterface.notify("SIGPIPE", "SIGSEGV 11", Severity.ERROR, arrayOf(nativeStackframe)) + verify(client, times(1)).populateAndNotifyAndroidEvent(eventCapture.capture(), any()) + + val notifiedEvent = eventCapture.value + assertNotNull(notifiedEvent) + assertEquals(1, notifiedEvent.errors.size) + + val error = notifiedEvent.errors.single() + assertEquals("SIGPIPE", error.errorClass) + assertEquals("SIGSEGV 11", error.errorMessage) + + assertEquals(1, error.stacktrace.size) + val stackframe = error.stacktrace.single() + assertEquals(nativeStackframe.method, stackframe.method) + assertEquals(nativeStackframe.file, stackframe.file) + assertEquals(nativeStackframe.frameAddress, stackframe.frameAddress) + assertEquals(nativeStackframe.type, stackframe.type) + assertEquals(nativeStackframe.codeIdentifier, stackframe.codeIdentifier) + } + @Test fun autoDetectAnrs() { NativeInterface.setAutoDetectAnrs(true) diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 1172040cb4..d52669a14f 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -3,6 +3,7 @@ ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) + LongMethod:EventMigrationV10Tests.kt$EventMigrationV10Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest() diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV10Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV10Tests.kt new file mode 100644 index 0000000000..4a9a83e76c --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV10Tests.kt @@ -0,0 +1,224 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Test + +/** Migration v10 added codeIdentifier to stack frames */ +class EventMigrationV10Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 81395165021L, + "durationInForeground" to 81395165010L, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to true, + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 8139512718L + ), + output["app"] + ) + + // breadcrumbs + val crumbs = output["breadcrumbs"] + if (crumbs is List) { + assertEquals(50, crumbs.size) + crumbs.forEachIndexed { index, crumb -> + assertEquals( + mapOf( + "type" to "state", + "name" to "mission $index", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Now we know what they mean by 'advanced' tactical training." + ) + ), + crumb + ) + } + } else { + fail("breadcrumbs is not a list of crumb objects?!") + } + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "🍨", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3839512576L + ), + output["device"] + ) + + // feature flags + assertEquals( + listOf( + mapOf( + "featureFlag" to "bluebutton", + "variant" to "on" + ), + mapOf( + "featureFlag" to "redbutton", + "variant" to "off" + ), + mapOf("featureFlag" to "nobutton"), + mapOf( + "featureFlag" to "switch", + "variant" to "left" + ) + ), + output["featureFlags"] + ) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to "0xfffffffe", + "lineNumber" to 4194967233L, + "loadAddress" to "0x242023", + "symbolAddress" to "0x308", + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to "0xb37a644b", + "lineNumber" to 0L, + "loadAddress" to "0x0", + "symbolAddress" to "0x0", + "method" to "0xb37a644b" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.5.toBigDecimal() + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5L, + "unhandled" to 2L + ) + ), + output["session"] + ) + + // threads + val threads = output["threads"] + if (threads is List) { + assertEquals(8, threads.size) + threads.forEachIndexed { index, thread -> + assertEquals( + mapOf( + "name" to "Thread #$index", + "state" to "paused-$index", + "id" to 1000L + index, + "type" to "c" + ), + thread + ) + } + } else { + fail("threads is not a list of thread objects?!") + } + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV9Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV9Tests.kt index 1c41667c2a..daae30063a 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV9Tests.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV9Tests.kt @@ -5,7 +5,7 @@ import org.junit.Assert.assertNotEquals import org.junit.Assert.fail import org.junit.Test -/** Migration v9 added feature flags ⛳️ */ +/** Migration v9 added opaque metadata */ class EventMigrationV9Tests : EventMigrationTest() { @Test diff --git a/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h b/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h index 754ad091b8..d025f64a72 100644 --- a/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h +++ b/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h @@ -63,6 +63,14 @@ void bugsnag_add_on_error(bsg_on_error on_error); */ void bugsnag_remove_on_error(); +/** + * Refresh cached symbol tables within Bugsnag. This can be used to force a + * refresh of the cached symbols used during stack unwinding. This is only + * useful if you are using `System.loadLibrary` or `dlopen` after your + * application startup is complete. + */ +void bugsnag_refresh_symbol_table(); + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h index 4f7d8a61cc..44cdd9eb73 100644 --- a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h +++ b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h @@ -72,6 +72,7 @@ typedef struct { char filename[256]; char method[256]; + char code_identifier[65]; } bugsnag_stackframe; #ifdef __cplusplus diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt index 75492299d8..9412b52cd4 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt @@ -18,9 +18,11 @@ internal class NdkPlugin : Plugin { private external fun getBinaryArch(): String - private var nativeBridge: NativeBridge? = null private var client: Client? = null + var nativeBridge: NativeBridge? = null + private set + private fun initNativeBridge(client: Client): NativeBridge { val nativeBridge = NativeBridge() client.addObserver(nativeBridge) @@ -72,3 +74,6 @@ internal class NdkPlugin : Plugin { return 0 } } + +internal val Client.ndkPlugin: NdkPlugin? + get() = getPlugin(NdkPlugin::class.java) as NdkPlugin? diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/BugsnagNDK.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/BugsnagNDK.kt new file mode 100644 index 0000000000..b63c01b615 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/BugsnagNDK.kt @@ -0,0 +1,14 @@ +package com.bugsnag.android.ndk + +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.ndkPlugin + +object BugsnagNDK { + @JvmStatic + fun refreshSymbolTable() { + if (Bugsnag.isStarted()) { + val client = Bugsnag.getClient() + client.ndkPlugin?.nativeBridge?.refreshSymbolTable() + } + } +} diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index cfff82b863..6ae19cb1de 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt @@ -82,6 +82,7 @@ class NativeBridge : StateObserver { external fun addFeatureFlag(name: String, variant: String?) external fun clearFeatureFlag(name: String) external fun clearFeatureFlags() + external fun refreshSymbolTable() override fun onStateChange(event: StateEvent) { if (isInvalidMessage(event)) return diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/OpaqueValue.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/OpaqueValue.kt index 88cd06d1c3..55b1b180cb 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/OpaqueValue.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/OpaqueValue.kt @@ -41,7 +41,7 @@ internal class OpaqueValue(val json: String) { value is Boolean -> value value is Number -> value value is String && isStringNDKSupported(value) -> value - value is String || value is Map<*, *> || value is List<*> -> OpaqueValue(encode(value)) + value is String || value is Map<*, *> || value is Collection<*> -> OpaqueValue(encode(value)) else -> null } } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c index 8046e7ee97..1d32bc95b9 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c @@ -104,11 +104,31 @@ static void populate_notify_stacktrace(JNIEnv *env, goto exit; } - // create StackTraceElement object - jobject jframe = - bsg_safe_new_object(env, bsg_jni_cache->StackTraceElement, - bsg_jni_cache->StackTraceElement_constructor, class, - method, filename, frame.line_number); + jobject line_number = bsg_safe_call_static_object_method( + env, bsg_jni_cache->Long, bsg_jni_cache->Long_valueOf, + frame.line_number); + jobject frame_address = bsg_safe_call_static_object_method( + env, bsg_jni_cache->Long, bsg_jni_cache->Long_valueOf, + frame.frame_address); + jobject symbol_address = bsg_safe_call_static_object_method( + env, bsg_jni_cache->Long, bsg_jni_cache->Long_valueOf, + frame.symbol_address); + jobject load_address = bsg_safe_call_static_object_method( + env, bsg_jni_cache->Long, bsg_jni_cache->Long_valueOf, + frame.load_address); + + jstring code_identifier = + bsg_safe_new_string_utf(env, frame.code_identifier); + if (code_identifier == NULL) { + goto exit; + } + + // create NativeStackframe object + jobject jframe = bsg_safe_new_object( + env, bsg_jni_cache->NativeStackframe, + bsg_jni_cache->NativeStackframe_constructor, method, filename, + line_number, frame_address, symbol_address, load_address, NULL, + bsg_jni_cache->ErrorType_C, code_identifier); if (jframe == NULL) { goto exit; } @@ -140,7 +160,7 @@ void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, // create StackTraceElement array jtrace = bsg_safe_new_object_array(env, frame_count, - bsg_jni_cache->StackTraceElement); + bsg_jni_cache->NativeStackframe); if (jtrace == NULL) { goto exit; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c index f9c68437c3..edcb467034 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -51,6 +51,8 @@ void bugsnag_remove_on_error() { } } +void bugsnag_refresh_symbol_table() { bsg_unwinder_refresh(); } + bool bsg_run_on_error() { bsg_on_error on_error = bsg_global_env->on_error; if (on_error != NULL) { @@ -495,6 +497,10 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateIsLaunching( bugsnag_app_set_is_launching(&bsg_global_env->next_event, new_value); bsg_update_next_run_info(bsg_global_env); release_env_write_lock(); + + if (!new_value) { + bugsnag_refresh_symbol_table(); + } } JNIEXPORT void JNICALL @@ -806,6 +812,12 @@ Java_com_bugsnag_android_ndk_NativeBridge_clearFeatureFlags(JNIEnv *env, release_env_write_lock(); } +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_NativeBridge_refreshSymbolTable(JNIEnv *env, + jobject thiz) { + bugsnag_refresh_symbol_table(); +} + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index 8383b57793..14db22e74f 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -34,7 +34,7 @@ /** * Version of the bugsnag_event struct. Serialized to report header. */ -#define BUGSNAG_EVENT_VERSION 9 +#define BUGSNAG_EVENT_VERSION 10 #ifdef __cplusplus extern "C" { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/external/libunwindstack-ndk b/bugsnag-plugin-android-ndk/src/main/jni/external/libunwindstack-ndk index 81b3598c5c..8a8ff02b08 160000 --- a/bugsnag-plugin-android-ndk/src/main/jni/external/libunwindstack-ndk +++ b/bugsnag-plugin-android-ndk/src/main/jni/external/libunwindstack-ndk @@ -1 +1 @@ -Subproject commit 81b3598c5c60044ed8a58bc8ce32257f2da0704d +Subproject commit 8a8ff02b08fd8c152c328150e00e02ff6f7654ce diff --git a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c index 82a27c7d33..74c0ea2e91 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c @@ -91,6 +91,29 @@ JNIEnv *bsg_jni_cache_get_env() { bsg_jni_cache->METHOD = mtd; \ } while (0) +#define CACHE_ENUM_CONSTANT(ENUM, CLASS, NAME) \ + do { \ + jclass cls = bsg_safe_find_class(env, CLASS); \ + if (cls == NULL) { \ + BUGSNAG_LOG("JNI Cache Init Error: JNI enum class " CLASS " is NULL"); \ + goto failed; \ + } \ + jfieldID fld = \ + bsg_safe_get_static_field_id(env, cls, NAME, "L" CLASS ";"); \ + if (fld == NULL) { \ + BUGSNAG_LOG("JNI Cache Init Error: JNI enum const " CLASS "." NAME \ + " is NULL"); \ + goto failed; \ + } \ + jobject value = bsg_safe_get_static_object_field(env, cls, fld); \ + if (value == NULL) { \ + BUGSNAG_LOG("JNI Cache Init Error: JNI enum value " CLASS "." NAME \ + " is NULL"); \ + goto failed; \ + } \ + bsg_jni_cache->ENUM = (*env)->NewGlobalRef(env, value); \ + } while (0) + bool bsg_jni_cache_init(JNIEnv *env) { if (bsg_jni_cache->initialized) { return true; @@ -111,6 +134,9 @@ bool bsg_jni_cache_init(JNIEnv *env) { CACHE_CLASS(number, "java/lang/Number"); CACHE_METHOD(number, number_double_value, "doubleValue", "()D"); + CACHE_CLASS(Long, "java/lang/Long"); + CACHE_STATIC_METHOD(Long, Long_valueOf, "valueOf", "(J)Ljava/lang/Long;"); + CACHE_CLASS(String, "java/lang/String"); CACHE_CLASS(ArrayList, "java/util/ArrayList"); @@ -142,9 +168,9 @@ bool bsg_jni_cache_init(JNIEnv *env) { "getMetadata", "()Ljava/util/Map;"); CACHE_STATIC_METHOD(NativeInterface, NativeInterface_getContext, "getContext", "()Ljava/lang/String;"); - CACHE_STATIC_METHOD( - NativeInterface, NativeInterface_notify, "notify", - "([B[BLcom/bugsnag/android/Severity;[Ljava/lang/StackTraceElement;)V"); + CACHE_STATIC_METHOD(NativeInterface, NativeInterface_notify, "notify", + "([B[BLcom/bugsnag/android/Severity;[Lcom/bugsnag/" + "android/NativeStackframe;)V"); CACHE_STATIC_METHOD(NativeInterface, NativeInterface_isDiscardErrorClass, "isDiscardErrorClass", "(Ljava/lang/String;)Z"); CACHE_STATIC_METHOD(NativeInterface, NativeInterface_deliverReport, @@ -153,9 +179,12 @@ bool bsg_jni_cache_init(JNIEnv *env) { "leaveBreadcrumb", "([BLcom/bugsnag/android/BreadcrumbType;)V"); - CACHE_CLASS(StackTraceElement, "java/lang/StackTraceElement"); - CACHE_METHOD(StackTraceElement, StackTraceElement_constructor, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); + CACHE_CLASS(NativeStackframe, "com/bugsnag/android/NativeStackframe"); + CACHE_METHOD( + NativeStackframe, NativeStackframe_constructor, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Number;" + "Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;" + "Lcom/bugsnag/android/ErrorType;Ljava/lang/String;)V"); CACHE_CLASS(Severity, "com/bugsnag/android/Severity"); @@ -165,6 +194,8 @@ bool bsg_jni_cache_init(JNIEnv *env) { CACHE_METHOD(OpaqueValue, OpaqueValue_getJson, "getJson", "()Ljava/lang/String;"); + CACHE_ENUM_CONSTANT(ErrorType_C, "com/bugsnag/android/ErrorType", "C"); + pthread_key_create(&jni_cleanup_key, detach_java_env); bsg_jni_cache->initialized = true; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h index 3ddf3cd931..72711b5972 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h @@ -26,6 +26,9 @@ typedef struct { jclass number; jmethodID number_double_value; + jclass Long; + jmethodID Long_valueOf; + jclass String; jclass Map; @@ -54,8 +57,8 @@ typedef struct { jmethodID NativeInterface_isDiscardErrorClass; jmethodID NativeInterface_deliverReport; - jclass StackTraceElement; - jmethodID StackTraceElement_constructor; + jclass NativeStackframe; + jmethodID NativeStackframe_constructor; jclass Severity; @@ -63,6 +66,8 @@ typedef struct { jclass OpaqueValue; jmethodID OpaqueValue_getJson; + + jobject ErrorType_C; } bsg_jni_cache_t; extern bsg_jni_cache_t *const bsg_jni_cache; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c index a4a975643c..a3e251ada2 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c @@ -9,7 +9,7 @@ #include #include -const int BSG_MIGRATOR_CURRENT_VERSION = 9; +const int BSG_MIGRATOR_CURRENT_VERSION = 10; #ifdef __cplusplus extern "C" { @@ -47,7 +47,9 @@ bugsnag_report_v7 *bsg_report_v7_read(int fd); bugsnag_report_v8 *bsg_report_v8_read(int fd); -bugsnag_event *bsg_report_v9_read(int fd); +bugsnag_report_v9 *bsg_report_v9_read(int fd); + +bugsnag_event *bsg_report_v10_read(int fd); /** * the map_*() functions convert a structure of an older format into the latest. @@ -55,6 +57,8 @@ bugsnag_event *bsg_report_v9_read(int fd); * complete. */ +bugsnag_event *bsg_map_v9_to_report(bugsnag_report_v9 *report_v9); + bugsnag_event *bsg_map_v8_to_report(bugsnag_report_v8 *report_v8); bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7); @@ -71,6 +75,8 @@ bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2); bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1); +void migrate_error_v1(bsg_error *error, bsg_error_v1 *error_v1); + void migrate_metadata_v1(bugsnag_metadata_v1 *metadata_v1, bugsnag_metadata *metadata); @@ -137,8 +143,10 @@ bugsnag_event *bsg_read_event(char *filepath) { return bsg_map_v7_to_report(bsg_report_v7_read(fildes)); case 8: return bsg_map_v8_to_report(bsg_report_v8_read(fildes)); + case 9: + return bsg_map_v9_to_report(bsg_report_v9_read(fildes)); case BSG_MIGRATOR_CURRENT_VERSION: - return bsg_report_v9_read(fildes); + return bsg_report_v10_read(fildes); default: return NULL; } @@ -244,7 +252,26 @@ bugsnag_report_v8 *bsg_report_v8_read(int fd) { return event; } -bugsnag_event *bsg_report_v9_read(int fd) { +bugsnag_report_v9 *bsg_report_v9_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v9); + bugsnag_report_v9 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + + // read the feature flags, if possible + bsg_read_feature_flags(fd, &event->feature_flags, &event->feature_flag_count); + bsg_read_opaque_metadata(fd, &event->metadata); + bsg_read_opaque_breadcrumb_metadata(fd, event->breadcrumbs, + event->crumb_count); + + return event; +} + +bugsnag_event *bsg_report_v10_read(int fd) { size_t event_size = sizeof(bugsnag_event); bugsnag_event *event = calloc(1, event_size); @@ -263,6 +290,49 @@ bugsnag_event *bsg_report_v9_read(int fd) { return event; } +bugsnag_event *bsg_map_v9_to_report(bugsnag_report_v9 *report_v9) { + if (report_v9 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + event->notifier = report_v9->notifier; + memcpy(&event->metadata, &report_v9->metadata, sizeof(bugsnag_metadata)); + memcpy(&event->app, &report_v9->app, sizeof(bsg_app_info)); + memcpy(&event->device, &report_v9->device, sizeof(bsg_device_info)); + event->user = report_v9->user; + migrate_error_v1(&event->error, &report_v9->error); + event->crumb_count = report_v9->crumb_count; + event->crumb_first_index = report_v9->crumb_first_index; + memcpy(&event->breadcrumbs, &report_v9->breadcrumbs, + sizeof(report_v9->breadcrumbs)); + memcpy(&event->context, report_v9->context, sizeof(report_v9->context)); + event->severity = report_v9->severity; + memcpy(&event->session_id, report_v9->session_id, + sizeof(report_v9->session_id)); + memcpy(&event->session_start, report_v9->session_start, + sizeof(report_v9->session_start)); + event->handled_events = report_v9->handled_events; + event->unhandled_events = report_v9->unhandled_events; + memcpy(&event->grouping_hash, report_v9->grouping_hash, + sizeof(report_v9->grouping_hash)); + event->unhandled = report_v9->unhandled; + memcpy(&event->api_key, report_v9->api_key, sizeof(report_v9->api_key)); + event->thread_count = report_v9->thread_count; + memcpy(&event->threads, report_v9->threads, sizeof(report_v9->threads)); + + // copy the feature-flags ref over, but don't free the actual data + // this is effectively a change of ownership from bugsnag_report_v8 -> + // bugsnag_event + event->feature_flags = report_v9->feature_flags; + event->feature_flag_count = report_v9->feature_flag_count; + + free(report_v9); + } + return event; +} + bugsnag_event *bsg_map_v8_to_report(bugsnag_report_v8 *report_v8) { if (report_v8 == NULL) { return NULL; @@ -275,7 +345,7 @@ bugsnag_event *bsg_map_v8_to_report(bugsnag_report_v8 *report_v8) { memcpy(&event->app, &report_v8->app, sizeof(bsg_app_info)); memcpy(&event->device, &report_v8->device, sizeof(bsg_device_info)); event->user = report_v8->user; - event->error = report_v8->error; + migrate_error_v1(&event->error, &report_v8->error); event->crumb_count = report_v8->crumb_count; event->crumb_first_index = report_v8->crumb_first_index; migrate_breadcrumb_v3(report_v8->breadcrumbs, event, @@ -318,7 +388,7 @@ bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7) { migrate_app_v3(&event->app, &report_v7->app); migrate_device_v2(&event->device, &report_v7->device); event->user = report_v7->user; - event->error = report_v7->error; + migrate_error_v1(&event->error, &report_v7->error); event->crumb_count = report_v7->crumb_count; event->crumb_first_index = report_v7->crumb_first_index; migrate_breadcrumb_v3(report_v7->breadcrumbs, event, @@ -355,7 +425,7 @@ bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6) { migrate_app_v3(&event->app, &report_v6->app); migrate_device_v2(&event->device, &report_v6->device); event->user = report_v6->user; - event->error = report_v6->error; + migrate_error_v1(&event->error, &report_v6->error); event->crumb_count = report_v6->crumb_count; event->crumb_first_index = report_v6->crumb_first_index; migrate_breadcrumb_v3(report_v6->breadcrumbs, event, @@ -391,7 +461,7 @@ bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { migrate_device_v2(&event->device, &report_v5->device); bsg_strcpy(event->context, report_v5->context); event->user = report_v5->user; - event->error = report_v5->error; + migrate_error_v1(&event->error, &report_v5->error); event->severity = report_v5->severity; bsg_strncpy(event->session_id, report_v5->session_id, sizeof(report_v5->session_id)); @@ -421,7 +491,7 @@ bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { migrate_metadata_v1(&report_v4->metadata, &event->metadata); migrate_device_v2(&event->device, &report_v4->device); event->user = report_v4->user; - event->error = report_v4->error; + migrate_error_v1(&event->error, &report_v4->error); event->crumb_count = report_v4->crumb_count; event->crumb_first_index = report_v4->crumb_first_index; migrate_breadcrumb_v3(report_v4->breadcrumbs, event, V2_BUGSNAG_CRUMBS_MAX); @@ -506,7 +576,7 @@ bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2) { strcpy(event->error.errorMessage, report_v2->exception.message); strcpy(event->error.type, report_v2->exception.type); event->error.frame_count = report_v2->exception.frame_count; - size_t error_size = sizeof(bugsnag_stackframe) * BUGSNAG_FRAMES_MAX; + size_t error_size = sizeof(bugsnag_stackframe_v1) * BUGSNAG_FRAMES_MAX; memcpy(&event->error.stacktrace, report_v2->exception.stacktrace, error_size); @@ -649,6 +719,22 @@ void migrate_metadata_v1(bugsnag_metadata_v1 *metadata_v1, } } +void migrate_error_v1(bsg_error *error, bsg_error_v1 *error_v1) { + memcpy(error->errorClass, error_v1->errorClass, sizeof(error_v1->errorClass)); + memcpy(error->errorMessage, error_v1->errorMessage, + sizeof(error_v1->errorMessage)); + memcpy(error->type, error_v1->type, sizeof(error_v1->type)); + + error->frame_count = error_v1->frame_count; + + for (int i = 0; i < error_v1->frame_count; i++) { + bugsnag_stackframe *frame = &error->stacktrace[i]; + bugsnag_stackframe_v1 *frame_v1 = &error_v1->stacktrace[i]; + + memcpy(frame, frame_v1, sizeof(bugsnag_stackframe_v1)); + } +} + int bsg_calculate_total_crumbs(int old_count) { return old_count < BUGSNAG_CRUMBS_MAX ? old_count : BUGSNAG_CRUMBS_MAX; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c index 446004d8a2..9194dd2cc1 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c @@ -275,6 +275,15 @@ void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, json_object_set_string(frame, "method", (*stackframe).method); } + if (*stackframe->code_identifier != 0) { + char code_identifier[sizeof(stackframe->code_identifier) + 1]; + code_identifier[sizeof(stackframe->code_identifier)] = + 0; // force zero terminator + strncpy(code_identifier, stackframe->code_identifier, + sizeof(stackframe->code_identifier)); + json_object_set_string(frame, "codeIdentifier", code_identifier); + } + json_array_append_value(stacktrace, frame_val); } @@ -287,6 +296,7 @@ void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, #define TIMESTAMP_DECODE atol #define TIMESTAMP_MILLIS_FORMAT "%s.%03ldZ" #endif + /** * Convert a string representing the number of milliseconds since the epoch * into the date format "yyyy-MM-ddTHH:mm:ss.SSSZ". Safe for all dates earlier @@ -322,6 +332,7 @@ static bool timestamp_to_iso8601_millis(const char *source, char *dest) { } return false; } + #undef TIMESTAMP_T #undef TIMESTAMP_DECODE #undef TIMESTAMP_MILLIS_FORMAT diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h index 81af1bd390..761b733d8e 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h @@ -34,6 +34,16 @@ typedef struct { char url[64]; } bsg_library; +typedef struct { + uintptr_t frame_address; + uintptr_t symbol_address; + uintptr_t load_address; + uintptr_t line_number; + + char filename[256]; + char method[256]; +} bugsnag_stackframe_v1; + /** a Bugsnag exception */ typedef struct { /** The exception name or stringified code */ @@ -51,9 +61,29 @@ typedef struct { /** * An ordered list of stack frames from the oldest to the most recent */ - bugsnag_stackframe stacktrace[BUGSNAG_FRAMES_MAX]; + bugsnag_stackframe_v1 stacktrace[BUGSNAG_FRAMES_MAX]; } bsg_exception; +/** a Bugsnag exception */ +typedef struct { + /** The exception name or stringified code */ + char errorClass[64]; + /** A description of what went wrong */ + char errorMessage[256]; + /** The variety of exception which needs to be processed by the pipeline */ + char type[32]; + + /** + * The number of frames used in the stacktrace. Must be less than + * BUGSNAG_FRAMES_MAX. + */ + ssize_t frame_count; + /** + * An ordered list of stack frames from the oldest to the most recent + */ + bugsnag_stackframe_v1 stacktrace[BUGSNAG_FRAMES_MAX]; +} bsg_error_v1; + typedef struct { char key[64]; char value[64]; @@ -236,7 +266,7 @@ typedef struct { bsg_app_info_v2 app; bsg_device_info_v2 device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -261,7 +291,7 @@ typedef struct { bsg_app_info_v2 app; bsg_device_info_v2 device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -287,7 +317,7 @@ typedef struct { bsg_app_info_v3 app; bsg_device_info_v2 device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -313,7 +343,7 @@ typedef struct { bsg_app_info_v3 app; bsg_device_info_v2 device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -339,7 +369,7 @@ typedef struct { bsg_app_info_v3 app; bsg_device_info_v2 device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -368,7 +398,7 @@ typedef struct { bsg_app_info app; bsg_device_info device; bugsnag_user user; - bsg_error error; + bsg_error_v1 error; bugsnag_metadata_v1 metadata; int crumb_count; @@ -395,6 +425,46 @@ typedef struct { bsg_feature_flag *feature_flags; } bugsnag_report_v8; +typedef struct { + bsg_notifier notifier; + bsg_app_info app; + bsg_device_info device; + bugsnag_user user; + bsg_error_v1 error; + bugsnag_metadata metadata; + + int crumb_count; + // Breadcrumbs are a ring; the first index moves as the + // structure is filled and replaced. + int crumb_first_index; + bugsnag_breadcrumb breadcrumbs[BUGSNAG_CRUMBS_MAX]; + + char context[64]; + bugsnag_severity severity; + + char session_id[33]; + char session_start[33]; + int handled_events; + int unhandled_events; + char grouping_hash[64]; + bool unhandled; + char api_key[64]; + + int thread_count; + bsg_thread threads[BUGSNAG_THREADS_MAX]; + + /** + * The number of feature flags currently specified. + */ + size_t feature_flag_count; + + /** + * Pointer to the current feature flags. This is dynamically allocated and + * serialized/deserialized separately to the rest of the struct. + */ + bsg_feature_flag *feature_flags; +} bugsnag_report_v9; + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp index 293f3c38d9..907cb67419 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp @@ -1,5 +1,6 @@ #include "stack_unwinder.h" +#include "logger.h" #include "string.h" #include @@ -53,7 +54,7 @@ void bsg_unwinder_init() { } attempted_init = true; - auto crash_time_maps = new unwindstack::LocalMaps(); + auto crash_time_maps = new unwindstack::LocalUpdatableMaps(); if (crash_time_maps->Parse()) { std::shared_ptr crash_time_memory( new unwindstack::MemoryLocal); @@ -72,6 +73,58 @@ void bsg_unwinder_init() { } } +static void populate_code_identifier(const unwindstack::FrameData &frame, + bugsnag_stackframe &dst_frame) { + auto maps = crash_time_unwinder->GetMaps(); + if (maps == nullptr) { + return; + } + + auto map_info = maps->Find(frame.pc); + if (map_info == nullptr) { + return; + } + + auto elf_fields = map_info->elf_fields(); + if (elf_fields == nullptr) { + return; + } + + auto shared_build_id = elf_fields->build_id_.load(); + std::string_view build_id; + if (shared_build_id == nullptr) { + auto elf = elf_fields->elf_.get(); + if (elf == nullptr) { + return; + } + + build_id = elf->GetBuildID(); + } else { + build_id = *shared_build_id; + } + + if (build_id.empty()) { + return; + } + + // MapInfo.GetPrintableBuildID is *not* async-safe so we need our own + // safe hex encoder to copy BuildID into code_identifier. + bsg_hex_encode(dst_frame.code_identifier, build_id.data(), build_id.length(), + sizeof(dst_frame.code_identifier)); +} + +void bsg_unwinder_refresh(void) { + if (crash_time_unwinder == nullptr) { + return; + } + + auto *crash_time_maps = dynamic_cast( + crash_time_unwinder->GetMaps()); + if (crash_time_maps != nullptr) { + crash_time_maps->Reparse(nullptr); + } +} + ssize_t bsg_unwind_crash_stack(bugsnag_stackframe stack[BUGSNAG_FRAMES_MAX], siginfo_t *info, void *user_context) { if (crash_time_unwinder == nullptr || unwinding_crash_stack) { @@ -96,6 +149,8 @@ ssize_t bsg_unwind_crash_stack(bugsnag_stackframe stack[BUGSNAG_FRAMES_MAX], dst_frame.load_address = frame.map_start; dst_frame.symbol_address = frame.pc - frame.function_offset; + populate_code_identifier(frame, dst_frame); + // if the filename or method name cannot be found (or are considered // invalid) - fallback to dladdr to find them if (bsg_check_invalid_libname(frame.map_name) || @@ -132,6 +187,10 @@ bsg_unwind_concurrent_stack(bugsnag_stackframe stack[BUGSNAG_FRAMES_MAX], dst_frame.load_address = frame.map_info->start(); dst_frame.symbol_address = frame.pc - frame.map_info->offset(); + bsg_strncpy(dst_frame.code_identifier, + frame.map_info->GetPrintableBuildID().c_str(), + sizeof(dst_frame.code_identifier)); + // if the filename is empty or invalid, use the `Elf` info to get the // correct Soname if the function_name is empty as well, fallback to // dladdr for the symbol data diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.h index 25078e898b..49cc6f4a89 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.h @@ -13,6 +13,12 @@ extern "C" { */ void bsg_unwinder_init(void); +/** + * Refresh the stack unwinder. This can be called to force a refresh of any + * cached data within the unwinder. + */ +void bsg_unwinder_refresh(void); + /** * Unwind a stack in a terminating context. If info and a user context pointer * are provided, the exception stack will be walked. Otherwise, the current diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c index e1d853a9e7..02959082ea 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c @@ -24,3 +24,20 @@ void bsg_strncpy(char *dst, const char *src, size_t dst_size) { strncat(dst, src, dst_size - 1); } } + +void bsg_hex_encode(char *dst, const void *src, size_t byte_count, + size_t max_chars) __asyncsafe { + static const char *hex = "0123456789abcdef"; + + const size_t byte_copy_count = + (max_chars > byte_count * 2) ? byte_count : (max_chars - 1) / 2; + + char *cursor = (char *)src; + char *outCursor = dst; + for (size_t i = 0; i < byte_copy_count; ++i) { + *outCursor++ = hex[(*cursor >> 4) & 0xF]; + *outCursor++ = hex[(*cursor++) & 0xF]; + } + + *outCursor = '\0'; +} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h index 6436e7b4ef..edafcfa60b 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h @@ -23,6 +23,19 @@ size_t bsg_strlen(const char *str) __asyncsafe; */ void bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; +/** + * Encode a number of bytes into dst while hex encoding the data. + * + * @param dst the destination buffer, which have enough space for at least + * max_chars + * @param src pointer to the first byte to encode + * @param byte_count the number of bytes to encode from src + * @param max_chars the maximum number of chars to encode (including a + * terminator) + */ +void bsg_hex_encode(char *dst, const void *src, size_t byte_count, + size_t max_chars) __asyncsafe; + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index 01199e034e..86e0f83fb8 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(bugsnag-ndk-test SHARED cpp/migrations/EventMigrationV7Tests.cpp cpp/migrations/EventMigrationV8Tests.cpp cpp/migrations/EventMigrationV9Tests.cpp + cpp/migrations/EventMigrationV10Tests.cpp cpp/UnwindTest.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV10Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV10Tests.cpp new file mode 100644 index 0000000000..52c8ff8231 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV10Tests.cpp @@ -0,0 +1,202 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v9 format + */ +static void *create_full_event() { + auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 81395165021; + event->app.duration_in_foreground = 81395165010; + event->app.in_foreground = true; + event->app.is_launching = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 8139512718; + + // breadcrumbs + auto max = 50; + event->crumb_first_index = 2; // test the circular buffer logic + char name[30]; + for (int i = event->crumb_first_index; i < max; i++) { + sprintf(name, "mission %d", i - event->crumb_first_index); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + for (int i = 0; i < event->crumb_first_index; i++) { + sprintf(name, "mission %d", (max - event->crumb_first_index) + i); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + event->crumb_count = max; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "🍨"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3839512576; + + // feature flags + event->feature_flag_count = 4; + event->feature_flags = + (bsg_feature_flag *)calloc(4, sizeof(bsg_feature_flag)); + event->feature_flags[0].name = strdup("bluebutton"); + event->feature_flags[0].variant = strdup("on"); + event->feature_flags[1].name = strdup("redbutton"); + event->feature_flags[1].variant = strdup("off"); + event->feature_flags[2].name = strdup("nobutton"); + event->feature_flags[3].name = strdup("switch"); + event->feature_flags[3].variant = strdup("left"); + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = (uintptr_t)4294967294; + event->error.stacktrace[0].load_address = (uintptr_t)2367523; + event->error.stacktrace[0].symbol_address = 776; + event->error.stacktrace[0].line_number = (uintptr_t)4194967233; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = + (uintptr_t)3011142731; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // threads + event->thread_count = 8; + for (int i = 0; i < event->thread_count; i++) { + event->threads[i].id = 1000 + i; + sprintf(event->threads[i].name, "Thread #%d", i); + sprintf(event->threads[i].state, "paused-%d", i); + } + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +static const char *write_event_v9(JNIEnv *env, jstring temp_file, + void *(event_generator)()) { + auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment)); + event_ctx->report_header.version = 10; + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + sprintf(event_ctx->next_event_path, "%s", path); + + // (old format) event struct -> file on disk + void *old_event = event_generator(); + memcpy(&event_ctx->next_event, old_event, sizeof(bugsnag_event)); + free(old_event); + // FUTURE(df): whenever migration v11 rolls around, the v10 version of + // bsg_serialize_event_to_file() function should be moved into this file to + // preserve the migration test behavior. The good news is—if this doesn't + // happen—the test will probably start failing loudly. + bsg_serialize_event_to_file(event_ctx); + free(event_ctx); + return path; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV10Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = write_event_v9(env, temp_file, create_payload_info_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *json_str = json_serialize_to_string(event_val); + auto result = (*env).NewStringUTF(json_str); + free(json_str); + + return result; +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV10Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = write_event_v9(env, temp_file, create_full_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + char *output = bsg_serialize_event_to_json_string(parsed_event); + for (int i = 0; i < parsed_event->feature_flag_count; i++) { + free(parsed_event->feature_flags[i].name); + free(parsed_event->feature_flags[i].variant); + } + free(parsed_event->feature_flags); + free(parsed_event); + + // latest event type -> temp file + write_str_to_file(output, path); + free(output); +} + +#ifdef __cplusplus +} +#endif diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV9Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV9Tests.cpp index 3e5354b6b1..109d622dd8 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV9Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV9Tests.cpp @@ -1,11 +1,12 @@ #include #include +#include #include "utils.hpp" static void *create_payload_info_event() { - auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + auto event = (bugsnag_report_v9 *)calloc(1, sizeof(bugsnag_report_v9)); strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); strcpy(event->notifier.name, "Test Library"); @@ -19,7 +20,7 @@ static void *create_payload_info_event() { * Create a new event in v9 format */ static void *create_full_event() { - auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + auto event = (bugsnag_report_v9 *)calloc(1, sizeof(bugsnag_report_v9)); strcpy(event->context, "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); @@ -102,10 +103,10 @@ static void *create_full_event() { // metadata strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + bsg_add_metadata_value_bool(&event->metadata, "metrics", "experimentX", false); + bsg_add_metadata_value_str(&event->metadata, "metrics", "subject", "percy"); + bsg_add_metadata_value_str(&event->metadata, "app", "weather", "rain"); + bsg_add_metadata_value_double(&event->metadata, "metrics", "counter", 47.5); // session info event->handled_events = 5; @@ -129,6 +130,100 @@ static void *create_full_event() { return event; } +static bool write_feature_flag(bsg_buffered_writer *writer, + bsg_feature_flag *flag) { + if (!writer->write_string(writer, flag->name)) { + return false; + } + + if (flag->variant) { + if (!writer->write_byte(writer, 1)) { + return false; + } + + if (!writer->write_string(writer, flag->variant)) { + return false; + } + } else { + if (!writer->write_byte(writer, 0)) { + return false; + } + } + + return true; +} + +static bool bsg_write_feature_flags(bugsnag_report_v9 *event, + bsg_buffered_writer *writer) { + const uint32_t feature_flag_count = event->feature_flag_count; + if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { + return false; + } + + for (uint32_t index = 0; index < feature_flag_count; index++) { + if (!write_feature_flag(writer, &event->feature_flags[index])) { + return false; + } + } + + return true; +} + +static bool bsg_write_opaque_metadata_unit(bugsnag_metadata *metadata, + bsg_buffered_writer *writer) { + + for (size_t index = 0; index < metadata->value_count; index++) { + uint32_t value_size = metadata->values[index].opaque_value_size; + if (metadata->values[index].type == BSG_METADATA_OPAQUE_VALUE && + value_size > 0) { + if (!writer->write(writer, metadata->values[index].opaque_value, + value_size)) { + return false; + } + } + } + + return true; +} + +bool bsg_write_opaque_metadata(bugsnag_report_v9 *event, + bsg_buffered_writer *writer) { + + if (!bsg_write_opaque_metadata_unit(&event->metadata, writer)) { + return false; + } + + for (int breadcrumb_index = 0; breadcrumb_index < event->crumb_count; + breadcrumb_index++) { + if (!bsg_write_opaque_metadata_unit( + &event->breadcrumbs[breadcrumb_index].metadata, writer)) { + return false; + } + } + + return true; +} + +bool bsg_event_write_v9(bsg_environment *env) { + bsg_buffered_writer writer; + if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { + return false; + } + + bool result = + // write header - determines format version, etc + bsg_report_header_write(&env->report_header, writer.fd) && + // add cached event info + writer.write(&writer, &env->next_event, sizeof(bugsnag_report_v9)) && + // append feature flags after event structure + bsg_write_feature_flags((bugsnag_report_v9*)(&env->next_event), &writer) && + // append opaque metadata after the feature flags + bsg_write_opaque_metadata((bugsnag_report_v9*)(&env->next_event), &writer); + + writer.dispose(&writer); + return result; +} + static const char *write_event_v9(JNIEnv *env, jstring temp_file, void *(event_generator)()) { auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment)); @@ -138,13 +233,9 @@ static const char *write_event_v9(JNIEnv *env, jstring temp_file, // (old format) event struct -> file on disk void *old_event = event_generator(); - memcpy(&event_ctx->next_event, old_event, sizeof(bugsnag_event)); + memcpy(&event_ctx->next_event, old_event, sizeof(bugsnag_report_v9)); free(old_event); - // FUTURE(df): whenever migration v10 rolls around, the v8 version of - // bsg_serialize_event_to_file() function should be moved into this file to - // preserve the migration test behavior. The good news is—if this doesn't - // happen—the test will probably start failing loudly. - bsg_serialize_event_to_file(event_ctx); + bsg_event_write_v9(event_ctx); free(event_ctx); return path; } diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_string.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_string.c index 711025fa71..69211ce65a 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_string.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_string.c @@ -56,6 +56,41 @@ TEST length_null_string(void) { PASS(); } +TEST hex_encode(void) { + char out_buffer[16]; + char *bytes = "bytes"; + bsg_hex_encode(out_buffer, bytes, strlen(bytes), sizeof(out_buffer)); + ASSERT_STR_EQ("6279746573", out_buffer); + PASS(); +} + +TEST hex_encode_zero(void) { + char out_buffer[256]; + out_buffer[0] = '1'; + + bsg_hex_encode(out_buffer, NULL, 0, sizeof(out_buffer)); + ASSERT_EQ(0, *out_buffer); + PASS(); +} + +TEST hex_encode_overflow(void) { + char out_buffer[7]; + char *bytes = "bytes"; + bsg_hex_encode(out_buffer, bytes, strlen(bytes), sizeof(out_buffer)); + // the output must still be zero-terminated + ASSERT_STR_EQ("627974", out_buffer); + PASS(); +} + +TEST hex_encode_exact_length(void) { + char out_buffer[10]; + char *bytes = "bytes"; + bsg_hex_encode(out_buffer, bytes, strlen(bytes), sizeof(out_buffer)); + // we expect the entire last byte of *input* to be dropped + ASSERT_STR_EQ("62797465", out_buffer); + PASS(); +} + SUITE(suite_string_utils) { RUN_TEST(test_copy_empty_string); RUN_TEST(test_copy_null_string); @@ -63,5 +98,9 @@ SUITE(suite_string_utils) { RUN_TEST(length_empty_string); RUN_TEST(length_literal_string); RUN_TEST(length_null_string); + RUN_TEST(hex_encode); + RUN_TEST(hex_encode_zero); + RUN_TEST(hex_encode_overflow); + RUN_TEST(hex_encode_exact_length); } diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index b48acd4f40..4707086442 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -38,7 +38,7 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:5.25.0" + implementation "com.bugsnag:bugsnag-android:5.26.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.4.0" implementation "com.google.android.material:material:1.4.0" diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MainActivity.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MainActivity.kt index bc62cea273..78e028d0ea 100644 --- a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MainActivity.kt +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MainActivity.kt @@ -12,9 +12,11 @@ import android.widget.EditText import com.bugsnag.android.mazerunner.scenarios.Scenario import org.json.JSONObject import java.io.File -import java.lang.IllegalArgumentException +import java.io.IOException +import java.net.HttpURLConnection import java.net.URL import kotlin.concurrent.thread +import kotlin.math.max class MainActivity : Activity() { @@ -96,8 +98,7 @@ class MainActivity : Activity() { Thread.sleep(1000) try { // Get the next command from Maze Runner - val commandUrl: String = "http://bs-local.com:9339/command" - val commandStr = URL(commandUrl).readText() + val commandStr = readCommand() if (commandStr == "null") { log("No Maze Runner commands queued") continue @@ -145,14 +146,47 @@ class MainActivity : Activity() { } } + private fun readCommand(): String { + val commandUrl = "http://bs-local.com:9339/command" + val urlConnection = URL(commandUrl).openConnection() as HttpURLConnection + try { + return urlConnection.inputStream.use { it.reader().readText() } + } catch (ioe: IOException) { + try { + val errorMessage = urlConnection.errorStream.use { it.reader().readText() } + log( + "Failed to GET $commandUrl (HTTP ${urlConnection.responseCode} " + + "${urlConnection.responseMessage}):\n" + + "${"-".repeat(errorMessage.width)}\n" + + "$errorMessage\n" + + "-".repeat(errorMessage.width) + ) + } catch (e: Exception) { + log("Failed to retrieve error message from connection", e) + } + + throw ioe + } + } + // load the scenario first, which initialises bugsnag without running any crashy code - private fun startBugsnag(eventType: String, mode: String, sessionsUrl: String, notifyUrl: String) { + private fun startBugsnag( + eventType: String, + mode: String, + sessionsUrl: String, + notifyUrl: String + ) { scenario = loadScenario(eventType, mode, sessionsUrl, notifyUrl) scenario?.startBugsnag(true) } // execute the pre-loaded scenario, or load it then execute it if needed - private fun runScenario(eventType: String, mode: String, sessionsUrl: String, notifyUrl: String) { + private fun runScenario( + eventType: String, + mode: String, + sessionsUrl: String, + notifyUrl: String + ) { if (scenario == null) { scenario = loadScenario(eventType, mode, sessionsUrl, notifyUrl) scenario?.startBugsnag(false) @@ -188,7 +222,12 @@ class MainActivity : Activity() { folder.deleteRecursively() } - private fun loadScenario(eventType: String, mode: String, sessionsUrl: String, notifyUrl: String): Scenario { + private fun loadScenario( + eventType: String, + mode: String, + sessionsUrl: String, + notifyUrl: String + ): Scenario { val apiKeyField = findViewById(R.id.manualApiKey) @@ -229,4 +268,7 @@ class MainActivity : Activity() { } private fun getStoredApiKey() = prefs.getString(apiKeyKey, "") + + private val String.width get() = + lineSequence().fold(0) { maxWidth, line -> max(maxWidth, line.length) } } diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/CMakeLists.txt b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/CMakeLists.txt index e8950d76ef..21849bb051 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/CMakeLists.txt +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.4.1) add_library(cxx-scenarios-bugsnag SHARED src/main/cpp/cxx-scenarios-bugsnag.cpp + src/main/cpp/CXXExternalStackElementScenario.cpp src/main/cpp/CXXExceptionSmokeScenario.cpp) set_target_properties(cxx-scenarios-bugsnag PROPERTIES diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXExternalStackElementScenario.cpp b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/CXXExternalStackElementScenario.cpp similarity index 92% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXExternalStackElementScenario.cpp rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/CXXExternalStackElementScenario.cpp index cafb37104f..68928f2e1d 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXExternalStackElementScenario.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/CXXExternalStackElementScenario.cpp @@ -1,6 +1,7 @@ #include #include #include +#include extern "C" { // defined in libs/[ABI]/libmonochrome.so @@ -10,6 +11,7 @@ JNIEXPORT int JNICALL Java_com_bugsnag_android_mazerunner_scenarios_CXXExternalStackElementScenario_crash( JNIEnv *env, jobject instance, jint counter) { void *monochrome = dlopen("libmonochrome.so", RTLD_GLOBAL); + bugsnag_refresh_symbol_table(); *(void**)(&something_innocuous) = dlsym(monochrome, "something_innocuous"); printf("Captain, why are we out here chasing comets?\n%d\n", counter); diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java similarity index 88% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java index 3f16823ecb..5017b1259b 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExternalStackElementScenario.java @@ -10,7 +10,8 @@ public class CXXExternalStackElementScenario extends Scenario { static { - System.loadLibrary("cxx-scenarios"); + System.loadLibrary("bugsnag-ndk"); + System.loadLibrary("cxx-scenarios-bugsnag"); } public native void crash(int counter); diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/arm64-v8a/libmonochrome.so b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/arm64-v8a/libmonochrome.so similarity index 100% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/arm64-v8a/libmonochrome.so rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/arm64-v8a/libmonochrome.so diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/armeabi-v7a/libmonochrome.so b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/armeabi-v7a/libmonochrome.so similarity index 100% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/armeabi-v7a/libmonochrome.so rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/armeabi-v7a/libmonochrome.so diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/x86/libmonochrome.so b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/x86/libmonochrome.so similarity index 100% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/x86/libmonochrome.so rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/x86/libmonochrome.so diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/x86_64/libmonochrome.so b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/x86_64/libmonochrome.so similarity index 100% rename from features/fixtures/mazerunner/cxx-scenarios/src/main/jniLibs/x86_64/libmonochrome.so rename to features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/jniLibs/x86_64/libmonochrome.so diff --git a/features/fixtures/mazerunner/cxx-scenarios/CMakeLists.txt b/features/fixtures/mazerunner/cxx-scenarios/CMakeLists.txt index 3b69a2cd05..1a3f3945aa 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/CMakeLists.txt +++ b/features/fixtures/mazerunner/cxx-scenarios/CMakeLists.txt @@ -5,7 +5,6 @@ add_library(cxx-scenarios SHARED src/main/cpp/CXXAbortScenario.cpp src/main/cpp/CXXCallNullFunctionPointerScenario.cpp src/main/cpp/CXXDereferenceNullScenario.cpp - src/main/cpp/CXXExternalStackElementScenario.cpp src/main/cpp/CXXImproperTypecastScenario.cpp src/main/cpp/CXXInvalidRethrow.cpp src/main/cpp/CXXStackoverflowScenario.cpp diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXConfigurationMetadataNativeCrashScenario.java b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXConfigurationMetadataNativeCrashScenario.java index 81653b2c19..85ae5c81b4 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXConfigurationMetadataNativeCrashScenario.java +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXConfigurationMetadataNativeCrashScenario.java @@ -11,7 +11,9 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; public class CXXConfigurationMetadataNativeCrashScenario extends Scenario { @@ -54,6 +56,11 @@ public CXXConfigurationMetadataNativeCrashScenario(@NonNull Configuration config "spring", "autumn" )); + + Set set = new LinkedHashSet<>(); + set.add("value1"); + set.add("2value"); + config.addMetadata("complex", "set", set); } } diff --git a/features/full_tests/native_crash_handling.feature b/features/full_tests/native_crash_handling.feature index 3943572dd4..21ad8d64d0 100644 --- a/features/full_tests/native_crash_handling.feature +++ b/features/full_tests/native_crash_handling.feature @@ -19,6 +19,7 @@ Feature: Native crash reporting And the error payload field "events.0.metaData.app.memoryLimit" is greater than 0 And the first significant stack frames match: | get_the_null_value() | CXXDereferenceNullScenario.cpp | 7 | + And the "codeIdentifier" of stack frame 0 is not null # This scenario will not pass on API levels < 18, as stack corruption # is handled without calling atexit handlers, etc. @@ -40,6 +41,7 @@ Feature: Native crash reporting And the event "unhandled" is true And the first significant stack frames match: | crash_stack_overflow | CXXStackoverflowScenario.cpp | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Program trap() When I run "CXXTrapScenario" and relaunch the crashed app @@ -57,6 +59,7 @@ Feature: Native crash reporting And the event "unhandled" is true And the first significant stack frames match: | trap_it() | CXXTrapScenario.cpp | 12 | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Write to read-only memory When I run "CXXWriteReadOnlyMemoryScenario" and relaunch the crashed app @@ -70,6 +73,7 @@ Feature: Native crash reporting And the first significant stack frames match: | crash_write_read_only_mem(int) | CXXWriteReadOnlyMemoryScenario.cpp | 12 | | Java_com_bugsnag_android_mazerunner_scenarios_CXXWriteReadOnlyMemoryScenario_crash | CXXWriteReadOnlyMemoryScenario.cpp | 22 | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Improper object type cast When I run "CXXImproperTypecastScenario" and relaunch the crashed app @@ -83,6 +87,7 @@ Feature: Native crash reporting And the first significant stack frames match: | crash_improper_cast(void*) | CXXImproperTypecastScenario.cpp | 12 | | Java_com_bugsnag_android_mazerunner_scenarios_CXXImproperTypecastScenario_crash | CXXImproperTypecastScenario.cpp | 20 | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Program abort() When I run "CXXAbortScenario" and relaunch the crashed app @@ -101,6 +106,7 @@ Feature: Native crash reporting And the first significant stack frames match: | evictor::exit_with_style() | CXXAbortScenario.cpp | 5 | | Java_com_bugsnag_android_mazerunner_scenarios_CXXAbortScenario_crash | CXXAbortScenario.cpp | 13 | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Undefined JNI method When I run "UnsatisfiedLinkErrorScenario" and relaunch the crashed app @@ -131,7 +137,8 @@ Feature: Native crash reporting And the exception "type" equals "c" And the first significant stack frames match: | something_innocuous | libmonochrome.so | (ignore) | - | Java_com_bugsnag_android_mazerunner_scenarios_CXXExternalStackElementScenario_crash | CXXExternalStackElementScenario.cpp | 18 | + | Java_com_bugsnag_android_mazerunner_scenarios_CXXExternalStackElementScenario_crash | CXXExternalStackElementScenario.cpp | 20 | + And the "codeIdentifier" of stack frame 0 is not null Scenario: Call null function pointer A null pointer should be the first element of a stack trace, diff --git a/features/full_tests/native_event_tracking.feature b/features/full_tests/native_event_tracking.feature index 72662f6acf..8c2944e20c 100644 --- a/features/full_tests/native_event_tracking.feature +++ b/features/full_tests/native_event_tracking.feature @@ -10,6 +10,7 @@ Feature: Synchronizing app/device metadata in the native layer And the event "app.inForeground" is true And the event "app.duration" is greater than 0 And the event "unhandled" is false + And the "codeIdentifier" of stack frame 0 is not null Scenario: Capture foreground state while in the background When I run "CXXBackgroundNotifyScenario" @@ -24,6 +25,7 @@ Feature: Synchronizing app/device metadata in the native layer # Appium 1.9.1 - 1.20.2 changes the orientation to landscape when foregrounding. # Return it to portrait to avoid impacting other scenarios. And I set the screen orientation to portrait + And the "codeIdentifier" of stack frame 0 is not null Scenario: Capture foreground state while in a foreground crash When I run "CXXTrapScenario" and relaunch the crashed app diff --git a/features/full_tests/native_metadata.feature b/features/full_tests/native_metadata.feature index 5e1fa58735..acfc2e3b1a 100644 --- a/features/full_tests/native_metadata.feature +++ b/features/full_tests/native_metadata.feature @@ -25,6 +25,8 @@ Feature: Native Metadata API And the event "metaData.complex.list.1" equals "winter" And the event "metaData.complex.list.2" equals "spring" And the event "metaData.complex.list.3" equals "autumn" + And the event "metaData.complex.set.0" equals "value1" + And the event "metaData.complex.set.1" equals "2value" And the event "unhandled" is true Scenario: Remove MetaData from the NDK layer diff --git a/features/smoke_tests/01_anr.feature b/features/smoke_tests/01_anr.feature new file mode 100644 index 0000000000..02926e2e30 --- /dev/null +++ b/features/smoke_tests/01_anr.feature @@ -0,0 +1,87 @@ +# This scenario is in its own file and folder so that it can be run first on Android 4 +Feature: ANR smoke test + + @skip_android_8_1 + Scenario: ANR detection + When I clear any error dialogue + And I run "JvmAnrLoopScenario" + And I wait for 1 seconds + And I tap the screen 3 times + And I wait for 5 seconds + And I tap the back-button 3 times + And I wait to receive an error + + # Exception details + Then the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the error payload field "events" is an array with 1 elements + And the exception "errorClass" equals "ANR" + And the exception "message" starts with " Input dispatching timed out" + And the exception "type" equals "android" + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "anrError" + And the event "severityReason.unhandledOverridden" is false + + # Stacktrace validation + And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array + And the event "exceptions.0.stacktrace.0.method" is not null + And the event "exceptions.0.stacktrace.0.file" is not null + And the event "exceptions.0.stacktrace.0.lineNumber" is not null + + # App data + And the event binary arch field is valid + And the event "app.buildUUID" equals "test-7.5.3" + And the event "app.id" equals "com.bugsnag.android.mazerunner" + And the event "app.releaseStage" equals "mazerunner" + And the event "app.type" equals "android" + And the event "app.version" equals "1.1.14" + And the event "app.versionCode" equals 34 + And the error payload field "events.0.app.duration" is an integer + And the error payload field "events.0.app.durationInForeground" is an integer + And the event "app.inForeground" is true + And the event "app.isLaunching" is false + And the event "metaData.app.name" equals "MazeRunner" + + # Device data + And the error payload field "events.0.device.cpuAbi" is a non-empty array + And the event "device.jailbroken" is false + And the event "device.id" is not null + And the error payload field "events.0.device.id" is stored as the value "device_id" + And the event "device.locale" is not null + And the event "device.manufacturer" is not null + And the event "device.model" is not null + And the event "device.osName" equals "android" + And the event "device.osVersion" is not null + And the event "device.runtimeVersions" is not null + And the event "device.runtimeVersions.androidApiLevel" is not null + And the event "device.runtimeVersions.osBuild" is not null + And the error payload field "events.0.device.totalMemory" is greater than 0 + And the error payload field "events.0.device.freeDisk" is greater than 0 + And the error payload field "events.0.device.freeMemory" is greater than 0 + And the event "device.orientation" equals "portrait" + And the event "device.time" is a timestamp + And the event "metaData.device.locationStatus" is not null + And the event "metaData.device.emulator" is false + And the event "metaData.device.networkAccess" is not null + And the event "metaData.device.screenDensity" is not null + And the event "metaData.device.dpi" is not null + And the event "metaData.device.screenResolution" is not null + And the event "metaData.device.brand" is not null + + # User + And the event "user.id" is not null + And the error payload field "events.0.user.id" equals the stored value "device_id" + + # Threads validation + And the error payload field "events.0.threads" is a non-empty array + And the error payload field "events.0.threads.0.id" is an integer + And the event "threads.0.name" is not null + And the event "threads.0.type" equals "android" + And the error payload field "events.0.threads.0.stacktrace" is a non-empty array + And the event "threads.0.stacktrace.0.method" is not null + And the event "threads.0.stacktrace.0.file" is not null + And the event "threads.0.stacktrace.0.lineNumber" is not null + + # Metadata validation + And the event "metaData.custom.global" equals "present in global metadata" + And the event "metaData.custom.local" equals "present in local metadata" diff --git a/features/smoke_tests/handled.feature b/features/smoke_tests/02_handled.feature similarity index 100% rename from features/smoke_tests/handled.feature rename to features/smoke_tests/02_handled.feature diff --git a/features/smoke_tests/sessions.feature b/features/smoke_tests/03_sessions.feature similarity index 100% rename from features/smoke_tests/sessions.feature rename to features/smoke_tests/03_sessions.feature diff --git a/features/smoke_tests/unhandled.feature b/features/smoke_tests/04_unhandled.feature similarity index 79% rename from features/smoke_tests/unhandled.feature rename to features/smoke_tests/04_unhandled.feature index 8ea97b62a5..5da265efaf 100644 --- a/features/smoke_tests/unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -286,85 +286,3 @@ Feature: Unhandled smoke tests # Breadcrumbs And the event has a "manual" breadcrumb named "CXXExceptionSmokeScenario" - @skip_android_8_1 - Scenario: ANR detection - When I clear any error dialogue - And I run "JvmAnrLoopScenario" - And I wait for 2 seconds - And I tap the back-button 3 times - And I wait to receive an error - - # Exception details - Then the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier - And the error payload field "events" is an array with 1 elements - And the exception "errorClass" equals "ANR" - And the exception "message" starts with " Input dispatching timed out" - And the exception "type" equals "android" - And the event "unhandled" is true - And the event "severity" equals "error" - And the event "severityReason.type" equals "anrError" - And the event "severityReason.unhandledOverridden" is false - - # Stacktrace validation - And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array - And the event "exceptions.0.stacktrace.0.method" is not null - And the event "exceptions.0.stacktrace.0.file" is not null - And the event "exceptions.0.stacktrace.0.lineNumber" is not null - - # App data - And the event binary arch field is valid - And the event "app.buildUUID" equals "test-7.5.3" - And the event "app.id" equals "com.bugsnag.android.mazerunner" - And the event "app.releaseStage" equals "mazerunner" - And the event "app.type" equals "android" - And the event "app.version" equals "1.1.14" - And the event "app.versionCode" equals 34 - And the error payload field "events.0.app.duration" is an integer - And the error payload field "events.0.app.durationInForeground" is an integer - And the event "app.inForeground" is true - And the event "app.isLaunching" is false - And the event "metaData.app.name" equals "MazeRunner" - - # Device data - And the error payload field "events.0.device.cpuAbi" is a non-empty array - And the event "device.jailbroken" is false - And the event "device.id" is not null - And the error payload field "events.0.device.id" is stored as the value "device_id" - And the event "device.locale" is not null - And the event "device.manufacturer" is not null - And the event "device.model" is not null - And the event "device.osName" equals "android" - And the event "device.osVersion" is not null - And the event "device.runtimeVersions" is not null - And the event "device.runtimeVersions.androidApiLevel" is not null - And the event "device.runtimeVersions.osBuild" is not null - And the error payload field "events.0.device.totalMemory" is greater than 0 - And the error payload field "events.0.device.freeDisk" is greater than 0 - And the error payload field "events.0.device.freeMemory" is greater than 0 - And the event "device.orientation" equals "portrait" - And the event "device.time" is a timestamp - And the event "metaData.device.locationStatus" is not null - And the event "metaData.device.emulator" is false - And the event "metaData.device.networkAccess" is not null - And the event "metaData.device.screenDensity" is not null - And the event "metaData.device.dpi" is not null - And the event "metaData.device.screenResolution" is not null - And the event "metaData.device.brand" is not null - - # User - And the event "user.id" is not null - And the error payload field "events.0.user.id" equals the stored value "device_id" - - # Threads validation - And the error payload field "events.0.threads" is a non-empty array - And the error payload field "events.0.threads.0.id" is an integer - And the event "threads.0.name" is not null - And the event "threads.0.type" equals "android" - And the error payload field "events.0.threads.0.stacktrace" is a non-empty array - And the event "threads.0.stacktrace.0.method" is not null - And the event "threads.0.stacktrace.0.file" is not null - And the event "threads.0.stacktrace.0.lineNumber" is not null - - # Metadata validation - And the event "metaData.custom.global" equals "present in global metadata" - And the event "metaData.custom.local" equals "present in local metadata" diff --git a/gradle.properties b/gradle.properties index aa8c0a4eb4..e91506b8f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=5.25.0 +VERSION_NAME=5.26.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git