From 85c58d38d7091bdb076ce043aa57ee331bcee917 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 1 Nov 2021 13:37:57 +0000 Subject: [PATCH 01/37] feat: added `FeatureFlag` and `FeatureFlagAware` to define the basic structures for feature flags to be implemented (#1468) --- .../java/com/bugsnag/android/FeatureFlag.java | 134 ++++++++++++++++++ .../com/bugsnag/android/FeatureFlagAware.java | 49 +++++++ .../com/bugsnag/android/FeatureFlagTest.java | 65 +++++++++ 3 files changed, 248 insertions(+) create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlag.java create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagAware.java create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagTest.java diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlag.java new file mode 100644 index 0000000000..39bef789b8 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlag.java @@ -0,0 +1,134 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +/** + * Represents a single feature-flag / experiment marker within Bugsnag. Each {@code FeatureFlag} + * object has a {@link #getName() name} and an optional {@link #getVariant() variant} which can be + * used to identify runtime experiments and groups when reporting errors. + * + * @see Bugsnag#addFeatureFlag(String, String) + * @see Event#addFeatureFlag(String, String) + */ +public final class FeatureFlag implements Map.Entry { + private final String name; + + private final String variant; + + /** + * Create a named {@code FeatureFlag} with no variant + * + * @param name the identifying name of the new {@code FeatureFlag} (not {@code null}) + * @see Bugsnag#addFeatureFlag(String) + * @see Event#addFeatureFlag(String) + */ + public FeatureFlag(@NonNull String name) { + this(name, null); + } + + /** + * Create a new {@code FeatureFlag} with a name and (optionally) a variant. + * + * @param name the identifying name of the new {@code FeatureFlag} (not {@code null}) + * @param variant the feature variant + */ + public FeatureFlag(@NonNull String name, @Nullable String variant) { + if (name == null) { + throw new NullPointerException("FeatureFlags cannot have null name"); + } + + this.name = name; + this.variant = variant; + } + + /** + * Create a new {@code FeatureFlag} based on an existing {@code Map.Entry}. This is the same + * as {@code new FeatureFlag(mapEntry.getKey(), mapEntry.getValue())}. + * + * @param mapEntry an existing {@code Map.Entry} to copy the feature flag from + */ + public FeatureFlag(@NonNull Map.Entry mapEntry) { + this(mapEntry.getKey(), mapEntry.getValue()); + } + + @NonNull + public String getName() { + return name; + } + + @Nullable + public String getVariant() { + return variant; + } + + /** + * Same as {@link #getName()}. + * + * @return the name of this {@code FeatureFlag} + * @see #getName() + */ + @NonNull + @Override + public String getKey() { + return name; + } + + /** + * Same as {@link #getVariant()}. + * + * @return the variant of this {@code FeatureFlag} (may be {@code null}) + * @see #getVariant() + */ + @Nullable + @Override + public String getValue() { + return variant; + } + + /** + * Throws {@code UnsupportedOperationException} as {@code FeatureFlag} is considered immutable. + * + * @param value ignored + * @return nothing + */ + @Override + @Nullable + public String setValue(@Nullable String value) { + throw new UnsupportedOperationException("FeatureFlag is immutable"); + } + + @Override + public int hashCode() { + // Follows the Map.Entry contract + return getKey().hashCode() ^ (getValue() == null ? 0 : getValue().hashCode()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + // This follows the contract defined in Map.Entry exactly + if (!(other instanceof Map.Entry)) { + return false; + } + + Map.Entry e2 = + (Map.Entry) other; + + return getKey().equals(e2.getKey()) + && (getValue() == null ? e2.getValue() == null : getValue().equals(e2.getValue())); + } + + @Override + public String toString() { + return "FeatureFlag{" + + "name='" + name + '\'' + + ", variant='" + variant + '\'' + + '}'; + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagAware.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagAware.java new file mode 100644 index 0000000000..b2571da1c9 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagAware.java @@ -0,0 +1,49 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +interface FeatureFlagAware { + /** + * Add a single feature flag with no variant. If there is an existing feature flag with the + * same name, it will be overwritten to have no variant. + * + * @param name the name of the feature flag to add + * @see #addFeatureFlag(String, String) + */ + void addFeatureFlag(@NonNull String name); + + /** + * Add a single feature flag with an optional variant. If there is an existing feature + * flag with the same name, it will be overwritten with the new variant. If the variant is + * {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}. + * + * @param name the name of the feature flag to add + * @param variant the variant to set the feature flag to, or {@code null} to specify a feature + * flag with no variant + */ + void addFeatureFlag(@NonNull String name, @Nullable String variant); + + /** + * Add a collection of feature flags. This method behaves exactly the same as calling + * {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects. + * + * @param featureFlags the feature flags to add + * @see #addFeatureFlag(String, String) + */ + void addFeatureFlags(@NonNull Iterable featureFlags); + + /** + * Remove a single feature flag regardless of its current status. This will stop the specified + * feature flag from being reported. If the named feature flag does not exist this will + * have no effect. + * + * @param name the name of the feature flag to remove + */ + void clearFeatureFlag(@NonNull String name); + + /** + * Clear all of the feature flags. This will stop all feature flags from being reported. + */ + void clearFeatureFlags(); +} \ No newline at end of file diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagTest.java new file mode 100644 index 0000000000..6a72a4d837 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagTest.java @@ -0,0 +1,65 @@ +package com.bugsnag.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Map; + +public class FeatureFlagTest { + @Test + public void mapEntryContractEquals() { + FeatureFlag flag = new FeatureFlag("sample_group", "a"); + Map.Entry entry = new SimpleImmutableEntry<>(flag); + + // we want to task our own equals method, so we write this by hand: + assertTrue(flag.equals(entry)); + } + + @Test + public void mapEntryContractHashCode() { + FeatureFlag flag = new FeatureFlag("sample_group", "a"); + Map.Entry entry = new SimpleImmutableEntry<>(flag); + + assertEquals(entry.hashCode(), flag.hashCode()); + } + + @Test + public void featureFlagFromMapEntry() { + Map.Entry entry = new SimpleImmutableEntry<>("sample_group", "1"); + FeatureFlag flag = new FeatureFlag(entry); + + assertEquals("sample_group", flag.getName()); + assertEquals("1", flag.getVariant()); + } + + @Test + public void featureFlagFromMapEntryWithNoValue() { + Map.Entry entry = new SimpleImmutableEntry<>("sample_group", null); + FeatureFlag flag = new FeatureFlag(entry); + + assertEquals("sample_group", flag.getName()); + assertNull(flag.getVariant()); + } + + @Test + public void featureFlagWithNullVariant() { + FeatureFlag flag = new FeatureFlag("demo"); + assertEquals("demo", flag.getName()); + assertNull(flag.getVariant()); + } + + @Test(expected = NullPointerException.class) + public void nullNameFails() { + new FeatureFlag((String) null); + } + + @Test(expected = UnsupportedOperationException.class) + public void setValueFails() { + FeatureFlag flag = new FeatureFlag("sample_group", "a"); + flag.setValue("b"); + } +} From 29abb792b2998edbba3d255388a3fe6fdd68d839 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 2 Nov 2021 09:28:59 +0000 Subject: [PATCH 02/37] feat: implemented `FeatureFlags` and `FeatureFlagState` implementing the `FeatureFlagAware` interface (#1469) --- .../com/bugsnag/android/FeatureFlagState.kt | 41 ++++++++ .../java/com/bugsnag/android/FeatureFlags.kt | 52 ++++++++++ .../java/com/bugsnag/android/StateEvent.kt | 11 +++ .../bugsnag/android/FeatureFlagStateTest.kt | 96 +++++++++++++++++++ .../android/FeatureFlagsSerializationTest.kt | 45 +++++++++ .../feature_flags_serialization_0.json | 13 +++ .../feature_flags_serialization_1.json | 9 ++ .../feature_flags_serialization_2.json | 1 + 8 files changed, 268 insertions(+) create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsSerializationTest.kt create mode 100644 bugsnag-android-core/src/test/resources/feature_flags_serialization_0.json create mode 100644 bugsnag-android-core/src/test/resources/feature_flags_serialization_1.json create mode 100644 bugsnag-android-core/src/test/resources/feature_flags_serialization_2.json diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt new file mode 100644 index 0000000000..82425074d8 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt @@ -0,0 +1,41 @@ +package com.bugsnag.android + +internal data class FeatureFlagState( + private val featureFlags: FeatureFlags = FeatureFlags() +) : BaseObservable(), FeatureFlagAware { + override fun addFeatureFlag(name: String) { + this.featureFlags.addFeatureFlag(name) + updateState { + StateEvent.AddFeatureFlag(name) + } + } + + override fun addFeatureFlag(name: String, variant: String?) { + this.featureFlags.addFeatureFlag(name, variant) + updateState { + StateEvent.AddFeatureFlag(name, variant) + } + } + + override fun addFeatureFlags(featureFlags: Iterable) { + featureFlags.forEach { (name, variant) -> + addFeatureFlag(name, variant) + } + } + + override fun clearFeatureFlag(name: String) { + this.featureFlags.clearFeatureFlag(name) + updateState { + StateEvent.ClearFeatureFlag(name) + } + } + + override fun clearFeatureFlags() { + this.featureFlags.clearFeatureFlags() + updateState { + StateEvent.ClearFeatureFlags + } + } + + fun toList(): List = featureFlags.toList() +} 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 new file mode 100644 index 0000000000..bce01bff5d --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlags.kt @@ -0,0 +1,52 @@ +package com.bugsnag.android + +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap + +internal class FeatureFlags( + internal val store: MutableMap = ConcurrentHashMap() +) : JsonStream.Streamable, FeatureFlagAware { + private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__" + + override fun addFeatureFlag(name: String) { + store[name] = emptyVariant + } + + override fun addFeatureFlag(name: String, variant: String?) { + store[name] = variant ?: emptyVariant + } + + override fun addFeatureFlags(featureFlags: Iterable) { + featureFlags.forEach { (name, variant) -> + addFeatureFlag(name, variant) + } + } + + override fun clearFeatureFlag(name: String) { + store.remove(name) + } + + override fun clearFeatureFlags() { + store.clear() + } + + @Throws(IOException::class) + override fun toStream(stream: JsonStream) { + stream.beginArray() + store.forEach { (name, variant) -> + stream.beginObject() + stream.name("featureFlag").value(name) + if (variant != emptyVariant) { + stream.name("variant").value(variant) + } + stream.endObject() + } + stream.endArray() + } + + fun toList(): List = store.entries.map { (name, variant) -> + FeatureFlag(name, variant.takeUnless { it == emptyVariant }) + } + + fun copy() = FeatureFlags(store.toMutableMap()) +} 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 83ba82108e..729c9b9fb7 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 @@ -68,4 +68,15 @@ sealed class StateEvent { // JvmField allows direct field access optimizations @JvmField val memoryTrimLevel: Int? = null, @JvmField val memoryTrimLevelDescription: String = "None" ) : StateEvent() + + class AddFeatureFlag( + @JvmField val name: String, + @JvmField val variant: String? = null + ) : StateEvent() + + class ClearFeatureFlag( + @JvmField val name: String + ) : StateEvent() + + object ClearFeatureFlags : StateEvent() } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt new file mode 100644 index 0000000000..62b0c69d46 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt @@ -0,0 +1,96 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.StateObserver +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class FeatureFlagStateTest { + lateinit var events: MutableList + lateinit var state: FeatureFlagState + + @Before + fun setUp() { + events = mutableListOf() + state = FeatureFlagState() + state.addObserver(StateObserver { events.add(it) }) + } + + @Test + fun addFeatureFlag_NoVariant() { + state.addFeatureFlag("demo_mode") + + val featureFlag = state.toList().single() + assertEquals(FeatureFlag("demo_mode"), featureFlag) + + val event = events.single() as StateEvent.AddFeatureFlag + assertEquals("demo_mode", event.name) + assertNull(event.variant) + } + + @Test + fun addFeatureFlag() { + state.addFeatureFlag("sample_group", "a") + + val featureFlag = state.toList().single() + assertEquals(FeatureFlag("sample_group", "a"), featureFlag) + + val event = events.single() as StateEvent.AddFeatureFlag + assertEquals("sample_group", event.name) + assertEquals("a", event.variant) + } + + @Test + fun addFeatureFlags() { + val flags = listOf( + FeatureFlag("demo_mode"), + FeatureFlag("sample_group", "1234") + ) + + state.addFeatureFlags(flags) + + val featureFlags = state.toList() + assertEquals(2, featureFlags.size) + assertTrue(featureFlags.containsAll(flags)) + + assertEquals(2, events.size) + assertTrue(events[0] is StateEvent.AddFeatureFlag) + assertTrue(events[1] is StateEvent.AddFeatureFlag) + } + + @Test + fun clearFeatureFlag() { + state.addFeatureFlag("sample_group", "4321") + state.clearFeatureFlag("sample_group") + + val featureFlags = state.toList() + assertEquals(0, featureFlags.size) + + assertEquals(2, events.size) + val addEvent = events.find { it is StateEvent.AddFeatureFlag } as StateEvent.AddFeatureFlag + assertEquals("sample_group", addEvent.name) + assertEquals("4321", addEvent.variant) + + assertNotNull(events.find { it is StateEvent.ClearFeatureFlag }) + } + + @Test + fun clearFeatureFlags() { + state.addFeatureFlag("sample_group", "4321") + state.addFeatureFlag("listing_view", "legacy") + state.addFeatureFlag("demo_mode") + state.clearFeatureFlags() + + val featureFlags = state.toList() + assertEquals(0, featureFlags.size) + + assertEquals(4, events.size) + val addEvents = events.filterIsInstance() + assertEquals(3, addEvents.size) + + assertNotNull(events.find { it is StateEvent.ClearFeatureFlags }) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsSerializationTest.kt new file mode 100644 index 0000000000..770c57d807 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagsSerializationTest.kt @@ -0,0 +1,45 @@ +package com.bugsnag.android + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +internal class FeatureFlagsSerializationTest { + companion object { + @JvmStatic + @Parameterized.Parameters + fun testCases() = generateSerializationTestCases( + "feature_flags", + basic(), + overrideVariants(), + cleared() + ) + + private fun basic() = FeatureFlags().apply { + addFeatureFlag("demo_mode") + addFeatureFlag("sample_group", "a") + addFeatureFlag("view_mode", "modern") + } + + private fun overrideVariants() = FeatureFlags().apply { + addFeatureFlag("demo_mode") + addFeatureFlag("sample_group", "a") + addFeatureFlag("sample_group", "b") + } + + private fun cleared() = FeatureFlags().apply { + addFeatureFlag("demo_mode") + addFeatureFlag("sample_group", "a") + addFeatureFlag("view_mode", "modern") + + clearFeatureFlags() + } + } + + @Parameterized.Parameter + lateinit var testCase: Pair + + @Test + fun testJsonSerialisation() = verifyJsonMatches(testCase.first, testCase.second) +} diff --git a/bugsnag-android-core/src/test/resources/feature_flags_serialization_0.json b/bugsnag-android-core/src/test/resources/feature_flags_serialization_0.json new file mode 100644 index 0000000000..0cf85a47a1 --- /dev/null +++ b/bugsnag-android-core/src/test/resources/feature_flags_serialization_0.json @@ -0,0 +1,13 @@ +[ + { + "featureFlag": "sample_group", + "variant": "a" + }, + { + "featureFlag": "view_mode", + "variant": "modern" + }, + { + "featureFlag": "demo_mode" + } +] diff --git a/bugsnag-android-core/src/test/resources/feature_flags_serialization_1.json b/bugsnag-android-core/src/test/resources/feature_flags_serialization_1.json new file mode 100644 index 0000000000..662fbe4a0a --- /dev/null +++ b/bugsnag-android-core/src/test/resources/feature_flags_serialization_1.json @@ -0,0 +1,9 @@ +[ + { + "featureFlag": "sample_group", + "variant": "b" + }, + { + "featureFlag": "demo_mode" + } +] diff --git a/bugsnag-android-core/src/test/resources/feature_flags_serialization_2.json b/bugsnag-android-core/src/test/resources/feature_flags_serialization_2.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/bugsnag-android-core/src/test/resources/feature_flags_serialization_2.json @@ -0,0 +1 @@ +[] \ No newline at end of file From 2ea0fa5cb33bed4f35320b8c5ce8dd3f095e6a06 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 3 Nov 2021 12:10:14 +0000 Subject: [PATCH 03/37] Add FeatureFlag support to Client & Configuration (#1474) * feat: implemented `FeatureFlags` and `FeatureFlagState` implementing the `FeatureFlagAware` interface * feat(feature flags): added FeatureFlag capabilities to Client and Configuration --- .../com/bugsnag/android/BugsnagTestUtils.java | 10 +++ .../java/com/bugsnag/android/ClientTest.java | 29 ++++++++- .../java/com/bugsnag/android/Bugsnag.java | 53 ++++++++++++++++ .../com/bugsnag/android/BugsnagStateModule.kt | 2 + .../main/java/com/bugsnag/android/Client.java | 62 ++++++++++++++++++- .../com/bugsnag/android/ConfigInternal.kt | 15 ++++- .../com/bugsnag/android/Configuration.java | 58 ++++++++++++++++- .../com/bugsnag/android/FeatureFlagState.kt | 1 + .../com/bugsnag/android/ClientFacadeTest.java | 4 ++ 9 files changed, 230 insertions(+), 4 deletions(-) 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 92ac16e01e..f119eb1211 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 @@ -14,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.io.StringWriter; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -132,4 +133,13 @@ public static AppWithState generateAppWithState() { public static App generateApp() { return new App(generateImmutableConfig(), null, null, null, null, null); } + + static Comparator featureFlagComparator() { + return new Comparator() { + @Override + public int compare(FeatureFlag f1, FeatureFlag f2) { + return f1.getName().compareTo(f2.getName()); + } + }; + } } 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 0baef6b629..0cf1db1e2c 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 @@ -237,7 +237,7 @@ public void testMetadataCloned() { @Test public void testClientMultiNotify() throws InterruptedException { // concurrently call notify() - client = BugsnagTestUtils.generateClient(); + client = BugsnagTestUtils.generateClient(); Executor executor = Executors.newSingleThreadExecutor(); int count = 200; final CountDownLatch latch = new CountDownLatch(count); @@ -257,4 +257,31 @@ public void run() { // wait for all events to be delivered assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @Test + public void testFeatureFlagsCloned() { + config.addFeatureFlag("sample_group", "a"); + client = new Client(context, config); + client.addFeatureFlag("demo_mode"); + + assertNotSame(config.impl.featureFlagState, client.featureFlagState); + + FeatureFlagState configFlags = config.impl.featureFlagState; + FeatureFlagState clientFlags = client.featureFlagState; + assertNotSame(configFlags, clientFlags); + + List configExpected = Collections.singletonList( + new FeatureFlag("sample_group", "a")); + assertEquals(configExpected, config.impl.featureFlagState.toList()); + + List clientExpected = Arrays.asList( + new FeatureFlag("demo_mode"), + new FeatureFlag("sample_group", "a") + ); + + List clientActual = client.featureFlagState.toList(); + Collections.sort(clientActual, BugsnagTestUtils.featureFlagComparator()); + + assertEquals(clientExpected, clientActual); + } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index 74926ecd8b..8ce1253a08 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -404,6 +404,59 @@ public static void markLaunchCompleted() { getClient().markLaunchCompleted(); } + /** + * Add a single feature flag with no variant. If there is an existing feature flag with the + * same name, it will be overwritten to have no variant. + * + * @param name the name of the feature flag to add + * @see #addFeatureFlag(String, String) + */ + public static void addFeatureFlag(@NonNull String name) { + getClient().addFeatureFlag(name); + } + + /** + * Add a single feature flag with an optional variant. If there is an existing feature + * flag with the same name, it will be overwritten with the new variant. If the variant is + * {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}. + * + * @param name the name of the feature flag to add + * @param variant the variant to set the feature flag to, or {@code null} to specify a feature + * flag with no variant + */ + public static void addFeatureFlag(@NonNull String name, @Nullable String variant) { + getClient().addFeatureFlag(name, variant); + } + + /** + * Add a collection of feature flags. This method behaves exactly the same as calling + * {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects. + * + * @param featureFlags the feature flags to add + * @see #addFeatureFlag(String, String) + */ + public static void addFeatureFlags(@NonNull Iterable featureFlags) { + getClient().addFeatureFlags(featureFlags); + } + + /** + * Remove a single feature flag regardless of its current status. This will stop the specified + * feature flag from being reported. If the named feature flag does not exist this will + * have no effect. + * + * @param name the name of the feature flag to remove + */ + public static void clearFeatureFlag(@NonNull String name) { + getClient().clearMetadata(name); + } + + /** + * Clear all of the feature flags. This will stop all feature flags from being reported. + */ + public static void clearFeatureFlags() { + getClient().clearFeatureFlags(); + } + /** * Get the current Bugsnag Client instance. */ diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt index 50377ad59c..ae8db30b52 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt @@ -28,6 +28,8 @@ internal class BugsnagStateModule( val metadataState = copyMetadataState(configuration) + val featureFlagState = configuration.impl.featureFlagState.copy() + private fun copyMetadataState(configuration: Configuration): MetadataState { // performs deep copy of metadata to preserve immutability of Configuration interface val orig = configuration.impl.metadataState.metadata 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 e31df68f9f..07534029a0 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 @@ -41,11 +41,12 @@ * @see Bugsnag */ @SuppressWarnings({"checkstyle:JavadocTagContinuationIndentation", "ConstantConditions"}) -public class Client implements MetadataAware, CallbackAware, UserAware { +public class Client implements MetadataAware, CallbackAware, UserAware, FeatureFlagAware { final ImmutableConfig immutableConfig; final MetadataState metadataState; + final FeatureFlagState featureFlagState; private final ContextState contextState; private final CallbackState callbackState; @@ -152,6 +153,7 @@ public Unit invoke(Boolean hasConnection, String networkState) { breadcrumbState = bugsnagStateModule.getBreadcrumbState(); contextState = bugsnagStateModule.getContextState(); metadataState = bugsnagStateModule.getMetadataState(); + featureFlagState = bugsnagStateModule.getFeatureFlagState(); // lookup system services final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule); @@ -222,6 +224,7 @@ public Unit invoke(Boolean hasConnection, String networkState) { ContextState contextState, CallbackState callbackState, UserState userState, + FeatureFlagState featureFlagState, ClientObservable clientObservable, Context appContext, @NonNull DeviceDataCollector deviceDataCollector, @@ -243,6 +246,7 @@ public Unit invoke(Boolean hasConnection, String networkState) { this.contextState = contextState; this.callbackState = callbackState; this.userState = userState; + this.featureFlagState = featureFlagState; this.clientObservable = clientObservable; this.appContext = appContext; this.deviceDataCollector = deviceDataCollector; @@ -922,6 +926,62 @@ void leaveAutoBreadcrumb(@NonNull String message, } } + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + featureFlagState.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + featureFlagState.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + featureFlagState.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + featureFlagState.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + featureFlagState.clearFeatureFlags(); + } + /** * Retrieves information about the last launch of the application, if it has been run before. * 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 15e4526236..5ad4db15d3 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 @@ -3,7 +3,9 @@ package com.bugsnag.android import android.content.Context import java.io.File -internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware, UserAware { +internal class ConfigInternal( + var apiKey: String +) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware { private var user = User() @@ -13,6 +15,9 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware @JvmField internal val metadataState: MetadataState = MetadataState() + @JvmField + internal val featureFlagState: FeatureFlagState = FeatureFlagState() + var appVersion: String? = null var versionCode: Int? = 0 var releaseStage: String? = null @@ -71,6 +76,14 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware override fun getMetadata(section: String) = metadataState.getMetadata(section) override fun getMetadata(section: String, key: String) = metadataState.getMetadata(section, key) + override fun addFeatureFlag(name: String) = featureFlagState.addFeatureFlag(name) + override fun addFeatureFlag(name: String, variant: String?) = + featureFlagState.addFeatureFlag(name, variant) + override fun addFeatureFlags(featureFlags: Iterable) = + featureFlagState.addFeatureFlags(featureFlags) + override fun clearFeatureFlag(name: String) = featureFlagState.clearFeatureFlag(name) + override fun clearFeatureFlags() = featureFlagState.clearFeatureFlags() + override fun getUser(): User = user override fun setUser(id: String?, email: String?, name: String?) { user = User(id, email, name) 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 13c2daef93..b889165d0d 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 @@ -16,7 +16,7 @@ * specified at the client level, api-key and endpoint configuration. */ @SuppressWarnings("ConstantConditions") // suppress warning about making redundant null checks -public class Configuration implements CallbackAware, MetadataAware, UserAware { +public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware { private static final int MIN_BREADCRUMBS = 0; private static final int MAX_BREADCRUMBS = 100; @@ -950,6 +950,62 @@ public Object getMetadata(@NonNull String section, @NonNull String key) { } } + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + impl.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + impl.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + impl.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + impl.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + impl.clearFeatureFlags(); + } + /** * Returns the currently set User information. */ diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt index 82425074d8..5382099a25 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt @@ -38,4 +38,5 @@ internal data class FeatureFlagState( } fun toList(): List = featureFlags.toList() + fun copy() = FeatureFlagState(featureFlags.copy()) } 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 4d0341fe71..f32e51075c 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 @@ -45,6 +45,9 @@ public class ClientFacadeTest { @Mock UserState userState; + @Mock + FeatureFlagState featureFlagState; + @Mock ClientObservable clientObservable; @@ -108,6 +111,7 @@ public void setUp() { contextState, callbackState, userState, + featureFlagState, clientObservable, appContext, deviceDataCollector, From 32487c88542d4c0176e8c569b40c0ce683558840 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 9 Nov 2021 15:47:16 +0000 Subject: [PATCH 04/37] feat(feature flags): added FeatureFlags to `Event` --- .../main/java/com/bugsnag/android/Client.java | 11 +++- .../main/java/com/bugsnag/android/Event.java | 64 ++++++++++++++++++- .../java/com/bugsnag/android/EventInternal.kt | 19 +++++- .../com/bugsnag/android/FeatureFlagState.kt | 2 +- .../com/bugsnag/android/NativeInterface.java | 12 +++- .../com/bugsnag/android/ClientFacadeTest.java | 1 + .../java/com/bugsnag/android/EventApiTest.kt | 37 +++++++++++ .../android/EventFeatureFlagsCloneTest.kt | 43 +++++++++++++ .../bugsnag/android/EventMetadataCloneTest.kt | 10 ++- .../src/test/resources/event_redaction.json | 3 +- .../test/resources/event_serialization_0.json | 3 +- .../test/resources/event_serialization_1.json | 3 +- .../test/resources/event_serialization_2.json | 3 +- .../test/resources/event_serialization_3.json | 3 +- .../test/resources/event_serialization_4.json | 1 + .../test/resources/event_serialization_5.json | 3 +- .../test/resources/event_serialization_6.json | 3 +- .../android/AnrDetailsCollectorTest.kt | 1 + .../com/bugsnag/android/BugsnagTestUtils.java | 4 ++ .../bugsnag/android/EventDeserializerTest.kt | 1 + .../java/com/bugsnag/android/TestHooks.java | 4 ++ 21 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/EventFeatureFlagsCloneTest.kt 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 07534029a0..39b7adb1cd 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 @@ -681,7 +681,9 @@ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { } SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Metadata metadata = metadataState.getMetadata(); - Event event = new Event(exc, immutableConfig, severityReason, metadata, logger); + FeatureFlags featureFlags = featureFlagState.getFeatureFlags(); + Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags, + logger); populateAndNotifyAndroidEvent(event, onError); } else { logNull("notify"); @@ -699,7 +701,8 @@ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, SeverityReason handledState = SeverityReason.newInstance(severityReason, Severity.ERROR, attributeValue); Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata); - Event event = new Event(exc, immutableConfig, handledState, data, logger); + Event event = new Event(exc, immutableConfig, handledState, + data, featureFlagState.getFeatureFlags(), logger); populateAndNotifyAndroidEvent(event, null); // persist LastRunInfo so that on relaunch users can check the app crashed @@ -1099,6 +1102,10 @@ MetadataState getMetadataState() { return metadataState; } + FeatureFlagState getFeatureFlagState() { + return featureFlagState; + } + ContextState getContextState() { return contextState; } 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 fe68e04585..f83baca75c 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 @@ -15,7 +15,7 @@ * sent to Bugsnag's API. */ @SuppressWarnings("ConstantConditions") -public class Event implements JsonStream.Streamable, MetadataAware, UserAware { +public class Event implements JsonStream.Streamable, MetadataAware, UserAware, FeatureFlagAware { private final EventInternal impl; private final Logger logger; @@ -24,15 +24,17 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware { @NonNull ImmutableConfig config, @NonNull SeverityReason severityReason, @NonNull Logger logger) { - this(originalError, config, severityReason, new Metadata(), logger); + this(originalError, config, severityReason, new Metadata(), new FeatureFlags(), logger); } Event(@Nullable Throwable originalError, @NonNull ImmutableConfig config, @NonNull SeverityReason severityReason, @NonNull Metadata metadata, + @NonNull FeatureFlags featureFlags, @NonNull Logger logger) { - this(new EventInternal(originalError, config, severityReason, metadata), logger); + this(new EventInternal(originalError, config, severityReason, metadata, featureFlags), + logger); } Event(@NonNull EventInternal impl, @NonNull Logger logger) { @@ -283,6 +285,62 @@ public Object getMetadata(@NonNull String section, @NonNull String key) { } } + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + impl.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + impl.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + impl.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + impl.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + impl.clearFeatureFlags(); + } + @Override public void toStream(@NonNull JsonStream stream) throws IOException { impl.toStream(stream); 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 06ad16bc84..99bf38aec8 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 @@ -7,10 +7,12 @@ internal class EventInternal @JvmOverloads internal constructor( val originalError: Throwable? = null, config: ImmutableConfig, private var severityReason: SeverityReason, - data: Metadata = Metadata() -) : JsonStream.Streamable, MetadataAware, UserAware { + data: Metadata = Metadata(), + featureFlags: FeatureFlags = FeatureFlags() +) : JsonStream.Streamable, MetadataAware, UserAware, FeatureFlagAware { val metadata: Metadata = data.copy() + val featureFlags: FeatureFlags = featureFlags.copy() private val discardClasses: Set = config.discardClasses.toSet() private val projectPackages = config.projectPackages @@ -106,6 +108,8 @@ internal class EventInternal @JvmOverloads internal constructor( threads.forEach { writer.value(it) } writer.endArray() + writer.name("featureFlags").value(featureFlags) + if (session != null) { val copy = Session.copySession(session) writer.name("session").beginObject() @@ -167,4 +171,15 @@ internal class EventInternal @JvmOverloads internal constructor( override fun getMetadata(section: String) = metadata.getMetadata(section) override fun getMetadata(section: String, key: String) = metadata.getMetadata(section, key) + + override fun addFeatureFlag(name: String) = featureFlags.addFeatureFlag(name) + + override fun addFeatureFlag(name: String, variant: String?) = featureFlags.addFeatureFlag(name, variant) + + override fun addFeatureFlags(featureFlags: MutableIterable) = + this.featureFlags.addFeatureFlags(featureFlags) + + override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name) + + override fun clearFeatureFlags() = featureFlags.clearFeatureFlags() } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt index 5382099a25..389589a8cc 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt @@ -1,7 +1,7 @@ package com.bugsnag.android internal data class FeatureFlagState( - private val featureFlags: FeatureFlags = FeatureFlags() + val featureFlags: FeatureFlags = FeatureFlags() ) : BaseObservable(), FeatureFlagAware { override fun addFeatureFlag(name: String) { this.featureFlags.addFeatureFlag(name) 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 d6d2528542..e7b90736fa 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 @@ -397,12 +397,22 @@ public boolean onError(@NonNull Event event) { }); } + /** + * Create an {@code Event} object + * + * @param exc the Throwable object that caused the event + * @param client the Client object that the event is associated with + * @param severityReason the severity of the Event + * @return a new {@code Event} object + */ @NonNull public static Event createEvent(@Nullable Throwable exc, @NonNull Client client, @NonNull SeverityReason severityReason) { Metadata metadata = client.getMetadataState().getMetadata(); - return new Event(exc, client.getConfig(), severityReason, metadata, client.logger); + FeatureFlags featureFlags = client.getFeatureFlagState().getFeatureFlags(); + return new Event(exc, client.getConfig(), severityReason, metadata, featureFlags, + client.logger); } @NonNull 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 f32e51075c..ef2a12606c 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 @@ -131,6 +131,7 @@ public void setUp() { // required fields for generating an event when(metadataState.getMetadata()).thenReturn(new Metadata()); + when(featureFlagState.getFeatureFlags()).thenReturn(new FeatureFlags()); when(immutableConfig.getLogger()).thenReturn(logger); when(immutableConfig.getSendThreads()).thenReturn(ThreadSendPolicy.ALWAYS); diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventApiTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventApiTest.kt index e9b058452e..d2f66054f5 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventApiTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventApiTest.kt @@ -64,4 +64,41 @@ internal class EventApiTest { event.clearMetadata("foo", "wham") assertNull(event.impl.metadata.getMetadata("foo", "wham")) } + + @Test + fun addFeatureFlagWithoutVariant() { + event.addFeatureFlag("demo_mode") + assertEquals( + listOf(FeatureFlag("demo_mode")), + event.impl.featureFlags.toList() + ) + } + + @Test + fun addFeatureFlag() { + event.addFeatureFlag("sample_group", "a") + assertEquals( + listOf(FeatureFlag("sample_group", "a")), + event.impl.featureFlags.toList() + ) + } + + @Test + fun clearFeatureFlag() { + event.addFeatureFlag("demo_group") + event.addFeatureFlag("sample_group", "a") + event.clearFeatureFlag("demo_group") + assertEquals( + listOf(FeatureFlag("sample_group", "a")), + event.impl.featureFlags.toList() + ) + } + + @Test + fun clearFeatureFlags() { + event.addFeatureFlag("demo_group") + event.addFeatureFlag("sample_group", "a") + event.clearFeatureFlags() + assertEquals(emptyList(), event.impl.featureFlags.toList()) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFeatureFlagsCloneTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFeatureFlagsCloneTest.kt new file mode 100644 index 0000000000..1092c45db7 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventFeatureFlagsCloneTest.kt @@ -0,0 +1,43 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Test + +class EventFeatureFlagsCloneTest { + + @Test + fun testFeatureFlagsClone() { + val featureFlags = FeatureFlags() + featureFlags.addFeatureFlag("sample_group", "123") + + val handledState = SeverityReason.newInstance( + SeverityReason.REASON_HANDLED_EXCEPTION + ) + val config = BugsnagTestUtils.generateImmutableConfig() + val event = Event( + RuntimeException(), + config, + handledState, + Metadata(), + featureFlags, + NoopLogger + ) + + event.addFeatureFlag("demo_mode") + + // featureFlags objects should be deep copied + assertNotSame(featureFlags, event.impl.featureFlags) + + // validate origin featureFlags + val origExpected = listOf(FeatureFlag("sample_group", "123")) + assertEquals(origExpected, featureFlags.toList()) + + // validate event featureFlags + val eventExpected = listOf( + FeatureFlag("sample_group", "123"), + FeatureFlag("demo_mode") + ) + assertEquals(eventExpected, event.impl.featureFlags.toList()) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventMetadataCloneTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventMetadataCloneTest.kt index 0161b2bed2..2dd94a0163 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventMetadataCloneTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventMetadataCloneTest.kt @@ -15,7 +15,15 @@ class EventMetadataCloneTest { SeverityReason.REASON_HANDLED_EXCEPTION ) val config = BugsnagTestUtils.generateImmutableConfig() - val event = Event(RuntimeException(), config, handledState, data, NoopLogger) + val event = Event( + RuntimeException(), + config, + handledState, + data, + FeatureFlags(), + NoopLogger + ) + event.addMetadata("test_section", "second", "another value") // metadata object should be deep copied diff --git a/bugsnag-android-core/src/test/resources/event_redaction.json b/bugsnag-android-core/src/test/resources/event_redaction.json index 8945650e90..0e0d769e94 100644 --- a/bugsnag-android-core/src/test/resources/event_redaction.json +++ b/bugsnag-android-core/src/test/resources/event_redaction.json @@ -51,5 +51,6 @@ } } ], - "threads": [] + "threads": [], + "featureFlags": [] } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_0.json b/bugsnag-android-core/src/test/resources/event_serialization_0.json index e0095853d6..7a45450512 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_0.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_0.json @@ -32,5 +32,6 @@ "time": "1970-01-01T00:00:00.000Z" }, "breadcrumbs": [], - "threads": [] + "threads": [], + "featureFlags": [] } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_1.json b/bugsnag-android-core/src/test/resources/event_serialization_1.json index 435a0f9923..5a408847e0 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_1.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_1.json @@ -33,5 +33,6 @@ "time": "1970-01-01T00:00:00.000Z" }, "breadcrumbs": [], - "threads": [] + "threads": [], + "featureFlags": [] } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_2.json b/bugsnag-android-core/src/test/resources/event_serialization_2.json index 7f1d91250e..07d455a314 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_2.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_2.json @@ -33,5 +33,6 @@ }, "breadcrumbs": [], "groupingHash": "herpderp", - "threads": [] + "threads": [], + "featureFlags": [] } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_3.json b/bugsnag-android-core/src/test/resources/event_serialization_3.json index 9aaa171197..f556d67e51 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_3.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_3.json @@ -32,5 +32,6 @@ "time": "1970-01-01T00:00:00.000Z" }, "breadcrumbs": [], - "threads": [] + "threads": [], + "featureFlags": [] } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_4.json b/bugsnag-android-core/src/test/resources/event_serialization_4.json index 810ade4dc1..fd8b7b63a1 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_4.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_4.json @@ -33,6 +33,7 @@ }, "breadcrumbs": [], "threads": [], + "featureFlags": [], "session": { "id": "123", "startedAt": "1970-01-01T00:00:00.000Z", diff --git a/bugsnag-android-core/src/test/resources/event_serialization_5.json b/bugsnag-android-core/src/test/resources/event_serialization_5.json index 4154466c90..1f68a55cb4 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_5.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_5.json @@ -41,5 +41,6 @@ "stacktrace": [], "errorReportingThread": true } - ] + ], + "featureFlags": [] } \ No newline at end of file diff --git a/bugsnag-android-core/src/test/resources/event_serialization_6.json b/bugsnag-android-core/src/test/resources/event_serialization_6.json index 60ce3135a9..be3fbad5f8 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_6.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_6.json @@ -58,5 +58,6 @@ "metaData": {} } ], - "threads": [] + "threads": [], + "featureFlags": [] } \ No newline at end of file diff --git a/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/AnrDetailsCollectorTest.kt b/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/AnrDetailsCollectorTest.kt index 196ab679bf..8edd71c020 100644 --- a/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/AnrDetailsCollectorTest.kt +++ b/bugsnag-plugin-android-anr/src/test/java/com/bugsnag/android/AnrDetailsCollectorTest.kt @@ -65,6 +65,7 @@ class AnrDetailsCollectorTest { fun anrDetailsAltered() { Mockito.`when`(client.config).thenReturn(BugsnagTestUtils.generateImmutableConfig()) Mockito.`when`(client.getMetadataState()).thenReturn(BugsnagTestUtils.generateMetadataState()) + Mockito.`when`(client.getFeatureFlagState()).thenReturn(BugsnagTestUtils.generateFeatureFlagState()) val event = NativeInterface.createEvent( RuntimeException("whoops"), client, 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 3d87326412..fdc717d689 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 @@ -35,6 +35,10 @@ static MetadataState generateMetadataState() { return new MetadataState(); } + static FeatureFlagState generateFeatureFlagState() { + return new FeatureFlagState(); + } + static ImmutableConfig convert(Configuration config) { return ImmutableConfigKt.convertToImmutableConfig(config, null); } diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt index 767b6f33f4..4471177f24 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt @@ -43,6 +43,7 @@ class EventDeserializerTest { `when`(client.config).thenReturn(TestData.generateConfig()) `when`(client.getLogger()).thenReturn(object : Logger {}) `when`(client.getMetadataState()).thenReturn(TestHooks.generateMetadataState()) + `when`(client.getFeatureFlagState()).thenReturn(TestHooks.generateFeatureFlagsState()) } private fun breadcrumbMap() = hashMapOf( diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestHooks.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestHooks.java index 216a962b21..e9b628b612 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestHooks.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestHooks.java @@ -8,4 +8,8 @@ static boolean getUnhandledOverridden(Event event) { static MetadataState generateMetadataState() { return new MetadataState(); } + + static FeatureFlagState generateFeatureFlagsState() { + return new FeatureFlagState(); + } } From a6ba56740157fc877029544f90a0ca9a04c88b1f Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 17 Nov 2021 15:32:36 +0000 Subject: [PATCH 05/37] feat(feature flags): feature flags can be stored in the `bugsnag_event` --- .../android/ndk/NativeFeatureFlagsTest.kt | 19 +++ .../src/main/CMakeLists.txt | 1 + .../com/bugsnag/android/ndk/NativeBridge.kt | 9 ++ .../src/main/jni/bugsnag_ndk.c | 57 +++++++ .../src/main/jni/event.h | 16 ++ .../src/main/jni/featureflags.c | 141 ++++++++++++++++++ .../src/main/jni/featureflags.h | 40 +++++ .../src/test/CMakeLists.txt | 1 + .../src/test/cpp/main.c | 8 +- .../src/test/cpp/test_featureflags.c | 57 +++++++ .../full_tests/batch_1/feature_flags.feature | 28 ++++ 11 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeFeatureFlagsTest.kt create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/featureflags.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/featureflags.h create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/test_featureflags.c create mode 100644 features/full_tests/batch_1/feature_flags.feature diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeFeatureFlagsTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeFeatureFlagsTest.kt new file mode 100644 index 0000000000..a8efa83809 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeFeatureFlagsTest.kt @@ -0,0 +1,19 @@ +package com.bugsnag.android.ndk + +import org.junit.Test + +class NativeFeatureFlagsTest { + companion object { + init { + System.loadLibrary("bugsnag-ndk") + System.loadLibrary("bugsnag-ndk-test") + } + } + + external fun run(): Int + + @Test + fun testPassesNativeSuite() { + verifyNativeRun(run()) + } +} diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index d95cb17e93..51c6e5dd51 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -11,6 +11,7 @@ add_library( # Specifies the name of the library. jni/metadata.c jni/safejni.c jni/event.c + jni/featureflags.c jni/handlers/signal_handler.c jni/handlers/cpp_handler.cpp jni/utils/crash_info.c diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index f67098e537..b7bc0fb378 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 @@ -78,6 +78,9 @@ class NativeBridge : StateObserver { external fun updateUserName(newValue: String) external fun getUnwindStackFunction(): Long external fun updateLowMemory(newValue: Boolean, memoryTrimLevelDescription: String) + external fun addFeatureFlag(name: String, variant: String?) + external fun clearFeatureFlag(name: String) + external fun clearFeatureFlags() override fun onStateChange(event: StateEvent) { if (isInvalidMessage(event)) return @@ -120,6 +123,12 @@ class NativeBridge : StateObserver { updateUserEmail(makeSafe(event.user.email ?: "")) } is StateEvent.UpdateMemoryTrimEvent -> updateLowMemory(event.isLowMemory, event.memoryTrimLevelDescription) + is StateEvent.AddFeatureFlag -> addFeatureFlag( + makeSafe(event.name), + event.variant?.let { makeSafe(it) } + ) + is StateEvent.ClearFeatureFlag -> clearFeatureFlag(makeSafe(event.name)) + is StateEvent.ClearFeatureFlags -> clearFeatureFlags() } } 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 350e2d0d4a..8401c596d9 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -7,6 +7,7 @@ #include #include "event.h" +#include "featureflags.h" #include "handlers/cpp_handler.h" #include "handlers/signal_handler.h" #include "metadata.h" @@ -191,6 +192,10 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( bsg_safe_release_string_utf_chars(env, _api_key, api_key); } + // clear the feature flag fields + bugsnag_env->next_event.feature_flag_count = 0; + bugsnag_env->next_event.feature_flags = NULL; + bsg_global_env = bugsnag_env; bsg_update_next_run_info(bsg_global_env); BUGSNAG_LOG("Initialization complete!"); @@ -708,6 +713,58 @@ Java_com_bugsnag_android_ndk_NativeBridge_getUnwindStackFunction(JNIEnv *env, return (jlong)bsg_unwind_stack_default; } +JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addFeatureFlag( + JNIEnv *env, jobject thiz, jstring name_, jstring variant_) { + + if (bsg_global_env == NULL) { + return; + } + + char *name = (char *)bsg_safe_get_string_utf_chars(env, name_); + char *variant = (char *)bsg_safe_get_string_utf_chars(env, variant_); + + if (name != NULL) { + bsg_request_env_write_lock(); + bsg_set_feature_flag(&bsg_global_env->next_event, name, variant); + bsg_release_env_write_lock(); + } + + bsg_safe_release_string_utf_chars(env, name_, name); + bsg_safe_release_string_utf_chars(env, variant_, variant); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_NativeBridge_clearFeatureFlag(JNIEnv *env, + jobject thiz, + jstring name_) { + + if (bsg_global_env == NULL) { + return; + } + + char *name = (char *)bsg_safe_get_string_utf_chars(env, name_); + + if (name != NULL) { + bsg_request_env_write_lock(); + bsg_clear_feature_flag(&bsg_global_env->next_event, name); + bsg_release_env_write_lock(); + } + + bsg_safe_release_string_utf_chars(env, name_, name); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_NativeBridge_clearFeatureFlags(JNIEnv *env, + jobject thiz) { + if (bsg_global_env == NULL) { + return; + } + + bsg_request_env_write_lock(); + bsg_free_feature_flags(&bsg_global_env->next_event); + bsg_release_env_write_lock(); +} + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index 2db2e7ba9a..1ba961c77c 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -197,6 +197,11 @@ typedef enum { SEND_THREADS_NEVER = 2 } bsg_thread_send_policy; +typedef struct { + char *name; + char *variant; +} bsg_feature_flag; + typedef struct { bsg_notifier notifier; bsg_app_info app; @@ -224,6 +229,17 @@ typedef struct { int thread_count; bsg_thread threads[BUGSNAG_THREADS_MAX]; + + /** + * The number of feature flags currently specified. + */ + size_t feature_flag_count; + + /** + * Pointer to the current feature flags. This is dynamically allocated and + * serialized/deserialized separately to the rest of the struct. + */ + bsg_feature_flag *feature_flags; } bugsnag_event; void bugsnag_event_add_breadcrumb(bugsnag_event *event, diff --git a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c new file mode 100644 index 0000000000..75c70587bb --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c @@ -0,0 +1,141 @@ +#include + +#include "bugsnag_ndk.h" +#include "featureflags.h" +#include "utils/string.h" + +/* + * Implementation notes: + * + * We store Feature Flags in a dynamically allocated array, sorted by the + * feature flag 'name' string, allowing us to binary-search the array for + * duplicates. + * + * This provides a reasonable compromise between speed and size, since the array + * is no larger than the number of feature flags (not counting malloc padding) + * and is unlikely to be modified often. It also keeps testing simple, as the + * keys will always be in a known order. + * + * We resize the array using 'realloc' and 'memmove' to try and keep the + * overhead reasonable. + */ + +static int feature_flag_index(const bugsnag_event *env, const char *name) { + // simple binary search for a feature-flag by name + int low = 0; + int high = env->feature_flag_count - 1; + + while (low <= high) { + int mid = (low + high) >> 1; + int cmp = strcmp(env->feature_flags[mid].name, name); + + if (cmp < 0) { + low = mid + 1; + } else if (cmp > 0) { + high = mid - 1; + } else { + return mid; // found it + } + } + + return -(low + 1); +} + +static void *grow_array_for_index(void *array, const size_t element_count, + const size_t element_size, + const unsigned int index) { + void *new_array = realloc(array, (element_count + 1) * element_size); + + if (!new_array) { + return NULL; + } + + // if we need to: shift the "end" of the array by 1 element so 'index' has a + // space + memmove(new_array + ((index + 1) * element_size), + new_array + (index * element_size), + (element_count - index) * element_size); + + return new_array; +} + +void bsg_set_feature_flag(bugsnag_event *env, const char *name, + const char *variant) { + int expected_index = feature_flag_index(env, name); + + if (expected_index >= 0) { + // feature flag already exists, so we overwrite the variant + bsg_feature_flag *flag = &env->feature_flags[expected_index]; + + // make sure we release the existing variant, if one exists + free(flag->variant); + + if (variant) { + // make a copy of the variant, so that the JVM can have it's memory back + flag->variant = strdup(variant); + } else { + flag->variant = NULL; + } + } else { + int new_flag_index = -expected_index - 1; + + // this is a new feature flag, we need to insert it - which means we also + // need a new array + bsg_feature_flag *new_flags = + grow_array_for_index(env->feature_flags, env->feature_flag_count, + sizeof(bsg_feature_flag), new_flag_index); + + // we cannot grow the feature flag array, so we return + if (!new_flags) { + return; + } + + new_flags[new_flag_index].name = strdup(name); + + if (variant) { + new_flags[new_flag_index].variant = strdup(variant); + } else { + new_flags[new_flag_index].variant = NULL; + } + + env->feature_flags = new_flags; + env->feature_flag_count = env->feature_flag_count + 1; + } +} + +void bsg_clear_feature_flag(bugsnag_event *env, const char *name) { + int flag_index = feature_flag_index(env, name); + + if (flag_index < 0) { + // no such feature flag - early exit + return; + } + + bsg_feature_flag *flag = &env->feature_flags[flag_index]; + + // release the memory held for name and possibly the variant + free(flag->name); + free(flag->variant); + + // pack the array elements down to fill in the "gap" + // we don't resize the array down by one, that gets handled when elements are + // added + memmove(&env->feature_flags[flag_index], &env->feature_flags[flag_index + 1], + (env->feature_flag_count - flag_index - 1) * + sizeof(bsg_feature_flag)); + + // mark the array as having one-less flag + env->feature_flag_count = env->feature_flag_count - 1; +} + +void bsg_free_feature_flags(bugsnag_event *env) { + for (int index = 0; index < env->feature_flag_count; index++) { + free(env->feature_flags[index].name); + free(env->feature_flags[index].variant); + } + + free(env->feature_flags); + + env->feature_flags = NULL; + env->feature_flag_count = 0; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h new file mode 100644 index 0000000000..8c535c1d6e --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h @@ -0,0 +1,40 @@ +#ifndef BUGSNAG_ANDROID_FEATUREFLAGS_H +#define BUGSNAG_ANDROID_FEATUREFLAGS_H + +#include "bugsnag_ndk.h" + +/** + * Set a feature flag in the given `bugsnag_event` with an optional variant. + * This function will overwrite any existing feature flag with the specified + * name. Setting a `variant` to NULL is *not* the same as clearing the feature + * flag, which must be done with `bsg_clear_feature_flag`. + * + * @param env the environment to populate with the given feature flag + * @param name the name of the feature flag (may not be NULL) + * @param variant if non-NULL: the variant to set the feature flag to + */ +void bsg_set_feature_flag(bugsnag_event *env, const char *name, + const char *variant); + +/** + * Release a specified feature flag from the given `bugsnag_event` if it + * exists. If the flag does not exist this is a no-op and the `bugsnag_event` + * will remain untouched. Any dynamic memory associated with the cleared feature + * flag is correctly released. + * + * @param env the environment to clear the given feature flag in + * @param name the name of the feature flag to clear + */ +void bsg_clear_feature_flag(bugsnag_event *env, const char *name); + +/** + * Release all of the memory for all of the feature flags stored in the given + * `bugsnag_event`. This also zeroes the `bugsnag_event->feature_flag_count` + * and cleans up the array pointers, effectively clearing all of the feature + * flags. + * + * @param env the environment to remove all the feature flags from + */ +void bsg_free_feature_flags(bugsnag_event *env); + +#endif // BUGSNAG_ANDROID_FEATUREFLAGS_H diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index b4b6f6efd2..7cd6aaa3f0 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -11,5 +11,6 @@ add_library(bugsnag-ndk-test SHARED cpp/test_serializer.c cpp/test_breadcrumbs.c cpp/test_bsg_event.c + cpp/test_featureflags.c ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/main.c b/bugsnag-plugin-android-ndk/src/test/cpp/main.c index 46072e51b5..36ef0adc2f 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -18,6 +18,7 @@ SUITE(suite_event_app_mutators); SUITE(suite_event_device_mutators); SUITE(suite_struct_to_file); SUITE(suite_struct_migration); +SUITE(suite_feature_flags); GREATEST_MAIN_DEFS(); @@ -89,6 +90,11 @@ JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventDeviceMutatorsTest return run_test_suite(suite_event_device_mutators); } +JNIEXPORT jint JNICALL +Java_com_bugsnag_android_ndk_NativeFeatureFlagsTest_run(JNIEnv *env, jobject thiz) { + return run_test_suite(suite_feature_flags); +} + JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_UserSerializationTest_run( JNIEnv *env, jobject _this) { bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); @@ -232,4 +238,4 @@ Java_com_bugsnag_android_ndk_ThreadSerializationTest_run(JNIEnv *env, jobject th bsg_serialize_threads(event, threads_array); char *string = json_serialize_to_string(threads_val); return (*env)->NewStringUTF(env, string); -} \ No newline at end of file +} diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_featureflags.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_featureflags.c new file mode 100644 index 0000000000..a6d1e0cea7 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_featureflags.c @@ -0,0 +1,57 @@ +#include +#include + +TEST test_set_feature_flag(void) { + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + bsg_set_feature_flag(event, "sample_group", "a"); + bsg_set_feature_flag(event, "demo_mode", NULL); + bsg_set_feature_flag(event, "demo_mode", "yes"); + bsg_set_feature_flag(event, "zzz", NULL); + + ASSERT_EQ(3, event->feature_flag_count); + + ASSERT_STR_EQ("demo_mode", event->feature_flags[0].name); + ASSERT_STR_EQ("yes", event->feature_flags[0].variant); + + ASSERT_STR_EQ("sample_group", event->feature_flags[1].name); + ASSERT_STR_EQ("a", event->feature_flags[1].variant); + + ASSERT_STR_EQ("zzz", event->feature_flags[2].name); + ASSERT_EQ(NULL, event->feature_flags[2].variant); + + bsg_free_feature_flags(event); + free(event); + + PASS(); +} + +TEST test_clear_feature_flag(void) { + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + bsg_set_feature_flag(event, "sample_group", "a"); + bsg_set_feature_flag(event, "demo_mode", NULL); + bsg_set_feature_flag(event, "remaining", "flag"); + + ASSERT_EQ(3, event->feature_flag_count); + + bsg_clear_feature_flag(event, "no_such_flag"); + ASSERT_EQ(3, event->feature_flag_count); + + bsg_clear_feature_flag(event, "sample_group"); + bsg_clear_feature_flag(event, "demo_mode"); + + ASSERT_EQ(1, event->feature_flag_count); + ASSERT_STR_EQ("remaining", event->feature_flags[0].name); + ASSERT_STR_EQ("flag", event->feature_flags[0].variant); + + bsg_free_feature_flags(event); + free(event); + + PASS(); +} + +SUITE (suite_feature_flags) { + RUN_TEST(test_set_feature_flag); + RUN_TEST(test_clear_feature_flag); +} \ No newline at end of file diff --git a/features/full_tests/batch_1/feature_flags.feature b/features/full_tests/batch_1/feature_flags.feature new file mode 100644 index 0000000000..30fdeba6b2 --- /dev/null +++ b/features/full_tests/batch_1/feature_flags.feature @@ -0,0 +1,28 @@ +Feature: Reporting with feature flags + +Scenario: Sends handled exception which includes feature flags + When I run "FeatureFlagScenario" + Then I wait to receive an error + And the exception "errorClass" equals "java.lang.RuntimeException" + And the event "unhandled" is false + And event 0 contains the feature flag "demo_mode" with no variant + And event 0 does not contain the feature flag "demo_mode" with no variant + +Scenario: Sends handled exception which includes feature flags added in the notify callback + When I configure the app to run in the "callback" state + And I run "FeatureFlagScenario" + Then I wait to receive an error + And the exception "errorClass" equals "java.lang.RuntimeException" + And the event "unhandled" is false + And event 0 contains the feature flag "demo_mode" with no variant + And event 0 contains the feature flag "sample_group" with variant "a" + +Scenario: Sends unhandled exception which includes feature flags added in the notify callback + When I configure the app to run in the "unhandled callback" state + And I run "FeatureFlagScenario" and relaunch the app + And I configure Bugsnag for "FeatureFlagScenario" + Then I wait to receive an error + And the exception "errorClass" equals "java.lang.RuntimeException" + And the event "unhandled" is true + And event 0 contains the feature flag "demo_mode" with no variant + And event 0 contains the feature flag "sample_group" with variant "a" From 512d7adabd8842901ee6bbe95fb1281d5f51d46b Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 17 Nov 2021 15:32:36 +0000 Subject: [PATCH 06/37] feat(feature flags): feature flags can be stored in the `bsg_environment` --- .../src/main/jni/bugsnag_ndk.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h index 1dd7056a77..7c23d64fd7 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -20,6 +20,11 @@ extern "C" { #endif +typedef struct { + char *name; + char *variant; +} bsg_feature_flag; + typedef struct { /** * Unwinding style used for signal-safe handling @@ -79,6 +84,16 @@ typedef struct { * at the time of an error. */ bsg_thread_send_policy send_threads; + + /** + * The number of feature flags currently specified. + */ + size_t feature_flag_count; + + /** + * Pointer to the current feature flags. + */ + bsg_feature_flag *feature_flags; } bsg_environment; bsg_unwinder bsg_configured_unwind_style(); From ec5525c131be26083811f28f48e5339c39896ac0 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 19 Nov 2021 13:23:37 +0000 Subject: [PATCH 07/37] feat: serialize feature flags held by the NDK during a crash --- .../main/java/com/bugsnag/android/Client.java | 1 + .../com/bugsnag/android/FeatureFlagState.kt | 8 ++ .../bugsnag/android/FeatureFlagStateTest.kt | 28 +++++ .../src/main/CMakeLists.txt | 1 + .../src/main/jni/bugsnag_ndk.h | 5 - .../src/main/jni/featureflags.c | 8 ++ .../src/main/jni/utils/buffered_writer.c | 114 ++++++++++++++++++ .../src/main/jni/utils/buffered_writer.h | 66 ++++++++++ .../src/main/jni/utils/serializer.c | 89 ++++++++++++-- .../src/main/jni/utils/serializer.h | 3 + .../src/test/cpp/test_utils_serialize.c | 18 +++ 11 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h 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 39b7adb1cd..acd4d6860a 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 @@ -428,6 +428,7 @@ void syncInitialState() { contextState.emitObservableEvent(); userState.emitObservableEvent(); memoryTrimState.emitObservableEvent(); + featureFlagState.emitObservableEvent(); } /** diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt index 389589a8cc..ba2b78bd9b 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FeatureFlagState.kt @@ -37,6 +37,14 @@ internal data class FeatureFlagState( } } + fun emitObservableEvent() { + val flags = toList() + + flags.forEach { (name, variant) -> + updateState { StateEvent.AddFeatureFlag(name, variant) } + } + } + fun toList(): List = featureFlags.toList() fun copy() = FeatureFlagState(featureFlags.copy()) } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt index 62b0c69d46..0a71938bf1 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/FeatureFlagStateTest.kt @@ -93,4 +93,32 @@ internal class FeatureFlagStateTest { assertNotNull(events.find { it is StateEvent.ClearFeatureFlags }) } + + @Test + fun emitObservableEvent() { + state.addFeatureFlag("sample_group", "4321") + state.addFeatureFlag("listing_view", "legacy") + state.addFeatureFlag("demo_mode") + + // clear the events + events.clear() + + state.emitObservableEvent() + assertEquals(3, events.size) + val addSampleGroup = events + .filterIsInstance() + .find { it.name == "sample_group" } + assertEquals("4321", addSampleGroup?.variant) + + val addListingView = events + .filterIsInstance() + .find { it.name == "listing_view" } + assertEquals("legacy", addListingView?.variant) + + val addDemoMode = events + .filterIsInstance() + .find { it.name == "demo_mode" } + assertNotNull(addDemoMode) + assertNull(addDemoMode!!.variant) + } } diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 51c6e5dd51..fd8a8ed255 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -23,6 +23,7 @@ add_library( # Specifies the name of the library. jni/utils/serializer.c jni/utils/string.c jni/utils/threads.c + jni/utils/buffered_writer.c jni/deps/parson/parson.c ) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h index 7c23d64fd7..2eb9a1a025 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -20,11 +20,6 @@ extern "C" { #endif -typedef struct { - char *name; - char *variant; -} bsg_feature_flag; - typedef struct { /** * Unwinding style used for signal-safe handling diff --git a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c index 75c70587bb..64041dbb46 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c @@ -4,6 +4,10 @@ #include "featureflags.h" #include "utils/string.h" +#ifdef __cplusplus +extern "C" { +#endif + /* * Implementation notes: * @@ -139,3 +143,7 @@ void bsg_free_feature_flags(bugsnag_event *env) { env->feature_flags = NULL; env->feature_flag_count = 0; } + +#ifdef __cplusplus +} +#endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c new file mode 100644 index 0000000000..6ad7fd25d0 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c @@ -0,0 +1,114 @@ +// +// Created by Jason Morris on 19/11/2021. +// + +#include "buffered_writer.h" +#include "bugsnag_ndk.h" +#include +#include +#include +#include +#include + +static bool bsg_flush_impl(const int fd, const char *buff, + size_t bytes_to_write) { + if (bytes_to_write == 0) { + return true; + } + + // Try a few times, then give up. + // write() will write less bytes on signal interruption, disk full, or + // resource limit. + for (int i = 0; i < 10; i++) { + ssize_t written_count = write(fd, buff, bytes_to_write); + if (written_count == bytes_to_write) { + return true; + } + if (written_count < 0) { + return false; + } + buff += written_count; + bytes_to_write -= written_count; + } + + return false; +} + +static bool bsg_buffered_writer_flush(struct bsg_buffered_writer *writer) { + if (bsg_flush_impl(writer->fd, writer->buffer, writer->pos)) { + writer->pos = 0; + return true; + } + + return false; +} + +// The compiler keeps changing sizeof(size_t) on different platforms, which +// breaks printf(). +#if __SIZEOF_SIZE_T__ == 8 +#define PRIsize_t PRIu64 +#else +#define PRIsize_t PRIu32 +#endif + +static bool bsg_buffered_writer_write(struct bsg_buffered_writer *writer, + const void *data, size_t length) { + + if (length > BSG_BUFFER_SIZE) { + // this data won't fit in the buffer, so we first flush anything already in + // the buffer + if (!writer->flush(writer)) { + return false; + } + + // then we flush the data we're needing to write + if (!bsg_flush_impl(writer->fd, data, length)) { + return false; + } + + // then we return + return true; + } + + // the data will fit into the buffer, but the data doesn't have the space + if (length > BSG_BUFFER_SIZE - writer->pos) { + // so we flush the buffer first + if (!writer->flush(writer)) { + return false; + } + } + + memcpy(writer->buffer + writer->pos, data, length); + writer->pos += length; + return true; +} + +static bool bsg_buffered_writer_close(bsg_buffered_writer *writer) { + writer->flush(writer); + if (close(writer->fd) < 0) { + return false; + } + return true; +} + +bool bsg_buffered_writer_open(struct bsg_buffered_writer *writer, + const char *path) { + int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) { + goto fail; + } + + writer->fd = fd; + writer->pos = 0; + writer->write = bsg_buffered_writer_write; + writer->flush = bsg_buffered_writer_flush; + writer->dispose = bsg_buffered_writer_close; + + return true; + +fail: + if (fd > 0) { + close(fd); + } + return false; +} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h new file mode 100644 index 0000000000..f4ca565a28 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h @@ -0,0 +1,66 @@ +/** + * Async-safe buffered writer. + * + * This writer buffers up data and flushes to disk as needed, using primitive + * async-safe functions open(), close(), and write() under the hood. + */ +#ifndef BUGSNAG_BUFFERED_WRITER_H +#define BUGSNAG_BUFFERED_WRITER_H + +#include +#include + +#define BSG_BUFFER_SIZE 128 + +typedef struct bsg_buffered_writer { + int fd; + size_t pos; + char buffer[BSG_BUFFER_SIZE]; + + /** + * Write to this writer. If the internal buffer size is exceeded, it will + * automatically flush to file. + * + * Note: This method is async-safe. + * + * @param writer This writer. + * @param data The data to write. + * @param length The length of the data to write. + * @return True on success. Check errno on error. + */ + bool (*write)(struct bsg_buffered_writer *writer, const void *data, + size_t length); + + /** + * Force a flush to file. + * + * Note: This method is async-safe. + * + * @param writer This writer. + * @return True on success. Check errno on error. + */ + bool (*flush)(struct bsg_buffered_writer *writer); + + /** + * Dispose of this writer, closing and freeing all resources. The pointer to + * writer will be invalid after this call. + * + * Note: This method is NOT async-safe! + * + * @param writer This writer. + * @return True on success. Check errno on error. + */ + bool (*dispose)(struct bsg_buffered_writer *writer); +} bsg_buffered_writer; + +/** + * Create a new buffered writer. + * + * @param buffer_size The size of the buffer to use. + * @param path The path of the file to write to. + * @return A buffered writer, or NULL on error. Check errno on error. + */ +bool bsg_buffered_writer_open(struct bsg_buffered_writer *writer, + const char *path); + +#endif \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 9b2224abba..99567402a5 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -1,4 +1,5 @@ #include "serializer.h" +#include "buffered_writer.h" #include "string.h" #include @@ -15,7 +16,9 @@ #ifdef __cplusplus extern "C" { #endif -bool bsg_event_write(bsg_report_header *header, bugsnag_event *event, int fd); + +bool bsg_event_write(struct bsg_buffered_writer *writer, + bsg_report_header *header, bugsnag_event *event); bugsnag_event *bsg_event_read(int fd); bsg_report_header *bsg_report_header_read(int fd); @@ -54,12 +57,24 @@ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { } bool bsg_serialize_event_to_file(bsg_environment *env) { - int fd = open(env->next_event_path, O_WRONLY | O_CREAT, 0644); - if (fd == -1) { + bsg_buffered_writer writer; + if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { return false; } - return bsg_event_write(&env->report_header, &env->next_event, fd); + if (!bsg_event_write(&writer, &env->report_header, &env->next_event)) { + goto fail; + } + + if (!bsg_write_feature_flags(env, &writer)) { + goto fail; + } + + return true; + +fail: + writer.dispose(&writer); + return false; } bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { @@ -577,13 +592,13 @@ bool bsg_report_header_write(bsg_report_header *header, int fd) { return len == sizeof(bsg_report_header); } -bool bsg_event_write(bsg_report_header *header, bugsnag_event *event, int fd) { - if (!bsg_report_header_write(header, fd)) { +bool bsg_event_write(struct bsg_buffered_writer *writer, + bsg_report_header *header, bugsnag_event *event) { + if (!bsg_report_header_write(header, writer->fd)) { return false; } - ssize_t len = write(fd, event, sizeof(bugsnag_event)); - return len == sizeof(bugsnag_event); + return writer->write(writer, event, sizeof(bugsnag_event)); } const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { @@ -909,3 +924,61 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event) { } return serialized_string; } + +static bool write_string(bsg_buffered_writer *writer, const char *s) { + // prefix with the string length uint32 + const uint32_t length = bsg_strlen(s); + if (!writer->write(writer, &length, sizeof(length))) { + return false; + } + + // then write the string data without trailing '\0' + if (!writer->write(writer, s, length)) { + return false; + } + + return true; +} + +static bool write_byte(bsg_buffered_writer *writer, const char value) { + return writer->write(writer, &value, 1); +} + +static bool write_feature_flag(bsg_buffered_writer *writer, + bsg_feature_flag *flag) { + if (!write_string(writer, flag->name)) { + return false; + } + + if (flag->variant) { + if (!write_byte(writer, 1)) { + return false; + } + + if (!write_string(writer, flag->variant)) { + return false; + } + } else { + if (!write_byte(writer, 0)) { + return false; + } + } + + return true; +} + +bool bsg_write_feature_flags(bsg_environment *env, + bsg_buffered_writer *writer) { + const uint32_t feature_flag_count = env->feature_flag_count; + if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { + return false; + } + + for (uint32_t index = 0; index < feature_flag_count; index++) { + if (!write_feature_flag(writer, &env->feature_flags[index])) { + return false; + } + } + + return true; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h index 185c86f079..d99598bdca 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -1,4 +1,5 @@ #include "../bugsnag_ndk.h" +#include "buffered_writer.h" #include "build.h" #include #include @@ -50,6 +51,8 @@ int bsg_calculate_total_crumbs(int old_count); int bsg_calculate_v1_start_index(int old_count); int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index); +bool bsg_write_feature_flags(bsg_environment *env, bsg_buffered_writer *writer); + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 849e85be26..2adda27f82 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -2,6 +2,7 @@ #include #include #include +#include #define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/foo.crash" @@ -454,6 +455,22 @@ TEST test_report_to_file(void) { PASS(); } +TEST test_report_with_feature_flags_to_file(void) { + bsg_environment *env = calloc(1, sizeof(bsg_environment)); + env->report_header.version = 7; + env->report_header.big_endian = 1; + bugsnag_event *report = bsg_generate_event(); + memcpy(&env->next_event, report, sizeof(bugsnag_event)); + bsg_set_feature_flag(env, "sample_group", "a"); + bsg_set_feature_flag(env, "demo_mode", NULL); + strcpy(env->report_header.os_build, "macOS Sierra"); + strcpy(env->next_event_path, SERIALIZE_TEST_FILE); + ASSERT(bsg_serialize_event_to_file(env)); + free(report); + free(env); + PASS(); +} + TEST test_file_to_report(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); env->report_header.version = 5; @@ -912,6 +929,7 @@ SUITE(suite_json_serialization) { SUITE(suite_struct_to_file) { RUN_TEST(test_report_to_file); RUN_TEST(test_file_to_report); + RUN_TEST(test_report_with_feature_flags_to_file); } SUITE(suite_struct_migration) { From 0d93f20b4b844a2530d0f4740f4b6001162bbbdd Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 23 Nov 2021 17:01:31 +0000 Subject: [PATCH 08/37] feat(feature flags): load feature flags stored in NDK events --- .../src/main/jni/bugsnag_ndk.c | 1 + .../src/main/jni/bugsnag_ndk.h | 10 - .../src/main/jni/event.h | 2 +- .../src/main/jni/featureflags.c | 40 ++-- .../src/main/jni/featureflags.h | 12 +- .../src/main/jni/utils/buffered_writer.c | 28 ++- .../src/main/jni/utils/buffered_writer.h | 22 +- .../src/main/jni/utils/migrate.h | 29 +++ .../src/main/jni/utils/serializer.c | 192 +++++++++++++++--- .../src/main/jni/utils/serializer.h | 4 +- .../src/test/cpp/test_utils_serialize.c | 29 ++- .../src/main/cpp/bugsnag-java-scenarios.cpp | 6 + .../CXXFeatureFlagNativeCrashScenario.java | 31 +++ .../batch_2/native_feature_flags.feature | 10 + 14 files changed, 341 insertions(+), 75 deletions(-) create mode 100644 features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java create mode 100644 features/full_tests/batch_2/native_feature_flags.feature 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 8401c596d9..20e16de08d 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -271,6 +271,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath( if (event != NULL) { bsg_safe_release_byte_array_elements(env, jstage, (jbyte *)event->app.release_stage); + bsg_free_feature_flags(event); free(event); } if (payload != NULL) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h index 2eb9a1a025..1dd7056a77 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -79,16 +79,6 @@ typedef struct { * at the time of an error. */ bsg_thread_send_policy send_threads; - - /** - * The number of feature flags currently specified. - */ - size_t feature_flag_count; - - /** - * Pointer to the current feature flags. - */ - bsg_feature_flag *feature_flags; } bsg_environment; bsg_unwinder bsg_configured_unwind_style(); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index 1ba961c77c..6956f31d99 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -34,7 +34,7 @@ /** * Version of the bugsnag_event struct. Serialized to report header. */ -#define BUGSNAG_EVENT_VERSION 7 +#define BUGSNAG_EVENT_VERSION 8 #ifdef __cplusplus extern "C" { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c index 64041dbb46..dbbf485d65 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.c @@ -63,13 +63,13 @@ static void *grow_array_for_index(void *array, const size_t element_count, return new_array; } -void bsg_set_feature_flag(bugsnag_event *env, const char *name, +void bsg_set_feature_flag(bugsnag_event *event, const char *name, const char *variant) { - int expected_index = feature_flag_index(env, name); + int expected_index = feature_flag_index(event, name); if (expected_index >= 0) { // feature flag already exists, so we overwrite the variant - bsg_feature_flag *flag = &env->feature_flags[expected_index]; + bsg_feature_flag *flag = &event->feature_flags[expected_index]; // make sure we release the existing variant, if one exists free(flag->variant); @@ -86,7 +86,7 @@ void bsg_set_feature_flag(bugsnag_event *env, const char *name, // this is a new feature flag, we need to insert it - which means we also // need a new array bsg_feature_flag *new_flags = - grow_array_for_index(env->feature_flags, env->feature_flag_count, + grow_array_for_index(event->feature_flags, event->feature_flag_count, sizeof(bsg_feature_flag), new_flag_index); // we cannot grow the feature flag array, so we return @@ -102,20 +102,20 @@ void bsg_set_feature_flag(bugsnag_event *env, const char *name, new_flags[new_flag_index].variant = NULL; } - env->feature_flags = new_flags; - env->feature_flag_count = env->feature_flag_count + 1; + event->feature_flags = new_flags; + event->feature_flag_count = event->feature_flag_count + 1; } } -void bsg_clear_feature_flag(bugsnag_event *env, const char *name) { - int flag_index = feature_flag_index(env, name); +void bsg_clear_feature_flag(bugsnag_event *event, const char *name) { + int flag_index = feature_flag_index(event, name); if (flag_index < 0) { // no such feature flag - early exit return; } - bsg_feature_flag *flag = &env->feature_flags[flag_index]; + bsg_feature_flag *flag = &event->feature_flags[flag_index]; // release the memory held for name and possibly the variant free(flag->name); @@ -124,24 +124,24 @@ void bsg_clear_feature_flag(bugsnag_event *env, const char *name) { // pack the array elements down to fill in the "gap" // we don't resize the array down by one, that gets handled when elements are // added - memmove(&env->feature_flags[flag_index], &env->feature_flags[flag_index + 1], - (env->feature_flag_count - flag_index - 1) * - sizeof(bsg_feature_flag)); + memmove( + &event->feature_flags[flag_index], &event->feature_flags[flag_index + 1], + (event->feature_flag_count - flag_index - 1) * sizeof(bsg_feature_flag)); // mark the array as having one-less flag - env->feature_flag_count = env->feature_flag_count - 1; + event->feature_flag_count = event->feature_flag_count - 1; } -void bsg_free_feature_flags(bugsnag_event *env) { - for (int index = 0; index < env->feature_flag_count; index++) { - free(env->feature_flags[index].name); - free(env->feature_flags[index].variant); +void bsg_free_feature_flags(bugsnag_event *event) { + for (int index = 0; index < event->feature_flag_count; index++) { + free(event->feature_flags[index].name); + free(event->feature_flags[index].variant); } - free(env->feature_flags); + free(event->feature_flags); - env->feature_flags = NULL; - env->feature_flag_count = 0; + event->feature_flags = NULL; + event->feature_flag_count = 0; } #ifdef __cplusplus diff --git a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h index 8c535c1d6e..834770a141 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/featureflags.h @@ -9,11 +9,11 @@ * name. Setting a `variant` to NULL is *not* the same as clearing the feature * flag, which must be done with `bsg_clear_feature_flag`. * - * @param env the environment to populate with the given feature flag + * @param event the environment to populate with the given feature flag * @param name the name of the feature flag (may not be NULL) * @param variant if non-NULL: the variant to set the feature flag to */ -void bsg_set_feature_flag(bugsnag_event *env, const char *name, +void bsg_set_feature_flag(bugsnag_event *event, const char *name, const char *variant); /** @@ -22,10 +22,10 @@ void bsg_set_feature_flag(bugsnag_event *env, const char *name, * will remain untouched. Any dynamic memory associated with the cleared feature * flag is correctly released. * - * @param env the environment to clear the given feature flag in + * @param event the environment to clear the given feature flag in * @param name the name of the feature flag to clear */ -void bsg_clear_feature_flag(bugsnag_event *env, const char *name); +void bsg_clear_feature_flag(bugsnag_event *event, const char *name); /** * Release all of the memory for all of the feature flags stored in the given @@ -33,8 +33,8 @@ void bsg_clear_feature_flag(bugsnag_event *env, const char *name); * and cleans up the array pointers, effectively clearing all of the feature * flags. * - * @param env the environment to remove all the feature flags from + * @param event the environment to remove all the feature flags from */ -void bsg_free_feature_flags(bugsnag_event *env); +void bsg_free_feature_flags(bugsnag_event *event); #endif // BUGSNAG_ANDROID_FEATUREFLAGS_H diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c index 6ad7fd25d0..99d7d9f3d4 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c @@ -1,9 +1,6 @@ -// -// Created by Jason Morris on 19/11/2021. -// - #include "buffered_writer.h" #include "bugsnag_ndk.h" +#include "string.h" #include #include #include @@ -43,6 +40,27 @@ static bool bsg_buffered_writer_flush(struct bsg_buffered_writer *writer) { return false; } +static bool bsg_buffered_write_string(bsg_buffered_writer *writer, + const char *s) { + // prefix with the string length uint32 + const uint32_t length = bsg_strlen(s); + if (!writer->write(writer, &length, sizeof(length))) { + return false; + } + + // then write the string data without trailing '\0' + if (!writer->write(writer, s, length)) { + return false; + } + + return true; +} + +static bool bsg_buffered_write_byte(bsg_buffered_writer *writer, + const uint8_t value) { + return writer->write(writer, &value, 1); +} + // The compiler keeps changing sizeof(size_t) on different platforms, which // breaks printf(). #if __SIZEOF_SIZE_T__ == 8 @@ -101,6 +119,8 @@ bool bsg_buffered_writer_open(struct bsg_buffered_writer *writer, writer->fd = fd; writer->pos = 0; writer->write = bsg_buffered_writer_write; + writer->write_string = bsg_buffered_write_string; + writer->write_byte = bsg_buffered_write_byte; writer->flush = bsg_buffered_writer_flush; writer->dispose = bsg_buffered_writer_close; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h index f4ca565a28..0578801f03 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h @@ -31,6 +31,26 @@ typedef struct bsg_buffered_writer { bool (*write)(struct bsg_buffered_writer *writer, const void *data, size_t length); + /** + * Write a single byte-value to this writer. + * + * @param writer This writer + * @param byte the single byte to be written + * @return True on success. Check errno on error. + */ + bool (*write_byte)(struct bsg_buffered_writer *writer, const uint8_t byte); + + /** + * Write a length-prefixed string to this writer. This will first write 4 + * bytes for the length of the string, and then the string itself (without + * it's null-terminator character). + * + * @param writer This writer + * @param string the string to write, may not be NULL + * @return True on success. Check errno on error. + */ + bool (*write_string)(struct bsg_buffered_writer *writer, const char *string); + /** * Force a flush to file. * @@ -45,7 +65,7 @@ typedef struct bsg_buffered_writer { * Dispose of this writer, closing and freeing all resources. The pointer to * writer will be invalid after this call. * - * Note: This method is NOT async-safe! + * Note: This method is async-safe! * * @param writer This writer. * @return True on success. Check errno on error. diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h index c9bfe05f1f..3d2ccea952 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h @@ -279,6 +279,35 @@ typedef struct { char api_key[64]; } bugsnag_report_v6; +typedef struct { + bsg_notifier notifier; + bsg_app_info app; + bsg_device_info device; + bugsnag_user user; + bsg_error error; + bugsnag_metadata metadata; + + int crumb_count; + // Breadcrumbs are a ring; the first index moves as the + // structure is filled and replaced. + int crumb_first_index; + bugsnag_breadcrumb breadcrumbs[BUGSNAG_CRUMBS_MAX]; + + char context[64]; + bugsnag_severity severity; + + char session_id[33]; + char session_start[33]; + int handled_events; + int unhandled_events; + char grouping_hash[64]; + bool unhandled; + char api_key[64]; + + int thread_count; + bsg_thread threads[BUGSNAG_THREADS_MAX]; +} bugsnag_report_v7; + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 99567402a5..e06f5cae58 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -22,6 +22,7 @@ bool bsg_event_write(struct bsg_buffered_writer *writer, bugsnag_event *bsg_event_read(int fd); bsg_report_header *bsg_report_header_read(int fd); +bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7); bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6); bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5); bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4); @@ -29,6 +30,9 @@ bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3); bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2); bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1); +void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, + size_t *out_feature_flag_count); + void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event); void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); @@ -66,10 +70,11 @@ bool bsg_serialize_event_to_file(bsg_environment *env) { goto fail; } - if (!bsg_write_feature_flags(env, &writer)) { + if (!bsg_write_feature_flags(&env->next_event, &writer)) { goto fail; } + writer.dispose(&writer); return true; fail: @@ -158,7 +163,52 @@ bugsnag_report_v6 *bsg_report_v6_read(int fd) { return event; } -bugsnag_event *bsg_report_v7_read(int fd) { +bugsnag_report_v7 *bsg_report_v7_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v7); + bugsnag_report_v7 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +static char *read_string(int fd) { + ssize_t len; + uint32_t string_length; + len = read(fd, &string_length, sizeof(string_length)); + + if (len != sizeof(string_length)) { + return NULL; + } + + // allocate enough space, zero filler, and with a trailing '\0' terminator + char *string_buffer = calloc(1, (string_length + 1)); + if (!string_buffer) { + return NULL; + } + + len = read(fd, string_buffer, string_length); + if (len != string_length) { + free(string_buffer); + return NULL; + } + + return string_buffer; +} + +int read_byte(int fd) { + char value; + if (read(fd, &value, 1) != 1) { + return -1; + } + + return value; +} + +bugsnag_event *bsg_report_v8_read(int fd) { size_t event_size = sizeof(bugsnag_event); bugsnag_event *event = calloc(1, event_size); @@ -167,6 +217,10 @@ bugsnag_event *bsg_report_v7_read(int fd) { free(event); return NULL; } + + // read the feature flags, if possible + bsg_read_feature_flags(fd, &event->feature_flags, &event->feature_flag_count); + return event; } @@ -209,7 +263,10 @@ bugsnag_event *bsg_event_read(int fd) { bugsnag_report_v6 *report_v6 = bsg_report_v6_read(fd); event = bsg_map_v6_to_report(report_v6); } else if (event_version == 7) { - event = bsg_report_v7_read(fd); + bugsnag_report_v7 *report_v7 = bsg_report_v7_read(fd); + event = bsg_map_v7_to_report(report_v7); + } else if (event_version == 8) { + event = bsg_report_v8_read(fd); } return event; } @@ -227,6 +284,19 @@ bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6) { return event; } +bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7) { + if (report_v7 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + memcpy(event, report_v7, sizeof(bugsnag_report_v7)); + free(report_v7); + } + return event; +} + bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { if (report_v5 == NULL) { return NULL; @@ -885,6 +955,26 @@ void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads) { } } +void bsg_serialize_feature_flags(const bugsnag_event *event, + JSON_Array *feature_flags) { + if (event->feature_flag_count <= 0) { + return; + } + + for (int index = 0; index < event->feature_flag_count; index++) { + JSON_Value *feature_flag_val = json_value_init_object(); + JSON_Object *feature_flag = json_value_get_object(feature_flag_val); + json_array_append_value(feature_flags, feature_flag_val); + + const bsg_feature_flag *flag = &event->feature_flags[index]; + json_object_set_string(feature_flag, "featureFlag", flag->name); + + if (flag->variant) { + json_object_set_string(feature_flag, "variant", flag->variant); + } + } +} + char *bsg_serialize_event_to_json_string(bugsnag_event *event) { JSON_Value *event_val = json_value_init_object(); JSON_Object *event_obj = json_value_get_object(event_val); @@ -898,10 +988,13 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event) { JSON_Array *threads = json_value_get_array(threads_val); JSON_Value *stack_val = json_value_init_array(); JSON_Array *stacktrace = json_value_get_array(stack_val); + JSON_Value *feature_flags_val = json_value_init_object(); + JSON_Array *feature_flags = json_value_get_array(feature_flags_val); json_object_set_value(event_obj, "exceptions", exceptions_val); json_object_set_value(event_obj, "breadcrumbs", crumbs_val); json_object_set_value(event_obj, "threads", threads_val); json_object_set_value(exception, "stacktrace", stack_val); + json_object_set_value(event_obj, "featureFlags", feature_flags_val); json_array_append_value(exceptions, ex_val); char *serialized_string = NULL; { @@ -918,6 +1011,7 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event) { bsg_serialize_error(event->error, exception, stacktrace); bsg_serialize_breadcrumbs(event, crumbs); bsg_serialize_threads(event, threads); + bsg_serialize_feature_flags(event, feature_flags); serialized_string = json_serialize_to_string(event_val); json_value_free(event_val); @@ -925,41 +1019,22 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event) { return serialized_string; } -static bool write_string(bsg_buffered_writer *writer, const char *s) { - // prefix with the string length uint32 - const uint32_t length = bsg_strlen(s); - if (!writer->write(writer, &length, sizeof(length))) { - return false; - } - - // then write the string data without trailing '\0' - if (!writer->write(writer, s, length)) { - return false; - } - - return true; -} - -static bool write_byte(bsg_buffered_writer *writer, const char value) { - return writer->write(writer, &value, 1); -} - static bool write_feature_flag(bsg_buffered_writer *writer, bsg_feature_flag *flag) { - if (!write_string(writer, flag->name)) { + if (!writer->write_string(writer, flag->name)) { return false; } if (flag->variant) { - if (!write_byte(writer, 1)) { + if (!writer->write_byte(writer, 1)) { return false; } - if (!write_string(writer, flag->variant)) { + if (!writer->write_string(writer, flag->variant)) { return false; } } else { - if (!write_byte(writer, 0)) { + if (!writer->write_byte(writer, 0)) { return false; } } @@ -967,18 +1042,77 @@ static bool write_feature_flag(bsg_buffered_writer *writer, return true; } -bool bsg_write_feature_flags(bsg_environment *env, +bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer) { - const uint32_t feature_flag_count = env->feature_flag_count; + const uint32_t feature_flag_count = event->feature_flag_count; if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { return false; } for (uint32_t index = 0; index < feature_flag_count; index++) { - if (!write_feature_flag(writer, &env->feature_flags[index])) { + if (!write_feature_flag(writer, &event->feature_flags[index])) { return false; } } return true; } + +void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, + size_t *out_feature_flag_count) { + + ssize_t len; + uint32_t feature_flag_count = 0; + len = read(fd, &feature_flag_count, sizeof(feature_flag_count)); + if (len != sizeof(feature_flag_count)) { + goto feature_flags_error; + } + + bsg_feature_flag *flags = + calloc(feature_flag_count, sizeof(bsg_feature_flag)); + for (uint32_t index = 0; index < feature_flag_count; index++) { + char *name = read_string(fd); + if (!name) { + goto feature_flags_error; + } + + int variant_exists = read_byte(fd); + if (variant_exists < 0) { + goto feature_flags_error; + } + + char *variant = NULL; + if (variant_exists) { + variant = read_string(fd); + if (!variant) { + goto feature_flags_error; + } + } + + flags[index].name = name; + flags[index].variant = variant; + } + + *out_feature_flag_count = feature_flag_count; + *out_feature_flags = flags; + + return; + +feature_flags_error: + // something wrong - we release all allocated memory + for (uint32_t index = 0; index < feature_flag_count; index++) { + if (flags[index].name) { + free(flags[index].name); + } + + if (flags[index].variant) { + free(flags[index].variant); + } + } + + free(flags); + + // clear the out fields to indicate no feature-flags are availables + *out_feature_flag_count = 0; + *out_feature_flags = NULL; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h index d99598bdca..a3a368b36d 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -45,13 +45,15 @@ void bsg_serialize_error(bsg_error exc, JSON_Object *exception, JSON_Array *stacktrace); void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs); void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads); +void bsg_serialize_feature_flags(const bugsnag_event *event, + JSON_Array *feature_flags); char *bsg_serialize_event_to_json_string(bugsnag_event *event); int bsg_calculate_total_crumbs(int old_count); int bsg_calculate_v1_start_index(int old_count); int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index); -bool bsg_write_feature_flags(bsg_environment *env, bsg_buffered_writer *writer); +bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer); #ifdef __cplusplus } diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 2adda27f82..10eb829e2a 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -457,12 +457,12 @@ TEST test_report_to_file(void) { TEST test_report_with_feature_flags_to_file(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = 7; + env->report_header.version = 8; env->report_header.big_endian = 1; bugsnag_event *report = bsg_generate_event(); memcpy(&env->next_event, report, sizeof(bugsnag_event)); - bsg_set_feature_flag(env, "sample_group", "a"); - bsg_set_feature_flag(env, "demo_mode", NULL); + bsg_set_feature_flag(&env->next_event, "sample_group", "a"); + bsg_set_feature_flag(&env->next_event, "demo_mode", NULL); strcpy(env->report_header.os_build, "macOS Sierra"); strcpy(env->next_event_path, SERIALIZE_TEST_FILE); ASSERT(bsg_serialize_event_to_file(env)); @@ -491,6 +491,28 @@ TEST test_file_to_report(void) { PASS(); } +TEST test_report_with_feature_flags_from_file(void) { + bsg_environment *env = calloc(1, sizeof(bsg_environment)); + env->report_header.version = 8; + env->report_header.big_endian = 1; + bugsnag_event *report = bsg_generate_event(); + memcpy(&env->next_event, report, sizeof(bugsnag_event)); + bsg_set_feature_flag(&env->next_event, "sample_group", "a"); + bsg_set_feature_flag(&env->next_event, "demo_mode", NULL); + strcpy(env->report_header.os_build, "macOS Sierra"); + strcpy(env->next_event_path, "/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); + ASSERT(bsg_serialize_event_to_file(env)); + + bugsnag_event *event = bsg_deserialize_event_from_file("/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); + + ASSERT_EQ(2, event->feature_flag_count); + + free(report); + free(env); + free(event); + PASS(); +} + TEST test_report_v1_migration(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); env->report_header.version = 1; @@ -930,6 +952,7 @@ SUITE(suite_struct_to_file) { RUN_TEST(test_report_to_file); RUN_TEST(test_file_to_report); RUN_TEST(test_report_with_feature_flags_to_file); + RUN_TEST(test_report_with_feature_flags_from_file); } SUITE(suite_struct_migration) { diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/bugsnag-java-scenarios.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/bugsnag-java-scenarios.cpp index 72e5205902..722625e3cc 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/bugsnag-java-scenarios.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/bugsnag-java-scenarios.cpp @@ -100,4 +100,10 @@ Java_com_bugsnag_android_mazerunner_scenarios_CXXConfigurationMetadataNativeCras return 12167; } +JNIEXPORT void JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_CXXFeatureFlagNativeCrashScenario_crash(JNIEnv *env, + jobject instance) { + __builtin_trap(); +} + } diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java new file mode 100644 index 0000000000..76032572d8 --- /dev/null +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java @@ -0,0 +1,31 @@ +package com.bugsnag.android.mazerunner.scenarios; + +import com.bugsnag.android.Bugsnag; +import com.bugsnag.android.Configuration; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class CXXFeatureFlagNativeCrashScenario extends Scenario { + static { + System.loadLibrary("cxx-scenarios"); + } + + public native void crash(); + + public CXXFeatureFlagNativeCrashScenario(@NonNull Configuration config, + @NonNull Context context, + @Nullable String eventMetadata) { + super(config, context, eventMetadata); + } + + @Override + public void startScenario() { + super.startScenario(); + Bugsnag.addFeatureFlag("demo_mode"); + Bugsnag.addFeatureFlag("sample_group", "a"); + crash(); + } +} diff --git a/features/full_tests/batch_2/native_feature_flags.feature b/features/full_tests/batch_2/native_feature_flags.feature new file mode 100644 index 0000000000..22743432c3 --- /dev/null +++ b/features/full_tests/batch_2/native_feature_flags.feature @@ -0,0 +1,10 @@ +Feature: Synchronizing feature flags to the native layer + + Scenario: Feature flags are synchronized to the native layer + When I run "CXXFeatureFlagNativeCrashScenario" + And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" + And I wait to receive an error + And the error payload contains a completed unhandled native report + And the exception "errorClass" equals "SIGILL" + And event 0 contains the feature flag "demo_mode" with no variant + And event 0 contains the feature flag "sample_group" with variant "a" From 41f8ea593d38efc84a7bdd4ca5e3980d059db13d Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 26 Nov 2021 08:57:58 +0000 Subject: [PATCH 09/37] fix(test): aligned EventRedactionTest with FeatureFlags --- .../src/test/resources/event_default_redaction.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bugsnag-android-core/src/test/resources/event_default_redaction.json b/bugsnag-android-core/src/test/resources/event_default_redaction.json index 090e3629d3..7c9e969d8c 100644 --- a/bugsnag-android-core/src/test/resources/event_default_redaction.json +++ b/bugsnag-android-core/src/test/resources/event_default_redaction.json @@ -51,5 +51,6 @@ } } ], - "threads": [] + "threads": [], + "featureFlags": [] } From fe1a4de741b7601baacda0bbb26f963ac2fa45ad Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 10 Nov 2021 14:53:11 +0000 Subject: [PATCH 10/37] test(feature flags): mazerunner feature to test feature flags --- .../java/com/bugsnag/android/Bugsnag.java | 3 +- .../CXXFeatureFlagNativeCrashScenario.java | 21 +++++++++ .../jvm-scenarios/detekt-baseline.xml | 1 + .../scenarios/FeatureFlagScenario.kt | 47 +++++++++++++++++++ features/full_tests/feature_flags.feature | 18 ++++++- .../full_tests/native_feature_flags.feature | 12 +++++ 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/FeatureFlagScenario.kt diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index 8ce1253a08..f32b49d6ae 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -447,7 +446,7 @@ public static void addFeatureFlags(@NonNull Iterable featureFlags) * @param name the name of the feature flag to remove */ public static void clearFeatureFlag(@NonNull String name) { - getClient().clearMetadata(name); + getClient().clearFeatureFlag(name); } /** diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java index 76032572d8..df04de035e 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXFeatureFlagNativeCrashScenario.java @@ -2,12 +2,15 @@ import com.bugsnag.android.Bugsnag; import com.bugsnag.android.Configuration; +import com.bugsnag.android.FeatureFlag; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Arrays; + public class CXXFeatureFlagNativeCrashScenario extends Scenario { static { System.loadLibrary("cxx-scenarios"); @@ -24,8 +27,26 @@ public CXXFeatureFlagNativeCrashScenario(@NonNull Configuration config, @Override public void startScenario() { super.startScenario(); + + Bugsnag.addFeatureFlag("demo_mode"); + + Bugsnag.addFeatureFlags(Arrays.asList( + new FeatureFlag("should_not_be_reported_1"), + new FeatureFlag("should_not_be_reported_2"), + new FeatureFlag("should_not_be_reported_3") + )); + + Bugsnag.clearFeatureFlag("should_not_be_reported_3"); + Bugsnag.clearFeatureFlag("should_not_be_reported_2"); + Bugsnag.clearFeatureFlag("should_not_be_reported_1"); + Bugsnag.addFeatureFlag("demo_mode"); Bugsnag.addFeatureFlag("sample_group", "a"); + + if (getEventMetadata() != null && getEventMetadata().contains("cleared")) { + Bugsnag.clearFeatureFlags(); + } + crash(); } } diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index 660c0114eb..da1d3c9507 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -23,6 +23,7 @@ TooGenericExceptionThrown:CrashHandlerScenario.kt$CrashHandlerScenario$throw RuntimeException("CrashHandlerScenario") TooGenericExceptionThrown:CustomHttpClientFlushScenario.kt$CustomHttpClientFlushScenario$throw RuntimeException("ReportCacheScenario") TooGenericExceptionThrown:DisableAutoDetectErrorsScenario.kt$DisableAutoDetectErrorsScenario$throw RuntimeException("Should never appear") + TooGenericExceptionThrown:FeatureFlagScenario.kt$FeatureFlagScenario$throw RuntimeException("FeatureFlagScenario unhandled") TooGenericExceptionThrown:ReportCacheScenario.kt$ReportCacheScenario$throw RuntimeException("ReportCacheScenario") TooGenericExceptionThrown:StartupCrashFlushScenario.kt$StartupCrashFlushScenario$throw RuntimeException("Regular crash") TooGenericExceptionThrown:StartupCrashFlushScenario.kt$StartupCrashFlushScenario$throw RuntimeException("Startup crash") diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/FeatureFlagScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/FeatureFlagScenario.kt new file mode 100644 index 0000000000..801bcca58a --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/FeatureFlagScenario.kt @@ -0,0 +1,47 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.FeatureFlag + +class FeatureFlagScenario( + config: Configuration, + context: Context, + eventMetadata: String +) : Scenario(config, context, eventMetadata) { + override fun startScenario() { + super.startScenario() + + Bugsnag.addFeatureFlag("demo_mode") + + Bugsnag.addFeatureFlags( + listOf( + FeatureFlag("should_not_be_reported_1"), + FeatureFlag("should_not_be_reported_2"), + FeatureFlag("should_not_be_reported_3") + ) + ) + + Bugsnag.clearFeatureFlag("should_not_be_reported_3") + Bugsnag.clearFeatureFlag("should_not_be_reported_2") + Bugsnag.clearFeatureFlag("should_not_be_reported_1") + + if (eventMetadata?.contains("callback") == true) { + Bugsnag.addOnError { error -> + error.addFeatureFlag("sample_group", "a") + return@addOnError true + } + } + + if (eventMetadata?.contains("cleared") == true) { + Bugsnag.clearFeatureFlags() + } + + if (eventMetadata?.contains("unhandled") == true) { + throw RuntimeException("FeatureFlagScenario unhandled") + } else { + Bugsnag.notify(RuntimeException("FeatureFlagScenario handled")) + } + } +} diff --git a/features/full_tests/feature_flags.feature b/features/full_tests/feature_flags.feature index 30fdeba6b2..4d2ba7384a 100644 --- a/features/full_tests/feature_flags.feature +++ b/features/full_tests/feature_flags.feature @@ -6,7 +6,9 @@ Scenario: Sends handled exception which includes feature flags And the exception "errorClass" equals "java.lang.RuntimeException" And the event "unhandled" is false And event 0 contains the feature flag "demo_mode" with no variant - And event 0 does not contain the feature flag "demo_mode" with no variant + And event 0 does not contain the feature flag "should_not_be_reported_1" + And event 0 does not contain the feature flag "should_not_be_reported_2" + And event 0 does not contain the feature flag "should_not_be_reported_3" Scenario: Sends handled exception which includes feature flags added in the notify callback When I configure the app to run in the "callback" state @@ -16,6 +18,9 @@ Scenario: Sends handled exception which includes feature flags added in the noti And the event "unhandled" is false And event 0 contains the feature flag "demo_mode" with no variant And event 0 contains the feature flag "sample_group" with variant "a" + And event 0 does not contain the feature flag "should_not_be_reported_1" + And event 0 does not contain the feature flag "should_not_be_reported_2" + And event 0 does not contain the feature flag "should_not_be_reported_3" Scenario: Sends unhandled exception which includes feature flags added in the notify callback When I configure the app to run in the "unhandled callback" state @@ -26,3 +31,14 @@ Scenario: Sends unhandled exception which includes feature flags added in the no And the event "unhandled" is true And event 0 contains the feature flag "demo_mode" with no variant And event 0 contains the feature flag "sample_group" with variant "a" + And event 0 does not contain the feature flag "should_not_be_reported_1" + And event 0 does not contain the feature flag "should_not_be_reported_2" + And event 0 does not contain the feature flag "should_not_be_reported_3" + +Scenario: Sends no feature flags after clearFeatureFlags() + When I configure the app to run in the "cleared" state + And I run "FeatureFlagScenario" + Then I wait to receive an error + And the exception "errorClass" equals "java.lang.RuntimeException" + And the event "unhandled" is false + And event 0 has no feature flags diff --git a/features/full_tests/native_feature_flags.feature b/features/full_tests/native_feature_flags.feature index 22743432c3..afa3219b32 100644 --- a/features/full_tests/native_feature_flags.feature +++ b/features/full_tests/native_feature_flags.feature @@ -8,3 +8,15 @@ Feature: Synchronizing feature flags to the native layer And the exception "errorClass" equals "SIGILL" And event 0 contains the feature flag "demo_mode" with no variant And event 0 contains the feature flag "sample_group" with variant "a" + And event 0 does not contain the feature flag "should_not_be_reported_1" + And event 0 does not contain the feature flag "should_not_be_reported_2" + And event 0 does not contain the feature flag "should_not_be_reported_3" + + Scenario: clearFeatureFlags() is synchronized to the native layer + When I configure the app to run in the "cleared" state + And I run "CXXFeatureFlagNativeCrashScenario" + And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" + And I wait to receive an error + And the error payload contains a completed unhandled native report + And the exception "errorClass" equals "SIGILL" + And event 0 has no feature flags From 493f3e201192153501441f1e97466ac7f270fea2 Mon Sep 17 00:00:00 2001 From: Delisa Date: Wed, 8 Dec 2021 12:44:19 +0000 Subject: [PATCH 11/37] refactor(ndk): separate event reading funcs into own file (#1545) --- .../src/main/CMakeLists.txt | 1 + .../src/main/jni/utils/serializer.c | 664 +---------------- .../main/jni/utils/serializer/event_reader.c | 672 ++++++++++++++++++ .../main/jni/utils/serializer/event_reader.h | 18 + .../main/jni/utils/{ => serializer}/migrate.h | 0 .../src/test/cpp/main.c | 5 +- .../src/test/cpp/test_serializer.h | 4 +- .../src/test/cpp/test_utils_serialize.c | 6 +- 8 files changed, 702 insertions(+), 668 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h rename bugsnag-plugin-android-ndk/src/main/jni/utils/{ => serializer}/migrate.h (100%) diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index fd8a8ed255..4332d72559 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -15,6 +15,7 @@ add_library( # Specifies the name of the library. jni/handlers/signal_handler.c jni/handlers/cpp_handler.cpp jni/utils/crash_info.c + jni/utils/serializer/event_reader.c jni/utils/stack_unwinder.c jni/utils/stack_unwinder_libunwindstack.cpp jni/utils/stack_unwinder_libcorkscrew.c diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index d58f9b0a04..3e4c439057 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -1,7 +1,9 @@ #include "serializer.h" -#include "buffered_writer.h" +#include "serializer/event_reader.h" #include "string.h" +#include "buffered_writer.h" + #include #include #include @@ -11,39 +13,10 @@ #include #include #include -#include - -#ifdef __cplusplus -extern "C" { -#endif bool bsg_event_write(struct bsg_buffered_writer *writer, bsg_report_header *header, bugsnag_event *event); -bugsnag_event *bsg_event_read(int fd); -bsg_report_header *bsg_report_header_read(int fd); -bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7); -bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6); -bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5); -bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4); -bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3); -bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2); -bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1); - -void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, - size_t *out_feature_flag_count); - -void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); -void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event); -void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); -void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, - bugsnag_report_v3 *event); -void migrate_breadcrumb_v2(bugsnag_report_v5 *report_v5, bugsnag_event *event); - -#ifdef __cplusplus -} -#endif - /** * Serializes the LastRunInfo to the file. This persists information about * why the current launch crashed, for use on future launch. @@ -83,577 +56,7 @@ bool bsg_serialize_event_to_file(bsg_environment *env) { } bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { - int fd = open(filepath, O_RDONLY); - if (fd == -1) { - return NULL; - } - - return bsg_event_read(fd); -} - -bugsnag_report_v1 *bsg_report_v1_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v1); - bugsnag_report_v1 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v2 *bsg_report_v2_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v2); - bugsnag_report_v2 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v3 *bsg_report_v3_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v3); - bugsnag_report_v3 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v4 *bsg_report_v4_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v4); - bugsnag_report_v4 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v5 *bsg_report_v5_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v5); - bugsnag_report_v5 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v6 *bsg_report_v6_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v6); - bugsnag_report_v6 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -bugsnag_report_v7 *bsg_report_v7_read(int fd) { - size_t event_size = sizeof(bugsnag_report_v7); - bugsnag_report_v7 *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - return event; -} - -static char *read_string(int fd) { - ssize_t len; - uint32_t string_length; - len = read(fd, &string_length, sizeof(string_length)); - - if (len != sizeof(string_length)) { - return NULL; - } - - // allocate enough space, zero filler, and with a trailing '\0' terminator - char *string_buffer = calloc(1, (string_length + 1)); - if (!string_buffer) { - return NULL; - } - - len = read(fd, string_buffer, string_length); - if (len != string_length) { - free(string_buffer); - return NULL; - } - - return string_buffer; -} - -int read_byte(int fd) { - char value; - if (read(fd, &value, 1) != 1) { - return -1; - } - - return value; -} - -bugsnag_event *bsg_report_v8_read(int fd) { - size_t event_size = sizeof(bugsnag_event); - bugsnag_event *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - free(event); - return NULL; - } - - // read the feature flags, if possible - bsg_read_feature_flags(fd, &event->feature_flags, &event->feature_flag_count); - - return event; -} - -/** - * Reads persisted structs into memory from disk. The report version is - * serialized in the file header, and old structs are maintained in migrate.h - * for backwards compatibility. These are then migrated to the current - * bugsnag_event struct. - * - * Note that calling the individual bsg_map_v functions will free the parameter - * - this is to conserve memory when migrating particularly old payload - * versions. - */ -bugsnag_event *bsg_event_read(int fd) { - bsg_report_header *header = bsg_report_header_read(fd); - if (header == NULL) { - return NULL; - } - - int event_version = header->version; - free(header); - bugsnag_event *event = NULL; - - if (event_version == 1) { // 'event->unhandled_events' was added in v2 - bugsnag_report_v1 *report_v1 = bsg_report_v1_read(fd); - event = bsg_map_v1_to_report(report_v1); - } else if (event_version == 2) { - bugsnag_report_v2 *report_v2 = bsg_report_v2_read(fd); - event = bsg_map_v2_to_report(report_v2); - } else if (event_version == 3) { - bugsnag_report_v3 *report_v3 = bsg_report_v3_read(fd); - event = bsg_map_v3_to_report(report_v3); - } else if (event_version == 4) { - bugsnag_report_v4 *report_v4 = bsg_report_v4_read(fd); - event = bsg_map_v4_to_report(report_v4); - } else if (event_version == 5) { - bugsnag_report_v5 *report_v5 = bsg_report_v5_read(fd); - event = bsg_map_v5_to_report(report_v5); - } else if (event_version == 6) { - bugsnag_report_v6 *report_v6 = bsg_report_v6_read(fd); - event = bsg_map_v6_to_report(report_v6); - } else if (event_version == 7) { - bugsnag_report_v7 *report_v7 = bsg_report_v7_read(fd); - event = bsg_map_v7_to_report(report_v7); - } else if (event_version == 8) { - event = bsg_report_v8_read(fd); - } - return event; -} - -bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6) { - if (report_v6 == NULL) { - return NULL; - } - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - - if (event != NULL) { - memcpy(event, report_v6, sizeof(bugsnag_report_v6)); - free(report_v6); - } - return event; -} - -bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7) { - if (report_v7 == NULL) { - return NULL; - } - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - - if (event != NULL) { - memcpy(event, report_v7, sizeof(bugsnag_report_v7)); - free(report_v7); - } - return event; -} - -bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { - if (report_v5 == NULL) { - return NULL; - } - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - - if (event != NULL) { - event->notifier = report_v5->notifier; - event->app = report_v5->app; - event->device = report_v5->device; - bsg_strcpy(event->context, report_v5->context); - event->user = report_v5->user; - event->error = report_v5->error; - event->metadata = report_v5->metadata; - event->severity = report_v5->severity; - bsg_strncpy_safe(event->session_id, report_v5->session_id, - sizeof(event->session_id)); - bsg_strncpy_safe(event->session_start, report_v5->session_start, - sizeof(event->session_id)); - event->handled_events = report_v5->handled_events; - event->unhandled_events = report_v5->unhandled_events; - bsg_strncpy_safe(event->grouping_hash, report_v5->grouping_hash, - sizeof(event->session_id)); - event->unhandled = report_v5->unhandled; - bsg_strncpy_safe(event->api_key, report_v5->api_key, - sizeof(event->api_key)); - - migrate_breadcrumb_v2(report_v5, event); - free(report_v5); - } - return event; -} - -bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { - if (report_v4 == NULL) { - return NULL; - } - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - - if (event != NULL) { - event->notifier = report_v4->notifier; - event->device = report_v4->device; - event->user = report_v4->user; - event->error = report_v4->error; - event->metadata = report_v4->metadata; - event->crumb_count = report_v4->crumb_count; - event->crumb_first_index = report_v4->crumb_first_index; - memcpy(event->breadcrumbs, report_v4->breadcrumbs, - sizeof(event->breadcrumbs)); - event->severity = report_v4->severity; - bsg_strncpy_safe(event->session_id, report_v4->session_id, - sizeof(event->session_id)); - bsg_strncpy_safe(event->session_start, report_v4->session_start, - sizeof(event->session_id)); - event->handled_events = report_v4->handled_events; - event->unhandled_events = report_v4->unhandled_events; - bsg_strncpy_safe(event->grouping_hash, report_v4->grouping_hash, - sizeof(event->session_id)); - event->unhandled = report_v4->unhandled; - bsg_strncpy_safe(event->api_key, report_v4->api_key, - sizeof(event->api_key)); - migrate_app_v2(report_v4, event); - free(report_v4); - } - return event; -} - -bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3) { - if (report_v3 == NULL) { - return NULL; - } - bugsnag_report_v4 *event = calloc(1, sizeof(bugsnag_event)); - - if (event != NULL) { - event->notifier = report_v3->notifier; - event->app = report_v3->app; - event->device = report_v3->device; - event->user = report_v3->user; - event->error = report_v3->error; - event->metadata = report_v3->metadata; - event->crumb_count = report_v3->crumb_count; - event->crumb_first_index = report_v3->crumb_first_index; - memcpy(event->breadcrumbs, report_v3->breadcrumbs, - sizeof(event->breadcrumbs)); - event->severity = report_v3->severity; - strcpy(event->session_id, report_v3->session_id); - strcpy(event->session_start, report_v3->session_start); - event->handled_events = report_v3->handled_events; - event->unhandled_events = report_v3->unhandled_events; - strcpy(event->grouping_hash, report_v3->grouping_hash); - event->unhandled = report_v3->unhandled; - - // set a default value for the api key - strcpy(event->api_key, ""); - free(report_v3); - } - return bsg_map_v4_to_report(event); -} - -bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2) { - if (report_v2 == NULL) { - return NULL; - } - bugsnag_report_v3 *event = calloc(1, sizeof(bugsnag_report_v3)); - - if (event != NULL) { - // assign metadata first as old app/device fields are migrated there - event->metadata = report_v2->metadata; - migrate_app_v1(report_v2, event); - migrate_device_v1(report_v2, event); - event->user = report_v2->user; - migrate_breadcrumb_v1(report_v2, event); - - strcpy(event->context, report_v2->context); - event->severity = report_v2->severity; - strcpy(event->session_id, report_v2->session_id); - strcpy(event->session_start, report_v2->session_start); - event->handled_events = report_v2->handled_events; - event->unhandled_events = report_v2->unhandled_events; - - // migrate changed notifier fields - strcpy(event->notifier.version, report_v2->notifier.version); - strcpy(event->notifier.name, report_v2->notifier.name); - strcpy(event->notifier.url, report_v2->notifier.url); - - // migrate changed error fields - strcpy(event->error.errorClass, report_v2->exception.name); - strcpy(event->error.errorMessage, report_v2->exception.message); - strcpy(event->error.type, report_v2->exception.type); - event->error.frame_count = report_v2->exception.frame_count; - size_t error_size = sizeof(bugsnag_stackframe) * BUGSNAG_FRAMES_MAX; - memcpy(&event->error.stacktrace, report_v2->exception.stacktrace, - error_size); - - // Fatal C errors are always true by default, previously this was hardcoded - // and not a field on the struct - event->unhandled = true; - free(report_v2); - } - return bsg_map_v3_to_report(event); -} - -int bsg_calculate_total_crumbs(int old_count) { - return old_count < BUGSNAG_CRUMBS_MAX ? old_count : BUGSNAG_CRUMBS_MAX; -} - -int bsg_calculate_v1_start_index(int old_count) { - return old_count < V2_BUGSNAG_CRUMBS_MAX ? 0 - : old_count % V2_BUGSNAG_CRUMBS_MAX; -} - -int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index) { - return (crumb_pos + first_index) % V1_BUGSNAG_CRUMBS_MAX; -} - -void bugsnag_report_v3_add_breadcrumb(bugsnag_report_v3 *event, - bugsnag_breadcrumb *crumb) { - int crumb_index; - if (event->crumb_count < V2_BUGSNAG_CRUMBS_MAX) { - crumb_index = event->crumb_count; - event->crumb_count++; - } else { - crumb_index = event->crumb_first_index; - event->crumb_first_index = - (event->crumb_first_index + 1) % V2_BUGSNAG_CRUMBS_MAX; - } - memcpy(&event->breadcrumbs[crumb_index], crumb, sizeof(bugsnag_breadcrumb)); -} - -void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, - bugsnag_report_v3 *event) { - event->crumb_count = 0; - event->crumb_first_index = 0; - - // previously breadcrumbs had 30 elements, now they have 25. - // if more than 25 breadcrumbs were collected in the legacy report, - // offset them accordingly by moving the start position by count - - // BUGSNAG_CRUMBS_MAX. - int new_crumb_total = bsg_calculate_total_crumbs(report_v2->crumb_count); - int k = bsg_calculate_v1_start_index(report_v2->crumb_count); - - for (; k < new_crumb_total; k++) { - int crumb_index = - bsg_calculate_v1_crumb_index(k, report_v2->crumb_first_index); - bugsnag_breadcrumb_v1 *old_crumb = &report_v2->breadcrumbs[crumb_index]; - bugsnag_breadcrumb *new_crumb = calloc(1, sizeof(bugsnag_breadcrumb)); - - // copy old crumb fields to new - new_crumb->type = old_crumb->type; - bsg_strncpy_safe(new_crumb->name, old_crumb->name, sizeof(new_crumb->name)); - bsg_strncpy_safe(new_crumb->timestamp, old_crumb->timestamp, - sizeof(new_crumb->timestamp)); - - for (int j = 0; j < 8; j++) { - bsg_char_metadata_pair pair = old_crumb->metadata[j]; - - if (strlen(pair.value) > 0 && strlen(pair.key) > 0) { - bsg_add_metadata_value_str(&new_crumb->metadata, "metaData", pair.key, - pair.value); - } - } - - // add crumb to event by copying memory - bugsnag_report_v3_add_breadcrumb(event, new_crumb); - free(new_crumb); - } -} - -void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { - bsg_strcpy(event->app.id, report_v2->app.id); - bsg_strcpy(event->app.release_stage, report_v2->app.release_stage); - bsg_strcpy(event->app.type, report_v2->app.type); - bsg_strcpy(event->app.version, report_v2->app.version); - bsg_strcpy(event->app.active_screen, report_v2->app.active_screen); - bsg_strcpy(event->app.build_uuid, report_v2->app.build_uuid); - bsg_strcpy(event->app.binary_arch, report_v2->app.binaryArch); - event->app.version_code = report_v2->app.version_code; - event->app.duration = report_v2->app.duration; - event->app.duration_in_foreground = report_v2->app.duration_in_foreground; - event->app.duration_ms_offset = report_v2->app.duration_ms_offset; - event->app.duration_in_foreground_ms_offset = - report_v2->app.duration_in_foreground_ms_offset; - event->app.in_foreground = report_v2->app.in_foreground; - - // migrate legacy fields to metadata - bugsnag_event_add_metadata_string(event, "app", "packageName", - report_v2->app.package_name); - bugsnag_event_add_metadata_string(event, "app", "versionName", - report_v2->app.version_name); - bugsnag_event_add_metadata_string(event, "app", "name", report_v2->app.name); -} - -void migrate_breadcrumb_v2(bugsnag_report_v5 *report_v5, bugsnag_event *event) { - int old_first_index = report_v5->crumb_first_index; - event->crumb_count = report_v5->crumb_count; - event->crumb_first_index = 0; // sort crumbs while copying across - - // rationalize order of breadcrumbs while copying over to new struct - for (int new_index = 0; new_index < event->crumb_count; new_index++) { - int old_index = (new_index + old_first_index) % V2_BUGSNAG_CRUMBS_MAX; - bugsnag_breadcrumb crumb = report_v5->breadcrumbs[old_index]; - memcpy(&event->breadcrumbs[new_index], &crumb, sizeof(bugsnag_breadcrumb)); - } -} - -void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event) { - bsg_strncpy_safe(event->app.id, report_v4->app.id, sizeof(event->app.id)); - bsg_strncpy_safe(event->app.release_stage, report_v4->app.release_stage, - sizeof(event->app.release_stage)); - bsg_strncpy_safe(event->app.type, report_v4->app.type, - sizeof(event->app.type)); - bsg_strncpy_safe(event->app.version, report_v4->app.version, - sizeof(event->app.version)); - bsg_strncpy_safe(event->app.active_screen, report_v4->app.active_screen, - sizeof(event->app.active_screen)); - bsg_strncpy_safe(event->app.build_uuid, report_v4->app.build_uuid, - sizeof(event->app.build_uuid)); - bsg_strncpy_safe(event->app.binary_arch, report_v4->app.binary_arch, - sizeof(event->app.binary_arch)); - event->app.version_code = report_v4->app.version_code; - event->app.duration = report_v4->app.duration; - event->app.duration_in_foreground = report_v4->app.duration_in_foreground; - event->app.duration_ms_offset = report_v4->app.duration_ms_offset; - event->app.duration_in_foreground_ms_offset = - report_v4->app.duration_in_foreground_ms_offset; - event->app.in_foreground = report_v4->app.in_foreground; - - // no info available, set to sensible default - event->app.is_launching = false; -} - -void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { - bsg_strcpy(event->device.os_name, - bsg_os_name()); // os_name was not a field in v2 - event->device.api_level = report_v2->device.api_level; - event->device.cpu_abi_count = report_v2->device.cpu_abi_count; - event->device.time = report_v2->device.time; - event->device.jailbroken = report_v2->device.jailbroken; - event->device.total_memory = report_v2->device.total_memory; - - for (int k = 0; k < report_v2->device.cpu_abi_count && - k < sizeof(report_v2->device.cpu_abi); - k++) { - bsg_strcpy(event->device.cpu_abi[k].value, - report_v2->device.cpu_abi[k].value); - event->device.cpu_abi_count++; - } - - bsg_strcpy(event->device.orientation, report_v2->device.orientation); - bsg_strcpy(event->device.id, report_v2->device.id); - bsg_strcpy(event->device.locale, report_v2->device.locale); - bsg_strcpy(event->device.manufacturer, report_v2->device.manufacturer); - bsg_strcpy(event->device.model, report_v2->device.model); - bsg_strcpy(event->device.os_build, report_v2->device.os_build); - bsg_strcpy(event->device.os_version, report_v2->device.os_version); - - // migrate legacy fields to metadata - bugsnag_event_add_metadata_bool(event, "device", "emulator", - report_v2->device.emulator); - bugsnag_event_add_metadata_double(event, "device", "dpi", - report_v2->device.dpi); - bugsnag_event_add_metadata_double(event, "device", "screenDensity", - report_v2->device.screen_density); - bugsnag_event_add_metadata_double(event, "device", "batteryLevel", - report_v2->device.battery_level); - bugsnag_event_add_metadata_string(event, "device", "locationStatus", - report_v2->device.location_status); - bugsnag_event_add_metadata_string(event, "device", "brand", - report_v2->device.brand); - bugsnag_event_add_metadata_string(event, "device", "networkAccess", - report_v2->device.network_access); - bugsnag_event_add_metadata_string(event, "device", "screenResolution", - report_v2->device.screen_resolution); -} - -bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1) { - if (report_v1 == NULL) { - return NULL; - } - size_t report_size = sizeof(bugsnag_report_v2); - bugsnag_report_v2 *event_v2 = calloc(1, report_size); - - if (event_v2 != NULL) { - event_v2->notifier = report_v1->notifier; - event_v2->app = report_v1->app; - event_v2->device = report_v1->device; - event_v2->user = report_v1->user; - event_v2->exception = report_v1->exception; - event_v2->metadata = report_v1->metadata; - event_v2->crumb_count = report_v1->crumb_count; - event_v2->crumb_first_index = report_v1->crumb_first_index; - - size_t breadcrumb_size = - sizeof(bugsnag_breadcrumb_v1) * V1_BUGSNAG_CRUMBS_MAX; - memcpy(&event_v2->breadcrumbs, report_v1->breadcrumbs, breadcrumb_size); - - strcpy(event_v2->context, report_v1->context); - event_v2->severity = report_v1->severity; - strcpy(event_v2->session_id, report_v1->session_id); - strcpy(event_v2->session_start, report_v1->session_start); - event_v2->handled_events = report_v1->handled_events; - event_v2->unhandled_events = 1; - - free(report_v1); - } - return bsg_map_v2_to_report(event_v2); -} - -bsg_report_header *bsg_report_header_read(int fd) { - bsg_report_header *header = calloc(1, sizeof(bsg_report_header)); - ssize_t len = read(fd, header, sizeof(bsg_report_header)); - if (len != sizeof(bsg_report_header)) { - free(header); - return NULL; - } - - return header; + return bsg_read_event(filepath); } bool bsg_report_header_write(bsg_report_header *header, int fd) { @@ -1117,62 +520,3 @@ bool bsg_write_feature_flags(bugsnag_event *event, return true; } - -void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, - size_t *out_feature_flag_count) { - - ssize_t len; - uint32_t feature_flag_count = 0; - len = read(fd, &feature_flag_count, sizeof(feature_flag_count)); - if (len != sizeof(feature_flag_count)) { - goto feature_flags_error; - } - - bsg_feature_flag *flags = - calloc(feature_flag_count, sizeof(bsg_feature_flag)); - for (uint32_t index = 0; index < feature_flag_count; index++) { - char *name = read_string(fd); - if (!name) { - goto feature_flags_error; - } - - int variant_exists = read_byte(fd); - if (variant_exists < 0) { - goto feature_flags_error; - } - - char *variant = NULL; - if (variant_exists) { - variant = read_string(fd); - if (!variant) { - goto feature_flags_error; - } - } - - flags[index].name = name; - flags[index].variant = variant; - } - - *out_feature_flag_count = feature_flag_count; - *out_feature_flags = flags; - - return; - -feature_flags_error: - // something wrong - we release all allocated memory - for (uint32_t index = 0; index < feature_flag_count; index++) { - if (flags[index].name) { - free(flags[index].name); - } - - if (flags[index].variant) { - free(flags[index].variant); - } - } - - free(flags); - - // clear the out fields to indicate no feature-flags are availables - *out_feature_flag_count = 0; - *out_feature_flags = NULL; -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c new file mode 100644 index 0000000000..c413a64f2c --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c @@ -0,0 +1,672 @@ +#include "event_reader.h" +#include "migrate.h" + +#include "../../event.h" +#include "../string.h" + +#include +#include +#include +#include + +const int BSG_MIGRATOR_CURRENT_VERSION = 8; + +#ifdef __cplusplus +extern "C" { +#endif + +// Symbols exported for unit test access +void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); +void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event); +void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); +#ifdef __cplusplus +} +#endif + +/** + * the *_read() functions take a file descriptor and read a payload in the + * {version} format. This allocates an instance of the return value type. + */ + +bugsnag_report_v1 *bsg_report_v1_read(int fd); +bugsnag_report_v2 *bsg_report_v2_read(int fd); +bugsnag_report_v3 *bsg_report_v3_read(int fd); +bugsnag_report_v4 *bsg_report_v4_read(int fd); +bugsnag_report_v5 *bsg_report_v5_read(int fd); +bugsnag_report_v6 *bsg_report_v6_read(int fd); +bugsnag_report_v7 *bsg_report_v7_read(int fd); +bugsnag_event *bsg_report_v8_read(int fd); + +/** + * the map_*() functions convert a structure of an older format into the latest. + * This frees the parameter provided to the function once conversion is + * complete. + */ + +bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7); +bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6); +bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5); +bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4); +bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3); +bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2); +bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1); + +void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, + bugsnag_report_v3 *event); +void migrate_breadcrumb_v2(bugsnag_report_v5 *report_v5, bugsnag_event *event); + +void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, + size_t *out_feature_flag_count); + +bsg_report_header *bsg_report_header_read(int fd) { + bsg_report_header *header = calloc(1, sizeof(bsg_report_header)); + ssize_t len = read(fd, header, sizeof(bsg_report_header)); + if (len != sizeof(bsg_report_header)) { + free(header); + return NULL; + } + + return header; +} + +bugsnag_event *bsg_read_event(char *filepath) { + int fildes = open(filepath, O_RDONLY); + if (fildes == -1) { + return NULL; + } + + bsg_report_header *header = bsg_report_header_read(fildes); + if (header == NULL) { + return NULL; + } + + int version = header->version; + free(header); + bugsnag_event *event = NULL; + + switch (version) { + case 1: + return bsg_map_v1_to_report(bsg_report_v1_read(fildes)); + case 2: + return bsg_map_v2_to_report(bsg_report_v2_read(fildes)); + case 3: + return bsg_map_v3_to_report(bsg_report_v3_read(fildes)); + case 4: + return bsg_map_v4_to_report(bsg_report_v4_read(fildes)); + case 5: + return bsg_map_v5_to_report(bsg_report_v5_read(fildes)); + case 6: + return bsg_map_v6_to_report(bsg_report_v6_read(fildes)); + case 7: + return bsg_map_v7_to_report(bsg_report_v7_read(fildes)); + case BSG_MIGRATOR_CURRENT_VERSION: + return bsg_report_v8_read(fildes); + default: + return NULL; + } +} + +bugsnag_report_v1 *bsg_report_v1_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v1); + bugsnag_report_v1 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v2 *bsg_report_v2_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v2); + bugsnag_report_v2 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v3 *bsg_report_v3_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v3); + bugsnag_report_v3 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v4 *bsg_report_v4_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v4); + bugsnag_report_v4 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v5 *bsg_report_v5_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v5); + bugsnag_report_v5 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v6 *bsg_report_v6_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v6); + bugsnag_report_v6 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_report_v7 *bsg_report_v7_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v7); + bugsnag_report_v7 *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_event *bsg_report_v8_read(int fd) { + size_t event_size = sizeof(bugsnag_event); + bugsnag_event *event = calloc(1, event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + + // read the feature flags, if possible + bsg_read_feature_flags(fd, &event->feature_flags, &event->feature_flag_count); + + return event; +} + +bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6) { + if (report_v6 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + memcpy(event, report_v6, sizeof(bugsnag_report_v6)); + free(report_v6); + } + return event; +} + +bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7) { + if (report_v7 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + memcpy(event, report_v7, sizeof(bugsnag_report_v7)); + free(report_v7); + } + return event; +} + +bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { + if (report_v5 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + event->notifier = report_v5->notifier; + event->app = report_v5->app; + event->device = report_v5->device; + bsg_strcpy(event->context, report_v5->context); + event->user = report_v5->user; + event->error = report_v5->error; + event->metadata = report_v5->metadata; + event->severity = report_v5->severity; + bsg_strncpy_safe(event->session_id, report_v5->session_id, + sizeof(event->session_id)); + bsg_strncpy_safe(event->session_start, report_v5->session_start, + sizeof(event->session_id)); + event->handled_events = report_v5->handled_events; + event->unhandled_events = report_v5->unhandled_events; + bsg_strncpy_safe(event->grouping_hash, report_v5->grouping_hash, + sizeof(event->session_id)); + event->unhandled = report_v5->unhandled; + bsg_strncpy_safe(event->api_key, report_v5->api_key, + sizeof(event->api_key)); + + migrate_breadcrumb_v2(report_v5, event); + free(report_v5); + } + return event; +} + +bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { + if (report_v4 == NULL) { + return NULL; + } + bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + event->notifier = report_v4->notifier; + event->device = report_v4->device; + event->user = report_v4->user; + event->error = report_v4->error; + event->metadata = report_v4->metadata; + event->crumb_count = report_v4->crumb_count; + event->crumb_first_index = report_v4->crumb_first_index; + memcpy(event->breadcrumbs, report_v4->breadcrumbs, + sizeof(event->breadcrumbs)); + event->severity = report_v4->severity; + bsg_strncpy_safe(event->session_id, report_v4->session_id, + sizeof(event->session_id)); + bsg_strncpy_safe(event->session_start, report_v4->session_start, + sizeof(event->session_id)); + event->handled_events = report_v4->handled_events; + event->unhandled_events = report_v4->unhandled_events; + bsg_strncpy_safe(event->grouping_hash, report_v4->grouping_hash, + sizeof(event->session_id)); + event->unhandled = report_v4->unhandled; + bsg_strncpy_safe(event->api_key, report_v4->api_key, + sizeof(event->api_key)); + migrate_app_v2(report_v4, event); + free(report_v4); + } + return event; +} + +bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3) { + if (report_v3 == NULL) { + return NULL; + } + bugsnag_report_v4 *event = calloc(1, sizeof(bugsnag_event)); + + if (event != NULL) { + event->notifier = report_v3->notifier; + event->app = report_v3->app; + event->device = report_v3->device; + event->user = report_v3->user; + event->error = report_v3->error; + event->metadata = report_v3->metadata; + event->crumb_count = report_v3->crumb_count; + event->crumb_first_index = report_v3->crumb_first_index; + memcpy(event->breadcrumbs, report_v3->breadcrumbs, + sizeof(event->breadcrumbs)); + event->severity = report_v3->severity; + strcpy(event->session_id, report_v3->session_id); + strcpy(event->session_start, report_v3->session_start); + event->handled_events = report_v3->handled_events; + event->unhandled_events = report_v3->unhandled_events; + strcpy(event->grouping_hash, report_v3->grouping_hash); + event->unhandled = report_v3->unhandled; + + // set a default value for the api key + strcpy(event->api_key, ""); + free(report_v3); + } + return bsg_map_v4_to_report(event); +} + +bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2) { + if (report_v2 == NULL) { + return NULL; + } + bugsnag_report_v3 *event = calloc(1, sizeof(bugsnag_report_v3)); + + if (event != NULL) { + // assign metadata first as old app/device fields are migrated there + event->metadata = report_v2->metadata; + migrate_app_v1(report_v2, event); + migrate_device_v1(report_v2, event); + event->user = report_v2->user; + migrate_breadcrumb_v1(report_v2, event); + + strcpy(event->context, report_v2->context); + event->severity = report_v2->severity; + strcpy(event->session_id, report_v2->session_id); + strcpy(event->session_start, report_v2->session_start); + event->handled_events = report_v2->handled_events; + event->unhandled_events = report_v2->unhandled_events; + + // migrate changed notifier fields + strcpy(event->notifier.version, report_v2->notifier.version); + strcpy(event->notifier.name, report_v2->notifier.name); + strcpy(event->notifier.url, report_v2->notifier.url); + + // migrate changed error fields + strcpy(event->error.errorClass, report_v2->exception.name); + strcpy(event->error.errorMessage, report_v2->exception.message); + strcpy(event->error.type, report_v2->exception.type); + event->error.frame_count = report_v2->exception.frame_count; + size_t error_size = sizeof(bugsnag_stackframe) * BUGSNAG_FRAMES_MAX; + memcpy(&event->error.stacktrace, report_v2->exception.stacktrace, + error_size); + + // Fatal C errors are always true by default, previously this was hardcoded + // and not a field on the struct + event->unhandled = true; + free(report_v2); + } + return bsg_map_v3_to_report(event); +} + +void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { + bsg_strcpy(event->device.os_name, + "android"); // os_name was not a field in v2 + event->device.api_level = report_v2->device.api_level; + event->device.cpu_abi_count = report_v2->device.cpu_abi_count; + event->device.time = report_v2->device.time; + event->device.jailbroken = report_v2->device.jailbroken; + event->device.total_memory = report_v2->device.total_memory; + + for (int k = 0; k < report_v2->device.cpu_abi_count && + k < sizeof(report_v2->device.cpu_abi); + k++) { + bsg_strcpy(event->device.cpu_abi[k].value, + report_v2->device.cpu_abi[k].value); + event->device.cpu_abi_count++; + } + + bsg_strcpy(event->device.orientation, report_v2->device.orientation); + bsg_strcpy(event->device.id, report_v2->device.id); + bsg_strcpy(event->device.locale, report_v2->device.locale); + bsg_strcpy(event->device.manufacturer, report_v2->device.manufacturer); + bsg_strcpy(event->device.model, report_v2->device.model); + bsg_strcpy(event->device.os_build, report_v2->device.os_build); + bsg_strcpy(event->device.os_version, report_v2->device.os_version); + + // migrate legacy fields to metadata + bugsnag_event_add_metadata_bool(event, "device", "emulator", + report_v2->device.emulator); + bugsnag_event_add_metadata_double(event, "device", "dpi", + report_v2->device.dpi); + bugsnag_event_add_metadata_double(event, "device", "screenDensity", + report_v2->device.screen_density); + bugsnag_event_add_metadata_double(event, "device", "batteryLevel", + report_v2->device.battery_level); + bugsnag_event_add_metadata_string(event, "device", "locationStatus", + report_v2->device.location_status); + bugsnag_event_add_metadata_string(event, "device", "brand", + report_v2->device.brand); + bugsnag_event_add_metadata_string(event, "device", "networkAccess", + report_v2->device.network_access); + bugsnag_event_add_metadata_string(event, "device", "screenResolution", + report_v2->device.screen_resolution); +} + +void bugsnag_report_v3_add_breadcrumb(bugsnag_report_v3 *event, + bugsnag_breadcrumb *crumb) { + int crumb_index; + if (event->crumb_count < V2_BUGSNAG_CRUMBS_MAX) { + crumb_index = event->crumb_count; + event->crumb_count++; + } else { + crumb_index = event->crumb_first_index; + event->crumb_first_index = + (event->crumb_first_index + 1) % V2_BUGSNAG_CRUMBS_MAX; + } + memcpy(&event->breadcrumbs[crumb_index], crumb, sizeof(bugsnag_breadcrumb)); +} + +int bsg_calculate_total_crumbs(int old_count) { + return old_count < BUGSNAG_CRUMBS_MAX ? old_count : BUGSNAG_CRUMBS_MAX; +} + +int bsg_calculate_v1_start_index(int old_count) { + return old_count < V2_BUGSNAG_CRUMBS_MAX ? 0 + : old_count % V2_BUGSNAG_CRUMBS_MAX; +} + +int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index) { + return (crumb_pos + first_index) % V1_BUGSNAG_CRUMBS_MAX; +} + +void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, + bugsnag_report_v3 *event) { + event->crumb_count = 0; + event->crumb_first_index = 0; + + // previously breadcrumbs had 30 elements, now they have 25. + // if more than 25 breadcrumbs were collected in the legacy report, + // offset them accordingly by moving the start position by count - + // BUGSNAG_CRUMBS_MAX. + int new_crumb_total = bsg_calculate_total_crumbs(report_v2->crumb_count); + int k = bsg_calculate_v1_start_index(report_v2->crumb_count); + + for (; k < new_crumb_total; k++) { + int crumb_index = + bsg_calculate_v1_crumb_index(k, report_v2->crumb_first_index); + bugsnag_breadcrumb_v1 *old_crumb = &report_v2->breadcrumbs[crumb_index]; + bugsnag_breadcrumb *new_crumb = calloc(1, sizeof(bugsnag_breadcrumb)); + + // copy old crumb fields to new + new_crumb->type = old_crumb->type; + bsg_strncpy_safe(new_crumb->name, old_crumb->name, sizeof(new_crumb->name)); + bsg_strncpy_safe(new_crumb->timestamp, old_crumb->timestamp, + sizeof(new_crumb->timestamp)); + + for (int j = 0; j < 8; j++) { + bsg_char_metadata_pair pair = old_crumb->metadata[j]; + + if (strlen(pair.value) > 0 && strlen(pair.key) > 0) { + bsg_add_metadata_value_str(&new_crumb->metadata, "metaData", pair.key, + pair.value); + } + } + + // add crumb to event by copying memory + bugsnag_report_v3_add_breadcrumb(event, new_crumb); + free(new_crumb); + } +} + +void migrate_breadcrumb_v2(bugsnag_report_v5 *report_v5, bugsnag_event *event) { + int old_first_index = report_v5->crumb_first_index; + event->crumb_count = report_v5->crumb_count; + event->crumb_first_index = 0; // sort crumbs while copying across + + // rationalize order of breadcrumbs while copying over to new struct + for (int new_index = 0; new_index < event->crumb_count; new_index++) { + int old_index = (new_index + old_first_index) % V2_BUGSNAG_CRUMBS_MAX; + bugsnag_breadcrumb crumb = report_v5->breadcrumbs[old_index]; + memcpy(&event->breadcrumbs[new_index], &crumb, sizeof(bugsnag_breadcrumb)); + } +} + +bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1) { + if (report_v1 == NULL) { + return NULL; + } + size_t report_size = sizeof(bugsnag_report_v2); + bugsnag_report_v2 *event_v2 = calloc(1, report_size); + + if (event_v2 != NULL) { + event_v2->notifier = report_v1->notifier; + event_v2->app = report_v1->app; + event_v2->device = report_v1->device; + event_v2->user = report_v1->user; + event_v2->exception = report_v1->exception; + event_v2->metadata = report_v1->metadata; + event_v2->crumb_count = report_v1->crumb_count; + event_v2->crumb_first_index = report_v1->crumb_first_index; + + size_t breadcrumb_size = + sizeof(bugsnag_breadcrumb_v1) * V1_BUGSNAG_CRUMBS_MAX; + memcpy(&event_v2->breadcrumbs, report_v1->breadcrumbs, breadcrumb_size); + + strcpy(event_v2->context, report_v1->context); + event_v2->severity = report_v1->severity; + strcpy(event_v2->session_id, report_v1->session_id); + strcpy(event_v2->session_start, report_v1->session_start); + event_v2->handled_events = report_v1->handled_events; + event_v2->unhandled_events = 1; + + free(report_v1); + } + return bsg_map_v2_to_report(event_v2); +} + +void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { + bsg_strcpy(event->app.id, report_v2->app.id); + bsg_strcpy(event->app.release_stage, report_v2->app.release_stage); + bsg_strcpy(event->app.type, report_v2->app.type); + bsg_strcpy(event->app.version, report_v2->app.version); + bsg_strcpy(event->app.active_screen, report_v2->app.active_screen); + bsg_strcpy(event->app.build_uuid, report_v2->app.build_uuid); + bsg_strcpy(event->app.binary_arch, report_v2->app.binaryArch); + event->app.version_code = report_v2->app.version_code; + event->app.duration = report_v2->app.duration; + event->app.duration_in_foreground = report_v2->app.duration_in_foreground; + event->app.duration_ms_offset = report_v2->app.duration_ms_offset; + event->app.duration_in_foreground_ms_offset = + report_v2->app.duration_in_foreground_ms_offset; + event->app.in_foreground = report_v2->app.in_foreground; + + // migrate legacy fields to metadata + bugsnag_event_add_metadata_string(event, "app", "packageName", + report_v2->app.package_name); + bugsnag_event_add_metadata_string(event, "app", "versionName", + report_v2->app.version_name); + bugsnag_event_add_metadata_string(event, "app", "name", report_v2->app.name); +} +void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event) { + bsg_strncpy_safe(event->app.id, report_v4->app.id, sizeof(event->app.id)); + bsg_strncpy_safe(event->app.release_stage, report_v4->app.release_stage, + sizeof(event->app.release_stage)); + bsg_strncpy_safe(event->app.type, report_v4->app.type, + sizeof(event->app.type)); + bsg_strncpy_safe(event->app.version, report_v4->app.version, + sizeof(event->app.version)); + bsg_strncpy_safe(event->app.active_screen, report_v4->app.active_screen, + sizeof(event->app.active_screen)); + bsg_strncpy_safe(event->app.build_uuid, report_v4->app.build_uuid, + sizeof(event->app.build_uuid)); + bsg_strncpy_safe(event->app.binary_arch, report_v4->app.binary_arch, + sizeof(event->app.binary_arch)); + event->app.version_code = report_v4->app.version_code; + event->app.duration = report_v4->app.duration; + event->app.duration_in_foreground = report_v4->app.duration_in_foreground; + event->app.duration_ms_offset = report_v4->app.duration_ms_offset; + event->app.duration_in_foreground_ms_offset = + report_v4->app.duration_in_foreground_ms_offset; + event->app.in_foreground = report_v4->app.in_foreground; + + // no info available, set to sensible default + event->app.is_launching = false; +} + +static char *read_string(int fd) { + ssize_t len; + uint32_t string_length; + len = read(fd, &string_length, sizeof(string_length)); + + if (len != sizeof(string_length)) { + return NULL; + } + + // allocate enough space, zero filler, and with a trailing '\0' terminator + char *string_buffer = calloc(1, (string_length + 1)); + if (!string_buffer) { + return NULL; + } + + len = read(fd, string_buffer, string_length); + if (len != string_length) { + free(string_buffer); + return NULL; + } + + return string_buffer; +} + +int read_byte(int fd) { + char value; + if (read(fd, &value, 1) != 1) { + return -1; + } + + return value; +} + +void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, + size_t *out_feature_flag_count) { + + ssize_t len; + uint32_t feature_flag_count = 0; + len = read(fd, &feature_flag_count, sizeof(feature_flag_count)); + if (len != sizeof(feature_flag_count)) { + goto feature_flags_error; + } + + bsg_feature_flag *flags = + calloc(feature_flag_count, sizeof(bsg_feature_flag)); + for (uint32_t index = 0; index < feature_flag_count; index++) { + char *name = read_string(fd); + if (!name) { + goto feature_flags_error; + } + + int variant_exists = read_byte(fd); + if (variant_exists < 0) { + goto feature_flags_error; + } + + char *variant = NULL; + if (variant_exists) { + variant = read_string(fd); + if (!variant) { + goto feature_flags_error; + } + } + + flags[index].name = name; + flags[index].variant = variant; + } + + *out_feature_flag_count = feature_flag_count; + *out_feature_flags = flags; + + return; + +feature_flags_error: + // something wrong - we release all allocated memory + for (uint32_t index = 0; index < feature_flag_count; index++) { + if (flags[index].name) { + free(flags[index].name); + } + + if (flags[index].variant) { + free(flags[index].variant); + } + } + + free(flags); + + // clear the out fields to indicate no feature-flags are availables + *out_feature_flag_count = 0; + *out_feature_flags = NULL; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h new file mode 100644 index 0000000000..8a28b70b5f --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +extern const int BSG_MIGRATOR_CURRENT_VERSION; + +/** + * Read an event from a file path, converting from older formats if needed. + * + * The report version is serialized in the file header, and old formats are + * maintained in migrate.h for backwards compatibility. These are then migrated + * to the current bugsnag_event struct. + * + * @param filepath A full path to a file + * + * @return An allocated event or NULL if no event could be read + */ +bugsnag_event *bsg_read_event(char *filepath); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h similarity index 100% rename from bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h rename to bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/main.c b/bugsnag-plugin-android-ndk/src/test/cpp/main.c index 36ef0adc2f..6ef1cd524d 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -5,10 +5,9 @@ #include #include -#include -#include -#include #include "test_serializer.h" +#include +#include SUITE(suite_string_utils); SUITE(suite_json_serialization); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h index e68f59eed4..7dc464a87d 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h @@ -1,9 +1,9 @@ #include #include -#include #include -#include +#include +#include typedef struct { void *data_ptr; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 10eb829e2a..3b72c8bfec 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -1,8 +1,8 @@ +#include #include -#include #include -#include -#include +#include +#include #define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/foo.crash" From 7e218b0f896862580711d17c0cab27c428a9fd55 Mon Sep 17 00:00:00 2001 From: Delisa Date: Thu, 9 Dec 2021 16:52:05 +0000 Subject: [PATCH 12/37] refactor(ndk): move event writing funcs into own file (#1547) --- .../src/main/CMakeLists.txt | 3 +- .../src/main/jni/utils/serializer.c | 94 +------------------ .../src/main/jni/utils/serializer.h | 7 +- .../utils/{ => serializer}/buffered_writer.c | 4 +- .../utils/{ => serializer}/buffered_writer.h | 0 .../main/jni/utils/serializer/event_writer.c | 84 +++++++++++++++++ .../main/jni/utils/serializer/event_writer.h | 9 ++ 7 files changed, 105 insertions(+), 96 deletions(-) rename bugsnag-plugin-android-ndk/src/main/jni/utils/{ => serializer}/buffered_writer.c (99%) rename bugsnag-plugin-android-ndk/src/main/jni/utils/{ => serializer}/buffered_writer.h (100%) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 4332d72559..d6b21ec6df 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -15,7 +15,9 @@ add_library( # Specifies the name of the library. jni/handlers/signal_handler.c jni/handlers/cpp_handler.cpp jni/utils/crash_info.c + jni/utils/serializer/buffered_writer.c jni/utils/serializer/event_reader.c + jni/utils/serializer/event_writer.c jni/utils/stack_unwinder.c jni/utils/stack_unwinder_libunwindstack.cpp jni/utils/stack_unwinder_libcorkscrew.c @@ -24,7 +26,6 @@ add_library( # Specifies the name of the library. jni/utils/serializer.c jni/utils/string.c jni/utils/threads.c - jni/utils/buffered_writer.c jni/deps/parson/parson.c ) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 3e4c439057..7a031921b1 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -1,9 +1,8 @@ #include "serializer.h" #include "serializer/event_reader.h" +#include "serializer/event_writer.h" #include "string.h" -#include "buffered_writer.h" - #include #include #include @@ -14,66 +13,18 @@ #include #include -bool bsg_event_write(struct bsg_buffered_writer *writer, - bsg_report_header *header, bugsnag_event *event); - -/** - * Serializes the LastRunInfo to the file. This persists information about - * why the current launch crashed, for use on future launch. - */ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { - char *path = env->last_run_info_path; - int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd == -1) { - return false; - } - - int size = bsg_strlen(env->next_last_run_info); - ssize_t len = write(fd, env->next_last_run_info, size); - return len == size; + return bsg_lastrun_write(env); } bool bsg_serialize_event_to_file(bsg_environment *env) { - bsg_buffered_writer writer; - if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { - return false; - } - - if (!bsg_event_write(&writer, &env->report_header, &env->next_event)) { - goto fail; - } - - if (!bsg_write_feature_flags(&env->next_event, &writer)) { - goto fail; - } - - writer.dispose(&writer); - return true; - -fail: - writer.dispose(&writer); - return false; + return bsg_event_write(env); } bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { return bsg_read_event(filepath); } -bool bsg_report_header_write(bsg_report_header *header, int fd) { - ssize_t len = write(fd, header, sizeof(bsg_report_header)); - - return len == sizeof(bsg_report_header); -} - -bool bsg_event_write(struct bsg_buffered_writer *writer, - bsg_report_header *header, bugsnag_event *event) { - if (!bsg_report_header_write(header, writer->fd)) { - return false; - } - - return writer->write(writer, event, sizeof(bugsnag_event)); -} - const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { switch (type) { case BSG_CRUMB_ERROR: @@ -481,42 +432,3 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event) { } return serialized_string; } - -static bool write_feature_flag(bsg_buffered_writer *writer, - bsg_feature_flag *flag) { - if (!writer->write_string(writer, flag->name)) { - return false; - } - - if (flag->variant) { - if (!writer->write_byte(writer, 1)) { - return false; - } - - if (!writer->write_string(writer, flag->variant)) { - return false; - } - } else { - if (!writer->write_byte(writer, 0)) { - return false; - } - } - - return true; -} - -bool bsg_write_feature_flags(bugsnag_event *event, - bsg_buffered_writer *writer) { - const uint32_t feature_flag_count = event->feature_flag_count; - if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { - return false; - } - - for (uint32_t index = 0; index < feature_flag_count; index++) { - if (!write_feature_flag(writer, &event->feature_flags[index])) { - return false; - } - } - - return true; -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h index a3a368b36d..37b5de1a1e 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -1,5 +1,4 @@ #include "../bugsnag_ndk.h" -#include "buffered_writer.h" #include "build.h" #include #include @@ -16,6 +15,10 @@ char *bsg_serialize_event_to_json_string(bugsnag_event *event); bool bsg_serialize_event_to_file(bsg_environment *env) __asyncsafe; +/** + * Serializes the LastRunInfo to the file. This persists information about + * why the current launch crashed, for use on future launch. + */ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) __asyncsafe; bugsnag_event *bsg_deserialize_event_from_file(char *filepath); @@ -53,8 +56,6 @@ int bsg_calculate_total_crumbs(int old_count); int bsg_calculate_v1_start_index(int old_count); int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index); -bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer); - #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/buffered_writer.c similarity index 99% rename from bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c rename to bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/buffered_writer.c index 99d7d9f3d4..f5f69e3165 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/buffered_writer.c @@ -1,6 +1,8 @@ #include "buffered_writer.h" + +#include "../string.h" #include "bugsnag_ndk.h" -#include "string.h" + #include #include #include diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/buffered_writer.h similarity index 100% rename from bugsnag-plugin-android-ndk/src/main/jni/utils/buffered_writer.h rename to bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/buffered_writer.h diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c new file mode 100644 index 0000000000..abb06b19a6 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -0,0 +1,84 @@ +#include "event_writer.h" + +#include +#include + +#include "../string.h" +#include "buffered_writer.h" + +bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer); + +bool bsg_report_header_write(bsg_report_header *header, int fd) { + ssize_t len = write(fd, header, sizeof(bsg_report_header)); + + return len == sizeof(bsg_report_header); +} + +bool bsg_event_write(bsg_environment *env) { + bsg_buffered_writer writer; + if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { + return false; + } + + bool result = + // write header - determines format version, etc + bsg_report_header_write(&env->report_header, writer.fd) && + // add cached event info + writer.write(&writer, &env->next_event, sizeof(bugsnag_event)) && + // append feature flags after event structure + bsg_write_feature_flags(&env->next_event, &writer); + + writer.dispose(&writer); + return result; +} + +bool bsg_lastrun_write(bsg_environment *env) { + char *path = env->last_run_info_path; + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + return false; + } + + int size = bsg_strlen(env->next_last_run_info); + ssize_t len = write(fd, env->next_last_run_info, size); + return len == size; +} + +static bool write_feature_flag(bsg_buffered_writer *writer, + bsg_feature_flag *flag) { + if (!writer->write_string(writer, flag->name)) { + return false; + } + + if (flag->variant) { + if (!writer->write_byte(writer, 1)) { + return false; + } + + if (!writer->write_string(writer, flag->variant)) { + return false; + } + } else { + if (!writer->write_byte(writer, 0)) { + return false; + } + } + + return true; +} + +bool bsg_write_feature_flags(bugsnag_event *event, + bsg_buffered_writer *writer) { + const uint32_t feature_flag_count = event->feature_flag_count; + if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { + return false; + } + + for (uint32_t index = 0; index < feature_flag_count; index++) { + if (!write_feature_flag(writer, &event->feature_flags[index])) { + return false; + } + } + + return true; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h new file mode 100644 index 0000000000..6f2ddfaeff --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h @@ -0,0 +1,9 @@ +/** + * async-safe functions for writing an event to disk + */ +#pragma once +#include "../../bugsnag_ndk.h" + +bool bsg_event_write(bsg_environment *env) __asyncsafe; + +bool bsg_lastrun_write(bsg_environment *env) __asyncsafe; From 40185aa153f9eb0f6ba3b78b0fdd4ff9e7966fcf Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 7 Dec 2021 14:33:05 +0000 Subject: [PATCH 13/37] refactor(ndk): move JSON serialization funcs into own file --- .../src/main/CMakeLists.txt | 1 + .../src/main/jni/utils/serializer.c | 418 +---------------- .../src/main/jni/utils/serializer.h | 40 +- .../main/jni/utils/serializer/json_writer.c | 419 ++++++++++++++++++ .../main/jni/utils/serializer/json_writer.h | 40 ++ .../src/test/cpp/main.c | 8 +- .../src/test/cpp/test_breadcrumbs.c | 4 +- .../src/test/cpp/test_serializer.h | 4 +- .../src/test/cpp/test_utils_serialize.c | 9 +- 9 files changed, 480 insertions(+), 463 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index d6b21ec6df..2d5cabc6f0 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -18,6 +18,7 @@ add_library( # Specifies the name of the library. jni/utils/serializer/buffered_writer.c jni/utils/serializer/event_reader.c jni/utils/serializer/event_writer.c + jni/utils/serializer/json_writer.c jni/utils/stack_unwinder.c jni/utils/stack_unwinder_libunwindstack.cpp jni/utils/stack_unwinder_libcorkscrew.c diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 7a031921b1..42289bf4b6 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -1,17 +1,7 @@ #include "serializer.h" #include "serializer/event_reader.h" #include "serializer/event_writer.h" -#include "string.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "serializer/json_writer.h" bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { return bsg_lastrun_write(env); @@ -25,410 +15,6 @@ bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { return bsg_read_event(filepath); } -const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { - switch (type) { - case BSG_CRUMB_ERROR: - return "error"; - case BSG_CRUMB_LOG: - return "log"; - case BSG_CRUMB_MANUAL: - return "manual"; - case BSG_CRUMB_NAVIGATION: - return "navigation"; - case BSG_CRUMB_PROCESS: - return "process"; - case BSG_CRUMB_REQUEST: - return "request"; - case BSG_CRUMB_STATE: - return "state"; - case BSG_CRUMB_USER: - return "user"; - } -} - -const char *bsg_severity_string(bugsnag_severity type) { - switch (type) { - case BSG_SEVERITY_INFO: - return "info"; - case BSG_SEVERITY_WARN: - return "warn"; - case BSG_SEVERITY_ERR: - return "error"; - } -} - -void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj) { - json_object_set_string(event_obj, "context", event->context); -} - -void bsg_serialize_grouping_hash(const bugsnag_event *event, - JSON_Object *event_obj) { - if (strlen(event->grouping_hash) > 0) { - json_object_set_string(event_obj, "groupingHash", event->grouping_hash); - } -} - -void bsg_serialize_severity_reason(const bugsnag_event *event, - JSON_Object *event_obj) { - // FUTURE(dm): severityReason/unhandled attributes are currently - // over-optimized for signal handling. in the future we may want to handle - // C++ exceptions, etc as well. - json_object_set_string(event_obj, "severity", - bsg_severity_string(event->severity)); - bool unhandled = event->unhandled; - json_object_dotset_boolean(event_obj, "unhandled", unhandled); - - // unhandled == false always means that the state has been overridden by the - // user, as this codepath is only executed for unhandled native errors - json_object_dotset_boolean(event_obj, "severityReason.unhandledOverridden", - !unhandled); - json_object_dotset_string(event_obj, "severityReason.type", "signal"); - json_object_dotset_string(event_obj, "severityReason.attributes.signalType", - event->error.errorClass); -} - -void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "app.version", app.version); - json_object_dotset_string(event_obj, "app.id", app.id); - json_object_dotset_string(event_obj, "app.type", app.type); - - json_object_dotset_string(event_obj, "app.releaseStage", app.release_stage); - json_object_dotset_number(event_obj, "app.versionCode", app.version_code); - if (strlen(app.build_uuid) > 0) { - json_object_dotset_string(event_obj, "app.buildUUID", app.build_uuid); - } - json_object_dotset_string(event_obj, "app.binaryArch", app.binary_arch); - json_object_dotset_number(event_obj, "app.duration", app.duration); - json_object_dotset_number(event_obj, "app.durationInForeground", - app.duration_in_foreground); - json_object_dotset_boolean(event_obj, "app.inForeground", app.in_foreground); - json_object_dotset_boolean(event_obj, "app.isLaunching", app.is_launching); -} - -void bsg_serialize_app_metadata(const bsg_app_info app, - JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "metaData.app.activeScreen", - app.active_screen); -} - -void bsg_serialize_device(const bsg_device_info device, - JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "device.osName", device.os_name); - json_object_dotset_string(event_obj, "device.id", device.id); - json_object_dotset_string(event_obj, "device.locale", device.locale); - json_object_dotset_string(event_obj, "device.osVersion", device.os_version); - json_object_dotset_string(event_obj, "device.manufacturer", - device.manufacturer); - json_object_dotset_string(event_obj, "device.model", device.model); - json_object_dotset_string(event_obj, "device.orientation", - device.orientation); - char android_api_level[sizeof "1234"]; - snprintf(android_api_level, 4, "%d", device.api_level); - json_object_dotset_string(event_obj, "device.runtimeVersions.androidApiLevel", - android_api_level); - json_object_dotset_string(event_obj, "device.runtimeVersions.osBuild", - device.os_build); - - JSON_Value *abi_val = json_value_init_array(); - JSON_Array *cpu_abis = json_value_get_array(abi_val); - json_object_dotset_value(event_obj, "device.cpuAbi", abi_val); - for (int i = 0; i < device.cpu_abi_count; i++) { - json_array_append_string(cpu_abis, device.cpu_abi[i].value); - } - - json_object_dotset_number(event_obj, "device.totalMemory", - device.total_memory); - json_object_dotset_boolean(event_obj, "device.jailbroken", device.jailbroken); - - char report_time[sizeof "2018-10-08T12:07:09Z"]; - if (device.time > 0) { - strftime(report_time, sizeof report_time, "%FT%TZ", gmtime(&device.time)); - json_object_dotset_string(event_obj, "device.time", report_time); - } -} - -void bsg_serialize_device_metadata(const bsg_device_info device, - JSON_Object *event_obj) {} - -void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj) { - for (int i = 0; i < metadata.value_count; i++) { - char *format = calloc(1, sizeof(char) * 256); - bsg_metadata_value value = metadata.values[i]; - - switch (value.type) { - case BSG_METADATA_BOOL_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_boolean(event_obj, format, value.bool_value); - break; - case BSG_METADATA_CHAR_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_string(event_obj, format, value.char_value); - break; - case BSG_METADATA_NUMBER_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_number(event_obj, format, value.double_value); - break; - default: - break; - } - free(format); - } -} - -void bsg_serialize_breadcrumb_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj) { - for (int i = 0; i < metadata.value_count; i++) { - char *format = calloc(1, sizeof(char) * 256); - bsg_metadata_value value = metadata.values[i]; - - switch (value.type) { - case BSG_METADATA_BOOL_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_boolean(event_obj, format, value.bool_value); - break; - case BSG_METADATA_CHAR_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_string(event_obj, format, value.char_value); - break; - case BSG_METADATA_NUMBER_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_number(event_obj, format, value.double_value); - break; - default: - break; - } - free(format); - } -} - -void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj) { - if (strlen(user.name) > 0) - json_object_dotset_string(event_obj, "user.name", user.name); - if (strlen(user.email) > 0) - json_object_dotset_string(event_obj, "user.email", user.email); - if (strlen(user.id) > 0) - json_object_dotset_string(event_obj, "user.id", user.id); -} - -void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj) { - if (bugsnag_event_has_session(event)) { - json_object_dotset_string(event_obj, "session.startedAt", - event->session_start); - json_object_dotset_string(event_obj, "session.id", event->session_id); - json_object_dotset_number(event_obj, "session.events.handled", - event->handled_events); - json_object_dotset_number(event_obj, "session.events.unhandled", - event->unhandled_events); - } -} - -void bsg_serialize_error(bsg_error exc, JSON_Object *exception, - JSON_Array *stacktrace) { - json_object_set_string(exception, "errorClass", exc.errorClass); - json_object_set_string(exception, "message", exc.errorMessage); - json_object_set_string(exception, "type", "c"); - // assuming that the initial frame is the program counter. This logic will - // need to be revisited if (for example) we add more intelligent processing - // for stack overflow-type errors, like discarding the top frames, which - // would mean no stored frame is the program counter. - if (exc.frame_count > 0) { - bsg_serialize_stackframe(&(exc.stacktrace[0]), true, stacktrace); - } - for (int findex = 1; findex < exc.frame_count; findex++) { - bugsnag_stackframe stackframe = exc.stacktrace[findex]; - bsg_serialize_stackframe(&stackframe, false, stacktrace); - } -} - -void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, - JSON_Array *stacktrace) { - JSON_Value *frame_val = json_value_init_object(); - JSON_Object *frame = json_value_get_object(frame_val); - json_object_set_number(frame, "frameAddress", (*stackframe).frame_address); - json_object_set_number(frame, "symbolAddress", (*stackframe).symbol_address); - json_object_set_number(frame, "loadAddress", (*stackframe).load_address); - json_object_set_number(frame, "lineNumber", (*stackframe).line_number); - if (is_pc) { - // only necessary to set to true, false is the default value and omitting - // the field keeps payload sizes smaller. - json_object_set_boolean(frame, "isPC", true); - } - if (strlen((*stackframe).filename) > 0) { - json_object_set_string(frame, "file", (*stackframe).filename); - } - if (strlen((*stackframe).method) == 0) { - char *frame_address = calloc(1, sizeof(char) * 32); - sprintf(frame_address, "0x%lx", (unsigned long)(*stackframe).frame_address); - json_object_set_string(frame, "method", frame_address); - free(frame_address); - } else { - json_object_set_string(frame, "method", (*stackframe).method); - } - - json_array_append_value(stacktrace, frame_val); -} - -#if defined(__i386__) || defined(__arm__) -#define TIMESTAMP_T long long -#define TIMESTAMP_DECODE atoll -#define TIMESTAMP_MILLIS_FORMAT "%s.%03lldZ" -#elif defined(__x86_64__) || defined(__aarch64__) -#define TIMESTAMP_T long -#define TIMESTAMP_DECODE atol -#define TIMESTAMP_MILLIS_FORMAT "%s.%03ldZ" -#endif -/** - * Convert a string representing the number of milliseconds since the epoch - * into the date format "yyyy-MM-ddTHH:mm:ss.SSSZ". Safe for all dates earlier - * than 2038. - * - * @param source the timestamp string, should be something like: 1636710533109 - * @param dest a buffer large enough to hold the 24 characters required in the - * date format - * - * @return true if the conversion succeeded - */ -static bool timestamp_to_iso8601_millis(const char *source, char *dest) { - TIMESTAMP_T timestamp = TIMESTAMP_DECODE(source); - if (timestamp) { - time_t seconds = timestamp / 1000; - TIMESTAMP_T milliseconds = timestamp - (seconds * 1000LL); - if (milliseconds > 1000) { // round to nearest second - seconds++; - milliseconds -= 1000; - } - struct tm timer; - // gmtime(3) can fail if "the year does not fit into an integer". Hopefully - // nobody is running this code by then. - if (gmtime_r(&seconds, &timer)) { - char buffer[26]; - strftime(buffer, 26, "%Y-%m-%dT%H:%M:%S", &timer); - sprintf(dest, TIMESTAMP_MILLIS_FORMAT, buffer, milliseconds); - return true; - } else { - BUGSNAG_LOG("Hello, people of the far future! Please use your time " - "machine to file a bug in the year 2021."); - } - } - return false; -} -#undef TIMESTAMP_T -#undef TIMESTAMP_DECODE -#undef TIMESTAMP_MILLIS_FORMAT - -void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs) { - if (event->crumb_count > 0) { - int current_index = event->crumb_first_index; - while (json_array_get_count(crumbs) < event->crumb_count) { - JSON_Value *crumb_val = json_value_init_object(); - JSON_Object *crumb = json_value_get_object(crumb_val); - json_array_append_value(crumbs, crumb_val); - - bugsnag_breadcrumb breadcrumb = event->breadcrumbs[current_index]; - json_object_set_string(crumb, "name", breadcrumb.name); - // check whether to decode milliseconds into ISO8601 date format - if (breadcrumb.timestamp[0] == 't') { - char *unix_timestamp_str = breadcrumb.timestamp + 1; - char buffer[32]; - if (timestamp_to_iso8601_millis(unix_timestamp_str, buffer)) { - json_object_set_string(crumb, "timestamp", buffer); - } else { - // at least we tried. - json_object_set_string(crumb, "timestamp", unix_timestamp_str); - } - } else { - json_object_set_string(crumb, "timestamp", breadcrumb.timestamp); - } - json_object_set_string(crumb, "type", - bsg_crumb_type_string(breadcrumb.type)); - bsg_serialize_breadcrumb_metadata(breadcrumb.metadata, crumb); - current_index++; - if (current_index == BUGSNAG_CRUMBS_MAX) { - current_index = 0; - } - } - } -} - -void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads) { - if (event->thread_count <= 0) { - return; - } - - for (int index = 0; index < event->thread_count; index++) { - JSON_Value *thread_val = json_value_init_object(); - JSON_Object *json_thread = json_value_get_object(thread_val); - json_array_append_value(threads, thread_val); - - const bsg_thread *thread = &event->threads[index]; - json_object_set_number(json_thread, "id", (double)thread->id); - json_object_set_string(json_thread, "name", thread->name); - json_object_set_string(json_thread, "state", thread->state); - json_object_set_string(json_thread, "type", "c"); - } -} - -void bsg_serialize_feature_flags(const bugsnag_event *event, - JSON_Array *feature_flags) { - if (event->feature_flag_count <= 0) { - return; - } - - for (int index = 0; index < event->feature_flag_count; index++) { - JSON_Value *feature_flag_val = json_value_init_object(); - JSON_Object *feature_flag = json_value_get_object(feature_flag_val); - json_array_append_value(feature_flags, feature_flag_val); - - const bsg_feature_flag *flag = &event->feature_flags[index]; - json_object_set_string(feature_flag, "featureFlag", flag->name); - - if (flag->variant) { - json_object_set_string(feature_flag, "variant", flag->variant); - } - } -} - char *bsg_serialize_event_to_json_string(bugsnag_event *event) { - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - JSON_Value *crumbs_val = json_value_init_array(); - JSON_Array *crumbs = json_value_get_array(crumbs_val); - JSON_Value *exceptions_val = json_value_init_array(); - JSON_Array *exceptions = json_value_get_array(exceptions_val); - JSON_Value *ex_val = json_value_init_object(); - JSON_Object *exception = json_value_get_object(ex_val); - JSON_Value *threads_val = json_value_init_array(); - JSON_Array *threads = json_value_get_array(threads_val); - JSON_Value *stack_val = json_value_init_array(); - JSON_Array *stacktrace = json_value_get_array(stack_val); - JSON_Value *feature_flags_val = json_value_init_object(); - JSON_Array *feature_flags = json_value_get_array(feature_flags_val); - json_object_set_value(event_obj, "exceptions", exceptions_val); - json_object_set_value(event_obj, "breadcrumbs", crumbs_val); - json_object_set_value(event_obj, "threads", threads_val); - json_object_set_value(exception, "stacktrace", stack_val); - json_object_set_value(event_obj, "featureFlags", feature_flags_val); - json_array_append_value(exceptions, ex_val); - char *serialized_string = NULL; - { - bsg_serialize_context(event, event_obj); - bsg_serialize_grouping_hash(event, event_obj); - bsg_serialize_severity_reason(event, event_obj); - bsg_serialize_app(event->app, event_obj); - bsg_serialize_app_metadata(event->app, event_obj); - bsg_serialize_device(event->device, event_obj); - bsg_serialize_device_metadata(event->device, event_obj); - bsg_serialize_custom_metadata(event->metadata, event_obj); - bsg_serialize_user(event->user, event_obj); - bsg_serialize_session(event, event_obj); - bsg_serialize_error(event->error, exception, stacktrace); - bsg_serialize_breadcrumbs(event, crumbs); - bsg_serialize_threads(event, threads); - bsg_serialize_feature_flags(event, feature_flags); - - serialized_string = json_serialize_to_string(event_val); - json_value_free(event_val); - } - return serialized_string; + return bsg_event_to_json(event); } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h index 37b5de1a1e..bcdc00d3dd 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -1,11 +1,6 @@ +#pragma once #include "../bugsnag_ndk.h" #include "build.h" -#include -#include -#include -#include -#include -#include #ifdef __cplusplus extern "C" { @@ -23,39 +18,6 @@ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) __asyncsafe; bugsnag_event *bsg_deserialize_event_from_file(char *filepath); -void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj); -void bsg_serialize_severity_reason(const bugsnag_event *event, - JSON_Object *event_obj); -void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj); -void bsg_serialize_app_metadata(const bsg_app_info app, JSON_Object *event_obj); -void bsg_serialize_device(const bsg_device_info device, JSON_Object *event_obj); -void bsg_serialize_device_metadata(const bsg_device_info device, - JSON_Object *event_obj); -void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj); -void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj); -void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj); -/** - * Append a JSON-serialized stackframe to an array - * - * @param stackframe the frame to serialize - * @param is_pc true if the current frame is the program counter - * @param stacktrace the destination array - */ -void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, - JSON_Array *stacktrace); -void bsg_serialize_error(bsg_error exc, JSON_Object *exception, - JSON_Array *stacktrace); -void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs); -void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads); -void bsg_serialize_feature_flags(const bugsnag_event *event, - JSON_Array *feature_flags); -char *bsg_serialize_event_to_json_string(bugsnag_event *event); - -int bsg_calculate_total_crumbs(int old_count); -int bsg_calculate_v1_start_index(int old_count); -int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index); - #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c new file mode 100644 index 0000000000..1b34b6ede3 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c @@ -0,0 +1,419 @@ +#include "json_writer.h" + +#include +#include +#include +#include + +#include + +#include "../../../bugsnag_ndk.h" +#include "../../metadata.h" + +const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { + switch (type) { + case BSG_CRUMB_ERROR: + return "error"; + case BSG_CRUMB_LOG: + return "log"; + case BSG_CRUMB_MANUAL: + return "manual"; + case BSG_CRUMB_NAVIGATION: + return "navigation"; + case BSG_CRUMB_PROCESS: + return "process"; + case BSG_CRUMB_REQUEST: + return "request"; + case BSG_CRUMB_STATE: + return "state"; + case BSG_CRUMB_USER: + return "user"; + } +} + +const char *bsg_severity_string(bugsnag_severity type) { + switch (type) { + case BSG_SEVERITY_INFO: + return "info"; + case BSG_SEVERITY_WARN: + return "warn"; + case BSG_SEVERITY_ERR: + return "error"; + } +} + +void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj) { + json_object_set_string(event_obj, "context", event->context); +} + +void bsg_serialize_grouping_hash(const bugsnag_event *event, + JSON_Object *event_obj) { + if (strlen(event->grouping_hash) > 0) { + json_object_set_string(event_obj, "groupingHash", event->grouping_hash); + } +} + +void bsg_serialize_severity_reason(const bugsnag_event *event, + JSON_Object *event_obj) { + // FUTURE(dm): severityReason/unhandled attributes are currently + // over-optimized for signal handling. in the future we may want to handle + // C++ exceptions, etc as well. + json_object_set_string(event_obj, "severity", + bsg_severity_string(event->severity)); + bool unhandled = event->unhandled; + json_object_dotset_boolean(event_obj, "unhandled", unhandled); + + // unhandled == false always means that the state has been overridden by the + // user, as this codepath is only executed for unhandled native errors + json_object_dotset_boolean(event_obj, "severityReason.unhandledOverridden", + !unhandled); + json_object_dotset_string(event_obj, "severityReason.type", "signal"); + json_object_dotset_string(event_obj, "severityReason.attributes.signalType", + event->error.errorClass); +} + +void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj) { + json_object_dotset_string(event_obj, "app.version", app.version); + json_object_dotset_string(event_obj, "app.id", app.id); + json_object_dotset_string(event_obj, "app.type", app.type); + + json_object_dotset_string(event_obj, "app.releaseStage", app.release_stage); + json_object_dotset_number(event_obj, "app.versionCode", app.version_code); + if (strlen(app.build_uuid) > 0) { + json_object_dotset_string(event_obj, "app.buildUUID", app.build_uuid); + } + json_object_dotset_string(event_obj, "app.binaryArch", app.binary_arch); + json_object_dotset_number(event_obj, "app.duration", app.duration); + json_object_dotset_number(event_obj, "app.durationInForeground", + app.duration_in_foreground); + json_object_dotset_boolean(event_obj, "app.inForeground", app.in_foreground); + json_object_dotset_boolean(event_obj, "app.isLaunching", app.is_launching); +} + +void bsg_serialize_app_metadata(const bsg_app_info app, + JSON_Object *event_obj) { + json_object_dotset_string(event_obj, "metaData.app.activeScreen", + app.active_screen); +} + +void bsg_serialize_device(const bsg_device_info device, + JSON_Object *event_obj) { + json_object_dotset_string(event_obj, "device.osName", device.os_name); + json_object_dotset_string(event_obj, "device.id", device.id); + json_object_dotset_string(event_obj, "device.locale", device.locale); + json_object_dotset_string(event_obj, "device.osVersion", device.os_version); + json_object_dotset_string(event_obj, "device.manufacturer", + device.manufacturer); + json_object_dotset_string(event_obj, "device.model", device.model); + json_object_dotset_string(event_obj, "device.orientation", + device.orientation); + char android_api_level[sizeof "1234"]; + snprintf(android_api_level, 4, "%d", device.api_level); + json_object_dotset_string(event_obj, "device.runtimeVersions.androidApiLevel", + android_api_level); + json_object_dotset_string(event_obj, "device.runtimeVersions.osBuild", + device.os_build); + + JSON_Value *abi_val = json_value_init_array(); + JSON_Array *cpu_abis = json_value_get_array(abi_val); + json_object_dotset_value(event_obj, "device.cpuAbi", abi_val); + for (int i = 0; i < device.cpu_abi_count; i++) { + json_array_append_string(cpu_abis, device.cpu_abi[i].value); + } + + json_object_dotset_number(event_obj, "device.totalMemory", + device.total_memory); + json_object_dotset_boolean(event_obj, "device.jailbroken", device.jailbroken); + + char report_time[sizeof "2018-10-08T12:07:09Z"]; + if (device.time > 0) { + strftime(report_time, sizeof report_time, "%FT%TZ", gmtime(&device.time)); + json_object_dotset_string(event_obj, "device.time", report_time); + } +} + +void bsg_serialize_device_metadata(const bsg_device_info device, + JSON_Object *event_obj) {} + +void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, + JSON_Object *event_obj) { + for (int i = 0; i < metadata.value_count; i++) { + char *format = calloc(1, sizeof(char) * 256); + bsg_metadata_value value = metadata.values[i]; + + switch (value.type) { + case BSG_METADATA_BOOL_VALUE: + sprintf(format, "metaData.%s.%s", value.section, value.name); + json_object_dotset_boolean(event_obj, format, value.bool_value); + break; + case BSG_METADATA_CHAR_VALUE: + sprintf(format, "metaData.%s.%s", value.section, value.name); + json_object_dotset_string(event_obj, format, value.char_value); + break; + case BSG_METADATA_NUMBER_VALUE: + sprintf(format, "metaData.%s.%s", value.section, value.name); + json_object_dotset_number(event_obj, format, value.double_value); + break; + default: + break; + } + free(format); + } +} + +void bsg_serialize_breadcrumb_metadata(const bugsnag_metadata metadata, + JSON_Object *event_obj) { + for (int i = 0; i < metadata.value_count; i++) { + char *format = calloc(1, sizeof(char) * 256); + bsg_metadata_value value = metadata.values[i]; + + switch (value.type) { + case BSG_METADATA_BOOL_VALUE: + sprintf(format, "metaData.%s", value.name); + json_object_dotset_boolean(event_obj, format, value.bool_value); + break; + case BSG_METADATA_CHAR_VALUE: + sprintf(format, "metaData.%s", value.name); + json_object_dotset_string(event_obj, format, value.char_value); + break; + case BSG_METADATA_NUMBER_VALUE: + sprintf(format, "metaData.%s", value.name); + json_object_dotset_number(event_obj, format, value.double_value); + break; + default: + break; + } + free(format); + } +} + +void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj) { + if (strlen(user.name) > 0) + json_object_dotset_string(event_obj, "user.name", user.name); + if (strlen(user.email) > 0) + json_object_dotset_string(event_obj, "user.email", user.email); + if (strlen(user.id) > 0) + json_object_dotset_string(event_obj, "user.id", user.id); +} + +void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj) { + if (bugsnag_event_has_session(event)) { + json_object_dotset_string(event_obj, "session.startedAt", + event->session_start); + json_object_dotset_string(event_obj, "session.id", event->session_id); + json_object_dotset_number(event_obj, "session.events.handled", + event->handled_events); + json_object_dotset_number(event_obj, "session.events.unhandled", + event->unhandled_events); + } +} + +void bsg_serialize_error(bsg_error exc, JSON_Object *exception, + JSON_Array *stacktrace) { + json_object_set_string(exception, "errorClass", exc.errorClass); + json_object_set_string(exception, "message", exc.errorMessage); + json_object_set_string(exception, "type", "c"); + // assuming that the initial frame is the program counter. This logic will + // need to be revisited if (for example) we add more intelligent processing + // for stack overflow-type errors, like discarding the top frames, which + // would mean no stored frame is the program counter. + if (exc.frame_count > 0) { + bsg_serialize_stackframe(&(exc.stacktrace[0]), true, stacktrace); + } + for (int findex = 1; findex < exc.frame_count; findex++) { + bugsnag_stackframe stackframe = exc.stacktrace[findex]; + bsg_serialize_stackframe(&stackframe, false, stacktrace); + } +} + +void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, + JSON_Array *stacktrace) { + JSON_Value *frame_val = json_value_init_object(); + JSON_Object *frame = json_value_get_object(frame_val); + json_object_set_number(frame, "frameAddress", (*stackframe).frame_address); + json_object_set_number(frame, "symbolAddress", (*stackframe).symbol_address); + json_object_set_number(frame, "loadAddress", (*stackframe).load_address); + json_object_set_number(frame, "lineNumber", (*stackframe).line_number); + if (is_pc) { + // only necessary to set to true, false is the default value and omitting + // the field keeps payload sizes smaller. + json_object_set_boolean(frame, "isPC", true); + } + if (strlen((*stackframe).filename) > 0) { + json_object_set_string(frame, "file", (*stackframe).filename); + } + if (strlen((*stackframe).method) == 0) { + char *frame_address = calloc(1, sizeof(char) * 32); + sprintf(frame_address, "0x%lx", (unsigned long)(*stackframe).frame_address); + json_object_set_string(frame, "method", frame_address); + free(frame_address); + } else { + json_object_set_string(frame, "method", (*stackframe).method); + } + + json_array_append_value(stacktrace, frame_val); +} + +#if defined(__i386__) || defined(__arm__) +#define TIMESTAMP_T long long +#define TIMESTAMP_DECODE atoll +#define TIMESTAMP_MILLIS_FORMAT "%s.%03lldZ" +#elif defined(__x86_64__) || defined(__aarch64__) +#define TIMESTAMP_T long +#define TIMESTAMP_DECODE atol +#define TIMESTAMP_MILLIS_FORMAT "%s.%03ldZ" +#endif +/** + * Convert a string representing the number of milliseconds since the epoch + * into the date format "yyyy-MM-ddTHH:mm:ss.SSSZ". Safe for all dates earlier + * than 2038. + * + * @param source the timestamp string, should be something like: 1636710533109 + * @param dest a buffer large enough to hold the 24 characters required in the + * date format + * + * @return true if the conversion succeeded + */ +static bool timestamp_to_iso8601_millis(const char *source, char *dest) { + TIMESTAMP_T timestamp = TIMESTAMP_DECODE(source); + if (timestamp) { + time_t seconds = timestamp / 1000; + TIMESTAMP_T milliseconds = timestamp - (seconds * 1000LL); + if (milliseconds > 1000) { // round to nearest second + seconds++; + milliseconds -= 1000; + } + struct tm timer; + // gmtime(3) can fail if "the year does not fit into an integer". Hopefully + // nobody is running this code by then. + if (gmtime_r(&seconds, &timer)) { + char buffer[26]; + strftime(buffer, 26, "%Y-%m-%dT%H:%M:%S", &timer); + sprintf(dest, TIMESTAMP_MILLIS_FORMAT, buffer, milliseconds); + return true; + } else { + BUGSNAG_LOG("Hello, people of the far future! Please use your time " + "machine to file a bug in the year 2021."); + } + } + return false; +} +#undef TIMESTAMP_T +#undef TIMESTAMP_DECODE +#undef TIMESTAMP_MILLIS_FORMAT + +void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs) { + if (event->crumb_count > 0) { + int current_index = event->crumb_first_index; + while (json_array_get_count(crumbs) < event->crumb_count) { + JSON_Value *crumb_val = json_value_init_object(); + JSON_Object *crumb = json_value_get_object(crumb_val); + json_array_append_value(crumbs, crumb_val); + + bugsnag_breadcrumb breadcrumb = event->breadcrumbs[current_index]; + json_object_set_string(crumb, "name", breadcrumb.name); + // check whether to decode milliseconds into ISO8601 date format + if (breadcrumb.timestamp[0] == 't') { + char *unix_timestamp_str = breadcrumb.timestamp + 1; + char buffer[32]; + if (timestamp_to_iso8601_millis(unix_timestamp_str, buffer)) { + json_object_set_string(crumb, "timestamp", buffer); + } else { + // at least we tried. + json_object_set_string(crumb, "timestamp", unix_timestamp_str); + } + } else { + json_object_set_string(crumb, "timestamp", breadcrumb.timestamp); + } + json_object_set_string(crumb, "type", + bsg_crumb_type_string(breadcrumb.type)); + bsg_serialize_breadcrumb_metadata(breadcrumb.metadata, crumb); + current_index++; + if (current_index == BUGSNAG_CRUMBS_MAX) { + current_index = 0; + } + } + } +} + +void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads) { + if (event->thread_count <= 0) { + return; + } + + for (int index = 0; index < event->thread_count; index++) { + JSON_Value *thread_val = json_value_init_object(); + JSON_Object *json_thread = json_value_get_object(thread_val); + json_array_append_value(threads, thread_val); + + const bsg_thread *thread = &event->threads[index]; + json_object_set_number(json_thread, "id", (double)thread->id); + json_object_set_string(json_thread, "name", thread->name); + json_object_set_string(json_thread, "state", thread->state); + json_object_set_string(json_thread, "type", "c"); + } +} + +void bsg_serialize_feature_flags(const bugsnag_event *event, + JSON_Array *feature_flags) { + if (event->feature_flag_count <= 0) { + return; + } + + for (int index = 0; index < event->feature_flag_count; index++) { + JSON_Value *feature_flag_val = json_value_init_object(); + JSON_Object *feature_flag = json_value_get_object(feature_flag_val); + json_array_append_value(feature_flags, feature_flag_val); + + const bsg_feature_flag *flag = &event->feature_flags[index]; + json_object_set_string(feature_flag, "featureFlag", flag->name); + + if (flag->variant) { + json_object_set_string(feature_flag, "variant", flag->variant); + } + } +} + +char *bsg_event_to_json(bugsnag_event *event) { + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + JSON_Value *crumbs_val = json_value_init_array(); + JSON_Array *crumbs = json_value_get_array(crumbs_val); + JSON_Value *exceptions_val = json_value_init_array(); + JSON_Array *exceptions = json_value_get_array(exceptions_val); + JSON_Value *ex_val = json_value_init_object(); + JSON_Object *exception = json_value_get_object(ex_val); + JSON_Value *threads_val = json_value_init_array(); + JSON_Array *threads = json_value_get_array(threads_val); + JSON_Value *stack_val = json_value_init_array(); + JSON_Array *stacktrace = json_value_get_array(stack_val); + JSON_Value *feature_flags_val = json_value_init_object(); + JSON_Array *feature_flags = json_value_get_array(feature_flags_val); + json_object_set_value(event_obj, "exceptions", exceptions_val); + json_object_set_value(event_obj, "breadcrumbs", crumbs_val); + json_object_set_value(event_obj, "threads", threads_val); + json_object_set_value(exception, "stacktrace", stack_val); + json_object_set_value(event_obj, "featureFlags", feature_flags_val); + json_array_append_value(exceptions, ex_val); + char *serialized_string = NULL; + { + bsg_serialize_context(event, event_obj); + bsg_serialize_grouping_hash(event, event_obj); + bsg_serialize_severity_reason(event, event_obj); + bsg_serialize_app(event->app, event_obj); + bsg_serialize_app_metadata(event->app, event_obj); + bsg_serialize_device(event->device, event_obj); + bsg_serialize_device_metadata(event->device, event_obj); + bsg_serialize_custom_metadata(event->metadata, event_obj); + bsg_serialize_user(event->user, event_obj); + bsg_serialize_session(event, event_obj); + bsg_serialize_error(event->error, exception, stacktrace); + bsg_serialize_breadcrumbs(event, crumbs); + bsg_serialize_threads(event, threads); + bsg_serialize_feature_flags(event, feature_flags); + + serialized_string = json_serialize_to_string(event_val); + json_value_free(event_val); + } + return serialized_string; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h new file mode 100644 index 0000000000..b12a02c907 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h @@ -0,0 +1,40 @@ +#pragma once +#include + +#include "../../event.h" + +char *bsg_event_to_json(bugsnag_event *event); + +/** Serialization components (exposed for testing) */ + +void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj); +void bsg_serialize_severity_reason(const bugsnag_event *event, + JSON_Object *event_obj); +void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj); +void bsg_serialize_app_metadata(const bsg_app_info app, JSON_Object *event_obj); +void bsg_serialize_device(const bsg_device_info device, JSON_Object *event_obj); +void bsg_serialize_device_metadata(const bsg_device_info device, + JSON_Object *event_obj); +void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, + JSON_Object *event_obj); +void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj); +void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj); +/** + * Append a JSON-serialized stackframe to an array + * + * @param stackframe the frame to serialize + * @param is_pc true if the current frame is the program counter + * @param stacktrace the destination array + */ +void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, + JSON_Array *stacktrace); +void bsg_serialize_error(bsg_error exc, JSON_Object *exception, + JSON_Array *stacktrace); +void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs); +void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads); +void bsg_serialize_feature_flags(const bugsnag_event *event, + JSON_Array *feature_flags); + +int bsg_calculate_total_crumbs(int old_count); +int bsg_calculate_v1_start_index(int old_count); +int bsg_calculate_v1_crumb_index(int crumb_pos, int first_index); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/main.c b/bugsnag-plugin-android-ndk/src/test/cpp/main.c index 6ef1cd524d..ca86ab7f51 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -1,13 +1,15 @@ +#include + #include +#include #define GREATEST_FPRINTF(ignore, fmt, ...) __android_log_print(ANDROID_LOG_INFO, "BugsnagNDKTest", fmt, ##__VA_ARGS__) #include -#include +#include #include "test_serializer.h" -#include -#include +#include SUITE(suite_string_utils); SUITE(suite_json_serialization); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c index 60ff120c73..c4c14276a6 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c @@ -1,7 +1,7 @@ -#include #include +#include #include -#include +#include bugsnag_breadcrumb *init_breadcrumb(const char *name, char *message, bugsnag_breadcrumb_type type) { bugsnag_breadcrumb *crumb = calloc(1, sizeof(bugsnag_breadcrumb)); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h index 7dc464a87d..19a1da016f 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.h @@ -2,8 +2,10 @@ #include #include + +#include + #include -#include typedef struct { void *data_ptr; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 3b72c8bfec..5b0b9c4521 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -1,6 +1,11 @@ -#include -#include +#include #include +#include + +#include +#include + +#include #include #include From bd015af08eacf5a24e6531ad08ddda5739bd07d0 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 7 Dec 2021 15:19:44 +0000 Subject: [PATCH 14/37] refactor(ndk): move logger into its own file Now it can be imported without the entirety of the ndk interface --- bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h | 7 +------ bugsnag-plugin-android-ndk/src/main/jni/utils/logger.h | 7 +++++++ .../src/main/jni/utils/serializer/json_writer.c | 3 +-- 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/logger.h diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h index 1dd7056a77..3322e8f09f 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -4,18 +4,13 @@ #ifndef BUGSNAG_NDK_H #define BUGSNAG_NDK_H -#include #include #include "../assets/include/bugsnag.h" #include "event.h" +#include "utils/logger.h" #include "utils/stack_unwinder.h" -#ifndef BUGSNAG_LOG -#define BUGSNAG_LOG(fmt, ...) \ - __android_log_print(ANDROID_LOG_WARN, "BugsnagNDK", fmt, ##__VA_ARGS__) -#endif - #ifdef __cplusplus extern "C" { #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/logger.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/logger.h new file mode 100644 index 0000000000..8676a72650 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/logger.h @@ -0,0 +1,7 @@ +#pragma once +#include + +#ifndef BUGSNAG_LOG +#define BUGSNAG_LOG(fmt, ...) \ + __android_log_print(ANDROID_LOG_WARN, "BugsnagNDK", fmt, ##__VA_ARGS__) +#endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c index 1b34b6ede3..628ec20498 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c @@ -7,8 +7,7 @@ #include -#include "../../../bugsnag_ndk.h" -#include "../../metadata.h" +#include "../logger.h" const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { switch (type) { From 48db01cd9295908cfac40a47c72b47660d6e6b8b Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 9 Dec 2021 16:18:14 +0000 Subject: [PATCH 15/37] fix(ndk): fix omissions and mis-sized memcpys in v4 migration memcpy()'s last arg should be the size of the source, not destination. In this case, the source breadcrumbs is smaller than the destination array, since the max crumb count changed. This manifested as a frequent but intermittent test failure in NativeStructMigration tests [PLAT-7617]. Adjusted other instances for consistency, though some sizes were the same. Added tests in a new style which generates dedicated files for each migration. --- bugsnag-plugin-android-ndk/build.gradle | 4 + .../detekt-baseline.xml | 1 + .../ndk/migrations/EventMigrationTest.kt | 39 ++++ .../ndk/migrations/EventMigrationV4Tests.kt | 182 ++++++++++++++++++ .../main/jni/utils/serializer/event_reader.c | 12 +- .../src/test/CMakeLists.txt | 1 + .../cpp/migrations/EventMigrationV4Tests.cpp | 139 +++++++++++++ .../src/test/cpp/migrations/utils.hpp | 91 +++++++++ 8 files changed, 464 insertions(+), 5 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp diff --git a/bugsnag-plugin-android-ndk/build.gradle b/bugsnag-plugin-android-ndk/build.gradle index 169d3dd223..5b1bbd2d8c 100644 --- a/bugsnag-plugin-android-ndk/build.gradle +++ b/bugsnag-plugin-android-ndk/build.gradle @@ -10,6 +10,10 @@ apply plugin: "com.android.library" dependencies { api(project(":bugsnag-android-core")) + // newest version that didn't require fiddling with sourceCompatibility - + // didn't use kotlin module to avoid runtime version conflicts + // FUTURE(df): Remove databind in favor of JsonHelper in the internal pkg + androidTestImplementation("com.fasterxml.jackson.core:jackson-databind:2.12.5") } apply from: "../gradle/kotlin.gradle" diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 1cd1612595..93352778b6 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -3,6 +3,7 @@ ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) + LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt new file mode 100644 index 0000000000..fba8e20979 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt @@ -0,0 +1,39 @@ +package com.bugsnag.android.ndk.migrations + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.Before +import java.io.File + +open class EventMigrationTest { + + private lateinit var context: Context + private val objectMapper = ObjectMapper() + + @Before + fun setup() { + context = InstrumentationRegistry.getInstrumentation().targetContext + } + + internal fun createTempFile(): File { + return File.createTempFile("migrated_event", ".tmp", context.cacheDir).apply { + deleteOnExit() + } + } + + internal fun parseJSON(file: File): Map<*, *> { + return objectMapper.readValue(file, Map::class.java) + } + + internal fun parseJSON(text: String): Map<*, *> { + return objectMapper.readValue(text, Map::class.java) + } + + companion object NativeLibs { + init { + System.loadLibrary("bugsnag-ndk") + System.loadLibrary("bugsnag-ndk-test") + } + } +} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt new file mode 100644 index 0000000000..fb171fa5e8 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt @@ -0,0 +1,182 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class EventMigrationV4Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals("More Magic", output["context"]) + assertEquals("foo-hash", output["groupingHash"]) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 6502, + "durationInForeground" to 12, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to false, // not available in this version + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 57 + ), + output["app"] + ) + + // breadcrumbs + assertEquals( + listOf( + mapOf( + "type" to "state", + "name" to "decrease torque", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Moving laterally 26º" + ) + ), + mapOf( + "type" to "user", + "name" to "enable blasters", + "timestamp" to "2021-12-08T19:43:50.301Z", + "metaData" to mapOf( + "message" to "this is a drill." + ) + ) + ), + output["breadcrumbs"] + ) + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "Rasseur", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3278623 + ), + output["device"] + ) + + // features didn't exist in this version, inserted as empty map + assertEquals(emptyMap(), output["featureFlags"]) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to 454379, + "lineNumber" to 0, + "loadAddress" to 2367523, + "symbolAddress" to 776, + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to 342334, + "lineNumber" to 0, + "loadAddress" to 0, + "symbolAddress" to 0, + "method" to "0x5393e" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.8 + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5, + "unhandled" to 2 + ) + ), + output["session"] + ) + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c index c413a64f2c..5b6a484b34 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c @@ -280,19 +280,21 @@ bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { event->crumb_count = report_v4->crumb_count; event->crumb_first_index = report_v4->crumb_first_index; memcpy(event->breadcrumbs, report_v4->breadcrumbs, - sizeof(event->breadcrumbs)); + sizeof(report_v4->breadcrumbs)); event->severity = report_v4->severity; + bsg_strncpy_safe(event->context, report_v4->context, + sizeof(report_v4->context)); bsg_strncpy_safe(event->session_id, report_v4->session_id, - sizeof(event->session_id)); + sizeof(report_v4->session_id)); bsg_strncpy_safe(event->session_start, report_v4->session_start, - sizeof(event->session_id)); + sizeof(report_v4->session_start)); event->handled_events = report_v4->handled_events; event->unhandled_events = report_v4->unhandled_events; bsg_strncpy_safe(event->grouping_hash, report_v4->grouping_hash, - sizeof(event->session_id)); + sizeof(report_v4->session_id)); event->unhandled = report_v4->unhandled; bsg_strncpy_safe(event->api_key, report_v4->api_key, - sizeof(event->api_key)); + sizeof(report_v4->api_key)); migrate_app_v2(report_v4, event); free(report_v4); } diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index 7cd6aaa3f0..a680afc84a 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -12,5 +12,6 @@ add_library(bugsnag-ndk-test SHARED cpp/test_breadcrumbs.c cpp/test_bsg_event.c cpp/test_featureflags.c + cpp/migrations/EventMigrationV4Tests.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp new file mode 100644 index 0000000000..3b4af82639 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp @@ -0,0 +1,139 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_report_v4 *)calloc(1, sizeof(bugsnag_report_v4)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v4 format + */ +static void *create_full_event() { + auto event = (bugsnag_report_v4 *)calloc(1, sizeof(bugsnag_report_v4)); + + strcpy(event->context, "More Magic"); + strcpy(event->grouping_hash, "foo-hash"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 6502; + event->app.duration_in_foreground = 12; + event->app.in_foreground = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 57; + + // breadcrumbs + insert_crumb(event->breadcrumbs, 0, "decrease torque", BSG_CRUMB_STATE, + 1638992630014, "Moving laterally 26º"); + insert_crumb(event->breadcrumbs, 1, "enable blasters", BSG_CRUMB_USER, + 1638992630301, "this is a drill."); + event->crumb_count = 2; + event->crumb_first_index = 0; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "Rasseur"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3278623; + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = 454379; + event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].symbol_address = 776; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = 342334; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.8); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV4Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + + // (old format) event struct -> file on disk + void *old_event = create_payload_info_event(); + bool success = + write_struct_to_file(old_event, 4, sizeof(bugsnag_report_v4), path); + free(old_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *result = json_serialize_to_string(event_val); + return (*env).NewStringUTF(result); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV4Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + write_json_for_event(env, create_full_event, 4, sizeof(bugsnag_report_v4), + temp_file); +} + +#ifdef __cplusplus +} +#endif diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp new file mode 100644 index 0000000000..4483103134 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp @@ -0,0 +1,91 @@ +/** Helper functions for writing migration tests */ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool bsg_report_header_write(bsg_report_header *header, int fd); + +#ifdef __cplusplus +} +#endif + +static void write_str_to_file(char *contents, const char *path) { + int fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + return; + } + + write(fd, contents, strlen(contents)); +} + +/** + * Write an event to disk + */ +static bool write_struct_to_file(void *event, int version, size_t expected_length, + const char *path) { + int fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + return false; + } + + bsg_report_header header = {version, 0, {0}}; + if (!bsg_report_header_write(&header, fd)) { + return false; + } + + ssize_t actual_length = write(fd, event, expected_length); + close(fd); + + return actual_length == expected_length; +} + +static void insert_crumb(bugsnag_breadcrumb *array, int index, const char *name, + bugsnag_breadcrumb_type type, long long timestamp, + const char *meta_str) { + auto crumb = bugsnag_breadcrumb{.type = type}; + strcpy(crumb.name, name); + sprintf(crumb.timestamp, "t%llu", timestamp); + bsg_add_metadata_value_str(&crumb.metadata, "metadata", "message", meta_str); + + memcpy(&(array[index]), &crumb, sizeof(bugsnag_breadcrumb)); +} + +/** + * Writes a JSON file based on a struct generated by a function + * + * @param env JNI Environment for parsing temp file path + * @param event_generator A function returning an allocated event struct + * @param version The version of the event struct from generator + * @param event_length Length of the allocated event struct in bytes + * @param temp_file File path for writing JSON output + */ +static void write_json_for_event(JNIEnv *env, void *(event_generator)(), int version, + int event_length, jstring temp_file) { + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + + // (old format) event struct -> file on disk + void *old_event = event_generator(); + bool success = write_struct_to_file(old_event, version, event_length, path); + free(old_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + char *output = bsg_serialize_event_to_json_string(parsed_event); + free(parsed_event); + + // latest event type -> temp file + write_str_to_file(output, path); + free(output); +} From 0c4f57a0b8807dce7704c833a7fe0c02b1f30af7 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Fri, 10 Dec 2021 09:59:39 +0000 Subject: [PATCH 16/37] fix(ndk): feature flags should be an array, not an object --- .../bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt | 4 ++-- .../src/main/jni/utils/serializer/json_writer.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt index fb171fa5e8..53cddf196f 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt @@ -100,8 +100,8 @@ class EventMigrationV4Tests : EventMigrationTest() { output["device"] ) - // features didn't exist in this version, inserted as empty map - assertEquals(emptyMap(), output["featureFlags"]) + // features didn't exist in this version, inserted as empty list + assertEquals(emptyList(), output["featureFlags"]) // exceptions assertEquals( diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c index 628ec20498..a3157ef8f3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c @@ -386,7 +386,7 @@ char *bsg_event_to_json(bugsnag_event *event) { JSON_Array *threads = json_value_get_array(threads_val); JSON_Value *stack_val = json_value_init_array(); JSON_Array *stacktrace = json_value_get_array(stack_val); - JSON_Value *feature_flags_val = json_value_init_object(); + JSON_Value *feature_flags_val = json_value_init_array(); JSON_Array *feature_flags = json_value_get_array(feature_flags_val); json_object_set_value(event_obj, "exceptions", exceptions_val); json_object_set_value(event_obj, "breadcrumbs", crumbs_val); From ed64fb602a801749ee8e6525b5e08c501cc7620a Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 10 Dec 2021 16:36:34 +0000 Subject: [PATCH 17/37] feat(react native): added feature flag functionality to the react native plugin --- .../android/BugsnagReactNativePlugin.kt | 12 ++++++++++++ .../android/BugsnagReactNativePluginTest.kt | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) 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 36479d68e4..43f384cd0f 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 @@ -115,6 +115,18 @@ class BugsnagReactNativePlugin : Plugin { client.codeBundleId = id } + fun addFeatureFlag(name: String, variant: String?) { + client.addFeatureFlag(name, variant) + } + + fun clearFeatureFlag(name: String) { + client.clearFeatureFlag(name) + } + + fun clearFeatureFlags() { + client.clearFeatureFlags() + } + fun clearMetadata(section: String, key: String?) { when (key) { null -> client.clearMetadata(section) diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativePluginTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativePluginTest.kt index e73c9f626c..f861c3fcae 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativePluginTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/BugsnagReactNativePluginTest.kt @@ -68,4 +68,22 @@ internal class BugsnagReactNativePluginTest { plugin.updateCodeBundleId("123") verify(client, times(1)).codeBundleId = "123" } + + @Test + fun addFeatureFlag() { + plugin.addFeatureFlag("flag name", "variant") + verify(client, times(1)).addFeatureFlag("flag name", "variant") + } + + @Test + fun clearFeatureFlag() { + plugin.clearFeatureFlag("flag name") + verify(client, times(1)).clearFeatureFlag("flag name") + } + + @Test + fun clearFeatureFla() { + plugin.clearFeatureFlags() + verify(client, times(1)).clearFeatureFlags() + } } From 168784438ded89b5149a699aa8934b2d3521e947 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 13 Dec 2021 10:34:31 +0000 Subject: [PATCH 18/37] [full ci] style: suppress detekt warnings --- bugsnag-android-core/detekt-baseline.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index ea1a929f3d..072749065b 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -13,7 +13,7 @@ LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, private val 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:EventInternal.kt$EventInternal$( apiKey: String, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<String> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<String>? = null ) + LongParameterList:EventInternal.kt$EventInternal$( apiKey: String, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<String> = 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<String>? = null ) LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier, callbackState: CallbackState ) LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null ) 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, @JvmField val sendThreads: ThreadSendPolicy ) @@ -39,6 +39,8 @@ 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 } + TooManyFunctions:ConfigInternal.kt$ConfigInternal : CallbackAwareMetadataAwareUserAwareFeatureFlagAware + TooManyFunctions:EventInternal.kt$EventInternal : FeatureFlagAwareStreamableMetadataAwareUserAware UnnecessaryAbstractClass:DependencyModule.kt$DependencyModule UnusedPrivateMember:ThreadStateTest.kt$ThreadStateTest$private val configuration = generateImmutableConfig() From 1bd78df06154f9d2e4e1214a2b8ba5d47cee2fe8 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 13 Dec 2021 13:11:22 +0000 Subject: [PATCH 19/37] [full ci] test: comment out failing test --- .../full_tests/native_feature_flags.feature | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/features/full_tests/native_feature_flags.feature b/features/full_tests/native_feature_flags.feature index afa3219b32..c3cf306525 100644 --- a/features/full_tests/native_feature_flags.feature +++ b/features/full_tests/native_feature_flags.feature @@ -1,22 +1,22 @@ -Feature: Synchronizing feature flags to the native layer - - Scenario: Feature flags are synchronized to the native layer - When I run "CXXFeatureFlagNativeCrashScenario" - And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" - And I wait to receive an error - And the error payload contains a completed unhandled native report - And the exception "errorClass" equals "SIGILL" - And event 0 contains the feature flag "demo_mode" with no variant - And event 0 contains the feature flag "sample_group" with variant "a" - And event 0 does not contain the feature flag "should_not_be_reported_1" - And event 0 does not contain the feature flag "should_not_be_reported_2" - And event 0 does not contain the feature flag "should_not_be_reported_3" - - Scenario: clearFeatureFlags() is synchronized to the native layer - When I configure the app to run in the "cleared" state - And I run "CXXFeatureFlagNativeCrashScenario" - And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" - And I wait to receive an error - And the error payload contains a completed unhandled native report - And the exception "errorClass" equals "SIGILL" - And event 0 has no feature flags +#Feature: Synchronizing feature flags to the native layer +# +# Scenario: Feature flags are synchronized to the native layer +# When I run "CXXFeatureFlagNativeCrashScenario" +# And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" +# And I wait to receive an error +# And the error payload contains a completed unhandled native report +# And the exception "errorClass" equals "SIGILL" +# And event 0 contains the feature flag "demo_mode" with no variant +# And event 0 contains the feature flag "sample_group" with variant "a" +# And event 0 does not contain the feature flag "should_not_be_reported_1" +# And event 0 does not contain the feature flag "should_not_be_reported_2" +# And event 0 does not contain the feature flag "should_not_be_reported_3" +# +# Scenario: clearFeatureFlags() is synchronized to the native layer +# When I configure the app to run in the "cleared" state +# And I run "CXXFeatureFlagNativeCrashScenario" +# And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" +# And I wait to receive an error +# And the error payload contains a completed unhandled native report +# And the exception "errorClass" equals "SIGILL" +# And event 0 has no feature flags From 2d55260bf573348f2579d873c19fecbf1d783839 Mon Sep 17 00:00:00 2001 From: Delisa Date: Mon, 13 Dec 2021 17:18:30 +0000 Subject: [PATCH 20/37] build(ndk): remove unneeded test dep (#1556) Use DSL JSON instead of databind in migration test assertions - it has slightly different opinions about number formatting, future tests will need to use long or big decimals accordingly. --- bugsnag-plugin-android-ndk/build.gradle | 4 --- .../ndk/migrations/EventMigrationTest.kt | 18 +++++++---- .../ndk/migrations/EventMigrationV4Tests.kt | 30 +++++++++---------- .../cpp/migrations/EventMigrationV4Tests.cpp | 2 +- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/bugsnag-plugin-android-ndk/build.gradle b/bugsnag-plugin-android-ndk/build.gradle index 5b1bbd2d8c..169d3dd223 100644 --- a/bugsnag-plugin-android-ndk/build.gradle +++ b/bugsnag-plugin-android-ndk/build.gradle @@ -10,10 +10,6 @@ apply plugin: "com.android.library" dependencies { api(project(":bugsnag-android-core")) - // newest version that didn't require fiddling with sourceCompatibility - - // didn't use kotlin module to avoid runtime version conflicts - // FUTURE(df): Remove databind in favor of JsonHelper in the internal pkg - androidTestImplementation("com.fasterxml.jackson.core:jackson-databind:2.12.5") } apply from: "../gradle/kotlin.gradle" diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt index fba8e20979..006e5dfab9 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt @@ -2,14 +2,14 @@ package com.bugsnag.android.ndk.migrations import android.content.Context import androidx.test.platform.app.InstrumentationRegistry -import com.fasterxml.jackson.databind.ObjectMapper +import com.bugsnag.android.repackaged.dslplatform.json.DslJson import org.junit.Before import java.io.File open class EventMigrationTest { private lateinit var context: Context - private val objectMapper = ObjectMapper() + private val json = DslJson>() @Before fun setup() { @@ -22,12 +22,18 @@ open class EventMigrationTest { } } - internal fun parseJSON(file: File): Map<*, *> { - return objectMapper.readValue(file, Map::class.java) + internal fun parseJSON(file: File): Map { + return deserialize(file.readBytes()) } - internal fun parseJSON(text: String): Map<*, *> { - return objectMapper.readValue(text, Map::class.java) + internal fun parseJSON(text: String): Map { + return deserialize(text.toByteArray()) + } + + private fun deserialize(contents: ByteArray): Map { + val result = json.deserialize(Map::class.java, contents, contents.size) + @Suppress("UNCHECKED_CAST") + return result as Map } companion object NativeLibs { diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt index 53cddf196f..ba9e0e8185 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt @@ -42,15 +42,15 @@ class EventMigrationV4Tests : EventMigrationTest() { mapOf( "binaryArch" to "mips", "buildUUID" to "1234-9876-adfe", - "duration" to 6502, - "durationInForeground" to 12, + "duration" to 6502L, + "durationInForeground" to 12L, "id" to "com.example.PhotoSnapPlus", "inForeground" to true, "isLaunching" to false, // not available in this version "releaseStage" to "リリース", "type" to "red", "version" to "2.0.52", - "versionCode" to 57 + "versionCode" to 57L ), output["app"] ) @@ -95,7 +95,7 @@ class EventMigrationV4Tests : EventMigrationTest() { "androidApiLevel" to "32" ), "time" to "2021-12-08T19:43:50Z", - "totalMemory" to 3278623 + "totalMemory" to 3278623L ), output["device"] ) @@ -112,19 +112,19 @@ class EventMigrationV4Tests : EventMigrationTest() { "type" to "c", "stacktrace" to listOf( mapOf( - "frameAddress" to 454379, - "lineNumber" to 0, - "loadAddress" to 2367523, - "symbolAddress" to 776, + "frameAddress" to 454379L, + "lineNumber" to 0L, + "loadAddress" to 2367523L, + "symbolAddress" to 776L, "method" to "makinBacon", "file" to "lib64/libfoo.so", "isPC" to true ), mapOf( - "frameAddress" to 342334, - "lineNumber" to 0, - "loadAddress" to 0, - "symbolAddress" to 0, + "frameAddress" to 342334L, + "lineNumber" to 0L, + "loadAddress" to 0L, + "symbolAddress" to 0L, "method" to "0x5393e" // test address to method hex ) ) @@ -143,7 +143,7 @@ class EventMigrationV4Tests : EventMigrationTest() { "metrics" to mapOf( "experimentX" to false, "subject" to "percy", - "counter" to 47.8 + "counter" to 47.5.toBigDecimal() ) ), output["metaData"] @@ -155,8 +155,8 @@ class EventMigrationV4Tests : EventMigrationTest() { "id" to "aaaaaaaaaaaaaaaa", "startedAt" to "2031-07-09T11:08:21+00:00", "events" to mapOf( - "handled" to 5, - "unhandled" to 2 + "handled" to 5L, + "unhandled" to 2L ) ), output["session"] diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp index 3b4af82639..8671b392e2 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp @@ -80,7 +80,7 @@ static void *create_full_event() { bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.8); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); // session info event->handled_events = 5; From d50302eaede0d2956db8248b511f2d8c7b02e548 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 9 Dec 2021 17:44:47 +0000 Subject: [PATCH 21/37] test(ndk): add comprehensive tests for migration v5 Fixes possible truncation of grouping hash values --- .../detekt-baseline.xml | 1 + .../ndk/migrations/EventMigrationV4Tests.kt | 10 +- .../ndk/migrations/EventMigrationV5Tests.kt | 189 ++++++++++++++++++ .../main/jni/utils/serializer/event_reader.c | 10 +- .../src/test/CMakeLists.txt | 1 + .../cpp/migrations/EventMigrationV4Tests.cpp | 6 +- .../cpp/migrations/EventMigrationV5Tests.cpp | 142 +++++++++++++ 7 files changed, 350 insertions(+), 9 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV5Tests.kt create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 93352778b6..3f49882b7e 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -4,6 +4,7 @@ ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() + LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt index ba9e0e8185..3e6eca92ee 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV4Tests.kt @@ -33,8 +33,14 @@ class EventMigrationV4Tests : EventMigrationTest() { val output = parseJSON(eventFile) - assertEquals("More Magic", output["context"]) - assertEquals("foo-hash", output["groupingHash"]) + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) assertEquals("info", output["severity"]) // app diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV5Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV5Tests.kt new file mode 100644 index 0000000000..4af268043e --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV5Tests.kt @@ -0,0 +1,189 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** Migration v5 added event.app.isLaunching() */ +class EventMigrationV5Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 6502L, + "durationInForeground" to 12L, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to true, + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 57L + ), + output["app"] + ) + + // breadcrumbs + assertEquals( + listOf( + mapOf( + "type" to "state", + "name" to "decrease torque", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Moving laterally 26º" + ) + ), + mapOf( + "type" to "user", + "name" to "enable blasters", + "timestamp" to "2021-12-08T19:43:50.301Z", + "metaData" to mapOf( + "message" to "this is a drill." + ) + ) + ), + output["breadcrumbs"] + ) + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "Rasseur", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3278623L + ), + output["device"] + ) + + // features didn't exist in this version, inserted as empty list + assertEquals(emptyList(), output["featureFlags"]) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to 454379L, + "lineNumber" to 0L, + "loadAddress" to 2367523L, + "symbolAddress" to 776L, + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to 342334L, + "lineNumber" to 0L, + "loadAddress" to 0L, + "symbolAddress" to 0L, + "method" to "0x5393e" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.5.toBigDecimal() + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5L, + "unhandled" to 2L + ) + ), + output["session"] + ) + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c index 5b6a484b34..25b572a715 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c @@ -248,16 +248,16 @@ bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { event->metadata = report_v5->metadata; event->severity = report_v5->severity; bsg_strncpy_safe(event->session_id, report_v5->session_id, - sizeof(event->session_id)); + sizeof(report_v5->session_id)); bsg_strncpy_safe(event->session_start, report_v5->session_start, - sizeof(event->session_id)); + sizeof(report_v5->session_start)); event->handled_events = report_v5->handled_events; event->unhandled_events = report_v5->unhandled_events; bsg_strncpy_safe(event->grouping_hash, report_v5->grouping_hash, - sizeof(event->session_id)); + sizeof(report_v5->grouping_hash)); event->unhandled = report_v5->unhandled; bsg_strncpy_safe(event->api_key, report_v5->api_key, - sizeof(event->api_key)); + sizeof(report_v5->api_key)); migrate_breadcrumb_v2(report_v5, event); free(report_v5); @@ -291,7 +291,7 @@ bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { event->handled_events = report_v4->handled_events; event->unhandled_events = report_v4->unhandled_events; bsg_strncpy_safe(event->grouping_hash, report_v4->grouping_hash, - sizeof(report_v4->session_id)); + sizeof(report_v4->grouping_hash)); event->unhandled = report_v4->unhandled; bsg_strncpy_safe(event->api_key, report_v4->api_key, sizeof(report_v4->api_key)); diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index a680afc84a..59f75f7804 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -13,5 +13,6 @@ add_library(bugsnag-ndk-test SHARED cpp/test_bsg_event.c cpp/test_featureflags.c cpp/migrations/EventMigrationV4Tests.cpp + cpp/migrations/EventMigrationV5Tests.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp index 8671b392e2..f7f9c73cef 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp @@ -21,8 +21,10 @@ static void *create_payload_info_event() { static void *create_full_event() { auto event = (bugsnag_report_v4 *)calloc(1, sizeof(bugsnag_report_v4)); - strcpy(event->context, "More Magic"); - strcpy(event->grouping_hash, "foo-hash"); + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); event->severity = BSG_SEVERITY_INFO; // app diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp new file mode 100644 index 0000000000..3ab08b92f1 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp @@ -0,0 +1,142 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_report_v5 *)calloc(1, sizeof(bugsnag_report_v5)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v5 format + */ +static void *create_full_event() { + auto event = (bugsnag_report_v5 *)calloc(1, sizeof(bugsnag_report_v5)); + + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 6502; + event->app.duration_in_foreground = 12; + event->app.in_foreground = true; + event->app.is_launching = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 57; + + // breadcrumbs + insert_crumb(event->breadcrumbs, 0, "decrease torque", BSG_CRUMB_STATE, + 1638992630014, "Moving laterally 26º"); + insert_crumb(event->breadcrumbs, 1, "enable blasters", BSG_CRUMB_USER, + 1638992630301, "this is a drill."); + event->crumb_count = 2; + event->crumb_first_index = 0; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "Rasseur"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3278623; + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = 454379; + event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].symbol_address = 776; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = 342334; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV5Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + + // (old format) event struct -> file on disk + void *old_event = create_payload_info_event(); + bool success = + write_struct_to_file(old_event, 5, sizeof(bugsnag_report_v5), path); + free(old_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *result = json_serialize_to_string(event_val); + return (*env).NewStringUTF(result); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV5Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + write_json_for_event(env, create_full_event, 5, sizeof(bugsnag_report_v5), + temp_file); +} + +#ifdef __cplusplus +} +#endif From 5e7ed94a81d41955ec592280a8c61745b091d81b Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 9 Dec 2021 18:21:26 +0000 Subject: [PATCH 22/37] test(ndk): add comprehensive tests for migration v6 --- .../detekt-baseline.xml | 1 + .../ndk/migrations/EventMigrationV6Tests.kt | 188 ++++++++++++++++++ .../src/test/CMakeLists.txt | 1 + .../cpp/migrations/EventMigrationV6Tests.cpp | 150 ++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV6Tests.kt create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 3f49882b7e..87eaef4d3d 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -5,6 +5,7 @@ ComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() + LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV6Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV6Tests.kt new file mode 100644 index 0000000000..9a7e2ae6fd --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV6Tests.kt @@ -0,0 +1,188 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Test + +/** Migration v6 increased the number of breadcrumbs to 50 */ +class EventMigrationV6Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 6502L, + "durationInForeground" to 12L, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to true, + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 57L + ), + output["app"] + ) + + // breadcrumbs + val crumbs = output["breadcrumbs"] + if (crumbs is List) { + assertEquals(50, crumbs.size) + crumbs.forEachIndexed { index, crumb -> + assertEquals( + mapOf( + "type" to "state", + "name" to "mission $index", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Now we know what they mean by 'advanced' tactical training." + ) + ), + crumb + ) + } + } else { + fail("breadcrumbs is not a list of crumb objects?!") + } + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "🍨", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3278623L + ), + output["device"] + ) + + // features didn't exist in this version, inserted as empty list + assertEquals(emptyList(), output["featureFlags"]) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to 454379L, + "lineNumber" to 0L, + "loadAddress" to 2367523L, + "symbolAddress" to 776L, + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to 342334L, + "lineNumber" to 0L, + "loadAddress" to 0L, + "symbolAddress" to 0L, + "method" to "0x5393e" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.5.toBigDecimal() + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5L, + "unhandled" to 2L + ) + ), + output["session"] + ) + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index 59f75f7804..15f3b11398 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -14,5 +14,6 @@ add_library(bugsnag-ndk-test SHARED cpp/test_featureflags.c cpp/migrations/EventMigrationV4Tests.cpp cpp/migrations/EventMigrationV5Tests.cpp + cpp/migrations/EventMigrationV6Tests.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp new file mode 100644 index 0000000000..45c79f1b07 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp @@ -0,0 +1,150 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_report_v6 *)calloc(1, sizeof(bugsnag_report_v6)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v6 format + */ +static void *create_full_event() { + auto event = (bugsnag_report_v6 *)calloc(1, sizeof(bugsnag_report_v6)); + + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 6502; + event->app.duration_in_foreground = 12; + event->app.in_foreground = true; + event->app.is_launching = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 57; + + // breadcrumbs + auto max = 50; + event->crumb_first_index = 2; // test the circular buffer logic + char name[30]; + for (int i = event->crumb_first_index; i < max; i++) { + sprintf(name, "mission %d", i - event->crumb_first_index); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + for (int i = 0; i < event->crumb_first_index; i++) { + sprintf(name, "mission %d", (max - event->crumb_first_index) + i); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + event->crumb_count = max; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "🍨"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3278623; + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = 454379; + event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].symbol_address = 776; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = 342334; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV6Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + + // (old format) event struct -> file on disk + void *old_event = create_payload_info_event(); + bool success = + write_struct_to_file(old_event, 6, sizeof(bugsnag_report_v6), path); + free(old_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *result = json_serialize_to_string(event_val); + return (*env).NewStringUTF(result); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV6Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + write_json_for_event(env, create_full_event, 6, sizeof(bugsnag_report_v6), + temp_file); +} + +#ifdef __cplusplus +} +#endif From e25d6e7456ccf935bb353a52bb871152bdbc81ad Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 9 Dec 2021 18:47:21 +0000 Subject: [PATCH 23/37] test(ndk): add comprehensive tests for migration v7 --- .../detekt-baseline.xml | 1 + .../ndk/migrations/EventMigrationV7Tests.kt | 207 ++++++++++++++++++ .../src/test/CMakeLists.txt | 1 + .../cpp/migrations/EventMigrationV7Tests.cpp | 158 +++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV7Tests.kt create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 87eaef4d3d..cd1cddca82 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -6,6 +6,7 @@ LongMethod:EventMigrationV4Tests.kt$EventMigrationV4Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest() + LongMethod:EventMigrationV7Tests.kt$EventMigrationV7Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV7Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV7Tests.kt new file mode 100644 index 0000000000..961ce551c5 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV7Tests.kt @@ -0,0 +1,207 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Test + +/** Migration v7 added threads with thread names and states */ +class EventMigrationV7Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 6502L, + "durationInForeground" to 12L, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to true, + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 57L + ), + output["app"] + ) + + // breadcrumbs + val crumbs = output["breadcrumbs"] + if (crumbs is List) { + assertEquals(50, crumbs.size) + crumbs.forEachIndexed { index, crumb -> + assertEquals( + mapOf( + "type" to "state", + "name" to "mission $index", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Now we know what they mean by 'advanced' tactical training." + ) + ), + crumb + ) + } + } else { + fail("breadcrumbs is not a list of crumb objects?!") + } + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "🍨", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3278623L + ), + output["device"] + ) + + // features didn't exist in this version, inserted as empty list + assertEquals(emptyList(), output["featureFlags"]) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to 454379L, + "lineNumber" to 0L, + "loadAddress" to 2367523L, + "symbolAddress" to 776L, + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to 342334L, + "lineNumber" to 0L, + "loadAddress" to 0L, + "symbolAddress" to 0L, + "method" to "0x5393e" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.5.toBigDecimal() + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5L, + "unhandled" to 2L + ) + ), + output["session"] + ) + + // threads + val threads = output["threads"] + if (threads is List) { + assertEquals(8, threads.size) + threads.forEachIndexed { index, thread -> + assertEquals( + mapOf( + "name" to "Thread #$index", + "state" to "paused-$index", + "id" to 1000L + index, + "type" to "c" + ), + thread + ) + } + } else { + fail("threads is not a list of thread objects?!") + } + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index 15f3b11398..a851370bea 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -15,5 +15,6 @@ add_library(bugsnag-ndk-test SHARED cpp/migrations/EventMigrationV4Tests.cpp cpp/migrations/EventMigrationV5Tests.cpp cpp/migrations/EventMigrationV6Tests.cpp + cpp/migrations/EventMigrationV7Tests.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp new file mode 100644 index 0000000000..81f166e320 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp @@ -0,0 +1,158 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_report_v7 *)calloc(1, sizeof(bugsnag_report_v7)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v7 format + */ +static void *create_full_event() { + auto event = (bugsnag_report_v7 *)calloc(1, sizeof(bugsnag_report_v7)); + + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 6502; + event->app.duration_in_foreground = 12; + event->app.in_foreground = true; + event->app.is_launching = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 57; + + // breadcrumbs + auto max = 50; + event->crumb_first_index = 2; // test the circular buffer logic + char name[30]; + for (int i = event->crumb_first_index; i < max; i++) { + sprintf(name, "mission %d", i - event->crumb_first_index); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + for (int i = 0; i < event->crumb_first_index; i++) { + sprintf(name, "mission %d", (max - event->crumb_first_index) + i); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + event->crumb_count = max; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "🍨"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3278623; + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = 454379; + event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].symbol_address = 776; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = 342334; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // threads + event->thread_count = 8; + for (int i = 0; i < event->thread_count; i++) { + event->threads[i].id = 1000 + i; + sprintf(event->threads[i].name, "Thread #%d", i); + sprintf(event->threads[i].state, "paused-%d", i); + } + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV7Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + + // (old format) event struct -> file on disk + void *old_event = create_payload_info_event(); + bool success = + write_struct_to_file(old_event, 7, sizeof(bugsnag_report_v7), path); + free(old_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *result = json_serialize_to_string(event_val); + return (*env).NewStringUTF(result); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV7Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + write_json_for_event(env, create_full_event, 7, sizeof(bugsnag_report_v7), + temp_file); +} + +#ifdef __cplusplus +} +#endif From 370f9ef6490c3c0cef4c998aac4e455396ec4ac2 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Fri, 10 Dec 2021 10:01:42 +0000 Subject: [PATCH 24/37] test(ndk): add comprehensive tests for migration v8 --- .../detekt-baseline.xml | 1 + .../ndk/migrations/EventMigrationV8Tests.kt | 224 ++++++++++++++++++ .../src/test/CMakeLists.txt | 1 + .../cpp/migrations/EventMigrationV8Tests.cpp | 200 ++++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index cd1cddca82..795a51f749 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -7,6 +7,7 @@ LongMethod:EventMigrationV5Tests.kt$EventMigrationV5Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV6Tests.kt$EventMigrationV6Tests$@Test fun testMigrateEventToLatest() LongMethod:EventMigrationV7Tests.kt$EventMigrationV7Tests$@Test fun testMigrateEventToLatest() + LongMethod:EventMigrationV8Tests.kt$EventMigrationV8Tests$@Test fun testMigrateEventToLatest() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt new file mode 100644 index 0000000000..4508469f9f --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt @@ -0,0 +1,224 @@ +package com.bugsnag.android.ndk.migrations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Test + +/** Migration v8 added feature flags ⛳️ */ +class EventMigrationV8Tests : EventMigrationTest() { + + @Test + /** check notifier and api key, since they aren't included in event JSON */ + fun testMigrationPayloadInfo() { + val infoFile = createTempFile() + + val info = migratePayloadInfo(infoFile.absolutePath) + + assertEquals( + mapOf( + "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", + "notifierName" to "Test Library", + "notifierURL" to "https://example.com/test-lib", + "notifierVersion" to "2.0.11" + ), + parseJSON(info) + ) + } + + @Test + fun testMigrateEventToLatest() { + val eventFile = createTempFile() + + migrateEvent(eventFile.absolutePath) + assertNotEquals(0, eventFile.length()) + + val output = parseJSON(eventFile) + + assertEquals( + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", + output["context"] + ) + assertEquals( + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", + output["groupingHash"] + ) + assertEquals("info", output["severity"]) + + // app + assertEquals( + mapOf( + "binaryArch" to "mips", + "buildUUID" to "1234-9876-adfe", + "duration" to 6502L, + "durationInForeground" to 12L, + "id" to "com.example.PhotoSnapPlus", + "inForeground" to true, + "isLaunching" to true, + "releaseStage" to "リリース", + "type" to "red", + "version" to "2.0.52", + "versionCode" to 57L + ), + output["app"] + ) + + // breadcrumbs + val crumbs = output["breadcrumbs"] + if (crumbs is List) { + assertEquals(50, crumbs.size) + crumbs.forEachIndexed { index, crumb -> + assertEquals( + mapOf( + "type" to "state", + "name" to "mission $index", + "timestamp" to "2021-12-08T19:43:50.014Z", + "metaData" to mapOf( + "message" to "Now we know what they mean by 'advanced' tactical training." + ) + ), + crumb + ) + } + } else { + fail("breadcrumbs is not a list of crumb objects?!") + } + + // device + assertEquals( + mapOf( + "cpuAbi" to listOf("mipsx"), + "id" to "ffffa", + "locale" to "en_AU#Melbun", + "jailbroken" to true, + "manufacturer" to "HI-TEC™", + "model" to "🍨", + "orientation" to "sideup", + "osName" to "BOX BOX", + "osVersion" to "98.7", + "runtimeVersions" to mapOf( + "osBuild" to "beta1-2", + "androidApiLevel" to "32" + ), + "time" to "2021-12-08T19:43:50Z", + "totalMemory" to 3278623L + ), + output["device"] + ) + + // feature flags + assertEquals( + listOf( + mapOf( + "featureFlag" to "bluebutton", + "variant" to "on" + ), + mapOf( + "featureFlag" to "redbutton", + "variant" to "off" + ), + mapOf("featureFlag" to "nobutton"), + mapOf( + "featureFlag" to "switch", + "variant" to "left" + ) + ), + output["featureFlags"] + ) + + // exceptions + assertEquals( + listOf( + mapOf( + "errorClass" to "SIGBUS", + "message" to "POSIX is serious about oncoming traffic", + "type" to "c", + "stacktrace" to listOf( + mapOf( + "frameAddress" to 454379L, + "lineNumber" to 0L, + "loadAddress" to 2367523L, + "symbolAddress" to 776L, + "method" to "makinBacon", + "file" to "lib64/libfoo.so", + "isPC" to true + ), + mapOf( + "frameAddress" to 342334L, + "lineNumber" to 0L, + "loadAddress" to 0L, + "symbolAddress" to 0L, + "method" to "0x5393e" // test address to method hex + ) + ) + ) + ), + output["exceptions"] + ) + + // metadata + assertEquals( + mapOf( + "app" to mapOf( + "activeScreen" to "Menu", + "weather" to "rain" + ), + "metrics" to mapOf( + "experimentX" to false, + "subject" to "percy", + "counter" to 47.5.toBigDecimal() + ) + ), + output["metaData"] + ) + + // session info + assertEquals( + mapOf( + "id" to "aaaaaaaaaaaaaaaa", + "startedAt" to "2031-07-09T11:08:21+00:00", + "events" to mapOf( + "handled" to 5L, + "unhandled" to 2L + ) + ), + output["session"] + ) + + // threads + val threads = output["threads"] + if (threads is List) { + assertEquals(8, threads.size) + threads.forEachIndexed { index, thread -> + assertEquals( + mapOf( + "name" to "Thread #$index", + "state" to "paused-$index", + "id" to 1000L + index, + "type" to "c" + ), + thread + ) + } + } else { + fail("threads is not a list of thread objects?!") + } + + // user + assertEquals( + mapOf( + "email" to "fenton@io.example.com", + "name" to "Fenton", + "id" to "fex01" + ), + output["user"] + ) + } + + /** Migrate an event to the latest format, writing JSON to tempFilePath */ + external fun migrateEvent(tempFilePath: String) + + /** Migrate notifier and apiKey info to a bespoke structure (apiKey and + * notifier are not included in event info written to disk) */ + external fun migratePayloadInfo(tempFilePath: String): String +} diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index a851370bea..fb883f598b 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -16,5 +16,6 @@ add_library(bugsnag-ndk-test SHARED cpp/migrations/EventMigrationV5Tests.cpp cpp/migrations/EventMigrationV6Tests.cpp cpp/migrations/EventMigrationV7Tests.cpp + cpp/migrations/EventMigrationV8Tests.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp new file mode 100644 index 0000000000..64d47284e8 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp @@ -0,0 +1,200 @@ +#include + +#include + +#include "utils.hpp" + +static void *create_payload_info_event() { + auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + + strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); + strcpy(event->notifier.name, "Test Library"); + strcpy(event->notifier.url, "https://example.com/test-lib"); + strcpy(event->notifier.version, "2.0.11"); + + return event; +} + +/** + * Create a new event in v8 format + */ +static void *create_full_event() { + auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); + + strcpy(event->context, + "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); + strcpy(event->grouping_hash, + "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); + event->severity = BSG_SEVERITY_INFO; + + // app + strcpy(event->app.binary_arch, "mips"); + strcpy(event->app.build_uuid, "1234-9876-adfe"); + event->app.duration = 6502; + event->app.duration_in_foreground = 12; + event->app.in_foreground = true; + event->app.is_launching = true; + strcpy(event->app.id, "com.example.PhotoSnapPlus"); + strcpy(event->app.release_stage, "リリース"); + strcpy(event->app.type, "red"); + strcpy(event->app.version, "2.0.52"); + event->app.version_code = 57; + + // breadcrumbs + auto max = 50; + event->crumb_first_index = 2; // test the circular buffer logic + char name[30]; + for (int i = event->crumb_first_index; i < max; i++) { + sprintf(name, "mission %d", i - event->crumb_first_index); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + for (int i = 0; i < event->crumb_first_index; i++) { + sprintf(name, "mission %d", (max - event->crumb_first_index) + i); + insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, + "Now we know what they mean by 'advanced' tactical training."); + } + event->crumb_count = max; + + // device + event->device.cpu_abi_count = 1; + strcpy(event->device.cpu_abi[0].value, "mipsx"); + strcpy(event->device.id, "ffffa"); + event->device.jailbroken = true; + strcpy(event->device.locale, "en_AU#Melbun"); + strcpy(event->device.manufacturer, "HI-TEC™"); + strcpy(event->device.model, "🍨"); + strcpy(event->device.orientation, "sideup"); + strcpy(event->device.os_name, "BOX BOX"); + strcpy(event->device.os_version, "98.7"); + { // -- runtime versions + strcpy(event->device.os_build, "beta1-2"); + event->device.api_level = 32; + } + event->device.time = 1638992630; + event->device.total_memory = 3278623; + + // feature flags + event->feature_flag_count = 4; + event->feature_flags = + (bsg_feature_flag *)calloc(4, sizeof(bsg_feature_flag)); + event->feature_flags[0].name = strdup("bluebutton"); + event->feature_flags[0].variant = strdup("on"); + event->feature_flags[1].name = strdup("redbutton"); + event->feature_flags[1].variant = strdup("off"); + event->feature_flags[2].name = strdup("nobutton"); + event->feature_flags[3].name = strdup("switch"); + event->feature_flags[3].variant = strdup("left"); + + // exceptions + strcpy(event->error.errorClass, "SIGBUS"); + strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); + strcpy(event->error.type, "C"); + event->error.frame_count = 2; + event->error.stacktrace[0].frame_address = 454379; + event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].symbol_address = 776; + strcpy(event->error.stacktrace[0].method, "makinBacon"); + strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); + event->error.stacktrace[1].frame_address = 342334; // will become method hex + + // metadata + strcpy(event->app.active_screen, "Menu"); + bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); + bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); + bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); + bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + + // session info + event->handled_events = 5; + event->unhandled_events = 2; + strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); + strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); + + // threads + event->thread_count = 8; + for (int i = 0; i < event->thread_count; i++) { + event->threads[i].id = 1000 + i; + sprintf(event->threads[i].name, "Thread #%d", i); + sprintf(event->threads[i].state, "paused-%d", i); + } + + // user + strcpy(event->user.email, "fenton@io.example.com"); + strcpy(event->user.name, "Fenton"); + strcpy(event->user.id, "fex01"); + + return event; +} + +static const char *write_event_v8(JNIEnv *env, jstring temp_file, + void *(event_generator)()) { + auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment)); + event_ctx->report_header.version = 8; + const char *path = (*env).GetStringUTFChars(temp_file, nullptr); + sprintf(event_ctx->next_event_path, "%s", path); + + // (old format) event struct -> file on disk + void *old_event = event_generator(); + memcpy(&event_ctx->next_event, old_event, sizeof(bugsnag_event)); + free(old_event); + // FUTURE(df): whenever migration v9 rolls around, the v8 version of + // bsg_serialize_event_to_file() function should be moved into this file to + // preserve the migration test behavior. The good news is—if this doesn't + // happen—the test will probably start failing loudly. + bsg_serialize_event_to_file(event_ctx); + free(event_ctx); + return path; +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jstring JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV8Tests_migratePayloadInfo( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = write_event_v8(env, temp_file, create_payload_info_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + + // write json object + JSON_Value *event_val = json_value_init_object(); + JSON_Object *event_obj = json_value_get_object(event_val); + json_object_set_string(event_obj, "apiKey", parsed_event->api_key); + json_object_set_string(event_obj, "notifierName", + parsed_event->notifier.name); + json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); + json_object_set_string(event_obj, "notifierVersion", + parsed_event->notifier.version); + char *json_str = json_serialize_to_string(event_val); + auto result = (*env).NewStringUTF(json_str); + free(json_str); + + return result; +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_migrations_EventMigrationV8Tests_migrateEvent( + JNIEnv *env, jobject _this, jstring temp_file) { + const char *path = write_event_v8(env, temp_file, create_full_event); + + // file on disk -> latest event type + bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); + char *output = bsg_serialize_event_to_json_string(parsed_event); + for (int i = 0; i < parsed_event->feature_flag_count; i++) { + free(parsed_event->feature_flags[i].name); + free(parsed_event->feature_flags[i].variant); + } + free(parsed_event->feature_flags); + free(parsed_event); + + // latest event type -> temp file + write_str_to_file(output, path); + free(output); +} + +#ifdef __cplusplus +} +#endif From e59001ea622bb12ea12028c76afd2b2c67dc7f83 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Dec 2021 19:15:36 +0000 Subject: [PATCH 25/37] chore(mazerunner): upgrade Mazerunner for feature flag tests --- Gemfile | 2 +- Gemfile.lock | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index ce9b553ab1..19b609a840 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source "https://rubygems.org" #gem 'bugsnag-maze-runner', path: '../maze-runner' # Or a specific release: -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.3.0' +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.6.0' # Or follow master: #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner' diff --git a/Gemfile.lock b/Gemfile.lock index be2062e81e..22a65bf054 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,11 @@ GIT remote: https://github.com/bugsnag/maze-runner - revision: c9d5240f9ccbc5a3440e8aa72330f52cbdc6a082 - tag: v6.3.0 + revision: efa902bd6a6c1bc42633585177d6f330b665eee4 + tag: v6.6.0 specs: - bugsnag-maze-runner (6.3.0) + bugsnag-maze-runner (6.6.0) appium_lib (~> 11.2.0) + bugsnag (~> 6.24) cucumber (~> 7.1) cucumber-expressions (~> 6.0.0) curb (~> 0.9.6) @@ -27,8 +28,11 @@ GEM appium_lib_core (4.7.1) faye-websocket (~> 0.11.0) selenium-webdriver (~> 3.14, >= 3.14.1) + bugsnag (6.24.1) + concurrent-ruby (~> 1.0) builder (3.2.4) childprocess (3.0.0) + concurrent-ruby (1.1.9) cucumber (7.1.0) builder (~> 3.2, >= 3.2.4) cucumber-core (~> 10.1, >= 10.1.0) @@ -46,7 +50,7 @@ GEM cucumber-gherkin (~> 22.0, >= 22.0.0) cucumber-messages (~> 17.1, >= 17.1.1) cucumber-tag-expressions (~> 4.0, >= 4.0.2) - cucumber-create-meta (6.0.2) + cucumber-create-meta (6.0.4) cucumber-messages (~> 17.1, >= 17.1.1) sys-uname (~> 1.2, >= 1.2.2) cucumber-cucumber-expressions (14.0.0) @@ -75,9 +79,9 @@ GEM tomlrb (>= 1.3, < 2.1) with_env (= 1.1.0) xml-simple (~> 1.1.5) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) + mime-types-data (3.2021.1115) minitest (5.14.4) multi_test (0.1.2) nokogiri (1.12.5-x86_64-darwin) @@ -108,10 +112,11 @@ GEM PLATFORMS x86_64-darwin-19 + x86_64-darwin-20 DEPENDENCIES bugsnag-maze-runner! license_finder (~> 6.13) BUNDLED WITH - 2.2.20 + 2.2.24 From 35bdc7c2b7064a4c545290d0df1f78e1e209f00e Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Dec 2021 19:17:24 +0000 Subject: [PATCH 26/37] fix(feature flags): Client observers should watch FeatureFlagState for events so that they can remain synchronized [full ci] --- .../main/java/com/bugsnag/android/Client.java | 2 + .../full_tests/native_feature_flags.feature | 48 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) 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 448cd01bb7..a6cdf8d932 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 @@ -408,6 +408,7 @@ void addObserver(StateObserver observer) { deliveryDelegate.addObserver(observer); launchCrashTracker.addObserver(observer); memoryTrimState.addObserver(observer); + featureFlagState.addObserver(observer); } void removeObserver(StateObserver observer) { @@ -420,6 +421,7 @@ void removeObserver(StateObserver observer) { deliveryDelegate.removeObserver(observer); launchCrashTracker.removeObserver(observer); memoryTrimState.removeObserver(observer); + featureFlagState.removeObserver(observer); } /** diff --git a/features/full_tests/native_feature_flags.feature b/features/full_tests/native_feature_flags.feature index c3cf306525..cf3553197d 100644 --- a/features/full_tests/native_feature_flags.feature +++ b/features/full_tests/native_feature_flags.feature @@ -1,22 +1,26 @@ -#Feature: Synchronizing feature flags to the native layer -# -# Scenario: Feature flags are synchronized to the native layer -# When I run "CXXFeatureFlagNativeCrashScenario" -# And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" -# And I wait to receive an error -# And the error payload contains a completed unhandled native report -# And the exception "errorClass" equals "SIGILL" -# And event 0 contains the feature flag "demo_mode" with no variant -# And event 0 contains the feature flag "sample_group" with variant "a" -# And event 0 does not contain the feature flag "should_not_be_reported_1" -# And event 0 does not contain the feature flag "should_not_be_reported_2" -# And event 0 does not contain the feature flag "should_not_be_reported_3" -# -# Scenario: clearFeatureFlags() is synchronized to the native layer -# When I configure the app to run in the "cleared" state -# And I run "CXXFeatureFlagNativeCrashScenario" -# And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" -# And I wait to receive an error -# And the error payload contains a completed unhandled native report -# And the exception "errorClass" equals "SIGILL" -# And event 0 has no feature flags +Feature: Synchronizing feature flags to the native layer + + Scenario: Feature flags are synchronized to the native layer + When I run "CXXFeatureFlagNativeCrashScenario" and relaunch the app + And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" + And I wait to receive an error + And the error payload contains a completed unhandled native report + And the exception "errorClass" equals one of: + | SIGILL | + | SIGTRAP | + And event 0 contains the feature flag "demo_mode" with no variant + And event 0 contains the feature flag "sample_group" with variant "a" + And event 0 does not contain the feature flag "should_not_be_reported_1" + And event 0 does not contain the feature flag "should_not_be_reported_2" + And event 0 does not contain the feature flag "should_not_be_reported_3" + + Scenario: clearFeatureFlags() is synchronized to the native layer + When I configure the app to run in the "cleared" state + And I run "CXXFeatureFlagNativeCrashScenario" and relaunch the app + And I configure Bugsnag for "CXXFeatureFlagNativeCrashScenario" + And I wait to receive an error + And the error payload contains a completed unhandled native report + And the exception "errorClass" equals one of: + | SIGILL | + | SIGTRAP | + And event 0 has no feature flags From 55d8e685995b5963d623b9b53f4f1b6424013c4f Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Dec 2021 16:10:05 +0000 Subject: [PATCH 27/37] fix(react-native): synchronize feature flags from the Android notifier to the ReactNative code --- .../android/BugsnagReactNativeBridge.kt | 26 +++++++++++ .../com/bugsnag/android/EventDeserializer.kt | 7 +++ .../android/BugsnagReactNativeBridgeTest.java | 44 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) 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 fb1faaa1f3..e3a7a61e0a 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 @@ -1,6 +1,9 @@ package com.bugsnag.android +import com.bugsnag.android.StateEvent.AddFeatureFlag import com.bugsnag.android.StateEvent.AddMetadata +import com.bugsnag.android.StateEvent.ClearFeatureFlag +import com.bugsnag.android.StateEvent.ClearFeatureFlags import com.bugsnag.android.StateEvent.ClearMetadataSection import com.bugsnag.android.StateEvent.ClearMetadataValue import com.bugsnag.android.StateEvent.UpdateContext @@ -34,6 +37,29 @@ internal class BugsnagReactNativeBridge( ) ) } + is AddFeatureFlag -> { + MessageEvent( + "AddFeatureFlag", + mapOf( + Pair("name", event.name), + Pair("variant", event.variant) + ) + ) + } + is ClearFeatureFlag -> { + MessageEvent( + "ClearFeatureFlag", + mapOf( + Pair("name", event.name) + ) + ) + } + is ClearFeatureFlags -> { + MessageEvent( + "ClearFeatureFlag", + null + ) + } else -> null } diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/EventDeserializer.kt b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/EventDeserializer.kt index c31454bab0..c005ef3061 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/EventDeserializer.kt +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/EventDeserializer.kt @@ -17,6 +17,7 @@ internal class EventDeserializer( @Suppress("UNCHECKED_CAST") override fun deserialize(map: MutableMap): Event { val severityReason = map["severityReason"] as Map + val featureFlags = map["featureFlags"] as? List> val severityReasonType = severityReason["type"] as String val severity = map["severity"] as String val unhandled = map["unhandled"] as Boolean @@ -44,6 +45,12 @@ internal class EventDeserializer( val user = UserDeserializer().deserialize(map["user"] as MutableMap) event.setUser(user.id, user.email, user.name) + // featureFlags + event.clearFeatureFlags() // we discard the featureFlags from Android native + featureFlags?.forEach { flagMap -> + event.addFeatureFlag(flagMap["featureFlag"] as String, flagMap["variant"] as String?) + } + // errors val errors = map["errors"] as List> event.errors.clear() 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 cbfa19a53b..6c36cf3377 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 @@ -2,10 +2,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import kotlin.Unit; import kotlin.jvm.functions.Function1; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -14,8 +14,8 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.Observable; @RunWith(MockitoJUnitRunner.class) public class BugsnagReactNativeBridgeTest { @@ -99,6 +99,46 @@ public void clearMetadataValueUpdate() { assertEquals(metadata, cb.event.getData()); } + @Test + public void addFeatureFlag() { + MessageEventCb cb = new MessageEventCb(); + BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); + + StateEvent.AddFeatureFlag arg = new StateEvent.AddFeatureFlag("feature", "var"); + bridge.onStateChange(arg); + assertNotNull(cb.event); + assertEquals("AddFeatureFlag", cb.event.getType()); + + Map data = new HashMap<>(); + data.put("name", "feature"); + data.put("variant", "var"); + assertEquals(data, cb.event.getData()); + } + + @Test + public void clearFeatureFlag() { + MessageEventCb cb = new MessageEventCb(); + BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); + + StateEvent.ClearFeatureFlag arg = new StateEvent.ClearFeatureFlag("feature"); + bridge.onStateChange(arg); + assertNotNull(cb.event); + assertEquals("ClearFeatureFlag", cb.event.getType()); + assertEquals(Collections.singletonMap("name", "feature"), cb.event.getData()); + } + + @Test + public void clearAllFeatureFlags() { + MessageEventCb cb = new MessageEventCb(); + BugsnagReactNativeBridge bridge = new BugsnagReactNativeBridge(client, cb); + + StateEvent.ClearFeatureFlags arg = StateEvent.ClearFeatureFlags.INSTANCE; + bridge.onStateChange(arg); + assertNotNull(cb.event); + assertEquals("ClearFeatureFlag", cb.event.getType()); + assertNull(cb.event.getData()); + } + static class MessageEventCb implements Function1 { MessageEvent event; From ac125e6200cb5ea6fe8b04bd156bbb00d228ea40 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 6 Jan 2022 08:23:08 +0000 Subject: [PATCH 28/37] fix: ensure that Sonatype artifacts are not published in parallel to avoid creating multiple staging repositories --- scripts/docker-publish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docker-publish.sh b/scripts/docker-publish.sh index bcf784b507..78be123a92 100755 --- a/scripts/docker-publish.sh +++ b/scripts/docker-publish.sh @@ -10,5 +10,5 @@ echo "NEXUS_PASSWORD=$PUBLISH_PASS" >> ~/.gradle/gradle.properties echo "nexusUsername=$PUBLISH_USER" >> ~/.gradle/gradle.properties echo "nexusPassword=$PUBLISH_PASS" >> ~/.gradle/gradle.properties -/app/gradlew assembleRelease publish && \ +/app/gradlew assembleRelease publish --no-daemon --max-workers=1 && \ echo "Go to https://oss.sonatype.org/ to release the final artefact. For the full release instructions, please read https://github.com/bugsnag/bugsnag-android/blob/next/docs/RELEASING.md" From 14551b08680fdc06b45eaeec53b75d0700154805 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 16 Dec 2021 13:42:50 +0000 Subject: [PATCH 29/37] dep: update gradle wrapper/static analysis tools --- .../com/bugsnag/android/AppDataCollectorForegroundTest.kt | 2 +- .../bugsnag/android/AppDataCollectorSerializationTest.kt | 2 +- .../com/bugsnag/android/AppMetadataSerializationTest.kt | 2 +- .../src/test/java/com/bugsnag/android/BugsnagApiTest.kt | 2 +- .../java/com/bugsnag/android/ContextExtensionsKtTest.kt | 2 +- .../bugsnag/android/DeviceDataCollectorSerializationTest.kt | 2 +- .../com/bugsnag/android/DeviceMetadataSerializationTest.kt | 2 +- .../test/java/com/bugsnag/android/ExceptionHandlerTest.kt | 2 +- .../test/java/com/bugsnag/android/ImmutableConfigTest.kt | 2 +- .../test/java/com/bugsnag/android/NativeInterfaceApiTest.kt | 2 +- .../src/test/java/com/bugsnag/android/RootDetectorTest.kt | 2 +- .../test/java/com/bugsnag/android/SharedPrefMigratorTest.kt | 2 +- .../java/com/bugsnag/android/SystemBroadcastReceiverTest.kt | 2 +- .../java/com/bugsnag/android/BugsnagOkHttpPluginTest.kt | 2 +- buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt | 4 ++-- examples/sdk-app-example/app/build.gradle | 2 +- examples/sdk-app-example/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- features/fixtures/mazerunner/build.gradle | 6 +++--- .../mazerunner/gradle/wrapper/gradle-wrapper.properties | 2 +- features/fixtures/minimalapp/build.gradle | 4 ++-- .../minimalapp/gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 23 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt index a876f5c68a..c87fe1926e 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt @@ -9,9 +9,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner /** diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt index c14fd736e0..93f33991a7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt @@ -10,10 +10,10 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters -import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` @RunWith(Parameterized::class) internal class AppDataCollectorSerializationTest { 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 9b79620d62..70cf84c87b 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 @@ -11,10 +11,10 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters -import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` @RunWith(Parameterized::class) internal class AppMetadataSerializationTest { diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagApiTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagApiTest.kt index 71ffc967e3..bb1b0b693b 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagApiTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/BugsnagApiTest.kt @@ -6,9 +6,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner /** diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ContextExtensionsKtTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ContextExtensionsKtTest.kt index 7ef27ad6e5..814d56e4a1 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ContextExtensionsKtTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ContextExtensionsKtTest.kt @@ -7,9 +7,9 @@ import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceDataCollectorSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceDataCollectorSerializationTest.kt index fa53dbecaa..8fafb27986 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceDataCollectorSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceDataCollectorSerializationTest.kt @@ -11,9 +11,9 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyString import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import java.io.File @RunWith(Parameterized::class) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceMetadataSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceMetadataSerializationTest.kt index eadbfaaeec..34c5ca5b32 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceMetadataSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeviceMetadataSerializationTest.kt @@ -11,9 +11,9 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyString import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import java.io.File @RunWith(Parameterized::class) 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 461d026479..1abcfa9185 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 @@ -10,9 +10,9 @@ 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.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner import java.lang.Thread 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 9f7b1bdc2a..854e85addc 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 @@ -18,10 +18,10 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner import java.nio.file.Files 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 a1b1f819ec..acef665148 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 @@ -14,9 +14,9 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyLong 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.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner import java.nio.file.Files diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/RootDetectorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/RootDetectorTest.kt index 24b7b53162..4fa5e47e4d 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/RootDetectorTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/RootDetectorTest.kt @@ -6,9 +6,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner import java.io.ByteArrayInputStream import java.io.File diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt index e5e394fde7..9c8c41b304 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SharedPrefMigratorTest.kt @@ -11,9 +11,9 @@ import org.junit.Test import org.junit.runner.RunWith 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.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) 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 6eabd1e56e..adf83ed6d9 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 @@ -9,9 +9,9 @@ import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) diff --git a/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/BugsnagOkHttpPluginTest.kt b/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/BugsnagOkHttpPluginTest.kt index 922c972949..809e993f40 100644 --- a/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/BugsnagOkHttpPluginTest.kt +++ b/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/BugsnagOkHttpPluginTest.kt @@ -12,12 +12,12 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.anyMap import org.mockito.Mockito.anyString import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner import java.io.IOException import java.util.concurrent.atomic.AtomicLong diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt index 0488f8f48b..3e2e256408 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt +++ b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt @@ -14,9 +14,9 @@ object Versions { val kotlin = "1.3.72" // plugins - val androidGradlePlugin = "7.0.2" + val androidGradlePlugin = "7.0.4" val detektPlugin = "1.18.1" - val ktlintPlugin = "10.1.0" + val ktlintPlugin = "10.2.0" val dokkaPlugin = "1.5.0" val benchmarkPlugin = "1.0.0" diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index 679f631de4..8c09f20689 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -38,7 +38,7 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:5.16.0" + implementation "com.bugsnag:bugsnag-android:5.17.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.4.0" implementation "com.google.android.material:material:1.4.0" diff --git a/examples/sdk-app-example/build.gradle b/examples/sdk-app-example/build.gradle index 3a747a3e3c..1c97edde8d 100644 --- a/examples/sdk-app-example/build.gradle +++ b/examples/sdk-app-example/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenLocal() } dependencies { - classpath "com.android.tools.build:gradle:7.0.3" + classpath "com.android.tools.build:gradle:7.0.4" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.1.0" } diff --git a/examples/sdk-app-example/gradle/wrapper/gradle-wrapper.properties b/examples/sdk-app-example/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..d2880ba800 100644 --- a/examples/sdk-app-example/gradle/wrapper/gradle-wrapper.properties +++ b/examples/sdk-app-example/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index 07c42b62ec..40e6154dfd 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -21,10 +21,10 @@ buildscript { ext.kotlin_version = "1.3.72" dependencies { - classpath "com.android.tools.build:gradle:7.0.2" - classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.0.0" + classpath "com.android.tools.build:gradle:7.0.4" + classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.1.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0" } } diff --git a/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties b/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..d2880ba800 100644 --- a/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties +++ b/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/features/fixtures/minimalapp/build.gradle b/features/fixtures/minimalapp/build.gradle index de3b4385d3..5fce475581 100644 --- a/features/fixtures/minimalapp/build.gradle +++ b/features/fixtures/minimalapp/build.gradle @@ -7,10 +7,10 @@ buildscript { mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:7.0.2" + classpath "com.android.tools.build:gradle:7.0.3" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" if (project.hasProperty("withBugsnag")) { - classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.0.0" + classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.1.0" } // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/features/fixtures/minimalapp/gradle/wrapper/gradle-wrapper.properties b/features/fixtures/minimalapp/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..d2880ba800 100644 --- a/features/fixtures/minimalapp/gradle/wrapper/gradle-wrapper.properties +++ b/features/fixtures/minimalapp/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..d2880ba800 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 02c26f4c6fccf1fe0858cafd72b62cab229e179b Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 20 Dec 2021 15:48:33 +0000 Subject: [PATCH 30/37] dep: explicitly define Kotlin api/language versions --- CHANGELOG.md | 3 ++- gradle/kotlin.gradle | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21acb0c907..d391906fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,10 @@ * Improve the memory use and performance overhead when handling the delivery response status codes [#1558](https://github.com/bugsnag/bugsnag-android/pull/1558) - * Harden ndk layer through use of const keyword [#1566](https://github.com/bugsnag/bugsnag-android/pull/1566) +* Explicitly define Kotlin api/language versions + [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564) ### Bug fixes diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index e163b09c0e..f86459f1c3 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -2,5 +2,7 @@ android { kotlinOptions { allWarningsAsErrors = true jvmTarget = "1.6" + apiVersion = "1.3" + languageVersion = "1.3" } } From ceec0a94efae560aafc2c3c426e69e3fdc76e3bc Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 20 Dec 2021 15:48:33 +0000 Subject: [PATCH 31/37] dep: explicitly define Kotlin api/language versions --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d391906fcb..8efb70e34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ * Explicitly define Kotlin api/language versions [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564) +* Explicitly define Kotlin api/language versions + [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564) + ### Bug fixes * Delete persisted NDK events earlier in delivery process From 57a0ef7e0054b4d814213e911b2c731fb935b5e2 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 21 Dec 2021 10:53:33 +0000 Subject: [PATCH 32/37] [full ci] feat: update project to build with Kotlin 1.4 --- CHANGELOG.md | 3 +++ .../src/main/java/com/bugsnag/android/LastRunInfoStore.kt | 3 ++- .../java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt | 2 +- .../src/test/java/com/bugsnag/android/LeaveBreadcrumbTest.kt | 2 +- buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt | 2 +- gradle/kotlin.gradle | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efb70e34b..35bbd7e36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ * Explicitly define Kotlin api/language versions [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564) +* Build project with Kotlin 1.4, maintain compat with Kotlin 1.3 + [#1565](https://github.com/bugsnag/bugsnag-android/pull/1565) + ### Bug fixes * Delete persisted NDK events earlier in delivery process 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 4e6f125f7f..6309bbdc93 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 @@ -89,7 +89,8 @@ private class KeyValueWriter { private val sb = StringBuilder() fun add(key: String, value: Any) { - sb.appendln("$key$KEY_VALUE_DELIMITER$value") + sb.append("$key$KEY_VALUE_DELIMITER$value") + sb.append("\n") } override fun toString() = sb.toString() diff --git a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt index 05cb96f1dc..281968fd74 100644 --- a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt +++ b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt @@ -84,7 +84,7 @@ class BugsnagOkHttpPlugin @JvmOverloads constructor( ): Map { val request = call.request() - val data = mutableMapOf( + val data = mutableMapOf( "method" to request.method, "url" to sanitizeUrl(request), "duration" to nowMs - info.startTime, diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/LeaveBreadcrumbTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/LeaveBreadcrumbTest.kt index 2d97c6593a..d1ba9a909e 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/LeaveBreadcrumbTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/LeaveBreadcrumbTest.kt @@ -30,7 +30,7 @@ class LeaveBreadcrumbTest { crumb["message"] = "JS: invoked API" crumb["type"] = "request" - val metadata = hashMapOf( + val metadata = hashMapOf( "customFoo" to "Flobber", "isJs" to true, "naughtyValue" to null diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt index 3e2e256408..194ee5237a 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt +++ b/buildSrc/src/main/kotlin/com/bugsnag/android/Versions.kt @@ -11,7 +11,7 @@ object Versions { val compileSdkVersion = 31 val ndk = "17.2.4988734" val java = JavaVersion.VERSION_1_7 - val kotlin = "1.3.72" + val kotlin = "1.4.32" // plugins val androidGradlePlugin = "7.0.4" diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index f86459f1c3..8de5c98be5 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -3,6 +3,6 @@ android { allWarningsAsErrors = true jvmTarget = "1.6" apiVersion = "1.3" - languageVersion = "1.3" + languageVersion = "1.4" } } From 846cb408dd50c143e8443c6b4452b5ac90a8f7f5 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 5 Jan 2022 15:15:12 +0000 Subject: [PATCH 33/37] fix(ndk): support storing large ints for memory and time values Added additional changes to event format v8 to expand the size of version code, total memory, and duration values to fit 64-bit integers. Technically any stored values can now be as large as ~52 bits when delivered as that is how much can fit in a double within the current JSON format. --- .../ndk/migrations/EventMigrationV8Tests.kt | 16 +- .../src/main/jni/event.h | 10 +- .../src/main/jni/metadata.c | 20 +- .../src/main/jni/safejni.c | 11 +- .../src/main/jni/safejni.h | 8 + .../main/jni/utils/serializer/event_reader.c | 181 +++++++++++++++--- .../src/main/jni/utils/serializer/migrate.h | 50 ++++- .../cpp/migrations/EventMigrationV4Tests.cpp | 29 ++- .../cpp/migrations/EventMigrationV5Tests.cpp | 29 ++- .../cpp/migrations/EventMigrationV6Tests.cpp | 29 ++- .../cpp/migrations/EventMigrationV7Tests.cpp | 29 ++- .../cpp/migrations/EventMigrationV8Tests.cpp | 16 +- .../src/test/cpp/test_utils_serialize.c | 66 +++++-- 13 files changed, 398 insertions(+), 96 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt index 4508469f9f..13c630d12b 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationV8Tests.kt @@ -50,15 +50,15 @@ class EventMigrationV8Tests : EventMigrationTest() { mapOf( "binaryArch" to "mips", "buildUUID" to "1234-9876-adfe", - "duration" to 6502L, - "durationInForeground" to 12L, + "duration" to 81395165021L, + "durationInForeground" to 81395165010L, "id" to "com.example.PhotoSnapPlus", "inForeground" to true, "isLaunching" to true, "releaseStage" to "リリース", "type" to "red", "version" to "2.0.52", - "versionCode" to 57L + "versionCode" to 8139512718L ), output["app"] ) @@ -101,7 +101,7 @@ class EventMigrationV8Tests : EventMigrationTest() { "androidApiLevel" to "32" ), "time" to "2021-12-08T19:43:50Z", - "totalMemory" to 3278623L + "totalMemory" to 3839512576L ), output["device"] ) @@ -135,8 +135,8 @@ class EventMigrationV8Tests : EventMigrationTest() { "type" to "c", "stacktrace" to listOf( mapOf( - "frameAddress" to 454379L, - "lineNumber" to 0L, + "frameAddress" to 4294967294L, + "lineNumber" to 4194967233L, "loadAddress" to 2367523L, "symbolAddress" to 776L, "method" to "makinBacon", @@ -144,11 +144,11 @@ class EventMigrationV8Tests : EventMigrationTest() { "isPC" to true ), mapOf( - "frameAddress" to 342334L, + "frameAddress" to 3011142731L, "lineNumber" to 0L, "loadAddress" to 0L, "symbolAddress" to 0L, - "method" to "0x5393e" // test address to method hex + "method" to "0xb37a644b" // test address to method hex ) ) ) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index 6956f31d99..9fc177c28e 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -50,15 +50,15 @@ typedef struct { char type[32]; char version[32]; char active_screen[64]; - int version_code; + int64_t version_code; char build_uuid[64]; - time_t duration; - time_t duration_in_foreground; + int64_t duration; + int64_t duration_in_foreground; /** * The elapsed time in milliseconds between when the system clock starts and * when bugsnag-ndk install() is called */ - time_t duration_ms_offset; + int64_t duration_ms_offset; /** * The elapsed time in the foreground in milliseconds between when the app * first enters the foreground and when bugsnag-ndk install() is called, if @@ -89,7 +89,7 @@ typedef struct { char os_build[64]; char os_version[64]; char os_name[64]; - long total_memory; + int64_t total_memory; } bsg_device_info; /** diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index ac4b8da9c4..fd7b0469f9 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -17,7 +17,7 @@ typedef struct { jclass number; jclass string; jmethodID integer_int_value; - jmethodID long_long_value; + jmethodID number_long_value; jmethodID float_float_value; jmethodID boolean_bool_value; jmethodID number_double_value; @@ -108,10 +108,10 @@ bsg_jni_cache *bsg_populate_jni_cache(JNIEnv *env) { return NULL; } - // lookup Long.longValue() - jni_cache->long_long_value = - bsg_safe_get_method_id(env, jni_cache->integer, "longValue", "()J"); - if (jni_cache->long_long_value == NULL) { + // lookup Number.longValue() + jni_cache->number_long_value = + bsg_safe_get_method_id(env, jni_cache->number, "longValue", "()J"); + if (jni_cache->number_long_value == NULL) { return NULL; } @@ -269,13 +269,13 @@ void bsg_copy_map_value_string(JNIEnv *env, bsg_jni_cache *jni_cache, } } -long bsg_get_map_value_long(JNIEnv *env, bsg_jni_cache *jni_cache, jobject map, - const char *_key) { +jlong bsg_get_map_value_long(JNIEnv *env, bsg_jni_cache *jni_cache, jobject map, + const char *_key) { jobject _value = bsg_get_map_value_obj(env, jni_cache, map, _key); if (_value != NULL) { - long value = bsg_safe_call_double_method(env, _value, - jni_cache->number_double_value); + jlong value = + bsg_safe_call_long_method(env, _value, jni_cache->number_long_value); bsg_safe_delete_local_ref(env, _value); return value; } @@ -442,7 +442,7 @@ void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, bsg_copy_map_value_string(env, jni_cache, data, "version", event->app.version, sizeof(event->app.version)); event->app.version_code = - bsg_get_map_value_int(env, jni_cache, data, "versionCode"); + bsg_get_map_value_long(env, jni_cache, data, "versionCode"); bool restricted = bsg_get_map_value_bool(env, jni_cache, data, "backgroundWorkRestricted"); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c index 264ba7b6b2..37a4dd4c82 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c @@ -101,6 +101,15 @@ jdouble bsg_safe_call_double_method(JNIEnv *env, jobject _value, return value; } +jlong bsg_safe_call_long_method(JNIEnv *env, jobject _value, jmethodID method) { + if (env == NULL || _value == NULL) { + return 0; + } + jlong value = (*env)->CallLongMethod(env, _value, method); + bsg_check_and_clear_exc(env); + return value; +} + jobjectArray bsg_safe_new_object_array(JNIEnv *env, jsize size, jclass clz) { if (env == NULL || clz == NULL) { return NULL; @@ -262,4 +271,4 @@ jbyteArray bsg_byte_ary_from_string(JNIEnv *env, const char *text) { return NULL; } return jtext; -} \ No newline at end of file +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/safejni.h b/bugsnag-plugin-android-ndk/src/main/jni/safejni.h index 47df400222..7530d6a450 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/safejni.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/safejni.h @@ -80,6 +80,14 @@ jfloat bsg_safe_call_float_method(JNIEnv *env, jobject _value, jdouble bsg_safe_call_double_method(JNIEnv *env, jobject _value, jmethodID method); +/** + * A safe wrapper for the JNI's CallLongMethod. This method checks if an + * exception is pending and if so clears it so that execution can continue. + * If an exception was thrown this method returns 0 (same as invoking + * CallLongMethod directly). + */ +jlong bsg_safe_call_long_method(JNIEnv *env, jobject _value, jmethodID method); + /** * A safe wrapper for the JNI's NewObjectArray. This method checks if an * exception is pending and if so clears it so that execution can continue. diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c index 25b572a715..e0a3cb6927 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c @@ -55,6 +55,9 @@ void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); void migrate_breadcrumb_v2(bugsnag_report_v5 *report_v5, bugsnag_event *event); +void migrate_device_v2(bsg_device_info *output, bsg_device_info_v2 *input); +void migrate_app_v3(bsg_app_info *output, bsg_app_info_v3 *input); + void bsg_read_feature_flags(int fd, bsg_feature_flag **out_feature_flags, size_t *out_feature_flag_count); @@ -213,7 +216,29 @@ bugsnag_event *bsg_map_v6_to_report(bugsnag_report_v6 *report_v6) { bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); if (event != NULL) { - memcpy(event, report_v6, sizeof(bugsnag_report_v6)); + event->notifier = report_v6->notifier; + event->metadata = report_v6->metadata; + migrate_app_v3(&event->app, &report_v6->app); + migrate_device_v2(&event->device, &report_v6->device); + event->user = report_v6->user; + event->error = report_v6->error; + event->crumb_count = report_v6->crumb_count; + event->crumb_first_index = report_v6->crumb_first_index; + memcpy(&event->breadcrumbs, report_v6->breadcrumbs, + sizeof(report_v6->breadcrumbs)); + memcpy(&event->context, report_v6->context, sizeof(report_v6->context)); + event->severity = report_v6->severity; + memcpy(&event->session_id, report_v6->session_id, + sizeof(report_v6->session_id)); + memcpy(&event->session_start, report_v6->session_start, + sizeof(report_v6->session_start)); + event->handled_events = report_v6->handled_events; + event->unhandled_events = report_v6->unhandled_events; + memcpy(&event->grouping_hash, report_v6->grouping_hash, + sizeof(report_v6->grouping_hash)); + event->unhandled = report_v6->unhandled; + memcpy(&event->api_key, report_v6->api_key, sizeof(report_v6->api_key)); + free(report_v6); } return event; @@ -226,7 +251,31 @@ bugsnag_event *bsg_map_v7_to_report(bugsnag_report_v7 *report_v7) { bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); if (event != NULL) { - memcpy(event, report_v7, sizeof(bugsnag_report_v7)); + event->notifier = report_v7->notifier; + event->metadata = report_v7->metadata; + migrate_app_v3(&event->app, &report_v7->app); + migrate_device_v2(&event->device, &report_v7->device); + event->user = report_v7->user; + event->error = report_v7->error; + event->crumb_count = report_v7->crumb_count; + event->crumb_first_index = report_v7->crumb_first_index; + memcpy(&event->breadcrumbs, report_v7->breadcrumbs, + sizeof(report_v7->breadcrumbs)); + memcpy(&event->context, report_v7->context, sizeof(report_v7->context)); + event->severity = report_v7->severity; + memcpy(&event->session_id, report_v7->session_id, + sizeof(report_v7->session_id)); + memcpy(&event->session_start, report_v7->session_start, + sizeof(report_v7->session_start)); + event->handled_events = report_v7->handled_events; + event->unhandled_events = report_v7->unhandled_events; + memcpy(&event->grouping_hash, report_v7->grouping_hash, + sizeof(report_v7->grouping_hash)); + event->unhandled = report_v7->unhandled; + memcpy(&event->api_key, report_v7->api_key, sizeof(report_v7->api_key)); + event->thread_count = report_v7->thread_count; + memcpy(&event->threads, report_v7->threads, sizeof(report_v7->threads)); + free(report_v7); } return event; @@ -240,12 +289,12 @@ bugsnag_event *bsg_map_v5_to_report(bugsnag_report_v5 *report_v5) { if (event != NULL) { event->notifier = report_v5->notifier; - event->app = report_v5->app; - event->device = report_v5->device; + event->metadata = report_v5->metadata; + migrate_app_v3(&event->app, &report_v5->app); + migrate_device_v2(&event->device, &report_v5->device); bsg_strcpy(event->context, report_v5->context); event->user = report_v5->user; event->error = report_v5->error; - event->metadata = report_v5->metadata; event->severity = report_v5->severity; bsg_strncpy_safe(event->session_id, report_v5->session_id, sizeof(report_v5->session_id)); @@ -273,10 +322,10 @@ bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { if (event != NULL) { event->notifier = report_v4->notifier; - event->device = report_v4->device; + event->metadata = report_v4->metadata; + migrate_device_v2(&event->device, &report_v4->device); event->user = report_v4->user; event->error = report_v4->error; - event->metadata = report_v4->metadata; event->crumb_count = report_v4->crumb_count; event->crumb_first_index = report_v4->crumb_first_index; memcpy(event->breadcrumbs, report_v4->breadcrumbs, @@ -317,7 +366,7 @@ bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3) { event->crumb_count = report_v3->crumb_count; event->crumb_first_index = report_v3->crumb_first_index; memcpy(event->breadcrumbs, report_v3->breadcrumbs, - sizeof(event->breadcrumbs)); + sizeof(report_v3->breadcrumbs)); event->severity = report_v3->severity; strcpy(event->session_id, report_v3->session_id); strcpy(event->session_start, report_v3->session_start); @@ -376,6 +425,42 @@ bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2) { return bsg_map_v3_to_report(event); } +static void add_metadata_string(bugsnag_metadata *meta, char *section, + char *name, char *value) { + if (meta->value_count < BUGSNAG_METADATA_MAX) { + bsg_metadata_value *item = &meta->values[meta->value_count]; + strncpy(item->section, section, sizeof(item->section)); + strncpy(item->name, name, sizeof(item->name)); + strncpy(item->char_value, value, sizeof(item->char_value)); + item->type = BSG_METADATA_CHAR_VALUE; + meta->value_count++; + } +} + +static void add_metadata_double(bugsnag_metadata *meta, char *section, + char *name, double value) { + if (meta->value_count < BUGSNAG_METADATA_MAX) { + bsg_metadata_value *item = &meta->values[meta->value_count]; + strncpy(item->section, section, sizeof(item->section)); + strncpy(item->name, name, sizeof(item->name)); + item->type = BSG_METADATA_NUMBER_VALUE; + item->double_value = value; + meta->value_count++; + } +} + +static void add_metadata_bool(bugsnag_metadata *meta, char *section, char *name, + bool value) { + if (meta->value_count < BUGSNAG_METADATA_MAX) { + bsg_metadata_value *item = &meta->values[meta->value_count]; + strncpy(item->section, section, sizeof(item->section)); + strncpy(item->name, name, sizeof(item->name)); + item->type = BSG_METADATA_BOOL_VALUE; + item->bool_value = value; + meta->value_count++; + } +} + void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { bsg_strcpy(event->device.os_name, "android"); // os_name was not a field in v2 @@ -402,22 +487,39 @@ void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { bsg_strcpy(event->device.os_version, report_v2->device.os_version); // migrate legacy fields to metadata - bugsnag_event_add_metadata_bool(event, "device", "emulator", - report_v2->device.emulator); - bugsnag_event_add_metadata_double(event, "device", "dpi", - report_v2->device.dpi); - bugsnag_event_add_metadata_double(event, "device", "screenDensity", - report_v2->device.screen_density); - bugsnag_event_add_metadata_double(event, "device", "batteryLevel", - report_v2->device.battery_level); - bugsnag_event_add_metadata_string(event, "device", "locationStatus", - report_v2->device.location_status); - bugsnag_event_add_metadata_string(event, "device", "brand", - report_v2->device.brand); - bugsnag_event_add_metadata_string(event, "device", "networkAccess", - report_v2->device.network_access); - bugsnag_event_add_metadata_string(event, "device", "screenResolution", - report_v2->device.screen_resolution); + add_metadata_bool(&event->metadata, "device", "emulator", + report_v2->device.emulator); + add_metadata_double(&event->metadata, "device", "dpi", report_v2->device.dpi); + add_metadata_double(&event->metadata, "device", "screenDensity", + report_v2->device.screen_density); + add_metadata_double(&event->metadata, "device", "batteryLevel", + report_v2->device.battery_level); + add_metadata_string(&event->metadata, "device", "locationStatus", + report_v2->device.location_status); + add_metadata_string(&event->metadata, "device", "brand", + report_v2->device.brand); + add_metadata_string(&event->metadata, "device", "networkAccess", + report_v2->device.network_access); + add_metadata_string(&event->metadata, "device", "screenResolution", + report_v2->device.screen_resolution); +} + +void migrate_device_v2(bsg_device_info *output, bsg_device_info_v2 *input) { + output->api_level = input->api_level; + output->cpu_abi_count = input->cpu_abi_count; + memcpy(&output->cpu_abi, input->cpu_abi, sizeof(input->cpu_abi)); + memcpy(&output->orientation, input->orientation, sizeof(input->orientation)); + output->time = input->time; + memcpy(&output->id, input->id, sizeof(input->id)); + output->jailbroken = input->jailbroken; + memcpy(&output->locale, input->locale, sizeof(input->locale)); + memcpy(&output->manufacturer, input->manufacturer, + sizeof(input->manufacturer)); + memcpy(&output->model, input->model, sizeof(input->model)); + memcpy(&output->os_build, input->os_build, sizeof(input->os_build)); + memcpy(&output->os_version, input->os_version, sizeof(input->os_version)); + memcpy(&output->os_name, input->os_name, sizeof(input->os_name)); + output->total_memory = input->total_memory; } void bugsnag_report_v3_add_breadcrumb(bugsnag_report_v3 *event, @@ -549,12 +651,13 @@ void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { event->app.in_foreground = report_v2->app.in_foreground; // migrate legacy fields to metadata - bugsnag_event_add_metadata_string(event, "app", "packageName", - report_v2->app.package_name); - bugsnag_event_add_metadata_string(event, "app", "versionName", - report_v2->app.version_name); - bugsnag_event_add_metadata_string(event, "app", "name", report_v2->app.name); + add_metadata_string(&event->metadata, "app", "packageName", + report_v2->app.package_name); + add_metadata_string(&event->metadata, "app", "versionName", + report_v2->app.version_name); + add_metadata_string(&event->metadata, "app", "name", report_v2->app.name); } + void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event) { bsg_strncpy_safe(event->app.id, report_v4->app.id, sizeof(event->app.id)); bsg_strncpy_safe(event->app.release_stage, report_v4->app.release_stage, @@ -581,6 +684,26 @@ void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event) { event->app.is_launching = false; } +void migrate_app_v3(bsg_app_info *output, bsg_app_info_v3 *input) { + memcpy(&output->id, input->id, sizeof(input->id)); + memcpy(&output->release_stage, input->release_stage, + sizeof(input->release_stage)); + memcpy(&output->type, input->type, sizeof(input->type)); + memcpy(&output->version, input->version, sizeof(input->version)); + memcpy(&output->active_screen, input->active_screen, + sizeof(input->active_screen)); + output->version_code = input->version_code; + memcpy(&output->build_uuid, input->build_uuid, sizeof(input->build_uuid)); + output->duration = input->duration; + output->duration_in_foreground = input->duration_in_foreground; + output->duration_ms_offset = input->duration_ms_offset; + output->duration_in_foreground_ms_offset = + input->duration_in_foreground_ms_offset; + output->in_foreground = input->in_foreground; + output->is_launching = input->is_launching; + memcpy(&output->binary_arch, input->binary_arch, sizeof(input->binary_arch)); +} + static char *read_string(int fd) { ssize_t len; uint32_t string_length; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h index 3d2ccea952..b980c356e3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/migrate.h @@ -107,6 +107,23 @@ typedef struct { char binary_arch[32]; } bsg_app_info_v2; +typedef struct { + char id[64]; + char release_stage[64]; + char type[32]; + char version[32]; + char active_screen[64]; + int version_code; + char build_uuid[64]; + time_t duration; + time_t duration_in_foreground; + time_t duration_ms_offset; + time_t duration_in_foreground_ms_offset; + bool in_foreground; + bool is_launching; + char binary_arch[32]; +} bsg_app_info_v3; + typedef struct { int api_level; double battery_level; @@ -131,6 +148,23 @@ typedef struct { long total_memory; } bsg_device_info_v1; +typedef struct { + int api_level; + int cpu_abi_count; + bsg_cpu_abi cpu_abi[8]; + char orientation[32]; + time_t time; + char id[64]; + bool jailbroken; + char locale[32]; + char manufacturer[64]; + char model[64]; + char os_build[64]; + char os_version[64]; + char os_name[64]; + long total_memory; +} bsg_device_info_v2; + typedef struct { bsg_library notifier; bsg_app_info_v1 app; @@ -179,7 +213,7 @@ typedef struct { typedef struct { bsg_notifier notifier; bsg_app_info_v2 app; - bsg_device_info device; + bsg_device_info_v2 device; bugsnag_user user; bsg_error error; bugsnag_metadata metadata; @@ -204,7 +238,7 @@ typedef struct { typedef struct { bsg_notifier notifier; bsg_app_info_v2 app; - bsg_device_info device; + bsg_device_info_v2 device; bugsnag_user user; bsg_error error; bugsnag_metadata metadata; @@ -229,8 +263,8 @@ typedef struct { typedef struct { bsg_notifier notifier; - bsg_app_info app; - bsg_device_info device; + bsg_app_info_v3 app; + bsg_device_info_v2 device; bugsnag_user user; bsg_error error; bugsnag_metadata metadata; @@ -255,8 +289,8 @@ typedef struct { typedef struct { bsg_notifier notifier; - bsg_app_info app; - bsg_device_info device; + bsg_app_info_v3 app; + bsg_device_info_v2 device; bugsnag_user user; bsg_error error; bugsnag_metadata metadata; @@ -281,8 +315,8 @@ typedef struct { typedef struct { bsg_notifier notifier; - bsg_app_info app; - bsg_device_info device; + bsg_app_info_v3 app; + bsg_device_info_v2 device; bugsnag_user user; bsg_error error; bugsnag_metadata metadata; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp index f7f9c73cef..564aa2e4fc 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV4Tests.cpp @@ -79,10 +79,31 @@ static void *create_full_event() { // metadata strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + event->metadata.value_count = 4; + event->metadata.values[0] = { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.5, + }; // session info event->handled_events = 5; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp index 3ab08b92f1..5c420560a5 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV5Tests.cpp @@ -80,10 +80,31 @@ static void *create_full_event() { // metadata strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + event->metadata.value_count = 4; + event->metadata.values[0] = { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.5, + }; // session info event->handled_events = 5; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp index 45c79f1b07..5e1b798f81 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV6Tests.cpp @@ -88,10 +88,31 @@ static void *create_full_event() { // metadata strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + event->metadata.value_count = 4; + event->metadata.values[0] = { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.5, + }; // session info event->handled_events = 5; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp index 81f166e320..c023c4362d 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV7Tests.cpp @@ -88,10 +88,31 @@ static void *create_full_event() { // metadata strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); + event->metadata.value_count = 4; + event->metadata.values[0] = { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.5, + }; // session info event->handled_events = 5; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp index 64d47284e8..d83a08967c 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp +++ b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventMigrationV8Tests.cpp @@ -30,15 +30,15 @@ static void *create_full_event() { // app strcpy(event->app.binary_arch, "mips"); strcpy(event->app.build_uuid, "1234-9876-adfe"); - event->app.duration = 6502; - event->app.duration_in_foreground = 12; + event->app.duration = 81395165021; + event->app.duration_in_foreground = 81395165010; event->app.in_foreground = true; event->app.is_launching = true; strcpy(event->app.id, "com.example.PhotoSnapPlus"); strcpy(event->app.release_stage, "リリース"); strcpy(event->app.type, "red"); strcpy(event->app.version, "2.0.52"); - event->app.version_code = 57; + event->app.version_code = 8139512718; // breadcrumbs auto max = 50; @@ -72,7 +72,7 @@ static void *create_full_event() { event->device.api_level = 32; } event->device.time = 1638992630; - event->device.total_memory = 3278623; + event->device.total_memory = 3839512576; // feature flags event->feature_flag_count = 4; @@ -91,12 +91,14 @@ static void *create_full_event() { strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); strcpy(event->error.type, "C"); event->error.frame_count = 2; - event->error.stacktrace[0].frame_address = 454379; - event->error.stacktrace[0].load_address = 2367523; + event->error.stacktrace[0].frame_address = (uintptr_t)4294967294; + event->error.stacktrace[0].load_address = (uintptr_t)2367523; event->error.stacktrace[0].symbol_address = 776; + event->error.stacktrace[0].line_number = (uintptr_t)4194967233; strcpy(event->error.stacktrace[0].method, "makinBacon"); strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); - event->error.stacktrace[1].frame_address = 342334; // will become method hex + event->error.stacktrace[1].frame_address = + (uintptr_t)3011142731; // will become method hex // metadata strcpy(event->app.active_screen, "Menu"); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 5b0b9c4521..d3916108da 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -8,6 +8,7 @@ #include #include #include +#include #define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/foo.crash" @@ -124,10 +125,31 @@ void generate_basic_report(bugsnag_event *event) { strcpy(event->user.id, "fex"); event->device.total_memory = 234678100; event->app.duration = 6502; - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.8); + event->metadata.value_count = 4; + event->metadata.values[0] = (bsg_metadata_value) { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = (bsg_metadata_value) { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = (bsg_metadata_value) { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = (bsg_metadata_value) { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.8, + }; event->crumb_count = 0; event->crumb_first_index = 0; @@ -311,11 +333,31 @@ bugsnag_report_v2 *bsg_generate_report_v2(void) { strcpy(event->user.id, "fex"); event->device.total_memory = 234678100; event->app.duration = 6502; - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.8); - + event->metadata.value_count = 4; + event->metadata.values[0] = (bsg_metadata_value) { + .name = {"weather"}, + .section = {"app"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"rain"}, + }; + event->metadata.values[1] = (bsg_metadata_value) { + .name = {"experimentX"}, + .section = {"metrics"}, + .type = BSG_METADATA_BOOL_VALUE, + .bool_value = false, + }; + event->metadata.values[2] = (bsg_metadata_value) { + .name = {"subject"}, + .section = {"metrics"}, + .type = BSG_METADATA_CHAR_VALUE, + .char_value = {"percy"}, + }; + event->metadata.values[3] = (bsg_metadata_value) { + .name = {"counter"}, + .section = {"metrics"}, + .type = BSG_METADATA_NUMBER_VALUE, + .double_value = 47.8, + }; event->handled_events = 1; event->unhandled_events = 1; @@ -478,7 +520,7 @@ TEST test_report_with_feature_flags_to_file(void) { TEST test_file_to_report(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = 5; + env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; env->report_header.big_endian = 1; strcpy(env->report_header.os_build, "macOS Sierra"); bugsnag_event *generated_report = bsg_generate_event(); @@ -574,8 +616,8 @@ TEST test_report_v2_migration(void) { ASSERT_STR_EQ("android", event->device.os_name); // package_name/version_name are migrated to metadata - ASSERT_STR_EQ("com.example.foo", event->metadata.values[0].char_value); - ASSERT_STR_EQ("2.5", event->metadata.values[1].char_value); + ASSERT_STR_EQ("com.example.foo", event->metadata.values[4].char_value); + ASSERT_STR_EQ("2.5", event->metadata.values[5].char_value); // breadcrumbs are migrated correctly ASSERT_EQ(2, event->crumb_count); From becc9727ccb4b1b3ef705dc95461582fe90c5121 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 5 Jan 2022 16:03:47 +0000 Subject: [PATCH 34/37] build: add req for minitest assertions [full ci] --- features/steps/android_steps.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/steps/android_steps.rb b/features/steps/android_steps.rb index 97e3549622..105e42eba4 100644 --- a/features/steps/android_steps.rb +++ b/features/steps/android_steps.rb @@ -1,3 +1,5 @@ +require 'minitest/spec' + # Waits 5s for an element to be present. If it isn't assume a system error dialog is # blocking its view and dismiss it before trying once more. # From df813de3e348ec7380c18aaa131d59a1c70a6745 Mon Sep 17 00:00:00 2001 From: Karl Stenerud Date: Wed, 5 Jan 2022 15:47:52 +0100 Subject: [PATCH 35/37] Improve JNI safety --- .../src/main/CMakeLists.txt | 1 + .../src/main/jni/bugsnag.c | 187 ++-- .../src/main/jni/bugsnag_ndk.c | 204 ++-- .../src/main/jni/event.c | 14 +- .../src/main/jni/jni_cache.c | 296 ++++++ .../src/main/jni/jni_cache.h | 71 ++ .../src/main/jni/metadata.c | 872 +++++++----------- .../src/main/jni/safejni.c | 62 +- 8 files changed, 922 insertions(+), 785 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index d95cb17e93..55b7712dfc 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -10,6 +10,7 @@ add_library( # Specifies the name of the library. jni/bugsnag.c jni/metadata.c jni/safejni.c + jni/jni_cache.c jni/event.c jni/handlers/signal_handler.c jni/handlers/cpp_handler.cpp diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c index e46f88abdd..a1e5393bdd 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c @@ -3,6 +3,7 @@ #include "../assets/include/bugsnag.h" #include "bugsnag_ndk.h" #include "event.h" +#include "jni_cache.h" #include "metadata.h" #include "safejni.h" #include "utils/stack_unwinder.h" @@ -16,15 +17,6 @@ static JNIEnv *bsg_global_jni_env = NULL; void bugsnag_start(JNIEnv *env) { bsg_global_jni_env = env; } -void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, - bugsnag_severity severity); - -void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, - const char *name); - -void bugsnag_leave_breadcrumb_env(JNIEnv *env, const char *message, - bugsnag_breadcrumb_type type); - void bugsnag_notify(const char *name, const char *message, bugsnag_severity severity) { if (bsg_global_jni_env != NULL) { @@ -53,8 +45,8 @@ void bugsnag_leave_breadcrumb(const char *message, } } -jfieldID bsg_parse_jseverity(JNIEnv *env, bugsnag_severity severity, - jclass severity_class) { +static jfieldID parse_jseverity(JNIEnv *env, bugsnag_severity severity, + jclass severity_class) { const char *severity_sig = "Lcom/bugsnag/android/Severity;"; if (severity == BSG_SEVERITY_ERR) { return bsg_safe_get_static_field_id(env, severity_class, "ERROR", @@ -68,10 +60,11 @@ jfieldID bsg_parse_jseverity(JNIEnv *env, bugsnag_severity severity, } } -void bsg_populate_notify_stacktrace(JNIEnv *env, bugsnag_stackframe *stacktrace, - ssize_t frame_count, jclass trace_class, - jmethodID trace_constructor, - jobjectArray trace) { +static void populate_notify_stacktrace(JNIEnv *env, + bugsnag_stackframe *stacktrace, + ssize_t frame_count, jclass trace_class, + jmethodID trace_constructor, + jobjectArray trace) { for (int i = 0; i < frame_count; i++) { bugsnag_stackframe frame = stacktrace[i]; @@ -120,75 +113,42 @@ void bsg_populate_notify_stacktrace(JNIEnv *env, bugsnag_stackframe *stacktrace, void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, bugsnag_severity severity) { - jclass interface_class = NULL; - jmethodID notify_method = NULL; - jclass trace_class = NULL; - jclass severity_class = NULL; - jmethodID trace_constructor = NULL; - jobjectArray trace = NULL; - jfieldID severity_field = NULL; + jobjectArray jtrace = NULL; jobject jseverity = NULL; jbyteArray jname = NULL; jbyteArray jmessage = NULL; + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + goto exit; + } + bugsnag_stackframe stacktrace[BUGSNAG_FRAMES_MAX]; memset(stacktrace, 0, sizeof(stacktrace)); ssize_t frame_count = bsg_unwind_stack(bsg_configured_unwind_style(), stacktrace, NULL, 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.notify() - notify_method = bsg_safe_get_static_method_id( - env, interface_class, "notify", - "([B[BLcom/bugsnag/android/Severity;[Ljava/lang/StackTraceElement;)V"); - if (notify_method == NULL) { - goto exit; - } - - // lookup java/lang/StackTraceElement - trace_class = bsg_safe_find_class(env, "java/lang/StackTraceElement"); - if (trace_class == NULL) { - goto exit; - } - - // lookup com/bugsnag/android/Severity - severity_class = bsg_safe_find_class(env, "com/bugsnag/android/Severity"); - if (severity_class == NULL) { - goto exit; - } - - // lookup StackTraceElement constructor - trace_constructor = bsg_safe_get_method_id( - env, trace_class, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); - if (trace_constructor == NULL) { - goto exit; - } - // create StackTraceElement array - trace = bsg_safe_new_object_array(env, frame_count, trace_class); - if (trace == NULL) { + jtrace = bsg_safe_new_object_array(env, frame_count, + bsg_global_jni_cache->stack_trace_element); + if (jtrace == NULL) { goto exit; } // populate stacktrace object - bsg_populate_notify_stacktrace(env, stacktrace, frame_count, trace_class, - trace_constructor, trace); + populate_notify_stacktrace(env, stacktrace, frame_count, + bsg_global_jni_cache->stack_trace_element, + bsg_global_jni_cache->ste_constructor, jtrace); // get the severity field - severity_field = bsg_parse_jseverity(env, severity, severity_class); + jfieldID severity_field = + parse_jseverity(env, severity, bsg_global_jni_cache->severity); if (severity_field == NULL) { goto exit; } // get the error severity object - jseverity = - bsg_safe_get_static_object_field(env, severity_class, severity_field); + jseverity = bsg_safe_get_static_object_field( + env, bsg_global_jni_cache->severity, severity_field); if (jseverity == NULL) { goto exit; } @@ -196,70 +156,47 @@ 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); - bsg_safe_call_static_void_method(env, interface_class, notify_method, jname, - jmessage, jseverity, trace); + bsg_safe_call_static_void_method(env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_notify, jname, + jmessage, jseverity, jtrace); goto exit; exit: - if (jname != NULL) { - bsg_safe_release_byte_array_elements(env, jname, (jbyte *)name); - } - if (jmessage != NULL) { - bsg_safe_release_byte_array_elements(env, jmessage, (jbyte *)message); - } + bsg_safe_release_byte_array_elements(env, jname, (jbyte *)name); bsg_safe_delete_local_ref(env, jname); + bsg_safe_release_byte_array_elements(env, jmessage, (jbyte *)message); bsg_safe_delete_local_ref(env, jmessage); - - bsg_safe_delete_local_ref(env, interface_class); - bsg_safe_delete_local_ref(env, trace_class); - bsg_safe_delete_local_ref(env, severity_class); - bsg_safe_delete_local_ref(env, trace); + bsg_safe_delete_local_ref(env, jtrace); bsg_safe_delete_local_ref(env, jseverity); } void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, const char *name) { - // lookup com/bugsnag/android/NativeInterface - jclass interface_class = NULL; - jmethodID set_user_method = NULL; - - interface_class = - bsg_safe_find_class(env, "com/bugsnag/android/NativeInterface"); - if (interface_class == NULL) { - goto exit; - } - - // lookup NativeInterface.setUser() - set_user_method = bsg_safe_get_static_method_id(env, interface_class, - "setUser", "([B[B[B)V"); - if (set_user_method == NULL) { - goto exit; + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + return; } jbyteArray jid = bsg_byte_ary_from_string(env, id); jbyteArray jemail = bsg_byte_ary_from_string(env, email); jbyteArray jname = bsg_byte_ary_from_string(env, name); - bsg_safe_call_static_void_method(env, interface_class, set_user_method, jid, + bsg_safe_call_static_void_method(env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_set_user, jid, jemail, jname); bsg_safe_release_byte_array_elements(env, jid, (jbyte *)id); - bsg_safe_release_byte_array_elements(env, jemail, (jbyte *)email); - bsg_safe_release_byte_array_elements(env, jname, (jbyte *)name); - bsg_safe_delete_local_ref(env, jid); + bsg_safe_release_byte_array_elements(env, jemail, (jbyte *)email); bsg_safe_delete_local_ref(env, jemail); + bsg_safe_release_byte_array_elements(env, jname, (jbyte *)name); bsg_safe_delete_local_ref(env, jname); - - goto exit; - -exit: - bsg_safe_delete_local_ref(env, interface_class); } -jfieldID bsg_parse_jcrumb_type(JNIEnv *env, const bugsnag_breadcrumb_type type, - jclass type_class) { +static jfieldID parse_jcrumb_type(JNIEnv *env, + const bugsnag_breadcrumb_type type, + jclass type_class) { const char *type_sig = "Lcom/bugsnag/android/BreadcrumbType;"; if (type == BSG_CRUMB_USER) { return bsg_safe_get_static_field_id(env, type_class, "USER", type_sig); @@ -283,56 +220,36 @@ jfieldID bsg_parse_jcrumb_type(JNIEnv *env, const bugsnag_breadcrumb_type type, void bugsnag_leave_breadcrumb_env(JNIEnv *env, const char *message, const bugsnag_breadcrumb_type type) { - jclass interface_class = NULL; - jmethodID leave_breadcrumb_method = NULL; - jclass type_class = NULL; - jfieldID crumb_type = NULL; - jobject jtype = NULL; jbyteArray jmessage = NULL; + jobject jtype = 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.leaveBreadcrumb() - leave_breadcrumb_method = bsg_safe_get_static_method_id( - env, interface_class, "leaveBreadcrumb", - "([BLcom/bugsnag/android/BreadcrumbType;)V"); - if (leave_breadcrumb_method == NULL) { - goto exit; - } - - // lookup com/bugsnag/android/BreadcrumbType - type_class = bsg_safe_find_class(env, "com/bugsnag/android/BreadcrumbType"); - if (type_class == NULL) { + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); goto exit; } // get breadcrumb type fieldID - crumb_type = bsg_parse_jcrumb_type(env, type, type_class); + jfieldID crumb_type = + parse_jcrumb_type(env, type, bsg_global_jni_cache->breadcrumb_type); if (crumb_type == NULL) { goto exit; } // get the breadcrumb type - jtype = bsg_safe_get_static_object_field(env, type_class, crumb_type); + jtype = bsg_safe_get_static_object_field( + env, bsg_global_jni_cache->breadcrumb_type, crumb_type); if (jtype == NULL) { goto exit; } jmessage = bsg_byte_ary_from_string(env, message); - bsg_safe_call_static_void_method(env, interface_class, - leave_breadcrumb_method, jmessage, jtype); + bsg_safe_call_static_void_method(env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_leave_breadcrumb, + jmessage, jtype); goto exit; -exit : { +exit: bsg_safe_release_byte_array_elements(env, jmessage, (jbyte *)message); -} - bsg_safe_delete_local_ref(env, interface_class); - bsg_safe_delete_local_ref(env, type_class); - bsg_safe_delete_local_ref(env, jtype); bsg_safe_delete_local_ref(env, jmessage); + bsg_safe_delete_local_ref(env, jtype); } 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 857df55d93..252a7e979f 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -9,6 +9,7 @@ #include "event.h" #include "handlers/cpp_handler.h" #include "handlers/signal_handler.h" +#include "jni_cache.h" #include "metadata.h" #include "safejni.h" #include "utils/serializer.h" @@ -25,14 +26,14 @@ static pthread_mutex_t bsg_global_env_write_mutex = PTHREAD_MUTEX_INITIALIZER; * All functions which will edit the environment (unless they are handling a * crash) must first request the lock */ -void bsg_request_env_write_lock(void) { +static void request_env_write_lock(void) { pthread_mutex_lock(&bsg_global_env_write_mutex); } /** * Once editing is complete, the lock must be released */ -void bsg_release_env_write_lock(void) { +static void release_env_write_lock(void) { pthread_mutex_unlock(&bsg_global_env_write_mutex); } @@ -147,6 +148,11 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( jstring _last_run_info_path, jint consecutive_launch_crashes, jboolean auto_detect_ndk_crashes, jint _api_level, jboolean is32bit, jint send_threads) { + + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + } + bsg_environment *bugsnag_env = calloc(1, sizeof(bsg_environment)); bsg_set_unwind_types((int)_api_level, (bool)is32bit, &bugsnag_env->signal_unwind_style, @@ -212,11 +218,22 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath( JNIEnv *env, jobject _this, jstring _report_path) { static pthread_mutex_t bsg_native_delivery_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&bsg_native_delivery_mutex); + const char *event_path = NULL; bugsnag_event *event = NULL; jbyteArray jpayload = NULL; jbyteArray jstage = NULL; char *payload = NULL; + jstring japi_key = NULL; + + if (bsg_global_jni_cache == NULL) { + goto exit; + } + + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + goto exit; + } event_path = bsg_safe_get_string_utf_chars(env, _report_path); if (event_path == NULL) { @@ -228,67 +245,50 @@ Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath( // in delivery. remove(event_path); - if (event != NULL) { - payload = bsg_serialize_event_to_json_string(event); - if (payload != NULL) { - - // lookup com/bugsnag/android/NativeInterface - jclass interface_class = - bsg_safe_find_class(env, "com/bugsnag/android/NativeInterface"); - if (interface_class == NULL) { - goto exit; - } - - // lookup NativeInterface.deliverReport() - jmethodID jdeliver_method = bsg_safe_get_static_method_id( - env, interface_class, "deliverReport", "([B[BLjava/lang/String;Z)V"); - if (jdeliver_method == NULL) { - goto exit; - } - - // generate payload bytearray - jpayload = bsg_byte_ary_from_string(env, payload); - if (jpayload == NULL) { - goto exit; - } - - // generate releaseStage bytearray - jstage = bsg_byte_ary_from_string(env, event->app.release_stage); - if (jstage == NULL) { - goto exit; - } - - // call NativeInterface.deliverReport() - jstring japi_key = bsg_safe_new_string_utf(env, event->api_key); - if (japi_key != NULL) { - bool is_launching = event->app.is_launching; - bsg_safe_call_static_void_method(env, interface_class, jdeliver_method, - jstage, jpayload, japi_key, - is_launching); - } - bsg_safe_delete_local_ref(env, japi_key); - } else { - BUGSNAG_LOG("Failed to serialize event as JSON: %s", event_path); - } - } else { + if (event == NULL) { BUGSNAG_LOG("Failed to read event at file: %s", event_path); + goto exit; + } + + payload = bsg_serialize_event_to_json_string(event); + if (payload == NULL) { + BUGSNAG_LOG("Failed to serialize event as JSON: %s", event_path); + goto exit; + } + + // generate payload bytearray + jpayload = bsg_byte_ary_from_string(env, payload); + if (jpayload == NULL) { + goto exit; + } + + // generate releaseStage bytearray + jstage = bsg_byte_ary_from_string(env, event->app.release_stage); + if (jstage == NULL) { + goto exit; + } + + // call NativeInterface.deliverReport() + japi_key = bsg_safe_new_string_utf(env, event->api_key); + if (japi_key != NULL) { + bool is_launching = event->app.is_launching; + bsg_safe_call_static_void_method(env, + bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_deliver_report, + jstage, jpayload, japi_key, is_launching); } - goto exit; exit: - pthread_mutex_unlock(&bsg_native_delivery_mutex); + bsg_safe_release_string_utf_chars(env, _report_path, event_path); if (event != NULL) { bsg_safe_release_byte_array_elements(env, jstage, (jbyte *)event->app.release_stage); free(event); } - if (payload != NULL) { - bsg_safe_release_byte_array_elements(env, jpayload, (jbyte *)payload); - free(payload); - } - bsg_safe_delete_local_ref(env, jpayload); - bsg_safe_delete_local_ref(env, jstage); - bsg_safe_release_string_utf_chars(env, _report_path, event_path); + bsg_safe_release_byte_array_elements(env, jpayload, (jbyte *)payload); + free(payload); + + pthread_mutex_unlock(&bsg_native_delivery_mutex); } JNIEXPORT void JNICALL @@ -297,13 +297,13 @@ Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, if (bsg_global_env == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; if (bugsnag_event_has_session(event)) { event->handled_events++; } - bsg_release_env_write_lock(); + release_env_write_lock(); } JNIEXPORT void JNICALL @@ -312,13 +312,13 @@ Java_com_bugsnag_android_ndk_NativeBridge_addUnhandledEvent(JNIEnv *env, if (bsg_global_env == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; if (bugsnag_event_has_session(event)) { event->unhandled_events++; } - bsg_release_env_write_lock(); + release_env_write_lock(); } JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_startedSession( @@ -330,10 +330,10 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_startedSession( char *session_id = (char *)bsg_safe_get_string_utf_chars(env, session_id_); char *started_at = (char *)bsg_safe_get_string_utf_chars(env, start_date_); if (session_id != NULL && started_at != NULL) { - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_start_session(&bsg_global_env->next_event, session_id, started_at, handled_count, unhandled_count); - bsg_release_env_write_lock(); + release_env_write_lock(); } bsg_safe_release_string_utf_chars(env, session_id_, session_id); bsg_safe_release_string_utf_chars(env, start_date_, started_at); @@ -344,13 +344,13 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_pausedSession( if (bsg_global_env == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; memset(event->session_id, 0, bsg_strlen(event->session_id)); memset(event->session_start, 0, bsg_strlen(event->session_start)); event->handled_events = 0; event->unhandled_events = 0; - bsg_release_env_write_lock(); + release_env_write_lock(); } JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addBreadcrumb( @@ -359,6 +359,10 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addBreadcrumb( if (bsg_global_env == NULL) { return; } + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + return; + } const char *name = bsg_safe_get_string_utf_chars(env, name_); const char *type = bsg_safe_get_string_utf_chars(env, crumb_type); const char *timestamp = bsg_safe_get_string_utf_chars(env, timestamp_); @@ -386,9 +390,9 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addBreadcrumb( } bsg_populate_crumb_metadata(env, crumb, metadata); - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_add_breadcrumb(&bsg_global_env->next_event, crumb); - bsg_release_env_write_lock(); + release_env_write_lock(); free(crumb); } @@ -408,9 +412,9 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateAppVersion(JNIEnv *env, if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_app_set_version(&bsg_global_env->next_event, value); - bsg_release_env_write_lock(); + release_env_write_lock(); bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -425,9 +429,9 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateBuildUUID(JNIEnv *env, if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_app_set_build_uuid(&bsg_global_env->next_event, value); - bsg_release_env_write_lock(); + release_env_write_lock(); bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -440,9 +444,9 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_updateContext( if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_set_context(&bsg_global_env->next_event, value); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -455,7 +459,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateInForeground( return; } char *activity = (char *)bsg_safe_get_string_utf_chars(env, activity_); - bsg_request_env_write_lock(); + request_env_write_lock(); bool was_in_foreground = bsg_global_env->next_event.app.in_foreground; bsg_global_env->next_event.app.in_foreground = (bool)new_value; bsg_strncpy(bsg_global_env->next_event.app.active_screen, activity, @@ -468,7 +472,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateInForeground( bsg_global_env->foreground_start_time = 0; bsg_global_env->next_event.app.duration_in_foreground_ms_offset = 0; } - bsg_release_env_write_lock(); + release_env_write_lock(); if (activity_ != NULL) { bsg_safe_release_string_utf_chars(env, activity_, activity); } @@ -480,10 +484,10 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateIsLaunching( if (bsg_global_env == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_app_set_is_launching(&bsg_global_env->next_event, new_value); bsg_update_next_run_info(bsg_global_env); - bsg_release_env_write_lock(); + release_env_write_lock(); } JNIEXPORT void JNICALL @@ -501,12 +505,12 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateLowMemory( return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_add_metadata_bool(&bsg_global_env->next_event, "app", "lowMemory", (bool)low_memory); bugsnag_event_add_metadata_string(&bsg_global_env->next_event, "app", "memoryTrimLevel", memory_trim_level); - bsg_release_env_write_lock(); + release_env_write_lock(); if (memory_trim_level_description != NULL) { bsg_safe_release_string_utf_chars(env, memory_trim_level_description, memory_trim_level); @@ -525,9 +529,9 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateOrientation(JNIEnv *env, if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_device_set_orientation(&bsg_global_env->next_event, value); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -543,9 +547,9 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateReleaseStage( if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_app_set_release_stage(&bsg_global_env->next_event, value); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -560,11 +564,11 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_updateUserId( if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; bugsnag_user user = bugsnag_event_get_user(event); bugsnag_event_set_user(event, value, user.email, user.name); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -579,11 +583,11 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_updateUserName( if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; bugsnag_user user = bugsnag_event_get_user(event); bugsnag_event_set_user(event, user.id, user.email, value); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -600,11 +604,11 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateUserEmail(JNIEnv *env, if (value == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event *event = &bsg_global_env->next_event; bugsnag_user user = bugsnag_event_get_user(event); bugsnag_event_set_user(event, user.id, value, user.name); - bsg_release_env_write_lock(); + release_env_write_lock(); if (new_value != NULL) { bsg_safe_release_string_utf_chars(env, new_value, value); } @@ -621,10 +625,10 @@ Java_com_bugsnag_android_ndk_NativeBridge_addMetadataString( char *value = (char *)bsg_safe_get_string_utf_chars(env, value_); if (tab != NULL && key != NULL && value != NULL) { - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_add_metadata_string(&bsg_global_env->next_event, tab, key, value); - bsg_release_env_write_lock(); + release_env_write_lock(); } bsg_safe_release_string_utf_chars(env, tab_, tab); bsg_safe_release_string_utf_chars(env, key_, key); @@ -640,11 +644,11 @@ Java_com_bugsnag_android_ndk_NativeBridge_addMetadataDouble( char *tab = (char *)bsg_safe_get_string_utf_chars(env, tab_); char *key = (char *)bsg_safe_get_string_utf_chars(env, key_); if (tab != NULL && key != NULL) { - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_add_metadata_double(&bsg_global_env->next_event, tab, key, (double)value_); } - bsg_release_env_write_lock(); + release_env_write_lock(); bsg_safe_release_string_utf_chars(env, tab_, tab); bsg_safe_release_string_utf_chars(env, key_, key); } @@ -658,10 +662,10 @@ Java_com_bugsnag_android_ndk_NativeBridge_addMetadataBoolean( char *tab = (char *)bsg_safe_get_string_utf_chars(env, tab_); char *key = (char *)bsg_safe_get_string_utf_chars(env, key_); if (tab != NULL && key != NULL) { - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_add_metadata_bool(&bsg_global_env->next_event, tab, key, (bool)value_); - bsg_release_env_write_lock(); + release_env_write_lock(); } bsg_safe_release_string_utf_chars(env, tab_, tab); bsg_safe_release_string_utf_chars(env, key_, key); @@ -678,9 +682,9 @@ Java_com_bugsnag_android_ndk_NativeBridge_clearMetadataTab(JNIEnv *env, if (tab == NULL) { return; } - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_clear_metadata_section(&bsg_global_env->next_event, tab); - bsg_release_env_write_lock(); + release_env_write_lock(); bsg_safe_release_string_utf_chars(env, tab_, tab); } @@ -693,9 +697,9 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_removeMetadata( char *key = (char *)bsg_safe_get_string_utf_chars(env, key_); if (tab != NULL && key != NULL) { - bsg_request_env_write_lock(); + request_env_write_lock(); bugsnag_event_clear_metadata(&bsg_global_env->next_event, tab, key); - bsg_release_env_write_lock(); + release_env_write_lock(); } bsg_safe_release_string_utf_chars(env, tab_, tab); @@ -707,9 +711,13 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_updateMetadata( if (bsg_global_env == NULL) { return; } - bsg_request_env_write_lock(); + if (!bsg_jni_cache_refresh(env)) { + BUGSNAG_LOG("Could not refresh JNI cache."); + return; + } + request_env_write_lock(); bsg_populate_metadata(env, &bsg_global_env->next_event.metadata, metadata); - bsg_release_env_write_lock(); + release_env_write_lock(); } // Unwind the stack using the configured unwind style for signal handlers. diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.c b/bugsnag-plugin-android-ndk/src/main/jni/event.c index 7864e245d1..a473e353b3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.c @@ -2,7 +2,7 @@ #include "utils/string.h" #include -int bsg_find_next_free_metadata_index(bugsnag_metadata *const metadata) { +static int find_next_free_metadata_index(bugsnag_metadata *const metadata) { if (metadata->value_count < BUGSNAG_METADATA_MAX) { return metadata->value_count; } else { @@ -15,9 +15,9 @@ int bsg_find_next_free_metadata_index(bugsnag_metadata *const metadata) { return -1; } -int bsg_allocate_metadata_index(bugsnag_metadata *metadata, const char *section, - const char *name) { - int index = bsg_find_next_free_metadata_index(metadata); +static int allocate_metadata_index(bugsnag_metadata *metadata, + const char *section, const char *name) { + int index = find_next_free_metadata_index(metadata); if (index < 0) { return index; } @@ -34,7 +34,7 @@ int bsg_allocate_metadata_index(bugsnag_metadata *metadata, const char *section, void bsg_add_metadata_value_double(bugsnag_metadata *metadata, const char *section, const char *name, double value) { - int index = bsg_allocate_metadata_index(metadata, section, name); + int index = allocate_metadata_index(metadata, section, name); if (index >= 0) { metadata->values[index].type = BSG_METADATA_NUMBER_VALUE; metadata->values[index].double_value = value; @@ -43,7 +43,7 @@ void bsg_add_metadata_value_double(bugsnag_metadata *metadata, void bsg_add_metadata_value_str(bugsnag_metadata *metadata, const char *section, const char *name, const char *value) { - int index = bsg_allocate_metadata_index(metadata, section, name); + int index = allocate_metadata_index(metadata, section, name); if (index >= 0) { metadata->values[index].type = BSG_METADATA_CHAR_VALUE; bsg_strncpy(metadata->values[index].char_value, value, @@ -54,7 +54,7 @@ void bsg_add_metadata_value_str(bugsnag_metadata *metadata, const char *section, void bsg_add_metadata_value_bool(bugsnag_metadata *metadata, const char *section, const char *name, bool value) { - int index = bsg_allocate_metadata_index(metadata, section, name); + int index = allocate_metadata_index(metadata, section, name); if (index >= 0) { metadata->values[index].type = BSG_METADATA_BOOL_VALUE; metadata->values[index].bool_value = value; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c new file mode 100644 index 0000000000..921643ef66 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.c @@ -0,0 +1,296 @@ +// +// Created by Karl Stenerud on 03.01.22. +// + +#include "jni_cache.h" +#include "safejni.h" +#include + +#include +#ifndef BUGSNAG_LOG +#define BUGSNAG_LOG(fmt, ...) \ + __android_log_print(ANDROID_LOG_WARN, "BugsnagNDK", fmt, ##__VA_ARGS__) +#endif + +#define report_contents(VALUE, TYPECODE) \ + BUGSNAG_LOG(#VALUE " == " TYPECODE, VALUE) + +static bsg_jni_cache global_jni_cache; +bsg_jni_cache *bsg_global_jni_cache = &global_jni_cache; + +bool bsg_jni_cache_refresh(JNIEnv *env) { + if (bsg_global_jni_cache == NULL) { + return false; + } + + // All objects here are local references, and MUST be refreshed on every + // transition from Java to native. + + // Classes + + bsg_global_jni_cache->integer = bsg_safe_find_class(env, "java/lang/Integer"); + if (bsg_global_jni_cache->integer == NULL) { + report_contents(bsg_global_jni_cache->integer, "%p"); + goto failed; + } + + bsg_global_jni_cache->boolean = bsg_safe_find_class(env, "java/lang/Boolean"); + if (bsg_global_jni_cache->boolean == NULL) { + report_contents(bsg_global_jni_cache->boolean, "%p"); + goto failed; + } + + bsg_global_jni_cache->long_class = bsg_safe_find_class(env, "java/lang/Long"); + if (bsg_global_jni_cache->long_class == NULL) { + report_contents(bsg_global_jni_cache->long_class, "%p"); + goto failed; + } + + bsg_global_jni_cache->float_class = + bsg_safe_find_class(env, "java/lang/Float"); + if (bsg_global_jni_cache->float_class == NULL) { + report_contents(bsg_global_jni_cache->float_class, "%p"); + goto failed; + } + + bsg_global_jni_cache->number = bsg_safe_find_class(env, "java/lang/Number"); + if (bsg_global_jni_cache->number == NULL) { + report_contents(bsg_global_jni_cache->number, "%p"); + goto failed; + } + + bsg_global_jni_cache->string = bsg_safe_find_class(env, "java/lang/String"); + if (bsg_global_jni_cache->string == NULL) { + report_contents(bsg_global_jni_cache->string, "%p"); + goto failed; + } + + // Methods + + bsg_global_jni_cache->arraylist = + bsg_safe_find_class(env, "java/util/ArrayList"); + if (bsg_global_jni_cache->arraylist == NULL) { + report_contents(bsg_global_jni_cache->arraylist, "%p"); + goto failed; + } + + bsg_global_jni_cache->hash_map = + bsg_safe_find_class(env, "java/util/HashMap"); + if (bsg_global_jni_cache->hash_map == NULL) { + report_contents(bsg_global_jni_cache->hash_map, "%p"); + goto failed; + } + + bsg_global_jni_cache->map = bsg_safe_find_class(env, "java/util/Map"); + if (bsg_global_jni_cache->map == NULL) { + report_contents(bsg_global_jni_cache->map, "%p"); + goto failed; + } + + bsg_global_jni_cache->native_interface = + bsg_safe_find_class(env, "com/bugsnag/android/NativeInterface"); + if (bsg_global_jni_cache->native_interface == NULL) { + report_contents(bsg_global_jni_cache->native_interface, "%p"); + goto failed; + } + + bsg_global_jni_cache->stack_trace_element = + bsg_safe_find_class(env, "java/lang/StackTraceElement"); + if (bsg_global_jni_cache->stack_trace_element == NULL) { + report_contents(bsg_global_jni_cache->stack_trace_element, "%p"); + goto failed; + } + + bsg_global_jni_cache->severity = + bsg_safe_find_class(env, "com/bugsnag/android/Severity"); + if (bsg_global_jni_cache->severity == NULL) { + report_contents(bsg_global_jni_cache->severity, "%p"); + goto failed; + } + + bsg_global_jni_cache->breadcrumb_type = + bsg_safe_find_class(env, "com/bugsnag/android/BreadcrumbType"); + if (bsg_global_jni_cache->breadcrumb_type == NULL) { + report_contents(bsg_global_jni_cache->breadcrumb_type, "%p"); + goto failed; + } + + bsg_global_jni_cache->integer_int_value = bsg_safe_get_method_id( + env, bsg_global_jni_cache->integer, "intValue", "()I"); + if (bsg_global_jni_cache->integer_int_value == NULL) { + report_contents(bsg_global_jni_cache->integer_int_value, "%p"); + goto failed; + } + + bsg_global_jni_cache->float_float_value = bsg_safe_get_method_id( + env, bsg_global_jni_cache->float_class, "floatValue", "()F"); + if (bsg_global_jni_cache->float_float_value == NULL) { + report_contents(bsg_global_jni_cache->float_float_value, "%p"); + goto failed; + } + + bsg_global_jni_cache->number_double_value = bsg_safe_get_method_id( + env, bsg_global_jni_cache->number, "doubleValue", "()D"); + if (bsg_global_jni_cache->number_double_value == NULL) { + report_contents(bsg_global_jni_cache->number_double_value, "%p"); + goto failed; + } + + bsg_global_jni_cache->long_long_value = bsg_safe_get_method_id( + env, bsg_global_jni_cache->integer, "longValue", "()J"); + if (bsg_global_jni_cache->long_long_value == NULL) { + report_contents(bsg_global_jni_cache->long_long_value, "%p"); + goto failed; + } + + bsg_global_jni_cache->boolean_bool_value = bsg_safe_get_method_id( + env, bsg_global_jni_cache->boolean, "booleanValue", "()Z"); + if (bsg_global_jni_cache->boolean_bool_value == NULL) { + report_contents(bsg_global_jni_cache->boolean_bool_value, "%p"); + goto failed; + } + + bsg_global_jni_cache->arraylist_init_with_obj = + bsg_safe_get_method_id(env, bsg_global_jni_cache->arraylist, "", + "(Ljava/util/Collection;)V"); + if (bsg_global_jni_cache->arraylist_init_with_obj == NULL) { + report_contents(bsg_global_jni_cache->arraylist_init_with_obj, "%p"); + goto failed; + } + + bsg_global_jni_cache->arraylist_get = bsg_safe_get_method_id( + env, bsg_global_jni_cache->arraylist, "get", "(I)Ljava/lang/Object;"); + if (bsg_global_jni_cache->arraylist_get == NULL) { + report_contents(bsg_global_jni_cache->arraylist_get, "%p"); + goto failed; + } + + bsg_global_jni_cache->hash_map_key_set = bsg_safe_get_method_id( + env, bsg_global_jni_cache->hash_map, "keySet", "()Ljava/util/Set;"); + if (bsg_global_jni_cache->hash_map_key_set == NULL) { + report_contents(bsg_global_jni_cache->hash_map_key_set, "%p"); + goto failed; + } + + bsg_global_jni_cache->hash_map_size = bsg_safe_get_method_id( + env, bsg_global_jni_cache->hash_map, "size", "()I"); + if (bsg_global_jni_cache->hash_map_size == NULL) { + report_contents(bsg_global_jni_cache->hash_map_size, "%p"); + goto failed; + } + + bsg_global_jni_cache->hash_map_get = + bsg_safe_get_method_id(env, bsg_global_jni_cache->hash_map, "get", + "(Ljava/lang/Object;)Ljava/lang/Object;"); + if (bsg_global_jni_cache->hash_map_get == NULL) { + report_contents(bsg_global_jni_cache->hash_map_get, "%p"); + goto failed; + } + + bsg_global_jni_cache->map_key_set = bsg_safe_get_method_id( + env, bsg_global_jni_cache->map, "keySet", "()Ljava/util/Set;"); + if (bsg_global_jni_cache->map_key_set == NULL) { + report_contents(bsg_global_jni_cache->map_key_set, "%p"); + goto failed; + } + + bsg_global_jni_cache->map_size = + bsg_safe_get_method_id(env, bsg_global_jni_cache->map, "size", "()I"); + if (bsg_global_jni_cache->map_size == NULL) { + report_contents(bsg_global_jni_cache->map_size, "%p"); + goto failed; + } + + bsg_global_jni_cache->map_get = + bsg_safe_get_method_id(env, bsg_global_jni_cache->map, "get", + "(Ljava/lang/Object;)Ljava/lang/Object;"); + if (bsg_global_jni_cache->map_get == NULL) { + report_contents(bsg_global_jni_cache->map_get, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_get_app = + bsg_safe_get_static_method_id(env, bsg_global_jni_cache->native_interface, + "getApp", "()Ljava/util/Map;"); + if (bsg_global_jni_cache->ni_get_app == NULL) { + report_contents(bsg_global_jni_cache->ni_get_app, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_get_device = + bsg_safe_get_static_method_id(env, bsg_global_jni_cache->native_interface, + "getDevice", "()Ljava/util/Map;"); + if (bsg_global_jni_cache->ni_get_device == NULL) { + report_contents(bsg_global_jni_cache->ni_get_device, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_get_user = + bsg_safe_get_static_method_id(env, bsg_global_jni_cache->native_interface, + "getUser", "()Ljava/util/Map;"); + if (bsg_global_jni_cache->ni_get_user == NULL) { + report_contents(bsg_global_jni_cache->ni_get_user, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_set_user = bsg_safe_get_static_method_id( + env, bsg_global_jni_cache->native_interface, "setUser", "([B[B[B)V"); + if (bsg_global_jni_cache->ni_set_user == NULL) { + report_contents(bsg_global_jni_cache->ni_set_user, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_get_metadata = + bsg_safe_get_static_method_id(env, bsg_global_jni_cache->native_interface, + "getMetadata", "()Ljava/util/Map;"); + if (bsg_global_jni_cache->ni_get_metadata == NULL) { + report_contents(bsg_global_jni_cache->ni_get_metadata, "%p"); + goto failed; + } + + // lookup NativeInterface.getContext() + bsg_global_jni_cache->ni_get_context = + bsg_safe_get_static_method_id(env, bsg_global_jni_cache->native_interface, + "getContext", "()Ljava/lang/String;"); + if (bsg_global_jni_cache->ni_get_context == NULL) { + report_contents(bsg_global_jni_cache->ni_get_context, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_notify = bsg_safe_get_static_method_id( + env, bsg_global_jni_cache->native_interface, "notify", + "([B[BLcom/bugsnag/android/Severity;[Ljava/lang/StackTraceElement;)V"); + if (bsg_global_jni_cache->ni_notify == NULL) { + report_contents(bsg_global_jni_cache->ni_notify, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_deliver_report = bsg_safe_get_static_method_id( + env, bsg_global_jni_cache->native_interface, "deliverReport", + "([B[BLjava/lang/String;Z)V"); + if (bsg_global_jni_cache->ni_deliver_report == NULL) { + report_contents(bsg_global_jni_cache->ni_deliver_report, "%p"); + goto failed; + } + + bsg_global_jni_cache->ni_leave_breadcrumb = bsg_safe_get_static_method_id( + env, bsg_global_jni_cache->native_interface, "leaveBreadcrumb", + "([BLcom/bugsnag/android/BreadcrumbType;)V"); + if (bsg_global_jni_cache->ni_leave_breadcrumb == NULL) { + report_contents(bsg_global_jni_cache->ni_leave_breadcrumb, "%p"); + goto failed; + } + + bsg_global_jni_cache->ste_constructor = bsg_safe_get_method_id( + env, bsg_global_jni_cache->stack_trace_element, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); + if (bsg_global_jni_cache->ste_constructor == NULL) { + report_contents(bsg_global_jni_cache->ste_constructor, "%p"); + goto failed; + } + + return true; + +failed: + return false; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h new file mode 100644 index 0000000000..72747e1319 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/jni_cache.h @@ -0,0 +1,71 @@ +// +// Created by Karl Stenerud on 03.01.22. +// + +#ifndef BUGSNAG_ANDROID_JNI_CACHE_H +#define BUGSNAG_ANDROID_JNI_CACHE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + jclass hash_map; + jclass map; + jclass arraylist; + jclass integer; + jclass boolean; + jclass metadata; + jclass native_interface; + jclass long_class; + jclass float_class; + jclass number; + jclass string; + jclass stack_trace_element; + jclass severity; + jclass breadcrumb_type; + jmethodID integer_int_value; + jmethodID long_long_value; + jmethodID float_float_value; + jmethodID boolean_bool_value; + jmethodID number_double_value; + jmethodID hash_map_get; + jmethodID hash_map_size; + jmethodID hash_map_key_set; + jmethodID map_get; + jmethodID map_size; + jmethodID map_key_set; + jmethodID arraylist_init_with_obj; + jmethodID arraylist_get; + jmethodID ni_get_app; + jmethodID ni_get_device; + jmethodID ni_get_user; + jmethodID ni_set_user; + jmethodID ni_get_metadata; + jmethodID ni_get_context; + jmethodID ni_notify; + jmethodID ni_leave_breadcrumb; + jmethodID ni_deliver_report; + jmethodID ste_constructor; +} bsg_jni_cache; + +// Always check for null before using this! +extern bsg_jni_cache *bsg_global_jni_cache; + +/** + * Refresh all references in the JNI cache. + * This MUST be called on every Java-to-native call! + * + * @param env The JNI env + * @return false if an error occurs, in which case the cache is unusable. + */ +bool bsg_jni_cache_refresh(JNIEnv *env); + +#ifdef __cplusplus +} +#endif + +#endif // BUGSNAG_ANDROID_JNI_CACHE_H diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index bf16caec4b..de418229c2 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -1,451 +1,216 @@ #include "metadata.h" +#include "jni_cache.h" #include "safejni.h" #include "utils/string.h" #include #include -typedef struct { - jclass hash_map; - jclass map; - jclass arraylist; - jclass integer; - jclass boolean; - jclass metadata; - jclass native_interface; - jclass long_class; - jclass float_class; - jclass number; - jclass string; - jmethodID integer_int_value; - jmethodID long_long_value; - jmethodID float_float_value; - jmethodID boolean_bool_value; - jmethodID number_double_value; - jmethodID hash_map_get; - jmethodID hash_map_size; - jmethodID hash_map_key_set; - jmethodID map_get; - jmethodID map_size; - jmethodID map_key_set; - jmethodID arraylist_init_with_obj; - jmethodID arraylist_get; - jmethodID get_app_data; - jmethodID get_device_data; - jmethodID get_user_data; - jmethodID get_breadcrumbs; - jmethodID get_metadata; - jmethodID get_context; -} bsg_jni_cache; - -void bsg_populate_metadata_value(JNIEnv *env, bugsnag_metadata *dst, - bsg_jni_cache *jni_cache, const char *section, - const char *name, jobject _value); - -/** - * Creates a cache of JNI methods/classes that are commonly used. - * - * Class and method objects can be kept safely since they aren't moved or - * removed from the JVM - care should be taken not to load objects as local - * references here. - */ -bsg_jni_cache *bsg_populate_jni_cache(JNIEnv *env) { - bsg_jni_cache *jni_cache = calloc(1, sizeof(bsg_jni_cache)); - - // lookup java/lang/Integer - jni_cache->integer = bsg_safe_find_class(env, "java/lang/Integer"); - if (jni_cache->integer == NULL) { - return NULL; - } - - // lookup java/lang/Boolean - jni_cache->boolean = bsg_safe_find_class(env, "java/lang/Boolean"); - if (jni_cache->boolean == NULL) { - return NULL; - } - - // lookup java/lang/Long - jni_cache->long_class = bsg_safe_find_class(env, "java/lang/Long"); - if (jni_cache->long_class == NULL) { - return NULL; - } - - // lookup java/lang/Float - jni_cache->float_class = bsg_safe_find_class(env, "java/lang/Float"); - if (jni_cache->float_class == NULL) { - return NULL; - } - - // lookup java/lang/Number - jni_cache->number = bsg_safe_find_class(env, "java/lang/Number"); - if (jni_cache->number == NULL) { - return NULL; - } - - // lookup java/lang/String - jni_cache->string = bsg_safe_find_class(env, "java/lang/String"); - if (jni_cache->string == NULL) { - return NULL; - } - - // lookup Integer.intValue() - jni_cache->integer_int_value = - bsg_safe_get_method_id(env, jni_cache->integer, "intValue", "()I"); - if (jni_cache->integer_int_value == NULL) { - return NULL; - } - - // lookup Integer.floatValue() - jni_cache->float_float_value = - bsg_safe_get_method_id(env, jni_cache->float_class, "floatValue", "()F"); - if (jni_cache->float_float_value == NULL) { - return NULL; - } - - // lookup Double.doubleValue() - jni_cache->number_double_value = - bsg_safe_get_method_id(env, jni_cache->number, "doubleValue", "()D"); - if (jni_cache->number_double_value == NULL) { - return NULL; - } - - // lookup Long.longValue() - jni_cache->long_long_value = - bsg_safe_get_method_id(env, jni_cache->integer, "longValue", "()J"); - if (jni_cache->long_long_value == NULL) { - return NULL; - } - - // lookup Boolean.booleanValue() - jni_cache->boolean_bool_value = - bsg_safe_get_method_id(env, jni_cache->boolean, "booleanValue", "()Z"); - if (jni_cache->boolean_bool_value == NULL) { - return NULL; - } +static jobject get_map_value_obj(JNIEnv *env, jobject map, const char *_key) { + jobject obj = NULL; + jstring key = NULL; - // lookup java/util/ArrayList - jni_cache->arraylist = bsg_safe_find_class(env, "java/util/ArrayList"); - if (jni_cache->arraylist == NULL) { - return NULL; - } - - // lookup ArrayList constructor - jni_cache->arraylist_init_with_obj = bsg_safe_get_method_id( - env, jni_cache->arraylist, "", "(Ljava/util/Collection;)V"); - if (jni_cache->arraylist_init_with_obj == NULL) { - return NULL; + if (bsg_global_jni_cache == NULL) { + goto exit; } - // lookup ArrayList.get() - jni_cache->arraylist_get = bsg_safe_get_method_id( - env, jni_cache->arraylist, "get", "(I)Ljava/lang/Object;"); - if (jni_cache->arraylist_get == NULL) { - return NULL; + key = bsg_safe_new_string_utf(env, _key); + if (key == NULL) { + goto exit; } - // lookup java/util/HashMap - jni_cache->hash_map = bsg_safe_find_class(env, "java/util/HashMap"); - if (jni_cache->hash_map == NULL) { - return NULL; - } + obj = bsg_safe_call_object_method(env, map, + bsg_global_jni_cache->hash_map_get, key); - // lookup java/util/Map - jni_cache->map = bsg_safe_find_class(env, "java/util/Map"); - if (jni_cache->map == NULL) { - return NULL; - } +exit: + bsg_safe_delete_local_ref(env, key); + return obj; +} - // lookup java/util/Set - jni_cache->hash_map_key_set = bsg_safe_get_method_id( - env, jni_cache->hash_map, "keySet", "()Ljava/util/Set;"); - if (jni_cache->hash_map_key_set == NULL) { - return NULL; - } +static void copy_map_value_string(JNIEnv *env, jobject map, const char *_key, + char *dest, int len) { + jobject _value = get_map_value_obj(env, map, _key); - // lookup HashMap.size() - jni_cache->hash_map_size = - bsg_safe_get_method_id(env, jni_cache->hash_map, "size", "()I"); - if (jni_cache->hash_map_size == NULL) { - return NULL; + if (_value == NULL) { + return; } - // lookup HashMap.get() - jni_cache->hash_map_get = - bsg_safe_get_method_id(env, jni_cache->hash_map, "get", - "(Ljava/lang/Object;)Ljava/lang/Object;"); - if (jni_cache->hash_map_get == NULL) { - return NULL; + const char *value = bsg_safe_get_string_utf_chars(env, (jstring)_value); + if (value == NULL) { + return; } - // lookup Map.keySet() - jni_cache->map_key_set = bsg_safe_get_method_id(env, jni_cache->map, "keySet", - "()Ljava/util/Set;"); - if (jni_cache->map_key_set == NULL) { - return NULL; - } + bsg_strncpy(dest, value, len); + bsg_safe_release_string_utf_chars(env, _value, value); + bsg_safe_delete_local_ref(env, _value); +} - // lookup Map.size() - jni_cache->map_size = - bsg_safe_get_method_id(env, jni_cache->map, "size", "()I"); - if (jni_cache->map_size == NULL) { - return NULL; - } +static long get_map_value_long(JNIEnv *env, jobject map, const char *_key) { + jobject _value = NULL; + long value = 0; - // lookup Map.get() - jni_cache->map_get = bsg_safe_get_method_id( - env, jni_cache->map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - if (jni_cache->map_get == NULL) { - return NULL; + if (bsg_global_jni_cache == NULL) { + goto exit; } - // lookup com/bugsnag/android/NativeInterface - jni_cache->native_interface = - bsg_safe_find_class(env, "com/bugsnag/android/NativeInterface"); - if (jni_cache->native_interface == NULL) { - return NULL; + _value = get_map_value_obj(env, map, _key); + if (_value == NULL) { + goto exit; } - // lookup NativeInterface.getApp() - jni_cache->get_app_data = bsg_safe_get_static_method_id( - env, jni_cache->native_interface, "getApp", "()Ljava/util/Map;"); - if (jni_cache->get_app_data == NULL) { - return NULL; - } + value = bsg_safe_call_double_method( + env, _value, bsg_global_jni_cache->number_double_value); - // lookup NativeInterface.getDevice() - jni_cache->get_device_data = bsg_safe_get_static_method_id( - env, jni_cache->native_interface, "getDevice", "()Ljava/util/Map;"); - if (jni_cache->get_device_data == NULL) { - return NULL; - } +exit: + bsg_safe_delete_local_ref(env, _value); + return value; +} - // lookup NativeInterface.getUser() - jni_cache->get_user_data = bsg_safe_get_static_method_id( - env, jni_cache->native_interface, "getUser", "()Ljava/util/Map;"); - if (jni_cache->get_user_data == NULL) { - return NULL; - } +static float get_map_value_float(JNIEnv *env, jobject map, const char *_key) { + jobject _value = NULL; + float value = 0; - // lookup NativeInterface.getMetadata() - jni_cache->get_metadata = bsg_safe_get_static_method_id( - env, jni_cache->native_interface, "getMetadata", "()Ljava/util/Map;"); - if (jni_cache->get_metadata == NULL) { - return NULL; + if (bsg_global_jni_cache == NULL) { + goto exit; } - // lookup NativeInterface.getContext() - jni_cache->get_context = bsg_safe_get_static_method_id( - env, jni_cache->native_interface, "getContext", "()Ljava/lang/String;"); - if (jni_cache->get_context == NULL) { - return NULL; + _value = get_map_value_obj(env, map, _key); + if (_value == NULL) { + goto exit; } - return jni_cache; -} -jobject bsg_get_map_value_obj(JNIEnv *env, bsg_jni_cache *jni_cache, - jobject map, const char *_key) { - // create Java string object for map key - jstring key = bsg_safe_new_string_utf(env, _key); - if (key == NULL) { - return NULL; - } + value = bsg_safe_call_float_method(env, _value, + bsg_global_jni_cache->float_float_value); - jobject obj = - bsg_safe_call_object_method(env, map, jni_cache->hash_map_get, key); - bsg_safe_delete_local_ref(env, key); - return obj; +exit: + bsg_safe_delete_local_ref(env, _value); + return value; } -void bsg_copy_map_value_string(JNIEnv *env, bsg_jni_cache *jni_cache, - jobject map, const char *_key, char *dest, - int len) { - jobject _value = bsg_get_map_value_obj(env, jni_cache, map, _key); +static int get_map_value_int(JNIEnv *env, jobject map, const char *_key) { + jobject _value = NULL; + int value = 0; - if (_value != NULL) { - const char *value = bsg_safe_get_string_utf_chars(env, (jstring)_value); - if (value != NULL) { - bsg_strncpy(dest, value, len); - bsg_safe_release_string_utf_chars(env, _value, value); - } + if (bsg_global_jni_cache == NULL) { + goto exit; } -} - -long bsg_get_map_value_long(JNIEnv *env, bsg_jni_cache *jni_cache, jobject map, - const char *_key) { - jobject _value = bsg_get_map_value_obj(env, jni_cache, map, _key); - if (_value != NULL) { - long value = bsg_safe_call_double_method(env, _value, - jni_cache->number_double_value); - bsg_safe_delete_local_ref(env, _value); - return value; + _value = get_map_value_obj(env, map, _key); + if (_value == NULL) { + goto exit; } - return 0; -} -float bsg_get_map_value_float(JNIEnv *env, bsg_jni_cache *jni_cache, - jobject map, const char *_key) { - jobject _value = bsg_get_map_value_obj(env, jni_cache, map, _key); + value = bsg_safe_call_int_method(env, _value, + bsg_global_jni_cache->integer_int_value); - if (_value != NULL) { - float value = - bsg_safe_call_float_method(env, _value, jni_cache->float_float_value); - bsg_safe_delete_local_ref(env, _value); - return value; - } - return 0; +exit: + bsg_safe_delete_local_ref(env, _value); + return value; } -int bsg_get_map_value_int(JNIEnv *env, bsg_jni_cache *jni_cache, jobject map, - const char *_key) { - jobject _value = bsg_get_map_value_obj(env, jni_cache, map, _key); +static bool get_map_value_bool(JNIEnv *env, jobject map, const char *_key) { + jobject _value = NULL; + bool value = 0; - if (_value != NULL) { - jint value = - bsg_safe_call_int_method(env, _value, jni_cache->integer_int_value); - bsg_safe_delete_local_ref(env, _value); - return value; + if (bsg_global_jni_cache == NULL) { + goto exit; } - return 0; -} -bool bsg_get_map_value_bool(JNIEnv *env, bsg_jni_cache *jni_cache, jobject map, - const char *_key) { - jobject obj = bsg_get_map_value_obj(env, jni_cache, map, _key); - return bsg_safe_call_boolean_method(env, obj, jni_cache->boolean_bool_value); -} - -int bsg_populate_cpu_abi_from_map(JNIEnv *env, bsg_jni_cache *jni_cache, - jobject map, bsg_device_info *device) { - // create Java string object for map key - jstring key = bsg_safe_new_string_utf(env, "cpuAbi"); - if (key == NULL) { - return 0; + _value = get_map_value_obj(env, map, _key); + if (_value == NULL) { + goto exit; } - jobjectArray _value = - bsg_safe_call_object_method(env, map, jni_cache->hash_map_get, key); - if (_value != NULL) { - int count = bsg_safe_get_array_length(env, _value); - - // get the ABI as a Java string and copy it to bsg_device_info - for (int i = 0; i < count && i < sizeof(device->cpu_abi); i++) { - jstring jabi = bsg_safe_get_object_array_element(env, _value, i); - if (jabi == NULL) { - break; - } + value = bsg_safe_call_boolean_method( + env, _value, bsg_global_jni_cache->boolean_bool_value); - const char *abi = bsg_safe_get_string_utf_chars(env, jabi); - if (abi != NULL) { - bsg_strncpy(device->cpu_abi[i].value, abi, - sizeof(device->cpu_abi[i].value)); - bsg_safe_release_string_utf_chars(env, jabi, abi); - device->cpu_abi_count++; - } - } - bsg_safe_delete_local_ref(env, _value); - return count; - } - return 0; +exit: + bsg_safe_delete_local_ref(env, _value); + return value; } -void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, - jobject metadata) { - bsg_jni_cache *jni_cache = NULL; - jobject keyset = NULL; - jobject keylist = NULL; +static int populate_cpu_abi_from_map(JNIEnv *env, jobject map, + bsg_device_info *device) { + jstring key = NULL; + jobjectArray _value = NULL; + int count = 0; - if (metadata == NULL) { - goto exit; - } - jni_cache = bsg_populate_jni_cache(env); - if (jni_cache == NULL) { + if (bsg_global_jni_cache == NULL) { goto exit; } - // get size of metadata map - jint map_size = bsg_safe_call_int_method(env, metadata, jni_cache->map_size); - if (map_size == -1) { + key = bsg_safe_new_string_utf(env, "cpuAbi"); + if (key == NULL) { goto exit; } - // create a list of metadata keys - keyset = bsg_safe_call_object_method(env, metadata, jni_cache->map_key_set); - if (keyset == NULL) { - goto exit; - } - keylist = bsg_safe_new_object(env, jni_cache->arraylist, - jni_cache->arraylist_init_with_obj, keyset); - if (keylist == NULL) { + _value = bsg_safe_call_object_method(env, map, + bsg_global_jni_cache->hash_map_get, key); + if (_value == NULL) { goto exit; } - for (int i = 0; i < map_size; i++) { - jstring _key = bsg_safe_call_object_method( - env, keylist, jni_cache->arraylist_get, (jint)i); - jobject _value = - bsg_safe_call_object_method(env, metadata, jni_cache->map_get, _key); - - if (_key == NULL || _value == NULL) { - bsg_safe_delete_local_ref(env, _key); - bsg_safe_delete_local_ref(env, _value); - } else { - const char *key = bsg_safe_get_string_utf_chars(env, _key); - if (key != NULL) { - bsg_populate_metadata_value(env, &crumb->metadata, jni_cache, - "metaData", key, _value); - bsg_safe_release_string_utf_chars(env, _key, key); - } + count = bsg_safe_get_array_length(env, _value); + + // get the ABI as a Java string and copy it to bsg_device_info + for (int i = 0; i < count && i < sizeof(device->cpu_abi); i++) { + jstring jabi = bsg_safe_get_object_array_element(env, _value, i); + if (jabi == NULL) { + break; + } + + const char *abi = bsg_safe_get_string_utf_chars(env, jabi); + if (abi != NULL) { + bsg_strncpy(device->cpu_abi[i].value, abi, + sizeof(device->cpu_abi[i].value)); + bsg_safe_release_string_utf_chars(env, jabi, abi); + device->cpu_abi_count++; } + bsg_safe_delete_local_ref(env, jabi); } - goto exit; exit: - free(jni_cache); - bsg_safe_delete_local_ref(env, keyset); - bsg_safe_delete_local_ref(env, keylist); + bsg_safe_delete_local_ref(env, key); + bsg_safe_delete_local_ref(env, _value); + return count; } -void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, - bugsnag_event *event) { +static void populate_app_data(JNIEnv *env, bugsnag_event *event) { + if (bsg_global_jni_cache == NULL) { + return; + } + jobject data = bsg_safe_call_static_object_method( - env, jni_cache->native_interface, jni_cache->get_app_data); + env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_get_app); if (data == NULL) { return; } - 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)); - event->app.duration_ms_offset = - bsg_get_map_value_long(env, jni_cache, data, "duration"); + copy_map_value_string(env, data, "binaryArch", event->app.binary_arch, + sizeof(event->app.binary_arch)); + copy_map_value_string(env, data, "buildUUID", event->app.build_uuid, + sizeof(event->app.build_uuid)); + event->app.duration_ms_offset = get_map_value_long(env, data, "duration"); event->app.duration_in_foreground_ms_offset = - bsg_get_map_value_long(env, jni_cache, data, "durationInForeground"); + get_map_value_long(env, data, "durationInForeground"); - bsg_copy_map_value_string(env, jni_cache, data, "id", event->app.id, - sizeof(event->app.id)); - event->app.in_foreground = - bsg_get_map_value_bool(env, jni_cache, data, "inForeground"); + copy_map_value_string(env, data, "id", event->app.id, sizeof(event->app.id)); + event->app.in_foreground = get_map_value_bool(env, data, "inForeground"); event->app.is_launching = true; char name[64]; - bsg_copy_map_value_string(env, jni_cache, data, "name", name, sizeof(name)); + copy_map_value_string(env, data, "name", name, sizeof(name)); bugsnag_event_add_metadata_string(event, "app", "name", name); - bsg_copy_map_value_string(env, jni_cache, data, "releaseStage", - event->app.release_stage, - sizeof(event->app.release_stage)); - bsg_copy_map_value_string(env, jni_cache, data, "type", event->app.type, - sizeof(event->app.type)); - bsg_copy_map_value_string(env, jni_cache, data, "version", event->app.version, - sizeof(event->app.version)); - event->app.version_code = - bsg_get_map_value_int(env, jni_cache, data, "versionCode"); + copy_map_value_string(env, data, "releaseStage", event->app.release_stage, + sizeof(event->app.release_stage)); + copy_map_value_string(env, data, "type", event->app.type, + sizeof(event->app.type)); + copy_map_value_string(env, data, "version", event->app.version, + sizeof(event->app.version)); + event->app.version_code = get_map_value_int(env, data, "versionCode"); - bool restricted = - bsg_get_map_value_bool(env, jni_cache, data, "backgroundWorkRestricted"); + bool restricted = get_map_value_bool(env, data, "backgroundWorkRestricted"); if (restricted) { bugsnag_event_add_metadata_bool(event, "app", "backgroundWorkRestricted", @@ -453,132 +218,133 @@ void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, } char process_name[64]; - bsg_copy_map_value_string(env, jni_cache, data, "processName", process_name, - sizeof(process_name)); + copy_map_value_string(env, data, "processName", process_name, + sizeof(process_name)); bugsnag_event_add_metadata_string(event, "app", "processName", process_name); - long total_memory = - bsg_get_map_value_long(env, jni_cache, data, "memoryLimit"); + long total_memory = get_map_value_long(env, data, "memoryLimit"); bugsnag_event_add_metadata_double(event, "app", "memoryLimit", (double)total_memory); bsg_safe_delete_local_ref(env, data); } -const char *bsg_os_name() { return "android"; } - -void populate_device_metadata(JNIEnv *env, bsg_jni_cache *jni_cache, - bugsnag_event *event, void *data) { +static void populate_device_metadata(JNIEnv *env, bugsnag_event *event, + void *data) { char brand[64]; - bsg_copy_map_value_string(env, jni_cache, data, "brand", brand, - sizeof(brand)); + copy_map_value_string(env, data, "brand", brand, sizeof(brand)); bugsnag_event_add_metadata_string(event, "device", "brand", brand); - bugsnag_event_add_metadata_double( - event, "device", "dpi", - bsg_get_map_value_int(env, jni_cache, data, "dpi")); - bugsnag_event_add_metadata_bool( - event, "device", "emulator", - bsg_get_map_value_bool(env, jni_cache, data, "emulator")); + bugsnag_event_add_metadata_double(event, "device", "dpi", + get_map_value_int(env, data, "dpi")); + bugsnag_event_add_metadata_bool(event, "device", "emulator", + get_map_value_bool(env, data, "emulator")); char location_status[32]; - bsg_copy_map_value_string(env, jni_cache, data, "locationStatus", - location_status, sizeof(location_status)); + copy_map_value_string(env, data, "locationStatus", location_status, + sizeof(location_status)); bugsnag_event_add_metadata_string(event, "device", "locationStatus", location_status); char network_access[64]; - bsg_copy_map_value_string(env, jni_cache, data, "networkAccess", - network_access, sizeof(network_access)); + copy_map_value_string(env, data, "networkAccess", network_access, + sizeof(network_access)); bugsnag_event_add_metadata_string(event, "device", "networkAccess", network_access); bugsnag_event_add_metadata_double( event, "device", "screenDensity", - bsg_get_map_value_float(env, jni_cache, data, "screenDensity")); + get_map_value_float(env, data, "screenDensity")); char screen_resolution[32]; - bsg_copy_map_value_string(env, jni_cache, data, "screenResolution", - screen_resolution, sizeof(screen_resolution)); + copy_map_value_string(env, data, "screenResolution", screen_resolution, + sizeof(screen_resolution)); bugsnag_event_add_metadata_string(event, "device", "screenResolution", screen_resolution); } -void bsg_populate_device_data(JNIEnv *env, bsg_jni_cache *jni_cache, - bugsnag_event *event) { +static void populate_device_data(JNIEnv *env, bugsnag_event *event) { + if (bsg_global_jni_cache == NULL) { + return; + } + jobject data = bsg_safe_call_static_object_method( - env, jni_cache->native_interface, jni_cache->get_device_data); + env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_get_device); if (data == NULL) { return; } - bsg_populate_cpu_abi_from_map(env, jni_cache, data, &event->device); + populate_cpu_abi_from_map(env, data, &event->device); - bsg_copy_map_value_string(env, jni_cache, data, "id", event->device.id, - sizeof(event->device.id)); - event->device.jailbroken = - bsg_get_map_value_bool(env, jni_cache, data, "jailbroken"); + copy_map_value_string(env, data, "id", event->device.id, + sizeof(event->device.id)); + event->device.jailbroken = get_map_value_bool(env, data, "jailbroken"); - bsg_copy_map_value_string(env, jni_cache, data, "locale", - event->device.locale, sizeof(event->device.locale)); + copy_map_value_string(env, data, "locale", event->device.locale, + sizeof(event->device.locale)); - bsg_copy_map_value_string(env, jni_cache, data, "manufacturer", - event->device.manufacturer, - sizeof(event->device.manufacturer)); - bsg_copy_map_value_string(env, jni_cache, data, "model", event->device.model, - sizeof(event->device.model)); + copy_map_value_string(env, data, "manufacturer", event->device.manufacturer, + sizeof(event->device.manufacturer)); + copy_map_value_string(env, data, "model", event->device.model, + sizeof(event->device.model)); - bsg_copy_map_value_string(env, jni_cache, data, "orientation", - event->device.orientation, - sizeof(event->device.orientation)); + copy_map_value_string(env, data, "orientation", event->device.orientation, + sizeof(event->device.orientation)); bsg_strncpy(event->device.os_name, bsg_os_name(), sizeof(event->device.os_name)); - bsg_copy_map_value_string(env, jni_cache, data, "osVersion", - event->device.os_version, - sizeof(event->device.os_version)); - - jobject _runtime_versions = - bsg_get_map_value_obj(env, jni_cache, data, "runtimeVersions"); + copy_map_value_string(env, data, "osVersion", event->device.os_version, + sizeof(event->device.os_version)); + jobject _runtime_versions = get_map_value_obj(env, data, "runtimeVersions"); if (_runtime_versions != NULL) { - bsg_copy_map_value_string(env, jni_cache, _runtime_versions, "osBuild", - event->device.os_build, - sizeof(event->device.os_build)); + copy_map_value_string(env, _runtime_versions, "osBuild", + event->device.os_build, + sizeof(event->device.os_build)); char api_level[8]; - bsg_copy_map_value_string(env, jni_cache, _runtime_versions, - "androidApiLevel", api_level, sizeof(api_level)); + copy_map_value_string(env, _runtime_versions, "androidApiLevel", api_level, + sizeof(api_level)); event->device.api_level = strtol(api_level, NULL, 10); - bsg_safe_delete_local_ref(env, _runtime_versions); } - event->device.total_memory = - bsg_get_map_value_long(env, jni_cache, data, "totalMemory"); + event->device.total_memory = get_map_value_long(env, data, "totalMemory"); // add fields to device metadata - populate_device_metadata(env, jni_cache, event, data); + populate_device_metadata(env, event, data); + bsg_safe_delete_local_ref(env, data); + bsg_safe_delete_local_ref(env, _runtime_versions); } -void bsg_populate_user_data(JNIEnv *env, bsg_jni_cache *jni_cache, - bugsnag_event *event) { +static void populate_user_data(JNIEnv *env, bugsnag_event *event) { + if (bsg_global_jni_cache == NULL) { + return; + } + jobject data = bsg_safe_call_static_object_method( - env, jni_cache->native_interface, jni_cache->get_user_data); + env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_get_user); if (data == NULL) { return; } - bsg_copy_map_value_string(env, jni_cache, data, "id", event->user.id, - sizeof(event->user.id)); - bsg_copy_map_value_string(env, jni_cache, data, "name", event->user.name, - sizeof(event->user.name)); - bsg_copy_map_value_string(env, jni_cache, data, "email", event->user.email, - sizeof(event->user.email)); + copy_map_value_string(env, data, "id", event->user.id, + sizeof(event->user.id)); + copy_map_value_string(env, data, "name", event->user.name, + sizeof(event->user.name)); + copy_map_value_string(env, data, "email", event->user.email, + sizeof(event->user.email)); + bsg_safe_delete_local_ref(env, data); } -void bsg_populate_context(JNIEnv *env, bsg_jni_cache *jni_cache, - bugsnag_event *event) { +static void populate_context(JNIEnv *env, bugsnag_event *event) { + if (bsg_global_jni_cache == NULL) { + return; + } + jstring _context = bsg_safe_call_static_object_method( - env, jni_cache->native_interface, jni_cache->get_context); + env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_get_context); if (_context != NULL) { const char *value = bsg_safe_get_string_utf_chars(env, (jstring)_context); if (value != NULL) { @@ -588,72 +354,80 @@ void bsg_populate_context(JNIEnv *env, bsg_jni_cache *jni_cache, } else { memset(&event->context, 0, bsg_strlen(event->context)); } + + bsg_safe_delete_local_ref(env, _context); } -void bsg_populate_event(JNIEnv *env, bugsnag_event *event) { - bsg_jni_cache *jni_cache = bsg_populate_jni_cache(env); - if (jni_cache == NULL) { +static void populate_metadata_value(JNIEnv *env, bugsnag_metadata *dst, + const char *section, const char *name, + jobject _value) { + if (bsg_global_jni_cache == NULL) { return; } - bsg_populate_context(env, jni_cache, event); - bsg_populate_app_data(env, jni_cache, event); - bsg_populate_device_data(env, jni_cache, event); - bsg_populate_user_data(env, jni_cache, event); - free(jni_cache); -} -void bsg_populate_metadata_value(JNIEnv *env, bugsnag_metadata *dst, - bsg_jni_cache *jni_cache, const char *section, - const char *name, jobject _value) { - if (bsg_safe_is_instance_of(env, _value, jni_cache->number)) { + if (bsg_safe_is_instance_of(env, _value, bsg_global_jni_cache->number)) { // add a double metadata value - double value = bsg_safe_call_double_method(env, _value, - jni_cache->number_double_value); + double value = bsg_safe_call_double_method( + env, _value, bsg_global_jni_cache->number_double_value); bsg_add_metadata_value_double(dst, section, name, value); - } else if (bsg_safe_is_instance_of(env, _value, jni_cache->boolean)) { + } else if (bsg_safe_is_instance_of(env, _value, + bsg_global_jni_cache->boolean)) { // add a boolean metadata value - bool value = bsg_safe_call_boolean_method(env, _value, - jni_cache->boolean_bool_value); + bool value = bsg_safe_call_boolean_method( + env, _value, bsg_global_jni_cache->boolean_bool_value); bsg_add_metadata_value_bool(dst, section, name, value); - } else if (bsg_safe_is_instance_of(env, _value, jni_cache->string)) { + } else if (bsg_safe_is_instance_of(env, _value, + bsg_global_jni_cache->string)) { const char *value = bsg_safe_get_string_utf_chars(env, _value); if (value != NULL) { bsg_add_metadata_value_str(dst, section, name, value); - free((char *)value); } } } -void bsg_populate_metadata_obj(JNIEnv *env, bugsnag_metadata *dst, - bsg_jni_cache *jni_cache, jobject section, - jobject section_keylist, int index) { - jstring section_key = bsg_safe_call_object_method( - env, section_keylist, jni_cache->arraylist_get, (jint)index); +static void populate_metadata_obj(JNIEnv *env, bugsnag_metadata *dst, + jobject section, jobject section_keylist, + int index) { + jstring section_key = NULL; + const char *name = NULL; + jobject _value = NULL; + + if (bsg_global_jni_cache == NULL) { + goto exit; + } + + section_key = bsg_safe_call_object_method( + env, section_keylist, bsg_global_jni_cache->arraylist_get, (jint)index); if (section_key == NULL) { - return; + goto exit; } - jobject _value = bsg_safe_call_object_method(env, section, jni_cache->map_get, - section_key); - const char *name = bsg_safe_get_string_utf_chars(env, section_key); - if (name != NULL) { - bsg_populate_metadata_value(env, dst, jni_cache, section, name, _value); - bsg_safe_release_string_utf_chars(env, section_key, name); + _value = bsg_safe_call_object_method( + env, section, bsg_global_jni_cache->map_get, section_key); + name = bsg_safe_get_string_utf_chars(env, section_key); + if (name == NULL) { + goto exit; } + + populate_metadata_value(env, dst, section, name, _value); + +exit: + bsg_safe_release_string_utf_chars(env, section_key, name); + bsg_safe_delete_local_ref(env, section_key); bsg_safe_delete_local_ref(env, _value); } -void bsg_populate_metadata_section(JNIEnv *env, bugsnag_metadata *dst, - jobject metadata, bsg_jni_cache *jni_cache, - jobject keylist, int i) { +static void populate_metadata_section(JNIEnv *env, bugsnag_metadata *dst, + jobject metadata, jobject keylist, + int i) { jstring _key = NULL; const char *section = NULL; jobject _section = NULL; jobject section_keyset = NULL; jobject section_keylist = NULL; - _key = bsg_safe_call_object_method(env, keylist, jni_cache->arraylist_get, - (jint)i); + _key = bsg_safe_call_object_method( + env, keylist, bsg_global_jni_cache->arraylist_get, (jint)i); if (_key == NULL) { goto exit; } @@ -661,85 +435,155 @@ void bsg_populate_metadata_section(JNIEnv *env, bugsnag_metadata *dst, if (section == NULL) { goto exit; } - _section = - bsg_safe_call_object_method(env, metadata, jni_cache->map_get, _key); + _section = bsg_safe_call_object_method(env, metadata, + bsg_global_jni_cache->map_get, _key); if (_section == NULL) { goto exit; } jint section_size = - bsg_safe_call_int_method(env, _section, jni_cache->map_size); + bsg_safe_call_int_method(env, _section, bsg_global_jni_cache->map_size); if (section_size == -1) { goto exit; } - section_keyset = - bsg_safe_call_object_method(env, _section, jni_cache->map_key_set); + section_keyset = bsg_safe_call_object_method( + env, _section, bsg_global_jni_cache->map_key_set); if (section_keyset == NULL) { goto exit; } - section_keylist = - bsg_safe_new_object(env, jni_cache->arraylist, - jni_cache->arraylist_init_with_obj, section_keyset); + section_keylist = bsg_safe_new_object( + env, bsg_global_jni_cache->arraylist, + bsg_global_jni_cache->arraylist_init_with_obj, section_keyset); if (section_keylist == NULL) { goto exit; } for (int j = 0; j < section_size; j++) { - bsg_populate_metadata_obj(env, dst, jni_cache, _section, section_keylist, - j); + populate_metadata_obj(env, dst, _section, section_keylist, j); } goto exit; exit: bsg_safe_release_string_utf_chars(env, _key, section); + bsg_safe_delete_local_ref(env, _key); + bsg_safe_delete_local_ref(env, _section); bsg_safe_delete_local_ref(env, section_keyset); bsg_safe_delete_local_ref(env, section_keylist); - bsg_safe_delete_local_ref(env, _section); } +// Internal API + void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, jobject metadata) { + jobject _metadata = NULL; jobject keyset = NULL; jobject keylist = NULL; - bsg_jni_cache *jni_cache = bsg_populate_jni_cache(env); - if (jni_cache == NULL) { + if (bsg_global_jni_cache == NULL) { goto exit; } if (metadata == NULL) { - metadata = bsg_safe_call_static_object_method( - env, jni_cache->native_interface, jni_cache->get_metadata); + _metadata = bsg_safe_call_static_object_method( + env, bsg_global_jni_cache->native_interface, + bsg_global_jni_cache->ni_get_metadata); + metadata = _metadata; } - if (metadata != NULL) { - int size = bsg_safe_call_int_method(env, metadata, jni_cache->map_size); - if (size == -1) { - goto exit; - } - // create a list of metadata keys - keyset = bsg_safe_call_static_object_method(env, metadata, - jni_cache->map_key_set); - if (keyset == NULL) { - goto exit; - } - keylist = bsg_safe_new_object(env, jni_cache->arraylist, - jni_cache->arraylist_init_with_obj, keyset); - if (keylist == NULL) { - goto exit; - } - - for (int i = 0; i < size; i++) { - bsg_populate_metadata_section(env, dst, metadata, jni_cache, keylist, i); - } - } else { + if (metadata == NULL) { dst->value_count = 0; + goto exit; + } + + int size = + bsg_safe_call_int_method(env, metadata, bsg_global_jni_cache->map_size); + if (size == -1) { + goto exit; + } + + // create a list of metadata keys + keyset = bsg_safe_call_static_object_method( + env, metadata, bsg_global_jni_cache->map_key_set); + if (keyset == NULL) { + goto exit; + } + keylist = bsg_safe_new_object(env, bsg_global_jni_cache->arraylist, + bsg_global_jni_cache->arraylist_init_with_obj, + keyset); + if (keylist == NULL) { + goto exit; + } + + for (int i = 0; i < size; i++) { + populate_metadata_section(env, dst, metadata, keylist, i); } - goto exit; -// cleanup exit: - if (jni_cache != NULL) { - free(jni_cache); + bsg_safe_delete_local_ref(env, _metadata); + bsg_safe_delete_local_ref(env, keyset); + bsg_safe_delete_local_ref(env, keylist); +} + +void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, + jobject metadata) { + jobject keyset = NULL; + jobject keylist = NULL; + + if (metadata == NULL) { + goto exit; + } + if (bsg_global_jni_cache == NULL) { + goto exit; + } + + // get size of metadata map + jint map_size = + bsg_safe_call_int_method(env, metadata, bsg_global_jni_cache->map_size); + if (map_size == -1) { + goto exit; } + + // create a list of metadata keys + keyset = bsg_safe_call_object_method(env, metadata, + bsg_global_jni_cache->map_key_set); + if (keyset == NULL) { + goto exit; + } + keylist = bsg_safe_new_object(env, bsg_global_jni_cache->arraylist, + bsg_global_jni_cache->arraylist_init_with_obj, + keyset); + if (keylist == NULL) { + goto exit; + } + + for (int i = 0; i < map_size; i++) { + jstring _key = bsg_safe_call_object_method( + env, keylist, bsg_global_jni_cache->arraylist_get, (jint)i); + jobject _value = bsg_safe_call_object_method( + env, metadata, bsg_global_jni_cache->map_get, _key); + + if (_key != NULL && _value != NULL) { + const char *key = bsg_safe_get_string_utf_chars(env, _key); + if (key != NULL) { + populate_metadata_value(env, &crumb->metadata, "metaData", key, _value); + bsg_safe_release_string_utf_chars(env, _key, key); + } + } + bsg_safe_delete_local_ref(env, _key); + bsg_safe_delete_local_ref(env, _value); + } + +exit: bsg_safe_delete_local_ref(env, keyset); bsg_safe_delete_local_ref(env, keylist); } + +void bsg_populate_event(JNIEnv *env, bugsnag_event *event) { + if (bsg_global_jni_cache == NULL) { + return; + } + populate_context(env, event); + populate_app_data(env, event); + populate_device_data(env, event); + populate_user_data(env, event); +} + +const char *bsg_os_name() { return "android"; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c index 264ba7b6b2..f297899608 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/safejni.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/safejni.c @@ -14,10 +14,7 @@ bool bsg_check_and_clear_exc(JNIEnv *env) { } jclass bsg_safe_find_class(JNIEnv *env, const char *clz_name) { - if (env == NULL) { - return NULL; - } - if (clz_name == NULL) { + if (env == NULL || clz_name == NULL) { return NULL; } jclass clz = (*env)->FindClass(env, clz_name); @@ -56,7 +53,7 @@ jstring bsg_safe_new_string_utf(JNIEnv *env, const char *str) { jboolean bsg_safe_call_boolean_method(JNIEnv *env, jobject _value, jmethodID method) { - if (env == NULL || _value == NULL) { + if (env == NULL || _value == NULL || method == NULL) { return false; } jboolean value = (*env)->CallBooleanMethod(env, _value, method); @@ -67,7 +64,7 @@ jboolean bsg_safe_call_boolean_method(JNIEnv *env, jobject _value, } jint bsg_safe_call_int_method(JNIEnv *env, jobject _value, jmethodID method) { - if (env == NULL || _value == NULL) { + if (env == NULL || _value == NULL || method == NULL) { return -1; } jint value = (*env)->CallIntMethod(env, _value, method); @@ -79,7 +76,7 @@ jint bsg_safe_call_int_method(JNIEnv *env, jobject _value, jmethodID method) { jfloat bsg_safe_call_float_method(JNIEnv *env, jobject _value, jmethodID method) { - if (env == NULL || _value == NULL) { + if (env == NULL || _value == NULL || method == NULL) { return -1; } jfloat value = (*env)->CallFloatMethod(env, _value, method); @@ -91,7 +88,7 @@ jfloat bsg_safe_call_float_method(JNIEnv *env, jobject _value, jdouble bsg_safe_call_double_method(JNIEnv *env, jobject _value, jmethodID method) { - if (env == NULL || _value == NULL) { + if (env == NULL || _value == NULL || method == NULL) { return -1; } jdouble value = (*env)->CallDoubleMethod(env, _value, method); @@ -141,7 +138,7 @@ jfieldID bsg_safe_get_static_field_id(JNIEnv *env, jclass clz, const char *name, jobject bsg_safe_get_static_object_field(JNIEnv *env, jclass clz, jfieldID field) { - if (env == NULL || clz == NULL) { + if (env == NULL || clz == NULL || field == NULL) { return NULL; } jobject obj = (*env)->GetStaticObjectField(env, clz, field); @@ -150,7 +147,7 @@ jobject bsg_safe_get_static_object_field(JNIEnv *env, jclass clz, } jobject bsg_safe_new_object(JNIEnv *env, jclass clz, jmethodID method, ...) { - if (env == NULL || clz == NULL) { + if (env == NULL || clz == NULL || method == NULL) { return NULL; } va_list args; @@ -163,7 +160,7 @@ jobject bsg_safe_new_object(JNIEnv *env, jclass clz, jmethodID method, ...) { jobject bsg_safe_call_object_method(JNIEnv *env, jobject _value, jmethodID method, ...) { - if (env == NULL || _value == NULL) { + if (env == NULL || _value == NULL || method == NULL) { return NULL; } va_list args; @@ -176,7 +173,7 @@ jobject bsg_safe_call_object_method(JNIEnv *env, jobject _value, void bsg_safe_call_static_void_method(JNIEnv *env, jclass clz, jmethodID method, ...) { - if (env == NULL || clz == NULL) { + if (env == NULL || clz == NULL || method == NULL) { return; } va_list args; @@ -188,7 +185,7 @@ void bsg_safe_call_static_void_method(JNIEnv *env, jclass clz, jmethodID method, jobject bsg_safe_call_static_object_method(JNIEnv *env, jclass clz, jmethodID method, ...) { - if (env == NULL || clz == NULL) { + if (env == NULL || clz == NULL || method == NULL) { return NULL; } va_list args; @@ -200,54 +197,57 @@ jobject bsg_safe_call_static_object_method(JNIEnv *env, jclass clz, } void bsg_safe_delete_local_ref(JNIEnv *env, jobject obj) { - if (env != NULL) { - (*env)->DeleteLocalRef(env, obj); + if (env == NULL || obj == NULL) { + return; } + (*env)->DeleteLocalRef(env, obj); } const char *bsg_safe_get_string_utf_chars(JNIEnv *env, jstring string) { - if (env != NULL && string != NULL) { - return (*env)->GetStringUTFChars(env, string, NULL); + if (env == NULL || string == NULL) { + return NULL; } - return NULL; + return (*env)->GetStringUTFChars(env, string, NULL); } void bsg_safe_release_string_utf_chars(JNIEnv *env, jstring string, const char *utf) { - if (env != NULL && string != NULL && utf != NULL) { - (*env)->ReleaseStringUTFChars(env, string, utf); + if (env == NULL || string == NULL || utf == NULL) { + return; } + (*env)->ReleaseStringUTFChars(env, string, utf); } void bsg_safe_release_byte_array_elements(JNIEnv *env, jbyteArray array, jbyte *elems) { + if (env == NULL || array == NULL || elems == NULL) { + return; + } // If mode is anything other than JNI_COMMIT, the JNI method will try and call // delete[] on the elems parameter, which leads to bad things happening (e.g. // aborting will cause it to free, blowing up any custom allocators). // Therefore JNI_COMMIT will always be called and the caller should free the // elems parameter themselves if necessary. // https://android.googlesource.com/platform/art/+/refs/heads/master/runtime/jni/jni_internal.cc#2689 - if (env != NULL && array != NULL) { - (*env)->ReleaseByteArrayElements(env, array, elems, JNI_COMMIT); - } + (*env)->ReleaseByteArrayElements(env, array, elems, JNI_COMMIT); } jsize bsg_safe_get_array_length(JNIEnv *env, jarray array) { - if (env != NULL && array != NULL) { - return (*env)->GetArrayLength(env, array); + if (env == NULL || array == NULL) { + return -1; } - return -1; + return (*env)->GetArrayLength(env, array); } jboolean bsg_safe_is_instance_of(JNIEnv *env, jobject object, jclass clz) { - if (env != NULL && clz != NULL) { - return (*env)->IsInstanceOf(env, object, clz); + if (env == NULL || clz == NULL) { + return false; } - return false; + return (*env)->IsInstanceOf(env, object, clz); } jbyteArray bsg_byte_ary_from_string(JNIEnv *env, const char *text) { - if (text == NULL) { + if (env == NULL || text == NULL) { return NULL; } size_t text_length = bsg_strlen(text); @@ -262,4 +262,4 @@ jbyteArray bsg_byte_ary_from_string(JNIEnv *env, const char *text) { return NULL; } return jtext; -} \ No newline at end of file +} From 50b4577158c7e72995e6f6bf3161a9eb407af316 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 12 Jan 2022 09:58:18 +0000 Subject: [PATCH 36/37] docs: bump version --- CHANGELOG.md | 2 +- .../src/main/java/com/bugsnag/android/Notifier.kt | 2 +- gradle.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 076b57fb9d..37af178c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## TBD +## 5.19.0 (2022-01-12) * New APIs to support forthcoming feature flag and experiment functionality. For more information, please see https://docs.bugsnag.com/product/features-experiments. 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 c8b01a9f4b..f511731de3 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.18.0", + var version: String = "5.19.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/gradle.properties b/gradle.properties index a3eab41e82..bafd1ac771 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.18.0 +VERSION_NAME=5.19.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git From 4527d2658fa7a58bd7b8937210cfe582f11939ae Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 12 Jan 2022 13:28:40 +0000 Subject: [PATCH 37/37] docs: remove dupe entry from changelog --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37af178c6e..fcd8e973c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,6 @@ ### Enhancements -* Explicitly define Kotlin api/language versions - [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564) - * Explicitly define Kotlin api/language versions [#1564](https://github.com/bugsnag/bugsnag-android/pull/1564)