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)dstValue).addAll((Collection)srcValue); + } else { + dst.put(key, srcValue); + } + } + } + /** * Deliver a report, serialized as an event JSON payload. * @@ -350,11 +375,24 @@ public static boolean isDiscardErrorClass(@NonNull String name) { @SuppressWarnings("unused") public static void deliverReport(@Nullable byte[] releaseStageBytes, @NonNull byte[] payloadBytes, + @Nullable byte[] staticDataBytes, @NonNull String apiKey, boolean isLaunching) { - if (payloadBytes == null) { - return; + // If there's saved static data, merge it directly into the payload map. + if (staticDataBytes != null) { + @SuppressWarnings("unchecked") + Map payloadMap = (Map) JsonHelper.INSTANCE.deserialize( + new ByteArrayInputStream(payloadBytes)); + @SuppressWarnings("unchecked") + Map staticDataMap = + (Map) JsonHelper.INSTANCE.deserialize( + new ByteArrayInputStream(staticDataBytes)); + deepMerge(staticDataMap, payloadMap); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + JsonHelper.INSTANCE.serialize(payloadMap, os); + payloadBytes = os.toByteArray(); } + String payload = new String(payloadBytes, UTF8Charset); String releaseStage = releaseStageBytes == null ? null @@ -561,5 +599,4 @@ public static void markLaunchCompleted() { public static LastRunInfo getLastRunInfo() { return getClient().getLastRunInfo(); } - } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NdkPluginCaller.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/NdkPluginCaller.kt new file mode 100644 index 0000000000..11deefa3cf --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NdkPluginCaller.kt @@ -0,0 +1,101 @@ +package com.bugsnag.android + +import java.lang.reflect.Method + +/** + * Calls the NDK plugin if it is loaded, otherwise does nothing / returns the default. + */ +internal object NdkPluginCaller { + private var ndkPlugin: Plugin? = null + private var setInternalMetricsEnabled: Method? = null + private var setStaticData: Method? = null + private var getSignalUnwindStackFunction: Method? = null + private var getCurrentCallbackSetCounts: Method? = null + private var getCurrentNativeApiCallUsage: Method? = null + private var initCallbackCounts: Method? = null + private var notifyAddCallback: Method? = null + private var notifyRemoveCallback: Method? = null + + private fun getMethod(name: String, vararg parameterTypes: Class<*>): Method? { + val plugin = ndkPlugin + if (plugin == null) { + return null + } + return plugin.javaClass.getMethod(name, *parameterTypes) + } + + fun setNdkPlugin(plugin: Plugin?) { + if (plugin != null) { + ndkPlugin = plugin + setInternalMetricsEnabled = getMethod("setInternalMetricsEnabled", Boolean::class.java) + setStaticData = getMethod("setStaticData", Map::class.java) + getSignalUnwindStackFunction = getMethod("getSignalUnwindStackFunction") + getCurrentCallbackSetCounts = getMethod("getCurrentCallbackSetCounts") + getCurrentNativeApiCallUsage = getMethod("getCurrentNativeApiCallUsage") + initCallbackCounts = getMethod("initCallbackCounts", Map::class.java) + notifyAddCallback = getMethod("notifyAddCallback", String::class.java) + notifyRemoveCallback = getMethod("notifyRemoveCallback", String::class.java) + } + } + + fun getSignalUnwindStackFunction(): Long { + val method = getSignalUnwindStackFunction + if (method != null) { + return method.invoke(ndkPlugin) as Long + } + return 0 + } + + fun setInternalMetricsEnabled(enabled: Boolean) { + val method = setInternalMetricsEnabled + if (method != null) { + method.invoke(ndkPlugin, enabled) + } + } + + fun getCurrentCallbackSetCounts(): Map? { + val method = getCurrentCallbackSetCounts + if (method != null) { + @Suppress("UNCHECKED_CAST") + return method.invoke(ndkPlugin) as Map + } + return null + } + + fun getCurrentNativeApiCallUsage(): Map? { + val method = getCurrentNativeApiCallUsage + if (method != null) { + @Suppress("UNCHECKED_CAST") + return method.invoke(ndkPlugin) as Map + } + return null + } + + fun initCallbackCounts(counts: Map) { + val method = initCallbackCounts + if (method != null) { + method.invoke(ndkPlugin, counts) + } + } + + fun notifyAddCallback(callback: String) { + val method = notifyAddCallback + if (method != null) { + method.invoke(ndkPlugin, callback) + } + } + + fun notifyRemoveCallback(callback: String) { + val method = notifyRemoveCallback + if (method != null) { + method.invoke(ndkPlugin, callback) + } + } + + fun setStaticData(data: Map) { + val method = setStaticData + if (method != null) { + method.invoke(ndkPlugin, data) + } + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index 01d9758392..d479e4c684 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.26.0", + var version: String = "5.27.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt index 01f268bd39..3764cf180c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt @@ -45,6 +45,8 @@ internal class PluginClient( } } + fun getNdkPlugin(): Plugin? = ndkPlugin + fun loadPlugins(client: Client) { plugins.forEach { plugin -> try { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Telemetry.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Telemetry.kt index 7d5a070a71..dde78fd5d6 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Telemetry.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Telemetry.kt @@ -8,7 +8,12 @@ enum class Telemetry { /** * Errors within the Bugsnag SDK. */ - INTERNAL_ERRORS; + INTERNAL_ERRORS, + + /** + * Differences from the default configuration. + */ + USAGE; internal companion object { fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 06558cfca2..1cde96634d 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -52,6 +52,7 @@ data class ImmutableConfig( val maxReportedThreads: Int, val persistenceDirectory: Lazy, val sendLaunchCrashesSynchronously: Boolean, + val attemptDeliveryOnCrash: Boolean, // results cached here to avoid unnecessary lookups in Client. val packageInfo: PackageInfo?, @@ -167,6 +168,7 @@ internal fun convertToImmutableConfig( telemetry = config.telemetry.toSet(), persistenceDirectory = persistenceDir, sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, + attemptDeliveryOnCrash = config.isAttemptDeliveryOnCrash, packageInfo = packageInfo, appInfo = appInfo, redactedKeys = config.redactedKeys.toSet() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt new file mode 100644 index 0000000000..ce83ac5153 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt @@ -0,0 +1,19 @@ +package com.bugsnag.android.internal + +/** + * Stores internal metrics for Bugsnag use. + */ +interface InternalMetrics { + /** + * Returns a map that can be merged with the top-level JSON report. + */ + fun toJsonableMap(): Map + + fun setConfigDifferences(differences: Map) + + fun setCallbackCounts(newCallbackCounts: Map) + + fun notifyAddCallback(callback: String) + + fun notifyRemoveCallback(callback: String) +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt new file mode 100644 index 0000000000..640a874f6a --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt @@ -0,0 +1,69 @@ +package com.bugsnag.android.internal + +import com.bugsnag.android.NdkPluginCaller + +class InternalMetricsImpl : InternalMetrics { + private val configDifferences = hashMapOf() + private val callbackCounts = hashMapOf() + + override fun toJsonableMap(): Map { + val callbacks = allCallbacks() + + return listOfNotNull( + if (configDifferences.isNotEmpty()) "config" to configDifferences else null, + if (callbacks.isNotEmpty()) "callbacks" to callbacks else null, + ).toMap() + } + + override fun setConfigDifferences(differences: Map) { + configDifferences.clear() + configDifferences.putAll(differences) + // This is currently the only place where we set static data. + // When that changes in future, we'll need a StaticData object to properly merge data + // coming from multiple sources. + NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences))) + } + + override fun setCallbackCounts(newCallbackCounts: Map) { + callbackCounts.clear() + callbackCounts.putAll(newCallbackCounts) + NdkPluginCaller.initCallbackCounts(newCallbackCounts) + } + + override fun notifyAddCallback(callback: String) { + modifyCallback(callback, 1) + NdkPluginCaller.notifyAddCallback(callback) + } + + override fun notifyRemoveCallback(callback: String) { + modifyCallback(callback, -1) + NdkPluginCaller.notifyRemoveCallback(callback) + } + + private fun modifyCallback(callback: String, delta: Int) { + var currentValue = callbackCounts[callback] ?: 0 + currentValue += delta + callbackCounts[callback] = currentValue.coerceAtLeast(0) + } + + private fun allCallbacks(): Map { + val result = hashMapOf() + result.putAll(callbackCounts) + + val counts = NdkPluginCaller.getCurrentCallbackSetCounts() + if (counts != null) { + // ndkOnError comes from the native side. The rest we already have. + val ndkOnError = counts["ndkOnError"] + if (ndkOnError != null) { + result["ndkOnError"] = ndkOnError + } + } + + val usage = NdkPluginCaller.getCurrentNativeApiCallUsage() + if (usage != null) { + result.putAll(usage) + } + + return result + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt new file mode 100644 index 0000000000..bbf0384e7b --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt @@ -0,0 +1,9 @@ +package com.bugsnag.android.internal + +class InternalMetricsNoop : InternalMetrics { + override fun toJsonableMap(): Map = emptyMap() + override fun setConfigDifferences(differences: Map) = Unit + override fun setCallbackCounts(newCallbackCounts: Map) = Unit + override fun notifyAddCallback(callback: String) = Unit + override fun notifyRemoveCallback(callback: String) = Unit +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java index 067f3355a2..ca13a2d103 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.when; import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.InternalMetrics; import com.bugsnag.android.internal.StateObserver; import android.content.Context; @@ -98,6 +99,9 @@ public class ClientFacadeTest { @Mock Notifier notifier; + @Mock + InternalMetrics internalMetrics; + private Client client; private InterceptingLogger logger; diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt index c50742ed0d..07ad7d29e7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt @@ -21,6 +21,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner +import java.io.File import java.nio.file.Files /** @@ -88,7 +89,8 @@ internal class NativeInterfaceApiTest { val customDir = Files.createTempDirectory("custom").toFile() `when`(immutableConfig.persistenceDirectory).thenReturn(lazy { customDir }) val observed = NativeInterface.getNativeReportPath() - assertEquals("${customDir.absolutePath}/bugsnag-native", observed) + val expected = File(customDir, "bugsnag-native") + assertEquals(expected, observed) } @Test @@ -236,7 +238,10 @@ internal class NativeInterfaceApiTest { @Test fun deliverReport() { - NativeInterface.deliverReport(null, "{}".toByteArray(), "", false) + NativeInterface.deliverReport( + null, "{}".toByteArray(), "{}".toByteArray(), + "", false + ) verify(eventStore, times(1)).enqueueContentForDelivery(eq("{}"), any()) } diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index d52669a14f..266786cb4c 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -4,6 +4,7 @@ ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) LongMethod:EventMigrationV10Tests.kt$EventMigrationV10Tests$@Test fun testMigrateEventToLatest() + LongMethod:EventMigrationV11Tests.kt$EventMigrationV11Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest() @@ -12,6 +13,7 @@ LongMethod:EventMigrationV9Tests.kt$EventMigrationV9Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() + SwallowedException:NdkPlugin.kt$NdkPlugin$catch (exc: IOException) { // Ignore } TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver 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