From c025f8d03ec7b7b55c44e15da395d90ffcc4ca0b Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Wed, 20 Mar 2024 17:03:27 +0000 Subject: [PATCH 01/17] Capture logs for scenario --- .../scenarios/JvmAnrOutsideReleaseStagesScenario.java | 8 ++++++++ features/full_tests/detect_anr_jvm.feature | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java index 7026f51ea9..83dc9310ff 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java @@ -1,6 +1,8 @@ package com.bugsnag.android.mazerunner.scenarios; import static com.bugsnag.android.mazerunner.AnrHelperKt.createDeadlock; +import static com.bugsnag.android.mazerunner.LogKt.getZeroEventsLogMessages; + import com.bugsnag.android.Configuration; @@ -10,6 +12,7 @@ import androidx.annotation.Nullable; import java.util.Collections; +import java.util.List; public class JvmAnrOutsideReleaseStagesScenario extends Scenario { @@ -28,4 +31,9 @@ public void startScenario() { super.startScenario(); createDeadlock(); } + + @Override + public List getInterceptedLogMessages() { + return getZeroEventsLogMessages(getStartBugsnagOnly()); + } } diff --git a/features/full_tests/detect_anr_jvm.feature b/features/full_tests/detect_anr_jvm.feature index 226fe45fe6..01539dfdb1 100644 --- a/features/full_tests/detect_anr_jvm.feature +++ b/features/full_tests/detect_anr_jvm.feature @@ -56,4 +56,10 @@ Feature: ANRs triggered in JVM code are captured When I run "JvmAnrOutsideReleaseStagesScenario" And I wait for 2 seconds And I tap the screen 3 times - Then I should receive no errors + And I wait for 10 seconds + And I close and relaunch the app after an ANR + And I configure Bugsnag for "JvmAnrOutsideReleaseStagesScenario" + Then Bugsnag confirms it has no errors to send + And I wait for 10 seconds + # Wait extra 10 seconds in the end, so appium will have enough time to terminated the previous anr session + From ec6b0de685377b4d8c762584df080c8406d7965e Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Wed, 20 Mar 2024 17:04:12 +0000 Subject: [PATCH 02/17] Allow for the app already having stopped [full ci] --- .../JvmAnrOutsideReleaseStagesScenario.java | 1 - features/full_tests/auto_notify.feature | 3 ++- features/steps/android_steps.rb | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java index 83dc9310ff..871f233b1f 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmAnrOutsideReleaseStagesScenario.java @@ -3,7 +3,6 @@ import static com.bugsnag.android.mazerunner.AnrHelperKt.createDeadlock; import static com.bugsnag.android.mazerunner.LogKt.getZeroEventsLogMessages; - import com.bugsnag.android.Configuration; import android.content.Context; diff --git a/features/full_tests/auto_notify.feature b/features/full_tests/auto_notify.feature index e84320c52c..980b819954 100644 --- a/features/full_tests/auto_notify.feature +++ b/features/full_tests/auto_notify.feature @@ -26,7 +26,8 @@ Feature: Switching automatic error detection on/off for Unity When I run "AutoDetectAnrsFalseScenario" And I wait for 2 seconds And I tap the screen 3 times - And I close and relaunch the app + And I wait for 10 seconds + And I close and relaunch the app after an ANR And I configure Bugsnag for "AutoDetectAnrsFalseScenario" Then Bugsnag confirms it has no errors to send And I wait for 10 seconds diff --git a/features/steps/android_steps.rb b/features/steps/android_steps.rb index 47e5a8cc91..8f37590151 100644 --- a/features/steps/android_steps.rb +++ b/features/steps/android_steps.rb @@ -70,14 +70,22 @@ def press_at(x, y) execute_command :start_bugsnag, event_type end +When("I terminate the app") do + Maze.driver.terminate_app Maze.driver.app_id +end + When("I close and relaunch the app") do - if Maze.config.legacy_driver? - Maze.driver.close_app - Maze.driver.launch_app - else + Maze.driver.terminate_app Maze.driver.app_id + Maze.driver.activate_app Maze.driver.app_id +end + +When("I close and relaunch the app after an ANR") do + begin Maze.driver.terminate_app Maze.driver.app_id - Maze.driver.activate_app Maze.driver.app_id + rescue Selenium::WebDriver::Error::ServerError + # Swallow any error, as Android may already have terminated the app end + Maze.driver.activate_app Maze.driver.app_id end When('I set the screen orientation to portrait') do From f6153b18e4e267733d69531dfb1327ceaf9ba788 Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Wed, 20 Mar 2024 18:05:31 +0000 Subject: [PATCH 03/17] Tidy [full ci] --- features/full_tests/detect_anr_jvm.feature | 1 - 1 file changed, 1 deletion(-) diff --git a/features/full_tests/detect_anr_jvm.feature b/features/full_tests/detect_anr_jvm.feature index 01539dfdb1..c65be424c6 100644 --- a/features/full_tests/detect_anr_jvm.feature +++ b/features/full_tests/detect_anr_jvm.feature @@ -62,4 +62,3 @@ Feature: ANRs triggered in JVM code are captured Then Bugsnag confirms it has no errors to send And I wait for 10 seconds # Wait extra 10 seconds in the end, so appium will have enough time to terminated the previous anr session - From 91559eb9cef3ae64eb8232431a59e5ef65acc65a Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Thu, 14 Mar 2024 19:02:15 +0000 Subject: [PATCH 04/17] Bump build steps to macOS 14 [full ci] --- .buildkite/pipeline.full.yml | 8 ++++---- .buildkite/pipeline.yml | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index a6b70c5abd..668b6a9bdd 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -3,21 +3,21 @@ steps: key: "fixture-minimal" timeout_in_minutes: 30 agents: - queue: macos-12-arm + queue: macos-14 artifact_paths: build/fixture-minimal.apk command: make fixture-minimal - label: ':android: Build Example App' timeout_in_minutes: 5 agents: - queue: macos-12-arm + queue: macos-14 command: 'make example-app' - label: ':android: Build debug fixture APK' key: "fixture-debug" timeout_in_minutes: 30 agents: - queue: macos-12-arm + queue: macos-14 artifact_paths: - "build/fixture-debug.apk" - "build/fixture-debug/*" @@ -26,7 +26,7 @@ steps: - label: ':android: Build Scan' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 command: './gradlew clean assembleRelease check --scan' # diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c46e257a85..c4cea0ee6b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -3,14 +3,14 @@ steps: - label: 'Audit current licenses' timeout_in_minutes: 30 agents: - queue: macos-12-arm + queue: macos-14 command: ./scripts/audit-dependency-licenses.sh - label: ':android: Build fixture APK r19' key: "fixture-r19" timeout_in_minutes: 30 agents: - queue: macos-12-arm + queue: macos-14 artifact_paths: - "build/fixture-r19.apk" - "build/fixture-r19-url.txt" @@ -24,7 +24,7 @@ steps: key: "fixture-r21" timeout_in_minutes: 30 agents: - queue: macos-12-arm + queue: macos-14 artifact_paths: - "build/fixture-r21.apk" - "build/fixture-r21-url.txt" @@ -37,31 +37,31 @@ steps: - label: ':android: Coding standards checks' timeout_in_minutes: 20 agents: - queue: macos-12-arm + queue: macos-14 command: './gradlew --continue checkstyle detekt lint ktlintCheck' - label: ':android: Binary compatibility checks' timeout_in_minutes: 20 agents: - queue: macos-12-arm + queue: macos-14 command: './gradlew apiCheck' - label: ':android: CppCheck' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 command: 'bash ./scripts/run-cpp-check.sh' - label: ':android: ClangFormat' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 command: 'bash ./scripts/run-clang-format-ci-check.sh' - label: ':android: Lint mazerunner scenarios' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 commands: - cd features/fixtures/mazerunner - ./gradlew ktlintCheck detekt checkstyle @@ -69,13 +69,13 @@ steps: - label: ':android: Android size reporting' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 command: scripts/run-sizer.sh - label: ':android: JVM tests' timeout_in_minutes: 10 agents: - queue: macos-12-arm + queue: macos-14 command: './gradlew test' # @@ -346,6 +346,6 @@ steps: - label: 'Conditionally include device farms/full tests' agents: - queue: macos-12-arm + queue: macos-14 command: sh -c .buildkite/pipeline_trigger.sh From 4412d0621af54aef574fbafeb2f4ad35c24db25d Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Thu, 14 Mar 2024 20:35:48 +0000 Subject: [PATCH 05/17] Bundle install first --- .buildkite/pipeline.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c4cea0ee6b..2962c40f2b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -4,7 +4,9 @@ steps: timeout_in_minutes: 30 agents: queue: macos-14 - command: ./scripts/audit-dependency-licenses.sh + commands: + - bundle install + - ./scripts/audit-dependency-licenses.sh - label: ':android: Build fixture APK r19' key: "fixture-r19" From d6f9f18edde73ee83e827093c171e205fd533db7 Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Thu, 14 Mar 2024 20:41:15 +0000 Subject: [PATCH 06/17] Correct clang format --- bugsnag-plugin-android-ndk/src/main/jni/utils/seqlock.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/seqlock.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/seqlock.h index 45638229ce..539c35e3b7 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/seqlock.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/seqlock.h @@ -25,7 +25,7 @@ bsg_seqlock_status_t bsg_seqlock_optimistic_read(bsg_seqlock_t *lock); bool bsg_seqlock_validate(bsg_seqlock_t *lock, bsg_seqlock_status_t expected); -#define bsg_seqlock_is_write_locked(c) (((c)&1uLL) != 0) +#define bsg_seqlock_is_write_locked(c) (((c) & 1uLL) != 0) #ifdef __cplusplus } From 64c8f5ceb5a413df644daf2991ab118001f8074a Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Fri, 15 Mar 2024 07:50:14 +0000 Subject: [PATCH 07/17] Use ruby 2.7.7 for the license finder --- .buildkite/pipeline.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 2962c40f2b..37f0f4d7fa 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -4,7 +4,8 @@ steps: timeout_in_minutes: 30 agents: queue: macos-14 - commands: + commands: + - rbenv local 2.7.7 - bundle install - ./scripts/audit-dependency-licenses.sh From c638d6bd57a0bb0bdc8969b5b82a08870efb5013 Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Fri, 15 Mar 2024 12:05:02 +0000 Subject: [PATCH 08/17] Use the latest license_finder --- .buildkite/pipeline.yml | 1 - Gemfile | 2 +- Gemfile.lock | 10 +++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 37f0f4d7fa..1ed4bcd747 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -5,7 +5,6 @@ steps: agents: queue: macos-14 commands: - - rbenv local 2.7.7 - bundle install - ./scripts/audit-dependency-licenses.sh diff --git a/Gemfile b/Gemfile index 9cd45a8f2d..9b581fb1b7 100644 --- a/Gemfile +++ b/Gemfile @@ -9,4 +9,4 @@ gem 'bugsnag-maze-runner', '~>9.0' # Or follow master: #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner' -gem "license_finder", "~> 6.13" +gem "license_finder", "~> 7.0" diff --git a/Gemfile.lock b/Gemfile.lock index 098535ec62..109a5b65a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,13 +78,13 @@ GEM regexp_parser (~> 2.0) simpleidn (~> 0.2) uri_template (~> 0.7) - license_finder (6.15.0) + license_finder (7.1.0) bundler rubyzip (>= 1, < 3) - thor (~> 1.0.1) + thor (~> 1.2) tomlrb (>= 1.3, < 2.1) with_env (= 1.1.0) - xml-simple (~> 1.1.5) + xml-simple (~> 1.1.9) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0206) @@ -113,7 +113,7 @@ GEM ffi (~> 1.1) test-unit (3.5.9) power_assert - thor (1.0.1) + thor (1.3.1) tomlrb (2.0.3) unf (0.1.4) unf_ext @@ -135,7 +135,7 @@ PLATFORMS DEPENDENCIES bugsnag-maze-runner (~> 9.0) - license_finder (~> 6.13) + license_finder (~> 7.0) BUNDLED WITH 2.4.8 From 09e3850868c70ffdb85579ccbcaa45b7e8c62373 Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:05:12 +0100 Subject: [PATCH 09/17] dokka html update with bugsnag android v6.3.0 (#2004) --- bugsnag-android-core/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bugsnag-android-core/build.gradle b/bugsnag-android-core/build.gradle index 223bc1e28a..e1d57b95f7 100644 --- a/bugsnag-android-core/build.gradle +++ b/bugsnag-android-core/build.gradle @@ -13,6 +13,10 @@ dokkaHtml.configure { dokkaSourceSets { named("main") { noAndroidSdkLink.set(false) + perPackageOption { + matchingRegex.set("com\\.bugsnag\\.android\\..*") + suppress.set(true) + } } } } From 8943350d9f9560f2f708f3a012dcd4c97fed2b34 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 25 Mar 2024 17:39:58 +0000 Subject: [PATCH 10/17] feat(feature flags): don't copy FeatureFlags on the crashing thread --- CHANGELOG.md | 7 + .../java/com/bugsnag/android/FeatureFlags.kt | 107 ++++++++++++--- .../com/bugsnag/android/FeatureFlagsTest.kt | 127 ++++++++++++++++++ 3 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ba52f58a..1c081e0398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## TBD + +### Enhancements + +* FeatureFlags are now a copy-on-write structure, and so don't need to be defensive copied on a crashing thread + [#2005](https://github.com/bugsnag/bugsnag-android/pull/2005) + ## 6.3.0 (2024-03-19) ### Enhancements diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt index efb57506ec..24de0276b2 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt @@ -1,42 +1,113 @@ package com.bugsnag.android import java.io.IOException +import kotlin.math.max -internal class FeatureFlags( - internal val store: MutableMap = mutableMapOf() +internal class FeatureFlags private constructor( + @Volatile + private var flags: Array ) : JsonStream.Streamable, FeatureFlagAware { - private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__" - @Synchronized override fun addFeatureFlag(name: String) { + /* + * Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are + * key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's + * internal locking) for us to be sure we are not adding duplicates. + * + * This class aims to have similar performance while also ensuring that the FeatureFlag object + * themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot + * operation for when an Event is created. + * + * It's assumed that *most* FeatureFlags will be added up-front, or during the normal app + * lifecycle (not during an Event). + * + * As such a copy-on-write structure allows an Event to simply capture a reference to the + * "snapshot" of FeatureFlags that were active when the Event was created. + */ + + constructor() : this(emptyArray()) + + override fun addFeatureFlag(name: String) { addFeatureFlag(name, null) } - @Synchronized override fun addFeatureFlag(name: String, variant: String?) { - store[name] = variant ?: emptyVariant + override fun addFeatureFlag(name: String, variant: String?) { + synchronized(this) { + val flagArray = flags + val index = flagArray.indexOfFirst { it.name == name } + + flags = when { + // this is a new FeatureFlag + index == -1 -> flagArray + FeatureFlag(name, variant) + + // this is a change to an existing FeatureFlag + flagArray[index].variant != variant -> flagArray.copyOf().also { + // replace the existing FeatureFlag in-place + it[index] = FeatureFlag(name, variant) + } + + // no actual change, so we return + else -> return + } + } } - @Synchronized override fun addFeatureFlags(featureFlags: Iterable) { - featureFlags.forEach { (name, variant) -> - addFeatureFlag(name, variant) + override fun addFeatureFlags(featureFlags: Iterable) { + synchronized(this) { + val flagArray = flags + + val newFlags = ArrayList( + // try to guess a reasonable upper-bound for the output array + if (featureFlags is Collection<*>) flagArray.size + featureFlags.size + else max(flagArray.size * 2, flagArray.size) + ) + + newFlags.addAll(flagArray) + + featureFlags.forEach { (name, variant) -> + val existingIndex = newFlags.indexOfFirst { it.name == name } + when (existingIndex) { + // add a new flag to the end of the list + -1 -> newFlags.add(FeatureFlag(name, variant)) + // replace the existing flag + else -> newFlags[existingIndex] = FeatureFlag(name, variant) + } + } + + flags = newFlags.toTypedArray() } } - @Synchronized override fun clearFeatureFlag(name: String) { - store.remove(name) + override fun clearFeatureFlag(name: String) { + synchronized(this) { + val flagArray = flags + val index = flagArray.indexOfFirst { it.name == name } + if (index == -1) { + return + } + + val out = arrayOfNulls(flagArray.size - 1) + flagArray.copyInto(out, 0, 0, index) + flagArray.copyInto(out, index, index + 1) + + @Suppress("UNCHECKED_CAST") + flags = out as Array + } } - @Synchronized override fun clearFeatureFlags() { - store.clear() + override fun clearFeatureFlags() { + synchronized(this) { + flags = emptyArray() + } } @Throws(IOException::class) override fun toStream(stream: JsonStream) { - val storeCopy = synchronized(this) { store.toMap() } + val storeCopy = flags stream.beginArray() storeCopy.forEach { (name, variant) -> stream.beginObject() stream.name("featureFlag").value(name) - if (variant != emptyVariant) { + if (variant != null) { stream.name("variant").value(variant) } stream.endObject() @@ -44,9 +115,7 @@ internal class FeatureFlags( stream.endArray() } - @Synchronized fun toList(): List = store.entries.map { (name, variant) -> - FeatureFlag(name, variant.takeUnless { it == emptyVariant }) - } + fun toList(): List = flags.map { (name, variant) -> FeatureFlag(name, variant) } - @Synchronized fun copy() = FeatureFlags(store.toMutableMap()) + fun copy() = FeatureFlags(flags) } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsTest.kt new file mode 100644 index 0000000000..f44d4114ae --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsTest.kt @@ -0,0 +1,127 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Before +import org.junit.Test + +class FeatureFlagsTest { + private lateinit var flags: FeatureFlags + + @Before + fun createEmptyFeatureFlags() { + flags = FeatureFlags() + } + + @Test + fun addDistinctFeatureFlag() { + flags.addFeatureFlag("empty") + flags.addFeatureFlag("keyWith", "value") + flags.addFeatureFlag("otherKey", "another value") + + assertEquals( + listOf( + FeatureFlag("empty"), + FeatureFlag("keyWith", "value"), + FeatureFlag("otherKey", "another value") + ), + flags.toList() + ) + } + + @Test + fun overwriteFeatureFlags() { + flags.addFeatureFlag("empty") + flags.addFeatureFlag("keyWith", "value") + flags.addFeatureFlag("otherKey", "another value") + + flags.addFeatureFlag("empty") + flags.addFeatureFlag("keyWith", "overwrite value") + flags.addFeatureFlag("newKey", "new value") + + flags.clearFeatureFlag("otherKey") + flags.clearFeatureFlag("no such key") + + assertEquals( + listOf( + FeatureFlag("empty"), + FeatureFlag("keyWith", "overwrite value"), + FeatureFlag("newKey", "new value") + ), + flags.toList() + ) + } + + @Test + fun clearFeatureFlags() { + // make sure clearing an empty table doesn't break anything + flags.clearFeatureFlags() + + flags.addFeatureFlag("empty") + flags.addFeatureFlag("keyWith", "value") + flags.addFeatureFlag("otherKey", "another value") + + flags.clearFeatureFlags() + + assertEquals(emptyList(), flags.toList()) + + // make sure that clearing the list doesn't break adding new flags afterwards + flags.addFeatureFlag("empty key") + flags.addFeatureFlag("keyWith", "magic value") + flags.addFeatureFlag("newKey", "new value") + + assertEquals( + listOf( + FeatureFlag("empty key"), + FeatureFlag("keyWith", "magic value"), + FeatureFlag("newKey", "new value") + ), + flags.toList() + ) + } + + @Test + fun addFeatureFlags() { + flags.addFeatureFlag("empty") + flags.addFeatureFlag("key1", "value1") + flags.addFeatureFlag("key2", "value2") + + flags.addFeatureFlags( + listOf( + FeatureFlag("key1", "overwrite value"), + FeatureFlag("bulk key1", "bulk value1"), + FeatureFlag("bulk key2", "bulk value2"), + ) + ) + + flags.addFeatureFlags( + sequence { + repeat(5) { index -> + yield(FeatureFlag("feature $index")) + } + }.asIterable() + ) + + val cloned = flags.copy() + assertNotSame(flags, cloned) + + // add an extra value and make sure it doesn't show up in 'cloned' + flags.addFeatureFlag("extra value", "extra value") + + assertEquals( + listOf( + FeatureFlag("empty"), + FeatureFlag("key1", "overwrite value"), + FeatureFlag("key2", "value2"), + FeatureFlag("bulk key1", "bulk value1"), + FeatureFlag("bulk key2", "bulk value2"), + FeatureFlag("feature 0"), + FeatureFlag("feature 1"), + FeatureFlag("feature 2"), + FeatureFlag("feature 3"), + FeatureFlag("feature 4") + ), + cloned.toList() + ) + } +} From 43e4cc8d2c8bc8b26f7b32ab2188170e9d605e4d Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:39:40 +0100 Subject: [PATCH 11/17] Allow early session starts in auto mode (#2006) * feat(session)Allow early session starts in auto mode * feat(session)Allow early session starts in auto mode * feat(session)End to end test * feat(feature flags): don't copy FeatureFlags on the crashing thread * feat(session)Allow early session starts in auto mode --------- Co-authored-by: jason --- CHANGELOG.md | 3 ++ bugsnag-android-core/detekt-baseline.xml | 1 - .../com/bugsnag/android/SessionTracker.java | 22 +++++++++++-- .../android/mazerunner/MazerunnerApp.kt | 1 + .../mazerunner/StartSessionBehaviour.kt | 32 +++++++++++++++++++ .../scenarios/StartSessionAutoModeScenario.kt | 27 ++++++++++++++++ features/smoke_tests/03_sessions.feature | 5 +++ 7 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartSessionBehaviour.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/StartSessionAutoModeScenario.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c081e0398..a71f93267c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ * FeatureFlags are now a copy-on-write structure, and so don't need to be defensive copied on a crashing thread [#2005](https://github.com/bugsnag/bugsnag-android/pull/2005) +* Allow `Bugsnag.startSession` to be called with automatic session tracking, and not have the first manual session be over written by the first automatic session. + [#2006](https://github.com/bugsnag/bugsnag-android/pull/2006) + ## 6.3.0 (2024-03-19) ### Enhancements diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index 1435111433..1e44fd786f 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -13,7 +13,6 @@ LongParameterList:Device.kt$Device$( buildInfo: DeviceBuildInfo, /** * The Application Binary Interface used */ var cpuAbi: Array<String>?, /** * Whether the device has been jailbroken */ var jailbroken: Boolean?, /** * A UUID generated by Bugsnag and used for the individual application on a device */ var id: String?, /** * The IETF language tag of the locale used */ var locale: String?, /** * The total number of bytes of memory on the device */ var totalMemory: Long?, /** * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ runtimeVersions: MutableMap<String, Any>? ) LongParameterList:DeviceBuildInfo.kt$DeviceBuildInfo$( val manufacturer: String?, val model: String?, val osVersion: String?, val apiLevel: Int?, val osBuild: String?, val fingerprint: String?, val tags: String?, val brand: String?, val cpuAbis: Array<String>? ) LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceId: String?, private val internalDeviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, private val bgTaskService: BackgroundTaskService, private val logger: Logger ) - LongParameterList:DeviceIdStore.kt$DeviceIdStore$( context: Context, deviceIdfile: File = File(context.filesDir, "device-id"), deviceIdGenerator: () -> UUID = { UUID.randomUUID() }, internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"), internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() }, private val sharedPrefMigrator: SharedPrefMigrator, 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:EventInternal.kt$EventInternal$( apiKey: String, logger: Logger, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<Pattern> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<Pattern>? = null ) 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 bb72ddc5dd..e9e15b2a2e 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 @@ -36,6 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi private volatile Session currentSession = null; final BackgroundTaskService backgroundTaskService; final Logger logger; + private boolean shouldSuppressFirstAutoSession = false; SessionTracker(ImmutableConfig configuration, CallbackState callbackState, @@ -76,7 +77,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi @VisibleForTesting Session startNewSession(@NonNull Date date, @Nullable User user, boolean autoCaptured) { - if (client.getConfig().shouldDiscardSession(autoCaptured)) { + if (shouldDiscardSession(autoCaptured)) { return null; } String id = UUID.randomUUID().toString(); @@ -92,12 +93,29 @@ Session startNewSession(@NonNull Date date, @Nullable User user, } Session startSession(boolean autoCaptured) { - if (client.getConfig().shouldDiscardSession(autoCaptured)) { + if (shouldDiscardSession(autoCaptured)) { return null; } return startNewSession(new Date(), client.getUser(), autoCaptured); } + private boolean shouldDiscardSession(boolean autoCaptured) { + if (client.getConfig().shouldDiscardSession(autoCaptured)) { + return true; + } else { + Session existingSession = currentSession; + if (autoCaptured + && existingSession != null + && !existingSession.isAutoCaptured() + && shouldSuppressFirstAutoSession) { + shouldSuppressFirstAutoSession = true; + return true; + } + } + return false; + } + + void pauseSession() { Session session = currentSession; 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 d8788d1a60..ad586c4f5d 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 @@ -10,6 +10,7 @@ class MazerunnerApp : Application() { super.onCreate() triggerStartupAnrIfRequired() setupNonSdkUsageStrictMode() + triggerManualSessionIfRequired() } /** diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartSessionBehaviour.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartSessionBehaviour.kt new file mode 100644 index 0000000000..b0c0ef4d07 --- /dev/null +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartSessionBehaviour.kt @@ -0,0 +1,32 @@ +package com.bugsnag.android.mazerunner + +import android.app.Application +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.EndpointConfiguration + +fun Application.triggerManualSessionIfRequired() { + val prefs = getSharedPreferences("SessionPreferences", android.content.Context.MODE_PRIVATE) + val manualSession = prefs.getBoolean("manualSession", false) + + if (manualSession) { + val notifyEndpoint = prefs.getString("notify", null) + val sessionsEndpoint = prefs.getString("sessions", null) + + // we remove the preferences so that we don't affect any future startup + prefs.edit() + .remove("notify") + .remove("sessions") + .remove("manualSession") + .commit() + + // we have to startup Bugsnag at this point + val config = Configuration.load(this) + if (!notifyEndpoint.isNullOrBlank() && !sessionsEndpoint.isNullOrBlank()) { + config.endpoints = EndpointConfiguration(notifyEndpoint, sessionsEndpoint) + } + + Bugsnag.start(this, config) + Bugsnag.startSession() + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/StartSessionAutoModeScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/StartSessionAutoModeScenario.kt new file mode 100644 index 0000000000..f157fbdac8 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/StartSessionAutoModeScenario.kt @@ -0,0 +1,27 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Configuration +import kotlin.system.exitProcess + +internal class StartSessionAutoModeScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + override fun startScenario() { + context.applicationContext + .getSharedPreferences("SessionPreferences", Context.MODE_PRIVATE) + .edit() + .putBoolean("manualSession", MANUAL_START) + .putString("notify", config.endpoints.notify) + .putString("sessions", config.endpoints.sessions) + .commit() + + exitProcess(0) + } + + companion object { + private const val MANUAL_START = true + } +} diff --git a/features/smoke_tests/03_sessions.feature b/features/smoke_tests/03_sessions.feature index 54a9701f5f..3033fed8bf 100644 --- a/features/smoke_tests/03_sessions.feature +++ b/features/smoke_tests/03_sessions.feature @@ -112,3 +112,8 @@ Feature: Session functionality smoke tests And the event "user.email" equals "ABC.CBA.CA" And the event "user.name" equals "ManualSessionSmokeScenario" + Scenario: Start session in auto mode + When I clear any error dialogue + And I run "StartSessionAutoModeScenario" + And I relaunch the app after a crash + Then I wait to receive a session \ No newline at end of file From 081d19498645ab8fc2d074e02ea6edbee2a71280 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 9 Apr 2024 17:58:56 +0100 Subject: [PATCH 12/17] fix(json): make the default ObjectJsonStreamer.redactedKeys a static shared value (it's immutable) --- CHANGELOG.md | 2 ++ .../src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a71f93267c..62fd3e1c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * FeatureFlags are now a copy-on-write structure, and so don't need to be defensive copied on a crashing thread [#2005](https://github.com/bugsnag/bugsnag-android/pull/2005) +* The default redactedKeys in ObjectJsonStreamer is now static and shared, reducing the overhead of some leaveBreadcrumb calls (those with complex metadata) + []() * Allow `Bugsnag.startSession` to be called with automatic session tracking, and not have the first manual session be over written by the first automatic session. [#2006](https://github.com/bugsnag/bugsnag-android/pull/2006) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt index 5df61b8ae9..abcb927c77 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt @@ -11,9 +11,11 @@ internal class ObjectJsonStreamer { companion object { internal const val REDACTED_PLACEHOLDER = "[REDACTED]" internal const val OBJECT_PLACEHOLDER = "[OBJECT]" + + internal val DEFAULT_REDACTED_KEYS = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE)) } - var redactedKeys = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE)) + var redactedKeys = DEFAULT_REDACTED_KEYS // Write complex/nested values to a JsonStreamer @Throws(IOException::class) From c9fa73870398f66ce3b8fd8b09b662c6ba4e7446 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 9 Apr 2024 15:26:54 +0100 Subject: [PATCH 13/17] fix(ndk): bugsnag_refresh_symbol_table is now protected by a soft-lock to avoid possible double-free conditions --- CHANGELOG.md | 5 +++++ .../src/main/jni/utils/stack_unwinder.cpp | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fd3e1c08..06bb69afe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ * Allow `Bugsnag.startSession` to be called with automatic session tracking, and not have the first manual session be over written by the first automatic session. [#2006](https://github.com/bugsnag/bugsnag-android/pull/2006) +### Bug fixes + +* Refreshing the NDK symbol table is now protected by a soft-lock to avoid possible double-free conditions + [#2007](https://github.com/bugsnag/bugsnag-android/pull/2007) + ## 6.3.0 (2024-03-19) ### Enhancements diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp index 5ff5a36d02..411b699e33 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/stack_unwinder.cpp @@ -17,6 +17,9 @@ static unwindstack::Unwinder *crash_time_unwinder; // attempting to unwind. This isn't a "real" lock to avoid deadlocking in the // event of a crash while handling an ANR or the reverse. static std::atomic_bool unwinding_crash_stack = ATOMIC_VAR_INIT(false); +// soft lock for refreshing the symbol tables - if active, bsg_unwinder_refresh +// will refresh without doing any work avoiding possible reentrancy problems +static std::atomic_bool refreshing_unwinder = ATOMIC_VAR_INIT(false); // Thread-safe, reusable unwinder - uses thread-specific memory caches static unwindstack::LocalUnwinder *current_time_unwinder; @@ -122,6 +125,12 @@ static void populate_code_identifier(const unwindstack::FrameData &frame, } void bsg_unwinder_refresh(void) { + bool expected = false; + if (!std::atomic_compare_exchange_strong(&refreshing_unwinder, &expected, + true)) { + return; + } + auto crash_time_maps = new unwindstack::LocalUpdatableMaps(); if (crash_time_maps->Parse()) { std::shared_ptr crash_time_memory( @@ -137,11 +146,13 @@ void bsg_unwinder_refresh(void) { crash_time_unwinder = new_uwinder; // don't destroy an unwinder that is currently in use - if (!unwinding_crash_stack.load()) { + if (!std::atomic_load(&unwinding_crash_stack)) { delete old_unwinder->GetMaps(); delete old_unwinder; } } + + std::atomic_store(&refreshing_unwinder, false); } ssize_t bsg_unwind_crash_stack(bugsnag_stackframe stack[BUGSNAG_FRAMES_MAX], From da031be9cda2b4a97acec6c7ce075a3b457dd22b Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 10 Apr 2024 09:04:56 +0100 Subject: [PATCH 14/17] chore(changelog): corrected the PR link for #2008 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06bb69afe9..5ffe7103bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * FeatureFlags are now a copy-on-write structure, and so don't need to be defensive copied on a crashing thread [#2005](https://github.com/bugsnag/bugsnag-android/pull/2005) * The default redactedKeys in ObjectJsonStreamer is now static and shared, reducing the overhead of some leaveBreadcrumb calls (those with complex metadata) - []() + [#2008](https://github.com/bugsnag/bugsnag-android/pull/2008) * Allow `Bugsnag.startSession` to be called with automatic session tracking, and not have the first manual session be over written by the first automatic session. [#2006](https://github.com/bugsnag/bugsnag-android/pull/2006) From dce1c8c09a93eba39e13fc3bda2ec6cfb8b56166 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 10 Apr 2024 16:14:04 +0100 Subject: [PATCH 15/17] fix(react-native): allow strings or numbers to be used for thread-id --- CHANGELOG.md | 1 - .../bugsnag/android/ThreadDeserializer.java | 5 +- .../bugsnag/android/ThreadDeserializerTest.kt | 56 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffe7103bb..95fcb8b30d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ [#2005](https://github.com/bugsnag/bugsnag-android/pull/2005) * The default redactedKeys in ObjectJsonStreamer is now static and shared, reducing the overhead of some leaveBreadcrumb calls (those with complex metadata) [#2008](https://github.com/bugsnag/bugsnag-android/pull/2008) - * Allow `Bugsnag.startSession` to be called with automatic session tracking, and not have the first manual session be over written by the first automatic session. [#2006](https://github.com/bugsnag/bugsnag-android/pull/2006) diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java index c2768e0e09..bd600fb194 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java @@ -27,8 +27,11 @@ public Thread deserialize(Map map) { Boolean errorReportingThread = MapUtils.getOrNull(map, "errorReportingThread"); errorReportingThread = errorReportingThread == null ? false : errorReportingThread; + + Object threadId = MapUtils.getOrThrow(map, "id"); + return new Thread( - MapUtils.getOrThrow(map, "id").toString(), + threadId.toString(), MapUtils.getOrThrow(map, "name"), ErrorType.valueOf(type.toUpperCase(Locale.US)), errorReportingThread, diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt index 39b4b75942..ce30eb5ee9 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt @@ -2,37 +2,28 @@ package com.bugsnag.android import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test -import java.util.HashMap class ThreadDeserializerTest { - private val map = HashMap() - - /** - * Generates a map for verifying the serializer - */ - @Before - fun setup() { - val frame = HashMap() - frame["method"] = "foo()" - frame["file"] = "Bar.kt" - frame["lineNumber"] = 29 - frame["inProject"] = true - - map["stacktrace"] = listOf(frame) - map["id"] = 52L - map["type"] = "reactnativejs" - map["name"] = "thread-worker-02" - map["state"] = "RUNNABLE" - map["errorReportingThread"] = true + @Test + fun deserialize() { + val thread = ThreadDeserializer(StackframeDeserializer(), object : Logger {}) + .deserialize(createThreadAsMap()) + assertEquals("thread-id-as-string", thread.id) + assertCommonThreadContent(thread) } @Test - fun deserialize() { - val thread = ThreadDeserializer(StackframeDeserializer(), object : Logger {}).deserialize(map) + fun deserializeLegacyThread() { + val thread = + ThreadDeserializer(StackframeDeserializer(), object : Logger {}) + .deserialize(createLegacyThreadAsMap()) assertEquals("52", thread.id) + assertCommonThreadContent(thread) + } + + private fun assertCommonThreadContent(thread: Thread) { assertEquals(ErrorType.REACTNATIVEJS, thread.type) assertEquals("thread-worker-02", thread.name) assertTrue(thread.errorReportingThread) @@ -43,4 +34,23 @@ class ThreadDeserializerTest { assertEquals(29, frame.lineNumber) assertTrue(frame.inProject as Boolean) } + + private fun createLegacyThreadAsMap(): Map = hashMapOf( + "stacktrace" to listOf( + hashMapOf( + "method" to "foo()", + "file" to "Bar.kt", + "lineNumber" to 29, + "inProject" to true + ) + ), + "id" to 52L, + "type" to "reactnativejs", + "name" to "thread-worker-02", + "state" to "RUNNABLE", + "errorReportingThread" to true + ) + + private fun createThreadAsMap(): Map = + createLegacyThreadAsMap() + mapOf("id" to "thread-id-as-string") } From 61d30007ef777abc93167bebfb5be655d563d0e0 Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:29:25 +0100 Subject: [PATCH 16/17] fix(test) null check for NDK internal metrics (#2012) --- .../src/main/jni/internal_metrics.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c index 8a329ea946..d30ab63100 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/internal_metrics.c @@ -100,7 +100,7 @@ static uint64_t get_called_api_array_slot_bit(bsg_called_api api) { } void bsg_notify_api_called(bugsnag_event *event, bsg_called_api api) { - if (!internal_metrics_enabled) { + if (!internal_metrics_enabled || event == NULL) { return; } @@ -157,7 +157,7 @@ static void bsg_modify_callback_count(bugsnag_event *event, const char *api, void bsg_set_callback_count(bugsnag_event *event, const char *api, int32_t count) { - if (!internal_metrics_enabled) { + if (!internal_metrics_enabled || event == NULL) { return; } @@ -187,7 +187,7 @@ void bsg_set_callback_count(bugsnag_event *event, const char *api, } void bsg_notify_add_callback(bugsnag_event *event, const char *api) { - if (!internal_metrics_enabled) { + if (!internal_metrics_enabled || event == NULL) { return; } @@ -195,7 +195,7 @@ void bsg_notify_add_callback(bugsnag_event *event, const char *api) { } void bsg_notify_remove_callback(bugsnag_event *event, const char *api) { - if (!internal_metrics_enabled) { + if (!internal_metrics_enabled || event == NULL) { return; } From 2f2fc15d822c581384b836ffeeeb989a3b142924 Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Mon, 15 Apr 2024 09:24:32 +0100 Subject: [PATCH 17/17] release/v6.4.0 --- CHANGELOG.md | 2 +- .../src/main/java/com/bugsnag/android/Notifier.kt | 2 +- examples/sdk-app-example/app/build.gradle | 4 ++-- gradle.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95fcb8b30d..6cecf42f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## TBD +## 6.4.0 (2024-04-15) ### Enhancements 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 e1c63593b5..bea14c68a8 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 = "6.3.0", + var version: String = "6.4.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index a973735833..d03a5b6c74 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -41,8 +41,8 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:6.3.0" - implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.3.0" + implementation "com.bugsnag:bugsnag-android:6.4.0" + implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.4.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.4.0" implementation "com.google.android.material:material:1.4.0" diff --git a/gradle.properties b/gradle.properties index e1bb90b391..73dfdc3f4e 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=6.3.0 +VERSION_NAME=6.4.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git