diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml
index 78c4dc4bf3..cbef206180 100644
--- a/.buildkite/pipeline.full.yml
+++ b/.buildkite/pipeline.full.yml
@@ -151,7 +151,7 @@ steps:
- "--exclude=features/full_tests/[^a-k].*.feature"
- "--app=/app/build/fixture-r19.apk"
- "--farm=bs"
- - "--device=ANDROID_7_1_SAMSUNG_GALAXY_NOTE_8"
+ - "--device=ANDROID_7_1"
- "--fail-fast"
env:
TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r19"
@@ -176,7 +176,7 @@ steps:
- "--exclude=features/full_tests/[^l-z].*.feature"
- "--app=/app/build/fixture-r19.apk"
- "--farm=bs"
- - "--device=ANDROID_7_1_SAMSUNG_GALAXY_NOTE_8"
+ - "--device=ANDROID_7_1"
- "--fail-fast"
env:
TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r19"
@@ -388,6 +388,56 @@ steps:
concurrency_group: 'browserstack-app'
concurrency_method: eager
+ - label: ':android: Android 13 Beta NDK r21 end-to-end tests - batch 1'
+ depends_on: "fixture-r21"
+ timeout_in_minutes: 60
+ plugins:
+ artifacts#v1.2.0:
+ download:
+ - "build/fixture-r21.apk"
+ - "build/fixture-r21/*"
+ upload: "maze_output/failed/**/*"
+ docker-compose#v3.7.0:
+ pull: android-maze-runner
+ run: android-maze-runner
+ command:
+ - "features/full_tests"
+ - "--exclude=features/full_tests/[^a-k].*.feature"
+ - "--app=/app/build/fixture-r21.apk"
+ - "--farm=bs"
+ - "--device=ANDROID_13_0_BETA"
+ - "--fail-fast"
+ env:
+ TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21"
+ concurrency: 24
+ concurrency_group: 'browserstack-app'
+ concurrency_method: eager
+
+ - label: ':android: Android 13 Beta NDK r21 end-to-end tests - batch 2'
+ depends_on: "fixture-r21"
+ timeout_in_minutes: 60
+ plugins:
+ artifacts#v1.2.0:
+ download:
+ - "build/fixture-r21.apk"
+ - "build/fixture-r21/*"
+ upload: "maze_output/failed/**/*"
+ docker-compose#v3.7.0:
+ pull: android-maze-runner
+ run: android-maze-runner
+ command:
+ - "features/full_tests"
+ - "--exclude=features/full_tests/[^l-z].*.feature"
+ - "--app=/app/build/fixture-r21.apk"
+ - "--farm=bs"
+ - "--device=ANDROID_13_0_BETA"
+ - "--fail-fast"
+ env:
+ TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21"
+ concurrency: 24
+ concurrency_group: 'browserstack-app'
+ concurrency_method: eager
+
# If there is a tag present activate a manual publishing step
- block: 'Trigger package publish'
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
index 755b4bd275..7683134ebc 100644
--- a/.buildkite/pipeline.yml
+++ b/.buildkite/pipeline.yml
@@ -144,7 +144,7 @@ steps:
- "features/smoke_tests"
- "--app=/app/build/fixture-r19.apk"
- "--farm=bs"
- - "--device=ANDROID_7_1_SAMSUNG_GALAXY_NOTE_8"
+ - "--device=ANDROID_7_1"
- "--fail-fast"
concurrency: 24
concurrency_group: 'browserstack-app'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e4ff78f0b..2f48697e18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 5.27.0 (2022-10-06)
+
+### Enhancements
+
+* Setting `Configuration.attemptDeliveryOnCrash` will cause Bugsnag to attempt error delivery during some crashes.
+ Use of this feature is discouraged, see the method JavaDoc for more information.
+ [#1749](https://github.com/bugsnag/bugsnag-android/pull/1749)
+
## 5.26.0 (2022-08-18)
### Enhancements
diff --git a/Gemfile b/Gemfile
index 668621b88c..0a6c27fdb9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,7 +4,7 @@ source "https://rubygems.org"
#gem 'bugsnag-maze-runner', path: '../maze-runner'
# Or a specific release:
-gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.15.0'
+gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.26.1'
# Or follow master:
# gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner'
diff --git a/Gemfile.lock b/Gemfile.lock
index 97f992e7a7..f4da702275 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,9 @@
GIT
remote: https://github.com/bugsnag/maze-runner
- revision: d71ba8a1b770e86800b4e9f6782a63fef019a989
- tag: v6.15.0
+ revision: 8cbfd525f4534afa4560bc8e4fbb80e92718cd80
+ tag: v6.26.1
specs:
- bugsnag-maze-runner (6.15.0)
+ bugsnag-maze-runner (6.26.1)
appium_lib (~> 11.2.0)
bugsnag (~> 6.24)
cucumber (~> 7.1)
@@ -81,7 +81,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
multi_test (0.1.2)
- nokogiri (1.13.6-x86_64-darwin)
+ nokogiri (1.13.8-x86_64-darwin)
racc (~> 1.4)
optimist (3.0.1)
os (1.0.1)
diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml
index 517ebb8201..bb85130ef8 100644
--- a/bugsnag-android-core/detekt-baseline.xml
+++ b/bugsnag-android-core/detekt-baseline.xml
@@ -2,6 +2,7 @@
+ ComplexMethod:ConfigInternal.kt$ConfigInternal$fun getConfigDifferences(): Map<String, Any>
ImplicitDefaultLocale:DeliveryHeaders.kt$String.format("%02x", byte)
LongParameterList:App.kt$App$( /** * The architecture of the running application binary */ var binaryArch: String?, /** * The package name of the application */ var id: String?, /** * The release stage set in [Configuration.releaseStage] */ var releaseStage: String?, /** * The version of the application set in [Configuration.version] */ var version: String?, /** The revision ID from the manifest (React Native apps only) */ var codeBundleId: String?, /** * The unique identifier for the build of the application set in [Configuration.buildUuid] */ var buildUuid: String?, /** * The application type set in [Configuration#version] */ var type: String?, /** * The version code of the application set in [Configuration.versionCode] */ var versionCode: Number? )
LongParameterList:AppDataCollector.kt$AppDataCollector$( appContext: Context, private val packageManager: PackageManager?, private val config: ImmutableConfig, private val sessionTracker: SessionTracker, private val activityManager: ActivityManager?, private val launchCrashTracker: LaunchCrashTracker, private val memoryTrimState: MemoryTrimState )
@@ -47,6 +48,7 @@
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:InternalMetrics.kt$InternalMetrics$catch (exc: Exception) { null }
SwallowedException:JsonHelperTest.kt$JsonHelperTest$catch (e: IllegalArgumentException) { didThrow = true }
SwallowedException:PluginClient.kt$PluginClient$catch (exc: ClassNotFoundException) { logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.") null }
ThrowsCount:JsonHelper.kt$JsonHelper$ fun jsonToLong(value: Any?): Long?
diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ManifestConfigLoaderTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ManifestConfigLoaderTest.kt
index 9b1566db6f..3ebe2f3b96 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ManifestConfigLoaderTest.kt
+++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ManifestConfigLoaderTest.kt
@@ -92,6 +92,7 @@ class ManifestConfigLoaderTest {
putBoolean("com.bugsnag.android.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY", false)
putString("com.bugsnag.android.APP_TYPE", "react-native")
putString("com.bugsnag.android.CODE_BUNDLE_ID", "123")
+ putBoolean("com.bugsnag.android.ATTEMPT_DELIVERY_ON_CRASH", true)
}
val config = configLoader.load(data, null)
@@ -128,6 +129,7 @@ class ManifestConfigLoaderTest {
assertEquals(launchDurationMillis, 7000)
assertFalse(sendLaunchCrashesSynchronously)
assertEquals("react-native", appType)
+ assertTrue(isAttemptDeliveryOnCrash)
}
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt
index ae8db30b52..2aa375226d 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt
@@ -1,6 +1,6 @@
package com.bugsnag.android
-import com.bugsnag.android.internal.dag.ConfigModule
+import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.dag.DependencyModule
/**
@@ -8,15 +8,13 @@ import com.bugsnag.android.internal.dag.DependencyModule
* class is responsible for creating classes which track the current breadcrumb/metadata state.
*/
internal class BugsnagStateModule(
- configModule: ConfigModule,
+ cfg: ImmutableConfig,
configuration: Configuration
) : DependencyModule() {
- private val cfg = configModule.config
-
val clientObservable = ClientObservable()
- val callbackState = configuration.impl.callbackState.copy()
+ val callbackState = configuration.impl.callbackState
val contextState = ContextState().apply {
if (configuration.context != null) {
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/CallbackState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/CallbackState.kt
index 39218f7d5c..facd203e80 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/CallbackState.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/CallbackState.kt
@@ -1,5 +1,7 @@
package com.bugsnag.android
+import com.bugsnag.android.internal.InternalMetrics
+import com.bugsnag.android.internal.InternalMetricsNoop
import java.util.concurrent.CopyOnWriteArrayList
internal data class CallbackState(
@@ -9,36 +11,66 @@ internal data class CallbackState(
val onSendTasks: MutableCollection = CopyOnWriteArrayList()
) : CallbackAware {
+ private var internalMetrics: InternalMetrics = InternalMetricsNoop()
+
+ companion object {
+ private const val onBreadcrumbName = "onBreadcrumb"
+ private const val onErrorName = "onError"
+ private const val onSendName = "onSendError"
+ private const val onSessionName = "onSession"
+ }
+
+ fun setInternalMetrics(metrics: InternalMetrics) {
+ internalMetrics = metrics
+ internalMetrics.setCallbackCounts(getCallbackCounts())
+ }
+
override fun addOnError(onError: OnErrorCallback) {
- onErrorTasks.add(onError)
+ if (onErrorTasks.add(onError)) {
+ internalMetrics.notifyAddCallback(onErrorName)
+ }
}
override fun removeOnError(onError: OnErrorCallback) {
- onErrorTasks.remove(onError)
+ if (onErrorTasks.remove(onError)) {
+ internalMetrics.notifyRemoveCallback(onErrorName)
+ }
}
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
- onBreadcrumbTasks.add(onBreadcrumb)
+ if (onBreadcrumbTasks.add(onBreadcrumb)) {
+ internalMetrics.notifyAddCallback(onBreadcrumbName)
+ }
}
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
- onBreadcrumbTasks.remove(onBreadcrumb)
+ if (onBreadcrumbTasks.remove(onBreadcrumb)) {
+ internalMetrics.notifyRemoveCallback(onBreadcrumbName)
+ }
}
override fun addOnSession(onSession: OnSessionCallback) {
- onSessionTasks.add(onSession)
+ if (onSessionTasks.add(onSession)) {
+ internalMetrics.notifyAddCallback(onSessionName)
+ }
}
override fun removeOnSession(onSession: OnSessionCallback) {
- onSessionTasks.remove(onSession)
+ if (onSessionTasks.remove(onSession)) {
+ internalMetrics.notifyRemoveCallback(onSessionName)
+ }
}
fun addOnSend(onSend: OnSendCallback) {
- onSendTasks.add(onSend)
+ if (onSendTasks.add(onSend)) {
+ internalMetrics.notifyAddCallback(onSendName)
+ }
}
fun removeOnSend(onSend: OnSendCallback) {
- onSendTasks.remove(onSend)
+ if (onSendTasks.remove(onSend)) {
+ internalMetrics.notifyRemoveCallback(onSendName)
+ }
}
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
@@ -120,4 +152,13 @@ internal data class CallbackState(
onSessionTasks = onSessionTasks,
onSendTasks = onSendTasks
)
+
+ private fun getCallbackCounts(): Map {
+ return hashMapOf().also { map ->
+ if (onBreadcrumbTasks.count() > 0) map[onBreadcrumbName] = onBreadcrumbTasks.count()
+ if (onErrorTasks.count() > 0) map[onErrorName] = onErrorTasks.count()
+ if (onSendTasks.count() > 0) map[onSendName] = onSendTasks.count()
+ if (onSessionTasks.count() > 0) map[onSessionName] = onSessionTasks.count()
+ }
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
index c91becd799..bd532bc4d3 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
@@ -3,6 +3,9 @@
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
import com.bugsnag.android.internal.ImmutableConfig;
+import com.bugsnag.android.internal.InternalMetrics;
+import com.bugsnag.android.internal.InternalMetricsImpl;
+import com.bugsnag.android.internal.InternalMetricsNoop;
import com.bugsnag.android.internal.StateObserver;
import com.bugsnag.android.internal.dag.ConfigModule;
import com.bugsnag.android.internal.dag.ContextModule;
@@ -16,7 +19,6 @@
import androidx.annotation.VisibleForTesting;
import kotlin.Unit;
-import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import java.io.File;
@@ -49,9 +51,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
final MetadataState metadataState;
final FeatureFlagState featureFlagState;
+ private final InternalMetrics internalMetrics;
private final ContextState contextState;
private final CallbackState callbackState;
private final UserState userState;
+ private final Map configDifferences;
final Context appContext;
@@ -140,7 +144,18 @@ public Unit invoke(Boolean hasConnection, String networkState) {
ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity);
immutableConfig = configModule.getConfig();
logger = immutableConfig.getLogger();
- warnIfNotAppContext(androidContext);
+
+ if (!(androidContext instanceof Application)) {
+ logger.w("You should initialize Bugsnag from the onCreate() callback of your "
+ + "Application subclass, as this guarantees errors are captured as early "
+ + "as possible. "
+ + "If a custom Application subclass is not possible in your app then you "
+ + "should suppress this warning by passing the Application context instead: "
+ + "Bugsnag.start(context.getApplicationContext()). "
+ + "For further info see: "
+ + "https://docs.bugsnag.com/platforms/android/#basic-configuration");
+
+ }
// setup storage as soon as possible
final StorageModule storageModule = new StorageModule(appContext,
@@ -148,7 +163,7 @@ public Unit invoke(Boolean hasConnection, String networkState) {
// setup state trackers for bugsnag
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(
- configModule, configuration);
+ immutableConfig, configuration);
clientObservable = bugsnagStateModule.getClientObservable();
callbackState = bugsnagStateModule.getCallbackState();
breadcrumbState = bugsnagStateModule.getBreadcrumbState();
@@ -180,8 +195,6 @@ public Unit invoke(Boolean hasConnection, String networkState) {
userState = storageModule.getUserStore().load(configuration.getUser());
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
- registerLifecycleCallbacks();
-
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
callbackState);
@@ -191,33 +204,25 @@ public Unit invoke(Boolean hasConnection, String networkState) {
deliveryDelegate = new DeliveryDelegate(logger, eventStore,
immutableConfig, callbackState, notifier, bgTaskService);
- // Install a default exception handler with this client
exceptionHandler = new ExceptionHandler(this, logger);
- if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
- exceptionHandler.install();
- }
// load last run info
lastRunInfoStore = storageModule.getLastRunInfoStore();
lastRunInfo = storageModule.getLastRunInfo();
- // initialise plugins before attempting to flush any errors
- loadPlugins(configuration);
+ Set userPlugins = configuration.getPlugins();
+ pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
- // Flush any on-disk errors and sessions
- eventStore.flushOnLaunch();
- eventStore.flushAsync();
- sessionTracker.flushAsync();
+ if (configuration.getTelemetry().contains(Telemetry.USAGE)) {
+ internalMetrics = new InternalMetricsImpl();
+ } else {
+ internalMetrics = new InternalMetricsNoop();
+ }
- // register listeners for system events in the background.
+ configDifferences = configuration.impl.getConfigDifferences();
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
- registerComponentCallbacks();
- registerListenersInBackground();
- // leave auto breadcrumb
- Map data = Collections.emptyMap();
- leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
- logger.d("Bugsnag loaded");
+ start();
}
@VisibleForTesting
@@ -266,6 +271,42 @@ public Unit invoke(Boolean hasConnection, String networkState) {
this.lastRunInfo = null;
this.exceptionHandler = exceptionHandler;
this.notifier = notifier;
+ internalMetrics = new InternalMetricsNoop();
+ configDifferences = new HashMap<>();
+ }
+
+ private void start() {
+ if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
+ exceptionHandler.install();
+ }
+
+ // Initialise plugins before attempting anything else
+ NativeInterface.setClient(Client.this);
+ pluginClient.loadPlugins(Client.this);
+ NdkPluginCaller.INSTANCE.setNdkPlugin(pluginClient.getNdkPlugin());
+ if (immutableConfig.getTelemetry().contains(Telemetry.USAGE)) {
+ NdkPluginCaller.INSTANCE.setInternalMetricsEnabled(true);
+ }
+
+ // Flush any on-disk errors and sessions
+ eventStore.flushOnLaunch();
+ eventStore.flushAsync();
+ sessionTracker.flushAsync();
+
+ // These call into NdkPluginCaller to sync with the native side, so they must happen later
+ internalMetrics.setConfigDifferences(configDifferences);
+ callbackState.setInternalMetrics(internalMetrics);
+
+ // Register listeners for system events in the background
+ registerLifecycleCallbacks();
+ registerComponentCallbacks();
+ registerListenersInBackground();
+
+ // Leave auto breadcrumb
+ Map data = Collections.emptyMap();
+ leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
+
+ logger.d("Bugsnag loaded");
}
void registerLifecycleCallbacks() {
@@ -327,13 +368,6 @@ public void run() {
}
}
- private void loadPlugins(@NonNull final Configuration configuration) {
- NativeInterface.setClient(Client.this);
- Set userPlugins = configuration.getPlugins();
- pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
- pluginClient.loadPlugins(Client.this);
- }
-
private void logNull(String property) {
logger.e("Invalid null value supplied to client." + property + ", ignoring");
}
@@ -390,7 +424,7 @@ private boolean setupNdkDirectory() {
return bgTaskService.submitTask(TaskType.IO, new Callable() {
@Override
public Boolean call() {
- File outFile = new File(NativeInterface.getNativeReportPath());
+ File outFile = NativeInterface.getNativeReportPath();
return outFile.exists() || outFile.mkdirs();
}
}).get();
@@ -746,6 +780,9 @@ void populateAndNotifyAndroidEvent(@NonNull Event event,
// Attach context to the event
event.setContext(contextState.getContext());
+
+ event.setInternalMetrics(internalMetrics);
+
notifyInternal(event, onError);
}
@@ -1064,20 +1101,6 @@ protected void finalize() throws Throwable {
super.finalize();
}
- private void warnIfNotAppContext(Context androidContext) {
- if (!(androidContext instanceof Application)) {
- logger.w("You should initialize Bugsnag from the onCreate() callback of your "
- + "Application subclass, as this guarantees errors are captured as early "
- + "as possible. "
- + "If a custom Application subclass is not possible in your app then you "
- + "should suppress this warning by passing the Application context instead: "
- + "Bugsnag.start(context.getApplicationContext()). "
- + "For further info see: "
- + "https://docs.bugsnag.com/platforms/android/#basic-configuration");
-
- }
- }
-
ImmutableConfig getConfig() {
return immutableConfig;
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ConfigInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ConfigInternal.kt
index 9a967d1853..1fc62957e3 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ConfigInternal.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ConfigInternal.kt
@@ -53,10 +53,12 @@ internal class ConfigInternal(
var discardClasses: Set = emptySet()
var enabledReleaseStages: Set? = null
var enabledBreadcrumbTypes: Set? = null
- var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS)
+ var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE)
var projectPackages: Set = emptySet()
var persistenceDirectory: File? = null
+ var attemptDeliveryOnCrash: Boolean = false
+
val notifier: Notifier = Notifier()
protected val plugins = HashSet()
@@ -98,6 +100,52 @@ internal class ConfigInternal(
plugins.add(plugin)
}
+ private fun toCommaSeparated(coll: Collection?): String {
+ return coll?.map { it.toString() }?.sorted()?.joinToString(",") ?: ""
+ }
+
+ fun getConfigDifferences(): Map {
+ // allocate a local ConfigInternal with all-defaults to compare against
+ val defaultConfig = ConfigInternal("")
+
+ return listOfNotNull(
+ if (plugins.count() > 0) "pluginCount" to plugins.count() else null,
+ if (autoDetectErrors != defaultConfig.autoDetectErrors)
+ "autoDetectErrors" to autoDetectErrors else null,
+ if (autoTrackSessions != defaultConfig.autoTrackSessions)
+ "autoTrackSessions" to autoTrackSessions else null,
+ if (discardClasses.count() > 0)
+ "discardClassesCount" to discardClasses.count() else null,
+ if (enabledBreadcrumbTypes != defaultConfig.enabledBreadcrumbTypes)
+ "enabledBreadcrumbTypes" to toCommaSeparated(enabledBreadcrumbTypes) else null,
+ if (enabledErrorTypes != defaultConfig.enabledErrorTypes)
+ "enabledErrorTypes" to toCommaSeparated(
+ listOfNotNull(
+ if (enabledErrorTypes.anrs) "anrs" else null,
+ if (enabledErrorTypes.ndkCrashes) "ndkCrashes" else null,
+ if (enabledErrorTypes.unhandledExceptions) "unhandledExceptions" else null,
+ if (enabledErrorTypes.unhandledRejections) "unhandledRejections" else null,
+ )
+ ) else null,
+ if (launchDurationMillis != 0L) "launchDurationMillis" to launchDurationMillis else null,
+ if (logger != NoopLogger) "logger" to true else null,
+ if (maxBreadcrumbs != defaultConfig.maxBreadcrumbs)
+ "maxBreadcrumbs" to maxBreadcrumbs else null,
+ if (maxPersistedEvents != defaultConfig.maxPersistedEvents)
+ "maxPersistedEvents" to maxPersistedEvents else null,
+ if (maxPersistedSessions != defaultConfig.maxPersistedSessions)
+ "maxPersistedSessions" to maxPersistedSessions else null,
+ if (maxReportedThreads != defaultConfig.maxReportedThreads)
+ "maxReportedThreads" to maxReportedThreads else null,
+ if (persistenceDirectory != null)
+ "persistenceDirectorySet" to true else null,
+ if (sendThreads != defaultConfig.sendThreads)
+ "sendThreads" to sendThreads else null,
+ if (attemptDeliveryOnCrash != defaultConfig.attemptDeliveryOnCrash)
+ "attemptDeliveryOnCrash" to attemptDeliveryOnCrash else null
+ ).toMap()
+ }
+
companion object {
private const val DEFAULT_MAX_BREADCRUMBS = 50
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Configuration.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Configuration.java
index 0be3d0ff9a..d1d5436cf5 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Configuration.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Configuration.java
@@ -7,7 +7,6 @@
import androidx.annotation.VisibleForTesting;
import java.io.File;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -1112,6 +1111,39 @@ public void addPlugin(@NonNull Plugin plugin) {
}
}
+ /**
+ * Whether Bugsnag should try to send crashing errors prior to app termination.
+ *
+ * Delivery will only be attempted for uncaught Java / Kotlin exceptions or errors, and
+ * while in progress will block the crashing thread for up to 3 seconds.
+ *
+ * Delivery on crash should be considered unreliable due to the necessary short timeout and
+ * potential for generating "errors on errors".
+ *
+ * Use of this feature is discouraged because it:
+ * - may cause Application Not Responding (ANR) errors on-top of existing crashes
+ * - will result in duplicate errors in your Dashboard when errors are not detected as sent
+ * before termination
+ * - may prevent other error handlers from detecting or reporting a crash
+ *
+ * By default this value is {@code false}.
+ *
+ * @param attemptDeliveryOnCrash {@code true} if Bugsnag should try to send crashing errors
+ * prior to app termination
+ */
+ public void setAttemptDeliveryOnCrash(boolean attemptDeliveryOnCrash) {
+ impl.setAttemptDeliveryOnCrash(attemptDeliveryOnCrash);
+ }
+
+ /**
+ * Whether Bugsnag should try to send crashing errors prior to app termination.
+ *
+ * @see #setAttemptDeliveryOnCrash(boolean)
+ */
+ public boolean isAttemptDeliveryOnCrash() {
+ return impl.getAttemptDeliveryOnCrash();
+ }
+
Set getPlugins() {
return impl.getPlugins();
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
index d9dd1aa168..7e4d1b207b 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
@@ -7,14 +7,15 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
class DeliveryDelegate extends BaseObservable {
+ @VisibleForTesting
+ static long DELIVERY_TIMEOUT = 3000L;
+
final Logger logger;
private final EventStore eventStore;
private final ImmutableConfig immutableConfig;
@@ -55,7 +56,13 @@ void deliver(@NonNull Event event) {
String severityReasonType = event.getImpl().getSeverityReasonType();
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
boolean anr = event.getImpl().isAnr(event);
- cacheEvent(event, anr || promiseRejection);
+ if (anr || promiseRejection) {
+ cacheEvent(event, true);
+ } else if (immutableConfig.getAttemptDeliveryOnCrash()) {
+ cacheAndSendSynchronously(event);
+ } else {
+ cacheEvent(event, false);
+ }
} else if (callbackState.runOnSendTasks(event, logger)) {
// Build the eventPayload
String apiKey = event.getApiKey();
@@ -107,6 +114,24 @@ DeliveryStatus deliverPayloadInternal(@NonNull EventPayload payload, @NonNull Ev
return deliveryStatus;
}
+ private void cacheAndSendSynchronously(@NonNull Event event) {
+ long cutoffTime = System.currentTimeMillis() + DELIVERY_TIMEOUT;
+ Future task = eventStore.writeAndDeliver(event);
+
+ long timeout = cutoffTime - System.currentTimeMillis();
+ if (task != null && timeout > 0) {
+ try {
+ task.get(timeout, TimeUnit.MILLISECONDS);
+ } catch (Exception ex) {
+ logger.w("failed to immediately deliver event", ex);
+ }
+
+ if (!task.isDone()) {
+ task.cancel(true);
+ }
+ }
+ }
+
private void cacheEvent(@NonNull Event event, boolean attemptSend) {
eventStore.write(event);
if (attemptSend) {
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorTypes.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorTypes.kt
index 03b75e7e87..63975d5d9c 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorTypes.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorTypes.kt
@@ -33,4 +33,20 @@ class ErrorTypes(
internal constructor(detectErrors: Boolean) : this(detectErrors, detectErrors, detectErrors, detectErrors)
internal fun copy() = ErrorTypes(anrs, ndkCrashes, unhandledExceptions, unhandledRejections)
+
+ override fun equals(other: Any?): Boolean {
+ return other is ErrorTypes &&
+ anrs == other.anrs &&
+ ndkCrashes == other.ndkCrashes &&
+ unhandledExceptions == other.unhandledExceptions &&
+ unhandledRejections == other.unhandledRejections
+ }
+
+ override fun hashCode(): Int {
+ var result = anrs.hashCode()
+ result = 31 * result + ndkCrashes.hashCode()
+ result = 31 * result + unhandledExceptions.hashCode()
+ result = 31 * result + unhandledRejections.hashCode()
+ return result
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
index 4d815b6323..181afeb75a 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
@@ -1,6 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
+import com.bugsnag.android.internal.InternalMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -9,7 +10,6 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* An Event object represents a Throwable captured by Bugsnag and is available as a parameter on
@@ -421,4 +421,8 @@ EventInternal getImpl() {
void setRedactedKeys(Collection redactedKeys) {
impl.setRedactedKeys(redactedKeys);
}
+
+ void setInternalMetrics(InternalMetrics metrics) {
+ impl.setInternalMetrics(metrics);
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
index 7c10ee61d9..07ae66567f 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
@@ -1,6 +1,8 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
+import com.bugsnag.android.internal.InternalMetrics
+import com.bugsnag.android.internal.InternalMetricsNoop
import java.io.IOException
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
@@ -103,6 +105,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
jsonStreamer.redactedKeys = value.toSet()
metadata.redactedKeys = value.toSet()
}
+ var internalMetrics: InternalMetrics = InternalMetricsNoop()
/**
* @return user information associated with this Event
@@ -162,6 +165,15 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
writer.name("device").value(device)
writer.name("breadcrumbs").value(breadcrumbs)
writer.name("groupingHash").value(groupingHash)
+ val usage = internalMetrics.toJsonableMap()
+ if (usage.isNotEmpty()) {
+ writer.name("usage")
+ writer.beginObject()
+ usage.forEach { entry ->
+ writer.name(entry.key).value(entry.value)
+ }
+ writer.endObject()
+ }
writer.name("threads")
writer.beginArray()
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java
index 5ae0c04c50..593003ce99 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java
@@ -13,6 +13,7 @@
import java.util.Comparator;
import java.util.Date;
import java.util.List;
+import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
@@ -132,6 +133,26 @@ File findLaunchCrashReport(Collection storedFiles) {
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
}
+ @Nullable
+ Future writeAndDeliver(@NonNull final JsonStream.Streamable streamable) {
+ final String filename = write(streamable);
+
+ if (filename != null) {
+ try {
+ return bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Callable() {
+ public String call() {
+ flushEventFile(new File(filename));
+ return filename;
+ }
+ });
+ } catch (RejectedExecutionException exception) {
+ logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.");
+ }
+ }
+
+ return null;
+ }
+
/**
* Flush any on-disk errors to Bugsnag
*/
@@ -163,7 +184,7 @@ void flushReports(Collection storedReports) {
}
}
- private void flushEventFile(File eventFile) {
+ void flushEventFile(File eventFile) {
try {
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
String apiKey = eventInfo.getApiKey();
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
index ad166e35b8..b121cbf83a 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
@@ -42,6 +42,7 @@ internal class ManifestConfigLoader {
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
private const val APP_TYPE = "$BUGSNAG_NS.APP_TYPE"
+ private const val ATTEMPT_DELIVERY_ON_CRASH = "$BUGSNAG_NS.ATTEMPT_DELIVERY_ON_CRASH"
}
fun load(ctx: Context, userSuppliedApiKey: String?): Configuration {
@@ -91,6 +92,10 @@ internal class ManifestConfigLoader {
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
sendLaunchCrashesSynchronously
)
+ isAttemptDeliveryOnCrash = data.getBoolean(
+ ATTEMPT_DELIVERY_ON_CRASH,
+ isAttemptDeliveryOnCrash
+ )
}
}
return config
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 4bcb0a2358..610c9d6a19 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
@@ -1,20 +1,21 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
+import com.bugsnag.android.internal.JsonHelper;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
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;
@@ -78,10 +79,16 @@ public static String getContext() {
* Retrieves the directory used to store native crash reports
*/
@NonNull
- public static String getNativeReportPath() {
- ImmutableConfig config = getClient().getConfig();
- File persistenceDirectory = config.getPersistenceDirectory().getValue();
- return new File(persistenceDirectory, "bugsnag-native").getAbsolutePath();
+ public static File getNativeReportPath() {
+ return getNativeReportPath(getPersistenceDirectory());
+ }
+
+ private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) {
+ return new File(persistenceDirectory, "bugsnag-native");
+ }
+
+ private static @NonNull File getPersistenceDirectory() {
+ return getClient().getConfig().getPersistenceDirectory().getValue();
}
/**
@@ -336,6 +343,24 @@ public static boolean isDiscardErrorClass(@NonNull String name) {
return getClient().getConfig().getDiscardClasses().contains(name);
}
+ @SuppressWarnings("unchecked")
+ private static void deepMerge(Map src, Map dst) {
+ for (Map.Entry entry: src.entrySet()) {
+ String key = entry.getKey();
+ Object srcValue = entry.getValue();
+ Object dstValue = dst.get(key);
+ if (srcValue instanceof Map && (dstValue instanceof Map)) {
+ deepMerge((Map)srcValue, (Map)dstValue);
+ } else if (srcValue instanceof Collection && dstValue instanceof Collection) {
+ // Just append everything because we don't know enough about the context or
+ // provenance of the data to make an intelligent decision about this.
+ ((Collection
diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV11Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV11Tests.kt
new file mode 100644
index 0000000000..053b08a303
--- /dev/null
+++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV11Tests.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 v11 added telemetry data */
+class EventMigrationV11Tests : 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/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt
index 37274eb046..c6bd4301f0 100644
--- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt
+++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt
@@ -13,6 +13,7 @@ add_library( # Specifies the name of the library.
jni/jni_cache.c
jni/event.c
jni/featureflags.c
+ jni/internal_metrics.c
jni/handlers/signal_handler.c
jni/handlers/cpp_handler.cpp
jni/utils/crash_info.c
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 9412b52cd4..fab6d402fe 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
@@ -1,6 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.ndk.NativeBridge
+import java.io.StringWriter
import java.util.concurrent.atomic.AtomicBoolean
internal class NdkPlugin : Plugin {
@@ -66,12 +67,37 @@ internal class NdkPlugin : Plugin {
}
}
+ fun setInternalMetricsEnabled(enabled: Boolean) {
+ nativeBridge?.setInternalMetricsEnabled(enabled)
+ }
+
fun getSignalUnwindStackFunction(): Long {
- val bridge = nativeBridge
- if (bridge != null) {
- return bridge.getSignalUnwindStackFunction()
- }
- return 0
+ return nativeBridge?.getSignalUnwindStackFunction() ?: 0
+ }
+
+ fun getCurrentCallbackSetCounts(): Map {
+ return nativeBridge?.getCurrentCallbackSetCounts() ?: mapOf()
+ }
+
+ fun getCurrentNativeApiCallUsage(): Map {
+ return nativeBridge?.getCurrentNativeApiCallUsage() ?: mapOf()
+ }
+
+ fun initCallbackCounts(counts: Map) {
+ nativeBridge?.initCallbackCounts(counts)
+ }
+
+ fun notifyAddCallback(callback: String) {
+ nativeBridge?.notifyAddCallback(callback)
+ }
+
+ fun notifyRemoveCallback(callback: String) {
+ nativeBridge?.notifyRemoveCallback(callback)
+ }
+
+ fun setStaticData(data: Map) {
+ val encoded = StringWriter().apply { use { writer -> JsonStream(writer).use { it.value(data) } } }.toString()
+ nativeBridge?.setStaticJsonData(encoded)
}
}
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 6ae19cb1de..911af747fb 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
@@ -19,6 +19,7 @@ import com.bugsnag.android.StateEvent.UpdateOrientation
import com.bugsnag.android.StateEvent.UpdateUser
import com.bugsnag.android.internal.StateObserver
import java.io.File
+import java.io.FileFilter
import java.nio.charset.Charset
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
@@ -31,7 +32,7 @@ class NativeBridge : StateObserver {
private val lock = ReentrantLock()
private val installed = AtomicBoolean(false)
- private val reportDirectory: String = NativeInterface.getNativeReportPath()
+ private val reportDirectory: File = NativeInterface.getNativeReportPath()
private val logger = NativeInterface.getLogger()
private val is32bit: Boolean
@@ -83,6 +84,13 @@ class NativeBridge : StateObserver {
external fun clearFeatureFlag(name: String)
external fun clearFeatureFlags()
external fun refreshSymbolTable()
+ external fun initCallbackCounts(counts: Map)
+ external fun notifyAddCallback(callback: String)
+ external fun notifyRemoveCallback(callback: String)
+ external fun getCurrentCallbackSetCounts(): Map?
+ external fun getCurrentNativeApiCallUsage(): Map?
+ external fun setStaticJsonData(data: String)
+ external fun setInternalMetricsEnabled(enabled: Boolean)
override fun onStateChange(event: StateEvent) {
if (isInvalidMessage(event)) return
@@ -157,10 +165,11 @@ class NativeBridge : StateObserver {
private fun deliverPendingReports() {
lock.lock()
+ val filenameRegex = """.*\.crash$""".toRegex()
try {
- val outDir = File(reportDirectory)
+ val outDir = reportDirectory
if (outDir.exists()) {
- val fileList = outDir.listFiles()
+ val fileList = outDir.listFiles(FileFilter { filenameRegex.containsMatchIn(it.name) })
if (fileList != null) {
for (file in fileList) {
deliverReportAtPath(file.absolutePath)
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 edcb467034..8fab21e22c 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c
@@ -11,16 +11,20 @@
#include "featureflags.h"
#include "handlers/cpp_handler.h"
#include "handlers/signal_handler.h"
+#include "internal_metrics.h"
#include "jni_cache.h"
#include "metadata.h"
#include "safejni.h"
#include "utils/serializer.h"
+#include "utils/serializer/event_reader.h"
#include "utils/string.h"
#ifdef __cplusplus
extern "C" {
#endif
+#define STATIC_DATA_FILENAME_EXTENSION ".static_data.json"
+
static bsg_environment *bsg_global_env;
static pthread_mutex_t bsg_global_env_write_mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -42,12 +46,14 @@ static void release_env_write_lock(void) {
void bugsnag_add_on_error(bsg_on_error on_error) {
if (bsg_global_env != NULL) {
bsg_global_env->on_error = on_error;
+ bsg_notify_add_callback(&bsg_global_env->next_event, "ndkOnError");
}
}
void bugsnag_remove_on_error() {
if (bsg_global_env != NULL) {
bsg_global_env->on_error = NULL;
+ bsg_notify_remove_callback(&bsg_global_env->next_event, "ndkOnError");
}
}
@@ -165,6 +171,8 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install(
goto error;
}
sprintf(bugsnag_env->next_event_path, "%s", event_path);
+ sprintf(bugsnag_env->next_event_static_data_path, "%s%s", event_path,
+ STATIC_DATA_FILENAME_EXTENSION);
bsg_safe_release_string_utf_chars(env, _event_path, event_path);
// copy last run info path to env struct
@@ -225,10 +233,13 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath(
pthread_mutex_lock(&bsg_native_delivery_mutex);
const char *event_path = NULL;
+ char static_data_path[384];
bugsnag_event *event = NULL;
jbyteArray jpayload = NULL;
jbyteArray jstage = NULL;
char *payload = NULL;
+ char *static_data = NULL;
+ jbyteArray jstatic_data = NULL;
jstring japi_key = NULL;
jstring errorClass = NULL;
@@ -243,9 +254,16 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath(
}
event = bsg_deserialize_event_from_file((char *)event_path);
+ snprintf(static_data_path, sizeof(static_data_path), "%s%s", event_path,
+ STATIC_DATA_FILENAME_EXTENSION);
+ if (bsg_read_text_file(static_data_path, &static_data) > 0) {
+ jstatic_data = bsg_byte_ary_from_string(env, static_data);
+ }
+
// remove persisted NDK struct early - this reduces the chance of crash loops
// in delivery.
remove(event_path);
+ remove(static_data_path);
if (event == NULL) {
BUGSNAG_LOG("Failed to read event at file: %s", event_path);
@@ -284,7 +302,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath(
bsg_safe_call_static_void_method(
env, bsg_jni_cache->NativeInterface,
bsg_jni_cache->NativeInterface_deliverReport, jstage, jpayload,
- japi_key, is_launching);
+ jstatic_data, japi_key, is_launching);
}
exit:
@@ -298,6 +316,8 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath(
}
bsg_safe_release_byte_array_elements(env, jpayload, (jbyte *)payload);
free(payload);
+ bsg_safe_release_byte_array_elements(env, jstatic_data, (jbyte *)static_data);
+ free(static_data);
pthread_mutex_unlock(&bsg_native_delivery_mutex);
}
@@ -311,7 +331,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env,
request_env_write_lock();
bugsnag_event *event = &bsg_global_env->next_event;
- if (bugsnag_event_has_session(event)) {
+ if (bsg_event_has_session(event)) {
event->handled_events++;
}
release_env_write_lock();
@@ -326,7 +346,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_addUnhandledEvent(JNIEnv *env,
request_env_write_lock();
bugsnag_event *event = &bsg_global_env->next_event;
- if (bugsnag_event_has_session(event)) {
+ if (bsg_event_has_session(event)) {
event->unhandled_events++;
}
release_env_write_lock();
@@ -342,8 +362,8 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_startedSession(
char *started_at = (char *)bsg_safe_get_string_utf_chars(env, start_date_);
if (session_id != NULL && started_at != NULL) {
request_env_write_lock();
- bugsnag_event_start_session(&bsg_global_env->next_event, session_id,
- started_at, handled_count, unhandled_count);
+ bsg_event_start_session(&bsg_global_env->next_event, session_id, started_at,
+ handled_count, unhandled_count);
release_env_write_lock();
}
bsg_safe_release_string_utf_chars(env, session_id_, session_id);
@@ -400,7 +420,7 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addBreadcrumb(
bsg_populate_crumb_metadata(env, crumb, metadata);
request_env_write_lock();
- bugsnag_event_add_breadcrumb(&bsg_global_env->next_event, crumb);
+ bsg_event_add_breadcrumb(&bsg_global_env->next_event, crumb);
release_env_write_lock();
free(crumb);
@@ -818,6 +838,139 @@ Java_com_bugsnag_android_ndk_NativeBridge_refreshSymbolTable(JNIEnv *env,
bugsnag_refresh_symbol_table();
}
+JNIEXPORT jobject JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_getCurrentCallbackSetCounts(
+ JNIEnv *env, jobject thiz) {
+
+ if (bsg_global_env == NULL) {
+ return NULL;
+ }
+
+ static const int total_callbacks =
+ sizeof(bsg_global_env->next_event.set_callback_counts) /
+ sizeof(*bsg_global_env->next_event.set_callback_counts);
+
+ jobject counts = bsg_safe_new_object(env, bsg_jni_cache->HashMap,
+ bsg_jni_cache->HashMap_constructor);
+ if (counts == NULL) {
+ return NULL;
+ }
+
+ for (int i = 0; i < total_callbacks; i++) {
+ jstring key = bsg_safe_new_string_utf(
+ env, bsg_global_env->next_event.set_callback_counts[i].name);
+ jobject value = bsg_safe_new_object(
+ env, bsg_jni_cache->Int, bsg_jni_cache->Int_constructor,
+ (jint)bsg_global_env->next_event.set_callback_counts[i].count);
+ if (value == NULL) {
+ return NULL;
+ }
+ bsg_safe_call_object_method(env, counts, bsg_jni_cache->HashMap_put, key,
+ value);
+ bsg_safe_delete_local_ref(env, value);
+ }
+
+ return counts;
+}
+
+JNIEXPORT jobject JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_getCurrentNativeApiCallUsage(
+ JNIEnv *env, jobject thiz) {
+ if (bsg_global_env == NULL) {
+ return NULL;
+ }
+
+ jobject map = bsg_safe_new_object(env, bsg_jni_cache->HashMap,
+ bsg_jni_cache->HashMap_constructor);
+ if (map == NULL) {
+ return NULL;
+ }
+
+ jobject trueValue = bsg_safe_new_object(
+ env, bsg_jni_cache->Boolean, bsg_jni_cache->Boolean_constructor, true);
+ if (trueValue == NULL) {
+ return NULL;
+ }
+ for (bsg_called_api i = 0; i < bsg_called_apis_count; i++) {
+ if (bsg_was_api_called(&bsg_global_env->next_event, i)) {
+ jstring key = bsg_safe_new_string_utf(env, bsg_called_api_names[i]);
+ bsg_safe_call_object_method(env, map, bsg_jni_cache->HashMap_put, key,
+ trueValue);
+ }
+ }
+ bsg_safe_delete_local_ref(env, trueValue);
+
+ return map;
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_setStaticJsonData(JNIEnv *env,
+ jobject thiz,
+ jstring data_) {
+ const char *data = bsg_safe_get_string_utf_chars(env, data_);
+ if (data == NULL) {
+ return;
+ }
+
+ size_t length = strlen(data);
+ if (length == 0) {
+ goto done;
+ }
+
+ const char *data_old = bsg_global_env->static_json_data;
+ bsg_global_env->static_json_data = strdup(data);
+ free((void *)data_old);
+
+done:
+ bsg_safe_release_string_utf_chars(env, data_, data);
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_initCallbackCounts(JNIEnv *env,
+ jobject thiz,
+ jobject counts) {
+ jobject entrySet =
+ bsg_safe_call_object_method(env, counts, bsg_jni_cache->Map_entrySet);
+ jobject iterator =
+ bsg_safe_call_object_method(env, entrySet, bsg_jni_cache->Set_iterator);
+
+ while (bsg_safe_call_boolean_method(env, iterator,
+ bsg_jni_cache->Iterator_hasNext)) {
+ jobject entry = bsg_safe_call_object_method(env, iterator,
+ bsg_jni_cache->Iterator_next);
+ jstring name =
+ bsg_safe_call_object_method(env, entry, bsg_jni_cache->MapEntry_getKey);
+ jobject value = bsg_safe_call_object_method(
+ env, entry, bsg_jni_cache->MapEntry_getValue);
+ const char *nameString = bsg_safe_get_string_utf_chars(env, name);
+ jint intValue =
+ (jint)bsg_safe_call_int_method(env, value, bsg_jni_cache->Int_intValue);
+ bsg_set_callback_count(&bsg_global_env->next_event, nameString,
+ (int32_t)intValue);
+ }
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_notifyAddCallback(JNIEnv *env,
+ jobject thiz,
+ jstring callback_) {
+ const char *callback = bsg_safe_get_string_utf_chars(env, callback_);
+ bsg_notify_add_callback(&bsg_global_env->next_event, callback);
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_notifyRemoveCallback(
+ JNIEnv *env, jobject thiz, jstring callback_) {
+ const char *callback = bsg_safe_get_string_utf_chars(env, callback_);
+ bsg_notify_remove_callback(&bsg_global_env->next_event, callback);
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_ndk_NativeBridge_setInternalMetricsEnabled(
+ JNIEnv *env, jobject thiz, jboolean enabled) {
+ bsg_set_internal_metrics_enabled(enabled);
+}
+
#ifdef __cplusplus
}
#endif
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h
index 592c9bb1d6..25334727db 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h
+++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h
@@ -25,6 +25,11 @@ typedef struct {
* File path on disk where the next crash report will be written if needed.
*/
char next_event_path[384];
+ /**
+ * File path on disk where the next crash report static data will be written
+ * if needed.
+ */
+ char next_event_static_data_path[384];
/**
* File path on disk where the last run info will be written if needed.
*/
@@ -67,6 +72,17 @@ typedef struct {
* at the time of an error.
*/
bsg_thread_send_policy send_threads;
+
+ /**
+ * Static data set on app launch and never again modified.
+ * Allocated dynamically, in JSON format.
+ * On crash, this data gets dumped to a new file with the same name as the
+ * event file + ".staticData.json".
+ * On delivery, this data gets sent to the JVM alongside of the JSONified
+ * event object.
+ */
+ const char *static_json_data;
+
} bsg_environment;
/**
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.c b/bugsnag-plugin-android-ndk/src/main/jni/event.c
index b3dc27e09d..5ef89aea8b 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/event.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/event.c
@@ -1,4 +1,5 @@
#include "event.h"
+#include "internal_metrics.h"
#include "utils/string.h"
#include
#include
@@ -206,31 +207,36 @@ void bsg_add_metadata_value_opaque(bugsnag_metadata *metadata,
void bugsnag_event_add_metadata_double(void *event_ptr, const char *section,
const char *name, double value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_ADD_METADATA_DOUBLE);
bsg_add_metadata_value_double(&event->metadata, section, name, value);
}
void bugsnag_event_add_metadata_string(void *event_ptr, const char *section,
const char *name, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_ADD_METADATA_STRING);
bsg_add_metadata_value_str(&event->metadata, section, name, value);
}
void bugsnag_event_add_metadata_bool(void *event_ptr, const char *section,
const char *name, bool value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_ADD_METADATA_BOOL);
bsg_add_metadata_value_bool(&event->metadata, section, name, value);
}
void bugsnag_event_clear_metadata(void *event_ptr, const char *section,
const char *name) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_CLEAR_METADATA);
bsg_event_clear_metadata(&event->metadata, section, name);
}
void bugsnag_event_clear_metadata_section(void *event_ptr,
const char *section) {
- size_t clearedCount = 0;
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_CLEAR_METADATA_SECTION);
+ size_t clearedCount = 0;
for (int i = 0; i < event->metadata.value_count; ++i) {
if (strcmp(event->metadata.values[i].section, section) == 0) {
bsg_clear_metadata_value(&(event->metadata.values[i]));
@@ -243,9 +249,8 @@ void bugsnag_event_clear_metadata_section(void *event_ptr,
}
}
-bsg_metadata_value bugsnag_get_metadata_value(void *event_ptr,
- const char *section,
- const char *name) {
+static bsg_metadata_value
+get_metadata_value(void *event_ptr, const char *section, const char *name) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
for (int k = 0; k < event->metadata.value_count; ++k) {
@@ -261,13 +266,16 @@ bsg_metadata_value bugsnag_get_metadata_value(void *event_ptr,
bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr,
const char *section,
const char *name) {
- return bugsnag_get_metadata_value(event_ptr, section, name).type;
+ bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_HAS_METADATA);
+ return get_metadata_value(event_ptr, section, name).type;
}
double bugsnag_event_get_metadata_double(void *event_ptr, const char *section,
const char *name) {
- bsg_metadata_value value =
- bugsnag_get_metadata_value(event_ptr, section, name);
+ bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_METADATA_DOUBLE);
+ bsg_metadata_value value = get_metadata_value(event_ptr, section, name);
if (value.type == BSG_METADATA_NUMBER_VALUE) {
return value.double_value;
@@ -278,6 +286,7 @@ double bugsnag_event_get_metadata_double(void *event_ptr, const char *section,
char *bugsnag_event_get_metadata_string(void *event_ptr, const char *section,
const char *name) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_METADATA_STRING);
for (int k = 0; k < event->metadata.value_count; ++k) {
if (strcmp(event->metadata.values[k].section, section) == 0 &&
@@ -290,8 +299,9 @@ char *bugsnag_event_get_metadata_string(void *event_ptr, const char *section,
bool bugsnag_event_get_metadata_bool(void *event_ptr, const char *section,
const char *name) {
- bsg_metadata_value value =
- bugsnag_get_metadata_value(event_ptr, section, name);
+ bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_METADATA_BOOL);
+ bsg_metadata_value value = get_metadata_value(event_ptr, section, name);
if (value.type == BSG_METADATA_BOOL_VALUE) {
return value.bool_value;
@@ -299,10 +309,9 @@ bool bugsnag_event_get_metadata_bool(void *event_ptr, const char *section,
return false;
}
-void bugsnag_event_start_session(bugsnag_event *event, const char *session_id,
- const char *started_at,
- const int handled_count,
- const int unhandled_count) {
+void bsg_event_start_session(bugsnag_event *event, const char *session_id,
+ const char *started_at, int handled_count,
+ int unhandled_count) {
bsg_strncpy(event->session_id, session_id, sizeof(event->session_id));
bsg_strncpy(event->session_start, started_at, sizeof(event->session_start));
event->handled_events = handled_count;
@@ -311,34 +320,38 @@ void bugsnag_event_start_session(bugsnag_event *event, const char *session_id,
char *bugsnag_event_get_api_key(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_API_KEY);
return event->api_key;
}
void bugsnag_event_set_api_key(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_API_KEY);
bsg_strncpy(event->api_key, value, sizeof(event->api_key));
}
char *bugsnag_event_get_context(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_CONTEXT);
return event->context;
}
void bugsnag_event_set_context(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_CONTEXT);
bsg_strncpy(event->context, value, sizeof(event->context));
}
void bugsnag_event_set_user(void *event_ptr, const char *id, const char *email,
const char *name) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_USER);
bsg_strncpy(event->user.id, id, sizeof(event->user.id));
bsg_strncpy(event->user.email, email, sizeof(event->user.email));
bsg_strncpy(event->user.name, name, sizeof(event->user.name));
}
-void bugsnag_event_add_breadcrumb(bugsnag_event *event,
- bugsnag_breadcrumb *crumb) {
+void bsg_event_add_breadcrumb(bugsnag_event *event, bugsnag_breadcrumb *crumb) {
int crumb_index;
if (event->crumb_count < BUGSNAG_CRUMBS_MAX) {
crumb_index = event->crumb_count;
@@ -353,17 +366,7 @@ void bugsnag_event_add_breadcrumb(bugsnag_event *event,
memcpy(&event->breadcrumbs[crumb_index], crumb, sizeof(bugsnag_breadcrumb));
}
-void bugsnag_event_clear_breadcrumbs(bugsnag_event *event) {
- for (int index = 0; index < event->crumb_count; index++) {
- int crumb_index = (index + event->crumb_first_index) % BUGSNAG_CRUMBS_MAX;
- bsg_free_opaque_metadata(&(event->breadcrumbs[crumb_index].metadata));
- }
-
- event->crumb_count = 0;
- event->crumb_first_index = 0;
-}
-
-bool bugsnag_event_has_session(const bugsnag_event *event) {
+bool bsg_event_has_session(const bugsnag_event *event) {
return bsg_strlen(event->session_id) > 0;
}
@@ -371,112 +374,134 @@ bool bugsnag_event_has_session(const bugsnag_event *event) {
char *bugsnag_app_get_binary_arch(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_BINARY_ARCH);
return event->app.binary_arch;
}
void bugsnag_app_set_binary_arch(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_BINARY_ARCH);
bsg_strncpy(event->app.binary_arch, value, sizeof(event->app.binary_arch));
}
char *bugsnag_app_get_build_uuid(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_BUILD_UUID);
return event->app.build_uuid;
}
void bugsnag_app_set_build_uuid(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_BUILD_UUID);
bsg_strncpy(event->app.build_uuid, value, sizeof(event->app.build_uuid));
}
char *bugsnag_app_get_id(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_ID);
return event->app.id;
}
void bugsnag_app_set_id(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_ID);
bsg_strncpy(event->app.id, value, sizeof(event->app.id));
}
char *bugsnag_app_get_release_stage(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_RELEASE_STAGE);
return event->app.release_stage;
}
void bugsnag_app_set_release_stage(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_RELEASE_STAGE);
bsg_strncpy(event->app.release_stage, value,
sizeof(event->app.release_stage));
}
char *bugsnag_app_get_type(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_TYPE);
return event->app.type;
}
void bugsnag_app_set_type(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_TYPE);
bsg_strncpy(event->app.type, value, sizeof(event->app.type));
}
char *bugsnag_app_get_version(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_VERSION);
return event->app.version;
}
void bugsnag_app_set_version(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_VERSION);
bsg_strncpy(event->app.version, value, sizeof(event->app.version));
}
int bugsnag_app_get_version_code(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_VERSION_CODE);
return event->app.version_code;
}
void bugsnag_app_set_version_code(void *event_ptr, int value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_VERSION_CODE);
event->app.version_code = value;
}
time_t bugsnag_app_get_duration(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_DURATION);
return event->app.duration;
}
void bugsnag_app_set_duration(void *event_ptr, time_t value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_DURATION);
event->app.duration = value;
}
time_t bugsnag_app_get_duration_in_foreground(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_DURATION_IN_FOREGROUND);
return event->app.duration_in_foreground;
}
void bugsnag_app_set_duration_in_foreground(void *event_ptr, time_t value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_DURATION_IN_FOREGROUND);
event->app.duration_in_foreground = value;
}
bool bugsnag_app_get_in_foreground(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_IN_FOREGROUND);
return event->app.in_foreground;
}
void bugsnag_app_set_in_foreground(void *event_ptr, bool value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_IN_FOREGROUND);
event->app.in_foreground = value;
}
bool bugsnag_app_get_is_launching(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_GET_IS_LAUNCHING);
return event->app.is_launching;
}
void bugsnag_app_set_is_launching(void *event_ptr, bool value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_APP_SET_IS_LAUNCHING);
event->app.is_launching = value;
}
@@ -484,180 +509,215 @@ void bugsnag_app_set_is_launching(void *event_ptr, bool value) {
bool bugsnag_device_get_jailbroken(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_JAILBROKEN);
return event->device.jailbroken;
}
void bugsnag_device_set_jailbroken(void *event_ptr, bool value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_JAILBROKEN);
event->device.jailbroken = value;
}
char *bugsnag_device_get_id(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_ID);
return event->device.id;
}
void bugsnag_device_set_id(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_ID);
bsg_strncpy(event->device.id, value, sizeof(event->device.id));
}
char *bugsnag_device_get_locale(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_LOCALE);
return event->device.locale;
}
void bugsnag_device_set_locale(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_LOCALE);
bsg_strncpy(event->device.locale, value, sizeof(event->device.locale));
}
char *bugsnag_device_get_manufacturer(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_MANUFACTURER);
return event->device.manufacturer;
}
void bugsnag_device_set_manufacturer(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_MANUFACTURER);
bsg_strncpy(event->device.manufacturer, value,
sizeof(event->device.manufacturer));
}
char *bugsnag_device_get_model(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_MODEL);
return event->device.model;
}
void bugsnag_device_set_model(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_MODEL);
bsg_strncpy(event->device.model, value, sizeof(event->device.model));
}
char *bugsnag_device_get_os_version(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_OS_VERSION);
return event->device.os_version;
}
void bugsnag_device_set_os_version(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_OS_VERSION);
bsg_strncpy(event->device.os_version, value,
sizeof(event->device.os_version));
}
long bugsnag_device_get_total_memory(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_TOTAL_MEMORY);
return event->device.total_memory;
}
void bugsnag_device_set_total_memory(void *event_ptr, long value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_TOTAL_MEMORY);
event->device.total_memory = value;
}
char *bugsnag_device_get_orientation(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_ORIENTATION);
return event->device.orientation;
}
void bugsnag_device_set_orientation(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_ORIENTATION);
bsg_strncpy(event->device.orientation, value,
sizeof(event->device.orientation));
}
time_t bugsnag_device_get_time(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_TIME);
return event->device.time;
}
void bugsnag_device_set_time(void *event_ptr, time_t value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_TIME);
event->device.time = value;
}
char *bugsnag_device_get_os_name(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_GET_OS_NAME);
return event->device.os_name;
}
void bugsnag_device_set_os_name(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_DEVICE_SET_OS_NAME);
bsg_strncpy(event->device.os_name, value, sizeof(event->device.os_name));
}
char *bugsnag_error_get_error_class(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_GET_ERROR_CLASS);
return event->error.errorClass;
}
void bugsnag_error_set_error_class(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_SET_ERROR_CLASS);
bsg_strncpy(event->error.errorClass, value, sizeof(event->error.errorClass));
}
char *bugsnag_error_get_error_message(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_GET_ERROR_MESSAGE);
return event->error.errorMessage;
}
void bugsnag_error_set_error_message(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_SET_ERROR_MESSAGE);
bsg_strncpy(event->error.errorMessage, value,
sizeof(event->error.errorMessage));
}
char *bugsnag_error_get_error_type(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_GET_ERROR_TYPE);
return event->error.type;
}
void bugsnag_error_set_error_type(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_ERROR_SET_ERROR_TYPE);
bsg_strncpy(event->error.type, value, sizeof(event->error.type));
}
bugsnag_severity bugsnag_event_get_severity(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_SEVERITY);
return event->severity;
}
void bugsnag_event_set_severity(void *event_ptr, bugsnag_severity value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_SEVERITY);
event->severity = value;
}
bool bugsnag_event_is_unhandled(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_IS_UNHANDLED);
return event->unhandled;
}
void bugsnag_event_set_unhandled(void *event_ptr, bool value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_UNHANDLED);
event->unhandled = value;
}
bugsnag_user bugsnag_event_get_user(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_USER);
return event->user;
}
char *bugsnag_event_get_grouping_hash(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_GROUPING_HASH);
return event->grouping_hash;
}
void bugsnag_event_set_grouping_hash(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_SET_GROUPING_HASH);
bsg_strncpy(event->grouping_hash, value, sizeof(event->grouping_hash));
}
int bugsnag_event_get_stacktrace_size(void *event_ptr) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_STACKTRACE_SIZE);
return event->error.frame_count;
}
bugsnag_stackframe *bugsnag_event_get_stackframe(void *event_ptr, int index) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
+ bsg_notify_api_called(event, BSG_API_EVENT_GET_STACKFRAME);
if (index >= 0 && index < event->error.frame_count) {
return &event->error.stacktrace[index];
} else {
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h
index 14db22e74f..a2b43ed47e 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 10
+#define BUGSNAG_EVENT_VERSION 11
#ifdef __cplusplus
extern "C" {
@@ -212,6 +212,11 @@ typedef struct {
char *variant;
} bsg_feature_flag;
+typedef struct {
+ char name[30];
+ int32_t count;
+} set_callback_count;
+
typedef struct {
bsg_notifier notifier;
bsg_app_info app;
@@ -250,15 +255,30 @@ typedef struct {
* serialized/deserialized separately to the rest of the struct.
*/
bsg_feature_flag *feature_flags;
+
+ /**
+ * Counters to count how many times a callback was set.
+ * There are actually less than 10 callbacks, but leave room for expansion.
+ */
+ set_callback_count set_callback_counts[10];
+
+ /**
+ * Flags to denote which native APIs have been called (see bsg_called_api).
+ * This only records that at least one call was made per API; it doesn't tally
+ * how many calls occurred.
+ *
+ * Implemented as a bit array:
+ * The high bits (call / 64) represent the index into event->called_apis.
+ * The low bits (call & 63) represent the bit index.
+ */
+ uint64_t called_apis[2];
} bugsnag_event;
-void bugsnag_event_add_breadcrumb(bugsnag_event *event,
- bugsnag_breadcrumb *crumb);
-void bugsnag_event_clear_breadcrumbs(bugsnag_event *event);
-void bugsnag_event_start_session(bugsnag_event *event, const char *session_id,
- const char *started_at, int handled_count,
- int unhandled_count);
-bool bugsnag_event_has_session(const bugsnag_event *event);
+void bsg_event_add_breadcrumb(bugsnag_event *event, bugsnag_breadcrumb *crumb);
+void bsg_event_start_session(bugsnag_event *event, const char *session_id,
+ const char *started_at, int handled_count,
+ int unhandled_count);
+bool bsg_event_has_session(const bugsnag_event *event);
void bsg_add_metadata_value_double(bugsnag_metadata *metadata,
const char *section, const char *name,
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c
new file mode 100644
index 0000000000..4bc7f2b0f8
--- /dev/null
+++ b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c
@@ -0,0 +1,201 @@
+//
+// Created by Karl Stenerud on 05.09.22.
+//
+
+#include "internal_metrics.h"
+#include "utils/logger.h"
+
+#include
+
+static bool internal_metrics_enabled = false;
+
+// This MUST remain consistent with bsg_called_api in jni/event.h
+const char *const bsg_called_api_names[] = {
+ "app_get_binary_arch",
+ "app_get_build_uuid",
+ "app_get_duration",
+ "app_get_duration_in_foreground",
+ "app_get_id",
+ "app_get_in_foreground",
+ "app_get_is_launching",
+ "app_get_release_stage",
+ "app_get_type",
+ "app_get_version",
+ "app_get_version_code",
+ "app_set_binary_arch",
+ "app_set_build_uuid",
+ "app_set_duration",
+ "app_set_duration_in_foreground",
+ "app_set_id",
+ "app_set_in_foreground",
+ "app_set_is_launching",
+ "app_set_release_stage",
+ "app_set_type",
+ "app_set_version",
+ "app_set_version_code",
+ "device_get_id",
+ "device_get_jailbroken",
+ "device_get_locale",
+ "device_get_manufacturer",
+ "device_get_model",
+ "device_get_orientation",
+ "device_get_os_name",
+ "device_get_os_version",
+ "device_get_time",
+ "device_get_total_memory",
+ "device_set_id",
+ "device_set_jailbroken",
+ "device_set_locale",
+ "device_set_manufacturer",
+ "device_set_model",
+ "device_set_orientation",
+ "device_set_os_name",
+ "device_set_os_version",
+ "device_set_time",
+ "device_set_total_memory",
+ "error_get_error_class",
+ "error_get_error_message",
+ "error_get_error_type",
+ "error_set_error_class",
+ "error_set_error_message",
+ "error_set_error_type",
+ "event_add_metadata_bool",
+ "event_add_metadata_double",
+ "event_add_metadata_string",
+ "event_clear_metadata",
+ "event_clear_metadata_section",
+ "event_get_api_key",
+ "event_get_context",
+ "event_get_grouping_hash",
+ "event_get_metadata_bool",
+ "event_get_metadata_double",
+ "event_get_metadata_string",
+ "event_get_severity",
+ "event_get_stackframe",
+ "event_get_stacktrace_size",
+ "event_get_user",
+ "event_has_metadata",
+ "event_is_unhandled",
+ "event_set_api_key",
+ "event_set_context",
+ "event_set_grouping_hash",
+ "event_set_severity",
+ "event_set_unhandled",
+ "event_set_user",
+};
+const int bsg_called_apis_count =
+ sizeof(bsg_called_api_names) / sizeof(*bsg_called_api_names);
+
+void bsg_set_internal_metrics_enabled(bool enabled) {
+ internal_metrics_enabled = enabled;
+}
+
+static int get_called_api_array_slot_index(bsg_called_api api) {
+ return api / 64;
+}
+
+static uint64_t get_called_api_array_slot_bit(bsg_called_api api) {
+ int bit_index = api & 63;
+ return ((uint64_t)1) << bit_index;
+}
+
+void bsg_notify_api_called(bugsnag_event *event, bsg_called_api api) {
+ if (!internal_metrics_enabled) {
+ return;
+ }
+
+ static const int slot_count =
+ sizeof(event->called_apis) / sizeof(*event->called_apis);
+ int slot_index = get_called_api_array_slot_index(api);
+ if (slot_index < slot_count) {
+ event->called_apis[slot_index] |= get_called_api_array_slot_bit(api);
+ }
+}
+
+bool bsg_was_api_called(const bugsnag_event *event, bsg_called_api api) {
+ // No internal_metrics_enabled check because this function reads after an app
+ // reload.
+
+ static const int slot_count =
+ sizeof(event->called_apis) / sizeof(*event->called_apis);
+ int slot_index = get_called_api_array_slot_index(api);
+ if (slot_index < slot_count) {
+ return (event->called_apis[slot_index] &
+ get_called_api_array_slot_bit(api)) != 0;
+ }
+ return false;
+}
+
+static void bsg_modify_callback_count(bugsnag_event *event, const char *api,
+ int delta) {
+ static const int total_callbacks =
+ sizeof(event->set_callback_counts) / sizeof(*event->set_callback_counts);
+ if (strlen(api) >= sizeof(event->set_callback_counts[0].name)) {
+ // API name is too big to store.
+ return;
+ }
+
+ int i = 0;
+ for (; i < total_callbacks && event->set_callback_counts[i].name[0] != 0;
+ i++) {
+ if (strcmp(event->set_callback_counts[i].name, api) == 0) {
+ event->set_callback_counts[i].count += delta;
+ if (event->set_callback_counts[i].count < 0) {
+ event->set_callback_counts[i].count = 0;
+ }
+ return;
+ }
+ }
+ if (i < total_callbacks && delta > 0) {
+ strncpy(event->set_callback_counts[i].name, api,
+ sizeof(event->set_callback_counts[i].name));
+ event->set_callback_counts[i].count = delta;
+ }
+}
+
+void bsg_set_callback_count(bugsnag_event *event, const char *api,
+ int32_t count) {
+ if (!internal_metrics_enabled) {
+ return;
+ }
+
+ static const int total_callbacks =
+ sizeof(event->set_callback_counts) / sizeof(*event->set_callback_counts);
+ if (strlen(api) >= sizeof(event->set_callback_counts[0].name)) {
+ // API name is too big to store.
+ return;
+ }
+
+ int i = 0;
+ for (; i < total_callbacks && event->set_callback_counts[i].name[0] != 0;
+ i++) {
+ if (strcmp(event->set_callback_counts[i].name, api) == 0) {
+ event->set_callback_counts[i].count = count;
+ if (event->set_callback_counts[i].count < 0) {
+ event->set_callback_counts[i].count = 0;
+ }
+ return;
+ }
+ }
+ if (i < total_callbacks && count > 0) {
+ strncpy(event->set_callback_counts[i].name, api,
+ sizeof(event->set_callback_counts[i].name));
+ event->set_callback_counts[i].count = count;
+ }
+}
+
+void bsg_notify_add_callback(bugsnag_event *event, const char *api) {
+ if (!internal_metrics_enabled) {
+ return;
+ }
+
+ bsg_modify_callback_count(event, api, 1);
+}
+
+void bsg_notify_remove_callback(bugsnag_event *event, const char *api) {
+ if (!internal_metrics_enabled) {
+ return;
+ }
+
+ bsg_modify_callback_count(event, api, -1);
+}
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.h b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.h
new file mode 100644
index 0000000000..e12b3383bf
--- /dev/null
+++ b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.h
@@ -0,0 +1,106 @@
+//
+// Created by Karl Stenerud on 05.09.22.
+//
+
+#ifndef BUGSNAG_ANDROID_INTERNAL_METRICS_H
+#define BUGSNAG_ANDROID_INTERNAL_METRICS_H
+
+#include "event.h"
+
+/**
+ * APIs whose calls will be recorded.
+ *
+ * This enum must remain consistent with bsg_called_api_names in bugsnag_ndk.c.
+ * The ordering must not be changed. Add new APIs to the end, not in
+ * alphabetical order. Naming of enums can be changed to denote deprecation
+ * (e.g. BSG_API_APP_GET_ID_DEPRECATED), but their enum values must not be
+ * re-used.
+ */
+typedef enum {
+ BSG_API_APP_GET_BINARY_ARCH = 0,
+ BSG_API_APP_GET_BUILD_UUID,
+ BSG_API_APP_GET_DURATION,
+ BSG_API_APP_GET_DURATION_IN_FOREGROUND,
+ BSG_API_APP_GET_ID,
+ BSG_API_APP_GET_IN_FOREGROUND,
+ BSG_API_APP_GET_IS_LAUNCHING,
+ BSG_API_APP_GET_RELEASE_STAGE,
+ BSG_API_APP_GET_TYPE,
+ BSG_API_APP_GET_VERSION,
+ BSG_API_APP_GET_VERSION_CODE,
+ BSG_API_APP_SET_BINARY_ARCH,
+ BSG_API_APP_SET_BUILD_UUID,
+ BSG_API_APP_SET_DURATION,
+ BSG_API_APP_SET_DURATION_IN_FOREGROUND,
+ BSG_API_APP_SET_ID,
+ BSG_API_APP_SET_IN_FOREGROUND,
+ BSG_API_APP_SET_IS_LAUNCHING,
+ BSG_API_APP_SET_RELEASE_STAGE,
+ BSG_API_APP_SET_TYPE,
+ BSG_API_APP_SET_VERSION,
+ BSG_API_APP_SET_VERSION_CODE,
+ BSG_API_DEVICE_GET_ID,
+ BSG_API_DEVICE_GET_JAILBROKEN,
+ BSG_API_DEVICE_GET_LOCALE,
+ BSG_API_DEVICE_GET_MANUFACTURER,
+ BSG_API_DEVICE_GET_MODEL,
+ BSG_API_DEVICE_GET_ORIENTATION,
+ BSG_API_DEVICE_GET_OS_NAME,
+ BSG_API_DEVICE_GET_OS_VERSION,
+ BSG_API_DEVICE_GET_TIME,
+ BSG_API_DEVICE_GET_TOTAL_MEMORY,
+ BSG_API_DEVICE_SET_ID,
+ BSG_API_DEVICE_SET_JAILBROKEN,
+ BSG_API_DEVICE_SET_LOCALE,
+ BSG_API_DEVICE_SET_MANUFACTURER,
+ BSG_API_DEVICE_SET_MODEL,
+ BSG_API_DEVICE_SET_ORIENTATION,
+ BSG_API_DEVICE_SET_OS_NAME,
+ BSG_API_DEVICE_SET_OS_VERSION,
+ BSG_API_DEVICE_SET_TIME,
+ BSG_API_DEVICE_SET_TOTAL_MEMORY,
+ BSG_API_ERROR_GET_ERROR_CLASS,
+ BSG_API_ERROR_GET_ERROR_MESSAGE,
+ BSG_API_ERROR_GET_ERROR_TYPE,
+ BSG_API_ERROR_SET_ERROR_CLASS,
+ BSG_API_ERROR_SET_ERROR_MESSAGE,
+ BSG_API_ERROR_SET_ERROR_TYPE,
+ BSG_API_EVENT_ADD_METADATA_BOOL,
+ BSG_API_EVENT_ADD_METADATA_DOUBLE,
+ BSG_API_EVENT_ADD_METADATA_STRING,
+ BSG_API_EVENT_CLEAR_METADATA,
+ BSG_API_EVENT_CLEAR_METADATA_SECTION,
+ BSG_API_EVENT_GET_API_KEY,
+ BSG_API_EVENT_GET_CONTEXT,
+ BSG_API_EVENT_GET_GROUPING_HASH,
+ BSG_API_EVENT_GET_METADATA_BOOL,
+ BSG_API_EVENT_GET_METADATA_DOUBLE,
+ BSG_API_EVENT_GET_METADATA_STRING,
+ BSG_API_EVENT_GET_SEVERITY,
+ BSG_API_EVENT_GET_STACKFRAME,
+ BSG_API_EVENT_GET_STACKTRACE_SIZE,
+ BSG_API_EVENT_GET_USER,
+ BSG_API_EVENT_HAS_METADATA,
+ BSG_API_EVENT_IS_UNHANDLED,
+ BSG_API_EVENT_SET_API_KEY,
+ BSG_API_EVENT_SET_CONTEXT,
+ BSG_API_EVENT_SET_GROUPING_HASH,
+ BSG_API_EVENT_SET_SEVERITY,
+ BSG_API_EVENT_SET_UNHANDLED,
+ BSG_API_EVENT_SET_USER,
+} bsg_called_api;
+
+extern const char *const bsg_called_api_names[];
+extern const int bsg_called_apis_count;
+
+void bsg_set_internal_metrics_enabled(bool enabled);
+
+void bsg_notify_api_called(bugsnag_event *event, bsg_called_api api);
+bool bsg_was_api_called(const bugsnag_event *event, bsg_called_api api);
+
+void bsg_set_callback_count(bugsnag_event *event, const char *api,
+ int32_t count);
+void bsg_notify_add_callback(bugsnag_event *event, const char *api);
+void bsg_notify_remove_callback(bugsnag_event *event, const char *api);
+
+#endif // BUGSNAG_ANDROID_INTERNAL_METRICS_H
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 74c0ea2e91..21b60ec282 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c
@@ -126,6 +126,7 @@ bool bsg_jni_cache_init(JNIEnv *env) {
}
CACHE_CLASS(Boolean, "java/lang/Boolean");
+ CACHE_METHOD(Boolean, Boolean_constructor, "", "(Z)V");
CACHE_METHOD(Boolean, Boolean_booleanValue, "booleanValue", "()Z");
CACHE_CLASS(Float, "java/lang/Float");
@@ -134,26 +135,50 @@ bool bsg_jni_cache_init(JNIEnv *env) {
CACHE_CLASS(number, "java/lang/Number");
CACHE_METHOD(number, number_double_value, "doubleValue", "()D");
+ CACHE_CLASS(Int, "java/lang/Integer");
+ CACHE_METHOD(Int, Int_constructor, "", "(I)V");
+ CACHE_METHOD(Int, Int_intValue, "intValue", "()I");
+
CACHE_CLASS(Long, "java/lang/Long");
CACHE_STATIC_METHOD(Long, Long_valueOf, "valueOf", "(J)Ljava/lang/Long;");
+ CACHE_METHOD(Long, Long_constructor, "", "(J)V");
CACHE_CLASS(String, "java/lang/String");
CACHE_CLASS(ArrayList, "java/util/ArrayList");
- CACHE_METHOD(ArrayList, ArrayList_constructor, "",
+ CACHE_METHOD(ArrayList, ArrayList_constructor_default, "", "()V");
+ CACHE_METHOD(ArrayList, ArrayList_constructor_collection, "",
"(Ljava/util/Collection;)V");
CACHE_METHOD(ArrayList, ArrayList_get, "get", "(I)Ljava/lang/Object;");
+ CACHE_METHOD(ArrayList, ArrayList_add, "add", "(Ljava/lang/Object;)Z");
+
+ CACHE_CLASS(Set, "java/util/Set");
+ CACHE_METHOD(Set, Set_iterator, "iterator", "()Ljava/util/Iterator;");
+
+ CACHE_CLASS(Iterator, "java/util/Iterator");
+ CACHE_METHOD(Iterator, Iterator_hasNext, "hasNext", "()Z");
+ CACHE_METHOD(Iterator, Iterator_next, "next", "()Ljava/lang/Object;");
CACHE_CLASS(Map, "java/util/Map");
CACHE_METHOD(Map, Map_keySet, "keySet", "()Ljava/util/Set;");
CACHE_METHOD(Map, Map_size, "size", "()I");
CACHE_METHOD(Map, Map_get, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ CACHE_METHOD(Map, Map_put, "put",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+ CACHE_METHOD(Map, Map_entrySet, "entrySet", "()Ljava/util/Set;");
+
+ CACHE_CLASS(MapEntry, "java/util/Map$Entry");
+ CACHE_METHOD(MapEntry, MapEntry_getKey, "getKey", "()Ljava/lang/Object;");
+ CACHE_METHOD(MapEntry, MapEntry_getValue, "getValue", "()Ljava/lang/Object;");
CACHE_CLASS(HashMap, "java/util/HashMap");
+ CACHE_METHOD(HashMap, HashMap_constructor, "", "()V");
CACHE_METHOD(HashMap, HashMap_keySet, "keySet", "()Ljava/util/Set;");
CACHE_METHOD(HashMap, HashMap_size, "size", "()I");
CACHE_METHOD(HashMap, HashMap_get, "get",
"(Ljava/lang/Object;)Ljava/lang/Object;");
+ CACHE_METHOD(HashMap, HashMap_put, "put",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
CACHE_CLASS(NativeInterface, "com/bugsnag/android/NativeInterface");
CACHE_STATIC_METHOD(NativeInterface, NativeInterface_getApp, "getApp",
@@ -174,7 +199,7 @@ bool bsg_jni_cache_init(JNIEnv *env) {
CACHE_STATIC_METHOD(NativeInterface, NativeInterface_isDiscardErrorClass,
"isDiscardErrorClass", "(Ljava/lang/String;)Z");
CACHE_STATIC_METHOD(NativeInterface, NativeInterface_deliverReport,
- "deliverReport", "([B[BLjava/lang/String;Z)V");
+ "deliverReport", "([B[B[BLjava/lang/String;Z)V");
CACHE_STATIC_METHOD(NativeInterface, NativeInterface_leaveBreadcrumb,
"leaveBreadcrumb",
"([BLcom/bugsnag/android/BreadcrumbType;)V");
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 72711b5972..9b3200379a 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h
+++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h
@@ -18,32 +18,55 @@ typedef struct {
JavaVM *jvm;
jclass Boolean;
+ jmethodID Boolean_constructor;
jmethodID Boolean_booleanValue;
+ jclass Int;
+ jmethodID Int_constructor;
+ jmethodID Int_intValue;
+
+ jclass Long;
+ jmethodID Long_constructor;
+ jmethodID Long_valueOf;
+
jclass Float;
jmethodID Float_floatValue;
jclass number;
jmethodID number_double_value;
- jclass Long;
- jmethodID Long_valueOf;
-
jclass String;
+ jclass Set;
+ jmethodID Set_iterator;
+
+ jclass Iterator;
+ jmethodID Iterator_hasNext;
+ jmethodID Iterator_next;
+
jclass Map;
jmethodID Map_get;
+ jmethodID Map_put;
jmethodID Map_size;
jmethodID Map_keySet;
+ jmethodID Map_entrySet;
+
+ jclass MapEntry;
+ jmethodID MapEntry_getKey;
+ jmethodID MapEntry_getValue;
jclass HashMap;
+ jmethodID HashMap_constructor;
jmethodID HashMap_get;
+ jmethodID HashMap_put;
jmethodID HashMap_size;
jmethodID HashMap_keySet;
jclass ArrayList;
- jmethodID ArrayList_constructor;
+ jmethodID ArrayList_constructor_default;
+ jmethodID ArrayList_constructor_collection;
jmethodID ArrayList_get;
+ jmethodID ArrayList_add;
jclass NativeInterface;
jmethodID NativeInterface_getApp;
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c
index 1516d459d8..818e0f7866 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c
@@ -440,9 +440,9 @@ static void populate_metadata_section(JNIEnv *env, bugsnag_metadata *dst,
goto exit;
}
- section_keylist =
- bsg_safe_new_object(env, bsg_jni_cache->ArrayList,
- bsg_jni_cache->ArrayList_constructor, section_keyset);
+ section_keylist = bsg_safe_new_object(
+ env, bsg_jni_cache->ArrayList,
+ bsg_jni_cache->ArrayList_constructor_collection, section_keyset);
if (section_keylist == NULL) {
goto exit;
}
@@ -495,7 +495,8 @@ void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst,
goto exit;
}
keylist = bsg_safe_new_object(env, bsg_jni_cache->ArrayList,
- bsg_jni_cache->ArrayList_constructor, keyset);
+ bsg_jni_cache->ArrayList_constructor_collection,
+ keyset);
if (keylist == NULL) {
goto exit;
}
@@ -536,7 +537,8 @@ void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb,
goto exit;
}
keylist = bsg_safe_new_object(env, bsg_jni_cache->ArrayList,
- bsg_jni_cache->ArrayList_constructor, keyset);
+ bsg_jni_cache->ArrayList_constructor_collection,
+ keyset);
if (keylist == NULL) {
goto exit;
}
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c
index 0dd2db043e..4975a9d535 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c
@@ -74,6 +74,18 @@ jstring bsg_safe_new_string_utf(JNIEnv *env, const char *str) {
return jstr;
}
+bool bsg_safe_call_void_method(JNIEnv *env, jobject _value, jmethodID method,
+ ...) {
+ if (env == NULL || _value == NULL || method == NULL) {
+ return false;
+ }
+ va_list args;
+ va_start(args, method);
+ (*env)->CallVoidMethodV(env, _value, method, args);
+ va_end(args);
+ return !bsg_check_and_clear_exc(env);
+}
+
jboolean bsg_safe_call_boolean_method(JNIEnv *env, jobject _value,
jmethodID method) {
if (env == NULL || _value == NULL || method == NULL) {
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/safejni.h b/bugsnag-plugin-android-ndk/src/main/jni/safejni.h
index f3447304b6..d618f9ce6f 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/safejni.h
+++ b/bugsnag-plugin-android-ndk/src/main/jni/safejni.h
@@ -2,6 +2,7 @@
#define BUGSNAG_SAFEJNI_H
#include
+#include
/**
* This provides safe JNI calls by wrapping functions and calling
@@ -49,6 +50,14 @@ jmethodID bsg_safe_get_static_method_id(JNIEnv *env, jclass clz,
*/
jstring bsg_safe_new_string_utf(JNIEnv *env, const char *str);
+/**
+ * A safe wrapper for the JNI's CallVoidMethod. This method checks if an
+ * exception is pending and if so clears it so that execution can continue.
+ * If an exception was thrown this method returns false.
+ */
+bool bsg_safe_call_void_method(JNIEnv *env, jobject _value, jmethodID method,
+ ...);
+
/**
* A safe wrapper for the JNI's CallBooleanMethod. This method checks if an
* exception is pending and if so clears it so that execution can continue.
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 a3e251ada2..a186e43ef8 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
@@ -7,9 +7,10 @@
#include
#include
#include
+#include
#include
-const int BSG_MIGRATOR_CURRENT_VERSION = 10;
+const int BSG_MIGRATOR_CURRENT_VERSION = 11;
#ifdef __cplusplus
extern "C" {
@@ -49,7 +50,9 @@ bugsnag_report_v8 *bsg_report_v8_read(int fd);
bugsnag_report_v9 *bsg_report_v9_read(int fd);
-bugsnag_event *bsg_report_v10_read(int fd);
+bugsnag_report_v10 *bsg_report_v10_read(int fd);
+
+bugsnag_event *bsg_report_v11_read(int fd);
/**
* the map_*() functions convert a structure of an older format into the latest.
@@ -57,6 +60,8 @@ bugsnag_event *bsg_report_v10_read(int fd);
* complete.
*/
+bugsnag_event *bsg_map_v10_to_report(bugsnag_report_v10 *report_v10);
+
bugsnag_event *bsg_map_v9_to_report(bugsnag_report_v9 *report_v9);
bugsnag_event *bsg_map_v8_to_report(bugsnag_report_v8 *report_v8);
@@ -145,8 +150,10 @@ bugsnag_event *bsg_read_event(char *filepath) {
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 10:
+ return bsg_map_v10_to_report(bsg_report_v10_read(fildes));
case BSG_MIGRATOR_CURRENT_VERSION:
- return bsg_report_v10_read(fildes);
+ return bsg_report_v11_read(fildes);
default:
return NULL;
}
@@ -271,7 +278,26 @@ bugsnag_report_v9 *bsg_report_v9_read(int fd) {
return event;
}
-bugsnag_event *bsg_report_v10_read(int fd) {
+bugsnag_report_v10 *bsg_report_v10_read(int fd) {
+ size_t event_size = sizeof(bugsnag_report_v10);
+ bugsnag_report_v10 *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_v11_read(int fd) {
size_t event_size = sizeof(bugsnag_event);
bugsnag_event *event = calloc(1, event_size);
@@ -290,6 +316,49 @@ bugsnag_event *bsg_report_v10_read(int fd) {
return event;
}
+bugsnag_event *bsg_map_v10_to_report(bugsnag_report_v10 *report_v10) {
+ if (report_v10 == NULL) {
+ return NULL;
+ }
+ bugsnag_event *event = calloc(1, sizeof(bugsnag_event));
+
+ if (event != NULL) {
+ event->notifier = report_v10->notifier;
+ memcpy(&event->metadata, &report_v10->metadata, sizeof(bugsnag_metadata));
+ memcpy(&event->app, &report_v10->app, sizeof(bsg_app_info));
+ memcpy(&event->device, &report_v10->device, sizeof(bsg_device_info));
+ event->user = report_v10->user;
+ memcpy(&event->error, &report_v10->error, sizeof(bsg_error));
+ event->crumb_count = report_v10->crumb_count;
+ event->crumb_first_index = report_v10->crumb_first_index;
+ memcpy(&event->breadcrumbs, &report_v10->breadcrumbs,
+ sizeof(report_v10->breadcrumbs));
+ memcpy(&event->context, report_v10->context, sizeof(report_v10->context));
+ event->severity = report_v10->severity;
+ memcpy(&event->session_id, report_v10->session_id,
+ sizeof(report_v10->session_id));
+ memcpy(&event->session_start, report_v10->session_start,
+ sizeof(report_v10->session_start));
+ event->handled_events = report_v10->handled_events;
+ event->unhandled_events = report_v10->unhandled_events;
+ memcpy(&event->grouping_hash, report_v10->grouping_hash,
+ sizeof(report_v10->grouping_hash));
+ event->unhandled = report_v10->unhandled;
+ memcpy(&event->api_key, report_v10->api_key, sizeof(report_v10->api_key));
+ event->thread_count = report_v10->thread_count;
+ memcpy(&event->threads, report_v10->threads, sizeof(report_v10->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_v10->feature_flags;
+ event->feature_flag_count = report_v10->feature_flag_count;
+
+ free(report_v10);
+ }
+ return event;
+}
+
bugsnag_event *bsg_map_v9_to_report(bugsnag_report_v9 *report_v9) {
if (report_v9 == NULL) {
return NULL;
@@ -1062,3 +1131,51 @@ void bsg_read_opaque_breadcrumb_metadata(int fd,
bsg_read_opaque_metadata(fd, &(breadcrumbs[breadcrumb_index].metadata));
}
}
+
+static bool read_from_file(int fd, ssize_t length, char *buffer) {
+ ssize_t bytes_read = 0;
+ ssize_t total_bytes_read = 0;
+ while (total_bytes_read < length) {
+ ssize_t bytes_to_read = length - total_bytes_read;
+ if ((bytes_read = read(fd, buffer + total_bytes_read, bytes_to_read)) < 0) {
+ return false;
+ }
+ total_bytes_read += bytes_read;
+ }
+ return true;
+}
+
+ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer) {
+ char *data = NULL;
+ ssize_t length = 0;
+ struct stat stats;
+ int fd = open(filename, O_RDONLY);
+ if (fd < 0) {
+ goto fail;
+ }
+ if (fstat(fd, &stats) < 0) {
+ goto fail;
+ }
+ length = (ssize_t)stats.st_size;
+ data = malloc(length + 1);
+ if (data == NULL) {
+ goto fail;
+ }
+ if (!read_from_file(fd, length, data)) {
+ goto fail;
+ }
+ data[length] = 0;
+ *buffer_pointer = data;
+ goto success;
+
+fail:
+ length = -1;
+success:
+ if (fd > 0) {
+ close(fd);
+ }
+ if (length < 0) {
+ free(data);
+ }
+ return length;
+}
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h
index 8a28b70b5f..a003946053 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h
+++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h
@@ -16,3 +16,12 @@ extern const int BSG_MIGRATOR_CURRENT_VERSION;
* @return An allocated event or NULL if no event could be read
*/
bugsnag_event *bsg_read_event(char *filepath);
+
+/**
+ * Read a text file from disk. Caller is responsible for freeing the buffer.
+ *
+ * @param filename The file to load
+ * @param buffer_pointer Pointer to the pointer to allocate a buffer.
+ * @return The length of the file, or -1 if the file could not be loaded.
+ */
+ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer);
diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c
index 8a009d81a7..92a5859a2e 100644
--- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c
+++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c
@@ -1,6 +1,7 @@
#include "event_writer.h"
#include
+#include
#include
#include "../string.h"
@@ -34,6 +35,17 @@ bool bsg_event_write(bsg_environment *env) {
bsg_write_opaque_metadata(&env->next_event, &writer);
writer.dispose(&writer);
+
+ if (result && env->static_json_data != NULL) {
+ // Attempt to write the static data, but don't worry if it fails.
+ // We'll check for truncated/missing static data on load.
+ if (bsg_buffered_writer_open(&writer, env->next_event_static_data_path)) {
+ writer.write(&writer, env->static_json_data,
+ strlen(env->static_json_data));
+ writer.dispose(&writer);
+ }
+ }
+
return result;
}
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 9194dd2cc1..7005ddf1f6 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
@@ -8,6 +8,7 @@
#include
#include "../logger.h"
+#include "internal_metrics.h"
const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) {
switch (type) {
@@ -217,7 +218,7 @@ void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj) {
}
void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj) {
- if (bugsnag_event_has_session(event)) {
+ if (bsg_event_has_session(event)) {
json_object_dotset_string(event_obj, "session.startedAt",
event->session_start);
json_object_dotset_string(event_obj, "session.id", event->session_id);
@@ -409,6 +410,37 @@ void bsg_serialize_feature_flags(const bugsnag_event *event,
}
}
+static void bsg_serialize_callbacks(const bugsnag_event *event,
+ JSON_Object *callbacks_obj) {
+ static const int callbacks_count =
+ sizeof(event->set_callback_counts) / sizeof(*event->set_callback_counts);
+
+ for (int i = 0; i < callbacks_count; i++) {
+ if (event->set_callback_counts[i].count > 0) {
+ json_object_set_number(callbacks_obj, event->set_callback_counts[i].name,
+ event->set_callback_counts[i].count);
+ }
+ }
+
+ for (int i = 0; i < bsg_called_apis_count; i++) {
+ if (bsg_was_api_called(event, i)) {
+ json_object_set_boolean(callbacks_obj, bsg_called_api_names[i], true);
+ }
+ }
+}
+
+static void bsg_serialize_usage(const bugsnag_event *event,
+ JSON_Object *event_obj) {
+ JSON_Value *usage_val = json_value_init_object();
+ JSON_Object *usage_obj = json_value_get_object(usage_val);
+ json_object_set_value(event_obj, "usage", usage_val);
+
+ JSON_Value *callbacks_val = json_value_init_object();
+ JSON_Object *callbacks = json_value_get_object(callbacks_val);
+ json_object_set_value(usage_obj, "callbacks", callbacks_val);
+ bsg_serialize_callbacks(event, callbacks);
+}
+
char *bsg_event_to_json(bugsnag_event *event) {
JSON_Value *event_val = json_value_init_object();
JSON_Object *event_obj = json_value_get_object(event_val);
@@ -446,6 +478,7 @@ char *bsg_event_to_json(bugsnag_event *event) {
bsg_serialize_breadcrumbs(event, crumbs);
bsg_serialize_threads(event, threads);
bsg_serialize_feature_flags(event, feature_flags);
+ bsg_serialize_usage(event, event_obj);
serialized_string = json_serialize_to_string(event_val);
json_value_free(event_val);
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 761b733d8e..56792d1b73 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
@@ -465,6 +465,46 @@ typedef struct {
bsg_feature_flag *feature_flags;
} bugsnag_report_v9;
+typedef struct {
+ bsg_notifier notifier;
+ bsg_app_info app;
+ bsg_device_info device;
+ bugsnag_user user;
+ bsg_error 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_v10;
+
#ifdef __cplusplus
}
#endif
diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt
index 86e0f83fb8..6e5ece11e4 100644
--- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt
+++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt
@@ -19,6 +19,7 @@ add_library(bugsnag-ndk-test SHARED
cpp/migrations/EventMigrationV8Tests.cpp
cpp/migrations/EventMigrationV9Tests.cpp
cpp/migrations/EventMigrationV10Tests.cpp
+ cpp/migrations/EventMigrationV11Tests.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
index 52c8ff8231..4e606fb441 100644
--- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV10Tests.cpp
+++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV10Tests.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_v10 *)calloc(1, sizeof(bugsnag_report_v10));
strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20");
strcpy(event->notifier.name, "Test Library");
@@ -16,10 +17,10 @@ static void *create_payload_info_event() {
}
/**
- * Create a new event in v9 format
+ * Create a new event in v10 format
*/
static void *create_full_event() {
- auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event));
+ auto event = (bugsnag_report_v10 *)calloc(1, sizeof(bugsnag_report_v10));
strcpy(event->context,
"00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c");
@@ -129,7 +130,101 @@ static void *create_full_event() {
return event;
}
-static const char *write_event_v9(JNIEnv *env, jstring temp_file,
+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_v10 *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;
+}
+
+static bool bsg_write_opaque_metadata(bugsnag_report_v10 *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;
+}
+
+static bool bsg_event_write_v10(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_v10)) &&
+ // append feature flags after event structure
+ bsg_write_feature_flags((bugsnag_report_v10*)&env->next_event, &writer) &&
+ // append opaque metadata after the feature flags
+ bsg_write_opaque_metadata((bugsnag_report_v10*)&env->next_event, &writer);
+
+ writer.dispose(&writer);
+ return result;
+}
+
+static const char *write_event_v10(JNIEnv *env, jstring temp_file,
void *(event_generator)()) {
auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment));
event_ctx->report_header.version = 10;
@@ -138,13 +233,11 @@ 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_v10));
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);
+ if (!bsg_event_write_v10(event_ctx)) {
+ path = nullptr;
+ }
free(event_ctx);
return path;
}
@@ -156,7 +249,10 @@ extern "C" {
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);
+ const char *path = write_event_v10(env, temp_file, create_payload_info_event);
+ if (path == nullptr) {
+ return nullptr;
+ }
// file on disk -> latest event type
bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path);
@@ -180,7 +276,10 @@ Java_com_bugsnag_android_ndk_migrations_EventMigrationV10Tests_migratePayloadInf
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);
+ const char *path = write_event_v10(env, temp_file, create_full_event);
+ if (path == nullptr) {
+ return;
+ }
// file on disk -> latest event type
bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path);
diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV11Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV11Tests.cpp
new file mode 100644
index 0000000000..24d24b135c
--- /dev/null
+++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV11Tests.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 v10 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_v10(JNIEnv *env, jstring temp_file,
+ void *(event_generator)()) {
+ auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment));
+ event_ctx->report_header.version = 11;
+ 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 v12 rolls around, the v11 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_EventMigrationV11Tests_migratePayloadInfo(
+ JNIEnv *env, jobject _this, jstring temp_file) {
+ const char *path = write_event_v10(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_EventMigrationV11Tests_migrateEvent(
+ JNIEnv *env, jobject _this, jstring temp_file) {
+ const char *path = write_event_v10(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/test_breadcrumbs.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c
index 0c0ef207b0..5abb8c33e7 100644
--- a/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c
+++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c
@@ -22,7 +22,7 @@ bugsnag_breadcrumb *init_breadcrumb(const char *name, const char *message, bugsn
TEST test_add_breadcrumb(void) {
bugsnag_event *event = calloc(1, sizeof(bugsnag_event));
bugsnag_breadcrumb *crumb = init_breadcrumb("stroll", "this is a drill.", BSG_CRUMB_USER);
- bugsnag_event_add_breadcrumb(event, crumb);
+ bsg_event_add_breadcrumb(event, crumb);
ASSERT_EQ(1, event->crumb_count);
ASSERT_EQ(0, event->crumb_first_index);
ASSERT(strcmp("stroll", event->breadcrumbs[0].name) == 0);
@@ -30,7 +30,7 @@ TEST test_add_breadcrumb(void) {
ASSERT(strcmp("this is a drill.", event->breadcrumbs[0].metadata.values[0].char_value) == 0);
free(crumb);
bugsnag_breadcrumb *crumb2 = init_breadcrumb("walking...", "this is not a drill.", BSG_CRUMB_USER);
- bugsnag_event_add_breadcrumb(event, crumb2);
+ bsg_event_add_breadcrumb(event, crumb2);
ASSERT_EQ(2, event->crumb_count);
ASSERT_EQ(0, event->crumb_first_index);
ASSERT(strcmp("stroll", event->breadcrumbs[0].name) == 0);
@@ -54,7 +54,7 @@ TEST test_add_breadcrumbs_over_max(void) {
memset(format, 0, sizeof(char) * breadcrumb_count);
sprintf(format, "crumb: %d", i);
bugsnag_breadcrumb *crumb = init_breadcrumb(format, "go go go", BSG_CRUMB_USER);
- bugsnag_event_add_breadcrumb(event, crumb);
+ bsg_event_add_breadcrumb(event, crumb);
free(crumb);
free(format);
}
diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c
index 8376cc97d5..debf5f68a6 100644
--- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c
+++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c
@@ -171,10 +171,10 @@ void generate_basic_report(bugsnag_event *event) {
event->crumb_count = 0;
event->crumb_first_index = 0;
bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º", BSG_CRUMB_STATE);
- bugsnag_event_add_breadcrumb(event, crumb1);
+ bsg_event_add_breadcrumb(event, crumb1);
bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.", BSG_CRUMB_USER);
- bugsnag_event_add_breadcrumb(event, crumb2);
+ bsg_event_add_breadcrumb(event, crumb2);
event->handled_events = 1;
event->unhandled_events = 1;
@@ -456,11 +456,11 @@ bugsnag_event *bsg_generate_event(void) {
report->crumb_first_index = 0;
bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º",
BSG_CRUMB_STATE);
- bugsnag_event_add_breadcrumb(report, crumb1);
+ bsg_event_add_breadcrumb(report, crumb1);
bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.",
BSG_CRUMB_USER);
- bugsnag_event_add_breadcrumb(report, crumb2);
+ bsg_event_add_breadcrumb(report, crumb2);
report->handled_events = 1;
report->unhandled_events = 1;
diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java
index 52be99c00c..4408361a25 100644
--- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java
+++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java
@@ -9,6 +9,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
@@ -24,7 +25,7 @@ static ImmutableConfig generateConfig() throws IOException {
Collections.singleton("production"),
Collections.singleton("com.example"),
new HashSet<>(Collections.singletonList(BreadcrumbType.MANUAL)),
- new HashSet<>(Collections.singletonList(Telemetry.INTERNAL_ERRORS)),
+ EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE),
"production",
"builduuid-123",
"1.4.3",
@@ -50,6 +51,7 @@ public File invoke() {
}
}),
true,
+ true,
null,
null,
Collections.singleton("password")
diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle
index 4707086442..65324eec46 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.26.0"
+ implementation "com.bugsnag:bugsnag-android:5.27.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/examples/sdk-app-example/app/src/main/cpp/entrypoint.cpp b/examples/sdk-app-example/app/src/main/cpp/entrypoint.cpp
index b78cae3e41..4a9c3ae31e 100644
--- a/examples/sdk-app-example/app/src/main/cpp/entrypoint.cpp
+++ b/examples/sdk-app-example/app/src/main/cpp/entrypoint.cpp
@@ -31,7 +31,7 @@ bool my_on_error_b(void *event) {
JNIEXPORT void JNICALL
Java_com_example_bugsnag_android_ExampleApplication_performNativeBugsnagSetup(
JNIEnv *env, jobject instance) {
- bugsnag_add_on_error(&my_on_error_b);
+ bugsnag_add_on_error(my_on_error_b);
}
JNIEXPORT void JNICALL
diff --git a/features/fixtures/mazerunner/app/proguard-rules.pro b/features/fixtures/mazerunner/app/proguard-rules.pro
index c82de9bb2b..ba642cad1e 100644
--- a/features/fixtures/mazerunner/app/proguard-rules.pro
+++ b/features/fixtures/mazerunner/app/proguard-rules.pro
@@ -1 +1,2 @@
-keep class com.bugsnag.android.mazerunner.** {*;}
+-keep class com.bugsnag.android.DeliveryDelegate {*;}
diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt
index cdbb900291..0b8aad1e31 100644
--- a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt
+++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt
@@ -24,6 +24,7 @@ fun Application.triggerStartupAnrIfRequired() {
// wait for Bugsnag's ANR handler to install first
Handler(Looper.getMainLooper()).post {
+ Log.i("StartupANR", "Going to sleep for $startupDelay seconds to trigger a startup ANR")
thread {
// This is a dirty hack to work around the limitations of our current testing
// systems - where external key-events are pushed through our main thread (which we
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml
index cd3b93f0e7..48d0c94c28 100644
--- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml
@@ -4,5 +4,10 @@
MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$500
MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$999
+ MagicNumber:CXXExceptionWithUsageScenario.kt$CXXExceptionWithUsageScenario$10
+ MagicNumber:CXXExceptionWithUsageScenario.kt$CXXExceptionWithUsageScenario$1000
+ MagicNumber:CXXSigsegvWithUsageScenario.kt$CXXSigsegvWithUsageScenario$10
+ MagicNumber:HandledExceptionWithUsageScenario.kt$HandledExceptionWithUsageScenario$10
+ MagicNumber:UnhandledExceptionWithUsageScenario.kt$UnhandledExceptionWithUsageScenario$10
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/cxx-scenarios-bugsnag.cpp b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/cxx-scenarios-bugsnag.cpp
index e355f5feef..151ccdcfbe 100644
--- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/cxx-scenarios-bugsnag.cpp
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/cpp/cxx-scenarios-bugsnag.cpp
@@ -329,4 +329,53 @@ Java_com_bugsnag_android_mazerunner_scenarios_CXXCaptureThreadsNotifyScenario_ac
(char *)"CXXCaptureThreadStatesNotifyScenario", BSG_SEVERITY_ERR);
}
+static bool on_error_sigsegv_with_usage(void* event) {
+ bugsnag_app_set_binary_arch(event, "something weird");
+ bugsnag_device_get_model(event);
+ bugsnag_event_get_severity(event);
+ return true;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_bugsnag_android_mazerunner_scenarios_CXXSigsegvWithUsageScenario_crash(JNIEnv *env,
+ jobject thiz,
+ jint value) {
+ bugsnag_add_on_error(on_error_sigsegv_with_usage);
+ bugsnag_set_user("id", "email", "name");
+ int x = 38;
+ if (value > 0) {
+ raise(SIGSEGV);
+ }
+ printf("In thousands of simulations, that's literally never happened before.\n");
+ return value / x / 8;
+}
+
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_mazerunner_scenarios_CXXExceptionWithUsageScenario_crash(JNIEnv *env,
+ jobject instance) {
+ int x = 61;
+ printf("This one here: %ld\n", (long) f_trigger_an_exception(x > 0));
+}
+
+static bool on_error_do_nothing(void* event) {
+ return true;
+}
+
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_mazerunner_scenarios_HandledExceptionWithUsageScenario_cxxsetup(
+ JNIEnv *env, jobject thiz) {
+ bugsnag_set_user("id", "email", "name");
+ bugsnag_add_on_error(on_error_do_nothing);
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_bugsnag_android_mazerunner_scenarios_UnhandledExceptionWithUsageScenario_cxxsetup(
+ JNIEnv *env, jobject thiz) {
+ bugsnag_set_user("id", "email", "name");
+ bugsnag_add_on_error(on_error_do_nothing);
+}
+
}
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExceptionWithUsageScenario.kt b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExceptionWithUsageScenario.kt
new file mode 100644
index 0000000000..4c1bfac183
--- /dev/null
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXExceptionWithUsageScenario.kt
@@ -0,0 +1,52 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.Configuration
+import com.bugsnag.android.OnBreadcrumbCallback
+import com.bugsnag.android.OnErrorCallback
+import com.bugsnag.android.OnSessionCallback
+import com.bugsnag.android.Telemetry
+
+class CXXExceptionWithUsageScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String?
+) : Scenario(config, context, eventMetadata) {
+
+ companion object {
+ init {
+ System.loadLibrary("cxx-scenarios-bugsnag")
+ }
+ }
+
+ init {
+ if (eventMetadata != "USAGE") {
+ config.setTelemetry(config.getTelemetry().filter { it != Telemetry.USAGE }.toSet())
+ } else {
+ config.setTelemetry(config.getTelemetry() + Telemetry.USAGE)
+ }
+ config.maxBreadcrumbs = 10
+ config.autoTrackSessions = false
+ config.discardClasses = setOf("one", "two", "three")
+ config.maxPersistedSessions = 1000
+ val breadcrumbCb = OnBreadcrumbCallback { true }
+ config.addOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.addOnBreadcrumb(OnBreadcrumbCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ }
+
+ external fun crash()
+
+ override fun startScenario() {
+ super.startScenario()
+ Bugsnag.addOnSession { true }
+ crash()
+ }
+}
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXJavaBreadcrumbNativeBreadcrumbScenario.java b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXJavaBreadcrumbNativeBreadcrumbScenario.java
index 3b44f8a5bc..391a7143c5 100644
--- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXJavaBreadcrumbNativeBreadcrumbScenario.java
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXJavaBreadcrumbNativeBreadcrumbScenario.java
@@ -15,7 +15,7 @@ public class CXXJavaBreadcrumbNativeBreadcrumbScenario extends Scenario {
System.loadLibrary("cxx-scenarios-bugsnag");
}
- public native void activate();
+ public native int activate();
public CXXJavaBreadcrumbNativeBreadcrumbScenario(@NonNull Configuration config,
@NonNull Context context,
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXSigsegvWithUsageScenario.kt b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXSigsegvWithUsageScenario.kt
new file mode 100644
index 0000000000..d76d5ea4db
--- /dev/null
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXSigsegvWithUsageScenario.kt
@@ -0,0 +1,50 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.Configuration
+import com.bugsnag.android.OnBreadcrumbCallback
+import com.bugsnag.android.OnErrorCallback
+import com.bugsnag.android.OnSessionCallback
+import com.bugsnag.android.Telemetry
+
+class CXXSigsegvWithUsageScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String?
+) : Scenario(config, context, eventMetadata) {
+
+ companion object {
+ init {
+ System.loadLibrary("cxx-scenarios-bugsnag")
+ }
+ }
+
+ init {
+ if (eventMetadata != "USAGE") {
+ config.setTelemetry(config.getTelemetry().filter { it != Telemetry.USAGE }.toSet())
+ } else {
+ config.setTelemetry(config.getTelemetry() + Telemetry.USAGE)
+ }
+ config.maxBreadcrumbs = 10
+ config.autoTrackSessions = false
+ val breadcrumbCb = OnBreadcrumbCallback { true }
+ config.addOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.addOnBreadcrumb(OnBreadcrumbCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ }
+
+ external fun crash(value: Int): Int
+
+ override fun startScenario() {
+ super.startScenario()
+ Bugsnag.addOnSession { true }
+ crash(1)
+ }
+}
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledExceptionWithUsageScenario.kt b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledExceptionWithUsageScenario.kt
new file mode 100644
index 0000000000..7e1a60a289
--- /dev/null
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledExceptionWithUsageScenario.kt
@@ -0,0 +1,52 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.Configuration
+import com.bugsnag.android.OnBreadcrumbCallback
+import com.bugsnag.android.OnErrorCallback
+import com.bugsnag.android.OnSessionCallback
+import com.bugsnag.android.Telemetry
+
+internal class HandledExceptionWithUsageScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String
+) : Scenario(config, context, eventMetadata) {
+
+ companion object {
+ init {
+ System.loadLibrary("cxx-scenarios-bugsnag")
+ }
+ }
+
+ init {
+ if (eventMetadata != "USAGE") {
+ config.setTelemetry(config.getTelemetry().filter { it != Telemetry.USAGE }.toSet())
+ } else {
+ config.setTelemetry(config.getTelemetry() + Telemetry.USAGE)
+ }
+ config.maxBreadcrumbs = 10
+ config.autoTrackSessions = false
+ config.addOnSend { true }
+ val breadcrumbCb = OnBreadcrumbCallback { true }
+ config.addOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.addOnBreadcrumb(OnBreadcrumbCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ }
+
+ external fun cxxsetup()
+
+ override fun startScenario() {
+ super.startScenario()
+ cxxsetup()
+ Bugsnag.addOnError { true }
+ Bugsnag.notify(generateException())
+ }
+}
diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionWithUsageScenario.kt b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionWithUsageScenario.kt
new file mode 100644
index 0000000000..e89e94134e
--- /dev/null
+++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionWithUsageScenario.kt
@@ -0,0 +1,55 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Bugsnag
+import com.bugsnag.android.Configuration
+import com.bugsnag.android.OnBreadcrumbCallback
+import com.bugsnag.android.OnErrorCallback
+import com.bugsnag.android.OnSessionCallback
+import com.bugsnag.android.Telemetry
+
+class UnhandledExceptionWithUsageScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String
+) : Scenario(config, context, eventMetadata) {
+
+ companion object {
+ init {
+ System.loadLibrary("cxx-scenarios-bugsnag")
+ }
+ }
+
+ val onSessionCallback = OnSessionCallback { true }
+
+ init {
+ if (eventMetadata != "USAGE") {
+ config.setTelemetry(config.getTelemetry().filter { it != Telemetry.USAGE }.toSet())
+ } else {
+ config.setTelemetry(config.getTelemetry() + Telemetry.USAGE)
+ }
+ config.maxBreadcrumbs = 10
+ config.autoTrackSessions = false
+ val breadcrumbCb = OnBreadcrumbCallback { true }
+ config.addOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.removeOnBreadcrumb(breadcrumbCb)
+ config.addOnBreadcrumb(OnBreadcrumbCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnError(OnErrorCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(OnSessionCallback { true })
+ config.addOnSession(onSessionCallback)
+ }
+
+ external fun cxxsetup()
+
+ override fun startScenario() {
+ super.startScenario()
+ cxxsetup()
+ Bugsnag.addOnBreadcrumb { true }
+ Bugsnag.addOnBreadcrumb { true }
+ Bugsnag.removeOnSession(onSessionCallback)
+ throw generateException()
+ }
+}
diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp
index f5a65c5760..d71da46bf1 100644
--- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp
+++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp
@@ -273,4 +273,5 @@ JNIEXPORT void JNICALL
Java_com_bugsnag_android_mazerunner_scenarios_UnhandledNdkAutoNotifyFalseScenario_crash(JNIEnv *env) {
abort();
}
+
}
diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml
index 20bbeafdcd..3ebc1a1b02 100644
--- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml
+++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml
@@ -31,6 +31,7 @@
ThrowingExceptionsWithoutMessageOrCause:TrimmedStacktraceScenario.kt$TrimmedStacktraceScenario$RuntimeException()
TooGenericExceptionThrown:CrashHandlerScenario.kt$CrashHandlerScenario$throw RuntimeException("CrashHandlerScenario")
TooGenericExceptionThrown:CustomHttpClientFlushScenario.kt$CustomHttpClientFlushScenario$throw RuntimeException("ReportCacheScenario")
+ TooGenericExceptionThrown:DeliverOnCrashScenario.kt$DeliverOnCrashScenario$throw RuntimeException("DeliverOnCrashScenario")
TooGenericExceptionThrown:DisableAutoDetectErrorsScenario.kt$DisableAutoDetectErrorsScenario$throw RuntimeException("Should never appear")
TooGenericExceptionThrown:FeatureFlagScenario.kt$FeatureFlagScenario$throw RuntimeException("FeatureFlagScenario unhandled")
TooGenericExceptionThrown:OnSendCallbackScenario.kt$OnSendCallbackScenario$throw RuntimeException("Unhandled Error")
diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java
index a66a9b8e8f..a7fadea1fc 100644
--- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java
+++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java
@@ -50,4 +50,9 @@ public DeliveryStatus deliver(@NonNull Session payload,
}
};
}
+
+ @NonNull
+ public static Delivery createDefaultDelivery() {
+ return new DefaultDelivery(null, NoopLogger.INSTANCE);
+ }
}
diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt
index b2a5b205d3..2d8c2f6987 100644
--- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt
+++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt
@@ -57,22 +57,7 @@ internal fun createCustomHeaderDelivery(): Delivery {
}
}
-fun createDefaultDelivery(): Delivery { // use reflection as DefaultDelivery is internal
- val clz = Class.forName("com.bugsnag.android.DefaultDelivery")
- return clz.constructors[0].newInstance(
- null,
- object : Logger {
- override fun e(msg: String) = Unit
- override fun e(msg: String, throwable: Throwable) = Unit
- override fun w(msg: String) = Unit
- override fun w(msg: String, throwable: Throwable) = Unit
- override fun i(msg: String) = Unit
- override fun i(msg: String, throwable: Throwable) = Unit
- override fun d(msg: String) = Unit
- override fun d(msg: String, throwable: Throwable) = Unit
- }
- ) as Delivery
-}
+fun createDefaultDelivery(): Delivery = JavaHooks.createDefaultDelivery()
internal fun writeErrorToStore(client: Client, event: Event) {
client.eventStore.write(event)
diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/DeliverOnCrashScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/DeliverOnCrashScenario.kt
new file mode 100644
index 0000000000..7854c95662
--- /dev/null
+++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/DeliverOnCrashScenario.kt
@@ -0,0 +1,27 @@
+package com.bugsnag.android.mazerunner.scenarios
+
+import android.content.Context
+import com.bugsnag.android.Configuration
+
+private const val TIMEOUT = 15_000L
+
+internal class DeliverOnCrashScenario(
+ config: Configuration,
+ context: Context,
+ eventMetadata: String
+) : Scenario(config, context, eventMetadata) {
+
+ init {
+ config.isAttemptDeliveryOnCrash = true
+ val deliveryDelegate = Class.forName("com.bugsnag.android.DeliveryDelegate")
+ deliveryDelegate.getDeclaredField("DELIVERY_TIMEOUT").apply {
+ isAccessible = true
+ set(null, TIMEOUT)
+ }
+ }
+
+ override fun startScenario() {
+ super.startScenario()
+ throw RuntimeException("DeliverOnCrashScenario")
+ }
+}
diff --git a/features/full_tests/crash_handler.feature b/features/full_tests/crash_handler.feature
index a401483129..98b6ff4e8b 100644
--- a/features/full_tests/crash_handler.feature
+++ b/features/full_tests/crash_handler.feature
@@ -12,3 +12,12 @@ Feature: Reporting with other exception handlers installed
And the exception "errorClass" equals "java.lang.RuntimeException"
And the exception "message" equals "CrashHandlerScenario"
And the event "metaData.customHandler.invoked" is true
+
+ Scenario: Delivers crashes synchronously when configured
+ When I run "DeliverOnCrashScenario"
+ And I wait to receive an error
+ 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 "java.lang.RuntimeException"
+ And the exception "message" equals "DeliverOnCrashScenario"
+ And the event "usage.config.attemptDeliveryOnCrash" is true
diff --git a/features/full_tests/native_crash_handling.feature b/features/full_tests/native_crash_handling.feature
index 21ad8d64d0..42e9422607 100644
--- a/features/full_tests/native_crash_handling.feature
+++ b/features/full_tests/native_crash_handling.feature
@@ -3,6 +3,8 @@ Feature: Native crash reporting
Background:
Given I clear all persistent data
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Dereference a null pointer
When I run "CXXDereferenceNullScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXDereferenceNullScenario"
@@ -21,6 +23,9 @@ Feature: Native crash reporting
| get_the_null_value() | CXXDereferenceNullScenario.cpp | 7 |
And the "codeIdentifier" of stack frame 0 is not null
+
+ # TODO: Several scenarios skipped on Android 7.1 (Google Pixel) do to additional stack frames. See PLAT-9011
+
# This scenario will not pass on API levels < 18, as stack corruption
# is handled without calling atexit handlers, etc.
# In the device logs you will see:
@@ -30,6 +35,8 @@ Feature: Native crash reporting
# Refactored here to use abort() on newer versions:
# https://android.googlesource.com/platform/bionic/+/fb7eb5e07f43587c2bedf2aaa53b21fa002417bb
@skip_below_api18
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Stack buffer overflow
When I run "CXXStackoverflowScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXStackoverflowScenario"
@@ -43,6 +50,8 @@ Feature: Native crash reporting
| crash_stack_overflow | CXXStackoverflowScenario.cpp |
And the "codeIdentifier" of stack frame 0 is not null
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Program trap()
When I run "CXXTrapScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXTrapScenario"
@@ -61,6 +70,8 @@ Feature: Native crash reporting
| trap_it() | CXXTrapScenario.cpp | 12 |
And the "codeIdentifier" of stack frame 0 is not null
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Write to read-only memory
When I run "CXXWriteReadOnlyMemoryScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXWriteReadOnlyMemoryScenario"
@@ -75,6 +86,8 @@ Feature: Native crash reporting
| Java_com_bugsnag_android_mazerunner_scenarios_CXXWriteReadOnlyMemoryScenario_crash | CXXWriteReadOnlyMemoryScenario.cpp | 22 |
And the "codeIdentifier" of stack frame 0 is not null
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Improper object type cast
When I run "CXXImproperTypecastScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXImproperTypecastScenario"
@@ -89,6 +102,8 @@ Feature: Native crash reporting
| Java_com_bugsnag_android_mazerunner_scenarios_CXXImproperTypecastScenario_crash | CXXImproperTypecastScenario.cpp | 20 |
And the "codeIdentifier" of stack frame 0 is not null
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Program abort()
When I run "CXXAbortScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXAbortScenario"
@@ -121,6 +136,8 @@ Feature: Native crash reporting
# Android 6 dladdr does report .so files that are not extracted from their .apk file
# this test cannot pass on these devices with extractNativeLibs=false
@skip_android_6
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Causing a crash in a separate library
When I run "CXXExternalStackElementScenario" and relaunch the crashed app
And I configure Bugsnag for "CXXExternalStackElementScenario"
@@ -140,6 +157,8 @@ Feature: Native crash reporting
| Java_com_bugsnag_android_mazerunner_scenarios_CXXExternalStackElementScenario_crash | CXXExternalStackElementScenario.cpp | 20 |
And the "codeIdentifier" of stack frame 0 is not null
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Call null function pointer
A null pointer should be the first element of a stack trace,
followed by the calling function
diff --git a/features/full_tests/native_throw_crash.feature b/features/full_tests/native_throw_crash.feature
index bfe83249d8..5567f00ad1 100644
--- a/features/full_tests/native_throw_crash.feature
+++ b/features/full_tests/native_throw_crash.feature
@@ -23,6 +23,8 @@ Feature: Native crash reporting with thrown objects
And the exception "errorClass" equals "SIGABRT"
And the exception "message" equals "Abort program"
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Rethrow in C++ without initial exception
When I run "CXXInvalidRethrow" and relaunch the crashed app
And I configure Bugsnag for "CXXInvalidRethrow"
@@ -35,6 +37,8 @@ Feature: Native crash reporting with thrown objects
And the first significant stack frames match:
| print_last_exception() | CXXInvalidRethrow.cpp | 7 |
+ # TODO: Pending PLAT-9011
+ @skip_android_7
Scenario: Throw from C++ noexcept function
When I run "CXXThrowFromNoexcept" and relaunch the crashed app
And I configure Bugsnag for "CXXThrowFromNoexcept"
diff --git a/features/full_tests/startup_anr.feature b/features/full_tests/startup_anr.feature
index acd91ed506..2dcf1657bc 100644
--- a/features/full_tests/startup_anr.feature
+++ b/features/full_tests/startup_anr.feature
@@ -7,6 +7,9 @@ Feature: onCreate ANR
# and then terminate the "misbehaving" app with a KILL (9) signal (almost like the ANR code doesn't
# fire at all). Since we can't cover a KILL signal in a test, we skip Android 10.
@skip_android_10
+# Android 13 Note: we no longer have permission to inject BACK button events, which are used to
+# trigger the ANR - so the test is not valid on Android 13 either
+ @skip_android_13
Scenario: onCreate ANR is reported
When I clear any error dialogue
And I run "ConfigureStartupAnrScenario"
diff --git a/features/full_tests/usage.feature b/features/full_tests/usage.feature
new file mode 100644
index 0000000000..6cb4062663
--- /dev/null
+++ b/features/full_tests/usage.feature
@@ -0,0 +1,150 @@
+Feature: Reporting Errors with usage info
+
+ Background:
+ Given I clear all persistent data
+
+ Scenario: Report a handled exception with custom configuration and set callbacks
+ When I configure the app to run in the "USAGE" state
+ And I run "HandledExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "java.lang.RuntimeException"
+ And the event "exceptions.0.message" equals "HandledExceptionWithUsageScenario"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" equals 10
+ And the event "usage.config.autoTrackSessions" is false
+ And the event "usage.callbacks.event_set_user" is true
+ And the event "usage.callbacks.ndkOnError" equals 1
+ And the event "usage.callbacks.onBreadcrumb" equals 1
+ And the event "usage.callbacks.onError" equals 3
+ And the event "usage.callbacks.onSession" equals 3
+
+ Scenario: Report a handled exception with custom configuration and set callbacks, usage disabled
+ When I configure the app to run in the "disable-usage" state
+ And I run "HandledExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "java.lang.RuntimeException"
+ And the event "exceptions.0.message" equals "HandledExceptionWithUsageScenario"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" is null
+ And the event "usage.config.autoTrackSessions" is null
+ And the event "usage.callbacks.event_set_user" is null
+ And the event "usage.callbacks.ndkOnError" is null
+ And the event "usage.callbacks.onBreadcrumb" is null
+ And the event "usage.callbacks.onError" is null
+ And the event "usage.callbacks.onSession" is null
+
+ Scenario: Report an unhandled exception with custom configuration and set callbacks
+ When I configure the app to run in the "USAGE" state
+ And I run "UnhandledExceptionWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "UnhandledExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "java.lang.RuntimeException"
+ And the event "exceptions.0.message" equals "UnhandledExceptionWithUsageScenario"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" equals 10
+ And the event "usage.config.autoTrackSessions" is false
+ And the event "usage.callbacks.event_set_user" is true
+ And the event "usage.callbacks.ndkOnError" equals 1
+ And the event "usage.callbacks.onBreadcrumb" equals 3
+ And the event "usage.callbacks.onError" equals 2
+ And the event "usage.callbacks.onSession" equals 2
+
+ Scenario: Report an unhandled exception with custom configuration and set callbacks, usage disabled
+ When I configure the app to run in the "disable-usage" state
+ And I run "UnhandledExceptionWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "UnhandledExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "java.lang.RuntimeException"
+ And the event "exceptions.0.message" equals "UnhandledExceptionWithUsageScenario"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" is null
+ And the event "usage.config.autoTrackSessions" is null
+ And the event "usage.callbacks.event_set_user" is null
+ And the event "usage.callbacks.ndkOnError" is null
+ And the event "usage.callbacks.onBreadcrumb" is null
+ And the event "usage.callbacks.onError" is null
+ And the event "usage.callbacks.onSession" is null
+
+ Scenario: Report a native exception with custom configuration and set callbacks
+ When I configure the app to run in the "USAGE" state
+ And I run "CXXExceptionWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "CXXExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" equals 10
+ And the event "usage.config.autoTrackSessions" is false
+ And the event "usage.config.discardClassesCount" equals 3
+ And the event "usage.config.maxPersistedSessions" equals 1000
+ And the event "usage.callbacks.onBreadcrumb" equals 1
+ And the event "usage.callbacks.onError" equals 2
+ And the event "usage.callbacks.onSession" equals 4
+
+ Scenario: Report a native exception with custom configuration and set callbacks, usage disabled
+ When I configure the app to run in the "disable-usage" state
+ And I run "CXXExceptionWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "CXXExceptionWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "usage.config.maxBreadcrumbs" is null
+ And the event "usage.config.autoTrackSessions" is null
+ And the event "usage.config.discardClassesCount" is null
+ And the event "usage.config.maxPersistedSessions" is null
+ And the event "usage.callbacks.onBreadcrumb" is null
+ And the event "usage.callbacks.onError" is null
+ And the event "usage.callbacks.onSession" is null
+
+ Scenario: Report a native exception with custom configuration and set callbacks
+ When I configure the app to run in the "USAGE" state
+ And I run "CXXSigsegvWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "CXXSigsegvWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "SIGSEGV"
+ And the event "exceptions.0.message" equals "Segmentation violation (invalid memory reference)"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "app.binaryArch" equals "something weird"
+ And the event "usage.config.maxBreadcrumbs" equals 10
+ And the event "usage.config.autoTrackSessions" is false
+ And the event "usage.callbacks.ndkOnError" equals 1
+ And the event "usage.callbacks.onBreadcrumb" equals 1
+ And the event "usage.callbacks.onError" equals 2
+ And the event "usage.callbacks.onSession" equals 4
+ And the event "usage.callbacks.event_set_user" is true
+ And the event "usage.callbacks.app_set_binary_arch" is true
+ And the event "usage.callbacks.device_get_model" is true
+ And the event "usage.callbacks.event_get_severity" is true
+
+ Scenario: Report a native exception with custom configuration and set callbacks, usage disabled
+ When I configure the app to run in the "disable-usage" state
+ And I run "CXXSigsegvWithUsageScenario" and relaunch the crashed app
+ And I configure Bugsnag for "CXXSigsegvWithUsageScenario"
+ 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 error payload field "events" is an array with 1 elements
+ And the exception "errorClass" equals "SIGSEGV"
+ And the event "exceptions.0.message" equals "Segmentation violation (invalid memory reference)"
+ And the error payload field "events.0.device.cpuAbi" is a non-empty array
+ And the event "app.binaryArch" equals "something weird"
+ And the event "usage.config.maxBreadcrumbs" is null
+ And the event "usage.config.autoTrackSessions" is null
+ And the event "usage.callbacks.ndkOnError" is null
+ And the event "usage.callbacks.onBreadcrumb" is null
+ And the event "usage.callbacks.onError" is null
+ And the event "usage.callbacks.onSession" is null
+ And the event "usage.callbacks.event_set_user" is null
+ And the event "usage.callbacks.app_set_binary_arch" is null
+ And the event "usage.callbacks.device_get_model" is null
+ And the event "usage.callbacks.event_get_severity" is null
diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature
index 5da265efaf..0da3e1d723 100644
--- a/features/smoke_tests/04_unhandled.feature
+++ b/features/smoke_tests/04_unhandled.feature
@@ -209,6 +209,8 @@ Feature: Unhandled smoke tests
And the event "metaData.opaque.nestedList.2" equals "two"
And the event "metaData.opaque.nestedList.3" equals "three"
+ # TODO: Pending PLAT-9011
+ @skip_android_7
@debug-safe
Scenario: C++ exception thrown with overwritten config
When I run "CXXExceptionSmokeScenario" and relaunch the crashed app
diff --git a/features/support/env.rb b/features/support/env.rb
index cfb790d3f7..862b820f06 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -38,12 +38,20 @@
skip_this_scenario("Skipping scenario") if Maze.config.os_version < 5
end
+Before('@skip_android_13') do |scenario|
+ skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 13
+end
+
Before('@skip_android_10') do |scenario|
- skip_this_scenario("Skipping scenario") if Maze.config.os_version == 10
+ skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 10
+end
+
+Before('@skip_android_7') do |scenario|
+ skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 7
end
Before('@skip_android_6') do |scenario|
- skip_this_scenario("Skipping scenario") if Maze.config.os_version == 6
+ skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 6
end
Before('@skip_samsung') do |scenario|
diff --git a/gradle.properties b/gradle.properties
index e91506b8f6..2a03ba3a8e 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.26.0
+VERSION_NAME=5.27.0
GROUP=com.bugsnag
POM_SCM_URL=https://github.com/bugsnag/bugsnag-android
POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git