diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfc8b48760..fe4891c80d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## TBD
+
+### Enhancements
+
+* Added `Session.apiKey` so that it can be changed in an `OnSessionCallback`
+ [#1855](https://github.com/bugsnag/bugsnag-android/pull/1855)
+
+### Bug fixes
+
+* Prevent rare app crash while migrating old `SharedPreferences` data from older versions of `bugsnag-android`
+ [#1860](https://github.com/bugsnag/bugsnag-android/pull/1860)
+
+* Prevent free memory calculation from potentially crashing the app when `ActivityManager` cannot be reached.
+ [#1861](https://github.com/bugsnag/bugsnag-android/pull/1861)
+
## 5.30.0 (2023-05-11)
### Enhancements
diff --git a/Gemfile.lock b/Gemfile.lock
index 408eb3158c..cd6739c256 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -88,6 +88,8 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
multi_test (0.1.2)
+ nokogiri (1.15.1-arm64-darwin)
+ racc (~> 1.4)
nokogiri (1.15.1-x86_64-darwin)
racc (~> 1.4)
optimist (3.0.1)
@@ -121,6 +123,7 @@ GEM
rexml
PLATFORMS
+ arm64-darwin-22
x86_64-darwin-20
DEPENDENCIES
diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api
index 2fb22c0674..0dd39b2bee 100644
--- a/bugsnag-android-core/api/bugsnag-android-core.api
+++ b/bugsnag-android-core/api/bugsnag-android-core.api
@@ -573,11 +573,13 @@ public abstract interface class com/bugsnag/android/Plugin {
}
public final class com/bugsnag/android/Session : com/bugsnag/android/JsonStream$Streamable, com/bugsnag/android/UserAware {
+ public fun getApiKey ()Ljava/lang/String;
public fun getApp ()Lcom/bugsnag/android/App;
public fun getDevice ()Lcom/bugsnag/android/Device;
public fun getId ()Ljava/lang/String;
public fun getStartedAt ()Ljava/util/Date;
public fun getUser ()Lcom/bugsnag/android/User;
+ public fun setApiKey (Ljava/lang/String;)V
public fun setId (Ljava/lang/String;)V
public fun setStartedAt (Ljava/util/Date;)V
public fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml
index 53c0a3c858..fb7824ddb0 100644
--- a/bugsnag-android-core/detekt-baseline.xml
+++ b/bugsnag-android-core/detekt-baseline.xml
@@ -45,12 +45,15 @@
SwallowedException:BugsnagEventMapper.kt$BugsnagEventMapper$catch (pe: IllegalArgumentException) { ndkDateFormatHolder.get()!!.parse(this) ?: throw IllegalArgumentException("cannot parse date $this") }
SwallowedException:ConnectivityCompat.kt$ConnectivityLegacy$catch (e: NullPointerException) { // in some rare cases we get a remote NullPointerException via Parcel.readException null }
SwallowedException:ContextExtensions.kt$catch (exc: RuntimeException) { null }
+ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (e: Throwable) { null }
+ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (e: Throwable) { return null }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exc: Exception) { false }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get battery status") }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get locationStatus") }
SwallowedException:DeviceIdFilePersistence.kt$DeviceIdFilePersistence$catch (exc: OverlappingFileLockException) { Thread.sleep(FILE_LOCK_WAIT_MS) }
SwallowedException:JsonHelperTest.kt$JsonHelperTest$catch (e: IllegalArgumentException) { didThrow = true }
SwallowedException:PluginClient.kt$PluginClient$catch (exc: ClassNotFoundException) { if (isWarningEnabled) { logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.") } null }
+ SwallowedException:SharedPrefMigrator.kt$SharedPrefMigrator$catch (e: RuntimeException) { null }
ThrowsCount:JsonHelper.kt$JsonHelper$ fun jsonToLong(value: Any?): Long?
TooManyFunctions:ConfigInternal.kt$ConfigInternal : CallbackAwareMetadataAwareUserAwareFeatureFlagAware
TooManyFunctions:DeviceDataCollector.kt$DeviceDataCollector
diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java
index 534218f2fb..077bdce16e 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java
+++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java
@@ -58,7 +58,7 @@ static Client generateClient() {
static Session generateSession() {
return new Session("test", new Date(), new User(), false,
- new Notifier(), NoopLogger.INSTANCE);
+ new Notifier(), NoopLogger.INSTANCE, "TEST APIKEY");
}
static Event generateEvent() {
diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV1PayloadTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV1PayloadTest.java
index e5e5b77861..3ad76fd6d1 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV1PayloadTest.java
+++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV1PayloadTest.java
@@ -47,7 +47,7 @@ void writeLegacyFile(Session session) throws IOException {
*/
@Test
public void testSessionFromFile() throws Exception {
- Session payload = new Session(file, new Notifier(), NoopLogger.INSTANCE);
+ Session payload = new Session(file, new Notifier(), NoopLogger.INSTANCE, "TEST APIKEY");
payload.setApp(generateApp());
payload.setDevice(generateDevice());
diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV2PayloadTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV2PayloadTest.java
index ac49f1ffd9..679296b927 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV2PayloadTest.java
+++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/SessionV2PayloadTest.java
@@ -52,7 +52,7 @@ public void testSessionFromFile() throws Exception {
session.toStream(stream);
out.flush();
- Session payload = new Session(file, new Notifier(), NoopLogger.INSTANCE);
+ Session payload = new Session(file, new Notifier(), NoopLogger.INSTANCE, "TEST APIKEY");
JSONObject obj = BugsnagTestUtils.streamableToJson(payload);
JSONObject rootNode = obj.getJSONArray("sessions").getJSONObject(0);
assertNotNull(rootNode);
@@ -71,7 +71,15 @@ public void testSessionFromFile() throws Exception {
@Test
public void testAutoCapturedOverride() throws Exception {
- session = new Session("id", new Date(), null, false, new Notifier(), NoopLogger.INSTANCE);
+ session = new Session(
+ "id",
+ new Date(),
+ null,
+ false,
+ new Notifier(),
+ NoopLogger.INSTANCE,
+ "TEST APIKEY"
+ );
assertFalse(session.isAutoCaptured());
session.setAutoCaptured(true);
assertTrue(session.isAutoCaptured());
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt
index 1cfdfd90f0..61f74229c5 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt
@@ -66,7 +66,7 @@ internal class BugsnagEventMapper(
// populate session
val sessionMap = map["session"] as? Map
sessionMap?.let {
- event.session = Session(it, logger)
+ event.session = Session(it, logger, apiKey)
}
// populate threads
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
index a7755a2edc..493763a493 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
@@ -242,21 +242,26 @@ internal class DeviceDataCollector(
/**
* Get the amount of memory remaining on the device
*/
- private fun calculateFreeMemory(): Long? {
+ fun calculateFreeMemory(): Long? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- val freeMemory = appContext.getActivityManager()
- ?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
- ?.availMem
-
- if (freeMemory != null) {
- return freeMemory
+ try {
+ val freeMemory = appContext.getActivityManager()
+ ?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
+ ?.availMem
+ if (freeMemory != null) {
+ return freeMemory
+ }
+ } catch (e: Throwable) {
+ return null
}
}
- return runCatching {
+ return try {
@Suppress("PrivateApi")
AndroidProcess::class.java.getDeclaredMethod("getFreeMemory").invoke(null) as Long?
- }.getOrNull()
+ } catch (e: Throwable) {
+ null
+ }
}
/**
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java
index 23dc782e2c..8e0d46e5a5 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java
@@ -34,17 +34,19 @@ public final class Session implements JsonStream.Streamable, UserAware {
private final AtomicBoolean tracked = new AtomicBoolean(false);
final AtomicBoolean isPaused = new AtomicBoolean(false);
+ private String apiKey;
+
static Session copySession(Session session) {
Session copy = new Session(session.id, session.startedAt, session.user,
session.unhandledCount.get(), session.handledCount.get(), session.notifier,
- session.logger);
+ session.logger, session.getApiKey());
copy.tracked.set(session.tracked.get());
copy.autoCaptured.set(session.isAutoCaptured());
return copy;
}
- Session(Map map, Logger logger) {
- this(null, null, logger);
+ Session(Map map, Logger logger, String apiKey) {
+ this(null, null, logger, apiKey);
setId((String) map.get("id"));
String timestamp = (String) map.get("startedAt");
@@ -61,25 +63,28 @@ static Session copySession(Session session) {
}
Session(String id, Date startedAt, User user, boolean autoCaptured,
- Notifier notifier, Logger logger) {
- this(null, notifier, logger);
+ Notifier notifier, Logger logger, String apiKey) {
+ this(null, notifier, logger, apiKey);
this.id = id;
this.startedAt = new Date(startedAt.getTime());
this.user = user;
this.autoCaptured.set(autoCaptured);
+ this.apiKey = apiKey;
}
Session(String id, Date startedAt, User user, int unhandledCount, int handledCount,
- Notifier notifier, Logger logger) {
- this(id, startedAt, user, false, notifier, logger);
+ Notifier notifier, Logger logger, String apiKey) {
+ this(id, startedAt, user, false, notifier, logger, apiKey);
this.unhandledCount.set(unhandledCount);
this.handledCount.set(handledCount);
this.tracked.set(true);
+ this.apiKey = apiKey;
}
- Session(File file, Notifier notifier, Logger logger) {
+ Session(File file, Notifier notifier, Logger logger, String apiKey) {
this.file = file;
this.logger = logger;
+ this.apiKey = SessionFilenameInfo.findApiKeyInFilename(file, apiKey);
if (notifier != null) {
Notifier copy = new Notifier(notifier.getName(),
notifier.getVersion(), notifier.getUrl());
@@ -261,4 +266,25 @@ void serializeSessionInfo(@NonNull JsonStream writer) throws IOException {
writer.name("user").value(user);
writer.endObject();
}
+
+ /**
+ * The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag
+ * is initialized, you may choose to send certain sessions to a different Bugsnag project.
+ */
+ public void setApiKey(@NonNull String apiKey) {
+ if (apiKey != null) {
+ this.apiKey = apiKey;
+ } else {
+ logNull("apiKey");
+ }
+ }
+
+ /**
+ * The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag
+ * is initialized, you may choose to send certain sessions to a different Bugsnag project.
+ */
+ @NonNull
+ public String getApiKey() {
+ return apiKey;
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
index 04efa4081b..554b1ee0d7 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
@@ -1,5 +1,6 @@
package com.bugsnag.android
+import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.util.UUID
@@ -11,12 +12,13 @@ import java.util.UUID
* timestamp - to sort error reports by time of capture
*/
internal data class SessionFilenameInfo(
+ var apiKey: String,
val timestamp: Long,
- val uuid: String,
+ val uuid: String
) {
fun encode(): String {
- return toFilename(timestamp, uuid)
+ return toFilename(apiKey, timestamp, uuid)
}
internal companion object {
@@ -27,29 +29,63 @@ internal data class SessionFilenameInfo(
* Generates a filename for the session in the format
* "[UUID][timestamp]_v2.json"
*/
- fun toFilename(timestamp: Long, uuid: String): String {
- return "${uuid}${timestamp}_v2.json"
+ fun toFilename(apiKey: String, timestamp: Long, uuid: String): String {
+ return "${apiKey}_${uuid}${timestamp}_v3.json"
}
@JvmStatic
- fun defaultFilename(): String {
- return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString())
+ fun defaultFilename(
+ obj: Any,
+ config: ImmutableConfig
+ ): SessionFilenameInfo {
+ val sanitizedApiKey = when (obj) {
+ is Session -> obj.apiKey
+ else -> config.apiKey
+ }
+
+ return SessionFilenameInfo(
+ sanitizedApiKey,
+ System.currentTimeMillis(),
+ UUID.randomUUID().toString()
+ )
}
- fun fromFile(file: File): SessionFilenameInfo {
+ fun fromFile(file: File, defaultApiKey: String): SessionFilenameInfo {
return SessionFilenameInfo(
+ findApiKeyInFilename(file, defaultApiKey),
findTimestampInFilename(file),
findUuidInFilename(file)
)
}
- private fun findUuidInFilename(file: File): String {
- return file.name.substring(0, uuidLength - 1)
+ @JvmStatic
+ fun findUuidInFilename(file: File): String {
+ var fileName = file.name
+ if (isFileV3(file)) {
+ fileName = file.name.substringAfter('_')
+ }
+ return fileName.takeIf { it.length >= uuidLength }?.take(uuidLength) ?: ""
}
@JvmStatic
fun findTimestampInFilename(file: File): Long {
- return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1
+ var fileName = file.name
+ if (isFileV3(file)) {
+ fileName = file.name.substringAfter('_')
+ }
+ return fileName.drop(findUuidInFilename(file).length)
+ .substringBefore('_')
+ .toLongOrNull() ?: -1
}
+
+ @JvmStatic
+ fun findApiKeyInFilename(file: File?, defaultApiKey: String): String {
+ if (file == null || !isFileV3(file)) {
+ return defaultApiKey
+ }
+ return file.name.substringBefore('_').takeUnless { it.isEmpty() } ?: defaultApiKey
+ }
+
+ internal fun isFileV3(file: File): Boolean = file.name.endsWith("_v3.json")
}
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java
index a0238a5feb..87e3524e28 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java
@@ -17,6 +17,7 @@
*/
class SessionStore extends FileStore {
+ private final ImmutableConfig config;
static final Comparator SESSION_COMPARATOR = new Comparator() {
@Override
public int compare(File lhs, File rhs) {
@@ -43,12 +44,15 @@ public int compare(File lhs, File rhs) {
SESSION_COMPARATOR,
logger,
delegate);
+ this.config = config;
}
@NonNull
@Override
String getFilename(Object object) {
- return SessionFilenameInfo.defaultFilename();
+ SessionFilenameInfo sessionInfo
+ = SessionFilenameInfo.defaultFilename(object, config);
+ return sessionInfo.encode();
}
public boolean isTooOld(File file) {
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java
index f620b32416..a2c1d90408 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java
@@ -89,7 +89,10 @@ Session startNewSession(@NonNull Date date, @Nullable User user,
return null;
}
String id = UUID.randomUUID().toString();
- Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger);
+ Session session = new Session(
+ id, date, user, autoCaptured,
+ client.getNotifier(), logger, configuration.getApiKey()
+ );
if (trackSessionIfNeeded(session)) {
return session;
} else {
@@ -157,7 +160,7 @@ Session registerExistingSession(@Nullable Date date, @Nullable String sessionId,
Session session = null;
if (date != null && sessionId != null) {
session = new Session(sessionId, date, user, unhandledCount, handledCount,
- client.getNotifier(), logger);
+ client.getNotifier(), logger, configuration.getApiKey());
notifySessionStartObserver(session);
} else {
updateState(StateEvent.PauseSession.INSTANCE);
@@ -256,7 +259,9 @@ void flushStoredSessions() {
void flushStoredSession(File storedFile) {
logger.d("SessionTracker#flushStoredSession() - attempting delivery");
- Session payload = new Session(storedFile, client.getNotifier(), logger);
+ Session payload = new Session(
+ storedFile, client.getNotifier(), logger, configuration.getApiKey()
+ );
if (!payload.isV2Payload()) { // collect data here
payload.setApp(client.getAppDataCollector().generateApp());
@@ -330,7 +335,7 @@ void deliverInMemorySession(Session session) {
}
DeliveryStatus deliverSessionPayload(Session payload) {
- DeliveryParams params = configuration.getSessionApiDeliveryParams();
+ DeliveryParams params = configuration.getSessionApiDeliveryParams(payload);
Delivery delivery = configuration.getDelivery();
return delivery.deliver(payload, params);
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
index d664576791..2f7ccc238f 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
@@ -2,32 +2,38 @@ package com.bugsnag.android
import android.annotation.SuppressLint
import android.content.Context
+import android.content.SharedPreferences
/**
* Reads legacy information left in SharedPreferences and migrates it to the new location.
*/
internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence {
- private val prefs = context
- .getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
+ private val prefs: SharedPreferences? =
+ try {
+ context.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
+ } catch (e: RuntimeException) {
+ null
+ }
/**
* This implementation will never create an ID; it will only fetch one if present.
*/
- override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = prefs.getString(INSTALL_ID_KEY, null)
+ override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) =
+ prefs?.getString(INSTALL_ID_KEY, null)
fun loadUser(deviceId: String?) = User(
- prefs.getString(USER_ID_KEY, deviceId),
- prefs.getString(USER_EMAIL_KEY, null),
- prefs.getString(USER_NAME_KEY, null)
+ prefs?.getString(USER_ID_KEY, deviceId),
+ prefs?.getString(USER_EMAIL_KEY, null),
+ prefs?.getString(USER_NAME_KEY, null)
)
- fun hasPrefs() = prefs.contains(INSTALL_ID_KEY)
+ fun hasPrefs() = prefs?.contains(INSTALL_ID_KEY) == true
@SuppressLint("ApplySharedPref")
fun deleteLegacyPrefs() {
if (hasPrefs()) {
- prefs.edit().clear().commit()
+ prefs?.edit()?.clear()?.commit()
}
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
index 07f2dcb835..8ff97eab96 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
@@ -18,6 +18,7 @@ import com.bugsnag.android.EventPayload
import com.bugsnag.android.Logger
import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID
import com.bugsnag.android.NoopLogger
+import com.bugsnag.android.Session
import com.bugsnag.android.Telemetry
import com.bugsnag.android.ThreadSendPolicy
import com.bugsnag.android.errorApiHeaders
@@ -65,8 +66,8 @@ data class ImmutableConfig(
DeliveryParams(endpoints.notify, errorApiHeaders(payload))
@JvmName("getSessionApiDeliveryParams")
- internal fun getSessionApiDeliveryParams() =
- DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey))
+ internal fun getSessionApiDeliveryParams(session: Session) =
+ DeliveryParams(endpoints.sessions, sessionApiHeaders(session.apiKey))
/**
* Returns whether the given throwable should be discarded
diff --git a/bugsnag-android-core/src/sharedTest/java/com/bugsnag/android/BugsnagTestUtils.java b/bugsnag-android-core/src/sharedTest/java/com/bugsnag/android/BugsnagTestUtils.java
index d08c6ed206..24a82a3422 100644
--- a/bugsnag-android-core/src/sharedTest/java/com/bugsnag/android/BugsnagTestUtils.java
+++ b/bugsnag-android-core/src/sharedTest/java/com/bugsnag/android/BugsnagTestUtils.java
@@ -62,7 +62,7 @@ static EventPayload generateEventPayload(ImmutableConfig config) {
static Session generateSession() {
return new Session("test", new Date(), new User(), false,
- new Notifier(), NoopLogger.INSTANCE);
+ new Notifier(), NoopLogger.INSTANCE, "BUGSNAG_API_KEY");
}
static Event generateEvent() {
diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
similarity index 64%
rename from bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt
rename to bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
index 443bf353c6..307151bc20 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
@@ -4,24 +4,34 @@ import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import com.bugsnag.android.internal.BackgroundTaskService
-import org.junit.Ignore
+import org.junit.Assert
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
import java.io.File
import kotlin.concurrent.thread
-@RunWith(MockitoJUnitRunner::class)
+@RunWith(MockitoJUnitRunner.Silent::class)
class DataCollectorTest {
- @Ignore("Disabled until we're able to mock final classes or auto-open classes")
- @Test
- fun testConcurretAccess() {
+ private lateinit var collector: DeviceDataCollector
+
+ @Mock
+ lateinit var context: Context
+
+ @Mock
+ lateinit var logger: Logger
+
+ @Before
+ fun setUp() {
val res = Mockito.mock(Resources::class.java)
- Mockito.`when`(res.configuration).thenReturn(Configuration())
+ `when`(res.configuration).thenReturn(Configuration())
- val collector = DeviceDataCollector(
+ collector = DeviceDataCollector(
Mockito.mock(Connectivity::class.java),
Mockito.mock(Context::class.java),
res,
@@ -33,7 +43,17 @@ class DataCollectorTest {
Mockito.mock(BackgroundTaskService::class.java),
Mockito.mock(Logger::class.java)
)
+ }
+ @Test
+ fun testCalculateFreeMemoryWithException() {
+ `when`(collector.calculateFreeMemory()).thenThrow(RuntimeException())
+ collector.generateDeviceWithState(0)
+ Assert.assertNull(collector.calculateFreeMemory())
+ }
+
+ @Test
+ fun testConcurrentAccess() {
repeat(10) { index ->
collector.addRuntimeVersionInfo("key" + index, "value" + index)
}
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
index 9f7291a4d1..42538a416e 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
@@ -19,6 +19,7 @@ internal class DeliveryDelegateTest {
@Mock
lateinit var eventStore: EventStore
+ private val apiKey = "BUGSNAG_API_KEY"
private val notifier = Notifier()
val config = generateImmutableConfig()
val callbackState = CallbackState()
@@ -40,7 +41,7 @@ internal class DeliveryDelegateTest {
notifier,
BackgroundTaskService()
)
- event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger)
+ event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey)
}
@Test
@@ -59,6 +60,8 @@ internal class DeliveryDelegateTest {
// check session count incremented
assertEquals(1, event.session!!.unhandledCount)
assertEquals(0, event.session!!.handledCount)
+
+ assertEquals("BUGSNAG_API_KEY", event.session!!.apiKey)
}
@Test
@@ -67,7 +70,7 @@ internal class DeliveryDelegateTest {
SeverityReason.REASON_HANDLED_EXCEPTION
)
val event = Event(RuntimeException("Whoops!"), config, state, NoopLogger)
- event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger)
+ event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey)
var msg: StateEvent.NotifyHandled? = null
deliveryDelegate.addObserver(
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt
index 46259c7eaa..0ce163179a 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt
@@ -17,6 +17,12 @@ class DeliveryHeadersTest {
private val sha1Regex = "sha1 [0-9a-f]{40}".toRegex()
+ var file = File.createTempFile(
+ "150450000000053a27e4e-967c-4e5c-91be-2e86f2eb7cdc_v2",
+ "json"
+ )
+ var session = Session(file, Notifier(), NoopLogger, "Test Apikey")
+
@Test
fun computeSha1Digest() {
val payload = generateEventPayload(generateImmutableConfig())
@@ -54,8 +60,8 @@ class DeliveryHeadersTest {
@Test
fun verifySessionApiHeaders() {
val config = generateImmutableConfig()
- val headers = config.getSessionApiDeliveryParams().headers
- assertEquals(config.apiKey, headers["Bugsnag-Api-Key"])
+ val headers = config.getSessionApiDeliveryParams(session).headers
+ assertEquals("Test Apikey", headers["Bugsnag-Api-Key"])
assertEquals("application/json", headers["Content-Type"])
assertNotNull(headers["Bugsnag-Sent-At"])
assertNotNull(headers["Bugsnag-Payload-Version"])
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
index b25ff529fb..61f6899509 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
@@ -17,6 +17,7 @@ internal class EventSerializationTest {
@JvmStatic
@Parameters
fun testCases(): Collection> {
+
return generateSerializationTestCases(
"event",
createEvent(),
@@ -33,7 +34,8 @@ internal class EventSerializationTest {
// session included
createEvent {
val user = User("123", "foo@example.com", "Joe")
- it.session = Session("123", Date(0), user, false, Notifier(), NoopLogger)
+ val apiKey = "BUGSNAG_API_KEY"
+ it.session = Session("123", Date(0), user, false, Notifier(), NoopLogger, apiKey)
},
// threads included
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFacadeTest.java
index d7de6e4b38..26daf94659 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFacadeTest.java
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFacadeTest.java
@@ -14,10 +14,16 @@ public class SessionFacadeTest {
private Session session;
private InterceptingLogger logger;
+ /**
+ * Captures session logs
+ */
@Before
public void setUp() {
logger = new InterceptingLogger();
- session = new Session("123", new Date(0), new User(), true, new Notifier(), logger);
+ session = new Session(
+ "123", new Date(0), new User(),
+ true, new Notifier(), logger, "BUGSNAG_API_KEY"
+ );
}
@Test
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFilenameTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFilenameTest.kt
new file mode 100644
index 0000000000..a49fa83448
--- /dev/null
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionFilenameTest.kt
@@ -0,0 +1,135 @@
+package com.bugsnag.android
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+class SessionFilenameTest {
+ private var config = BugsnagTestUtils.generateImmutableConfig()
+
+ @Test
+ fun getSessionDetailsFromV3FileName() {
+ val apiKey = "TEST APIKEY"
+ val fileName = SessionFilenameInfo(
+ apiKey,
+ 1504255147933,
+ "my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu"
+ ).encode()
+ val file = File(fileName)
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+
+ assertEquals(
+ "TEST APIKEY_my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu1504255147933_v3.json",
+ fileName
+ )
+ assertEquals("TEST APIKEY", sessionInfo.apiKey)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ assertEquals("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getFileDetailsFromV2FileName() {
+ val file = File("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu1504255147933_v2.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu", sessionInfo.uuid)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ assertEquals("5d1ec5bd39a74caa1267142706a7fb21", sessionInfo.apiKey)
+ }
+
+ @Test
+ fun getSessionApiKeyFromV3FileNameWithoutApiKey() {
+ val fileName = SessionFilenameInfo(
+ "",
+ 1504255147933,
+ "my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu"
+ ).encode()
+
+ val file = File(fileName)
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(config.apiKey, sessionInfo.apiKey)
+ assertEquals("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu", sessionInfo.uuid)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ }
+
+ @Test
+ fun getSessionTimeStampFromV3FileNameWithoutUuid() {
+ val apiKey = "TEST APIKEY"
+ val fileName = SessionFilenameInfo(
+ apiKey,
+ 1504255147933,
+ ""
+ ).encode()
+
+ val file = File(fileName)
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals("TEST APIKEY", sessionInfo.apiKey)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ assertEquals("", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getSessionUuidAndTimeStampFromV2FileNameWithoutUuid() {
+ val file = File("1504255147933_v2.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals("", sessionInfo.uuid)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ }
+
+ @Test
+ fun getSessionTimeStampFromV3FileNameWithoutUuidAndApiKey() {
+ val fileName = SessionFilenameInfo(
+ "",
+ 1504255147933,
+ ""
+ ).encode()
+
+ val file = File(fileName)
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(config.apiKey, sessionInfo.apiKey)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ assertEquals("", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getSessionUuidFromV2FileNameWithoutUuidAndApiKey() {
+ val file = File("1504255147933_v2.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals("", sessionInfo.uuid)
+ assertEquals(config.apiKey, sessionInfo.apiKey)
+ assertEquals(1504255147933, sessionInfo.timestamp)
+ }
+
+ @Test
+ fun getSessionTimeStampFromV3FileNameWithoutTimeStamp() {
+ val file = File("_my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu_v3.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(config.apiKey, sessionInfo.apiKey)
+ assertEquals(-1, sessionInfo.timestamp)
+ assertEquals("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getSessionUuidAndTimeStampFromV2FileNameWithoutTimeStamp() {
+ val file = File("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu_v2.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(-1, sessionInfo.timestamp)
+ assertEquals("my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getSessionUuidAndTimeStampFromV2FileNameWithNoTimeStampAndNoUuid() {
+ val file = File("_v2.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(-1, sessionInfo.timestamp)
+ assertEquals("", sessionInfo.uuid)
+ }
+
+ @Test
+ fun getSessionDetailsFromV2FileNameWithNoTimeStampAndNoUuidAndApiKey() {
+ val file = File("_v3.json")
+ val sessionInfo = SessionFilenameInfo.fromFile(file, defaultApiKey = config.apiKey)
+ assertEquals(-1, sessionInfo.timestamp)
+ assertEquals("", sessionInfo.uuid)
+ assertEquals(config.apiKey, sessionInfo.apiKey)
+ }
+}
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionSerializationTest.kt
index 00a221d60a..a124f20f00 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionSerializationTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionSerializationTest.kt
@@ -18,7 +18,7 @@ internal class SessionSerializationTest {
@JvmStatic
@Parameters
fun testCases(): Collection> {
- val session = Session("123", Date(0), User(null, null, null), 1, 0, notifier, NoopLogger)
+ val session = Session("123", Date(0), User(null, null, null), 1, 0, notifier, NoopLogger, "BUGSNAG_API_KEY")
session.app = generateApp()
session.device = generateDevice()
return generateSerializationTestCases("session", session)
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTest.kt
index 5195fb63cc..779b10f8ea 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTest.kt
@@ -23,7 +23,9 @@ class SessionTest {
@Mock
lateinit var app: AppWithState
- private val session = Session("123", Date(0), User(), true, Notifier(), NoopLogger)
+ private val apiKey = "BUGSNAG_API_KEY"
+
+ private var session = Session("123", Date(0), User(), true, Notifier(), NoopLogger, apiKey)
/**
* Verifies that all the fields in session are copied into a new object correctly
@@ -56,6 +58,23 @@ class SessionTest {
assertEquals("foo", session.id)
}
+ @Test
+ fun overrideApiKey() {
+ assertEquals("BUGSNAG_API_KEY", session.apiKey)
+ session.apiKey = "foo"
+ assertEquals("foo", session.apiKey)
+ }
+
+ @Test
+ fun defaultApiKey() {
+ val file = File("_my-uuid-uuuuuuuuuuuuuuuuuuuuuuuuuuuu1504255147933_v3.json")
+ session = Session(
+ "123", Date(0), User(), true, Notifier(), NoopLogger,
+ SessionFilenameInfo.findApiKeyInFilename(file, "Default apikey")
+ )
+ assertEquals("Default apikey", session.apiKey)
+ }
+
@Test
fun overrideStartedAt() {
assertEquals(0, session.startedAt.time)
@@ -90,12 +109,13 @@ class SessionTest {
fun isV2() {
assertFalse(session.isV2Payload)
val file = File("150450000000053a27e4e-967c-4e5c-91be-2e86f2eb7cdc.json")
- assertFalse(Session(file, Notifier(), NoopLogger).isV2Payload)
+ assertFalse(Session(file, Notifier(), NoopLogger, apiKey).isV2Payload)
assertTrue(
Session(
File("150450000000053a27e4e-967c-4e5c-91be-2e86f2eb7cdc_v2.json"),
Notifier(),
- NoopLogger
+ NoopLogger,
+ apiKey
).isV2Payload
)
}
@@ -105,7 +125,7 @@ class SessionTest {
val original = Notifier()
val dep = Notifier("bugsnag-cobol")
original.dependencies = listOf(dep)
- val payload = Session(null, original, NoopLogger)
+ val payload = Session(null, original, NoopLogger, apiKey)
val copy = payload.notifier
assertNotSame(original, copy)
assertNotSame(original.dependencies, copy.dependencies)
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java
index de1cd60c5d..4de50103c6 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java
@@ -106,6 +106,17 @@ public void startNewSession() {
assertNotNull(newSession.getUser());
}
+ @Test
+ public void changeSessionApiKey() {
+ assertNotNull(sessionTracker);
+ assertNull(sessionTracker.getCurrentSession());
+ Date date = new Date();
+ sessionTracker.startNewSession(date, user, false);
+ Session newSession = sessionTracker.getCurrentSession();
+ newSession.setApiKey("Test ApiKey");
+ assertEquals("Test ApiKey", newSession.getApiKey());
+ }
+
@Test
public void startSessionDisabled() {
assertNull(sessionTracker.getCurrentSession());
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt
index 020646542b..21a889aa74 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt
@@ -9,6 +9,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.times
@@ -32,10 +33,24 @@ internal class SharedPrefMigratorTest {
@Before
fun setUp() {
- `when`(context.getSharedPreferences(eq("com.bugsnag.android"), eq(0))).thenReturn(prefs)
+ `when`(context.getSharedPreferences(eq("com.bugsnag.android"), anyInt())).thenReturn(prefs)
prefMigrator = SharedPrefMigrator(context)
}
+ @Test
+ fun nullSharedPreferences() {
+ `when`(context.getSharedPreferences(eq("com.bugsnag.android"), anyInt())).thenReturn(null)
+ prefMigrator = SharedPrefMigrator(context)
+ assertFalse(prefMigrator.hasPrefs())
+ }
+
+ @Test
+ fun gettingSharedPreferencesWithException() {
+ `when`(context.getSharedPreferences(eq("com.bugsnag.android"), anyInt())).thenThrow(RuntimeException())
+ prefMigrator = SharedPrefMigrator(context)
+ assertFalse(prefMigrator.hasPrefs())
+ }
+
@Test
fun nullDeviceId() {
`when`(prefs.getString("install.iud", null)).thenReturn(null)
diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt
index e8ff256ede..9160f43818 100644
--- a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt
+++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt
@@ -38,6 +38,7 @@ internal fun generateSession(): Session {
null,
false,
Notifier(),
- object : Logger {}
+ object : Logger {},
+ null
)
}
diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/SessionApiKeyResetScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/SessionApiKeyResetScenario.kt
new file mode 100644
index 0000000000..2643e0e545
--- /dev/null
+++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/SessionApiKeyResetScenario.kt
@@ -0,0 +1,30 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.Configuration
+import com.bugsnag.android.OnSessionCallback
+
+/**
+ * Reset session apikey.
+ */
+internal class SessionApiKeyResetScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String
+) : Scenario(config, context, eventMetadata) {
+
+ init {
+ config.addOnSession(
+ OnSessionCallback { session ->
+ session.setApiKey("TEST APIKEY")
+ true
+ }
+ )
+ }
+
+ override fun startScenario() {
+ super.startScenario()
+ Bugsnag.startSession()
+ }
+}
diff --git a/features/full_tests/session_tracking.feature b/features/full_tests/session_tracking.feature
index 20bcfd676c..2211bdd92c 100644
--- a/features/full_tests/session_tracking.feature
+++ b/features/full_tests/session_tracking.feature
@@ -42,3 +42,19 @@ Feature: Session Tracking
And the session "user.id" is not null
And the session "user.name" is null
And the session "user.email" is null
+
+ Scenario: Session apikey can be reset
+ When I run "SessionApiKeyResetScenario"
+ And I wait to receive a session
+ Then the session Bugsnag-Integrity header is valid
+ And the session "Bugsnag-Api-Key" header equals "TEST APIKEY"
+ And the session "bugsnag-payload-version" header equals "1.0"
+ And the session "Content-Type" header equals "application/json"
+ And the session "Bugsnag-Sent-At" header is a timestamp
+
+ And the session payload field "notifier.name" equals "Android Bugsnag Notifier"
+ And the session payload field "notifier.url" is not null
+ And the session payload field "notifier.version" is not null
+
+ And the session payload field "app" is not null
+ And the session payload field "device" is not null
diff --git a/features/full_tests/trimming.feature b/features/full_tests/trimming.feature
index 938aa8f384..39b060e678 100644
--- a/features/full_tests/trimming.feature
+++ b/features/full_tests/trimming.feature
@@ -56,17 +56,17 @@ Feature: Excess data is trimmed when the payload is too big
Then I wait to receive an error
And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier
And the exception "message" equals "EventTooBigScenario"
- And the event has 98 breadcrumbs
- And the event "breadcrumbs.97.name" equals "Removed, along with 2 older breadcrumbs, to reduce payload size"
- And the event "usage.system.breadcrumbsRemoved" equals 3
+ And the event has less than 99 breadcrumbs
+ Then the event last breadcrumb has a message that matches the regex "Removed, along with [0-9]+ older breadcrumbs, to reduce payload size"
+ And the event "usage.system.breadcrumbsRemoved" is not null
And the event "usage.system.breadcrumbBytesRemoved" is not null
And I close and relaunch the app
Then I wait to receive an error
And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier
And the exception "message" equals "EventTooBigScenario"
- And the event has 98 breadcrumbs
- And the event "breadcrumbs.97.name" equals "Removed, along with 2 older breadcrumbs, to reduce payload size"
- And the event "usage.system.breadcrumbsRemoved" equals 3
+ And the event has less than 99 breadcrumbs
+ Then the event last breadcrumb has a message that matches the regex "Removed, along with [0-9]+ older breadcrumbs, to reduce payload size"
+ And the event "usage.system.breadcrumbsRemoved" is not null
And the event "usage.system.breadcrumbBytesRemoved" is not null
Scenario: Payload is too big by 3 breadcrumbs, handled exception
@@ -75,9 +75,9 @@ Feature: Excess data is trimmed when the payload is too big
Then I wait to receive an error
And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier
And the exception "message" equals "EventTooBigScenario"
- And the event has 98 breadcrumbs
- And the event "breadcrumbs.97.name" equals "Removed, along with 2 older breadcrumbs, to reduce payload size"
- And the event "usage.system.breadcrumbsRemoved" equals 3
+ And the event has less than 99 breadcrumbs
+ Then the event last breadcrumb has a message that matches the regex "Removed, along with [0-9]+ older breadcrumbs, to reduce payload size"
+ And the event "usage.system.breadcrumbsRemoved" is not null
And the event "usage.system.breadcrumbBytesRemoved" is not null
Scenario: Payload is too big by 3 breadcrumbs, jvm exception
diff --git a/features/steps/android_steps.rb b/features/steps/android_steps.rb
index cba07ef123..ba86c3dc2e 100644
--- a/features/steps/android_steps.rb
+++ b/features/steps/android_steps.rb
@@ -275,3 +275,17 @@ def click_if_present(element)
end
end
end
+
+Then("the event has less than {int} breadcrumb(s)") do |expected|
+ breadcrumbs = Maze::Server.errors.current[:body]['events'].first['breadcrumbs']
+ Maze.check.operator(
+ breadcrumbs&.length || 0, :<, expected,
+ "Expected event to have less '#{expected}' breadcrumbs, but got: #{breadcrumbs}"
+ )
+end
+
+Then("the event last breadcrumb has a message that matches the regex {string}") do |pattern|
+ lastBreadcrumbName = Maze::Server.errors.current[:body]['events'].first['breadcrumbs'].last['name']
+ regex = Regexp.new pattern
+ Maze.check.match regex, lastBreadcrumbName
+end
\ No newline at end of file