diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index be25736eb5..535bec7060 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -35,6 +35,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-minimal.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -44,8 +45,6 @@ steps: - "--farm=bs" - "--device=ANDROID_9_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -66,6 +65,7 @@ steps: run: android-ci env: INSTRUMENTATION_DEVICES: '["Google Pixel-7.1"]' + TEST_APK_LOCATION: 'bugsnag-android-core/build/outputs/apk/androidTest/debug/bugsnag-android-core-debug-androidTest.apk' concurrency: 9 concurrency_group: 'browserstack-app' @@ -79,6 +79,7 @@ steps: run: android-ci env: INSTRUMENTATION_DEVICES: '["Google Pixel 3-9.0"]' + TEST_APK_LOCATION: 'bugsnag-android-core/build/outputs/apk/androidTest/debug/bugsnag-android-core-debug-androidTest.apk' concurrency: 9 concurrency_group: 'browserstack-app' @@ -90,6 +91,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -99,8 +101,6 @@ steps: - "--farm=bs" - "--device=ANDROID_5_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' soft_fail: @@ -114,6 +114,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -123,8 +124,6 @@ steps: - "--farm=bs" - "--device=ANDROID_5_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' soft_fail: @@ -138,6 +137,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -147,8 +147,6 @@ steps: - "--farm=bs" - "--device=ANDROID_6_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -160,6 +158,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -169,8 +168,6 @@ steps: - "--farm=bs" - "--device=ANDROID_6_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -182,6 +179,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -191,8 +189,6 @@ steps: - "--farm=bs" - "--device=ANDROID_7_1" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -204,6 +200,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -213,8 +210,6 @@ steps: - "--farm=bs" - "--device=ANDROID_7_1" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -226,6 +221,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -235,8 +231,6 @@ steps: - "--farm=bs" - "--device=ANDROID_8_1" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -248,6 +242,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -257,8 +252,6 @@ steps: - "--farm=bs" - "--device=ANDROID_8_1" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -270,6 +263,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -279,8 +273,6 @@ steps: - "--farm=bs" - "--device=ANDROID_10_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -292,6 +284,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -301,8 +294,6 @@ steps: - "--farm=bs" - "--device=ANDROID_10_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -318,6 +309,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -327,8 +319,6 @@ steps: - "--farm=bs" - "--device=ANDROID_11_0" - "--fail-fast" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -340,6 +330,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -349,173 +340,6 @@ steps: - "--farm=bs" - "--device=ANDROID_11_0" - "--fail-fast" - - "--tags" - - "not @Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - # - # Flaky scenarios. These should be skipped if there are no scenarios annotated with the @Flaky tag (as we should - # always be aiming for) to avoid unnecessary CI wait times and potential flakes from resource unavailability. - # - - label: ':hankey: Flaky Android 4.4 NDK r16 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r16" - timeout_in_minutes: 90 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r16.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r16.apk" - - "--farm=bs" - - "--device=ANDROID_4_4" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 5 NDK r16 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r16" - timeout_in_minutes: 90 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r16.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r16.apk" - - "--farm=bs" - - "--device=ANDROID_5_0" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 6 NDK r16 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r16" - timeout_in_minutes: 90 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r16.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r16.apk" - - "--farm=bs" - - "--device=ANDROID_6_0" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 7 NDK r19 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r19" - timeout_in_minutes: 60 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r19.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r19.apk" - - "--farm=bs" - - "--device=ANDROID_7_1" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 8.1 NDK r19 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r19" - timeout_in_minutes: 60 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r19.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r19.apk" - - "--farm=bs" - - "--device=ANDROID_8_1" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 10 NDK r21 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r21" - timeout_in_minutes: 60 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r21.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r21.apk" - - "--farm=bs" - - "--device=ANDROID_10_0" - - "--tags=@Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - - label: ':hankey: Flaky Android 11 NDK r21 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r21" - timeout_in_minutes: 60 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r21.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r21.apk" - - "--farm=bs" - - "--device=ANDROID_11_0" - - "--tags=@Flaky" concurrency: 9 concurrency_group: 'browserstack-app' diff --git a/.buildkite/pipeline.quick.yml b/.buildkite/pipeline.quick.yml index 4857af1412..c82b4942f7 100644 --- a/.buildkite/pipeline.quick.yml +++ b/.buildkite/pipeline.quick.yml @@ -6,6 +6,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -27,6 +28,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -46,6 +48,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -65,6 +68,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r19.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -84,6 +88,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -104,6 +109,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -112,8 +118,6 @@ steps: - "--app=/app/build/release/fixture-r21.apk" - "--farm=bs" - "--device=ANDROID_9_0" - - "--tags" - - "not @Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -125,6 +129,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -133,35 +138,6 @@ steps: - "--app=/app/build/release/fixture-r21.apk" - "--farm=bs" - "--device=ANDROID_9_0" - - "--tags" - - "not @Flaky" - concurrency: 9 - concurrency_group: 'browserstack-app' - - # - # Flaky scenarios. These should be skipped if there are no scenarios annotated with the @Flaky tag (as we should - # always be aiming for) to avoid unnecessary CI wait times and potential flakes from resource unavailability. - # - - label: ':hankey: Flaky Android 9 NDK r21 end-to-end tests' - skip: There are no @Flaky scenarios at present - depends_on: - - "fixture-r21" - timeout_in_minutes: 60 - plugins: - artifacts#v1.2.0: - download: "build/release/fixture-r21.apk" - docker-compose#v3.7.0: - pull: android-maze-runner - run: android-maze-runner - command: - - "features/full_tests" - - "--retry=2" - - "--strict-undefined" - - "--strict-pending" - - "--app=/app/build/release/fixture-r21.apk" - - "--farm=bs" - - "--device=ANDROID_9_0" - - "--tags=@Flaky" concurrency: 9 concurrency_group: 'browserstack-app' @@ -172,6 +148,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 64f967e71d..d0a685f200 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -164,6 +164,22 @@ steps: run: android-ci env: INSTRUMENTATION_DEVICES: '["Google Pixel-7.1"]' + TEST_APK_LOCATION: 'bugsnag-android-core/build/outputs/apk/androidTest/debug/bugsnag-android-core-debug-androidTest.apk' + concurrency: 9 + concurrency_group: 'browserstack-app' + + - label: ':android: Performance benchmarks' + key: 'perf-benchmarks' + depends_on: + - "android-ci" + timeout_in_minutes: 30 + command: ./scripts/build-instrumentation-tests.sh && ./scripts/run-instrumentation-test.sh + plugins: + - docker-compose#v3.7.0: + run: android-ci + env: + INSTRUMENTATION_DEVICES: '["Google Pixel-7.1"]' + TEST_APK_LOCATION: 'bugsnag-benchmarks/build/outputs/apk/androidTest/release/bugsnag-benchmarks-release-androidTest.apk' concurrency: 9 concurrency_group: 'browserstack-app' @@ -174,6 +190,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r16.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner @@ -197,6 +214,7 @@ steps: plugins: artifacts#v1.2.0: download: "build/release/fixture-r21.apk" + upload: "maze_output/failed/**/*" docker-compose#v3.7.0: pull: android-maze-runner run: android-maze-runner diff --git a/CHANGELOG.md b/CHANGELOG.md index db31db94e9..36cdd124c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## 5.9.5 (2021-06-25) + +* Unity: Properly handle ANRs after multiple calls to autoNotify and autoDetectAnrs + [#1265](https://github.com/bugsnag/bugsnag-android/pull/1265) + +* Cache value of app.backgroundWorkRestricted + [#1275](https://github.com/bugsnag/bugsnag-android/pull/1275) + +* Optimize execution of callbacks + [#1276](https://github.com/bugsnag/bugsnag-android/pull/1276) + +* Optimize implementation of internal state change observers + [#1274](https://github.com/bugsnag/bugsnag-android/pull/1274) + +* Optimize metadata implementation by reducing type casts + [#1277](https://github.com/bugsnag/bugsnag-android/pull/1277) + +* Trim stacktraces to <200 frames before attempting to construct POJOs + [#1281](https://github.com/bugsnag/bugsnag-android/pull/1281) + +* Use direct field access when adding breadcrumbs and state updates + [#1279](https://github.com/bugsnag/bugsnag-android/pull/1279) + +* Avoid using regex to validate api key + [#1282](https://github.com/bugsnag/bugsnag-android/pull/1282) + +* Discard unwanted automatic data earlier where possible + [#1280](https://github.com/bugsnag/bugsnag-android/pull/1280) + +* Enable ANR handling on immediately if started from the main thread + [#1283](https://github.com/bugsnag/bugsnag-android/pull/1283) + +* Include `app.binaryArch` in all events + [#1287](https://github.com/bugsnag/bugsnag-android/pull/1287) + +* Cache results from PackageManager + [#1288](https://github.com/bugsnag/bugsnag-android/pull/1288) + +* Use ring buffer to store breadcrumbs + [#1286](https://github.com/bugsnag/bugsnag-android/pull/1286) + +* Avoid expensive set construction in Config constructor + [#1289](https://github.com/bugsnag/bugsnag-android/pull/1289) + +* Replace calls to String.format() with concatenation + [#1293](https://github.com/bugsnag/bugsnag-android/pull/1293) + +* Optimize capture of thread traces + [#1300](https://github.com/bugsnag/bugsnag-android/pull/1300) + ## 5.9.4 (2021-05-26) * Unity: add methods for setting autoNotify and autoDetectAnrs diff --git a/bugsnag-android-core/build.gradle b/bugsnag-android-core/build.gradle index db07e7e9b7..ce57159a6a 100644 --- a/bugsnag-android-core/build.gradle +++ b/bugsnag-android-core/build.gradle @@ -15,3 +15,5 @@ dokka { outputFormat = "html" outputDirectory = "$buildDir/dokka" } + +apply from: "../gradle/kotlin.gradle" diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index 3173ed5f4f..0c3e0f3b63 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -2,6 +2,7 @@ + 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 logger: Logger ) LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ var isLaunching: Boolean? ) @@ -11,20 +12,27 @@ LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, private val resources: Resources?, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, bgTaskService: BackgroundTaskService, private val logger: Logger ) LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap<String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? ) LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null ) - LongParameterList:StateEvent.kt$StateEvent.Install$( val apiKey: String, val autoDetectNdkCrashes: Boolean, val appVersion: String?, val buildUuid: String?, val releaseStage: String?, val lastRunInfoPath: String, val consecutiveLaunchCrashes: Int ) - LongParameterList:ThreadState.kt$ThreadState$( exc: Throwable?, isUnhandled: Boolean, sendThreads: ThreadSendPolicy, projectPackages: Collection<String>, logger: Logger, currentThread: java.lang.Thread = java.lang.Thread.currentThread(), stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>> = java.lang.Thread.getAllStackTraces() ) + LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int ) LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection<String>, logger: Logger ) MagicNumber:DefaultDelivery.kt$DefaultDelivery$299 MagicNumber:DefaultDelivery.kt$DefaultDelivery$429 MagicNumber:DefaultDelivery.kt$DefaultDelivery$499 MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3 MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)" - ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = mutableSetOf<Plugin>() + ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet<Plugin>() ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun updateSeverityInternal(severity: Severity) - ProtectedMemberInFinalClass:PluginClient.kt$PluginClient$protected val plugins: Set<Plugin> ReturnCount:DefaultDelivery.kt$DefaultDelivery$fun deliver( urlString: String, streamable: JsonStream.Streamable, headers: Map<String, String?> ): DeliveryStatus + SwallowedException:AppDataCollector.kt$AppDataCollector$catch (exception: Exception) { logger.w("Could not check lowMemory status") } + SwallowedException:ContextExtensions.kt$catch (exc: RuntimeException) { null } + SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exc: Exception) { false } + SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get battery status") } + SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get locationStatus") } + SwallowedException:DeviceIdStore.kt$DeviceIdStore$catch (exc: OverlappingFileLockException) { Thread.sleep(FILE_LOCK_WAIT_MS) } + SwallowedException:PluginClient.kt$PluginClient$catch (exc: ClassNotFoundException) { logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.") null } + UnusedPrivateMember:ThreadStateTest.kt$ThreadStateTest$private val configuration = generateImmutableConfig() + VarCouldBeVal:SystemBroadcastReceiverTest.kt$SystemBroadcastReceiverTest$var config = BugsnagTestUtils.generateConfiguration() VariableNaming:EventInternal.kt$EventInternal$/** * @return user information associated with this Event */ internal var _user = User(null, null, null) diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt index 678e4009d1..d8092ece01 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt @@ -24,8 +24,7 @@ class BreadcrumbFilterTest { client = generateClient(configuration) client.leaveBreadcrumb("Hello World") - - assertEquals(1, client.breadcrumbState.store.size) + assertEquals(1, client.breadcrumbState.copy().size) } @Test @@ -36,6 +35,6 @@ class BreadcrumbFilterTest { client.leaveBreadcrumb("Hello World") - assertEquals(1, client.breadcrumbState.store.size) + assertEquals(1, client.breadcrumbState.copy().size) } } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt index 34ff644a55..1f5831ca20 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt @@ -31,7 +31,7 @@ class BreadcrumbStateTest { fun testClientMethods() { client = generateClient() client!!.leaveBreadcrumb("Hello World") - val store = client!!.breadcrumbState.store + val store = client!!.breadcrumbState.copy() var count = 0 for (breadcrumb in store) { diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java index 1841aed26c..92ac16e01e 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfigKt; + import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; @@ -87,7 +90,7 @@ static ImmutableConfig convert(Configuration config) { } catch (IOException ignored) { // swallow } - return ImmutableConfigKt.convertToImmutableConfig(config, null); + return ImmutableConfigKt.convertToImmutableConfig(config, null, null, null); } static Device generateDevice() { diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java index f09093c666..94aba83749 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -76,14 +76,14 @@ public void testMaxBreadcrumbs() { config.setEnabledBreadcrumbTypes(new HashSet<>(breadcrumbTypes)); config.setMaxBreadcrumbs(2); client = generateClient(config); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); client.leaveBreadcrumb("test"); client.leaveBreadcrumb("another"); client.leaveBreadcrumb("yet another"); - assertEquals(2, client.breadcrumbState.getStore().size()); + assertEquals(2, client.breadcrumbState.copy().size()); - Breadcrumb poll = client.breadcrumbState.getStore().poll(); + Breadcrumb poll = client.breadcrumbState.copy().get(0); assertEquals(BreadcrumbType.MANUAL, poll.getType()); assertEquals("another", poll.getMessage()); } @@ -126,10 +126,12 @@ public void testClientUser() { @Test public void testClientBreadcrumbRetrieval() { - client = generateClient(); + Configuration config = new Configuration("api-key"); + config.setEnabledBreadcrumbTypes(Collections.emptySet()); + client = generateClient(config); client.leaveBreadcrumb("Hello World"); List breadcrumbs = client.getBreadcrumbs(); - List store = new ArrayList<>(client.breadcrumbState.getStore()); + List store = new ArrayList<>(client.breadcrumbState.copy()); assertEquals(store, breadcrumbs); assertNotSame(store, breadcrumbs); } @@ -153,8 +155,8 @@ public void testBreadcrumbStoreNotModified() { breadcrumbs.clear(); // only the copy should be cleared assertTrue(breadcrumbs.isEmpty()); - assertEquals(1, client.breadcrumbState.getStore().size()); - assertEquals("Manual breadcrumb", client.breadcrumbState.getStore().remove().getMessage()); + assertEquals(1, client.breadcrumbState.copy().size()); + assertEquals("Manual breadcrumb", client.breadcrumbState.copy().get(0).getMessage()); } @Test diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventPayloadTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventPayloadTest.java index 6c143b2176..da9c1c4ee9 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventPayloadTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/EventPayloadTest.java @@ -5,6 +5,8 @@ import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.test.filters.SmallTest; import org.json.JSONArray; @@ -22,7 +24,6 @@ public class EventPayloadTest { /** * Generates a eventPayload - * */ @Before public void setUp() { diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt index 06ae6b0fe9..e6b503264d 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt @@ -3,6 +3,7 @@ package com.bugsnag.android import android.content.Context import androidx.test.core.app.ApplicationProvider import com.bugsnag.android.BugsnagTestUtils.generateConfiguration +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -21,7 +22,9 @@ internal class LastRunInfoStoreTest { val config = convertToImmutableConfig( generateConfiguration().apply { persistenceDirectory = ApplicationProvider.getApplicationContext().cacheDir - } + }, + packageInfo = null, + appInfo = null ) file = File(config.persistenceDirectory, "last-run-info") file.delete() diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java index 3d0804b030..9d6cfe6cf7 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java @@ -40,7 +40,7 @@ public void onLowMemoryEvent() { verify(context, times(1)).registerComponentCallbacks(componentCallbacksCaptor.capture()); BugsnagTestObserver observer = new BugsnagTestObserver(); - client.registerObserver(observer); + client.addObserver(observer); ComponentCallbacks callbacks = componentCallbacksCaptor.getValue(); callbacks.onLowMemory(); @@ -55,7 +55,7 @@ public void onLowMemoryEvent() { assertTrue( "observed event should be marked isLowMemory", - ((StateEvent.UpdateMemoryTrimEvent) observedEvent).isLowMemory() + ((StateEvent.UpdateMemoryTrimEvent) observedEvent).isLowMemory ); } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index 1adc99f61f..329d68de4e 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -8,10 +8,13 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.bugsnag.android.internal.StateObserver; + import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; +import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -19,8 +22,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Observable; -import java.util.Observer; import java.util.Set; @SmallTest @@ -47,7 +48,7 @@ public void setUp() { config.addMetadata("foo", "bar", true); client = new Client(ApplicationProvider.getApplicationContext(), config); observer = new BugsnagTestObserver(); - client.registerObserver(observer); + client.addObserver(observer); } @After @@ -84,9 +85,9 @@ public void testSyncInitialState() { public void testAddMetadataSendsMessage() { client.addMetadata("foo", "bar", "baz"); StateEvent.AddMetadata msg = findMessageInQueue(StateEvent.AddMetadata.class); - assertEquals("foo", msg.getSection()); - assertEquals("bar", msg.getKey()); - assertEquals("baz", msg.getValue()); + assertEquals("foo", msg.section); + assertEquals("bar", msg.key); + assertEquals("baz", msg.value); } @Test @@ -94,8 +95,8 @@ public void testAddNullMetadataSendsMessage() { client.addMetadata("foo", "bar", "baz"); client.addMetadata("foo", "bar", null); StateEvent.ClearMetadataValue msg = findMessageInQueue(StateEvent.ClearMetadataValue.class); - assertEquals("foo", msg.getSection()); - assertEquals("bar", msg.getKey()); + assertEquals("foo", msg.section); + assertEquals("bar", msg.key); } @Test @@ -103,7 +104,7 @@ public void testClearTopLevelTabSendsMessage() { client.clearMetadata("axis"); StateEvent.ClearMetadataSection value = findMessageInQueue(StateEvent.ClearMetadataSection.class); - assertEquals("axis", value.getSection()); + assertEquals("axis", value.section); } @Test @@ -111,8 +112,8 @@ public void testClearTabSendsMessage() { client.clearMetadata("axis", "foo"); StateEvent.ClearMetadataValue value = findMessageInQueue(StateEvent.ClearMetadataValue.class); - assertEquals("axis", value.getSection()); - assertEquals("foo", value.getKey()); + assertEquals("axis", value.section); + assertEquals("foo", value.key); } @Test @@ -126,9 +127,9 @@ public void testNotifySendsMessage() { public void testStartSessionSendsMessage() { client.startSession(); StateEvent.StartSession sessionInfo = findMessageInQueue(StateEvent.StartSession.class); - assertNotNull(sessionInfo.getId()); - assertNotNull(sessionInfo.getStartedAt()); - assertEquals(0, sessionInfo.getHandledCount()); + assertNotNull(sessionInfo.id); + assertNotNull(sessionInfo.startedAt); + assertEquals(0, sessionInfo.handledCount); assertEquals(0, sessionInfo.getUnhandledCount()); } @@ -149,32 +150,32 @@ public void testRegisterSessionSendsMessage() { public void testClientSetContextSendsMessage() { client.setContext("Pod Bay"); StateEvent.UpdateContext msg = findMessageInQueue(StateEvent.UpdateContext.class); - assertEquals("Pod Bay", msg.getContext()); + assertEquals("Pod Bay", msg.context); } @Test public void testClientMarkLaunchCompletedSendsMessage() { client.markLaunchCompleted(); StateEvent.UpdateIsLaunching msg = findMessageInQueue(StateEvent.UpdateIsLaunching.class); - assertFalse(msg.isLaunching()); + assertFalse(msg.isLaunching); } @Test public void testClientSetUserId() { client.setUser("personX", "bip@example.com", "Loblaw"); StateEvent.UpdateUser idMsg = findMessageInQueue(StateEvent.UpdateUser.class); - assertEquals("personX", idMsg.getUser().getId()); - assertEquals("bip@example.com", idMsg.getUser().getEmail()); - assertEquals("Loblaw", idMsg.getUser().getName()); + assertEquals("personX", idMsg.user.getId()); + assertEquals("bip@example.com", idMsg.user.getEmail()); + assertEquals("Loblaw", idMsg.user.getName()); } @Test public void testLeaveStringBreadcrumbSendsMessage() { client.leaveBreadcrumb("Drift 4 units left"); StateEvent.AddBreadcrumb crumb = findMessageInQueue(StateEvent.AddBreadcrumb.class); - assertEquals(BreadcrumbType.MANUAL, crumb.getType()); - assertEquals("Drift 4 units left", crumb.getMessage()); - assertTrue(crumb.getMetadata().isEmpty()); + assertEquals(BreadcrumbType.MANUAL, crumb.type); + assertEquals("Drift 4 units left", crumb.message); + assertTrue(crumb.metadata.isEmpty()); } @Test @@ -182,18 +183,18 @@ public void testLeaveStringBreadcrumbDirectlySendsMessage() { Breadcrumb obj = new Breadcrumb("Drift 4 units left", NoopLogger.INSTANCE); client.breadcrumbState.add(obj); StateEvent.AddBreadcrumb crumb = findMessageInQueue(StateEvent.AddBreadcrumb.class); - assertEquals(BreadcrumbType.MANUAL, crumb.getType()); - assertEquals("Drift 4 units left", crumb.getMessage()); - assertTrue(crumb.getMetadata().isEmpty()); + assertEquals(BreadcrumbType.MANUAL, crumb.type); + assertEquals("Drift 4 units left", crumb.message); + assertTrue(crumb.metadata.isEmpty()); } @Test public void testLeaveBreadcrumbSendsMessage() { client.leaveBreadcrumb("Rollback", new HashMap(), BreadcrumbType.LOG); StateEvent.AddBreadcrumb crumb = findMessageInQueue(StateEvent.AddBreadcrumb.class); - assertEquals(BreadcrumbType.LOG, crumb.getType()); - assertEquals("Rollback", crumb.getMessage()); - assertEquals(0, crumb.getMetadata().size()); + assertEquals(BreadcrumbType.LOG, crumb.type); + assertEquals("Rollback", crumb.message); + assertEquals(0, crumb.metadata.size()); } @NonNull @@ -206,7 +207,7 @@ private T findMessageInQueue(Class argClass) { throw new RuntimeException("Failed to find StateEvent message " + argClass.getSimpleName()); } - static class BugsnagTestObserver implements Observer { + static class BugsnagTestObserver implements StateObserver { final ArrayList observed; BugsnagTestObserver() { @@ -214,8 +215,8 @@ static class BugsnagTestObserver implements Observer { } @Override - public void update(Observable observable, Object arg) { - observed.add(arg); + public void onStateChange(@NotNull StateEvent event) { + observed.add(event); } } } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java index 8ecdb7e136..ecdd889d93 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java @@ -34,7 +34,7 @@ public void setUp() { breadcrumbTypes.add(BreadcrumbType.USER); configuration.setEnabledBreadcrumbTypes(breadcrumbTypes); client = generateClient(configuration); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @After @@ -45,7 +45,7 @@ public void tearDown() { @Test public void noCallback() { client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } @Test @@ -57,7 +57,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @Test @@ -69,7 +69,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } @Test @@ -87,7 +87,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @Test @@ -161,7 +161,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { client.leaveBreadcrumb("Hello"); client.removeOnBreadcrumb(cb); client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/UserStoreTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/UserStoreTest.kt index f65be4c79b..8125e6ff6a 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/UserStoreTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/UserStoreTest.kt @@ -3,6 +3,7 @@ package com.bugsnag.android import android.content.Context import android.content.SharedPreferences import androidx.test.core.app.ApplicationProvider +import com.bugsnag.android.internal.ImmutableConfig import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt index bbdf02b4ea..e603b2926e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/App.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException /** diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt index f39d817a20..625f3f8b33 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -2,10 +2,10 @@ package com.bugsnag.android import android.app.ActivityManager import android.content.Context -import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Build import android.os.SystemClock +import com.bugsnag.android.internal.ImmutableConfig /** * Collects various data on the application state @@ -22,13 +22,12 @@ internal class AppDataCollector( var codeBundleId: String? = null private val packageName: String = appContext.packageName - private var packageInfo = packageManager?.getPackageInfo(packageName, 0) - private var appInfo: ApplicationInfo? = packageManager?.getApplicationInfo(packageName, 0) + private val bgWorkRestricted = isBackgroundWorkRestricted() private var binaryArch: String? = null private val appName = getAppName() private val releaseStage = config.releaseStage - private val versionName = config.appVersion ?: packageInfo?.versionName + private val versionName = config.appVersion ?: config.packageInfo?.versionName fun generateApp(): App = App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) @@ -51,8 +50,8 @@ internal class AppDataCollector( map["memoryUsage"] = getMemoryUsage() map["lowMemory"] = isLowMemory() - isBackgroundWorkRestricted()?.let { - map["backgroundWorkRestricted"] = it + bgWorkRestricted?.let { + map["backgroundWorkRestricted"] = bgWorkRestricted } return map } @@ -129,7 +128,7 @@ internal class AppDataCollector( * AndroidManifest.xml */ private fun getAppName(): String? { - val copy = appInfo + val copy = config.appInfo return when { packageManager != null && copy != null -> { packageManager.getApplicationLabel(copy).toString() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt index 173ce4e192..bf05c4af6b 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + /** * Stateful information set by the notifier about your app can be found on this class. These values * can be accessed and amended if necessary. diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BaseObservable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BaseObservable.kt index 1f61b03d9b..d9e8e36185 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BaseObservable.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BaseObservable.kt @@ -1,10 +1,46 @@ package com.bugsnag.android -import java.util.Observable +import com.bugsnag.android.internal.StateObserver +import java.util.concurrent.CopyOnWriteArrayList -internal open class BaseObservable : Observable() { - fun notifyObservers(event: StateEvent) { - setChanged() - super.notifyObservers(event) +internal open class BaseObservable { + + internal val observers = CopyOnWriteArrayList() + + /** + * Adds an observer that can react to [StateEvent] messages. + */ + fun addObserver(observer: StateObserver) { + observers.addIfAbsent(observer) + } + + /** + * Removes a previously added observer that reacts to [StateEvent] messages. + */ + fun removeObserver(observer: StateObserver) { + observers.remove(observer) } + + /** + * This method should be invoked when the notifier's state has changed. If an observer + * has been set, it will be notified of the [StateEvent] message so that it can react + * appropriately. If no observer has been set then this method will no-op. + */ + internal inline fun updateState(provider: () -> StateEvent) { + // optimization to avoid unnecessary iterator and StateEvent construction + if (observers.isEmpty()) { + return + } + + // construct the StateEvent object and notify observers + val event = provider() + observers.forEach { it.onStateChange(event) } + } + + /** + * An eager version of [updateState], which is intended primarily for use in Java code. + * If the event will occur very frequently, you should consider calling the lazy method + * instead. + */ + fun updateState(event: StateEvent) = updateState { event } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java index 4da160a4ee..5d7ba6a78c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -10,7 +10,8 @@ @SuppressWarnings("ConstantConditions") public class Breadcrumb implements JsonStream.Streamable { - private final BreadcrumbInternal impl; + // non-private to allow direct field access optimizations + final BreadcrumbInternal impl; private final Logger logger; Breadcrumb(@NonNull String message, @NonNull Logger logger) { @@ -36,7 +37,7 @@ private void logNull(String property) { */ public void setMessage(@NonNull String message) { if (message != null) { - impl.setMessage(message); + impl.message = message; } else { logNull("message"); } @@ -47,7 +48,7 @@ public void setMessage(@NonNull String message) { */ @NonNull public String getMessage() { - return impl.getMessage(); + return impl.message; } /** @@ -56,7 +57,7 @@ public String getMessage() { */ public void setType(@NonNull BreadcrumbType type) { if (type != null) { - impl.setType(type); + impl.type = type; } else { logNull("type"); } @@ -68,14 +69,14 @@ public void setType(@NonNull BreadcrumbType type) { */ @NonNull public BreadcrumbType getType() { - return impl.getType(); + return impl.type; } /** * Sets diagnostic data relating to the breadcrumb */ public void setMetadata(@Nullable Map metadata) { - impl.setMetadata(metadata); + impl.metadata = metadata; } /** @@ -83,7 +84,7 @@ public void setMetadata(@Nullable Map metadata) { */ @Nullable public Map getMetadata() { - return impl.getMetadata(); + return impl.metadata; } /** @@ -91,12 +92,12 @@ public Map getMetadata() { */ @NonNull public Date getTimestamp() { - return impl.getTimestamp(); + return impl.timestamp; } @NonNull String getStringTimestamp() { - return DateUtils.toIso8601(impl.getTimestamp()); + return DateUtils.toIso8601(impl.timestamp); } @Override diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt index 0bc68fb349..49499b770d 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt @@ -9,11 +9,11 @@ import java.util.Date * attached to a crash to help diagnose what events lead to the error. */ internal class BreadcrumbInternal internal constructor( - var message: String, - var type: BreadcrumbType, - var metadata: MutableMap?, - val timestamp: Date = Date() -) : JsonStream.Streamable { + @JvmField var message: String, + @JvmField var type: BreadcrumbType, + @JvmField var metadata: MutableMap?, + @JvmField val timestamp: Date = Date() +) : JsonStream.Streamable { // JvmField allows direct field access optimizations internal constructor(message: String) : this( message, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt index a4d8593731..331ca721a5 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt @@ -1,55 +1,96 @@ package com.bugsnag.android import java.io.IOException -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +/** + * Stores breadcrumbs added to the [Client] in a ring buffer. If the number of breadcrumbs exceeds + * the maximum configured limit then the oldest breadcrumb in the ring buffer will be overwritten. + * + * When the breadcrumbs are required for generation of an event a [List] is constructed and + * breadcrumbs added in the order of their addition. + */ internal class BreadcrumbState( - maxBreadcrumbs: Int, - val callbackState: CallbackState, - val logger: Logger + private val maxBreadcrumbs: Int, + private val callbackState: CallbackState, + private val logger: Logger ) : BaseObservable(), JsonStream.Streamable { - val store: Queue = ConcurrentLinkedQueue() + /* + * We use the `index` as both a pointer to the tail of our ring-buffer, and also as "cheat" + * semaphore. When the ring-buffer is being copied - the index is set to a negative number, + * which is an invalid array-index. By masking the `expected` value in a `compareAndSet` with + * `validIndexMask`: the CAS operation will only succeed if it wouldn't interrupt a concurrent + * `copy()` call. + */ + private val validIndexMask: Int = Int.MAX_VALUE - private val maxBreadcrumbs: Int - - init { - when { - maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs - else -> this.maxBreadcrumbs = 0 - } - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - pruneBreadcrumbs() - writer.beginArray() - store.forEach { it.toStream(writer) } - writer.endArray() - } + private val store = arrayOfNulls(maxBreadcrumbs) + private val index = AtomicInteger(0) fun add(breadcrumb: Breadcrumb) { - if (!callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { + if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { return } - store.add(breadcrumb) - pruneBreadcrumbs() - notifyObservers( + // store the breadcrumb in the ring buffer + val position = getBreadcrumbIndex() + store[position] = breadcrumb + + updateState { + // use direct field access to avoid overhead of accessor method StateEvent.AddBreadcrumb( - breadcrumb.message, - breadcrumb.type, - DateUtils.toIso8601(breadcrumb.timestamp), - breadcrumb.metadata ?: mutableMapOf() + breadcrumb.impl.message, + breadcrumb.impl.type, + DateUtils.toIso8601(breadcrumb.impl.timestamp), + breadcrumb.impl.metadata ?: mutableMapOf() ) - ) + } + } + + /** + * Retrieves the index in the ring buffer where the breadcrumb should be stored. + */ + private fun getBreadcrumbIndex(): Int { + while (true) { + val currentValue = index.get() and validIndexMask + val nextValue = (currentValue + 1) % maxBreadcrumbs + if (index.compareAndSet(currentValue, nextValue)) { + return currentValue + } + } } - private fun pruneBreadcrumbs() { - // Remove oldest breadcrumbState until new max size reached - while (store.size > maxBreadcrumbs) { - store.poll() + /** + * Creates a copy of the breadcrumbs in the order of their addition. + */ + fun copy(): List { + if (maxBreadcrumbs == 0) { + return emptyList() + } + + // Set a negative value that stops any other thread from adding a breadcrumb. + // This handles reentrancy by waiting here until the old value has been reset. + var tail = -1 + while (tail == -1) { + tail = index.getAndSet(-1) + } + + try { + val result = arrayOfNulls(maxBreadcrumbs) + store.copyInto(result, 0, tail, maxBreadcrumbs) + store.copyInto(result, maxBreadcrumbs - tail, 0, tail) + return result.filterNotNull() + } finally { + index.set(tail) } } + + @Throws(IOException::class) + override fun toStream(writer: JsonStream) { + val crumbs = copy() + writer.beginArray() + crumbs.forEach { it.toStream(writer) } + writer.endArray() + } } 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 cb7ae14d3c..734d26b18d 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 @@ -33,6 +33,10 @@ internal data class CallbackState( } fun runOnErrorTasks(event: Event, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onErrorTasks.isEmpty()) { + return true + } onErrorTasks.forEach { try { if (!it.onError(event)) { @@ -46,6 +50,10 @@ internal data class CallbackState( } fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onBreadcrumbTasks.isEmpty()) { + return true + } onBreadcrumbTasks.forEach { try { if (!it.onBreadcrumb(breadcrumb)) { @@ -59,6 +67,10 @@ internal data class CallbackState( } fun runOnSessionTasks(session: Session, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onSessionTasks.isEmpty()) { + return true + } onSessionTasks.forEach { try { if (!it.onSession(session)) { 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 6ed0a6f7b6..f7ec7fd4fb 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 @@ -2,8 +2,11 @@ import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom; import static com.bugsnag.android.ContextExtensionsKt.getStorageManagerFrom; -import static com.bugsnag.android.ImmutableConfigKt.sanitiseConfiguration; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; +import static com.bugsnag.android.internal.ImmutableConfigKt.sanitiseConfiguration; + +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.StateObserver; import android.app.ActivityManager; import android.app.Application; @@ -28,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Observer; import java.util.Set; import java.util.concurrent.RejectedExecutionException; @@ -186,7 +188,7 @@ public Unit invoke(Boolean hasConnection, String networkState) { sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker); application.registerActivityLifecycleCallbacks(sessionLifecycleCallback); - if (immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.STATE)) { + if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector( new Function2, Unit>() { @SuppressWarnings("unchecked") @@ -379,7 +381,7 @@ void setupNdkPlugin() { clientObservable.postNdkDeliverPending(); } - void registerObserver(Observer observer) { + void addObserver(StateObserver observer) { metadataState.addObserver(observer); breadcrumbState.addObserver(observer); sessionTracker.addObserver(observer); @@ -390,15 +392,15 @@ void registerObserver(Observer observer) { launchCrashTracker.addObserver(observer); } - void unregisterObserver(Observer observer) { - metadataState.deleteObserver(observer); - breadcrumbState.deleteObserver(observer); - sessionTracker.deleteObserver(observer); - clientObservable.deleteObserver(observer); - userState.deleteObserver(observer); - contextState.deleteObserver(observer); - deliveryDelegate.deleteObserver(observer); - launchCrashTracker.deleteObserver(observer); + void removeObserver(StateObserver observer) { + metadataState.removeObserver(observer); + breadcrumbState.removeObserver(observer); + sessionTracker.removeObserver(observer); + clientObservable.removeObserver(observer); + userState.removeObserver(observer); + contextState.removeObserver(observer); + deliveryDelegate.removeObserver(observer); + launchCrashTracker.removeObserver(observer); } /** @@ -656,6 +658,9 @@ public void notify(@NonNull Throwable exception) { */ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { if (exc != null) { + if (immutableConfig.shouldDiscardError(exc)) { + return; + } SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Metadata metadata = metadataState.getMetadata(); Event event = new Event(exc, immutableConfig, severityReason, metadata, logger); @@ -706,7 +711,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event, event.addMetadata("app", appDataCollector.getAppDataMetadata()); // Attach breadcrumbState to the event - event.setBreadcrumbs(new ArrayList<>(breadcrumbState.getStore())); + event.setBreadcrumbs(breadcrumbState.copy()); // Attach user info to the event User user = userState.getUser(); @@ -722,19 +727,6 @@ void populateAndNotifyAndroidEvent(@NonNull Event event, void notifyInternal(@NonNull Event event, @Nullable OnErrorCallback onError) { - String type = event.getImpl().getSeverityReasonType(); - logger.d("Client#notifyInternal() - event captured by Client, type=" + type); - // Don't notify if this event class should be ignored - if (event.shouldDiscardClass()) { - logger.d("Skipping notification - should not notify for this class"); - return; - } - - if (!immutableConfig.shouldNotifyForReleaseStage()) { - logger.d("Skipping notification - should not notify for this release stage"); - return; - } - // set the redacted keys on the event as this // will not have been set for RN/Unity events Set redactedKeys = metadataState.getMetadata().getRedactedKeys(); @@ -773,7 +765,7 @@ void notifyInternal(@NonNull Event event, */ @NonNull public List getBreadcrumbs() { - return new ArrayList<>(breadcrumbState.getStore()); + return breadcrumbState.copy(); } @NonNull @@ -864,9 +856,12 @@ public Object getMetadata(@NonNull String section, @NonNull String key) { } } + // cast map to retain original signature until next major version bump, as this + // method signature is used by Unity/React native @NonNull + @SuppressWarnings({"unchecked", "rawtypes"}) Map getMetadata() { - return metadataState.getMetadata().toMap(); + return (Map) metadataState.getMetadata().toMap(); } /** @@ -911,7 +906,7 @@ public void leaveBreadcrumb(@NonNull String message, void leaveAutoBreadcrumb(@NonNull String message, @NonNull BreadcrumbType type, @NonNull Map metadata) { - if (immutableConfig.shouldRecordBreadcrumbType(type)) { + if (!immutableConfig.shouldDiscardBreadcrumb(type)) { breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt index 88e97d745e..8bd8e47522 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -1,17 +1,23 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + internal class ClientObservable : BaseObservable() { fun postOrientationChange(orientation: String?) { - notifyObservers(StateEvent.UpdateOrientation(orientation)) + updateState { StateEvent.UpdateOrientation(orientation) } } fun postMemoryTrimEvent(isLowMemory: Boolean) { - notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory)) + updateState { StateEvent.UpdateMemoryTrimEvent(isLowMemory) } } - fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) { - notifyObservers( + fun postNdkInstall( + conf: ImmutableConfig, + lastRunInfoPath: String, + consecutiveLaunchCrashes: Int + ) { + updateState { StateEvent.Install( conf.apiKey, conf.enabledErrorTypes.ndkCrashes, @@ -21,10 +27,10 @@ internal class ClientObservable : BaseObservable() { lastRunInfoPath, consecutiveLaunchCrashes ) - ) + } } fun postNdkDeliverPending() { - notifyObservers(StateEvent.DeliverPending) + updateState { StateEvent.DeliverPending } } } 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 9e60eb1262..e02e0ffdff 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 @@ -37,19 +37,19 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS var context: String? = null - var redactedKeys: Set = metadataState.metadata.redactedKeys + var redactedKeys: Set + get() = metadataState.metadata.redactedKeys set(value) { metadataState.metadata.redactedKeys = value - field = value } var discardClasses: Set = emptySet() var enabledReleaseStages: Set? = null - var enabledBreadcrumbTypes: Set? = BreadcrumbType.values().toSet() + var enabledBreadcrumbTypes: Set? = null var projectPackages: Set = emptySet() var persistenceDirectory: File? = null - protected val plugins = mutableSetOf() + protected val plugins = HashSet() override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError) override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError) 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 c4dc5a5a01..033a4d96e1 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 @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.io.File; import java.util.Locale; @@ -19,7 +20,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { private static final int MIN_BREADCRUMBS = 0; private static final int MAX_BREADCRUMBS = 100; - private static final String API_KEY_REGEX = "[A-Fa-f0-9]{32}"; + private static final int VALID_API_KEY_LEN = 32; private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0; final ConfigInternal impl; @@ -47,14 +48,29 @@ static Configuration load(@NonNull Context context, @NonNull String apiKey) { } private void validateApiKey(String value) { - if (Intrinsics.isEmpty(value)) { - throw new IllegalArgumentException("No Bugsnag API Key set"); + if (isInvalidApiKey(value)) { + DebugLogger.INSTANCE.w("Invalid configuration. " + + "apiKey should be a 32-character hexademical string, got " + value); } + } - if (!value.matches(API_KEY_REGEX)) { - DebugLogger.INSTANCE.w(String.format("Invalid configuration. apiKey should be a " - + "32-character hexademical string, got \"%s\"", value)); + @VisibleForTesting + static boolean isInvalidApiKey(String apiKey) { + if (Intrinsics.isEmpty(apiKey)) { + throw new IllegalArgumentException("No Bugsnag API Key set"); + } + if (apiKey.length() != VALID_API_KEY_LEN) { + return true; + } + // check whether each character is hexadecimal (either a digit or a-f). + // this avoids using a regex to improve startup performance. + for (int k = 0; k < VALID_API_KEY_LEN; k++) { + char chr = apiKey.charAt(k); + if (!Character.isDigit(chr) && (chr < 'a' || chr > 'f')) { + return true; + } } + return false; } private void logNull(String property) { @@ -294,9 +310,9 @@ public void setLaunchDurationMillis(long launchDurationMillis) { if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) { impl.setLaunchDurationMillis(launchDurationMillis); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option launchDurationMillis should be a positive long value." - + "Supplied value is %d", launchDurationMillis)); + + "Supplied value is " + launchDurationMillis); } } @@ -513,9 +529,9 @@ public void setMaxBreadcrumbs(int maxBreadcrumbs) { if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) { impl.setMaxBreadcrumbs(maxBreadcrumbs); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxBreadcrumbs should be an integer between 0-100. " - + "Supplied value is %d", maxBreadcrumbs)); + + "Supplied value is " + maxBreadcrumbs); } } @@ -539,9 +555,9 @@ public void setMaxPersistedEvents(int maxPersistedEvents) { if (maxPersistedEvents >= 0) { impl.setMaxPersistedEvents(maxPersistedEvents); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxPersistedEvents should be a positive integer." - + "Supplied value is %d", maxPersistedEvents)); + + "Supplied value is " + maxPersistedEvents); } } @@ -565,9 +581,9 @@ public void setMaxPersistedSessions(int maxPersistedSessions) { if (maxPersistedSessions >= 0) { impl.setMaxPersistedSessions(maxPersistedSessions); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxPersistedSessions should be a positive integer." - + "Supplied value is %d", maxPersistedSessions)); + + "Supplied value is " + maxPersistedSessions); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ContextState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ContextState.kt index 47ca70cc3c..690f244ab5 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ContextState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ContextState.kt @@ -7,7 +7,7 @@ internal class ContextState(context: String? = null) : BaseObservable() { emitObservableEvent() } - fun emitObservableEvent() = notifyObservers(StateEvent.UpdateContext(context)) + fun emitObservableEvent() = updateState { StateEvent.UpdateContext(context) } fun copy() = ContextState(context) } 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 7f3af2bdd4..bf2e5bf95a 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 @@ -2,6 +2,8 @@ import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -44,10 +46,10 @@ void deliver(@NonNull Event event) { if (session != null) { if (event.isUnhandled()) { event.setSession(session.incrementUnhandledAndCopy()); - notifyObservers(StateEvent.NotifyUnhandled.INSTANCE); + updateState(StateEvent.NotifyUnhandled.INSTANCE); } else { event.setSession(session.incrementHandledAndCopy()); - notifyObservers(StateEvent.NotifyHandled.INSTANCE); + updateState(StateEvent.NotifyHandled.INSTANCE); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 2cd4795134..7243463f0d 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -187,7 +187,7 @@ internal class DeviceDataCollector( return if (displayMetrics != null) { val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels) val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels) - String.format(Locale.US, "%dx%d", max, min) + "${max}x$min" } else { null } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt index 77a19659ef..6b247dd9ca 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt @@ -15,8 +15,7 @@ internal class ErrorInternal @JvmOverloads internal constructor( .mapTo(mutableListOf()) { currentEx -> // Somehow it's possible for stackTrace to be null in rare cases val stacktrace = currentEx.stackTrace ?: arrayOf() - val trace = - Stacktrace.stacktraceFromJavaTrace(stacktrace, projectPackages, logger) + val trace = Stacktrace(stacktrace, projectPackages, logger) val errorInternal = ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace) 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 1f43ca9869..31fb88c074 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,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventFilenameInfo.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventFilenameInfo.kt index 1dce6f8a1f..6d9ff766c9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventFilenameInfo.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventFilenameInfo.kt @@ -1,7 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File -import java.util.Locale import java.util.UUID /** @@ -27,15 +27,7 @@ internal data class EventFilenameInfo( * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" */ fun encode(): String { - return String.format( - Locale.US, - "%d_%s_%s_%s_%s.json", - timestamp, - apiKey, - serializeErrorTypeHeader(errorTypes), - uuid, - suffix - ) + return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json" } fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH 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 f367444efd..8195a2025c 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,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException internal class EventInternal @JvmOverloads internal constructor( diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt index 9294d9ac0e..f0f70e83f9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File import java.io.IOException 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 4e5d235e37..d22d8ddb24 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 @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -148,8 +150,8 @@ public void run() { void flushReports(Collection storedReports) { if (!storedReports.isEmpty()) { - logger.i(String.format(Locale.US, - "Sending %d saved error(s) to Bugsnag", storedReports.size())); + int size = storedReports.size(); + logger.i("Sending " + size + " saved error(s) to Bugsnag"); for (File eventFile : storedReports) { flushEventFile(eventFile); @@ -200,14 +202,12 @@ private void handleEventFlushFailure(Exception exc, File eventFile) { String getFilename(Object object) { EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromEvent(object, null, config); - String encodedInfo = eventInfo.encode(); - return String.format(Locale.US, "%s", encodedInfo); + return eventInfo.encode(); } String getNdkFilename(Object object, String apiKey) { EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromEvent(object, apiKey, config); - String encodedInfo = eventInfo.encode(); - return String.format(Locale.US, "%s", encodedInfo); + return eventInfo.encode(); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java index 4e5f9beab9..057f69b72c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java @@ -35,6 +35,9 @@ void uninstall() { @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { + if (client.getConfig().shouldDiscardError(throwable)) { + return; + } boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); // Notify any subscribed clients of the uncaught exception diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java index ee3cdd180e..4aae1e30e8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java @@ -104,8 +104,7 @@ void enqueueContentForDelivery(String content, String filename) { out.close(); } } catch (Exception exception) { - logger.w(String.format("Failed to close unsent payload writer (%s) ", - filename), exception); + logger.w("Failed to close unsent payload writer: " + filename, exception); } lock.unlock(); } @@ -130,7 +129,7 @@ String write(@NonNull JsonStream.Streamable streamable) { Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); stream = new JsonStream(out); stream.value(streamable); - logger.i(String.format("Saved unsent payload to disk (%s) ", filename)); + logger.i("Saved unsent payload to disk: '" + filename + '\''); return filename; } catch (FileNotFoundException exc) { logger.w("Ignoring FileNotFoundException - unable to create file", exc); @@ -168,8 +167,8 @@ void discardOldestFileIfNeeded() { File oldestFile = files.get(k); if (!queuedFiles.contains(oldestFile)) { - logger.w(String.format("Discarding oldest error as stored " - + "error limit reached (%s)", oldestFile.getPath())); + logger.w("Discarding oldest error as stored " + + "error limit reached: '" + oldestFile.getPath() + '\''); deleteStoredFiles(Collections.singleton(oldestFile)); files.remove(k); k--; diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java index 53ab5a66fa..b299ac201c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java @@ -3,6 +3,8 @@ import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR; import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION; +import com.bugsnag.android.internal.ImmutableConfig; + import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index d79fb69a47..770cb65a92 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt index 30774ac663..018b1788fe 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.util.concurrent.RejectedExecutionException import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit @@ -34,7 +35,7 @@ internal class LaunchCrashTracker @JvmOverloads constructor( fun markLaunchCompleted() { executor.shutdown() launching.set(false) - notifyObservers(StateEvent.UpdateIsLaunching(false)) + updateState { StateEvent.UpdateIsLaunching(false) } logger.d("App launch period marked as complete") } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Metadata.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Metadata.kt index a2e07ae2d1..4dc8a51668 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Metadata.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Metadata.kt @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap * Diagnostic information is presented on your Bugsnag dashboard in tabs. */ internal data class Metadata @JvmOverloads constructor( - internal val store: ConcurrentHashMap = ConcurrentHashMap() + internal val store: MutableMap> = ConcurrentHashMap() ) : JsonStream.Streamable, MetadataAware { val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer() @@ -38,12 +38,9 @@ internal data class Metadata @JvmOverloads constructor( if (value == null) { clearMetadata(section, key) } else { - var tab = store[section] - if (tab !is MutableMap<*, *>) { - tab = ConcurrentHashMap() - store[section] = tab - } - insertValue(tab as MutableMap, key, value) + val tab = store[section] ?: ConcurrentHashMap() + store[section] = tab + insertValue(tab, key, value) } } @@ -52,7 +49,7 @@ internal data class Metadata @JvmOverloads constructor( // only merge if both the existing and new value are maps val existingValue = map[key] - if (obj is MutableMap<*, *> && existingValue is MutableMap<*, *>) { + if (existingValue != null && obj is Map<*, *>) { val maps = listOf(existingValue as Map, newValue as Map) obj = mergeMaps(maps) } @@ -65,49 +62,41 @@ internal data class Metadata @JvmOverloads constructor( override fun clearMetadata(section: String, key: String) { val tab = store[section] + tab?.remove(key) - if (tab is MutableMap<*, *>) { - tab.remove(key) - - if (tab.isEmpty()) { - store.remove(section) - } + if (tab.isNullOrEmpty()) { + store.remove(section) } } override fun getMetadata(section: String): Map? { - return store[section] as (Map?) + return store[section] } override fun getMetadata(section: String, key: String): Any? { - return when (val tab = store[section]) { - is Map<*, *> -> (tab as Map?)!![key] - else -> tab - } + return getMetadata(section)?.get(key) } - fun toMap(): ConcurrentHashMap { - val hashMap = ConcurrentHashMap(store) + fun toMap(): MutableMap> { + val copy = ConcurrentHashMap(store) // deep copy each section store.entries.forEach { - if (it.value is ConcurrentHashMap<*, *>) { - hashMap[it.key] = ConcurrentHashMap(it.value as ConcurrentHashMap<*, *>) - } + copy[it.key] = ConcurrentHashMap(it.value) } - return hashMap + return copy } companion object { fun merge(vararg data: Metadata): Metadata { val stores = data.map { it.toMap() } val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys } - val newMeta = Metadata(mergeMaps(stores)) + val newMeta = Metadata(mergeMaps(stores) as MutableMap>) newMeta.redactedKeys = redactKeys.toSet() return newMeta } - internal fun mergeMaps(data: List>): ConcurrentHashMap { + internal fun mergeMaps(data: List>): MutableMap { val keys = data.flatMap { it.keys }.toSet() val result = ConcurrentHashMap() @@ -120,7 +109,7 @@ internal data class Metadata @JvmOverloads constructor( } private fun getMergeValue( - result: ConcurrentHashMap, + result: MutableMap, key: String, map: Map ) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt index d89035b635..d95177a5a4 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt @@ -28,8 +28,8 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : private fun notifyClear(section: String, key: String?) { when (key) { - null -> notifyObservers(StateEvent.ClearMetadataSection(section)) - else -> notifyObservers(StateEvent.ClearMetadataValue(section, key)) + null -> updateState { StateEvent.ClearMetadataSection(section) } + else -> updateState { StateEvent.ClearMetadataValue(section, key) } } } @@ -55,13 +55,13 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : private fun notifyMetadataAdded(section: String, key: String, value: Any?) { when (value) { null -> notifyClear(section, key) - else -> notifyObservers(AddMetadata(section, key, metadata.getMetadata(section, key))) + else -> updateState { AddMetadata(section, key, metadata.getMetadata(section, key)) } } } private fun notifyMetadataAdded(section: String, value: Map) { value.entries.forEach { - notifyObservers(AddMetadata(section, it.key, metadata.getMetadata(it.key))) + updateState { AddMetadata(section, it.key, metadata.getMetadata(it.key)) } } } } 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 30e857b601..56a000d90c 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,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import android.annotation.SuppressLint; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -325,7 +327,7 @@ public static void deliverReport(@Nullable byte[] releaseStageBytes, ImmutableConfig config = client.getConfig(); if (releaseStage == null || releaseStage.length() == 0 - || config.shouldNotifyForReleaseStage()) { + || !config.shouldDiscardByReleaseStage()) { EventStore eventStore = client.getEventStore(); String filename = eventStore.getNdkFilename(payload, apiKey); @@ -368,6 +370,9 @@ public static void notify(@NonNull final String name, @NonNull final String message, @NonNull final Severity severity, @NonNull final StackTraceElement[] stacktrace) { + if (getClient().getConfig().shouldDiscardError(name)) { + return; + } Throwable exc = new RuntimeException(); exc.setStackTrace(stacktrace); 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 6d02f69e7e..05c201731f 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.9.4", + var version: String = "5.9.5", 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 d4b4f93127..01f268bd39 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 @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + internal class PluginClient( userPlugins: Set, private val immutableConfig: ImmutableConfig, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java index c3e9f35bc9..66bfa46f55 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,10 +47,7 @@ public int compare(File lhs, File rhs) { @NonNull @Override String getFilename(Object object) { - return String.format(Locale.US, - "%s%d_v2.json", - UUID.randomUUID().toString(), - System.currentTimeMillis()); + return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json"; } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java index c02fdd3b51..6e4b8467d3 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -79,6 +81,9 @@ class SessionTracker extends BaseObservable { @VisibleForTesting Session startNewSession(@NonNull Date date, @Nullable User user, boolean autoCaptured) { + if (client.getConfig().shouldDiscardSession(autoCaptured)) { + return null; + } String id = UUID.randomUUID().toString(); Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger); currentSession.set(session); @@ -87,6 +92,9 @@ Session startNewSession(@NonNull Date date, @Nullable User user, } Session startSession(boolean autoCaptured) { + if (client.getConfig().shouldDiscardSession(autoCaptured)) { + return null; + } return startNewSession(new Date(), client.getUser(), autoCaptured); } @@ -95,7 +103,7 @@ void pauseSession() { if (session != null) { session.isPaused.set(true); - notifyObservers(StateEvent.PauseSession.INSTANCE); + updateState(StateEvent.PauseSession.INSTANCE); } } @@ -116,10 +124,10 @@ boolean resumeSession() { return resumed; } - private void notifySessionStartObserver(Session session) { - String startedAt = DateUtils.toIso8601(session.getStartedAt()); - notifyObservers(new StateEvent.StartSession(session.getId(), startedAt, - session.getHandledCount(), session.getUnhandledCount())); + private void notifySessionStartObserver(final Session session) { + final String startedAt = DateUtils.toIso8601(session.getStartedAt()); + updateState(new StateEvent.StartSession(session.getId(), startedAt, + session.getHandledCount(), session.getUnhandledCount())); } /** @@ -137,13 +145,16 @@ private void notifySessionStartObserver(Session session) { Session registerExistingSession(@Nullable Date date, @Nullable String sessionId, @Nullable User user, int unhandledCount, int handledCount) { + if (client.getConfig().shouldDiscardSession(false)) { + return null; + } Session session = null; if (date != null && sessionId != null) { session = new Session(sessionId, date, user, unhandledCount, handledCount, client.getNotifier(), logger); notifySessionStartObserver(session); } else { - notifyObservers(StateEvent.PauseSession.INSTANCE); + updateState(StateEvent.PauseSession.INSTANCE); } currentSession.set(session); return session; @@ -157,18 +168,12 @@ Session registerExistingSession(@Nullable Date date, @Nullable String sessionId, */ private void trackSessionIfNeeded(final Session session) { logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client"); - - boolean notifyForRelease = configuration.shouldNotifyForReleaseStage(); - session.setApp(client.getAppDataCollector().generateApp()); session.setDevice(client.getDeviceDataCollector().generateDevice()); boolean deliverSession = callbackState.runOnSessionTasks(session, logger); - if (deliverSession && notifyForRelease - && (configuration.getAutoTrackSessions() || !session.isAutoCaptured()) - && session.isTracked().compareAndSet(false, true)) { + if (deliverSession && session.isTracked().compareAndSet(false, true)) { notifySessionStartObserver(session); - flushAsync(); flushInMemorySession(session); } @@ -360,8 +365,8 @@ void updateForegroundTracker(String activityName, boolean activityStarting, long private void notifyNdkInForeground() { Boolean inForeground = isInForeground(); - boolean foreground = inForeground != null ? inForeground : false; - notifyObservers(new StateEvent.UpdateInForeground(foreground, getContextActivity())); + final boolean foreground = inForeground != null ? inForeground : false; + updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity())); } @Nullable diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SeverityReason.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SeverityReason.java index 1a472d8224..d445e6fd95 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SeverityReason.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SeverityReason.java @@ -69,8 +69,7 @@ static SeverityReason newInstance(@SeverityReasonType String severityReasonType, case REASON_LOG: return new SeverityReason(severityReasonType, severity, false, attrVal); default: - String msg = String.format("Invalid argument '%s' for severityReason", - severityReasonType); + String msg = "Invalid argument for severityReason: '" + severityReasonType + '\''; throw new IllegalArgumentException(msg); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Stacktrace.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Stacktrace.kt index a3deebc005..d7ea3f6b49 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Stacktrace.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Stacktrace.kt @@ -20,43 +20,9 @@ internal class Stacktrace : JsonStream.Streamable { * not. */ fun inProject(className: String, projectPackages: Collection): Boolean? { - for (packageName in projectPackages) { - if (className.startsWith(packageName)) { - return true - } - } - return null - } - - fun stacktraceFromJavaTrace( - stacktrace: Array, - projectPackages: Collection, - logger: Logger - ): Stacktrace { - val frames = stacktrace.mapNotNull { serializeStackframe(it, projectPackages, logger) } - return Stacktrace(frames) - } - - private fun serializeStackframe( - el: StackTraceElement, - projectPackages: Collection, - logger: Logger - ): Stackframe? { - try { - val methodName = when { - el.className.isNotEmpty() -> el.className + "." + el.methodName - else -> el.methodName - } - - return Stackframe( - methodName, - if (el.fileName == null) "Unknown" else el.fileName, - el.lineNumber, - inProject(el.className, projectPackages) - ) - } catch (lineEx: Exception) { - logger.w("Failed to serialize stacktrace", lineEx) - return null + return when { + projectPackages.any { className.startsWith(it) } -> true + else -> null } } } @@ -67,13 +33,53 @@ internal class Stacktrace : JsonStream.Streamable { trace = limitTraceLength(frames) } - private fun limitTraceLength(frames: List): List { + constructor( + stacktrace: Array, + projectPackages: Collection, + logger: Logger + ) { + val frames = limitTraceLength(stacktrace) + trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) } + } + + private fun limitTraceLength(frames: Array): Array { + return when { + frames.size >= STACKTRACE_TRIM_LENGTH -> frames.sliceArray(0 until STACKTRACE_TRIM_LENGTH) + else -> frames + } + } + + private fun limitTraceLength(frames: List): List { return when { frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH) else -> frames } } + private fun serializeStackframe( + el: StackTraceElement, + projectPackages: Collection, + logger: Logger + ): Stackframe? { + try { + val className = el.className + val methodName = when { + className.isNotEmpty() -> className + "." + el.methodName + else -> el.methodName + } + + return Stackframe( + methodName, + el.fileName ?: "Unknown", + el.lineNumber, + inProject(className, projectPackages) + ) + } catch (lineEx: Exception) { + logger.w("Failed to serialize stacktrace", lineEx) + return null + } + } + @Throws(IOException::class) override fun toStream(writer: JsonStream) { writer.beginArray() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt index bc3c83e0ea..a94a0301fb 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt @@ -1,47 +1,66 @@ package com.bugsnag.android -sealed class StateEvent { +sealed class StateEvent { // JvmField allows direct field access optimizations + class Install( - val apiKey: String, - val autoDetectNdkCrashes: Boolean, - val appVersion: String?, - val buildUuid: String?, - val releaseStage: String?, - val lastRunInfoPath: String, - val consecutiveLaunchCrashes: Int + @JvmField val apiKey: String, + @JvmField val autoDetectNdkCrashes: Boolean, + @JvmField val appVersion: String?, + @JvmField val buildUuid: String?, + @JvmField val releaseStage: String?, + @JvmField val lastRunInfoPath: String, + @JvmField val consecutiveLaunchCrashes: Int ) : StateEvent() object DeliverPending : StateEvent() - class AddMetadata(val section: String, val key: String?, val value: Any?) : StateEvent() - class ClearMetadataSection(val section: String) : StateEvent() - class ClearMetadataValue(val section: String, val key: String?) : StateEvent() + class AddMetadata( + @JvmField val section: String, + @JvmField val key: String?, + @JvmField val value: Any? + ) : StateEvent() + + class ClearMetadataSection(@JvmField val section: String) : StateEvent() + + class ClearMetadataValue( + @JvmField val section: String, + @JvmField val key: String? + ) : StateEvent() class AddBreadcrumb( - val message: String, - val type: BreadcrumbType, - val timestamp: String, - val metadata: MutableMap + @JvmField val message: String, + @JvmField val type: BreadcrumbType, + @JvmField val timestamp: String, + @JvmField val metadata: MutableMap ) : StateEvent() object NotifyHandled : StateEvent() + object NotifyUnhandled : StateEvent() object PauseSession : StateEvent() + class StartSession( - val id: String, - val startedAt: String, - val handledCount: Int, + @JvmField val id: String, + @JvmField val startedAt: String, + @JvmField val handledCount: Int, val unhandledCount: Int ) : StateEvent() - class UpdateContext(val context: String?) : StateEvent() - class UpdateInForeground(val inForeground: Boolean, val contextActivity: String?) : StateEvent() - class UpdateLastRunInfo(val consecutiveLaunchCrashes: Int) : StateEvent() - class UpdateIsLaunching(val isLaunching: Boolean) : StateEvent() - class UpdateOrientation(val orientation: String?) : StateEvent() + class UpdateContext(@JvmField val context: String?) : StateEvent() + + class UpdateInForeground( + @JvmField val inForeground: Boolean, + val contextActivity: String? + ) : StateEvent() + + class UpdateLastRunInfo(@JvmField val consecutiveLaunchCrashes: Int) : StateEvent() + + class UpdateIsLaunching(@JvmField val isLaunching: Boolean) : StateEvent() + + class UpdateOrientation(@JvmField val orientation: String?) : StateEvent() - class UpdateUser(val user: User) : StateEvent() + class UpdateUser(@JvmField val user: User) : StateEvent() - class UpdateMemoryTrimEvent(val isLowMemory: Boolean) : StateEvent() + class UpdateMemoryTrimEvent(@JvmField val isLowMemory: Boolean) : StateEvent() } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java index 0b0f9bc5cd..44092a0358 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java @@ -1,10 +1,13 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; + import androidx.annotation.NonNull; import java.util.HashMap; @@ -79,7 +82,7 @@ public void onReceive(@NonNull Context context, @NonNull Intent intent) { String val = valObj.toString(); if (isAndroidKey(key)) { // shorten the Intent action - meta.put("Extra", String.format("%s: %s", shortAction, val)); + meta.put("Extra", shortAction + ": " + val); } else { meta.put(key, val); } @@ -121,7 +124,9 @@ static String shortenActionNameIfNeeded(@NonNull String action) { private Map buildActions() { Map actions = new HashMap<>(); - if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.USER)) { + ImmutableConfig config = client.getConfig(); + + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.USER)) { actions.put("android.appwidget.action.APPWIDGET_DELETED", BreadcrumbType.USER); actions.put("android.appwidget.action.APPWIDGET_DISABLED", BreadcrumbType.USER); actions.put("android.appwidget.action.APPWIDGET_ENABLED", BreadcrumbType.USER); @@ -130,7 +135,7 @@ private Map buildActions() { actions.put("android.intent.action.DOCK_EVENT", BreadcrumbType.USER); } - if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.STATE)) { + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { actions.put("android.appwidget.action.APPWIDGET_HOST_RESTORED", BreadcrumbType.STATE); actions.put("android.appwidget.action.APPWIDGET_RESTORED", BreadcrumbType.STATE); actions.put("android.appwidget.action.APPWIDGET_UPDATE", BreadcrumbType.STATE); @@ -158,7 +163,7 @@ private Map buildActions() { actions.put("android.os.action.POWER_SAVE_MODE_CHANGED", BreadcrumbType.STATE); } - if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.NAVIGATION)) { + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.NAVIGATION)) { actions.put("android.intent.action.DREAMING_STARTED", BreadcrumbType.NAVIGATION); actions.put("android.intent.action.DREAMING_STOPPED", BreadcrumbType.NAVIGATION); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt index 8839badaec..ea27eb92a8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException /** @@ -11,7 +12,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc sendThreads: ThreadSendPolicy, projectPackages: Collection, logger: Logger, - currentThread: java.lang.Thread = java.lang.Thread.currentThread(), + currentThread: java.lang.Thread? = null, stackTraces: MutableMap>? = null ) : JsonStream.Streamable { @@ -30,7 +31,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc threads = when { recordThreads -> captureThreadTrace( stackTraces ?: java.lang.Thread.getAllStackTraces(), - currentThread, + currentThread ?: java.lang.Thread.currentThread(), exc, isUnhandled, projectPackages, @@ -64,7 +65,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc val trace = stackTraces[thread] if (trace != null) { - val stacktrace = Stacktrace.stacktraceFromJavaTrace(trace, projectPackages, logger) + val stacktrace = Stacktrace(trace, projectPackages, logger) val errorThread = thread.id == currentThreadId Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger) } else { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserState.kt index 8f408b7156..16da106515 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserState.kt @@ -7,5 +7,5 @@ internal class UserState(user: User) : BaseObservable() { emitObservableEvent() } - fun emitObservableEvent() = notifyObservers(StateEvent.UpdateUser(user)) + fun emitObservableEvent() = updateState { StateEvent.UpdateUser(user) } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt index 30540ca41c..29ccd71a19 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.StateObserver import java.io.File import java.io.IOException import java.util.concurrent.atomic.AtomicReference @@ -55,11 +57,13 @@ internal class UserStore @JvmOverloads constructor( else -> UserState(User(deviceId, null, null)) } - userState.addObserver { _, arg -> - if (arg is StateEvent.UpdateUser) { - save(arg.user) + userState.addObserver( + StateObserver { event -> + if (event is StateEvent.UpdateUser) { + save(event.user) + } } - } + ) return userState } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ImmutableConfig.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt similarity index 58% rename from bugsnag-android-core/src/main/java/com/bugsnag/android/ImmutableConfig.kt rename to bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index d4d0753c2e..5c434a7d32 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ImmutableConfig.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -1,11 +1,30 @@ -package com.bugsnag.android +package com.bugsnag.android.internal import android.content.Context import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo import android.content.pm.PackageManager +import androidx.annotation.VisibleForTesting +import com.bugsnag.android.BreadcrumbType +import com.bugsnag.android.Configuration +import com.bugsnag.android.Connectivity +import com.bugsnag.android.DebugLogger +import com.bugsnag.android.DefaultDelivery +import com.bugsnag.android.Delivery +import com.bugsnag.android.DeliveryParams +import com.bugsnag.android.EndpointConfiguration +import com.bugsnag.android.ErrorTypes +import com.bugsnag.android.EventPayload +import com.bugsnag.android.Logger +import com.bugsnag.android.ManifestConfigLoader +import com.bugsnag.android.NoopLogger +import com.bugsnag.android.ThreadSendPolicy +import com.bugsnag.android.errorApiHeaders +import com.bugsnag.android.safeUnrollCauses +import com.bugsnag.android.sessionApiHeaders import java.io.File -internal data class ImmutableConfig( +data class ImmutableConfig( val apiKey: String, val autoDetectErrors: Boolean, val enabledErrorTypes: ErrorTypes, @@ -29,21 +48,12 @@ internal data class ImmutableConfig( val maxPersistedEvents: Int, val maxPersistedSessions: Int, val persistenceDirectory: File, - val sendLaunchCrashesSynchronously: Boolean -) { + val sendLaunchCrashesSynchronously: Boolean, - /** - * Checks if the given release stage should be notified or not - * - * @return true if the release state should be notified else false - */ - @JvmName("shouldNotifyForReleaseStage") - internal fun shouldNotifyForReleaseStage() = - enabledReleaseStages == null || enabledReleaseStages.contains(releaseStage) - - @JvmName("shouldRecordBreadcrumbType") - internal fun shouldRecordBreadcrumbType(type: BreadcrumbType) = - enabledBreadcrumbTypes == null || enabledBreadcrumbTypes.contains(type) + // results cached here to avoid unnecessary lookups in Client. + val packageInfo: PackageInfo?, + val appInfo: ApplicationInfo? +) { @JvmName("getErrorApiDeliveryParams") internal fun getErrorApiDeliveryParams(payload: EventPayload) = @@ -52,11 +62,73 @@ internal data class ImmutableConfig( @JvmName("getSessionApiDeliveryParams") internal fun getSessionApiDeliveryParams() = DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey)) + + /** + * Returns whether the given throwable should be discarded + * based on the automatic data capture settings in [Configuration]. + */ + fun shouldDiscardError(exc: Throwable): Boolean { + return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc) + } + + /** + * Returns whether the given error should be discarded + * based on the automatic data capture settings in [Configuration]. + */ + fun shouldDiscardError(errorClass: String?): Boolean { + return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(errorClass) + } + + /** + * Returns whether a session should be discarded based on the + * automatic data capture settings in [Configuration]. + */ + fun shouldDiscardSession(autoCaptured: Boolean): Boolean { + return shouldDiscardByReleaseStage() || (autoCaptured && !autoTrackSessions) + } + + /** + * Returns whether breadcrumbs with the given type should be discarded or not. + */ + fun shouldDiscardBreadcrumb(type: BreadcrumbType): Boolean { + return enabledBreadcrumbTypes != null && !enabledBreadcrumbTypes.contains(type) + } + + /** + * Returns whether errors/sessions should be discarded or not based on the enabled + * release stages. + */ + fun shouldDiscardByReleaseStage(): Boolean { + return enabledReleaseStages != null && !enabledReleaseStages.contains(releaseStage) + } + + /** + * Returns whether errors with the given errorClass should be discarded or not. + */ + @VisibleForTesting + internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean { + return discardClasses.contains(errorClass) + } + + /** + * Returns whether errors should be discarded or not based on the errorClass, as deduced + * by the Throwable's class name. + */ + @VisibleForTesting + internal fun shouldDiscardByErrorClass(exc: Throwable): Boolean { + return exc.safeUnrollCauses().any { throwable -> + val errorClass = throwable.javaClass.name + shouldDiscardByErrorClass(errorClass) + } + } } +@JvmOverloads internal fun convertToImmutableConfig( config: Configuration, - buildUuid: String? = null + buildUuid: String? = null, + packageInfo: PackageInfo? = null, + appInfo: ApplicationInfo? = null ): ImmutableConfig { val errorTypes = when { config.autoDetectErrors -> config.enabledErrorTypes.copy() @@ -87,7 +159,9 @@ internal fun convertToImmutableConfig( maxPersistedSessions = config.maxPersistedSessions, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), persistenceDirectory = config.persistenceDirectory!!, - sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously + sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, + packageInfo = packageInfo, + appInfo = appInfo ) } @@ -144,7 +218,7 @@ internal fun sanitiseConfiguration( if (configuration.persistenceDirectory == null) { configuration.persistenceDirectory = appContext.cacheDir } - return convertToImmutableConfig(configuration, buildUuid) + return convertToImmutableConfig(configuration, buildUuid, packageInfo, appInfo) } internal const val RELEASE_STAGE_DEVELOPMENT = "development" diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/StateObserver.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/StateObserver.java new file mode 100644 index 0000000000..d9230e21e1 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/StateObserver.java @@ -0,0 +1,14 @@ +package com.bugsnag.android.internal; + +import com.bugsnag.android.StateEvent; + +import androidx.annotation.NonNull; + +public interface StateObserver { + + /** + * This is called whenever the notifier's state is altered, so that observers can react + * appropriately. This is intended for internal use only. + */ + void onStateChange(@NonNull StateEvent event); +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyConfigValidationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyConfigValidationTest.kt new file mode 100644 index 0000000000..8916acbed0 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyConfigValidationTest.kt @@ -0,0 +1,53 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApiKeyConfigValidationTest { + + @Test(expected = IllegalArgumentException::class) + fun testEmptyApiKey() { + Configuration("") + } + + @Test + fun testWrongSizeApiKey() { + val config = Configuration("abfe05f") + assertEquals("abfe05f", config.apiKey) + } + + @Test + fun testNonHexApiKey() { + val config = Configuration("yej0492j55z92z2p") + assertEquals("yej0492j55z92z2p", config.apiKey) + } + + @Test(expected = IllegalArgumentException::class) + fun testSettingEmptyApiKey() { + val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") + config.apiKey = "" + assertEquals("", config.apiKey) + } + + @Test + fun testSettingWrongSizeApiKey() { + val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") + config.apiKey = "abfe05f" + assertEquals("abfe05f", config.apiKey) + } + + @Test + fun testSettingNonHexApiKey() { + val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") + config.apiKey = "yej0492j55z92z2p" + assertEquals("yej0492j55z92z2p", config.apiKey) + } + + @Test + fun setApiKey() { + val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") + assertEquals("5d1ec5bd39a74caa1267142706a7fb21", config.apiKey) + config.apiKey = "000005bd39a74caa1267142706a7fb21" + assertEquals("000005bd39a74caa1267142706a7fb21", config.apiKey) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyValidationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyValidationTest.kt index 93abf835b7..3e7e52214d 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyValidationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ApiKeyValidationTest.kt @@ -1,48 +1,37 @@ package com.bugsnag.android -import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class ApiKeyValidationTest { @Test(expected = IllegalArgumentException::class) - fun testEmptyApiKey() { - Configuration("") - } - - @Test - fun testWrongSizeApiKey() { - Configuration("abfe05f") - } - - @Test - fun testNonHexApiKey() { - Configuration("yej0492j55z92z2p") + fun testNullApiKey() { + Configuration.isInvalidApiKey(null) } @Test(expected = IllegalArgumentException::class) - fun testSettingEmptyApiKey() { - val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") - config.apiKey = "" + fun testEmptyApiKey() { + Configuration.isInvalidApiKey("") } @Test - fun testSettingWrongSizeApiKey() { - val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") - config.apiKey = "abfe05f" + fun testWrongSizeApiKey() { + assertTrue(Configuration.isInvalidApiKey("abfe05f")) + assertTrue(Configuration.isInvalidApiKey("5d1ec5bd39a74caa1267142706a7fb212")) } @Test fun testSettingNonHexApiKey() { - val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") - config.apiKey = "yej0492j55z92z2p" + assertTrue(Configuration.isInvalidApiKey("5d1ec5bd3Ga74caa1267142706a7fb21")) + assertTrue(Configuration.isInvalidApiKey("5d1ec5bd39a7%caa1267_42706P7fb212")) + assertFalse(Configuration.isInvalidApiKey("5d1ec5bd39a74caa1267142706a7fb21")) } @Test fun setApiKey() { - val config = Configuration("5d1ec5bd39a74caa1267142706a7fb21") - assertEquals("5d1ec5bd39a74caa1267142706a7fb21", config.apiKey) - config.apiKey = "000005bd39a74caa1267142706a7fb21" - assertEquals("000005bd39a74caa1267142706a7fb21", config.apiKey) + assertFalse(Configuration.isInvalidApiKey("5d1ec5bd39a74caa1267142706a7fb21")) + assertFalse(Configuration.isInvalidApiKey("000005bd39a74caa1267142706a7fb21")) } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt index 264fa20cbd..5c87ad95e7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt @@ -4,7 +4,7 @@ import android.app.ActivityManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import com.bugsnag.android.BugsnagTestUtils.convert +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -40,14 +40,14 @@ internal class AppMetadataSerializationTest { // populate metadata fields `when`(sessionTracker.contextActivity).thenReturn("MyActivity") - `when`(pm.getApplicationInfo(any(), anyInt())).thenReturn(ApplicationInfo()) `when`(pm.getApplicationLabel(any())).thenReturn("MyApp") + `when`(pm.getApplicationInfo(any(), anyInt())).thenReturn(ApplicationInfo()) // construct AppDataCollector object val appData = AppDataCollector( context, pm, - convert(config), + convertToImmutableConfig(config, null, null, ApplicationInfo()), sessionTracker, am, launchCrashTracker, diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt index 219c58d697..8153a7c669 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt @@ -18,7 +18,15 @@ class BreadcrumbStateTest { @Before fun setUp() { - breadcrumbState = BreadcrumbState(20, CallbackState(), NoopLogger) + breadcrumbState = createBreadcrumbState(CallbackState()) + } + + private fun createBreadcrumbState(cbState: CallbackState): BreadcrumbState { + return BreadcrumbState( + 20, + cbState, + NoopLogger + ) } /** @@ -36,46 +44,119 @@ class BreadcrumbStateTest { ) breadcrumbState.add(Breadcrumb(longStr, NoopLogger)) - val crumbs = breadcrumbState.store.toList() + val crumbs = breadcrumbState.copy() assertEquals(3, crumbs.size) assertEquals(longStr, crumbs[2].message) } /** - * Verifies that leaving breadcrumbs drops the oldest breadcrumb after reaching the max limit + * Verifies that no breadcrumbs are added if the size limit is set to 0 */ @Test - fun testSizeLimitBeforeAdding() { + fun testZeroMaxBreadcrumbs() { + breadcrumbState = BreadcrumbState(0, CallbackState(), NoopLogger) + breadcrumbState.add(Breadcrumb("1", NoopLogger)) + breadcrumbState.add(Breadcrumb("2", NoopLogger)) + assertTrue(breadcrumbState.copy().isEmpty()) + } + + /** + * Verifies that one breadcrumb is added if the size limit is set to 1 + */ + @Test + fun testOneMaxBreadcrumb() { + breadcrumbState = BreadcrumbState(1, CallbackState(), NoopLogger) + assertTrue(breadcrumbState.copy().isEmpty()) + + breadcrumbState.add(Breadcrumb("A", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("A", breadcrumbState.copy()[0].message) + + breadcrumbState.add(Breadcrumb("B", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("B", breadcrumbState.copy()[0].message) + + breadcrumbState.add(Breadcrumb("C", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("C", breadcrumbState.copy()[0].message) + } + + /** + * Verifies that the ring buffer can be filled up to the maxBreadcrumbs limit + */ + @Test + fun testRingBufferFilled() { + breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) + + for (k in 1..5) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("1", crumbs[0].message) + assertEquals("2", crumbs[1].message) + assertEquals("3", crumbs[2].message) + assertEquals("4", crumbs[3].message) + assertEquals("5", crumbs[4].message) + } + + /** + * Verifies that the breadcrumbs are in order after the ring buffer wraps around by one + */ + @Test + fun testRingBufferExceededByOne() { breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) for (k in 1..6) { breadcrumbState.add(Breadcrumb("$k", NoopLogger)) } - val crumbs = breadcrumbState.store.toList() - assertEquals("2", crumbs.first().message) - assertEquals("6", crumbs.last().message) + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("2", crumbs[0].message) + assertEquals("3", crumbs[1].message) + assertEquals("4", crumbs[2].message) + assertEquals("5", crumbs[3].message) + assertEquals("6", crumbs[4].message) } /** - * Verifies that no breadcrumbs are added if the size limit is set to 0 + * Verifies that the breadcrumbs are in order after the ring buffer wraps around by four */ @Test - fun testSetSizeEmpty() { - breadcrumbState = BreadcrumbState(0, CallbackState(), NoopLogger) - breadcrumbState.add(Breadcrumb("1", NoopLogger)) - breadcrumbState.add(Breadcrumb("2", NoopLogger)) - assertTrue(breadcrumbState.store.isEmpty()) + fun testRingBufferExceededByFour() { + breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) + + for (k in 1..9) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("5", crumbs[0].message) + assertEquals("6", crumbs[1].message) + assertEquals("7", crumbs[2].message) + assertEquals("8", crumbs[3].message) + assertEquals("9", crumbs[4].message) } /** - * Verifies that setting a negative size has no effect + * Verifies that the breadcrumbs are in order after the ring buffer is filled twice */ @Test - fun testSetSizeNegative() { - breadcrumbState = BreadcrumbState(-1, CallbackState(), NoopLogger) - breadcrumbState.add(Breadcrumb("1", NoopLogger)) - assertEquals(0, breadcrumbState.store.size) + fun testRingBufferFilledTwice() { + breadcrumbState = BreadcrumbState(3, CallbackState(), NoopLogger) + + for (k in 1..6) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(3, crumbs.size) + assertEquals("4", crumbs[0].message) + assertEquals("5", crumbs[1].message) + assertEquals("6", crumbs[2].message) } /** @@ -84,7 +165,7 @@ class BreadcrumbStateTest { @Test fun testDefaultBreadcrumbType() { breadcrumbState.add(Breadcrumb("1", NoopLogger)) - val crumb = requireNotNull(breadcrumbState.store.peek()) + val crumb = breadcrumbState.copy()[0] assertEquals(MANUAL, crumb.type) } @@ -97,8 +178,16 @@ class BreadcrumbStateTest { for (i in 0..399) { metadata[String.format(Locale.US, "%d", i)] = "!!" } - breadcrumbState.add(Breadcrumb("Rotated Menu", BreadcrumbType.STATE, metadata, Date(0), NoopLogger)) - assertFalse(breadcrumbState.store.isEmpty()) + breadcrumbState.add( + Breadcrumb( + "Rotated Menu", + BreadcrumbType.STATE, + metadata, + Date(0), + NoopLogger + ) + ) + assertFalse(breadcrumbState.copy().isEmpty()) } /** @@ -125,14 +214,18 @@ class BreadcrumbStateTest { @Test fun testOnBreadcrumbCallback() { val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { - true - } + val cbState = CallbackState( + onBreadcrumbTasks = mutableListOf( + OnBreadcrumbCallback { + true + } + ) ) + breadcrumbState = createBreadcrumbState(cbState) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(breadcrumb, breadcrumbState.store.peek()) + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(breadcrumb, copy.first()) } /** @@ -140,25 +233,31 @@ class BreadcrumbStateTest { */ @Test fun testOnBreadcrumbCallbackFalse() { + val cbState = CallbackState() + breadcrumbState = createBreadcrumbState(cbState) + val requiredBreadcrumb = Breadcrumb("Hello there", NoopLogger) breadcrumbState.add(requiredBreadcrumb) - val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( + cbState.addOnBreadcrumb( OnBreadcrumbCallback { givenBreadcrumb -> givenBreadcrumb.metadata?.put("callback", "first") false } ) - breadcrumbState.callbackState.addOnBreadcrumb( + cbState.addOnBreadcrumb( OnBreadcrumbCallback { givenBreadcrumb -> givenBreadcrumb.metadata?.put("callback", "second") true } ) + + val breadcrumb = Breadcrumb("Whoops", NoopLogger) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(requiredBreadcrumb, breadcrumbState.store.first()) + + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(requiredBreadcrumb, copy.first()) assertNotNull(breadcrumb.metadata) assertEquals("first", breadcrumb.metadata?.get("callback")) } @@ -168,22 +267,37 @@ class BreadcrumbStateTest { */ @Test fun testOnBreadcrumbCallbackException() { - val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { - throw IllegalStateException("Oh no") - } - ) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { givenBreadcrumb -> - givenBreadcrumb.metadata?.put("callback", "second") - true - } + val cbState = CallbackState( + onBreadcrumbTasks = mutableListOf( + OnBreadcrumbCallback { + throw IllegalStateException("Oh no") + }, + OnBreadcrumbCallback { givenBreadcrumb -> + givenBreadcrumb.metadata?.put("callback", "second") + true + } + ) ) + breadcrumbState = createBreadcrumbState(cbState) + + val breadcrumb = Breadcrumb("Whoops", NoopLogger) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(breadcrumb, breadcrumbState.store.peek()) + + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(breadcrumb, copy[0]) assertNotNull(breadcrumb.metadata) assertEquals("second", breadcrumb.metadata?.get("callback")) } + + @Test + fun testCopyThenAdd() { + breadcrumbState = BreadcrumbState(25, CallbackState(), NoopLogger) + + repeat(1000) { count -> + breadcrumbState.add(Breadcrumb("$count", NoopLogger)) + } + + assertEquals(25, breadcrumbState.copy().size) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagTestUtils.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagTestUtils.java index 3f4714fe8c..061d2f68c6 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagTestUtils.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagTestUtils.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfigKt; + import org.jetbrains.annotations.NotNull; import java.io.File; 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 052bea11b0..94162c04e4 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 @@ -7,23 +7,25 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.StateObserver; + import android.content.Context; import android.os.storage.StorageManager; import androidx.annotation.NonNull; +import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Observable; -import java.util.Observer; @SuppressWarnings("ConstantConditions") @RunWith(MockitoJUnitRunner.class) @@ -140,14 +142,13 @@ public void setUp() { when(metadataState.getMetadata()).thenReturn(new Metadata()); when(immutableConfig.getLogger()).thenReturn(logger); when(immutableConfig.getSendThreads()).thenReturn(ThreadSendPolicy.ALWAYS); - when(immutableConfig.shouldNotifyForReleaseStage()).thenReturn(true); when(deviceDataCollector.generateDeviceWithState(anyLong())).thenReturn(device); when(deviceDataCollector.getDeviceMetadata()).thenReturn(new HashMap()); when(appDataCollector.generateAppWithState()).thenReturn(app); when(appDataCollector.getAppDataMetadata()).thenReturn(new HashMap()); - when(breadcrumbState.getStore()).thenReturn(new ArrayDeque()); + when(breadcrumbState.copy()).thenReturn(new ArrayList()); when(userState.getUser()).thenReturn(new User()); when(callbackState.runOnErrorTasks(any(Event.class), any(Logger.class))).thenReturn(true); } @@ -492,12 +493,12 @@ public void markLaunchCompleted() { @Test public void registerObserver() { - Observer observer = new Observer() { + StateObserver observer = new StateObserver() { @Override - public void update(Observable observable, Object arg) { + public void onStateChange(@NotNull StateEvent event) { } }; - client.registerObserver(observer); + client.addObserver(observer); verify(metadataState, times(1)).addObserver(observer); verify(breadcrumbState, times(1)).addObserver(observer); @@ -511,21 +512,21 @@ public void update(Observable observable, Object arg) { @Test public void unregisterObserver() { - Observer observer = new Observer() { + StateObserver observer = new StateObserver() { @Override - public void update(Observable observable, Object arg) { + public void onStateChange(@NotNull StateEvent event) { } }; - client.unregisterObserver(observer); - - verify(metadataState, times(1)).deleteObserver(observer); - verify(breadcrumbState, times(1)).deleteObserver(observer); - verify(sessionTracker, times(1)).deleteObserver(observer); - verify(clientObservable, times(1)).deleteObserver(observer); - verify(userState, times(1)).deleteObserver(observer); - verify(contextState, times(1)).deleteObserver(observer); - verify(deliveryDelegate, times(1)).deleteObserver(observer); - verify(launchCrashTracker, times(1)).deleteObserver(observer); + client.removeObserver(observer); + + verify(metadataState, times(1)).removeObserver(observer); + verify(breadcrumbState, times(1)).removeObserver(observer); + verify(sessionTracker, times(1)).removeObserver(observer); + verify(clientObservable, times(1)).removeObserver(observer); + verify(userState, times(1)).removeObserver(observer); + verify(contextState, times(1)).removeObserver(observer); + verify(deliveryDelegate, times(1)).removeObserver(observer); + verify(launchCrashTracker, times(1)).removeObserver(observer); } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt index 2577a90c57..a1b3626e7a 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientObservableTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.StateObserver import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -10,26 +11,32 @@ class ClientObservableTest { @Test fun postOrientationChange() { - clientObservable.addObserver { _, arg -> - val msg = arg as StateEvent.UpdateOrientation - assertEquals("landscape", msg.orientation) - } + clientObservable.addObserver( + StateObserver { + val msg = it as StateEvent.UpdateOrientation + assertEquals("landscape", msg.orientation) + } + ) clientObservable.postOrientationChange("landscape") } @Test fun postNdkInstall() { clientObservable.postNdkInstall(BugsnagTestUtils.generateImmutableConfig(), "/foo", 0) - clientObservable.addObserver { _, arg -> - assertTrue(arg is StateEvent.Install) - } + clientObservable.addObserver( + StateObserver { + assertTrue(it is StateEvent.Install) + } + ) } @Test fun postNdkDeliverPending() { clientObservable.postNdkDeliverPending() - clientObservable.addObserver { _, arg -> - assertTrue(arg is StateEvent.DeliverPending) - } + clientObservable.addObserver( + StateObserver { + assertTrue(it is StateEvent.DeliverPending) + } + ) } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ConfigurationTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ConfigurationTest.java index 760266f875..c28cb70890 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ConfigurationTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ConfigurationTest.java @@ -6,6 +6,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.bugsnag.android.internal.ImmutableConfig; + import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; @@ -39,51 +41,26 @@ public void testShouldNotify() { ImmutableConfig immutableConfig; immutableConfig = createConfigWithReleaseStages(config, null, "development"); - assertTrue(immutableConfig.shouldNotifyForReleaseStage()); + assertFalse(immutableConfig.shouldDiscardByReleaseStage()); // Should not notify if enabledReleaseStages is null immutableConfig = createConfigWithReleaseStages(config, Collections.emptySet(), "development"); - assertFalse(immutableConfig.shouldNotifyForReleaseStage()); + assertTrue(immutableConfig.shouldDiscardByReleaseStage()); // Shouldn't notify if enabledReleaseStages is set and releaseStage is null Set example = Collections.singleton("example"); immutableConfig = createConfigWithReleaseStages(config, example, null); - assertFalse(immutableConfig.shouldNotifyForReleaseStage()); + assertTrue(immutableConfig.shouldDiscardByReleaseStage()); // Shouldn't notify if releaseStage not in enabledReleaseStages Set stages = Collections.singleton("production"); immutableConfig = createConfigWithReleaseStages(config, stages, "not-production"); - assertFalse(immutableConfig.shouldNotifyForReleaseStage()); + assertTrue(immutableConfig.shouldDiscardByReleaseStage()); // Should notify if releaseStage in enabledReleaseStages immutableConfig = createConfigWithReleaseStages(config, stages, "production"); - assertTrue(immutableConfig.shouldNotifyForReleaseStage()); - } - - @Test - public void testShouldSendBreadcrumb() { - ImmutableConfig immutableConfig; - - // Should notify if enabledBreadcrumbTypes is null - config.setEnabledBreadcrumbTypes(null); - immutableConfig = BugsnagTestUtils.convert(config); - assertTrue(immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.MANUAL)); - - // Should not notify if enabledBreadcrumbTypes is empty - config.setEnabledBreadcrumbTypes(Collections.emptySet()); - immutableConfig = BugsnagTestUtils.convert(config); - assertFalse(immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.MANUAL)); - - // Should notify if present in enabled types - config.setEnabledBreadcrumbTypes(Collections.singleton(BreadcrumbType.MANUAL)); - immutableConfig = BugsnagTestUtils.convert(config); - assertTrue(immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.MANUAL)); - - // Should not notify if not present in enabled types - config.setEnabledBreadcrumbTypes(Collections.singleton(BreadcrumbType.ERROR)); - immutableConfig = BugsnagTestUtils.convert(config); - assertFalse(immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.MANUAL)); + assertFalse(immutableConfig.shouldDiscardByReleaseStage()); } private ImmutableConfig createConfigWithReleaseStages(Configuration config, diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt index 9f1a881341..9e1b65f449 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt @@ -1,6 +1,7 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig +import com.bugsnag.android.internal.StateObserver import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -44,9 +45,11 @@ internal class DeliveryDelegateTest { @Test fun generateUnhandledReport() { var msg: StateEvent.NotifyUnhandled? = null - deliveryDelegate.addObserver { _, arg -> - msg = arg as StateEvent.NotifyUnhandled - } + deliveryDelegate.addObserver( + StateObserver { + msg = it as StateEvent.NotifyUnhandled + } + ) deliveryDelegate.deliver(event) // verify message sent @@ -66,9 +69,11 @@ internal class DeliveryDelegateTest { event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger) var msg: StateEvent.NotifyHandled? = null - deliveryDelegate.addObserver { _, arg -> - msg = arg as StateEvent.NotifyHandled - } + deliveryDelegate.addObserver( + StateObserver { + msg = it as StateEvent.NotifyHandled + } + ) deliveryDelegate.deliver(event) // verify message sent @@ -88,9 +93,11 @@ internal class DeliveryDelegateTest { event.errors.clear() var msg: StateEvent.NotifyHandled? = null - deliveryDelegate.addObserver { _, arg -> - msg = arg as StateEvent.NotifyHandled - } + deliveryDelegate.addObserver( + StateObserver { + msg = it as StateEvent.NotifyHandled + } + ) deliveryDelegate.deliver(event) // verify no payload was sent for an Event with no errors @@ -104,7 +111,7 @@ internal class DeliveryDelegateTest { assertEquals(DeliveryStatus.DELIVERED, status) assertEquals("Sent 1 new event to Bugsnag", logger.msg) - val breadcrumb = requireNotNull(breadcrumbState.store.peek()) + val breadcrumb = requireNotNull(breadcrumbState.copy().first()) assertEquals(BreadcrumbType.ERROR, breadcrumb.type) assertEquals("java.lang.RuntimeException", breadcrumb.message) assertEquals("java.lang.RuntimeException", breadcrumb.metadata!!["errorClass"]) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt index 4a3dc45b36..f578bca911 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt @@ -3,6 +3,7 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateConfiguration import com.bugsnag.android.BugsnagTestUtils.generateEventPayload import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DiscardTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DiscardTest.kt new file mode 100644 index 0000000000..87f3a11c13 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DiscardTest.kt @@ -0,0 +1,135 @@ +package com.bugsnag.android + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.lang.RuntimeException + +/** + * Verifies the logic for discarding automatically captured errors/sessions/breadcrumbs. + */ +class DiscardTest { + + private lateinit var config: Configuration + + @Before + fun setUp() { + config = BugsnagTestUtils.generateConfiguration() + } + + @Test + fun testShouldDiscardErrorUsingThrowable() { + val exc = RuntimeException() + + // Should not discard if enabledReleaseStages is null + config.enabledReleaseStages = null + var cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError(exc)) + + // Should discard if outside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "dev" + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardError(exc)) + + // Should not discard if inside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "prod" + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError(exc)) + + // Should not discard if outside discard classes + config.discardClasses = setOf("UnwantedError") + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError(exc)) + + // Should discard if inside discard classes + config.discardClasses = setOf("java.lang.RuntimeException") + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardError(exc)) + } + + @Test + fun testShouldDiscardErrorUsingClz() { + // Should not discard if enabledReleaseStages is null + config.enabledReleaseStages = null + var cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError("MyError")) + + // Should discard if outside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "dev" + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardError("MyError")) + + // Should not discard if inside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "prod" + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError("MyError")) + + // Should not discard if outside discard classes + config.discardClasses = setOf("UnwantedError") + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardError("MyError")) + + // Should discard if inside discard classes + config.discardClasses = setOf("UnwantedError") + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardError("UnwantedError")) + } + + @Test + fun testShouldDiscardSession() { + // Should not discard if enabledReleaseStages is null + config.enabledReleaseStages = null + var cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardSession(false)) + + // Should discard if outside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "dev" + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardSession(false)) + + // Should not discard if inside enabledReleaseStages + config.enabledReleaseStages = setOf("prod") + config.releaseStage = "prod" + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardSession(false)) + + // Should not discard if autoTrack disabled and autoCapture == false + config.autoTrackSessions = false + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardSession(false)) + + // Should discard if autoTrack disabled and autoCapture == false + config.autoTrackSessions = false + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardSession(true)) + } + + @Test + fun testShouldDiscardBreadcrumb() { + // Should not discard if enabledBreadcrumbTypes is null + config.enabledBreadcrumbTypes = null + var cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardBreadcrumb(BreadcrumbType.MANUAL)) + + // Should discard if enabledBreadcrumbTypes is empty + config.enabledBreadcrumbTypes = emptySet() + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardBreadcrumb(BreadcrumbType.MANUAL)) + + // Should not discard if present in enabled types + config.enabledBreadcrumbTypes = setOf(BreadcrumbType.MANUAL) + cfg = BugsnagTestUtils.convert(config) + assertFalse(cfg.shouldDiscardBreadcrumb(BreadcrumbType.MANUAL)) + + // Should discard if not present in enabled types + config.enabledBreadcrumbTypes = setOf(BreadcrumbType.ERROR) + cfg = BugsnagTestUtils.convert(config) + assertTrue(cfg.shouldDiscardBreadcrumb(BreadcrumbType.MANUAL)) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EnabledErrorTypesTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EnabledErrorTypesTest.kt index ec1e81c3c0..b7b6d16d2e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EnabledErrorTypesTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EnabledErrorTypesTest.kt @@ -1,6 +1,7 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateConfiguration +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventErrorTypeTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventErrorTypeTest.kt index 799dee80df..4c39373886 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventErrorTypeTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventErrorTypeTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Assert.assertEquals import org.junit.Test import java.io.File diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFacadeTest.java index c6a70ea075..cf8ee75f6c 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFacadeTest.java @@ -6,6 +6,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.bugsnag.android.internal.ImmutableConfig; + import org.junit.Before; import org.junit.Test; diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt index 3a6abc2094..a06f233667 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt @@ -38,7 +38,7 @@ internal class EventSerializationTest { // threads included createEvent { - val stacktrace = Stacktrace.stacktraceFromJavaTrace(arrayOf(), emptySet(), NoopLogger) + val stacktrace = Stacktrace(arrayOf(), emptySet(), NoopLogger) it.threads.clear() it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, stacktrace, NoopLogger)) }, @@ -53,7 +53,7 @@ internal class EventSerializationTest { val crumb = Breadcrumb("hello world", BreadcrumbType.MANUAL, mutableMapOf(), Date(0), NoopLogger) it.breadcrumbs = listOf(crumb) - val stacktrace = Stacktrace.stacktraceFromJavaTrace(arrayOf(), emptySet(), NoopLogger) + val stacktrace = Stacktrace(arrayOf(), emptySet(), NoopLogger) val err = Error(ErrorInternal("WhoopsException", "Whoops", stacktrace), NoopLogger) it.errors.clear() it.errors.add(err) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStateTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStateTest.java index 9b35b664e1..6a90ac3557 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStateTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStateTest.java @@ -5,6 +5,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.bugsnag.android.internal.ImmutableConfig; + import org.junit.Before; import org.junit.Test; diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStoreMaxLimitTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStoreMaxLimitTest.kt index 672aeebc73..35c128520a 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStoreMaxLimitTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventStoreMaxLimitTest.kt @@ -2,6 +2,8 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateConfiguration import com.bugsnag.android.BugsnagTestUtils.generateEvent +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventTest.java index 8ab688b0e3..02db319b4e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventTest.java @@ -6,6 +6,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.bugsnag.android.internal.ImmutableConfig; + import org.junit.Before; import org.junit.Test; diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt index 1ba84aa619..461d026479 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import org.junit.After import org.junit.Assert.assertSame import org.junit.Assert.assertTrue @@ -9,6 +10,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnitRunner @@ -20,11 +22,15 @@ internal class ExceptionHandlerTest { @Mock lateinit var client: Client + @Mock + lateinit var cfg: ImmutableConfig + var originalHandler: Thread.UncaughtExceptionHandler? = null @Before fun setUp() { originalHandler = Thread.getDefaultUncaughtExceptionHandler() + `when`(client.config).thenReturn(cfg) } @After @@ -67,4 +73,19 @@ internal class ExceptionHandlerTest { exceptionHandler.uncaughtException(thread, RuntimeException("Whoops")) assertTrue(propagated) } + + @Test + fun uncaughtExceptionOutsideReleaseStages() { + val exceptionHandler = ExceptionHandler(client, NoopLogger) + val thread = Thread.currentThread() + val exc = RuntimeException("Whoops") + `when`(cfg.shouldDiscardError(exc)).thenReturn(true) + exceptionHandler.uncaughtException(thread, exc) + verify(client, times(0)).notifyUnhandledException( + eq(exc), + any(), + eq(SeverityReason.REASON_UNHANDLED_EXCEPTION), + eq(null) + ) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt index 7729df8c29..7eb173d81a 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt @@ -5,6 +5,8 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import com.bugsnag.android.BugsnagTestUtils.generateConfiguration +import com.bugsnag.android.internal.convertToImmutableConfig +import com.bugsnag.android.internal.sanitiseConfiguration import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -81,7 +83,7 @@ internal class ImmutableConfigTest { assertEquals(seed.maxPersistedEvents, maxPersistedEvents) assertEquals(seed.maxPersistedSessions, maxPersistedSessions) assertEquals(seed.persistUser, persistUser) - assertEquals(seed.enabledBreadcrumbTypes, BreadcrumbType.values().toSet()) + assertNull(seed.enabledBreadcrumbTypes) assertNotNull(persistenceDirectory) } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataConcurrentModificationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataConcurrentModificationTest.kt index cbe12752d2..2db90f9de3 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataConcurrentModificationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataConcurrentModificationTest.kt @@ -16,7 +16,7 @@ internal class MetadataConcurrentModificationTest { repeat(100) { count -> assertNotNull(metadata.toMap()) executor.execute { - metadata.store["$count"] = count + metadata.store["$count"] = mutableMapOf(Pair("$count", count)) } } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataStateTest.kt index 8f218a7219..d796380e64 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataStateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/MetadataStateTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.StateObserver import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before @@ -50,10 +51,12 @@ internal class MetadataStateTest { state.addMetadata("bar", "another", true) val data = mutableSetOf() - state.addObserver { _, arg -> - val msg = arg as StateEvent.AddMetadata - data.add(msg.section) - } + state.addObserver( + StateObserver { + val msg = it as StateEvent.AddMetadata + data.add(msg.section) + } + ) state.emitObservableEvent() assertEquals(setOf("foo", "bar"), data) @@ -72,11 +75,13 @@ internal class MetadataStateTest { val sections = mutableSetOf() val keys = mutableSetOf() - state.addObserver { _, arg -> - val msg = arg as StateEvent.AddMetadata - sections.add(msg.section) - keys.add(msg.key) - } + state.addObserver( + StateObserver { + val msg = it as StateEvent.AddMetadata + sections.add(msg.section) + keys.add(msg.key) + } + ) state.emitObservableEvent() assertEquals(setOf("foo"), sections) 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 8a63a710d8..b0e03023f6 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 @@ -3,6 +3,7 @@ package com.bugsnag.android import android.content.Context import com.bugsnag.android.BugsnagTestUtils.generateAppWithState import com.bugsnag.android.BugsnagTestUtils.generateDeviceWithState +import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/NullMetadataTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/NullMetadataTest.java index 498ec9ae6d..bf8e9d32c9 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/NullMetadataTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/NullMetadataTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import com.bugsnag.android.internal.ImmutableConfig; + import org.junit.Before; import org.junit.Test; @@ -45,7 +47,7 @@ public void testSecondErrorDefaultMetadata() { = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION); Event event = new Event(new RuntimeException(), config, reason, NoopLogger.INSTANCE); List projectPackages = Collections.emptyList(); - Stacktrace trace = Stacktrace.Companion.stacktraceFromJavaTrace(new StackTraceElement[]{}, + Stacktrace trace = new Stacktrace(new StackTraceElement[]{}, projectPackages, NoopLogger.INSTANCE); Error err = new Error(new ErrorInternal("RuntimeException", "Something broke", diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionStoreMaxLimitTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionStoreMaxLimitTest.kt index a6cabc894a..06df35228e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionStoreMaxLimitTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionStoreMaxLimitTest.kt @@ -2,6 +2,8 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateConfiguration import com.bugsnag.android.BugsnagTestUtils.generateSession +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerPauseResumeTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerPauseResumeTest.kt index f406b98dbe..e83491ae65 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerPauseResumeTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerPauseResumeTest.kt @@ -4,6 +4,7 @@ import android.app.ActivityManager import android.content.Context import com.bugsnag.android.BugsnagTestUtils.generateConfiguration import com.bugsnag.android.BugsnagTestUtils.generateDevice +import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals @@ -28,6 +29,9 @@ internal class SessionTrackerPauseResumeTest { @Mock lateinit var client: Client + @Mock + lateinit var cfg: ImmutableConfig + @Mock lateinit var appDataCollector: AppDataCollector @@ -51,6 +55,7 @@ internal class SessionTrackerPauseResumeTest { `when`(client.getNotifier()).thenReturn(Notifier()) `when`(client.getAppContext()).thenReturn(context) `when`(client.getAppDataCollector()).thenReturn(appDataCollector) + `when`(client.config).thenReturn(cfg) `when`(appDataCollector.generateApp()).thenReturn(app) `when`(client.getDeviceDataCollector()).thenReturn(deviceDataCollector) `when`(deviceDataCollector.generateDevice()).thenReturn(generateDevice()) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java index 2f17387968..e8f2506e90 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SessionTrackerTest.java @@ -7,6 +7,8 @@ import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; +import com.bugsnag.android.internal.ImmutableConfig; + import android.app.ActivityManager; import android.content.Context; @@ -33,6 +35,9 @@ public class SessionTrackerTest { @Mock Client client; + @Mock + ImmutableConfig cfg; + @Mock AppDataCollector appDataCollector; @@ -59,6 +64,7 @@ public void setUp() { when(client.getNotifier()).thenReturn(new Notifier()); when(client.getAppContext()).thenReturn(context); when(client.getAppDataCollector()).thenReturn(appDataCollector); + when(client.getConfig()).thenReturn(cfg); when(appDataCollector.generateApp()).thenReturn(app); when(client.getDeviceDataCollector()).thenReturn(deviceDataCollector); when(deviceDataCollector.generateDevice()).thenReturn(generateDevice()); diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceSerializationTest.kt index 22b2a10046..7a8ef7416e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceSerializationTest.kt @@ -19,7 +19,7 @@ internal class StacktraceSerializationTest { "stacktrace", // empty stacktrace element ctor - Stacktrace.stacktraceFromJavaTrace(arrayOf(), emptySet(), NoopLogger), + Stacktrace(arrayOf(), emptySet(), NoopLogger), // empty custom frames ctor Stacktrace(listOf(frame)), @@ -37,13 +37,13 @@ internal class StacktraceSerializationTest { } private fun basic() = - Stacktrace.stacktraceFromJavaTrace( + Stacktrace( RuntimeException("Whoops").stackTrace.sliceArray(IntRange(0, 1)), emptySet(), NoopLogger ) - private fun inProject() = Stacktrace.stacktraceFromJavaTrace( + private fun inProject() = Stacktrace( RuntimeException("Whoops").stackTrace.sliceArray(IntRange(0, 1)), setOf("com.bugsnag.android"), NoopLogger @@ -53,7 +53,7 @@ internal class StacktraceSerializationTest { val elements = (0..999).map { StackTraceElement("SomeClass", "someMethod", "someFile", it) } - return Stacktrace.stacktraceFromJavaTrace(elements.toTypedArray(), emptyList(), NoopLogger) + return Stacktrace(elements.toTypedArray(), emptyList(), NoopLogger) } private fun trimStacktraceListCtor(): Stacktrace { diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceTest.kt index d219f60c6f..53049a3d55 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/StacktraceTest.kt @@ -6,10 +6,9 @@ import org.junit.Test class StacktraceTest { @Test - fun stackframeLimits() { - val stackList = mutableListOf() - for (i in 1..300) { - stackList.add(Stackframe("A", "B", i, true)) + fun stackframeListTrimmed() { + val stackList = (1..300).map { index -> + Stackframe("A", "B", index, true) } val stacktrace = Stacktrace(stackList) // Confirm the length of the stackList @@ -18,4 +17,18 @@ class StacktraceTest { assertEquals(1, stacktrace.trace.first().lineNumber) assertEquals(200, stacktrace.trace.last().lineNumber) } + + @Test + fun stacktraceElementArrayTrimmed() { + val trace = (1..300).map { index -> + StackTraceElement("A", "B", "C", index) + }.toTypedArray() + + val stacktrace = Stacktrace(trace, emptyList(), NoopLogger) + // Confirm the length of the stackList + assertEquals(300, trace.size) + assertEquals(200, stacktrace.trace.size) + assertEquals(1, stacktrace.trace.first().lineNumber) + assertEquals(200, stacktrace.trace.last().lineNumber) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt index 21fd281921..d9d249589b 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.bugsnag.android.SystemBroadcastReceiver.shortenActionNameIfNeeded +import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt index 0b287d02a1..c60b0bf50d 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt @@ -24,7 +24,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, true, - Stacktrace.stacktraceFromJavaTrace( + Stacktrace( stacktrace, emptySet(), NoopLogger @@ -43,7 +43,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, false, - Stacktrace.stacktraceFromJavaTrace( + Stacktrace( stacktrace1, emptySet(), NoopLogger @@ -66,7 +66,7 @@ internal class ThreadSerializationTest { StackTraceElement("Runner", "runFunc", "Runner.java", 14), StackTraceElement("App", "launch", "App.java", 70) ) - val trace = Stacktrace.stacktraceFromJavaTrace( + val trace = Stacktrace( stacktrace, emptyList(), NoopLogger @@ -88,7 +88,7 @@ internal class ThreadSerializationTest { StackTraceElement("Runner", "runFunc", "Runner.java", 14), StackTraceElement("App", "launch", "App.java", 70) ) - val trace = Stacktrace.stacktraceFromJavaTrace( + val trace = Stacktrace( stacktrace, emptyList(), NoopLogger diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/UserStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/UserStateTest.kt index f983b99dc9..27bf0fde70 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/UserStateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/UserStateTest.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.StateObserver import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -23,9 +24,11 @@ internal class UserStateTest { @Test fun setUser() { val msgs = mutableListOf() - state.addObserver { _, arg -> - msgs.add(arg as StateEvent) - } + state.addObserver( + StateObserver { + msgs.add(it) + } + ) state.user = User("99", "tc@example.com", "Tobias") diff --git a/bugsnag-benchmarks/benchmark-proguard-rules.pro b/bugsnag-benchmarks/benchmark-proguard-rules.pro new file mode 100644 index 0000000000..e4061d2224 --- /dev/null +++ b/bugsnag-benchmarks/benchmark-proguard-rules.pro @@ -0,0 +1,37 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-dontobfuscate + +-ignorewarnings + +-keepattributes *Annotation* + +-dontnote junit.framework.** +-dontnote junit.runner.** + +-dontwarn androidx.test.** +-dontwarn org.junit.** +-dontwarn org.hamcrest.** +-dontwarn com.squareup.javawriter.JavaWriter + +-keepclasseswithmembers @org.junit.runner.RunWith public class * \ No newline at end of file diff --git a/bugsnag-benchmarks/build.gradle b/bugsnag-benchmarks/build.gradle new file mode 100644 index 0000000000..bc1c73ec2a --- /dev/null +++ b/bugsnag-benchmarks/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "com.android.library" + id "androidx.benchmark" + id "kotlin-android" +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner" + + // suppress warnings that the CPU clock is not locked, and allow running on an emulator. + // it's not possible to lock the CPU clock without rooting a device, which isn't possible + // on our CI setup currently. + testInstrumentationRunnerArgument "androidx.benchmark.suppressErrors", "EMULATOR,LOW_BATTERY,UNLOCKED" + } + + testBuildType = "release" + buildTypes { + debug { + // Since debuggable can"t be modified by gradle for library modules, + // it must be done in a manifest - see src/androidTest/AndroidManifest.xml + minifyEnabled true + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" + } + release { + isDefault = true + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72" + androidTestImplementation "androidx.test:runner:1.3.0" + androidTestImplementation "androidx.test.ext:junit:1.1.2" + androidTestImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.benchmark:benchmark-junit4:1.0.0" + + // Add your dependencies here. Note that you cannot benchmark code + // in an app module this way - you will need to move any code you + // want to benchmark to a library module: + // https://developer.android.com/studio/projects/android-library#Convert + implementation(project(":bugsnag-android")) +} diff --git a/bugsnag-benchmarks/src/androidTest/AndroidManifest.xml b/bugsnag-benchmarks/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..da7b9f89d3 --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt new file mode 100644 index 0000000000..e8ff256ede --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/ClientHooks.kt @@ -0,0 +1,43 @@ +package com.bugsnag.android + +import android.content.Context +import java.util.Date + +internal fun generateClient(ctx: Context, cfg: Configuration = generateConfig()): Client { + return Client(ctx, cfg) +} + +fun generateConfig(): Configuration { + return Configuration("your-api-key").apply { + // logging is disabled by default in production apps + logger = object : Logger {} + + // avoid making network requests in performance tests + delivery = object : Delivery { + override fun deliver( + payload: Session, + deliveryParams: DeliveryParams + ) = DeliveryStatus.DELIVERED + + override fun deliver( + payload: EventPayload, + deliveryParams: DeliveryParams + ) = DeliveryStatus.DELIVERED + } + } +} + +internal fun generateSeverityReason() = + SeverityReason.newInstance(SeverityReason.REASON_UNHANDLED_EXCEPTION) + + +internal fun generateSession(): Session { + return Session( + "test", + Date(), + null, + false, + Notifier(), + object : Logger {} + ) +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/EventHooks.java b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/EventHooks.java new file mode 100644 index 0000000000..e5d971cd64 --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/EventHooks.java @@ -0,0 +1,71 @@ +package com.bugsnag.android; + +import static com.bugsnag.android.ClientHooksKt.generateConfig; + +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfigKt; + +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; + +public class EventHooks { + + static HashMap runtimeVersions = new HashMap<>(); + + static { + runtimeVersions.put("osBuild", "bulldog"); + runtimeVersions.put("androidApiLevel", 24); + } + + public static EventPayload generateEvent() { + Throwable exc = new RuntimeException(); + ImmutableConfig cfg = convert(generateConfig()); + Event event = new Event( + exc, + cfg, + SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), + NoopLogger.INSTANCE + ); + event.setApp(generateAppWithState(cfg)); + event.setDevice(generateDeviceWithState()); + return new EventPayload("api-key", event, null, new Notifier(), cfg); + } + + static ImmutableConfig convert(Configuration config) { + try { + config.setPersistenceDirectory(File.createTempFile("tmp", null)); + } catch (IOException ignored) { + // swallow + } + return ImmutableConfigKt.convertToImmutableConfig(config, null); + } + + static AppWithState generateAppWithState(ImmutableConfig cfg) { + return new AppWithState(cfg, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } + + static DeviceWithState generateDeviceWithState() { + DeviceBuildInfo buildInfo = DeviceBuildInfo.Companion.defaultInfo(); + return new DeviceWithState(buildInfo, + null, + null, + null, + 109230923452L, + runtimeVersions, + 22234423124L, + 92340255592L, + "portrait", + new Date(0)); + } +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/ClientDataBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/ClientDataBenchmarkTest.kt new file mode 100644 index 0000000000..c4a5f70693 --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/ClientDataBenchmarkTest.kt @@ -0,0 +1,53 @@ +package com.bugsnag.android.benchmark + +import android.app.Application +import android.content.Context +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bugsnag.android.Client +import com.bugsnag.android.generateClient +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmarks the performance of Bugsnag APIs for setting the user/context. + */ +@RunWith(AndroidJUnit4::class) +class ClientDataBenchmarkTest { + + lateinit var client: Client + lateinit var ctx: Context + + @Before + fun setUp() { + ctx = ApplicationProvider.getApplicationContext() + client = generateClient(ctx) + } + + @get:Rule + val benchmarkRule = BenchmarkRule() + + /** + * Alters the user information + */ + @Test + fun setUser() { + benchmarkRule.measureRepeated { + client.setUser("123", "jane@fake.com", "Jane Eyre") + } + } + + /** + * Sets the error context + */ + @Test + fun setContext() { + benchmarkRule.measureRepeated { + client.context = "Something went wrong" + } + } +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt new file mode 100644 index 0000000000..2778187aca --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt @@ -0,0 +1,184 @@ +package com.bugsnag.android.benchmark + +import android.app.Application +import android.content.Context +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bugsnag.android.BreadcrumbType +import com.bugsnag.android.Client +import com.bugsnag.android.Configuration +import com.bugsnag.android.generateClient +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmarks the performance of Bugsnag APIs which are typically on the critical path. + * For example, calling notify(), leaving breadcrumbs, and altering metadata is likely to happen + * many times over an application's lifecycle. This means special care should be taken to ensure + * that these functions are optimized. + */ +@RunWith(AndroidJUnit4::class) +class CriticalPathBenchmarkTest { + + lateinit var client: Client + lateinit var ctx: Context + + @Before + fun setUp() { + ctx = ApplicationProvider.getApplicationContext() + client = generateClient(ctx) + } + + @get:Rule + val benchmarkRule = BenchmarkRule() + + /** + * Construct a configuration object from an API key + */ + @Test + fun configConstructor() { + benchmarkRule.measureRepeated { + Configuration("your-api-key") + } + } + + /** + * Construct a configuration object from the AndroidManifest + */ + @Test + fun configManifestLoad() { + benchmarkRule.measureRepeated { + Configuration.load(ctx) + } + } + + /** + * Calls Bugsnag.notify() with an exception. This is not an ideal benchmark + * as it's a fairly complex operation that involves I/O, but at least gives a general feel + * for how the API is performing. + */ + @Test + fun clientNotify() { + val exc = benchmarkRule.scope.runWithTimingDisabled { + RuntimeException("Whoops") + } + benchmarkRule.measureRepeated { + client.notify(exc) + } + } + + /** + * Leave a simple breadcrumb on the Client + */ + @Test + fun leaveSimpleBreadcrumb() { + benchmarkRule.measureRepeated { + client.leaveBreadcrumb("Hello world") + } + } + + /** + * Leave a simple breadcrumb with metadata on the Client + */ + @Test + fun leaveComplexBreadcrumb() { + val data = benchmarkRule.scope.runWithTimingDisabled { + mapOf(Pair("isLaunching", true)) + } + benchmarkRule.measureRepeated { + client.leaveBreadcrumb("Hello world", data, BreadcrumbType.NAVIGATION) + } + } + + /** + * Make a copy of breadcrumbs on the Client (required when generating events) + */ + @Test + fun copyBreadcrumbs() { + repeat(201) { count -> + client.leaveBreadcrumb("Hello world $count") + } + benchmarkRule.measureRepeated { + client.breadcrumbs + } + } + + /** + * Add a single value to the Client metadata + */ + @Test + fun addSingleMetadataValue() { + benchmarkRule.measureRepeated { + client.addMetadata("custom", "mykey", "myvalue") + } + } + + /** + * Add a single value to the Client metadata + */ + @Test + fun addMetadataSection() { + val data = benchmarkRule.scope.runWithTimingDisabled { + mapOf(Pair("mykey", "myvalue")) + } + benchmarkRule.measureRepeated { + client.addMetadata("custom", data) + } + } + + /** + * Get a single value from the Client metadata + */ + @Test + fun getSingleMetadataValue() { + benchmarkRule.scope.runWithTimingDisabled { + client.addMetadata("custom", "mykey", "myvalue") + } + benchmarkRule.measureRepeated { + client.getMetadata("custom", "mykey") + } + } + + /** + * Get a single value from the Client metadata + */ + @Test + fun getMetadataSection() { + benchmarkRule.scope.runWithTimingDisabled { + client.addMetadata("custom", "mykey", "myvalue") + } + benchmarkRule.measureRepeated { + client.getMetadata("custom") + } + } + + /** + * Clear a single value from the Client metadata + */ + @Test + fun clearSingleMetadataValue() { + benchmarkRule.scope.runWithTimingDisabled { + client.addMetadata("custom", "mykey", "myvalue") + } + benchmarkRule.measureRepeated { + client.clearMetadata("custom", "mykey") + } + } + + /** + * Clear a single value from the Client metadata + */ + @Test + fun clearMetadataSection() { + benchmarkRule.scope.runWithTimingDisabled { + client.addMetadata("custom", "mykey", "myvalue") + } + benchmarkRule.measureRepeated { + client.clearMetadata("custom") + } + } +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/EventBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/EventBenchmarkTest.kt new file mode 100644 index 0000000000..af4a47a91c --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/EventBenchmarkTest.kt @@ -0,0 +1,49 @@ +package com.bugsnag.android.benchmark + +import android.app.Application +import android.content.Context +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bugsnag.android.Client +import com.bugsnag.android.NativeInterface +import com.bugsnag.android.generateClient +import com.bugsnag.android.generateSeverityReason +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmarks the performance of Bugsnag APIs for setting the user/context. + */ +@RunWith(AndroidJUnit4::class) +class EventBenchmarkTest { + + lateinit var client: Client + lateinit var ctx: Context + + @Before + fun setUp() { + ctx = ApplicationProvider.getApplicationContext() + client = generateClient(ctx) + } + + @get:Rule + val benchmarkRule = BenchmarkRule() + + /** + * Constructs an event, which captures all the necessary information for an error report. + */ + @Test + fun createEvent() { + benchmarkRule.measureRepeated { + NativeInterface.createEvent( + RuntimeException("Whoops"), + client, + generateSeverityReason() + ) + } + } +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/JsonSerializationBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/JsonSerializationBenchmarkTest.kt new file mode 100644 index 0000000000..77a1855ad4 --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/JsonSerializationBenchmarkTest.kt @@ -0,0 +1,57 @@ +package com.bugsnag.android.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bugsnag.android.EventHooks +import com.bugsnag.android.JsonStream +import com.bugsnag.android.generateSession +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.ByteArrayOutputStream +import java.io.PrintWriter + +/** + * Benchmarks the performance of serializing error/session payloads to JSON. + */ +@RunWith(AndroidJUnit4::class) +class JsonSerializationBenchmarkTest { + + @get:Rule + val benchmarkRule = BenchmarkRule() + + /** + * Serializes an event payload to JSON + */ + @Test + fun serializeEventPayload() { + val payload = EventHooks.generateEvent() + + benchmarkRule.measureRepeated { + val stream = benchmarkRule.scope.runWithTimingDisabled { + JsonStream(PrintWriter(ByteArrayOutputStream()).buffered()) + } + stream.use { + payload?.toStream(stream) + } + } + } + + /** + * Serializes a session payload to JSON + */ + @Test + fun serializeSessionPayload() { + val payload = generateSession() + + benchmarkRule.measureRepeated { + val stream = benchmarkRule.scope.runWithTimingDisabled { + JsonStream(PrintWriter(ByteArrayOutputStream()).buffered()) + } + stream.use { + payload.toStream(stream) + } + } + } +} diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/SessionBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/SessionBenchmarkTest.kt new file mode 100644 index 0000000000..13d205b72f --- /dev/null +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/SessionBenchmarkTest.kt @@ -0,0 +1,70 @@ +package com.bugsnag.android.benchmark + +import android.app.Application +import android.content.Context +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.bugsnag.android.Client +import com.bugsnag.android.generateClient +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmarks the performance of Bugsnag session tracking APIs. + */ +@RunWith(AndroidJUnit4::class) +class SessionBenchmarkTest { + + lateinit var client: Client + lateinit var ctx: Context + + @Before + fun setUp() { + ctx = ApplicationProvider.getApplicationContext() + client = generateClient(ctx) + } + + @get:Rule + val benchmarkRule = BenchmarkRule() + + /** + * Starts a new session + */ + @Test + fun startSession() { + benchmarkRule.measureRepeated { + client.startSession() + } + } + + /** + * Pauses a session + */ + @Test + fun pauseSession() { + benchmarkRule.scope.runWithTimingDisabled { + client.startSession() + } + benchmarkRule.measureRepeated { + client.pauseSession() + } + } + + /** + * Resumes a session + */ + @Test + fun resumeSession() { + benchmarkRule.scope.runWithTimingDisabled { + client.startSession() + client.pauseSession() + } + benchmarkRule.measureRepeated { + client.resumeSession() + } + } +} diff --git a/bugsnag-benchmarks/src/main/AndroidManifest.xml b/bugsnag-benchmarks/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6c0f5bcb68 --- /dev/null +++ b/bugsnag-benchmarks/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/bugsnag-plugin-android-anr/build.gradle b/bugsnag-plugin-android-anr/build.gradle index 401ac15c33..169d3dd223 100644 --- a/bugsnag-plugin-android-anr/build.gradle +++ b/bugsnag-plugin-android-anr/build.gradle @@ -11,3 +11,5 @@ apply plugin: "com.android.library" dependencies { api(project(":bugsnag-android-core")) } + +apply from: "../gradle/kotlin.gradle" diff --git a/bugsnag-plugin-android-anr/detekt-baseline.xml b/bugsnag-plugin-android-anr/detekt-baseline.xml index 05308663d1..3b4b88835a 100644 --- a/bugsnag-plugin-android-anr/detekt-baseline.xml +++ b/bugsnag-plugin-android-anr/detekt-baseline.xml @@ -1,5 +1,9 @@ - + + SwallowedException:AnrDetailsCollector.kt$AnrDetailsCollector$catch (exc: RuntimeException) { null } + SwallowedException:AnrPlugin.kt$AnrPlugin$catch (exc: Throwable) { null } + UnusedPrivateMember:AnrPlugin.kt$AnrPlugin$ private fun notifyAnrDetected(nativeTrace: List<NativeStackframe>) + diff --git a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt index 2091b2ba49..ab45e5907d 100644 --- a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt +++ b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt @@ -7,8 +7,11 @@ import java.util.concurrent.atomic.AtomicBoolean internal class AnrPlugin : Plugin { internal companion object { + private const val LOAD_ERR_MSG = "Native library could not be linked. Bugsnag will " + "not report ANRs. See https://docs.bugsnag.com/platforms/android/anr-link-errors" + private const val ANR_ERROR_CLASS = "ANR" + private const val ANR_ERROR_MSG = "Application did not respond to UI input" internal fun doesJavaTraceLeadToNativeTrace( javaTrace: Array @@ -45,17 +48,24 @@ internal class AnrPlugin : Plugin { performOneTimeSetup(client) } if (libraryLoader.isLoaded) { - Handler(Looper.getMainLooper()).post( - Runnable { - enableAnrReporting() - client.logger.i("Initialised ANR Plugin") + val mainLooper = Looper.getMainLooper() + if (Looper.myLooper() == mainLooper) { + initNativePlugin() + } else { + Handler(mainLooper).postAtFrontOfQueue { + initNativePlugin() } - ) + } } else { client.logger.e(LOAD_ERR_MSG) } } + private fun initNativePlugin() { + enableAnrReporting() + client.logger.i("Initialised ANR Plugin") + } + private fun performOneTimeSetup(client: Client) { libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { val error = it.errors[0] @@ -89,6 +99,9 @@ internal class AnrPlugin : Plugin { */ private fun notifyAnrDetected(nativeTrace: List) { try { + if (client.immutableConfig.shouldDiscardError(ANR_ERROR_CLASS)) { + return + } // generate a full report as soon as possible, then wait for extra process error info val stackTrace = Looper.getMainLooper().thread.stackTrace val hasNativeComponent = doesJavaTraceLeadToNativeTrace(stackTrace) @@ -101,8 +114,8 @@ internal class AnrPlugin : Plugin { SeverityReason.newInstance(SeverityReason.REASON_ANR) ) val err = event.errors[0] - err.errorClass = "ANR" - err.errorMessage = "Application did not respond to UI input" + err.errorClass = ANR_ERROR_CLASS + err.errorMessage = ANR_ERROR_MSG // append native stackframes to error/thread stacktrace if (hasNativeComponent) { diff --git a/bugsnag-plugin-android-anr/src/main/jni/anr_handler.c b/bugsnag-plugin-android-anr/src/main/jni/anr_handler.c index fe3e8ee0ec..572660456e 100644 --- a/bugsnag-plugin-android-anr/src/main/jni/anr_handler.c +++ b/bugsnag-plugin-android-anr/src/main/jni/anr_handler.c @@ -22,8 +22,8 @@ static bool installed = false; static pthread_t watchdog_thread; static bool should_wait_for_semaphore = false; -static sem_t anr_reporting_semaphore; -static volatile bool should_report_anr_flag = false; +static sem_t watchdog_thread_semaphore; +static volatile bool watchdog_thread_triggered = false; static JavaVM *bsg_jvm = NULL; static jmethodID mthd_notify_anr_detected = NULL; @@ -230,14 +230,14 @@ static inline void unblock_sigquit() { static inline void trigger_sigquit_watchdog_thread() { // Set the trigger flag for the fallback spin-lock in // sigquit_watchdog_thread_main() - should_report_anr_flag = true; + watchdog_thread_triggered = true; if (should_wait_for_semaphore) { // Although sem_post() is not officially marked as async-safe, the Android // implementation simply does an atomic compare-and-exchange when there is // only one thread waiting (which is the case here). // https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/semaphore.cpp;l=289?q=sem_post&ss=android - if (sem_post(&anr_reporting_semaphore) != 0) { + if (sem_post(&watchdog_thread_semaphore) != 0) { // The only possible failure from sem_post is EOVERFLOW, which won't // happen in this code. But just to be thorough... BUGSNAG_LOG("Could not unlock Bugsnag sigquit handler semaphore"); @@ -245,35 +245,35 @@ static inline void trigger_sigquit_watchdog_thread() { } } -static void *sigquit_watchdog_thread_main(__unused void *_) { +static void watchdog_wait_for_trigger() { static const useconds_t delay_100ms = 100000; - while (enabled) { - // Unblock SIGQUIT so that handle_sigquit() will be called. - unblock_sigquit(); - - // Wait until our SIGQUIT handler is ready for us to start. - // Use sem_wait() if possible, falling back to polling. - should_report_anr_flag = false; - if (!should_wait_for_semaphore || sem_wait(&anr_reporting_semaphore) != 0) { - while (!should_report_anr_flag) { - usleep(delay_100ms); - } + // Use sem_wait() if possible, falling back to polling. + watchdog_thread_triggered = false; + if (!should_wait_for_semaphore || sem_wait(&watchdog_thread_semaphore) != 0) { + while (!watchdog_thread_triggered) { + usleep(delay_100ms); } + } +} - if (!enabled) { - // This happens if bsg_handler_uninstall_anr() woke us. - break; - } +_Noreturn static void *sigquit_watchdog_thread_main(__unused void *_) { + static const useconds_t delay_100ms = 100000; + + for (;;) { + watchdog_wait_for_trigger(); // Trigger Google ANR processing (occurs on a different thread). bsg_google_anr_call(); - // Do our ANR processing. - notify_anr_detected(); - } + if (enabled) { + // Do our ANR processing. + notify_anr_detected(); + } - return NULL; + // Unblock SIGQUIT again so that handle_sigquit() will run again. + unblock_sigquit(); + } } static void handle_sigquit(__unused int signum, siginfo_t *info, @@ -301,11 +301,11 @@ static void install_signal_handler() { // We can still report to Bugsnag, so continue. } - if (sem_init(&anr_reporting_semaphore, 0, 0) == 0) { + if (sem_init(&watchdog_thread_semaphore, 0, 0) == 0) { should_wait_for_semaphore = true; } else { BUGSNAG_LOG("Failed to init semaphore"); - // We can still poll should_report_anr_flag, so continue. + // We can still poll watchdog_thread_triggered, so continue. } // Start the watchdog thread sigquit_watchdog_thread_main(). @@ -350,8 +350,6 @@ bool bsg_handler_install_anr(JNIEnv *env, jobject plugin) { void bsg_handler_uninstall_anr() { pthread_mutex_lock(&bsg_anr_handler_config); enabled = false; - // Trigger sigquit_watchdog_thread_main() so that it can exit. - trigger_sigquit_watchdog_thread(); pthread_mutex_unlock(&bsg_anr_handler_config); } diff --git a/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/BugsnagTestUtils.java b/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/BugsnagTestUtils.java index 7f1eebcad9..3d87326412 100644 --- a/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/BugsnagTestUtils.java +++ b/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/BugsnagTestUtils.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfigKt; + import org.jetbrains.annotations.NotNull; import java.io.File; diff --git a/bugsnag-plugin-android-ndk/build.gradle b/bugsnag-plugin-android-ndk/build.gradle index 401ac15c33..169d3dd223 100644 --- a/bugsnag-plugin-android-ndk/build.gradle +++ b/bugsnag-plugin-android-ndk/build.gradle @@ -11,3 +11,5 @@ apply plugin: "com.android.library" dependencies { api(project(":bugsnag-android-core")) } + +apply from: "../gradle/kotlin.gradle" diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 08d0891aa8..ab89d21317 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -2,9 +2,9 @@ - ComplexMethod:NativeBridge.kt$NativeBridge$override fun update(observable: Observable, arg: Any?) + ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, appVersion: String, buildUuid: String, releaseStage: String ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() - TooManyFunctions:NativeBridge.kt$NativeBridge : Observer + TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver 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 918ddacc54..88846ea784 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 @@ -16,12 +16,14 @@ internal class NdkPlugin : Plugin { private external fun enableCrashReporting() private external fun disableCrashReporting() + private external fun getBinaryArch(): String + private var nativeBridge: NativeBridge? = null private var client: Client? = null private fun initNativeBridge(client: Client): NativeBridge { val nativeBridge = NativeBridge() - client.registerObserver(nativeBridge) + client.addObserver(nativeBridge) client.setupNdkPlugin() return nativeBridge } @@ -46,6 +48,7 @@ internal class NdkPlugin : Plugin { true } if (libraryLoader.isLoaded) { + client.setBinaryArch(getBinaryArch()) nativeBridge = initNativeBridge(client) } else { client.logger.e(LOAD_ERR_MSG) @@ -56,7 +59,7 @@ internal class NdkPlugin : Plugin { if (libraryLoader.isLoaded) { disableCrashReporting() nativeBridge?.let { bridge -> - client?.unregisterObserver(bridge) + client?.removeObserver(bridge) } } } 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 1e908c5386..7f1e82d152 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 @@ -17,10 +17,9 @@ import com.bugsnag.android.StateEvent.UpdateContext import com.bugsnag.android.StateEvent.UpdateInForeground import com.bugsnag.android.StateEvent.UpdateOrientation import com.bugsnag.android.StateEvent.UpdateUser +import com.bugsnag.android.internal.StateObserver import java.io.File import java.nio.charset.Charset -import java.util.Observable -import java.util.Observer import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock @@ -28,7 +27,7 @@ import java.util.concurrent.locks.ReentrantLock /** * Observes changes in the Bugsnag environment, propagating them to the native layer */ -class NativeBridge : Observer { +class NativeBridge : StateObserver { private val lock = ReentrantLock() private val installed = AtomicBoolean(false) @@ -95,47 +94,47 @@ class NativeBridge : Observer { } } - override fun update(observable: Observable, arg: Any?) { - if (isInvalidMessage(arg)) return + override fun onStateChange(event: StateEvent) { + if (isInvalidMessage(event)) return - when (val msg = arg as StateEvent) { - is Install -> handleInstallMessage(msg) + when (event) { + is Install -> handleInstallMessage(event) DeliverPending -> deliverPendingReports() - is AddMetadata -> handleAddMetadata(msg) - is ClearMetadataSection -> clearMetadataTab(makeSafe(msg.section)) + is AddMetadata -> handleAddMetadata(event) + is ClearMetadataSection -> clearMetadataTab(makeSafe(event.section)) is ClearMetadataValue -> removeMetadata( - makeSafe(msg.section), - makeSafe(msg.key ?: "") + makeSafe(event.section), + makeSafe(event.key ?: "") ) is AddBreadcrumb -> addBreadcrumb( - makeSafe(msg.message), - makeSafe(msg.type.toString()), - makeSafe(msg.timestamp), - msg.metadata + makeSafe(event.message), + makeSafe(event.type.toString()), + makeSafe(event.timestamp), + event.metadata ) NotifyHandled -> addHandledEvent() NotifyUnhandled -> addUnhandledEvent() PauseSession -> pausedSession() is StartSession -> startedSession( - makeSafe(msg.id), - makeSafe(msg.startedAt), - msg.handledCount, - msg.unhandledCount + makeSafe(event.id), + makeSafe(event.startedAt), + event.handledCount, + event.unhandledCount ) - is UpdateContext -> updateContext(makeSafe(msg.context ?: "")) + is UpdateContext -> updateContext(makeSafe(event.context ?: "")) is UpdateInForeground -> updateInForeground( - msg.inForeground, - makeSafe(msg.contextActivity ?: "") + event.inForeground, + makeSafe(event.contextActivity ?: "") ) - is StateEvent.UpdateLastRunInfo -> updateLastRunInfo(msg.consecutiveLaunchCrashes) - is StateEvent.UpdateIsLaunching -> updateIsLaunching(msg.isLaunching) - is UpdateOrientation -> updateOrientation(msg.orientation ?: "") + is StateEvent.UpdateLastRunInfo -> updateLastRunInfo(event.consecutiveLaunchCrashes) + is StateEvent.UpdateIsLaunching -> updateIsLaunching(event.isLaunching) + is UpdateOrientation -> updateOrientation(event.orientation ?: "") is UpdateUser -> { - updateUserId(makeSafe(msg.user.id ?: "")) - updateUserName(makeSafe(msg.user.name ?: "")) - updateUserEmail(makeSafe(msg.user.email ?: "")) + updateUserId(makeSafe(event.user.id ?: "")) + updateUserName(makeSafe(event.user.name ?: "")) + updateUserEmail(makeSafe(event.user.email ?: "")) } - is StateEvent.UpdateMemoryTrimEvent -> updateLowMemory(msg.isLowMemory) + is StateEvent.UpdateMemoryTrimEvent -> updateLowMemory(event.isLowMemory) } } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c index e36aac8404..c4fdcf371d 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c @@ -14,8 +14,6 @@ static JNIEnv *bsg_global_jni_env = NULL; -void bugsnag_set_binary_arch(JNIEnv *env); - void bugsnag_start(JNIEnv *env) { bsg_global_jni_env = env; } void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, @@ -188,9 +186,6 @@ void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, jname = bsg_byte_ary_from_string(env, name); jmessage = bsg_byte_ary_from_string(env, message); - // set application's binary arch - bugsnag_set_binary_arch(env); - bsg_safe_call_static_void_method(env, interface_class, notify_method, jname, jmessage, jseverity, trace); @@ -213,39 +208,6 @@ void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, bsg_safe_delete_local_ref(env, jseverity); } -void bugsnag_set_binary_arch(JNIEnv *env) { - jclass interface_class = NULL; - jmethodID set_arch_method = NULL; - jstring arch = NULL; - - // lookup com/bugsnag/android/NativeInterface - interface_class = - bsg_safe_find_class(env, "com/bugsnag/android/NativeInterface"); - if (interface_class == NULL) { - goto exit; - } - - // lookup NativeInterface.setBinaryArch() - set_arch_method = bsg_safe_get_static_method_id( - env, interface_class, "setBinaryArch", "(Ljava/lang/String;)V"); - if (set_arch_method == NULL) { - goto exit; - } - - // call NativeInterface.setBinaryArch() - arch = bsg_safe_new_string_utf(env, bsg_binary_arch()); - if (arch != NULL) { - bsg_safe_call_static_void_method(env, interface_class, set_arch_method, - arch); - } - - goto exit; - -exit: - bsg_safe_delete_local_ref(env, arch); - bsg_safe_delete_local_ref(env, interface_class); -} - void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, const char *name) { // lookup com/bugsnag/android/NativeInterface 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 92adc53384..9b85853764 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -80,6 +80,22 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_NdkPlugin_disableCrashReporting( bsg_handler_uninstall_cpp(); } +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_NdkPlugin_getBinaryArch(JNIEnv *env, jobject _this) { +#if defined(__i386__) + const char *binary_arch = "x86"; +#elif defined(__x86_64__) + const char *binary_arch = "x86_64"; +#elif defined(__arm__) + const char *binary_arch = "arm32"; +#elif defined(__aarch64__) + const char *binary_arch = "arm64"; +#else + const char *binary_arch = "unknown"; +#endif + return bsg_safe_new_string_utf(env, binary_arch); +} + JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_enableCrashReporting(JNIEnv *env, jobject _this) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index e4bd39d1bb..bcef18c1a9 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -405,20 +405,6 @@ void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, bsg_safe_delete_local_ref(env, keylist); } -char *bsg_binary_arch() { -#if defined(__i386__) - return "x86"; -#elif defined(__x86_64__) - return "x86_64"; -#elif defined(__arm__) - return "arm32"; -#elif defined(__aarch64__) - return "arm64"; -#else - return "unknown"; -#endif -} - void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, bugsnag_event *event) { jobject data = bsg_safe_call_static_object_method( @@ -427,9 +413,9 @@ void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, return; } - bsg_strncpy_safe(event->app.binary_arch, bsg_binary_arch(), - sizeof(event->app.binary_arch)); - + bsg_copy_map_value_string(env, jni_cache, data, "binaryArch", + event->app.binary_arch, + sizeof(event->app.binary_arch)); bsg_copy_map_value_string(env, jni_cache, data, "buildUUID", event->app.build_uuid, sizeof(event->app.build_uuid)); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.h b/bugsnag-plugin-android-ndk/src/main/jni/metadata.h index cb9987c61b..c94f1edcb6 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.h @@ -23,8 +23,6 @@ void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, jobject metadata); -char *bsg_binary_arch(); - char *bsg_os_name(); #endif diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativeBridge.kt b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativeBridge.kt index b269973cb4..fb1faaa1f3 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativeBridge.kt +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativeBridge.kt @@ -5,8 +5,7 @@ import com.bugsnag.android.StateEvent.ClearMetadataSection import com.bugsnag.android.StateEvent.ClearMetadataValue import com.bugsnag.android.StateEvent.UpdateContext import com.bugsnag.android.StateEvent.UpdateUser -import java.util.Observable -import java.util.Observer +import com.bugsnag.android.internal.StateObserver /** * Listens for changes in the user, context, and metadata, then informs the JS layer @@ -15,33 +14,31 @@ import java.util.Observer internal class BugsnagReactNativeBridge( private val client: Client, private val cb: (event: MessageEvent) -> Unit -) : Observer { +) : StateObserver { - override fun update(observable: Observable, arg: Any?) { - if (arg is StateEvent) { - val event: MessageEvent? = when (arg) { - is UpdateContext -> { - MessageEvent("ContextUpdate", arg.context) - } - is AddMetadata, is ClearMetadataSection, is ClearMetadataValue -> { - MessageEvent("MetadataUpdate", client.metadata) - } - is UpdateUser -> { - MessageEvent( - "UserUpdate", - mapOf( - Pair("id", arg.user.id), - Pair("email", arg.user.email), - Pair("name", arg.user.name) - ) + override fun onStateChange(event: StateEvent) { + val msgEvent: MessageEvent? = when (event) { + is UpdateContext -> { + MessageEvent("ContextUpdate", event.context) + } + is AddMetadata, is ClearMetadataSection, is ClearMetadataValue -> { + MessageEvent("MetadataUpdate", client.metadata) + } + is UpdateUser -> { + MessageEvent( + "UserUpdate", + mapOf( + Pair("id", event.user.id), + Pair("email", event.user.email), + Pair("name", event.user.name) ) - } - else -> null + ) } + else -> null + } - if (event != null) { - cb(event) - } + if (msgEvent != null) { + cb(msgEvent) } } } diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativePlugin.kt b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativePlugin.kt index ee2541e004..36479d68e4 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativePlugin.kt +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/BugsnagReactNativePlugin.kt @@ -35,7 +35,7 @@ class BugsnagReactNativePlugin : Plugin { observerBridge = BugsnagReactNativeBridge(client) { jsCallback?.invoke(it) } - client.registerObserver(observerBridge) + client.addObserver(observerBridge) client.logger.i("Initialized React Native Plugin") } @@ -77,7 +77,7 @@ class BugsnagReactNativePlugin : Plugin { if (!ignoreJsExceptionCallbackAdded) { ignoreJavaScriptExceptions() } val map = HashMap() - configSerializer.serialize(map, internalHooks.config) + configSerializer.serialize(map, client.config) return map } @@ -138,6 +138,14 @@ class BugsnagReactNativePlugin : Plugin { requireNotNull(payload) val projectPackages = internalHooks.getProjectPackages(client.config) val event = EventDeserializer(client, projectPackages).deserialize(payload) + + if (event.errors.isEmpty()) { + return + } + val errorClass = event.errors[0].errorClass + if (client.immutableConfig.shouldDiscardError(errorClass)) { + return + } client.notifyInternal(event, null) } diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ConfigSerializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ConfigSerializer.java index 1b36bbdf9b..d54a500bf9 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ConfigSerializer.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ConfigSerializer.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import java.util.Collection; import java.util.HashMap; import java.util.HashSet; diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/InternalHooks.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/InternalHooks.java index e8ebf3ef74..2e2477511a 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/InternalHooks.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/InternalHooks.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import java.util.Collection; import java.util.Date; import java.util.List; @@ -13,10 +15,6 @@ public InternalHooks(Client client) { this.client = client; } - public ImmutableConfig getConfig() { - return client.getConfig(); - } - public AppWithState getAppWithState() { return client.appDataCollector.generateAppWithState(); } @@ -34,7 +32,7 @@ public DeviceWithState getDeviceWithState() { } public List getThreads(boolean unhandled) { - return new ThreadState(null, unhandled, getConfig()).getThreads(); + return new ThreadState(null, unhandled, client.getConfig()).getThreads(); } public Collection getProjectPackages(ImmutableConfig config) { diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/MetadataDeserializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/MetadataDeserializer.java index 1aebf3bc16..766835681d 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/MetadataDeserializer.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/MetadataDeserializer.java @@ -6,7 +6,11 @@ class MetadataDeserializer implements MapDeserializer { @Override public Metadata deserialize(Map map) { - ConcurrentHashMap store = new ConcurrentHashMap<>(map); + // cast map to retain original signature until next major version bump, as this + // method signature is used by Unity/React native + @SuppressWarnings({"unchecked", "rawtypes"}) + Map> data = (Map) map; + ConcurrentHashMap> store = new ConcurrentHashMap<>(data); return new Metadata(store); } } diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/NativeStackDeserializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/NativeStackDeserializer.java index 0cfb7e8af0..ba55bdab96 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/NativeStackDeserializer.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/NativeStackDeserializer.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -44,7 +46,7 @@ private Stackframe deserializeStackframe(Map map, } String clz = MapUtils.getOrNull(map, "class"); - String method = String.format("%s.%s", clz, methodName); + String method = clz + "." + methodName; // RN <0.63.2 doesn't add class, gracefully fallback by only reporting // method name. see https://github.com/facebook/react-native/pull/25014 diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativeBridgeTest.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativeBridgeTest.java index 16d3616c68..cbfa19a53b 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativeBridgeTest.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativeBridgeTest.java @@ -41,7 +41,7 @@ public void userUpdate() { BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); User user = new User("123", "joe@example.com", "Joe Bloggs"); - bridge.update(new Observable(), new StateEvent.UpdateUser(user)); + bridge.onStateChange(new StateEvent.UpdateUser(user)); assertNotNull(cb.event); assertEquals("UserUpdate", cb.event.getType()); @@ -57,7 +57,7 @@ public void contextUpdate() { MessageEventCb cb = new MessageEventCb(); BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); - bridge.update(new Observable(), new StateEvent.UpdateContext("Foo")); + bridge.onStateChange(new StateEvent.UpdateContext("Foo")); assertNotNull(cb.event); assertEquals("ContextUpdate", cb.event.getType()); assertEquals("Foo", cb.event.getData()); @@ -69,7 +69,7 @@ public void addMetadataUpdate() { BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); StateEvent.AddMetadata arg = new StateEvent.AddMetadata("foo", "bar", true); - bridge.update(new Observable(), arg); + bridge.onStateChange(arg); assertNotNull(cb.event); assertEquals("MetadataUpdate", cb.event.getType()); assertEquals(metadata, cb.event.getData()); @@ -81,7 +81,7 @@ public void clearMetadataSectionUpdate() { BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); StateEvent.ClearMetadataSection arg = new StateEvent.ClearMetadataSection("baz"); - bridge.update(new Observable(), arg); + bridge.onStateChange(arg); assertNotNull(cb.event); assertEquals("MetadataUpdate", cb.event.getType()); assertEquals(metadata, cb.event.getData()); @@ -93,7 +93,7 @@ public void clearMetadataValueUpdate() { BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); StateEvent.ClearMetadataValue arg = new StateEvent.ClearMetadataValue("baz", "wham"); - bridge.update(new Observable(), arg); + bridge.onStateChange(arg); assertNotNull(cb.event); assertEquals("MetadataUpdate", cb.event.getType()); assertEquals(metadata, cb.event.getData()); diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ConfigSerializerTest.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ConfigSerializerTest.java index 4247f16082..bc15e2536a 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ConfigSerializerTest.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ConfigSerializerTest.java @@ -1,10 +1,9 @@ package com.bugsnag.android; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.bugsnag.android.ImmutableConfig; +import com.bugsnag.android.internal.ImmutableConfig; import org.junit.Before; import org.junit.Test; 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 33bc0e82ec..58bccc8f1b 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 @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import java.io.IOException; import java.nio.file.Files; import java.util.Collections; @@ -32,7 +34,9 @@ static ImmutableConfig generateConfig() throws IOException { 32, 32, Files.createTempDirectory("foo").toFile(), - true + true, + null, + null ); } diff --git a/build.gradle b/build.gradle index 260f00dfb5..57373c14d0 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,6 @@ buildscript { repositories { google() mavenCentral() - jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { @@ -14,6 +13,7 @@ buildscript { classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Versions.detektPlugin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokkaPlugin}" classpath "org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktlintPlugin}" + classpath "androidx.benchmark:benchmark-gradle-plugin:${Versions.benchmarkPlugin}" } } plugins { @@ -24,19 +24,11 @@ allprojects { repositories { google() mavenCentral() - jcenter() } gradle.projectsEvaluated { tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:all" << "-Werror" } - - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - allWarningsAsErrors = true - jvmTarget = "1.6" - } - } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 9ba0ee06da..7dad091141 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,6 @@ gradlePlugin { repositories { google() mavenCentral() - jcenter() } dependencies { diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt index ab26539b0b..b928b7ed33 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt +++ b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt @@ -16,9 +16,10 @@ object Versions { // plugins val androidGradlePlugin = "4.1.1" val bintrayPlugin = "1.8.5" - val detektPlugin = "1.14.1" + val detektPlugin = "1.17.1" val ktlintPlugin = "9.4.1" val dokkaPlugin = "0.10.1" + val benchmarkPlugin = "1.0.0" // dependencies val supportLib = "1.1.0" diff --git a/docker-compose.yml b/docker-compose.yml index 15823f6148..0254af19d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: SAUCE_LABS_USERNAME: SAUCE_LABS_ACCESS_KEY: INSTRUMENTATION_DEVICES: + TEST_APK_LOCATION: android-builder: build: @@ -66,8 +67,8 @@ services: SAUCE_LABS_ACCESS_KEY: volumes: - ./build:/app/build - - ./maze-output:/app/maze-output - ./features/:/app/features/ + - ./maze_output:/app/maze_output android-license-audit: build: diff --git a/dockerfiles/Dockerfile.android-ci-base b/dockerfiles/Dockerfile.android-ci-base index 796f02b44a..4dc195861a 100644 --- a/dockerfiles/Dockerfile.android-ci-base +++ b/dockerfiles/Dockerfile.android-ci-base @@ -24,6 +24,7 @@ COPY bugsnag-plugin-android-anr/ bugsnag-plugin-android-anr/ COPY bugsnag-android-core/ bugsnag-android-core/ COPY bugsnag-plugin-android-ndk/ bugsnag-plugin-android-ndk/ COPY bugsnag-plugin-react-native/ bugsnag-plugin-react-native/ +COPY bugsnag-benchmarks/ bugsnag-benchmarks/ COPY scripts/ scripts/ COPY config/ config/ COPY LICENSE LICENSE diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md new file mode 100644 index 0000000000..4096b0a566 --- /dev/null +++ b/docs/BENCHMARKS.md @@ -0,0 +1,39 @@ +# Performance benchmarks + +This contains baseline performance benchmarks for Bugsnag's API from the +[microbenchmark library](https://developer.android.com/studio/profile/benchmark). + +These are collected manually from the [Browserstack run](https://app-automate.browserstack.com/dashboard/v2/builds/3953b399be9a856b01bcd675f6b6854542f55405). +This was last collected from the [following commit](https://github.com/bugsnag/bugsnag-android/pull/1273/commits/94f288c706abf6ef6773439188ec4d09c6fcfe36). + +## Caveats + +The following factors introduce variance to some of the benchmarking results: + +- Some APIs use I/O which can have high variance +- Some functions use one-off initialization or caching +- Clock locking is not enabled as this would require a rooted device, this decreases the accuracy of the benchmarking library + +It is therefore important to compare results against a previous baseline rather than looking at the absolute values. + +## Results + +3,395 ns CriticalPathBenchmarkTest.addMetadataSection +10,679 ns CriticalPathBenchmarkTest.addSingleMetadataValue +7,028 ns CriticalPathBenchmarkTest.clearMetadataSection +10,118 ns CriticalPathBenchmarkTest.clearSingleMetadataValue +4,649,115 ns CriticalPathBenchmarkTest.clientNotify +140,625 ns CriticalPathBenchmarkTest.configConstructor +952,448 ns CriticalPathBenchmarkTest.configManifestLoad +190 ns CriticalPathBenchmarkTest.getMetadataSection +351 ns CriticalPathBenchmarkTest.getSingleMetadataValue +163,021 ns CriticalPathBenchmarkTest.leaveComplexBreadcrumb +426,666 ns CriticalPathBenchmarkTest.leaveSimpleBreadcrumb +1,456,772 ns JsonSerializationBenchmarkTest.serializeEventPayload +93,932 ns JsonSerializationBenchmarkTest.serializeSessionPayload +1,631 ns SessionBenchmarkTest.pauseSession +40,355 ns SessionBenchmarkTest.resumeSession +126,563 ns SessionBenchmarkTest.startSession +6,725 ns ClientDataBenchmarkTest.setContext +176,953 ns ClientDataBenchmarkTest.setUser +1,525,417 ns EventBenchmarkTest.createEvent diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 92d3e51215..ea539dd8fd 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -31,6 +31,7 @@ This contains a prompt of checks which you may want to test, depending on the ex ## Making the release +- Check the performance benchmarks against the [baseline](BENCHMARKS.md) to confirm there are no serious regressions - Make a PR from `next` to `master` (or from a bug fix branch for urgent hot fixes): - [ ] Update the version number with `make VERSION=[number] bump` - [ ] Inspect the updated CHANGELOG, README, and version files to ensure they are correct diff --git a/examples/sdk-app-example/build.gradle b/examples/sdk-app-example/build.gradle index 58469375e9..5a536b6827 100644 --- a/examples/sdk-app-example/build.gradle +++ b/examples/sdk-app-example/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = "1.3.72" repositories { google() - jcenter() + mavenCentral() mavenLocal() } dependencies { @@ -15,7 +15,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() mavenLocal() maven { // add this to use snapshots url "https://oss.sonatype.org/content/repositories/snapshots/" diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt index 9b3956bdd6..d8788d1a60 100644 --- a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt @@ -8,6 +8,7 @@ class MazerunnerApp : Application() { override fun onCreate() { super.onCreate() + triggerStartupAnrIfRequired() setupNonSdkUsageStrictMode() } 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 new file mode 100644 index 0000000000..c63caf5361 --- /dev/null +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt @@ -0,0 +1,42 @@ +package com.bugsnag.android.mazerunner + +import android.app.Application +import android.content.Context +import android.util.Log +import com.bugsnag.android.Bugsnag +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread + +fun Application.triggerStartupAnrIfRequired() { + val prefs = getSharedPreferences("AnrPreferences", Context.MODE_PRIVATE) + val startupDelay = prefs.getLong("onCreateDelay", 0) + + if (startupDelay > 0L) { + // we remove the preference so that we don't affect any future startup + prefs.edit() + .remove("onCreateDelay") + .commit() + + // we have to startup Bugsnag at this point + Bugsnag.start(this) + + 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 + // are pausing to test for ANRs) + + // if there is a startup delay, we assume we are testing ANRs and send a "BACK" + // key-press from the *system* in an attempt to cause an ANR + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(1L)) + Runtime.getRuntime() + .exec("input keyevent 4") + .waitFor() + } catch (ex: Exception) { + Log.w("StartupANR", "Couldn't send keyevent for BACK", ex) + } + } + + Thread.sleep(TimeUnit.SECONDS.toMillis(startupDelay)) + } +} diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index aba4b56e5a..e47c7da46a 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -21,7 +21,6 @@ buildscript { repositories { google() mavenCentral() - jcenter() maven { url "https://plugins.gradle.org/m2/" } } ext.kotlin_version = "1.3.72" 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 32c5b32942..a66a9b8e8f 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 @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import java.util.Collections; diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ConfigureStartupAnrScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ConfigureStartupAnrScenario.kt new file mode 100644 index 0000000000..7ad0773e54 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ConfigureStartupAnrScenario.kt @@ -0,0 +1,25 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Configuration +import kotlin.system.exitProcess + +class ConfigureStartupAnrScenario( + config: Configuration, + context: Context, + eventMetadata: String +) : Scenario(config, context, eventMetadata) { + override fun startScenario() { + context.applicationContext + .getSharedPreferences("AnrPreferences", Context.MODE_PRIVATE) + .edit() + .putLong("onCreateDelay", STARTUP_DELAY) + .commit() + + exitProcess(0) + } + + companion object { + private const val STARTUP_DELAY = 430L + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyEnabledReleaseStageScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyEnabledReleaseStageScenario.kt index c87b9dd673..b52ab27621 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyEnabledReleaseStageScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyEnabledReleaseStageScenario.kt @@ -23,6 +23,5 @@ internal class EmptyEnabledReleaseStageScenario( Bugsnag.notify(generateException()) } - override fun getInterceptedLogMessages() = - listOf("Skipping notification - should not notify for this release stage") + override fun getInterceptedLogMessages() = listOf("Bugsnag loaded") } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/IgnoredExceptionScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/IgnoredExceptionScenario.kt index 0b5f8c83d9..cb502c89b3 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/IgnoredExceptionScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/IgnoredExceptionScenario.kt @@ -3,6 +3,7 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration +import java.lang.IllegalStateException /** * Attempts to send an ignored handled exception to Bugsnag, which should not result @@ -21,8 +22,6 @@ internal class IgnoredExceptionScenario( override fun startScenario() { super.startScenario() Bugsnag.notify(RuntimeException("Should never appear")) + Bugsnag.notify(IllegalStateException("Is it me you're looking for?")) } - - override fun getInterceptedLogMessages() = - listOf("Skipping notification - should not notify for this class") } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullReleaseStageScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullReleaseStageScenario.kt index 2c68958d8b..c254829d91 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullReleaseStageScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullReleaseStageScenario.kt @@ -23,6 +23,5 @@ internal class NullReleaseStageScenario( Bugsnag.notify(generateException()) } - override fun getInterceptedLogMessages() = - listOf("Skipping notification - should not notify for this release stage") + override fun getInterceptedLogMessages() = listOf("Bugsnag loaded") } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OutsideReleaseStageScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OutsideReleaseStageScenario.kt index fa43a2163b..fc621c091d 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OutsideReleaseStageScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OutsideReleaseStageScenario.kt @@ -24,6 +24,5 @@ internal class OutsideReleaseStageScenario( Bugsnag.notify(RuntimeException("OutsideReleaseStageScenario")) } - override fun getInterceptedLogMessages() = - listOf("Skipping notification - should not notify for this release stage") + override fun getInterceptedLogMessages() = listOf("Bugsnag loaded") } diff --git a/features/fixtures/minimalapp/app/build.gradle b/features/fixtures/minimalapp/app/build.gradle index 84b91c6129..4eb5644c25 100644 --- a/features/fixtures/minimalapp/app/build.gradle +++ b/features/fixtures/minimalapp/app/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'kotlin-android-extensions' repositories { mavenLocal() google() - jcenter() + mavenCentral() } android { diff --git a/features/fixtures/minimalapp/build.gradle b/features/fixtures/minimalapp/build.gradle index 386503224f..2bc1457523 100644 --- a/features/fixtures/minimalapp/build.gradle +++ b/features/fixtures/minimalapp/build.gradle @@ -4,7 +4,7 @@ buildscript { ext.kotlin_version = "1.3.72" repositories { google() - jcenter() + mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:4.1.1" diff --git a/features/full_tests/batch_1/auto_notify.feature b/features/full_tests/batch_1/auto_notify.feature index 163a83b528..ae401a2754 100644 --- a/features/full_tests/batch_1/auto_notify.feature +++ b/features/full_tests/batch_1/auto_notify.feature @@ -48,20 +48,20 @@ Scenario: NDK signal captured with autoNotify reenabled And the event "severityReason.unhandledOverridden" is false # PLAT-6620 -# @skip_android_8_1 -# Scenario: ANR captured with autoDetectAnrs reenabled -# When I run "AutoDetectAnrsTrueScenario" -# And I wait for 2 seconds -# And I tap the screen 3 times -# And I wait for 4 seconds -# And I clear any error dialogue -# 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 "ANR" -# And the exception "message" starts with " Input dispatching timed out" -# And the exception "type" equals "android" -# And the event "unhandled" is true -# And the event "severity" equals "error" -# And the event "severityReason.type" equals "anrError" -# And the event "severityReason.unhandledOverridden" is false +@skip_android_8_1 +Scenario: ANR captured with autoDetectAnrs reenabled + When I run "AutoDetectAnrsTrueScenario" + And I wait for 2 seconds + And I tap the screen 3 times + And I wait for 4 seconds + And I clear any error dialogue + 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 "ANR" + And the exception "message" starts with " Input dispatching timed out" + And the exception "type" equals "android" + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "anrError" + And the event "severityReason.unhandledOverridden" is false diff --git a/features/full_tests/batch_1/ignored_reports.feature b/features/full_tests/batch_1/ignored_reports.feature index f273b268a2..4993463d28 100644 --- a/features/full_tests/batch_1/ignored_reports.feature +++ b/features/full_tests/batch_1/ignored_reports.feature @@ -2,8 +2,11 @@ Feature: Reports are ignored Scenario: Exception classname ignored When I run "IgnoredExceptionScenario" - And I wait to receive 1 logs - Then the "debug" level log message equals "Skipping notification - should not notify for this class" + 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 exception "errorClass" equals "java.lang.IllegalStateException" + And the exception "message" equals "Is it me you're looking for?" + And the event "unhandled" is false Scenario: Disabled Exception Handler When I run "DisableAutoDetectErrorsScenario" and relaunch the app diff --git a/features/full_tests/batch_1/startup_anr.feature b/features/full_tests/batch_1/startup_anr.feature new file mode 100644 index 0000000000..d763af0572 --- /dev/null +++ b/features/full_tests/batch_1/startup_anr.feature @@ -0,0 +1,10 @@ +Feature: onCreate ANR + +@skip_android_10 +Scenario: onCreate ANR is reported + When I run "ConfigureStartupAnrScenario" + And I relaunch the app after a crash + And I wait for 30 seconds + And I clear any error dialogue + Then I wait to receive an error + And the exception "errorClass" equals "ANR" diff --git a/features/full_tests/batch_2/release_stage.feature b/features/full_tests/batch_2/release_stage.feature index 96cae81796..93ff6454a3 100644 --- a/features/full_tests/batch_2/release_stage.feature +++ b/features/full_tests/batch_2/release_stage.feature @@ -3,17 +3,17 @@ Feature: Reporting exceptions with release stages Scenario: Exception not reported when outside release stage When I run "OutsideReleaseStageScenario" And I wait to receive 1 logs - Then the "debug" level log message equals "Skipping notification - should not notify for this release stage" + Then the "debug" level log message equals "Bugsnag loaded" Scenario: Exception not reported when release stage null When I run "NullReleaseStageScenario" And I wait to receive 1 logs - Then the "debug" level log message equals "Skipping notification - should not notify for this release stage" + Then the "debug" level log message equals "Bugsnag loaded" Scenario: Exception reported when release stages empty When I run "EmptyEnabledReleaseStageScenario" And I wait to receive 1 logs - Then the "debug" level log message equals "Skipping notification - should not notify for this release stage" + Then the "debug" level log message equals "Bugsnag loaded" Scenario: Exception reported when inside Notify release stage array When I run "ArrayEnabledReleaseStageScenario" diff --git a/features/smoke_tests/handled.feature b/features/smoke_tests/handled.feature index dd314a940a..4c99566477 100644 --- a/features/smoke_tests/handled.feature +++ b/features/smoke_tests/handled.feature @@ -30,6 +30,7 @@ Scenario: Notify caught Java exception with default configuration And the error payload field "events.0.threads.0.stacktrace.0.method" ends with "getThreadStackTrace" # App data + And the event binary arch field is valid And the event "app.buildUUID" equals "test-7.5.3" And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "mazerunner" @@ -179,6 +180,7 @@ Scenario: Handled C functionality And the event "projectPackages.0" equals "com.bugsnag.android.mazerunner" # App data + And the event binary arch field is valid And the event "app.buildUUID" is not null And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "mazerunner" diff --git a/features/smoke_tests/unhandled.feature b/features/smoke_tests/unhandled.feature index 9dd18c0753..dd93cd8172 100644 --- a/features/smoke_tests/unhandled.feature +++ b/features/smoke_tests/unhandled.feature @@ -31,6 +31,7 @@ Scenario: Unhandled Java Exception with loaded configuration And the error payload field "events.0.threads.0.stacktrace.0.method" equals "com.bugsnag.android.mazerunner.scenarios.UnhandledJavaLoadedConfigScenario.startScenario" # App data + And the event binary arch field is valid And the event "app.buildUUID" equals "test-7.5.3" And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "mazerunner" @@ -122,6 +123,7 @@ Scenario: Signal raised with overwritten config And the error payload field "events.0.exceptions.0.stacktrace.0.lineNumber" is greater than 0 # App data + And the event binary arch field is valid And the event "app.buildUUID" equals "test-7.5.3" And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "CXXSignalSmokeScenario" @@ -202,6 +204,7 @@ Scenario: C++ exception thrown with overwritten config And the error payload field "events.0.exceptions.0.stacktrace.0.lineNumber" is greater than 0 # App data + And the event binary arch field is valid And the event "app.buildUUID" equals "test-7.5.3" And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "CXXExceptionSmokeScenario" @@ -277,6 +280,7 @@ Scenario: ANR detection And the event "exceptions.0.stacktrace.0.lineNumber" is not null # App data + And the event binary arch field is valid And the event "app.buildUUID" equals "test-7.5.3" And the event "app.id" equals "com.bugsnag.android.mazerunner" And the event "app.releaseStage" equals "mazerunner" diff --git a/features/steps/android_steps.rb b/features/steps/android_steps.rb index 7c611b5319..8e15bb031b 100644 --- a/features/steps/android_steps.rb +++ b/features/steps/android_steps.rb @@ -232,6 +232,9 @@ def click_if_present(element) return false unless Maze.driver.wait_for_element(element, 1) Maze.driver.click_element_if_present(element) +rescue Selenium::WebDriver::Error::UnknownError + # Ignore Appium errors (e.g. during an ANR) + return false end Then("I sort the errors by {string}") do |comparator| @@ -251,6 +254,13 @@ def click_if_present(element) end end +Then("the event binary arch field is valid") do + arch = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.app.binaryArch") + assert_block "'#{arch}' is not a valid value for app.binaryArch" do + ["x86", "x86_64", "arm32", "arm64"].include? arch + end +end + # EventStore flushes multiple times on launch with access controlled via a semaphore, # which results in multiple similar log messages Then("Bugsnag confirms it has no errors to send") do diff --git a/gradle.properties b/gradle.properties index 1be5d4a9a1..6318a0c2a6 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.9.4 +VERSION_NAME=5.9.5 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle new file mode 100644 index 0000000000..e163b09c0e --- /dev/null +++ b/gradle/kotlin.gradle @@ -0,0 +1,6 @@ +android { + kotlinOptions { + allWarningsAsErrors = true + jvmTarget = "1.6" + } +} diff --git a/maze_output/.gitignore b/maze_output/.gitignore new file mode 100644 index 0000000000..91224e5de8 --- /dev/null +++ b/maze_output/.gitignore @@ -0,0 +1 @@ +**/* diff --git a/scripts/run-instrumentation-test.sh b/scripts/run-instrumentation-test.sh index 1b4eb259f2..79ce1af51f 100755 --- a/scripts/run-instrumentation-test.sh +++ b/scripts/run-instrumentation-test.sh @@ -4,13 +4,17 @@ timestamp() { date +"%T" } -export APP_LOCATION=examples/sdk-app-example/app/build/outputs/apk/release/app-release.apk -export TEST_LOCATION=bugsnag-android-core/build/outputs/apk/androidTest/debug/bugsnag-android-core-debug-androidTest.apk +if [ -z "$TEST_APK_LOCATION" ]; then + echo "Please supply the path to the instrumentation test APK in the TEST_APK_LOCATION variable." + exit 1 +fi + +export TARGET_APK_LOCATION=examples/sdk-app-example/app/build/outputs/apk/release/app-release.apk # First app. This is not actually used, but must be present and different to the test app. echo "Android Tests [$(timestamp)]: Starting instrumentation test run against devices: $INSTRUMENTATION_DEVICES" -echo "Android Tests [$(timestamp)]: Uploading first test app from $APP_LOCATION to BrowserStack" -app_response=$(curl -u "$BROWSER_STACK_USERNAME:$BROWSER_STACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$APP_LOCATION") +echo "Android Tests [$(timestamp)]: Uploading first test app from $TARGET_APK_LOCATION to BrowserStack" +app_response=$(curl -u "$BROWSER_STACK_USERNAME:$BROWSER_STACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$TARGET_APK_LOCATION") app_url=$(echo "$app_response" | jq -r ".app_url") if [ -z "$app_url" ]; then @@ -22,8 +26,8 @@ fi echo "Android Tests [$(timestamp)]: First app upload successful, url: $app_url" # Second app - the tests. -echo "Android Tests [$(timestamp)]: Uploading second test app from $TEST_LOCATION to BrowserStack" -test_response=$(curl -u "$BROWSER_STACK_USERNAME:$BROWSER_STACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/espresso/test-suite" -F "file=@$TEST_LOCATION") +echo "Android Tests [$(timestamp)]: Uploading second test app from $TEST_APK_LOCATION to BrowserStack" +test_response=$(curl -u "$BROWSER_STACK_USERNAME:$BROWSER_STACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/espresso/test-suite" -F "file=@$TEST_APK_LOCATION") test_url=$(echo "$test_response" | jq -r ".test_url") if [ -z "$test_url" ]; then @@ -45,7 +49,7 @@ if [ -z "$build_id" ] || [ "$build_id" = "null" ]; then exit 1 fi -echo "Android Tests [$(timestamp)]: Test run creation successful, id: $build_id" +echo "Android Tests [$(timestamp)]: Test run creation successful, browserstack session: https://app-automate.browserstack.com/builds/$build_id" echo "Android Tests [$(timestamp)]: Waiting for test run to begin" sleep 10 # Allow the tests to kick off diff --git a/settings.gradle b/settings.gradle index 880490ee9f..0dd7b52740 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,5 +15,6 @@ include( ":bugsnag-android-core", ":bugsnag-plugin-android-anr", ":bugsnag-plugin-android-ndk", - ":bugsnag-plugin-react-native" + ":bugsnag-plugin-react-native", + ":bugsnag-benchmarks" )