Skip to content

Commit

Permalink
Merge pull request #1734 from bugsnag/release/v5.26.0
Browse files Browse the repository at this point in the history
Release v5.26.0
  • Loading branch information
lemnik authored Aug 18, 2022
2 parents b09e46b + a817716 commit 99ecf57
Show file tree
Hide file tree
Showing 50 changed files with 1,310 additions and 168 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -65,7 +89,7 @@ public static String getNativeReportPath() {
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,String> getUser() {
public static Map<String, String> getUser() {
HashMap<String, String> userData = new HashMap<>();
User user = getClient().getUser();
userData.put("id", user.getId());
Expand All @@ -79,8 +103,8 @@ public static Map<String,String> getUser() {
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getApp() {
HashMap<String,Object> data = new HashMap<>();
public static Map<String, Object> getApp() {
HashMap<String, Object> data = new HashMap<>();
AppDataCollector source = getClient().getAppDataCollector();
AppWithState app = source.generateAppWithState();
data.put("version", app.getVersion());
Expand All @@ -103,7 +127,7 @@ public static Map<String,Object> getApp() {
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getDevice() {
public static Map<String, Object> getDevice() {
DeviceDataCollector source = getClient().getDeviceDataCollector();
HashMap<String, Object> deviceData = new HashMap<>(source.getDeviceMetadata());

Expand Down Expand Up @@ -152,9 +176,9 @@ public static List<Breadcrumb> 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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<Stackframe> 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,22 +51,30 @@ internal class NativeInterfaceApiTest {
@Mock
lateinit var eventStore: EventStore

@Captor
lateinit var eventCapture: ArgumentCaptor<Event>

@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", "[email protected]", "Tod"))
`when`(client.getMetadataState()).thenReturn(MetadataState())
}

@Test
Expand All @@ -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)
}
Expand Down Expand Up @@ -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<StackTraceElement>()
)
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)
Expand Down
1 change: 1 addition & 0 deletions bugsnag-plugin-android-ndk/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent)</ID>
<ID>LongMethod:EventMigrationV10Tests.kt$EventMigrationV10Tests$@Test fun testMigrateEventToLatest()</ID>
<ID>LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest()</ID>
<ID>LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest()</ID>
<ID>LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest()</ID>
Expand Down
Loading

0 comments on commit 99ecf57

Please sign in to comment.