From baadae6e482df6f910cc4c113c0e894ae5d2f989 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 25 Apr 2024 15:25:13 +0100 Subject: [PATCH 01/28] feat(correlation): added the trace correlation property to the Event model --- .../api/bugsnag-android-core.api | 1 + bugsnag-android-core/detekt-baseline.xml | 3 ++ .../com/bugsnag/android/BugsnagEventMapper.kt | 47 +++++++++++++++---- .../main/java/com/bugsnag/android/Event.java | 16 +++++++ .../java/com/bugsnag/android/EventInternal.kt | 6 +++ .../com/bugsnag/android/TraceCorrelation.kt | 20 ++++++++ .../bugsnag/android/EventSerializationTest.kt | 12 ++++- .../test/resources/event_serialization_8.json | 41 ++++++++++++++++ 8 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt create mode 100644 bugsnag-android-core/src/test/resources/event_serialization_8.json diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index ecca487084..0664e27494 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -378,6 +378,7 @@ public class com/bugsnag/android/Event : com/bugsnag/android/FeatureFlagAware, c public fun setContext (Ljava/lang/String;)V public fun setGroupingHash (Ljava/lang/String;)V public fun setSeverity (Lcom/bugsnag/android/Severity;)V + public fun setTraceCorrelation (Ljava/util/UUID;J)V public fun setUnhandled (Z)V public fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V protected fun shouldDiscardClass ()Z diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index a5e99e257b..2d253c40c8 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -20,6 +20,9 @@ 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, /** * Identifies the exact build this frame originates from. */ var codeIdentifier: String? = 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, @JvmField val maxBreadcrumbs: Int ) LongParameterList:ThreadState.kt$ThreadState$( allThreads: List<JavaThread>, currentThread: JavaThread, exc: Throwable?, isUnhandled: Boolean, maxThreadCount: Int, threadCollectionTimeLimitMillis: Long, projectPackages: Collection<String>, logger: Logger ) + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$16 + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$32 + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$56 MagicNumber:DefaultDelivery.kt$DefaultDelivery$299 MagicNumber:DefaultDelivery.kt$DefaultDelivery$429 MagicNumber:DefaultDelivery.kt$DefaultDelivery$499 diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index c3af9864b5..82000f6a38 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -7,6 +7,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.util.UUID internal class BugsnagEventMapper( private val logger: Logger @@ -93,6 +94,16 @@ internal class BugsnagEventMapper( // populate internalMetrics event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap?) + // populate correlation + (map["correlation"] as? Map)?.let { + val traceId = parseTraceId(it["traceid"]) + val spanId = it["spanid"]?.parseUnsignedLong() + + if (traceId != null && spanId != null) { + event.traceCorrelation = TraceCorrelation(traceId, spanId) + } + } + return event } @@ -234,15 +245,6 @@ internal class BugsnagEventMapper( } } - // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. - private val ndkDateFormatHolder = object : ThreadLocal() { - override fun initialValue(): DateFormat { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - } - } - private fun String.toDate(): Date { return try { DateUtils.fromIso8601(this) @@ -251,4 +253,31 @@ internal class BugsnagEventMapper( ?: throw IllegalArgumentException("cannot parse date $this") } } + + private fun parseTraceId(traceId: String?): UUID? { + if (traceId?.length != 32) return null + val mostSigBits = traceId.substring(0, 16).parseUnsignedLong() ?: return null + val leastSigBits = traceId.substring(16).parseUnsignedLong() ?: return null + + return UUID(mostSigBits, leastSigBits) + } + + private fun String.parseUnsignedLong(): Long? { + if (length != 16) return null + return try { + (substring(0, 2).toLong(16) shl 56) or + substring(2).toLong(16) + } catch (nfe: NumberFormatException) { + null + } + } + + // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. + private val ndkDateFormatHolder = object : ThreadLocal() { + override fun initialValue(): DateFormat { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + } + } } 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 ac498bb174..4dc9b31e4f 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 @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.regex.Pattern; /** @@ -382,6 +383,21 @@ public void setUnhandled(boolean unhandled) { impl.setUnhandled(unhandled); } + /** + * Associate this event with a specific trace. This is usually done automatically when + * using bugsnag-android-performance, but can also be set manually if required. + * + * @param traceId the ID of the trace the event occurred within + * @param spanId the ID of the span that the event occurred within + */ + public void setTraceCorrelation(@NonNull UUID traceId, long spanId) { + if (traceId != null) { + impl.setTraceCorrelation(new TraceCorrelation(traceId, spanId)); + } else { + logNull("traceId"); + } + } + protected boolean shouldDiscardClass() { return impl.shouldDiscardClass(); } 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 4a1881f71d..88b46df334 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 @@ -119,6 +119,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata */ internal var userImpl: User + var traceCorrelation: TraceCorrelation? = null + fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled @@ -193,6 +195,10 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata writer.name("featureFlags").value(featureFlags) + traceCorrelation?.let { correlation -> + writer.name("correlation").value(correlation) + } + if (session != null) { val copy = Session.copySession(session) writer.name("session").beginObject() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt new file mode 100644 index 0000000000..0909406b6e --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt @@ -0,0 +1,20 @@ +package com.bugsnag.android + +import java.util.UUID + +internal data class TraceCorrelation(val traceId: UUID, val spanId: Long) : JsonStream.Streamable { + override fun toStream(writer: JsonStream) { + writer.beginObject() + .name("traceid").value(traceId.toHexString()) + .name("spanid").value(spanId.toHexString()) + writer.endObject() + } + + private fun UUID.toHexString(): String { + return "%016x%016x".format(mostSignificantBits, leastSignificantBits) + } + + private fun Long.toHexString(): String { + return "%016x".format(this) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt index f930fe313c..84175e34e7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt @@ -9,6 +9,7 @@ import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters import java.util.Date +import java.util.UUID @RunWith(Parameterized::class) internal class EventSerializationTest { @@ -35,7 +36,8 @@ internal class EventSerializationTest { createEvent { val user = User("123", "foo@example.com", "Joe") val apiKey = "BUGSNAG_API_KEY" - it.session = Session("123", Date(0), user, false, Notifier(), NoopLogger, apiKey) + it.session = + Session("123", Date(0), user, false, Notifier(), NoopLogger, apiKey) }, // threads included @@ -82,6 +84,14 @@ internal class EventSerializationTest { createEvent { it.addFeatureFlag("no_variant") it.addFeatureFlag("flag", "with_variant") + }, + + // with a trace correlation + createEvent { + it.setTraceCorrelation( + UUID(0x24b8b82900d34da3, -0x659434b74a5b9edc), + 0x3dbe7c7ae84945b9 + ) } ) } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_8.json b/bugsnag-android-core/src/test/resources/event_serialization_8.json new file mode 100644 index 0000000000..8d0da45a80 --- /dev/null +++ b/bugsnag-android-core/src/test/resources/event_serialization_8.json @@ -0,0 +1,41 @@ +{ + "metaData": {}, + "severity": "warning", + "severityReason": { + "type": "handledException", + "unhandledOverridden": false + }, + "unhandled": false, + "exceptions": [], + "projectPackages":[ + "com.example.foo" + ], + "user": {}, + "app": { + "type": "android", + "versionCode": 0 + }, + "device": { + "cpuAbi": [], + "manufacturer": "samsung", + "model": "s7", + "osName": "android", + "osVersion": "7.1", + "runtimeVersions": { + "osBuild": "bulldog", + "androidApiLevel": "24" + }, + "totalMemory": 109230923452, + "freeDisk": 22234423124, + "freeMemory": 92340255592, + "orientation": "portrait", + "time": "1970-01-01T00:00:00.000Z" + }, + "breadcrumbs": [], + "threads": [], + "featureFlags": [], + "correlation": { + "traceid": "24b8b82900d34da39a6bcb48b5a46124", + "spanid": "3dbe7c7ae84945b9" + } +} From 084df8bf7d0092f18191ddc8fb8bc2484fc5eca3 Mon Sep 17 00:00:00 2001 From: Steve Kirkland-Walton Date: Wed, 15 May 2024 17:38:00 +0100 Subject: [PATCH 02/28] Add Android 14 tests [full ci] --- .buildkite/pipeline.full.yml | 66 ++++++++++++++++++++++ .buildkite/pipeline.yml | 32 +++++++++++ features/full_tests/detect_anr_jvm.feature | 3 +- features/full_tests/multi_process.feature | 2 + 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index 02fe640f07..eb0a5deac6 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -488,6 +488,72 @@ steps: concurrency_group: 'bitbar' concurrency_method: eager + - label: ':bitbar: Android 14 NDK r21 end-to-end tests - batch 1' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/full_tests" + - "--exclude=features/full_tests/[^a-k].*.feature" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + + - label: ':bitbar: Android 14 NDK r21 end-to-end tests - batch 2' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/full_tests" + - "--exclude=features/full_tests/[^l-z].*.feature" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + # If there is a tag present activate a manual publishing step - block: 'Trigger package publish' diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 1ed4bcd747..17e408882d 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -346,6 +346,38 @@ steps: concurrency_group: 'bitbar' concurrency_method: eager + - label: ':bitbar: Android 14 NDK r21 smoke tests' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/smoke_tests" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + - label: 'Conditionally include device farms/full tests' agents: queue: macos-14 diff --git a/features/full_tests/detect_anr_jvm.feature b/features/full_tests/detect_anr_jvm.feature index c65be424c6..e80fdfbc16 100644 --- a/features/full_tests/detect_anr_jvm.feature +++ b/features/full_tests/detect_anr_jvm.feature @@ -41,8 +41,7 @@ Feature: ANRs triggered in JVM code are captured @skip_android_10 Scenario: ANR triggered in JVM code is not captured when detectAnrs = false When I run "JvmAnrDisabledScenario" - And I wait for 2 seconds - And I tap the screen 3 times + # No screen taps for this scenario as it seems to break Appium with Android 8 Then I wait to receive an error And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier And the error payload field "events" is an array with 1 elements diff --git a/features/full_tests/multi_process.feature b/features/full_tests/multi_process.feature index d4995d3077..aa8e178a70 100644 --- a/features/full_tests/multi_process.feature +++ b/features/full_tests/multi_process.feature @@ -32,6 +32,8 @@ Feature: Reporting errors in multi process apps And the error payload field "events.0.user.name" equals "MultiProcessHandledExceptionScenario" And the error payload field "events.0.user.email" equals "foreground@example.com" + # Skipped pending PLAT-12145 + @skip Scenario: Unhandled JVM error And I configure the app to run in the "main-activity" state When I run "MultiProcessUnhandledExceptionScenario" and relaunch the crashed app From df65b5fa869ba6b3fbb1cf048b85d7c33c342326 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 1 May 2024 14:12:15 +0100 Subject: [PATCH 03/28] refactor(ndk): added BSG_KSJSONCodec to the NDK plugin --- .../src/main/CMakeLists.txt | 50 +- .../serializer/BSG_KSCrashStringConversion.c | 188 ++++++ .../serializer/BSG_KSCrashStringConversion.h | 73 +++ .../jni/utils/serializer/BSG_KSJSONCodec.c | 550 ++++++++++++++++++ .../jni/utils/serializer/BSG_KSJSONCodec.h | 479 +++++++++++++++ 5 files changed, 1316 insertions(+), 24 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c create mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 6492ae9e7d..0e47e07a8b 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -6,35 +6,37 @@ add_library( # Specifies the name of the library. SHARED # Provides a relative path to your source file(s). - jni/bugsnag_ndk.c - jni/bugsnag.c - jni/metadata.c - jni/safejni.c - jni/jni_cache.c - jni/event.c - jni/featureflags.c - jni/internal_metrics.c - jni/handlers/signal_handler.c - jni/handlers/cpp_handler.cpp - jni/utils/crash_info.c - 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.cpp - jni/utils/seqlock.c - jni/utils/serializer.c - jni/utils/string.c - jni/utils/threads.c - jni/utils/memory.c - jni/deps/parson/parson.c - ) + jni/bugsnag_ndk.c + jni/bugsnag.c + jni/metadata.c + jni/safejni.c + jni/jni_cache.c + jni/event.c + jni/featureflags.c + jni/internal_metrics.c + jni/handlers/signal_handler.c + jni/handlers/cpp_handler.cpp + jni/utils/crash_info.c + 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/serializer/BSG_KSJSONCodec.c + jni/utils/serializer/BSG_KSCrashStringConversion.c + jni/utils/stack_unwinder.cpp + jni/utils/seqlock.c + jni/utils/serializer.c + jni/utils/string.c + jni/utils/threads.c + jni/utils/memory.c + jni/deps/parson/parson.c +) include_directories( jni jni/deps jni/external/libunwindstack-ndk/include - ) +) target_include_directories(bugsnag-ndk PRIVATE ${BUGSNAG_DIR}/assets/include) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c new file mode 100644 index 0000000000..35abc12828 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c @@ -0,0 +1,188 @@ +// +// BSG_KSCrashStringConversion.c +// Bugsnag +// +// Created by Karl Stenerud on 31.05.22. +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#include "BSG_KSCrashStringConversion.h" +#include +#include + +// Max uint64 is 18446744073709551615 +#define MAX_UINT64_DIGITS 20 + +size_t bsg_uint64_to_string(uint64_t value, char* dst) { + if(value == 0) { + dst[0] = '0'; + dst[1] = 0; + return 1; + } + + char buff[MAX_UINT64_DIGITS+1]; + buff[sizeof(buff)-1] = 0; + size_t index = sizeof(buff) - 2; + for(;;) { + buff[index] = (value%10) + '0'; + value /= 10; + if (value == 0) { + break; + } + index--; + } + + size_t length = sizeof(buff) - index; + memcpy(dst, buff+index, length); + return length - 1; +} + +size_t bsg_int64_to_string(int64_t value, char* dst) { + if (value < 0) { + dst[0] = '-'; + return bsg_uint64_to_string((uint64_t)-value, dst+1) + 1; + } + return bsg_uint64_to_string((uint64_t)value, dst); +} + +static char g_hexNybbles[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + +size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits) { + if (min_digits < 1) { + min_digits = 1; + } else if (min_digits > 16) { + min_digits = 16; + } + + char buff[MAX_UINT64_DIGITS+1]; + buff[sizeof(buff)-1] = 0; + size_t index = sizeof(buff) - 2; + for(int digitCount = 1;; digitCount++) { + buff[index] = g_hexNybbles[(value&15)]; + value >>= 4; + if (value == 0 && digitCount >= min_digits) { + break; + } + index--; + } + + size_t length = sizeof(buff) - index; + memcpy(dst, buff+index, length); + return length - 1; +} + +/** + * Convert a positive double to a string, allowing up to max_sig_digits. + * To reduce the complexity of this algorithm, values with an exponent + * other than 0 are always printed in exponential form. + * + * Values are rounded half-up. + * + * This function makes use of compiler intrinsic functions which, though not + * officially async-safe, are actually async-safe (no allocations, locks, etc). + * + * This function will write a maximum of 21 characters (including the NUL) to dst. + * + * Returns the length of the string written to dst (not including the NUL). + */ +static size_t positive_double_to_string(const double value, char* dst, int max_sig_digits) { + const char* const orig_dst = dst; + if (max_sig_digits > 16) { + max_sig_digits = 16; + } + + if(value == 0) { + dst[0] = '0'; + dst[1] = 0; + return 1; + } + + // isnan() is basically ((x) != (x)) + if(isnan(value)) { + strlcpy(dst, "nan", 4); + return 3; + } + + // isinf() is a compiler intrinsic. + if(isinf(value)) { + strlcpy(dst, "inf", 4); + return 3; + } + + // log10() is a compiler intrinsic. + int exponent = (int)log10(value); + // Values < 1.0 must subtract 1 from exponent to handle zero wraparound. + if (value < 1.0) { + exponent--; + } + + // pow() is a compiler intrinsic. + double normalized = value / pow(10, exponent); + // Special case for 0.1, 0.01, 0.001, etc giving a normalized value of 10.xyz. + // We use 9.999... because 10.0 converts to a value > 10 in ieee754 binary floats. + if (normalized > 9.99999999999999822364316059975) { + exponent++; + normalized = value / pow(10, exponent); + } + + // Put all of the digits we'll use into an integer. + double digits_and_remainder = normalized * pow(10, max_sig_digits-1); + uint64_t digits = (uint64_t)digits_and_remainder; + // Also round up if necessary (note: 0.5 is exact in both binary and decimal). + if (digits_and_remainder - (double)digits >= 0.5) { + digits++; + // Special case: Adding one bumps us to next magnitude. + if (digits >= (uint64_t)pow(10, max_sig_digits)) { + exponent++; + digits /= 10; + } + } + + // Extract the fractional digits. + for (int i = max_sig_digits; i > 1; i--) { + dst[i] = digits % 10 + '0'; + digits /= 10; + } + // Extract the single-digit whole part. + dst[0] = (char)digits + '0'; + dst[1] = '.'; + + // Strip off trailing zeroes, and also the '.' if there is no fractional part. + int e_offset = max_sig_digits; + for (int i = max_sig_digits; i > 0; i--) { + if (dst[i] != '0') { + if (dst[i] == '.') { + e_offset = i; + } else { + e_offset = i + 1; + } + break; + } + } + dst += e_offset; + + // Add the exponent if it's not 0. + if (exponent != 0) { + *dst++ = 'e'; + if (exponent >= 0) { + *dst++ = '+'; + } + dst += bsg_int64_to_string(exponent, dst); + } else { + *dst = 0; + } + + return (size_t)(dst - orig_dst); +} + +size_t bsg_double_to_string(double value, char* dst, int max_sig_digits) { + if (max_sig_digits < 1) { + max_sig_digits = 1; + } + if (value < 0) { + dst[0] = '-'; + return positive_double_to_string(-value, dst+1, max_sig_digits) + 1; + } + return positive_double_to_string(value, dst, max_sig_digits); +} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h new file mode 100644 index 0000000000..bc522487d3 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h @@ -0,0 +1,73 @@ +// +// BSG_KSCrashStringConversion.h +// Bugsnag +// +// Created by Karl Stenerud on 31.05.22. +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#ifndef BSG_KSCrashStringConversion_h +#define BSG_KSCrashStringConversion_h + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Convert an unsigned integer to a string. + * This will write a maximum of 21 characters (including the NUL) to dst. + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_uint64_to_string(uint64_t value, char* dst); + +/** + * Convert an integer to a string. + * This will write a maximum of 22 characters (including the null terminator) to dst. + * + * Returns the length of the string written to dst (not including the null termination byte). + */ +size_t bsg_int64_to_string(int64_t value, char* dst); + +/** + * Convert an unsigned integer to a hex string. + * This will write a maximum of 17 characters (including the NUL) to dst. + * + * If min_digits is greater than 1, it will prepad with zeroes to reach this number of digits + * (up to a maximum of 16 digits). + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits); + +/** + * Convert a positive double to a string, allowing up to max_sig_digits. + * To reduce the complexity of this algorithm, values with an exponent + * other than 0 are always printed in exponential form. + * + * Values are rounded half-up, and thus will differ slightly from printf. + * + * This function makes use of compiler intrinsic functions which, though not + * officially async-safe, are actually async-safe (no allocations, locks, etc). + * + * Note: Double conversion is not intended to be round-trippable. + * It is 99.99% correct but has subtle differences from printf. + * + * This function will write a maximum of 22 characters (including the NUL) to dst. + * + * max_sig_digits is capped between 1 and 16 (inclusive) because that's the range + * of significant digits an ieee754 binary float64 can represent. + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_double_to_string(double value, char* dst, int max_sig_digits); + + +#ifdef __cplusplus +} +#endif + +#endif /* BSG_KSCrashStringConversion_h */ \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c new file mode 100644 index 0000000000..12e791e651 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c @@ -0,0 +1,550 @@ +// +// BSG_KSJSONCodec.c +// +// Created by Karl Stenerud on 2012-01-07. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include "BSG_KSJSONCodec.h" +#include "BSG_KSCrashStringConversion.h" + +#include +#include +#include +#include + +// ============================================================================ +#pragma mark - Configuration - +// ============================================================================ + +/** Set to 1 if you're also compiling BSG_KSLogger and want to use it here */ +#ifndef BSG_KSJSONCODEC_UseKSLogger +#define BSG_KSJSONCODEC_UseKSLogger 0 +#endif + +#if BSG_KSJSONCODEC_UseKSLogger +#include "BSG_KSLogger.h" +#else +#define BSG_KSLOG_ERROR(FMT, ...) +#endif + + +/** The work buffer size to use when escaping string values. + * There's little reason to change this since nothing ever gets truncated. + */ +#ifndef BSG_KSJSONCODEC_WorkBufferSize +#define BSG_KSJSONCODEC_WorkBufferSize 512 +#endif + +/** + * The maximum number of significant digits when printing floats. + * 7 (6 + 1 whole digit in exp form) is the default used by the old sprintf code. + */ +#define MAX_SIGNIFICANT_DIGITS 7 + +// ============================================================================ +#pragma mark - Helpers - +// ============================================================================ + +// Compiler hints for "if" statements +#define likely_if(x) if (__builtin_expect(x, 1)) +#define unlikely_if(x) if (__builtin_expect(x, 0)) + +/** Used for writing hex string values. */ +static char bsg_g_hexNybbles[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + +const char *bsg_ksjsonstringForError(const int error) { + switch (error) { + case BSG_KSJSON_ERROR_INVALID_CHARACTER: + return "Invalid character"; + case BSG_KSJSON_ERROR_CANNOT_ADD_DATA: + return "Cannot add data"; + case BSG_KSJSON_ERROR_INCOMPLETE: + return "Incomplete data"; + case BSG_KSJSON_ERROR_INVALID_DATA: + return "Invalid data"; + default: + return "(unknown error)"; + } +} + + +// ============================================================================ +#pragma mark - Encode - +// ============================================================================ + +// Avoiding static functions due to linker issues. + +/** Add JSON encoded data to an external handler. + * The external handler will decide how to handle the data (store/transmit/etc). + * + * @param context The encoding context. + * + * @param data The encoded data. + * + * @param length The length of the data. + * + * @return true if the data was handled successfully. + */ +#define addJSONData(CONTEXT, DATA, LENGTH) \ + (CONTEXT)->addJSONData(DATA, LENGTH, (CONTEXT)->userData) + +/** Escape a string portion for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_appendEscapedString( + BSG_KSJSONEncodeContext *const context, const char *restrict const string, + size_t length) { + char workBuffer[BSG_KSJSONCODEC_WorkBufferSize]; + const char *const srcEnd = string + length; + + const char *restrict src = string; + char *restrict dst = workBuffer; + + // Simple case (no escape or special characters) + for (; src < srcEnd && *src != '\\' && *src != '\"' && + (unsigned char)*src >= ' '; + src++) { + *dst++ = *src; + } + + // Deal with complicated case (if any) + int result; + for (; src < srcEnd; src++) { + + // If we add an escaped control character this may exceed the buffer by up to + // 6 characters: add this chunk now, reset the buffer and carry on + if (dst + 6 > workBuffer + BSG_KSJSONCODEC_WorkBufferSize) { + size_t encLength = (size_t)(dst - workBuffer); + unlikely_if((result = addJSONData(context, dst - encLength, encLength)) != + BSG_KSJSON_OK) { + return result; + } + dst = workBuffer; + } + + switch (*src) { + case '\\': + case '\"': + *dst++ = '\\'; + *dst++ = *src; + break; + case '\b': + *dst++ = '\\'; + *dst++ = 'b'; + break; + case '\f': + *dst++ = '\\'; + *dst++ = 'f'; + break; + case '\n': + *dst++ = '\\'; + *dst++ = 'n'; + break; + case '\r': + *dst++ = '\\'; + *dst++ = 'r'; + break; + case '\t': + *dst++ = '\\'; + *dst++ = 't'; + break; + default: + + // escape control chars (U+0000 - U+001F) + // see https://www.ietf.org/rfc/rfc4627.txt + + if ((unsigned char)*src < ' ') { + unsigned int last = (unsigned int)*src % 16; + unsigned int first = ((unsigned int)*src - last) / 16; + + *dst++ = '\\'; + *dst++ = 'u'; + *dst++ = '0'; + *dst++ = '0'; + *dst++ = bsg_g_hexNybbles[first]; + *dst++ = bsg_g_hexNybbles[last]; + } else { + *dst++ = *src; + } + break; + } + } + size_t encLength = (size_t)(dst - workBuffer); + return addJSONData(context, dst - encLength, encLength); +} + +/** Escape a string for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_addEscapedString(BSG_KSJSONEncodeContext *const context, + const char *restrict const string, + size_t length) { + int result = BSG_KSJSON_OK; + + // Keep adding portions until the whole string has been processed. + size_t offset = 0; + while (offset < length) { + size_t toAdd = length - offset; + unlikely_if(toAdd > BSG_KSJSONCODEC_WorkBufferSize) { + toAdd = BSG_KSJSONCODEC_WorkBufferSize; + } + result = bsg_ksjsoncodec_i_appendEscapedString(context, string + offset, + toAdd); + unlikely_if(result != BSG_KSJSON_OK) { break; } + offset += toAdd; + } + return result; +} + +/** Escape and quote a string for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_addQuotedEscapedString( + BSG_KSJSONEncodeContext *const context, const char *restrict const string, + size_t length) { + int result; + unlikely_if((result = addJSONData(context, "\"", 1)) != BSG_KSJSON_OK) { + return result; + } + unlikely_if((result = bsg_ksjsoncodec_i_addEscapedString( + context, string, length)) != BSG_KSJSON_OK) { + return result; + } + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = BSG_KSJSON_OK; + + // Decide if a comma is warranted. + unlikely_if(context->containerFirstEntry) { + context->containerFirstEntry = false; + } + else { + unlikely_if((result = addJSONData(context, ",", 1)) != BSG_KSJSON_OK) { + return result; + } + } + + // Pretty printing + unlikely_if(context->prettyPrint && context->containerLevel > 0) { + unlikely_if((result = addJSONData(context, "\n", 1)) != BSG_KSJSON_OK) { + return result; + } + for (int i = 0; i < context->containerLevel; i++) { + unlikely_if((result = addJSONData(context, " ", 4)) != + BSG_KSJSON_OK) { + return result; + } + } + } + + // Add a name field if we're in an object. + if (context->isObject[context->containerLevel]) { + unlikely_if(name == NULL) { + BSG_KSLOG_ERROR("Name was null inside an object"); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + unlikely_if((result = bsg_ksjsoncodec_i_addQuotedEscapedString( + context, name, strlen(name))) != BSG_KSJSON_OK) { + return result; + } + unlikely_if(context->prettyPrint) { + unlikely_if((result = addJSONData(context, ": ", 2)) != + BSG_KSJSON_OK) { + return result; + } + } + else { + unlikely_if((result = addJSONData(context, ":", 1)) != + BSG_KSJSON_OK) { + return result; + } + } + } + return result; +} + +int bsg_ksjsonaddRawJSONData(BSG_KSJSONEncodeContext *const context, + const char *const data, const size_t length) { + return addJSONData(context, data, length); +} + +int bsg_ksjsonaddBooleanElement(BSG_KSJSONEncodeContext *const context, + const char *const name, const bool value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + if (value) { + return addJSONData(context, "true", 4); + } else { + return addJSONData(context, "false", 5); + } +} + +int bsg_ksjsonaddFloatingPointElement(BSG_KSJSONEncodeContext *const context, + const char *const name, double value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + char buff[30]; + bsg_double_to_string(value, buff, MAX_SIGNIFICANT_DIGITS); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddIntegerElement(BSG_KSJSONEncodeContext *const context, + const char *const name, long long value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + char buff[30]; + bsg_int64_to_string(value, buff); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddUIntegerElement(BSG_KSJSONEncodeContext *const context, + const char *const name, + unsigned long long value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + char buff[30]; + bsg_uint64_to_string(value, buff); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddJSONElement(BSG_KSJSONEncodeContext *const context, + const char *restrict const name, + const char *restrict const element, + size_t length) { + unlikely_if(element == NULL) { + return bsg_ksjsonaddNullElement(context, name); + } + size_t idx = 0; + while (idx < length && (element[idx] == ' ' || element[idx] == '\r' || + element[idx] == '\n' || element[idx] == '\t' || + element[idx] == '\f')) { + idx++; + } + unlikely_if(idx >= length) { + BSG_KSLOG_ERROR("JSON element contained no JSON data: %s", element); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + switch (element[idx]) { + case '[': + case '{': + case '\"': + case 'f': + case 't': + case 'n': + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + default: + BSG_KSLOG_ERROR("Invalid character '%c' in: ", element[idx]); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, element, length); +} + +int bsg_ksjsonaddNullElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, "null", 4); +} + +int bsg_ksjsonaddStringElement(BSG_KSJSONEncodeContext *const context, + const char *const name, const char *const value, + size_t length) { + unlikely_if(value == NULL) { + return bsg_ksjsonaddNullElement(context, name); + } + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + if (length == BSG_KSJSON_SIZE_AUTOMATIC) { + length = strlen(value); + } + return bsg_ksjsoncodec_i_addQuotedEscapedString(context, value, length); +} + +int bsg_ksjsonbeginStringElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonappendStringElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length) { + return bsg_ksjsoncodec_i_addEscapedString(context, value, length); +} + +int bsg_ksjsonendStringElement(BSG_KSJSONEncodeContext *const context) { + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonaddDataElement(BSG_KSJSONEncodeContext *const context, + const char *name, const char *value, + size_t length) { + int result = BSG_KSJSON_OK; + result = bsg_ksjsonbeginDataElement(context, name); + if (result == BSG_KSJSON_OK) { + result = bsg_ksjsonappendDataElement(context, value, length); + } + if (result == BSG_KSJSON_OK) { + result = bsg_ksjsonendDataElement(context); + } + return result; +} + +int bsg_ksjsonbeginDataElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + return bsg_ksjsonbeginStringElement(context, name); +} + +int bsg_ksjsonappendDataElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length) { + const unsigned char *currentByte = (const unsigned char *)value; + const unsigned char *end = currentByte + length; + char chars[2]; + int result = BSG_KSJSON_OK; + while (currentByte < end) { + chars[0] = bsg_g_hexNybbles[(*currentByte >> 4) & 15]; + chars[1] = bsg_g_hexNybbles[*currentByte & 15]; + result = addJSONData(context, chars, sizeof(chars)); + if (result != BSG_KSJSON_OK) { + break; + } + currentByte++; + } + return result; +} + +int bsg_ksjsonendDataElement(BSG_KSJSONEncodeContext *const context) { + return bsg_ksjsonendStringElement(context); +} + +int bsg_ksjsonbeginArray(BSG_KSJSONEncodeContext *const context, + const char *const name) { + likely_if(context->containerLevel >= 0) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + } + + context->containerLevel++; + context->isObject[context->containerLevel] = false; + context->containerFirstEntry = true; + + return addJSONData(context, "[", 1); +} + +int bsg_ksjsonbeginObject(BSG_KSJSONEncodeContext *const context, + const char *const name) { + likely_if(context->containerLevel >= 0) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + } + + context->containerLevel++; + context->isObject[context->containerLevel] = true; + context->containerFirstEntry = true; + + return addJSONData(context, "{", 1); +} + +int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *const context) { + unlikely_if(context->containerLevel <= 0) { return BSG_KSJSON_OK; } + + bool isObject = context->isObject[context->containerLevel]; + context->containerLevel--; + + // Pretty printing + unlikely_if(context->prettyPrint && !context->containerFirstEntry) { + int result; + unlikely_if((result = addJSONData(context, "\n", 1)) != BSG_KSJSON_OK) { + return result; + } + for (int i = 0; i < context->containerLevel; i++) { + unlikely_if((result = addJSONData(context, " ", 4)) != + BSG_KSJSON_OK) { + return result; + } + } + } + context->containerFirstEntry = false; + return addJSONData(context, isObject ? "}" : "]", 1); +} + +void bsg_ksjsonbeginEncode(BSG_KSJSONEncodeContext *const context, + bool prettyPrint, + BSG_KSJSONAddDataFunc addJSONDataFunc, + void *const userData) { + memset(context, 0, sizeof(*context)); + context->addJSONData = addJSONDataFunc; + context->userData = userData; + context->prettyPrint = prettyPrint; + context->containerFirstEntry = true; +} + +int bsg_ksjsonendEncode(BSG_KSJSONEncodeContext *const context) { + int result = BSG_KSJSON_OK; + while (context->containerLevel > 0) { + unlikely_if((result = bsg_ksjsonendContainer(context)) != + BSG_KSJSON_OK) { + return result; + } + } + return result; +} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h new file mode 100644 index 0000000000..529a7c625b --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h @@ -0,0 +1,479 @@ +// +// BSG_KSJSONCodec.h +// +// Created by Karl Stenerud on 2012-01-07. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/* Reads and writes JSON encoded data. + */ + +#ifndef HDR_BSG_KSJSONCodec_h +#define HDR_BSG_KSJSONCodec_h + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Tells the encoder to automatically determine the length of a field value. + * Currently, this is done using strlen(). + */ +#define BSG_KSJSON_SIZE_AUTOMATIC ((size_t)~0) + +enum { + /** Encoding or decoding: Everything completed without error */ + BSG_KSJSON_OK = 0, + + /** Encoding or decoding: Encountered an unexpected or invalid character */ + BSG_KSJSON_ERROR_INVALID_CHARACTER = 1, + + /** Encoding: addJSONData could not handle the data. + * This code is not used by the decoder, but is meant to be returned by + * the addJSONData callback method if it couldn't handle the data. + */ + BSG_KSJSON_ERROR_CANNOT_ADD_DATA = 2, + + /** Decoding: Source data appears to be truncated. */ + BSG_KSJSON_ERROR_INCOMPLETE = 3, + + /** Decoding: Parsing failed due to bad data structure/type/contents. + * This code is not used by the decoder, but is meant to be returned + * by the user callback methods if the decoded data is incorrect for + * semantic or structural reasons. + */ + BSG_KSJSON_ERROR_INVALID_DATA = 4, +}; + +/** Get a description for an error code. + * + * @param error The error code. + * + * @return A string describing the error. + */ +const char *bsg_ksjsonstringForError(const int error); + +// ============================================================================ +// Encode +// ============================================================================ + +/** Function pointer for adding more UTF-8 encoded JSON data. + * + * @param data The UTF-8 data to add. + * + * @param length The length of the data. + * + * @param userData user-specified contextual data. + * + * @return BSG_KSJSON_OK if the data was handled. + * otherwise BSG_KSJSON_ERROR_CANNOT_ADD_DATA. + */ +typedef int (*BSG_KSJSONAddDataFunc)(const char *data, size_t length, + void *userData); + +typedef struct { + /** Function to call to add more encoded JSON data. */ + BSG_KSJSONAddDataFunc addJSONData; + + /** User-specified data */ + void *userData; + + /** How many containers deep we are. */ + int containerLevel; + + /** Whether or not the current container is an object. */ + bool isObject[200]; + + /** true if this is the first entry at the current container level. */ + bool containerFirstEntry; + + bool prettyPrint; + +} BSG_KSJSONEncodeContext; + +/** Begin a new encoding process. + * + * @param context The encoding context. + * + * @param prettyPrint If true, insert whitespace to make the output pretty. + * + * @param addJSONData Function to handle adding data. + * + * @param userData User-specified data which gets passed to addJSONData. + */ +void bsg_ksjsonbeginEncode(BSG_KSJSONEncodeContext *context, bool prettyPrint, + BSG_KSJSONAddDataFunc addJSONData, void *userData); + +/** End the encoding process, ending any remaining open containers. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendEncode(BSG_KSJSONEncodeContext *context); + +/** Add a boolean element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddBooleanElement(BSG_KSJSONEncodeContext *context, + const char *name, bool value); + +/** Add an integer element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddIntegerElement(BSG_KSJSONEncodeContext *context, + const char *name, long long value); + +/** Add an unsigned integer element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddUIntegerElement(BSG_KSJSONEncodeContext *context, + const char *name, unsigned long long value); + +/** Add a floating point element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddFloatingPointElement(BSG_KSJSONEncodeContext *context, + const char *name, double value); + +/** Add a null element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddNullElement(BSG_KSJSONEncodeContext *context, + const char *name); + +/** Add a string element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param length the length of the string, or BSG_KSJSON_SIZE_AUTOMATIC. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddStringElement(BSG_KSJSONEncodeContext *context, + const char *name, const char *value, + size_t length); + +/** Start an incrementally-built string element. + * + * Use this for constructing very large strings. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginStringElement(BSG_KSJSONEncodeContext *context, + const char *name); + +/** Add a string fragment to an incrementally-built string element. + * + * @param context The encoding context. + * + * @param value The string fragment. + * + * @param length the length of the string fragment. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonappendStringElement(BSG_KSJSONEncodeContext *context, + const char *value, size_t length); + +/** End an incrementally-built string element. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendStringElement(BSG_KSJSONEncodeContext *context); + +/** Add a string element. The element will be converted to string-coded hex. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param length the length of the data. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddDataElement(BSG_KSJSONEncodeContext *const context, + const char *name, const char *value, + size_t length); + +/** Start an incrementally-built data element. The element will be converted + * to string-coded hex. + * + * Use this for constructing very large data elements. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginDataElement(BSG_KSJSONEncodeContext *const context, + const char *const name); + +/** Add a data fragment to an incrementally-built data element. + * + * @param context The encoding context. + * + * @param value The data fragment. + * + * @param length the length of the data fragment. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonappendDataElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length); + +/** End an incrementally-built data element. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendDataElement(BSG_KSJSONEncodeContext *const context); + +/** Add a pre-formatted JSON element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. MUST BE VALID JSON! + * + * @param length The length of the element. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddJSONElement(BSG_KSJSONEncodeContext *context, + const char *restrict name, + const char *restrict value, size_t length); + +/** Begin a new object container. + * + * @param context The encoding context. + * + * @param name The object's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginObject(BSG_KSJSONEncodeContext *context, const char *name); + +/** Begin a new array container. + * + * @param context The encoding context. + * + * @param name The array's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginArray(BSG_KSJSONEncodeContext *context, const char *name); + +/** Begin a generic JSON element, adding any necessary JSON preamble text, + * including commas and names. + * Note: This does not add any object or array specifiers ('{', '['). + * + * @param context The JSON context. + * + * @param name The name of the next element (only needed if parent is a + * dictionary). + */ +int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, + const char *const name); + +/** Add JSON data manually. + * This function just passes your data directly through, even if it's malforned. + * + * @param context The encoding context. + * + * @param data The data to write. + * + * @param length The length of the data. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddRawJSONData(BSG_KSJSONEncodeContext *const context, + const char *const data, const size_t length); + +/** End the current container and return to the next higher level. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *context); + +// ============================================================================ +// Decode +// ============================================================================ + +/** + * Callbacks called during a JSON decode process. + * All function pointers must point to valid functions. + */ +typedef struct BSG_KSJSONDecodeCallbacks { + /** Called when a boolean element is decoded. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onBooleanElement)(const char *name, bool value, void *userData); + + /** Called when a floating point element is decoded. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onFloatingPointElement)(const char *name, double value, + void *userData); + + /** Called when an integer element is decoded. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onIntegerElement)(const char *name, long long value, void *userData); + + /** Called when a null element is decoded. + * + * @param name The element's name. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onNullElement)(const char *name, void *userData); + + /** Called when a string element is decoded. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onStringElement)(const char *name, const char *value, void *userData); + + /** Called when a new object is encountered. + * + * @param name The object's name. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onBeginObject)(const char *name, void *userData); + + /** Called when a new array is encountered. + * + * @param name The array's name. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onBeginArray)(const char *name, void *userData); + + /** Called when leaving the current container and returning to the next + * higher level container. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onEndContainer)(void *userData); + + /** Called when the end of the input data is reached. + * + * @param userData Data that was specified when calling bsg_ksjsondecode(). + * + * @return BSG_KSJSON_OK if decoding should continue. + */ + int (*onEndData)(void *userData); + +} BSG_KSJSONDecodeCallbacks; + +#ifdef __cplusplus +} +#endif + +#endif // HDR_KSJSONCodec_h \ No newline at end of file From 6c59e8f5dad2abae53a2d6f9167b2da329f81ae0 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 2 May 2024 16:32:31 +0100 Subject: [PATCH 04/28] refactor(ndk): add simplified ISO 8601 time formatting to BSG_KSCrashStringConversion --- .../serializer/BSG_KSCrashStringConversion.c | 145 ++++++++++++++---- .../serializer/BSG_KSCrashStringConversion.h | 34 ++-- .../jni/utils/serializer/BSG_KSJSONCodec.c | 40 ++--- .../jni/utils/serializer/BSG_KSJSONCodec.h | 117 +------------- 4 files changed, 162 insertions(+), 174 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c index 35abc12828..e727ec3407 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c @@ -7,24 +7,25 @@ // #include "BSG_KSCrashStringConversion.h" -#include #include +#include +#include // Max uint64 is 18446744073709551615 #define MAX_UINT64_DIGITS 20 -size_t bsg_uint64_to_string(uint64_t value, char* dst) { - if(value == 0) { +size_t bsg_uint64_to_string(uint64_t value, char *dst) { + if (value == 0) { dst[0] = '0'; dst[1] = 0; return 1; } - char buff[MAX_UINT64_DIGITS+1]; - buff[sizeof(buff)-1] = 0; + char buff[MAX_UINT64_DIGITS + 1]; + buff[sizeof(buff) - 1] = 0; size_t index = sizeof(buff) - 2; - for(;;) { - buff[index] = (value%10) + '0'; + for (;;) { + buff[index] = (value % 10) + '0'; value /= 10; if (value == 0) { break; @@ -33,14 +34,14 @@ size_t bsg_uint64_to_string(uint64_t value, char* dst) { } size_t length = sizeof(buff) - index; - memcpy(dst, buff+index, length); + memcpy(dst, buff + index, length); return length - 1; } -size_t bsg_int64_to_string(int64_t value, char* dst) { +size_t bsg_int64_to_string(int64_t value, char *dst) { if (value < 0) { dst[0] = '-'; - return bsg_uint64_to_string((uint64_t)-value, dst+1) + 1; + return bsg_uint64_to_string((uint64_t)-value, dst + 1) + 1; } return bsg_uint64_to_string((uint64_t)value, dst); } @@ -48,18 +49,18 @@ size_t bsg_int64_to_string(int64_t value, char* dst) { static char g_hexNybbles[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; -size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits) { +size_t bsg_uint64_to_hex(uint64_t value, char *dst, int min_digits) { if (min_digits < 1) { min_digits = 1; } else if (min_digits > 16) { min_digits = 16; } - char buff[MAX_UINT64_DIGITS+1]; - buff[sizeof(buff)-1] = 0; + char buff[MAX_UINT64_DIGITS + 1]; + buff[sizeof(buff) - 1] = 0; size_t index = sizeof(buff) - 2; - for(int digitCount = 1;; digitCount++) { - buff[index] = g_hexNybbles[(value&15)]; + for (int digitCount = 1;; digitCount++) { + buff[index] = g_hexNybbles[(value & 15)]; value >>= 4; if (value == 0 && digitCount >= min_digits) { break; @@ -68,7 +69,7 @@ size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits) { } size_t length = sizeof(buff) - index; - memcpy(dst, buff+index, length); + memcpy(dst, buff + index, length); return length - 1; } @@ -82,30 +83,32 @@ size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits) { * This function makes use of compiler intrinsic functions which, though not * officially async-safe, are actually async-safe (no allocations, locks, etc). * - * This function will write a maximum of 21 characters (including the NUL) to dst. + * This function will write a maximum of 21 characters (including the NUL) to + * dst. * * Returns the length of the string written to dst (not including the NUL). */ -static size_t positive_double_to_string(const double value, char* dst, int max_sig_digits) { - const char* const orig_dst = dst; +static size_t positive_double_to_string(const double value, char *dst, + int max_sig_digits) { + const char *const orig_dst = dst; if (max_sig_digits > 16) { max_sig_digits = 16; } - if(value == 0) { + if (value == 0) { dst[0] = '0'; dst[1] = 0; return 1; } // isnan() is basically ((x) != (x)) - if(isnan(value)) { + if (isnan(value)) { strlcpy(dst, "nan", 4); return 3; } // isinf() is a compiler intrinsic. - if(isinf(value)) { + if (isinf(value)) { strlcpy(dst, "inf", 4); return 3; } @@ -120,14 +123,15 @@ static size_t positive_double_to_string(const double value, char* dst, int max_s // pow() is a compiler intrinsic. double normalized = value / pow(10, exponent); // Special case for 0.1, 0.01, 0.001, etc giving a normalized value of 10.xyz. - // We use 9.999... because 10.0 converts to a value > 10 in ieee754 binary floats. + // We use 9.999... because 10.0 converts to a value > 10 in ieee754 binary + // floats. if (normalized > 9.99999999999999822364316059975) { exponent++; normalized = value / pow(10, exponent); } // Put all of the digits we'll use into an integer. - double digits_and_remainder = normalized * pow(10, max_sig_digits-1); + double digits_and_remainder = normalized * pow(10, max_sig_digits - 1); uint64_t digits = (uint64_t)digits_and_remainder; // Also round up if necessary (note: 0.5 is exact in both binary and decimal). if (digits_and_remainder - (double)digits >= 0.5) { @@ -176,13 +180,100 @@ static size_t positive_double_to_string(const double value, char* dst, int max_s return (size_t)(dst - orig_dst); } -size_t bsg_double_to_string(double value, char* dst, int max_sig_digits) { +size_t bsg_double_to_string(double value, char *dst, int max_sig_digits) { if (max_sig_digits < 1) { max_sig_digits = 1; } if (value < 0) { dst[0] = '-'; - return positive_double_to_string(-value, dst+1, max_sig_digits) + 1; + return positive_double_to_string(-value, dst + 1, max_sig_digits) + 1; } return positive_double_to_string(value, dst, max_sig_digits); -} \ No newline at end of file +} + +// async-safe gmtime_r implementation +static void safe_gmtime_r(time_t time, struct tm *out) { + const int seconds_per_day = 86400; + const int days_per_400years = 365 * 400 + 97; + const int days_per_100years = 365 * 100 + 24; + const int days_in_month_leap_year[] = {31, 29, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + const int days_in_month_non_leap_year[] = {31, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + const int days_per_4years = 365 * 4 + 1; + + int days = time / seconds_per_day; + int secs = time % seconds_per_day; + int years = 1970; + + int quotient = days / days_per_400years; + days -= quotient * days_per_400years; + years += quotient * 400; + + quotient = days / days_per_100years; + days -= quotient * days_per_100years; + years += quotient * 100; + + quotient = days / days_per_4years; + days -= quotient * days_per_4years; + years += quotient * 4; + + quotient = days / 365; + days -= quotient * 365; + years += quotient; + + out->tm_year = years - 1900; + out->tm_yday = days; + + const int *days_per_month = + out->tm_year % 4 == 0 + ? (out->tm_year % 100 == 0 + ? (out->tm_year % 400 == 0 ? days_in_month_leap_year + : days_in_month_non_leap_year) + : days_in_month_leap_year) + : days_in_month_non_leap_year; + + int month = 0; + while (days >= days_per_month[month]) { + days -= days_per_month[month]; + month++; + } + + out->tm_mon = month; + out->tm_mday = days + 1; + + out->tm_hour = secs / 3600; + secs %= 3600; + out->tm_min = secs / 60; + out->tm_sec = secs % 60; +} + +size_t bsg_time_to_simplified_iso8601_string(time_t time, char *dst) { + struct tm tm; + safe_gmtime_r(time, &tm); + size_t length = 0; + length += bsg_uint64_to_string(tm.tm_year + 1900, dst + length); + dst[length++] = '-'; + if (tm.tm_mon + 1 < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_mon + 1, dst + length); + dst[length++] = '-'; + if (tm.tm_mday < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_mday, dst + length); + dst[length++] = 'T'; + if (tm.tm_hour < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_hour, dst + length); + dst[length++] = ':'; + if (tm.tm_min < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_min, dst + length); + dst[length++] = ':'; + if (tm.tm_sec < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_sec, dst + length); + dst[length++] = 'Z'; + dst[length] = '\0'; + return length; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h index bc522487d3..36db4ce8b6 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h @@ -11,6 +11,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -22,26 +23,28 @@ extern "C" { * * Returns the length of the string written to dst (not including the NUL). */ -size_t bsg_uint64_to_string(uint64_t value, char* dst); +size_t bsg_uint64_to_string(uint64_t value, char *dst); /** * Convert an integer to a string. - * This will write a maximum of 22 characters (including the null terminator) to dst. + * This will write a maximum of 22 characters (including the null terminator) to + * dst. * - * Returns the length of the string written to dst (not including the null termination byte). + * Returns the length of the string written to dst (not including the null + * termination byte). */ -size_t bsg_int64_to_string(int64_t value, char* dst); +size_t bsg_int64_to_string(int64_t value, char *dst); /** * Convert an unsigned integer to a hex string. * This will write a maximum of 17 characters (including the NUL) to dst. * - * If min_digits is greater than 1, it will prepad with zeroes to reach this number of digits - * (up to a maximum of 16 digits). + * If min_digits is greater than 1, it will prepad with zeroes to reach this + * number of digits (up to a maximum of 16 digits). * * Returns the length of the string written to dst (not including the NUL). */ -size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits); +size_t bsg_uint64_to_hex(uint64_t value, char *dst, int min_digits); /** * Convert a positive double to a string, allowing up to max_sig_digits. @@ -56,15 +59,24 @@ size_t bsg_uint64_to_hex(uint64_t value, char* dst, int min_digits); * Note: Double conversion is not intended to be round-trippable. * It is 99.99% correct but has subtle differences from printf. * - * This function will write a maximum of 22 characters (including the NUL) to dst. + * This function will write a maximum of 22 characters (including the NUL) to + * dst. * - * max_sig_digits is capped between 1 and 16 (inclusive) because that's the range - * of significant digits an ieee754 binary float64 can represent. + * max_sig_digits is capped between 1 and 16 (inclusive) because that's the + * range of significant digits an ieee754 binary float64 can represent. * * Returns the length of the string written to dst (not including the NUL). */ -size_t bsg_double_to_string(double value, char* dst, int max_sig_digits); +size_t bsg_double_to_string(double value, char *dst, int max_sig_digits); +/** + * Convert a time to a simplified ISO8601 date string. No timezone information + * is added, and the output date is always in UTC. + * + * Returns the length of the string written to dst (not including the null + * termination byte). + */ +size_t bsg_time_to_simplified_iso8601_string(time_t time, char *dst); #ifdef __cplusplus } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c index 12e791e651..a543178136 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c @@ -47,7 +47,6 @@ #define BSG_KSLOG_ERROR(FMT, ...) #endif - /** The work buffer size to use when escaping string values. * There's little reason to change this since nothing ever gets truncated. */ @@ -57,7 +56,8 @@ /** * The maximum number of significant digits when printing floats. - * 7 (6 + 1 whole digit in exp form) is the default used by the old sprintf code. + * 7 (6 + 1 whole digit in exp form) is the default used by the old sprintf + * code. */ #define MAX_SIGNIFICANT_DIGITS 7 @@ -88,7 +88,6 @@ const char *bsg_ksjsonstringForError(const int error) { } } - // ============================================================================ #pragma mark - Encode - // ============================================================================ @@ -107,7 +106,7 @@ const char *bsg_ksjsonstringForError(const int error) { * @return true if the data was handled successfully. */ #define addJSONData(CONTEXT, DATA, LENGTH) \ - (CONTEXT)->addJSONData(DATA, LENGTH, (CONTEXT)->userData) + (CONTEXT)->addJSONData(DATA, LENGTH, (CONTEXT)->userData) /** Escape a string portion for use with JSON and send to data handler. * @@ -139,8 +138,8 @@ int bsg_ksjsoncodec_i_appendEscapedString( int result; for (; src < srcEnd; src++) { - // If we add an escaped control character this may exceed the buffer by up to - // 6 characters: add this chunk now, reset the buffer and carry on + // If we add an escaped control character this may exceed the buffer by up + // to 6 characters: add this chunk now, reset the buffer and carry on if (dst + 6 > workBuffer + BSG_KSJSONCODEC_WorkBufferSize) { size_t encLength = (size_t)(dst - workBuffer); unlikely_if((result = addJSONData(context, dst - encLength, encLength)) != @@ -223,8 +222,8 @@ int bsg_ksjsoncodec_i_addEscapedString(BSG_KSJSONEncodeContext *const context, unlikely_if(toAdd > BSG_KSJSONCODEC_WorkBufferSize) { toAdd = BSG_KSJSONCODEC_WorkBufferSize; } - result = bsg_ksjsoncodec_i_appendEscapedString(context, string + offset, - toAdd); + result = + bsg_ksjsoncodec_i_appendEscapedString(context, string + offset, toAdd); unlikely_if(result != BSG_KSJSON_OK) { break; } offset += toAdd; } @@ -275,8 +274,7 @@ int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, return result; } for (int i = 0; i < context->containerLevel; i++) { - unlikely_if((result = addJSONData(context, " ", 4)) != - BSG_KSJSON_OK) { + unlikely_if((result = addJSONData(context, " ", 4)) != BSG_KSJSON_OK) { return result; } } @@ -293,14 +291,12 @@ int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, return result; } unlikely_if(context->prettyPrint) { - unlikely_if((result = addJSONData(context, ": ", 2)) != - BSG_KSJSON_OK) { + unlikely_if((result = addJSONData(context, ": ", 2)) != BSG_KSJSON_OK) { return result; } } else { - unlikely_if((result = addJSONData(context, ":", 1)) != - BSG_KSJSON_OK) { + unlikely_if((result = addJSONData(context, ":", 1)) != BSG_KSJSON_OK) { return result; } } @@ -360,9 +356,9 @@ int bsg_ksjsonaddJSONElement(BSG_KSJSONEncodeContext *const context, return bsg_ksjsonaddNullElement(context, name); } size_t idx = 0; - while (idx < length && (element[idx] == ' ' || element[idx] == '\r' || - element[idx] == '\n' || element[idx] == '\t' || - element[idx] == '\f')) { + while (idx < length && + (element[idx] == ' ' || element[idx] == '\r' || element[idx] == '\n' || + element[idx] == '\t' || element[idx] == '\f')) { idx++; } unlikely_if(idx >= length) { @@ -408,9 +404,7 @@ int bsg_ksjsonaddNullElement(BSG_KSJSONEncodeContext *const context, int bsg_ksjsonaddStringElement(BSG_KSJSONEncodeContext *const context, const char *const name, const char *const value, size_t length) { - unlikely_if(value == NULL) { - return bsg_ksjsonaddNullElement(context, name); - } + unlikely_if(value == NULL) { return bsg_ksjsonaddNullElement(context, name); } int result = bsg_ksjsonbeginElement(context, name); unlikely_if(result != BSG_KSJSON_OK) { return result; } if (length == BSG_KSJSON_SIZE_AUTOMATIC) { @@ -517,8 +511,7 @@ int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *const context) { return result; } for (int i = 0; i < context->containerLevel; i++) { - unlikely_if((result = addJSONData(context, " ", 4)) != - BSG_KSJSON_OK) { + unlikely_if((result = addJSONData(context, " ", 4)) != BSG_KSJSON_OK) { return result; } } @@ -541,8 +534,7 @@ void bsg_ksjsonbeginEncode(BSG_KSJSONEncodeContext *const context, int bsg_ksjsonendEncode(BSG_KSJSONEncodeContext *const context) { int result = BSG_KSJSON_OK; while (context->containerLevel > 0) { - unlikely_if((result = bsg_ksjsonendContainer(context)) != - BSG_KSJSON_OK) { + unlikely_if((result = bsg_ksjsonendContainer(context)) != BSG_KSJSON_OK) { return result; } } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h index 529a7c625b..a0a9a635c0 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h @@ -50,8 +50,8 @@ enum { BSG_KSJSON_ERROR_INVALID_CHARACTER = 1, /** Encoding: addJSONData could not handle the data. - * This code is not used by the decoder, but is meant to be returned by - * the addJSONData callback method if it couldn't handle the data. + * This code is not used by the decoder, but is meant to be returned by + * the addJSONData callback method if it couldn't handle the data. */ BSG_KSJSON_ERROR_CANNOT_ADD_DATA = 2, @@ -59,9 +59,9 @@ enum { BSG_KSJSON_ERROR_INCOMPLETE = 3, /** Decoding: Parsing failed due to bad data structure/type/contents. - * This code is not used by the decoder, but is meant to be returned - * by the user callback methods if the decoded data is incorrect for - * semantic or structural reasons. + * This code is not used by the decoder, but is meant to be returned + * by the user callback methods if the decoded data is incorrect for + * semantic or structural reasons. */ BSG_KSJSON_ERROR_INVALID_DATA = 4, }; @@ -365,113 +365,6 @@ int bsg_ksjsonaddRawJSONData(BSG_KSJSONEncodeContext *const context, */ int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *context); -// ============================================================================ -// Decode -// ============================================================================ - -/** - * Callbacks called during a JSON decode process. - * All function pointers must point to valid functions. - */ -typedef struct BSG_KSJSONDecodeCallbacks { - /** Called when a boolean element is decoded. - * - * @param name The element's name. - * - * @param value The element's value. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onBooleanElement)(const char *name, bool value, void *userData); - - /** Called when a floating point element is decoded. - * - * @param name The element's name. - * - * @param value The element's value. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onFloatingPointElement)(const char *name, double value, - void *userData); - - /** Called when an integer element is decoded. - * - * @param name The element's name. - * - * @param value The element's value. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onIntegerElement)(const char *name, long long value, void *userData); - - /** Called when a null element is decoded. - * - * @param name The element's name. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onNullElement)(const char *name, void *userData); - - /** Called when a string element is decoded. - * - * @param name The element's name. - * - * @param value The element's value. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onStringElement)(const char *name, const char *value, void *userData); - - /** Called when a new object is encountered. - * - * @param name The object's name. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onBeginObject)(const char *name, void *userData); - - /** Called when a new array is encountered. - * - * @param name The array's name. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onBeginArray)(const char *name, void *userData); - - /** Called when leaving the current container and returning to the next - * higher level container. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onEndContainer)(void *userData); - - /** Called when the end of the input data is reached. - * - * @param userData Data that was specified when calling bsg_ksjsondecode(). - * - * @return BSG_KSJSON_OK if decoding should continue. - */ - int (*onEndData)(void *userData); - -} BSG_KSJSONDecodeCallbacks; - #ifdef __cplusplus } #endif From 7681abf525bcddc49326451e5f1b9448d03b9cb3 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 2 May 2024 17:42:14 +0100 Subject: [PATCH 05/28] refactor(ndk): rewrote the event_writer.c to output JSON instead of a binary dump --- .../main/jni/utils/serializer/event_writer.c | 668 +++++++++++++++--- 1 file changed, 581 insertions(+), 87 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index df787e479d..61f0c8475f 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -4,154 +4,648 @@ #include #include -#include "../string.h" +#include "BSG_KSCrashStringConversion.h" +#include "BSG_KSJSONCodec.h" #include "buffered_writer.h" -#include "utils/seqlock.h" +#include "event.h" +#include "internal_metrics.h" -bool bsg_write_breadcrumbs(bugsnag_event *event, bsg_buffered_writer *writer); -bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer); +#define CHECKED(e) \ + if ((e) != BSG_KSJSON_OK) { \ + goto error; \ + } + +#define JSON_CONSTANT_ELEMENT(name, value) \ + bsg_ksjsonaddStringElement(json, name, value, \ + sizeof(value) - 1 /* remove terminator */) +#define JSON_LIMITED_STRING_ELEMENT(name, value) \ + bsg_ksjsonaddStringElement(json, (name), (value), \ + strnlen((value), sizeof((value)))) -bool bsg_write_opaque_metadata(bugsnag_event *event, - bsg_buffered_writer *writer); +#define STRING_NOT_EMPTY(s) (*(s) != 0) -bool bsg_write_breadcrumbs(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)); +static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, + bugsnag_metadata *metadata); +static bool bsg_write_severity_reason(BSG_KSJSONEncodeContext *json, + bugsnag_event *event); +static bool bsg_write_user(BSG_KSJSONEncodeContext *json, bugsnag_user *user); +static bool bsg_write_error(BSG_KSJSONEncodeContext *json, bsg_error *error); +static bool bsg_write_app(BSG_KSJSONEncodeContext *json, bsg_app_info *app); +static bool bsg_write_device(BSG_KSJSONEncodeContext *json, + bsg_device_info *device); +static bool bsg_write_breadcrumbs(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumbs, + int first_crumb_index, int crumb_count, + int max_crumb_count); +static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, + bsg_thread *threads, int thread_count); +static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, + bsg_feature_flag *flags, int flag_count); +static bool bsg_write_session(BSG_KSJSONEncodeContext *json, + bugsnag_event *event); +static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, + bugsnag_event *event); - return len == sizeof(bsg_report_header); +static int bsg_write(const char *data, size_t length, void *userData) { + bsg_buffered_writer *writer = userData; + return writer->write(writer, data, length) ? BSG_KSJSON_OK + : BSG_KSJSON_ERROR_CANNOT_ADD_DATA; } bool bsg_event_write(bsg_environment *env) { bsg_buffered_writer writer; + BSG_KSJSONEncodeContext jsonContext; + bugsnag_event *event = &env->next_event; + BSG_KSJSONEncodeContext *json = &jsonContext; + 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 the breadcrumbs after the event structure - bsg_write_breadcrumbs(&env->next_event, &writer) && - // append feature flags - bsg_write_feature_flags(&env->next_event, &writer) && - // append opaque metadata after the feature flags - bsg_write_opaque_metadata(&env->next_event, &writer); + bsg_ksjsonbeginEncode(json, false, &bsg_write, &writer); + + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(bsg_ksjsonaddStringElement( + json, "context", event->context, + strnlen(event->context, sizeof(event->context)))); + + if (!bsg_write_metadata(json, &event->metadata)) { + goto error; + } + if (!bsg_write_severity_reason(json, event)) { + goto error; + } + // Write the exception / error info + if (!bsg_write_error(json, &event->error)) { + goto error; + } + + // Write user info + if (!bsg_write_user(json, &event->user)) { + goto error; + } + + // Write diagnostics + if (!bsg_write_app(json, &event->app)) { + goto error; + } + if (!bsg_write_device(json, &event->device)) { + goto error; + } + if (!bsg_write_breadcrumbs(json, event->breadcrumbs, + event->crumb_first_index, event->crumb_count, + event->max_crumb_count)) { + goto error; + } + if (STRING_NOT_EMPTY(event->grouping_hash)) { + CHECKED( + JSON_LIMITED_STRING_ELEMENT("groupingHash", event->grouping_hash)); + } + if (!bsg_write_usage(json, event)) { + goto error; + } + if (!bsg_write_threads(json, event->threads, event->thread_count)) { + goto error; + } + if (!bsg_write_feature_flags(json, event->feature_flags, + event->feature_flag_count)) { + goto error; + } + if (!bsg_write_session(json, event)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + bsg_ksjsonendEncode(json); writer.dispose(&writer); + return true; +error: + writer.dispose(&writer); + return false; +} + +static bool bsg_write_metadata_value(BSG_KSJSONEncodeContext *json, + bsg_metadata_value *value) { + switch (value->type) { + case BSG_METADATA_BOOL_VALUE: + CHECKED(bsg_ksjsonaddBooleanElement(json, value->name, value->bool_value)); + break; + case BSG_METADATA_CHAR_VALUE: + CHECKED(JSON_LIMITED_STRING_ELEMENT(value->name, value->char_value)); + break; + case BSG_METADATA_NUMBER_VALUE: + CHECKED(bsg_ksjsonaddFloatingPointElement(json, value->name, + value->double_value)); + break; + case BSG_METADATA_OPAQUE_VALUE: + CHECKED(bsg_ksjsonbeginElement(json, value->name)); + CHECKED(bsg_ksjsonaddRawJSONData(json, value->opaque_value, + value->opaque_value_size)); + break; + default: + break; + } + + return true; +error: + return false; +} + +static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, + bugsnag_metadata *metadata) { + const int value_count = metadata->value_count; + // The metadata values can appear in any order and each entry records which + // "section" it should appear in. We need to group the values by section and + // write them out in that order. This array keeps track of which values have + // already been written, and each time we write a section we mark the values + // as written. + // When running through the array of metadata, we have an inner-loop to write + // all of the entries in the same "section" (a bit like a bubble-sort, if that + // helps explain it). + bool written[value_count]; - const char *static_json_data = atomic_load(&env->static_json_data); - if (result && static_json_data != NULL) { - // Attempt to write the static data, but don't worry if it fails. - // We'll check for truncated/missing static data on load. - if (bsg_buffered_writer_open(&writer, env->next_event_static_data_path)) { - writer.write(&writer, static_json_data, strlen(static_json_data)); - writer.dispose(&writer); + CHECKED(bsg_ksjsonbeginObject(json, "metaData")); + memset(written, 0, sizeof(bool) * value_count); + for (int i = 0; i < value_count; i++) { + bsg_metadata_value *value = &metadata->values[i]; + if (written[i] || value->type == BSG_METADATA_NONE_VALUE) { + continue; } + + CHECKED(bsg_ksjsonbeginObject(json, value->section)); + if (!bsg_write_metadata_value(json, value)) { + goto error; + } + + // we flush all of the values within this section, marking each one as + // written + const char *section = value->section; + for (int j = i + 1; j < value_count; j++) { + bsg_metadata_value *value2 = &metadata->values[j]; + if (written[j] || value2->type == BSG_METADATA_NONE_VALUE) { + continue; + } + + if (strncmp(section, value2->section, sizeof(value->section)) == 0) { + if (!bsg_write_metadata_value(json, value2)) { + goto error; + } + + // remember that we have already written this element + written[j] = true; + } + } + CHECKED(bsg_ksjsonendContainer(json)); } + CHECKED(bsg_ksjsonendContainer(json)); - return result; + return true; +error: + return false; } -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; +static bool bsg_write_severity(BSG_KSJSONEncodeContext *json, + bugsnag_severity severity) { + + switch (severity) { + case BSG_SEVERITY_ERR: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "error")); + break; + case BSG_SEVERITY_WARN: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "warning")); + break; + case BSG_SEVERITY_INFO: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "info")); + break; + } + return true; +error: + return false; +} + +static bool bsg_write_severity_reason(BSG_KSJSONEncodeContext *json, + bugsnag_event *event) { + + if (!bsg_write_severity(json, event->severity)) { + goto error; } + CHECKED(bsg_ksjsonaddBooleanElement(json, "unhandled", event->unhandled)); - int size = bsg_strlen(env->next_last_run_info); - ssize_t len = write(fd, env->next_last_run_info, size); - return len == size; + CHECKED(bsg_ksjsonbeginObject(json, "severityReason")); + { + // unhandled == false always means that the state has been overridden by the + // user, as this codepath is only executed for unhandled native errors + CHECKED(bsg_ksjsonaddBooleanElement(json, "unhandledOverridden", + !event->unhandled)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "signal")); + + bsg_error *error = &event->error; + CHECKED(bsg_ksjsonbeginObject(json, "attributes")); + { CHECKED(JSON_LIMITED_STRING_ELEMENT("signalType", error->errorClass)); } + CHECKED(bsg_ksjsonendContainer(json)); + } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; } -static bool write_feature_flag(bsg_buffered_writer *writer, - bsg_feature_flag *flag) { - if (!writer->write_string(writer, flag->name)) { - return false; +static bool bsg_write_stackframe(BSG_KSJSONEncodeContext *json, + bugsnag_stackframe *frame, bool isPC) { + + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + // we use a single buffer for all of the hex encoded strings + // the bsg_uint64_to_hex doesn't prefix the '0x' so we pre-place that in the + // buffer, and then overwrite the rest of the buffer + char hex_str[2 /* '0x' */ + 20 /* number */ + 1 /* NULL */] = "0x"; + char *hex_output_buffer = &hex_str[2]; + bsg_uint64_to_hex(frame->frame_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("frameAddress", hex_str)); + + bsg_uint64_to_hex(frame->symbol_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("symbolAddress", hex_str)); + + bsg_uint64_to_hex(frame->load_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("loadAddress", hex_str)); + + CHECKED( + bsg_ksjsonaddUIntegerElement(json, "lineNumber", frame->line_number)); + + if (isPC) { + CHECKED(bsg_ksjsonaddBooleanElement(json, "isPC", true)); + } + + if (STRING_NOT_EMPTY(frame->filename)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("file", frame->filename)); + } + + if (STRING_NOT_EMPTY(frame->method)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("method", frame->method)); + } else { + bsg_uint64_to_hex(frame->symbol_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("method", hex_str)); + } + + if (STRING_NOT_EMPTY(frame->code_identifier)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("codeIdentifier", + frame->code_identifier)); + } } + CHECKED(bsg_ksjsonendContainer(json)); - if (flag->variant) { - if (!writer->write_byte(writer, 1)) { - return false; + return true; +error: + return false; +} + +static bool bsg_write_stacktrace(BSG_KSJSONEncodeContext *json, + bugsnag_stackframe *stacktrace, + size_t frame_count) { + + for (int findex = 0; findex < frame_count; findex++) { + if (!bsg_write_stackframe(json, &stacktrace[findex], findex == 0)) { + goto error; } + } - if (!writer->write_string(writer, flag->variant)) { - return false; + return true; +error: + return false; +} + +static bool bsg_write_error(BSG_KSJSONEncodeContext *json, bsg_error *error) { + CHECKED(bsg_ksjsonbeginArray(json, "exceptions")); + { + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("errorClass", error->errorClass)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("message", error->errorMessage)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); + + const ssize_t frame_count = error->frame_count; + // 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 (frame_count > 0) { + CHECKED(bsg_ksjsonbeginArray(json, "stacktrace")); + { + if (!bsg_write_stacktrace(json, error->stacktrace, frame_count)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + } } - } else { - if (!writer->write_byte(writer, 0)) { - return false; + CHECKED(bsg_ksjsonendContainer(json)); + } + CHECKED(bsg_ksjsonendContainer(json)); + + return true; +error: + return false; +} + +static bool bsg_write_user(BSG_KSJSONEncodeContext *json, bugsnag_user *user) { + const bool has_id = STRING_NOT_EMPTY(user->id); + const bool has_name = STRING_NOT_EMPTY(user->name); + const bool has_email = STRING_NOT_EMPTY(user->email); + + const bool has_user = has_id || has_name || has_email; + if (has_user) { + CHECKED(bsg_ksjsonbeginObject(json, "user")); + { + if (has_id) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", user->id)); + } + + if (has_name) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", user->name)); + } + + if (has_email) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("email", user->email)); + } } + CHECKED(bsg_ksjsonendContainer(json)); } + return true; +error: + return false; +} +static bool bsg_write_app(BSG_KSJSONEncodeContext *json, bsg_app_info *app) { + CHECKED(bsg_ksjsonbeginObject(json, "app")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("version", app->version)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", app->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("type", app->type)); + + CHECKED(JSON_LIMITED_STRING_ELEMENT("releaseStage", app->release_stage)); + CHECKED( + bsg_ksjsonaddIntegerElement(json, "versionCode", app->version_code)); + if (STRING_NOT_EMPTY(app->build_uuid)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("buildUUID", app->build_uuid)); + } + + CHECKED(JSON_LIMITED_STRING_ELEMENT("binaryArch", app->binary_arch)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "duration", app->duration)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "durationInForeground", + app->duration_in_foreground)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "inForeground", app->in_foreground)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "isLaunching", app->is_launching)); + } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; } -bool bsg_write_breadcrumbs(bugsnag_event *event, bsg_buffered_writer *writer) { - return writer->write(writer, event->breadcrumbs, - sizeof(bugsnag_breadcrumb) * event->max_crumb_count); +static bool bsg_write_device(BSG_KSJSONEncodeContext *json, + bsg_device_info *device) { + + CHECKED(bsg_ksjsonbeginObject(json, "device")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("osName", device->os_name)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", device->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("locale", device->locale)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("osVersion", device->os_version)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("manufacturer", device->manufacturer)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("model", device->model)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("orientation", device->orientation)); + + CHECKED(bsg_ksjsonbeginObject(json, "runtimeVersions")); + { + CHECKED(bsg_ksjsonaddIntegerElement(json, "androidApiLevel", + device->api_level)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("osBuild", device->os_build)); + } + CHECKED(bsg_ksjsonendContainer(json)); + + CHECKED(bsg_ksjsonbeginArray(json, "cpuAbi")); + { + const int cpu_api_count = device->cpu_abi_count; + for (int i = 0; i < cpu_api_count; i++) { + CHECKED(JSON_LIMITED_STRING_ELEMENT(NULL, device->cpu_abi[i].value)); + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + CHECKED(bsg_ksjsonaddUIntegerElement(json, "totalMemory", + device->total_memory)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "jailbroken", device->jailbroken)); + + if (device->time > 0) { + char buffer[sizeof "2018-10-08T12:07:09Z"]; + bsg_time_to_simplified_iso8601_string(device->time, buffer); + CHECKED(JSON_LIMITED_STRING_ELEMENT("time", buffer)); + } + } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; } -extern bsg_seqlock_t bsg_feature_flag_lock; +static bool bsg_write_breadcrumb_type(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb_type type) { -bool bsg_write_feature_flags(bugsnag_event *event, - bsg_buffered_writer *writer) { - bsg_seqlock_status_t lock_status = - bsg_seqlock_optimistic_read(&bsg_feature_flag_lock); - const uint32_t feature_flag_count = event->feature_flag_count; - if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { - return false; + switch (type) { + case BSG_CRUMB_ERROR: + CHECKED(JSON_CONSTANT_ELEMENT("type", "error")); + break; + case BSG_CRUMB_LOG: + CHECKED(JSON_CONSTANT_ELEMENT("type", "log")); + break; + case BSG_CRUMB_MANUAL: + CHECKED(JSON_CONSTANT_ELEMENT("type", "manual")); + break; + case BSG_CRUMB_NAVIGATION: + CHECKED(JSON_CONSTANT_ELEMENT("type", "navigation")); + break; + case BSG_CRUMB_PROCESS: + CHECKED(JSON_CONSTANT_ELEMENT("type", "process")); + break; + case BSG_CRUMB_REQUEST: + CHECKED(JSON_CONSTANT_ELEMENT("type", "request")); + break; + case BSG_CRUMB_STATE: + CHECKED(JSON_CONSTANT_ELEMENT("type", "state")); + break; + case BSG_CRUMB_USER: + CHECKED(JSON_CONSTANT_ELEMENT("type", "user")); + break; + } + return true; +error: + return false; +} + +static bool bsg_write_breadcrumb(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumb) { + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("timestamp", breadcrumb->timestamp)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", breadcrumb->name)); + if (!bsg_write_breadcrumb_type(json, breadcrumb->type)) { + goto error; + } + if (!bsg_write_metadata(json, &breadcrumb->metadata)) { + goto error; + } } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} + +static bool bsg_write_breadcrumbs(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumbs, + int first_crumb_index, int crumb_count, + int max_crumb_count) { - for (uint32_t index = 0; index < feature_flag_count; index++) { - if (!write_feature_flag(writer, &event->feature_flags[index])) { - return false; + CHECKED(bsg_ksjsonbeginArray(json, "breadcrumbs")); + { + for (int i = 0; i < crumb_count; i++) { + int crumb_index = i % max_crumb_count; + if (!bsg_write_breadcrumb(json, &breadcrumbs[crumb_index])) { + goto error; + } } } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} +static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, + bsg_thread *threads, int thread_count) { + + if (thread_count == 0) { + return true; + } - // write a single byte to indicate whether the feature flags were modified - // when writing them out, 0 indicates "valid", anything else indicates - // "invalid" - writer->write_byte( - writer, - bsg_seqlock_validate(&bsg_feature_flag_lock, lock_status) ? 0 : 1); + CHECKED(bsg_ksjsonbeginArray(json, "threads")); + { + for (int i = 0; i < thread_count; i++) { + bsg_thread *thread = &threads[i]; + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(bsg_ksjsonaddUIntegerElement(json, "id", thread->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", thread->name)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("state", thread->state)); + } + CHECKED(bsg_ksjsonendContainer(json)); + } + } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; } -static bool bsg_write_opaque_metadata_unit(bugsnag_metadata *metadata, - bsg_buffered_writer *writer) { +static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, + bsg_feature_flag *flags, int flag_count) { + if (flag_count == 0) { + return true; + } - for (size_t index = 0; index < metadata->value_count; index++) { - uint32_t value_size = metadata->values[index].opaque_value_size; - if (metadata->values[index].type == BSG_METADATA_OPAQUE_VALUE && - value_size > 0) { - if (!writer->write(writer, metadata->values[index].opaque_value, - value_size)) { - return false; + CHECKED(bsg_ksjsonbeginArray(json, "featureFlags")); + { + for (int i = 0; i < flag_count; i++) { + bsg_feature_flag *flag = &flags[i]; + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(bsg_ksjsonaddStringElement(json, "featureFlag", flag->name, + strlen(flag->name))); + if (flag->variant != NULL) { + CHECKED(bsg_ksjsonaddStringElement(json, "variant", flag->variant, + strlen(flag->variant))); + } } + CHECKED(bsg_ksjsonendContainer(json)); } } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; } -bool bsg_write_opaque_metadata(bugsnag_event *event, - bsg_buffered_writer *writer) { +static bool bsg_write_session(BSG_KSJSONEncodeContext *json, + bugsnag_event *event) { + CHECKED(bsg_ksjsonbeginObject(json, "session")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", event->session_id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("startedAt", event->session_start)); - if (!bsg_write_opaque_metadata_unit(&event->metadata, writer)) { - return false; + CHECKED(bsg_ksjsonbeginObject(json, "events")); + { + CHECKED( + bsg_ksjsonaddIntegerElement(json, "handled", event->handled_events)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "unhandled", + event->unhandled_events)); + } + CHECKED(bsg_ksjsonendContainer(json)); } + CHECKED(bsg_ksjsonendContainer(json)); - for (int breadcrumb_index = 0; breadcrumb_index < event->crumb_count; - breadcrumb_index++) { - if (!bsg_write_opaque_metadata_unit( - &event->breadcrumbs[breadcrumb_index].metadata, writer)) { - return false; + return true; +error: + return false; +} + +static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, + bugsnag_event *event) { + CHECKED(bsg_ksjsonbeginObject(json, "usage")); + { + CHECKED(bsg_ksjsonbeginObject(json, "callbacks")); + { + static const int callbacks_count = sizeof(event->set_callback_counts) / + sizeof(*event->set_callback_counts); + + for (int i = 0; i < callbacks_count; i++) { + if (event->set_callback_counts[i].count > 0) { + CHECKED(bsg_ksjsonaddIntegerElement( + json, event->set_callback_counts[i].name, + event->set_callback_counts[i].count)); + } + } + + for (int i = 0; i < bsg_called_apis_count; i++) { + if (bsg_was_api_called(event, i)) { + CHECKED( + bsg_ksjsonaddBooleanElement(json, bsg_called_api_names[i], true)); + } + } } + CHECKED(bsg_ksjsonendContainer(json)); } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; +} + +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 = strlen(env->next_last_run_info); + ssize_t len = write(fd, env->next_last_run_info, size); + return len == size; } From 481b0de272bd85015f912e36a61e8880caf7b587 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 9 May 2024 08:57:12 +0100 Subject: [PATCH 06/28] refactor(ndk): removed old binary-dump event logic & added a new test for the JSON serializer --- .../java/com/bugsnag/android/FileStore.kt | 2 +- .../com/bugsnag/android/NativeInterface.java | 10 +- .../bugsnag/android/NativeInterfaceApiTest.kt | 7 +- .../api/bugsnag-plugin-android-ndk.api | 3 +- .../detekt-baseline.xml | 2 +- .../ndk/AppMetadataSerializationTest.kt | 23 - .../android/ndk/AppSerializationTest.kt | 23 - .../ndk/BreadcrumbStateSerializationTest.kt | 23 - .../android/ndk/ContextSerializationTest.kt | 23 - .../ndk/CustomMetadataSerializationTest.kt | 23 - .../android/ndk/DeviceSerializationTest.kt | 23 - .../android/ndk/ExceptionSerializationTest.kt | 23 - .../android/ndk/NativeJsonSerializeTest.kt | 23 +- .../android/ndk/NativeStructToFileTest.kt | 20 - .../android/ndk/SessionSerializationTest.kt | 23 - .../ndk/SeverityReasonSerializationTest.kt | 23 - .../ndk/StackframeSerializationTest.kt | 23 - .../android/ndk/ThreadSerializationTest.kt | 23 - .../android/ndk/UserSerializationTest.kt | 23 - .../ndk/migrations/EventMigrationTest.kt | 45 -- .../ndk/migrations/EventOnDiskTests.kt | 224 -------- .../app_meta_data_serialization.json | 1 - .../resources/app_serialization.json | 1 - .../resources/breadcrumbs_serialization.json | 1 - .../resources/context_serialization.json | 1 - .../custom_meta_data_serialization.json | 1 - .../resources/device_serialization.json | 1 - .../resources/event_serialization.json | 1 + .../resources/exception_serialization.json | 1 - .../resources/session_serialization.json | 1 - .../severity_reason_serialization.json | 1 - .../resources/stackframe_serialization.json | 1 - .../resources/thread_serialization.json | 1 - .../resources/user_serialization.json | 1 - .../src/main/CMakeLists.txt | 2 - .../com/bugsnag/android/ndk/NativeBridge.kt | 32 +- .../src/main/jni/bugsnag_ndk.c | 118 +---- .../src/main/jni/bugsnag_ndk.h | 7 +- .../src/main/jni/utils/serializer.c | 10 - .../src/main/jni/utils/serializer.h | 2 - .../main/jni/utils/serializer/event_reader.c | 298 ----------- .../main/jni/utils/serializer/event_reader.h | 27 - .../main/jni/utils/serializer/event_writer.c | 55 +- .../main/jni/utils/serializer/event_writer.h | 3 + .../main/jni/utils/serializer/json_writer.c | 489 ------------------ .../main/jni/utils/serializer/json_writer.h | 36 -- .../src/main/jni/utils/string.c | 7 +- .../src/main/jni/utils/string.h | 2 +- .../src/test/CMakeLists.txt | 2 +- .../src/test/cpp/main.c | 227 ++------ .../test/cpp/migrations/EventOnDiskTests.cpp | 201 ------- .../src/test/cpp/migrations/utils.hpp | 90 ---- .../src/test/cpp/test_breadcrumbs.c | 1 - .../src/test/cpp/test_bsg_event.c | 3 +- .../src/test/cpp/test_bsg_event.h | 8 + .../src/test/cpp/test_utils_serialize.c | 385 +------------- 56 files changed, 172 insertions(+), 2458 deletions(-) delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json delete mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c delete mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h delete mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c delete mode 100644 bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h delete mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp delete mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp create mode 100644 bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt index 1e0ba13731..235ba699bf 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt @@ -14,7 +14,7 @@ import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock internal abstract class FileStore( - private val storageDir: File, + val storageDir: File, private val maxStoreCount: Int, private val comparator: Comparator, protected open val logger: Logger, 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 6f622fdf27..64e59d4161 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 @@ -81,15 +81,7 @@ public static String getContext() { */ @NonNull public static File getNativeReportPath() { - return getNativeReportPath(getPersistenceDirectory()); - } - - private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) { - return new File(persistenceDirectory, "bugsnag/native"); - } - - private static @NonNull File getPersistenceDirectory() { - return getClient().getConfig().getPersistenceDirectory().getValue(); + return getClient().getEventStore().getStorageDir(); } /** 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 dce80a6b79..f768c9176f 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 @@ -7,6 +7,7 @@ import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -21,7 +22,6 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner -import java.io.File import java.nio.file.Files /** @@ -87,10 +87,9 @@ internal class NativeInterfaceApiTest { @Test fun getNativeReportPathPersistenceDirectory() { val customDir = Files.createTempDirectory("custom").toFile() - `when`(immutableConfig.persistenceDirectory).thenReturn(lazy { customDir }) + `when`(eventStore.storageDir).thenReturn(customDir) val observed = NativeInterface.getNativeReportPath() - val expected = File(customDir, "bugsnag/native") - assertEquals(expected, observed) + assertSame(customDir, observed) } @Test diff --git a/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api b/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api index cdc5a7496c..4b7eea278f 100644 --- a/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api +++ b/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api @@ -16,12 +16,11 @@ public final class com/bugsnag/android/ndk/NativeBridge : com/bugsnag/android/in public final fun clearFeatureFlag (Ljava/lang/String;)V public final fun clearFeatureFlags ()V public final fun clearMetadataTab (Ljava/lang/String;)V - public final fun deliverReportAtPath (Ljava/lang/String;)V public final fun getCurrentCallbackSetCounts ()Ljava/util/Map; public final fun getCurrentNativeApiCallUsage ()Ljava/util/Map; public final fun getSignalUnwindStackFunction ()J public final fun initCallbackCounts (Ljava/util/Map;)V - public final fun install (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZIZII)V + public final fun install (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZIZII)V public final fun notifyAddCallback (Ljava/lang/String;)V public final fun notifyRemoveCallback (Ljava/lang/String;)V public fun onStateChange (Lcom/bugsnag/android/StateEvent;)V diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 57df3f72f2..ce2169a8d2 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -4,7 +4,7 @@ CyclomaticComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) LongMethod:EventOnDiskTests.kt$EventOnDiskTests$@Test fun testEvent() - LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int, maxBreadcrumbs: Int, ) + LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, eventUUID: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int, maxBreadcrumbs: Int, ) NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver UseCheckOrError:ResourceUtils.kt$throw IllegalStateException("Failed to read JSON from $resourceName") diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt deleted file mode 100644 index 2d050023b9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class AppMetadataSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("app_meta_data_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt deleted file mode 100644 index bb67499cd2..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class AppSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("app_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt deleted file mode 100644 index aa9de489ea..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class BreadcrumbStateSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testBreadcrumbSerialization() { - val expected = loadJson("breadcrumbs_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt deleted file mode 100644 index 969f25e7b6..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class ContextSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("context_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt deleted file mode 100644 index 0d2c5d95f5..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class CustomMetadataSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("custom_meta_data_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt deleted file mode 100644 index 2953883f00..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class DeviceSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("device_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt deleted file mode 100644 index c187ac0f34..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class ExceptionSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("exception_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt index 652db32832..f866a97837 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt @@ -1,6 +1,10 @@ package com.bugsnag.android.ndk +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Test +import java.io.File class NativeJsonSerializeTest { @@ -11,10 +15,25 @@ class NativeJsonSerializeTest { } } - external fun run(): Int + private val path = File(System.getProperty("java.io.tmpdir"), this::class.simpleName!!) + + @Before + fun setupTmpdir() { + path.mkdirs() + } + + @After + fun deleteTmpdir() { + path.deleteRecursively() + } + + external fun run(outputDir: String): Int @Test fun testPassesNativeSuite() { - verifyNativeRun(run()) + verifyNativeRun(run(path.absolutePath)) + val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!! + val expected = loadJson("event_serialization.json") + assertEquals(expected, jsonFile.readText()) } } diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt deleted file mode 100644 index d2fe5fdfa7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Test - -class NativeStructToFileTest { - - 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/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt deleted file mode 100644 index d21934e9d9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class SessionSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("session_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt deleted file mode 100644 index 4fedf3c166..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class SeverityReasonSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("severity_reason_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt deleted file mode 100644 index 783942452a..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class StackframeSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("stackframe_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt deleted file mode 100644 index ca690e464d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert -import org.junit.Test - -class ThreadSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("thread_serialization.json") - val json = run() - Assert.assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt deleted file mode 100644 index 844e6873b7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class UserSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("user_serialization.json") - val json = run() - assertEquals(expected, json) - } -} 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 deleted file mode 100644 index 006e5dfab9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.bugsnag.android.ndk.migrations - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -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 json = DslJson>() - - @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 deserialize(file.readBytes()) - } - - 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 { - 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/EventOnDiskTests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt deleted file mode 100644 index 87f49fe60d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt +++ /dev/null @@ -1,224 +0,0 @@ -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 v12 added telemetry data */ -class EventOnDiskTests : EventMigrationTest() { - - @Test - /** check notifier and api key, since they aren't included in event JSON */ - fun testPayloadInfo() { - val infoFile = createTempFile() - - val info = generatePayloadInfo(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 testEvent() { - val eventFile = createTempFile() - - generateAndStoreEvent(eventFile.absolutePath) - assertNotEquals(0, eventFile.length()) - - val output = parseJSON(eventFile) - - assertEquals( - "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", - output["context"] - ) - assertEquals( - "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", - output["groupingHash"] - ) - assertEquals("info", output["severity"]) - - // app - assertEquals( - mapOf( - "binaryArch" to "mips", - "buildUUID" to "1234-9876-adfe", - "duration" to 81395165021L, - "durationInForeground" to 81395165010L, - "id" to "com.example.PhotoSnapPlus", - "inForeground" to true, - "isLaunching" to true, - "releaseStage" to "リリース", - "type" to "red", - "version" to "2.0.52", - "versionCode" to 8139512718L - ), - output["app"] - ) - - // breadcrumbs - val crumbs = output["breadcrumbs"] - if (crumbs is List) { - assertEquals(50, crumbs.size) - crumbs.forEachIndexed { index, crumb -> - assertEquals( - mapOf( - "type" to "state", - "name" to "mission $index", - "timestamp" to "2021-12-08T19:43:50.014Z", - "metaData" to mapOf( - "message" to "Now we know what they mean by 'advanced' tactical training." - ) - ), - crumb - ) - } - } else { - fail("breadcrumbs is not a list of crumb objects?!") - } - - // device - assertEquals( - mapOf( - "cpuAbi" to listOf("mipsx"), - "id" to "ffffa", - "locale" to "en_AU#Melbun", - "jailbroken" to true, - "manufacturer" to "HI-TEC™", - "model" to "🍨", - "orientation" to "sideup", - "osName" to "BOX BOX", - "osVersion" to "98.7", - "runtimeVersions" to mapOf( - "osBuild" to "beta1-2", - "androidApiLevel" to "32" - ), - "time" to "2021-12-08T19:43:50Z", - "totalMemory" to 3839512576L - ), - output["device"] - ) - - // feature flags - assertEquals( - listOf( - mapOf( - "featureFlag" to "bluebutton", - "variant" to "on" - ), - mapOf( - "featureFlag" to "redbutton", - "variant" to "off" - ), - mapOf("featureFlag" to "nobutton"), - mapOf( - "featureFlag" to "switch", - "variant" to "left" - ) - ), - output["featureFlags"] - ) - - // exceptions - assertEquals( - listOf( - mapOf( - "errorClass" to "SIGBUS", - "message" to "POSIX is serious about oncoming traffic", - "type" to "c", - "stacktrace" to listOf( - mapOf( - "frameAddress" to "0xfffffffe", - "lineNumber" to 4194967233L, - "loadAddress" to "0x242023", - "symbolAddress" to "0x308", - "method" to "makinBacon", - "file" to "lib64/libfoo.so", - "isPC" to true - ), - mapOf( - "frameAddress" to "0xb37a644b", - "lineNumber" to 0L, - "loadAddress" to "0x0", - "symbolAddress" to "0x0", - "method" to "0xb37a644b" // test address to method hex - ) - ) - ) - ), - output["exceptions"] - ) - - // metadata - assertEquals( - mapOf( - "app" to mapOf( - "activeScreen" to "Menu", - "weather" to "rain" - ), - "metrics" to mapOf( - "experimentX" to false, - "subject" to "percy", - "counter" to 47.5.toBigDecimal() - ) - ), - output["metaData"] - ) - - // session info - assertEquals( - mapOf( - "id" to "aaaaaaaaaaaaaaaa", - "startedAt" to "2031-07-09T11:08:21+00:00", - "events" to mapOf( - "handled" to 5L, - "unhandled" to 2L - ) - ), - output["session"] - ) - - // threads - val threads = output["threads"] - if (threads is List) { - assertEquals(8, threads.size) - threads.forEachIndexed { index, thread -> - assertEquals( - mapOf( - "name" to "Thread #$index", - "state" to "paused-$index", - "id" to (1000L + index).toString(), - "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"] - ) - } - - /** Generate an event to the latest format, writing JSON to tempFilePath */ - external fun generateAndStoreEvent(tempFilePath: String) - - /** Generate notifier and apiKey info to a bespoke structure (apiKey and - * notifier are not included in event info written to disk) */ - external fun generatePayloadInfo(tempFilePath: String): String -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json deleted file mode 100644 index 779fbb2d83..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"metaData":{"app":{"activeScreen":"MainActivity"}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json deleted file mode 100644 index e7a69b0d5d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"app":{"version":"22","id":"com.bugsnag.example","type":"android","releaseStage":"prod","versionCode":55,"buildUUID":"1234-uuid","binaryArch":"x86","duration":6502,"durationInForeground":6502,"inForeground":true,"isLaunching":true}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json deleted file mode 100644 index 5605a4987f..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"Jane","timestamp":"2018-10-08T12:07:09Z","type":"user","metaData":{"str":"Foo"}},{"name":"Something went wrong","timestamp":"2018-10-08T12:07:11Z","type":"manual","metaData":{"bool":true}},{"name":"MainActivity","timestamp":"2018-10-08T12:07:15.563Z","type":"navigation"},{"name":"Updated store","timestamp":"2018-10-08T12:07:16Z","type":"state"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json deleted file mode 100644 index f503716fb7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"context":"CustomContext"} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json deleted file mode 100644 index 7000565f31..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"metaData":{"custom":{"str":"Foo","bool":true,"num":55}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json deleted file mode 100644 index 91c48f9f9e..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"device":{"osName":"android","id":"f5gh7","locale":"En","osVersion":"8.1","manufacturer":"Samsung","model":"S7","orientation":"portrait","runtimeVersions":{"androidApiLevel":"29","osBuild":"BullDog 5.2"},"cpuAbi":["x86"],"totalMemory":512340922,"jailbroken":true,"time":"2029-01-01T00:00:00Z"}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json new file mode 100644 index 0000000000..ec3576f15a --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json @@ -0,0 +1 @@ +{"context":"Foo","metaData":{},"severity":"info","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGSEGV"}},"exceptions":[{"errorClass":"SIGSEGV","message":"Whoops!","type":"c","stacktrace":[{"frameAddress":"0x0","symbolAddress":"0x0","loadAddress":"0x0","lineNumber":58,"isPC":true,"file":"Something.c","method":"foo()"}]}],"user":{"id":"123","name":"Bob Bobbiton","email":"bob@example.com"},"app":{"version":"1.0","id":"fa02","type":"C","releaseStage":"dev","versionCode":55,"buildUUID":"123","binaryArch":"x86","duration":9019,"durationInForeground":7017,"inForeground":true,"isLaunching":true},"device":{"osName":"android","id":"my-id-123","locale":"en","osVersion":"9.1","manufacturer":"Google","model":"Nexus","orientation":"portrait","runtimeVersions":{"androidApiLevel":0,"osBuild":""},"cpuAbi":[],"totalMemory":1095092340,"jailbroken":true,"time":"1970-01-01T02:06:49Z"},"breadcrumbs":[],"groupingHash":"Bar","usage":{"callbacks":{}},"session":{"id":"","startedAt":"","events":{"handled":0,"unhandled":0}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json deleted file mode 100644 index 4bb42ad10e..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"stacktrace":[{"frameAddress":"0x20000000","symbolAddress":"0x16000000","loadAddress":"0x12000000","lineNumber":52,"isPC":true,"file":"foo.c","method":"bar()"}],"errorClass":"signal","message":"whoops something went wrong","type":"c"} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json deleted file mode 100644 index 57da6eac70..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"session":{"startedAt":"2018-10-08T12:07:09Z","id":"123","events":{"handled":2,"unhandled":1}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json deleted file mode 100644 index 18142a7369..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"severity":"error","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGABRT"}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json deleted file mode 100644 index f62a8babb8..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"frameAddress":"0x20000000","symbolAddress":"0x16000000","loadAddress":"0x12000000","lineNumber":52,"file":"foo.c","method":"bar()"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json deleted file mode 100644 index 81c37a66c1..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"1234","name":"Binder 1","state":"Running","type":"c"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json deleted file mode 100644 index 5527107eb3..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"user":{"name":"Fenton","email":"fenton@io.example.com","id":"1234"}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 0e47e07a8b..959c790255 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -18,9 +18,7 @@ add_library( # Specifies the name of the library. 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/serializer/json_writer.c jni/utils/serializer/BSG_KSJSONCodec.c jni/utils/serializer/BSG_KSCrashStringConversion.c jni/utils/stack_unwinder.cpp 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 33115f355c..89bca859a5 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 @@ -8,7 +8,6 @@ import com.bugsnag.android.StateEvent.AddBreadcrumb import com.bugsnag.android.StateEvent.AddMetadata import com.bugsnag.android.StateEvent.ClearMetadataSection import com.bugsnag.android.StateEvent.ClearMetadataValue -import com.bugsnag.android.StateEvent.DeliverPending import com.bugsnag.android.StateEvent.Install import com.bugsnag.android.StateEvent.NotifyHandled import com.bugsnag.android.StateEvent.NotifyUnhandled @@ -22,7 +21,6 @@ import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.StateObserver import com.bugsnag.android.internal.TaskType import java.io.File -import java.io.FileFilter import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock @@ -48,6 +46,7 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse apiKey: String, reportingDirectory: String, lastRunInfoPath: String, + eventUUID: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, @@ -63,7 +62,6 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse unhandledCount: Int ) - external fun deliverReportAtPath(filePath: String) fun addBreadcrumb(name: String, type: String, timestamp: String, metadata: Any) { val breadcrumbType = BreadcrumbType.values() .find { it.toString() == type } @@ -109,7 +107,6 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse when (event) { is Install -> handleInstallMessage(event) - DeliverPending -> deliverPendingReports() is AddMetadata -> handleAddMetadata(event) is ClearMetadataSection -> clearMetadataTab(event.section) is ClearMetadataValue -> removeMetadata( @@ -183,39 +180,16 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse return false } - private fun deliverPendingReports() { - val filenameRegex = """.*\.crash$""".toRegex() - lock.lock() - try { - val outDir = reportDirectory - if (outDir.exists()) { - val fileList = - outDir.listFiles(FileFilter { filenameRegex.containsMatchIn(it.name) }) - if (fileList != null) { - for (file in fileList) { - deliverReportAtPath(file.absolutePath) - } - } - } else { - logger.w("Payload directory does not exist, cannot read pending reports") - } - } catch (ex: Exception) { - logger.w("Failed to parse/write pending reports: $ex") - } finally { - lock.unlock() - } - } - private fun handleInstallMessage(arg: Install) { lock.withLock { if (installed.get()) { logger.w("Received duplicate setup message with arg: $arg") } else { - val reportPath = File(reportDirectory, "${UUID.randomUUID()}.crash").absolutePath install( arg.apiKey, - reportPath, + reportDirectory.absolutePath, arg.lastRunInfoPath, + UUID.randomUUID().toString(), arg.consecutiveLaunchCrashes, arg.autoDetectNdkCrashes, Build.VERSION.SDK_INT, 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 8b06c9b17d..eb0abe2beb 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -16,7 +16,6 @@ #include "metadata.h" #include "safejni.h" #include "utils/serializer.h" -#include "utils/serializer/event_reader.h" #include "utils/string.h" #ifdef __cplusplus @@ -148,9 +147,10 @@ void bsg_update_next_run_info(bsg_environment *env) { JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( JNIEnv *env, jobject _this, jstring _api_key, jstring _event_path, - jstring _last_run_info_path, jint consecutive_launch_crashes, - jboolean auto_detect_ndk_crashes, jint _api_level, jboolean is32bit, - jint send_threads, jint max_breadcrumbs) { + jstring _last_run_info_path, jstring _event_uuid, + jint consecutive_launch_crashes, jboolean auto_detect_ndk_crashes, + jint _api_level, jboolean is32bit, jint send_threads, + jint max_breadcrumbs) { if (!bsg_jni_cache_init(env)) { BUGSNAG_LOG("Could not init JNI jni_cache."); @@ -178,11 +178,19 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( if (event_path == NULL) { goto error; } - sprintf(bugsnag_env->next_event_path, "%s", event_path); - sprintf(bugsnag_env->next_event_static_data_path, "%s%s", event_path, - STATIC_DATA_FILENAME_EXTENSION); + bsg_strncpy(bugsnag_env->event_path, event_path, + sizeof(bugsnag_env->event_path)); bsg_safe_release_string_utf_chars(env, _event_path, event_path); + // copy the event UUID to the env struct + const char *event_uuid = bsg_safe_get_string_utf_chars(env, _event_uuid); + if (event_uuid == NULL) { + goto error; + } + bsg_strncpy(bugsnag_env->event_uuid, event_uuid, + sizeof(bugsnag_env->event_uuid)); + bsg_safe_release_string_utf_chars(env, _event_uuid, event_uuid); + // copy last run info path to env struct const char *last_run_info_path = bsg_safe_get_string_utf_chars(env, _last_run_info_path); @@ -236,102 +244,6 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( free(bugsnag_env); } -JNIEXPORT void JNICALL -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; - char static_data_path[384]; - bugsnag_event *event = NULL; - jbyteArray jpayload = NULL; - jbyteArray jstage = NULL; - char *payload = NULL; - char *static_data = NULL; - jbyteArray jstatic_data = NULL; - jstring japi_key = NULL; - jstring errorClass = NULL; - - if (!bsg_jni_cache->initialized) { - BUGSNAG_LOG("deliverReportAtPath failed: JNI cache not initialized."); - goto exit; - } - - event_path = bsg_safe_get_string_utf_chars(env, _report_path); - if (event_path == NULL) { - goto exit; - } - event = bsg_deserialize_event_from_file((char *)event_path); - - snprintf(static_data_path, sizeof(static_data_path), "%s%s", event_path, - STATIC_DATA_FILENAME_EXTENSION); - if (bsg_read_text_file(static_data_path, &static_data) > 0) { - jstatic_data = bsg_byte_ary_from_string(env, static_data); - } - - // remove persisted NDK struct early - this reduces the chance of crash loops - // in delivery. - remove(event_path); - remove(static_data_path); - - if (event == NULL) { - BUGSNAG_LOG("Failed to read event at file: %s", event_path); - goto exit; - } - - errorClass = bsg_safe_new_string_utf(env, event->error.errorClass); - if (bsg_safe_call_static_boolean_method( - env, bsg_jni_cache->NativeInterface, - bsg_jni_cache->NativeInterface_isDiscardErrorClass, errorClass)) { - 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_jni_cache->NativeInterface, - bsg_jni_cache->NativeInterface_deliverReport, jstage, jpayload, - jstatic_data, japi_key, is_launching); - } - -exit: - bsg_safe_delete_local_ref(env, errorClass); - 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); - bsg_free_feature_flags(event); - free(event); - } - bsg_safe_release_byte_array_elements(env, jpayload, (jbyte *)payload); - free(payload); - bsg_safe_release_byte_array_elements(env, jstatic_data, (jbyte *)static_data); - free(static_data); - - pthread_mutex_unlock(&bsg_native_delivery_mutex); -} - JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, jobject _this) { 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 5373abd018..aff3673033 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -24,12 +24,11 @@ typedef struct { /** * File path on disk where the next crash report will be written if needed. */ - char next_event_path[384]; + char event_path[384]; /** - * File path on disk where the next crash report static data will be written - * if needed. + * The pre-generated UUID of the next crash report. */ - char next_event_static_data_path[384]; + char event_uuid[37]; /** * File path on disk where the last run info will be written if needed. */ 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 42289bf4b6..3e64ff9cf4 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,5 @@ #include "serializer.h" -#include "serializer/event_reader.h" #include "serializer/event_writer.h" -#include "serializer/json_writer.h" bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { return bsg_lastrun_write(env); @@ -10,11 +8,3 @@ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { bool bsg_serialize_event_to_file(bsg_environment *env) { return bsg_event_write(env); } - -bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { - return bsg_read_event(filepath); -} - -char *bsg_serialize_event_to_json_string(bugsnag_event *event) { - 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 bcdc00d3dd..400614c40a 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -16,8 +16,6 @@ bool bsg_serialize_event_to_file(bsg_environment *env) __asyncsafe; */ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) __asyncsafe; -bugsnag_event *bsg_deserialize_event_from_file(char *filepath); - #ifdef __cplusplus } #endif 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 deleted file mode 100644 index 021de3e18e..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ /dev/null @@ -1,298 +0,0 @@ -#include "event_reader.h" - -#include "../../event.h" -#include "../string.h" -#include "utils/logger.h" - -#include -#include -#include -#include -#include - -const int BSG_MIGRATOR_CURRENT_VERSION = 14; - -void bsg_read_feature_flags(int fd, bool expect_verification, - bsg_feature_flag **out_feature_flags, - size_t *out_feature_flag_count); - -void bsg_read_opaque_metadata(int fd, bugsnag_metadata *metadata); - -void bsg_read_opaque_breadcrumb_metadata(int fd, - bugsnag_breadcrumb *breadcrumbs, - int crumb_count); - -bool bsg_read_breadcrumbs(int fd, bugsnag_event *event); - -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 fd = open(filepath, O_RDONLY); - if (fd == -1) { - return NULL; - } - - bsg_report_header *header = bsg_report_header_read(fd); - if (header == NULL) { - return NULL; - } - - int version = header->version; - free(header); - - if (version != BSG_MIGRATOR_CURRENT_VERSION) { - return NULL; - } - - 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) { - goto error; - } - - // read the breadcrumbs - if (!bsg_read_breadcrumbs(fd, event)) { - goto error; - } - // read the feature flags, if possible - bsg_read_feature_flags(fd, true, &event->feature_flags, - &event->feature_flag_count); - bsg_read_opaque_metadata(fd, &event->metadata); - bsg_read_opaque_breadcrumb_metadata(fd, event->breadcrumbs, - event->crumb_count); - - return event; -error: - free(event); - return NULL; -} - -bool bsg_read_breadcrumbs(int fd, bugsnag_event *event) { - bugsnag_breadcrumb *breadcrumbs = - calloc(event->max_crumb_count, sizeof(bugsnag_breadcrumb)); - if (breadcrumbs == NULL) { - goto error; - } - const size_t bytes_to_read = - event->max_crumb_count * sizeof(bugsnag_breadcrumb); - const ssize_t read_count = read(fd, breadcrumbs, bytes_to_read); - if (read_count != bytes_to_read) { - goto error; - } - - event->breadcrumbs = breadcrumbs; - return true; -error: - event->breadcrumbs = NULL; - event->crumb_count = 0; - event->crumb_first_index = 0; - return 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, bool expect_verification, - 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; - } - - if (expect_verification) { - const uint8_t feature_flags_valid = read_byte(fd); - if (feature_flags_valid != 0) { - goto feature_flags_error; - } - } - - *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 available - *out_feature_flag_count = 0; - *out_feature_flags = NULL; -} - -void bsg_read_opaque_metadata(int fd, bugsnag_metadata *metadata) { - size_t read_index = 0; - for (; read_index < metadata->value_count; read_index++) { - if (metadata->values[read_index].type == BSG_METADATA_OPAQUE_VALUE && - metadata->values[read_index].opaque_value_size > 0) { - - size_t opaque_value_size = metadata->values[read_index].opaque_value_size; - - void *opaque_value = calloc(1, opaque_value_size); - if (opaque_value == NULL) { - goto opaque_metadata_fail; - } - - if (read(fd, opaque_value, opaque_value_size) != opaque_value_size) { - free(opaque_value); - goto opaque_metadata_fail; - } - - metadata->values[read_index].opaque_value_size = opaque_value_size; - metadata->values[read_index].opaque_value = opaque_value; - } - } - - return; - -opaque_metadata_fail: - // ensure that only the OPAQUE values we read successfully are considered - // "valid" this allows for a partial recovery of the OPAQUE data - for (; read_index < metadata->value_count; read_index++) { - if (metadata->values[read_index].type == BSG_METADATA_OPAQUE_VALUE) { - // set all unread OPAQUE values to NONE as their opaque_values are invalid - metadata->values[read_index].type = BSG_METADATA_NONE_VALUE; - metadata->values[read_index].opaque_value_size = 0; - metadata->values[read_index].opaque_value = NULL; - } - } -} - -void bsg_read_opaque_breadcrumb_metadata(int fd, - bugsnag_breadcrumb *breadcrumbs, - int crumb_count) { - - for (int breadcrumb_index = 0; breadcrumb_index < crumb_count; - breadcrumb_index++) { - - bsg_read_opaque_metadata(fd, &(breadcrumbs[breadcrumb_index].metadata)); - } -} - -static bool read_from_file(int fd, ssize_t length, char *buffer) { - ssize_t bytes_read = 0; - ssize_t total_bytes_read = 0; - while (total_bytes_read < length) { - ssize_t bytes_to_read = length - total_bytes_read; - if ((bytes_read = read(fd, buffer + total_bytes_read, bytes_to_read)) < 0) { - return false; - } - total_bytes_read += bytes_read; - } - return true; -} - -ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer) { - char *data = NULL; - ssize_t length = 0; - struct stat stats; - int fd = open(filename, O_RDONLY); - if (fd < 0) { - goto fail; - } - if (fstat(fd, &stats) < 0) { - goto fail; - } - length = (ssize_t)stats.st_size; - data = malloc(length + 1); - if (data == NULL) { - goto fail; - } - if (!read_from_file(fd, length, data)) { - goto fail; - } - data[length] = 0; - *buffer_pointer = data; - goto success; - -fail: - length = -1; -success: - if (fd > 0) { - close(fd); - } - if (length < 0) { - free(data); - } - return length; -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h deleted file mode 100644 index a003946053..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h +++ /dev/null @@ -1,27 +0,0 @@ -#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); - -/** - * Read a text file from disk. Caller is responsible for freeing the buffer. - * - * @param filename The file to load - * @param buffer_pointer Pointer to the pointer to allocate a buffer. - * @return The length of the file, or -1 if the file could not be loaded. - */ -ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 61f0c8475f..d434d86d7b 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -2,6 +2,7 @@ #include #include +#include #include #include "BSG_KSCrashStringConversion.h" @@ -9,6 +10,7 @@ #include "buffered_writer.h" #include "event.h" #include "internal_metrics.h" +#include "utils/string.h" #define CHECKED(e) \ if ((e) != BSG_KSJSON_OK) { \ @@ -23,6 +25,7 @@ strnlen((value), sizeof((value)))) #define STRING_NOT_EMPTY(s) (*(s) != 0) +#define STR_CONST_CAT(dst, src) bsg_strncpy((dst), (src), sizeof(src)) static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, bugsnag_metadata *metadata); @@ -52,13 +55,61 @@ static int bsg_write(const char *data, size_t length, void *userData) { : BSG_KSJSON_ERROR_CANNOT_ADD_DATA; } +/* + * Build the event filename in the same format as defined in EventFilenameInfo: + * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" + */ +static size_t build_filename(bsg_environment *env, char *out) { + time_t now; + time(&now); + + int length = strnlen(env->event_path, sizeof(env->event_path)); + memcpy(out, env->event_path, length); + out[length++] = '/'; + + // the timestamp is encoded as unix time + length += bsg_uint64_to_string(now, &out[length]); + + // append the api_key to the filename + out[length++] = '_'; + length += bsg_strncpy(out + length, env->next_event.api_key, + sizeof(env->next_event.api_key)); + + // append the errorType (c) + out[length++] = '_'; + out[length++] = 'c'; + out[length++] = '_'; + + // use the pre-generated UUID in `env` - avoiding needing any source of + // randomness on the signal-handler path + length += bsg_strncpy(out + length, env->event_uuid, sizeof(env->event_uuid)); + + if (env->next_event.app.is_launching) { + length += STR_CONST_CAT(out + length, "_startupcrash"); + } else { + length += STR_CONST_CAT(out + length, "_not-jvm"); + } + + length += STR_CONST_CAT(out + length, ".json"); + + return length; +} + bool bsg_event_write(bsg_environment *env) { - bsg_buffered_writer writer; + char filename[sizeof(env->event_path) + 256]; + filename[0] = '\0'; + build_filename(env, filename); + + return bsg_write_event_file(env, filename); +} + +bool bsg_write_event_file(bsg_environment *env, const char *filename) { BSG_KSJSONEncodeContext jsonContext; + bsg_buffered_writer writer; bugsnag_event *event = &env->next_event; BSG_KSJSONEncodeContext *json = &jsonContext; - if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { + if (!bsg_buffered_writer_open(&writer, filename)) { return false; } 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 index 6f2ddfaeff..d8593dc818 100644 --- 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 @@ -6,4 +6,7 @@ bool bsg_event_write(bsg_environment *env) __asyncsafe; +bool bsg_write_event_file(bsg_environment *env, + const char *filename) __asyncsafe; + bool bsg_lastrun_write(bsg_environment *env) __asyncsafe; 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 deleted file mode 100644 index ad3c356a5b..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c +++ /dev/null @@ -1,489 +0,0 @@ -#include "json_writer.h" - -#include -#include -#include -#include - -#include - -#include "../logger.h" -#include "internal_metrics.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"; - default: - return ""; - } -} - -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"; - default: - return ""; - } -} - -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) {} - -static JSON_Value * -bsg_json_for_opaque_metadata(const bsg_metadata_value *metadata) { - return (metadata->opaque_value_size > 0) - ? json_parse_string(metadata->opaque_value) - : NULL; -} - -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; - case BSG_METADATA_OPAQUE_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - JSON_Value *metadata_json_value = bsg_json_for_opaque_metadata(&value); - json_object_dotset_value(event_obj, format, metadata_json_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; - case BSG_METADATA_OPAQUE_VALUE: - sprintf(format, "metaData.%s", value.name); - JSON_Value *metadata_json_value = bsg_json_for_opaque_metadata(&value); - json_object_dotset_value(event_obj, format, metadata_json_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 (bsg_event_has_session(event)) { - json_object_dotset_string(event_obj, "session.startedAt", - event->session_start); - json_object_dotset_string(event_obj, "session.id", event->session_id); - 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); - } -} - -static void set_hex_number(JSON_Object *frame, const char *name, - unsigned long value) { - char hex_str[20]; - sprintf(hex_str, "0x%lx", value); - json_object_set_string(frame, name, hex_str); -} - -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); - set_hex_number(frame, "frameAddress", (*stackframe).frame_address); - set_hex_number(frame, "symbolAddress", (*stackframe).symbol_address); - set_hex_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) { - set_hex_number(frame, "method", (*stackframe).frame_address); - } else { - json_object_set_string(frame, "method", (*stackframe).method); - } - - if (*stackframe->code_identifier != 0) { - char code_identifier[sizeof(stackframe->code_identifier) + 1]; - code_identifier[sizeof(stackframe->code_identifier)] = - 0; // force zero terminator - strncpy(code_identifier, stackframe->code_identifier, - sizeof(stackframe->code_identifier)); - json_object_set_string(frame, "codeIdentifier", code_identifier); - } - - 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 == event->max_crumb_count) { - 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]; - char buffer[32]; - sprintf(buffer, "%d", thread->id); - json_object_set_string(json_thread, "id", buffer); - 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); - } - } -} - -static void bsg_serialize_callbacks(const bugsnag_event *event, - JSON_Object *callbacks_obj) { - static const int callbacks_count = - sizeof(event->set_callback_counts) / sizeof(*event->set_callback_counts); - - for (int i = 0; i < callbacks_count; i++) { - if (event->set_callback_counts[i].count > 0) { - json_object_set_number(callbacks_obj, event->set_callback_counts[i].name, - event->set_callback_counts[i].count); - } - } - - for (int i = 0; i < bsg_called_apis_count; i++) { - if (bsg_was_api_called(event, i)) { - json_object_set_boolean(callbacks_obj, bsg_called_api_names[i], true); - } - } -} - -static void bsg_serialize_usage(const bugsnag_event *event, - JSON_Object *event_obj) { - JSON_Value *usage_val = json_value_init_object(); - JSON_Object *usage_obj = json_value_get_object(usage_val); - json_object_set_value(event_obj, "usage", usage_val); - - JSON_Value *callbacks_val = json_value_init_object(); - JSON_Object *callbacks = json_value_get_object(callbacks_val); - json_object_set_value(usage_obj, "callbacks", callbacks_val); - bsg_serialize_callbacks(event, callbacks); -} - -char *bsg_event_to_json(bugsnag_event *event) { - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - 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_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); - 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); - bsg_serialize_usage(event, event_obj); - - 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 deleted file mode 100644 index 9b9696dd5a..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h +++ /dev/null @@ -1,36 +0,0 @@ -#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); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c index 02959082ea..5774ebdca3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c @@ -15,14 +15,15 @@ size_t bsg_strlen(const char *str) { return strnlen(str, STRING_MAX_LENGTH); } -void bsg_strncpy(char *dst, const char *src, size_t dst_size) { +size_t bsg_strncpy(char *dst, const char *src, size_t dst_size) { if (dst == NULL || dst_size == 0) { - return; + return 0; } dst[0] = '\0'; if (src != NULL) { - strncat(dst, src, dst_size - 1); + return strlcat(dst, src, dst_size); } + return 0; } void bsg_hex_encode(char *dst, const void *src, size_t byte_count, diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h index edafcfa60b..7e79f041da 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h @@ -21,7 +21,7 @@ size_t bsg_strlen(const char *str) __asyncsafe; /** * Copy a maximum number of bytes from src to dst */ -void bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; +size_t bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; /** * Encode a number of bytes into dst while hex encoding the data. diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index f1dac42308..d16594b30e 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(bugsnag-ndk-test SHARED cpp/test_breadcrumbs.c cpp/test_bsg_event.c cpp/test_featureflags.c - cpp/migrations/EventOnDiskTests.cpp + cpp/test_bsg_event.c cpp/UnwindTest.cpp ) 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 68624d369a..600d5a46b0 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -3,13 +3,14 @@ #include #include -#define GREATEST_FPRINTF(ignore, fmt, ...) __android_log_print(ANDROID_LOG_INFO, "BugsnagNDKTest", fmt, ##__VA_ARGS__) +#define GREATEST_FPRINTF(ignore, fmt, ...) \ + __android_log_print(ANDROID_LOG_INFO, "BugsnagNDKTest", fmt, ##__VA_ARGS__) #include #include +#include "test_bsg_event.h" #include "test_serializer.h" -#include SUITE(suite_string_utils); SUITE(suite_json_serialization); @@ -29,11 +30,11 @@ GREATEST_MAIN_DEFS(); * @return the exit code of the test suite */ int run_test_suite(void (*test_suite)(void)) { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_SUITE(test_suite); - GREATEST_MAIN_END(); + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_SUITE(test_suite); + GREATEST_MAIN_END(); } /** @@ -43,196 +44,68 @@ int run_test_suite(void (*test_suite)(void)) { * @return the exit code of the test suite */ int run_test(enum greatest_test_res (*test)(void)) { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_TEST(test); - GREATEST_MAIN_END(); + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_TEST(test); + GREATEST_MAIN_END(); } -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeStringTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_string_utils); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_json_serialization); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeStructToFileTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_struct_to_file); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeBreadcrumbTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_breadcrumbs); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventMutatorsTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_event_mutators); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventAppMutatorsTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_event_app_mutators); +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeStringTest_run(JNIEnv *_env, jobject _this) { + return run_test_suite(suite_string_utils); } -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventDeviceMutatorsTest_run( - JNIEnv *_env, jobject _this) { - 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); -} +extern bool bsg_event_write(bsg_environment *env); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_UserSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadUserTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_user(event->user, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run( + JNIEnv *_env, jobject _this, jstring _dir) { -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_AppSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadAppTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_app(event->app, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + const char *dir = (*_env)->GetStringUTFChars(_env, _dir, NULL); + if (dir == NULL) { + return 0; + } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_AppMetadataSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadAppMetadataTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_app_metadata(event->app, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + BUGSNAG_LOG("Writing event file to %s", dir); + bsg_environment env; + bugsnag_event *event = init_event(); + memcpy(&env.next_event, event, sizeof(bugsnag_event)); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_DeviceSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadDeviceTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_device(event->device, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + strcpy(env.event_path, dir); + strcpy(env.event_uuid, "test-uuid"); -TEST test_custom_meta_data_serialization(test_case *test_case) { - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event = json_value_get_object(event_val); - bugsnag_metadata *meta_data = test_case->data_ptr; - bsg_serialize_custom_metadata(*meta_data, event); - free(meta_data); - return validate_serialized_json(test_case, event_val); -} + bsg_event_write(&env); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_CustomMetadataSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadCustomMetadataTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_custom_metadata(event->metadata, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + free(event); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_ContextSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadContextTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_context(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + (*_env)->ReleaseStringUTFChars(_env, _dir, dir); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_SeverityReasonSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadSeverityReasonTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_severity_reason(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); + return 0; } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_SessionSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadSessionTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_session(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeBreadcrumbTest_run( + JNIEnv *_env, jobject _this) { + return run_test_suite(suite_breadcrumbs); } -JNIEXPORT jstring JNICALL -Java_com_bugsnag_android_ndk_BreadcrumbStateSerializationTest_run(JNIEnv *env, - jobject thiz) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - event->max_crumb_count = 50; - event->breadcrumbs = calloc(event->max_crumb_count, sizeof(bugsnag_breadcrumb)); - loadBreadcrumbsTestCase(event); - JSON_Value *eventVal = json_value_init_array(); - JSON_Array *eventAry = json_value_get_array(eventVal); - bsg_serialize_breadcrumbs(event, eventAry); - char *string = json_serialize_to_string(eventVal); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventMutatorsTest_run( + JNIEnv *_env, jobject _this) { + return run_test_suite(suite_event_mutators); } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_StackframeSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_stackframe *frame = loadStackframeTestCase(); - JSON_Value *event_val = json_value_init_array(); - JSON_Array *event_obj = json_value_get_array(event_val); - bsg_serialize_stackframe(frame, false, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeEventAppMutatorsTest_run(JNIEnv *_env, + jobject _this) { + return run_test_suite(suite_event_app_mutators); } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_ExceptionSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadExceptionTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *exception = json_value_get_object(event_val); - JSON_Value *stack_val = json_value_init_array(); - JSON_Array *stacktrace = json_value_get_array(stack_val); - json_object_set_value(exception, "stacktrace", stack_val); - bsg_serialize_error(event->error, exception, stacktrace); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); - +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeEventDeviceMutatorsTest_run(JNIEnv *_env, + jobject _this) { + return run_test_suite(suite_event_device_mutators); } -JNIEXPORT jstring JNICALL -Java_com_bugsnag_android_ndk_ThreadSerializationTest_run(JNIEnv *env, jobject thiz) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadThreadTestCase(event); - JSON_Value *threads_val = json_value_init_array(); - JSON_Array *threads_array = json_value_get_array(threads_val); - bsg_serialize_threads(event, threads_array); - char *string = json_serialize_to_string(threads_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT jint JNICALL Java_com_bugsnag_android_ndk_NativeFeatureFlagsTest_run( + JNIEnv *env, jobject thiz) { + return run_test_suite(suite_feature_flags); } diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp deleted file mode 100644 index 18a06b4acf..0000000000 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#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 the current format - */ -static void *create_full_event() { - auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); - - strcpy(event->context, - "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); - strcpy(event->grouping_hash, - "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); - event->severity = BSG_SEVERITY_INFO; - - // app - strcpy(event->app.binary_arch, "mips"); - strcpy(event->app.build_uuid, "1234-9876-adfe"); - event->app.duration = 81395165021; - event->app.duration_in_foreground = 81395165010; - event->app.in_foreground = true; - event->app.is_launching = true; - strcpy(event->app.id, "com.example.PhotoSnapPlus"); - strcpy(event->app.release_stage, "リリース"); - strcpy(event->app.type, "red"); - strcpy(event->app.version, "2.0.52"); - event->app.version_code = 8139512718; - - // breadcrumbs - event->max_crumb_count = 50; - event->breadcrumbs = new bugsnag_breadcrumb[event->max_crumb_count]; - auto max = event->max_crumb_count; - event->crumb_first_index = 2; // test the circular buffer logic - char name[30]; - for (int i = event->crumb_first_index; i < max; i++) { - sprintf(name, "mission %d", i - event->crumb_first_index); - insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, - "Now we know what they mean by 'advanced' tactical training."); - } - for (int i = 0; i < event->crumb_first_index; i++) { - sprintf(name, "mission %d", (max - event->crumb_first_index) + i); - insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, - "Now we know what they mean by 'advanced' tactical training."); - } - event->crumb_count = max; - - // device - event->device.cpu_abi_count = 1; - strcpy(event->device.cpu_abi[0].value, "mipsx"); - strcpy(event->device.id, "ffffa"); - event->device.jailbroken = true; - strcpy(event->device.locale, "en_AU#Melbun"); - strcpy(event->device.manufacturer, "HI-TEC™"); - strcpy(event->device.model, "🍨"); - strcpy(event->device.orientation, "sideup"); - strcpy(event->device.os_name, "BOX BOX"); - strcpy(event->device.os_version, "98.7"); - { // -- runtime versions - strcpy(event->device.os_build, "beta1-2"); - event->device.api_level = 32; - } - event->device.time = 1638992630; - event->device.total_memory = 3839512576; - - // feature flags - event->feature_flag_count = 4; - event->feature_flags = - (bsg_feature_flag *)calloc(4, sizeof(bsg_feature_flag)); - event->feature_flags[0].name = strdup("bluebutton"); - event->feature_flags[0].variant = strdup("on"); - event->feature_flags[1].name = strdup("redbutton"); - event->feature_flags[1].variant = strdup("off"); - event->feature_flags[2].name = strdup("nobutton"); - event->feature_flags[3].name = strdup("switch"); - event->feature_flags[3].variant = strdup("left"); - - // exceptions - strcpy(event->error.errorClass, "SIGBUS"); - strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); - strcpy(event->error.type, "C"); - event->error.frame_count = 2; - event->error.stacktrace[0].frame_address = (uintptr_t)4294967294; - event->error.stacktrace[0].load_address = (uintptr_t)2367523; - event->error.stacktrace[0].symbol_address = 776; - event->error.stacktrace[0].line_number = (uintptr_t)4194967233; - strcpy(event->error.stacktrace[0].method, "makinBacon"); - strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); - event->error.stacktrace[1].frame_address = - (uintptr_t)3011142731; // will become method hex - - // metadata - strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); - - // session info - event->handled_events = 5; - event->unhandled_events = 2; - strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); - strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); - - // threads - event->thread_count = 8; - for (int i = 0; i < event->thread_count; i++) { - event->threads[i].id = 1000 + i; - sprintf(event->threads[i].name, "Thread #%d", i); - sprintf(event->threads[i].state, "paused-%d", i); - } - - // user - strcpy(event->user.email, "fenton@io.example.com"); - strcpy(event->user.name, "Fenton"); - strcpy(event->user.id, "fex01"); - - return event; -} - -static const char *write_event(JNIEnv *env, jstring temp_file, - void *(event_generator)()) { - auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment)); - event_ctx->report_header.version = BUGSNAG_EVENT_VERSION; - 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); - 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_EventOnDiskTests_generatePayloadInfo( - JNIEnv *env, jobject _this, jstring temp_file) { - const char *path = write_event(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_EventOnDiskTests_generateAndStoreEvent( - JNIEnv *env, jobject _this, jstring temp_file) { - const char *path = write_event(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->breadcrumbs); - free(parsed_event->feature_flags); - free(parsed_event); - - // latest event type -> temp file - write_str_to_file(output, path); - free(output); -} - -#ifdef __cplusplus -} -#endif diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp deleted file mode 100644 index 38a2c10c38..0000000000 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp +++ /dev/null @@ -1,90 +0,0 @@ -/** Helper functions for writing migration tests */ -#pragma once - -#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); -} 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 5f5a9588c8..7973a7a349 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,6 @@ #include #include #include -#include bugsnag_breadcrumb *init_breadcrumb(const char *name, const 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_bsg_event.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c index 4dcd44093c..b89a4dc635 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c @@ -1,8 +1,7 @@ #include -#include #include #include "../../main/jni/include/bugsnag.h" -#include +#include "test_bsg_event.h" bugsnag_event *init_event() { bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h new file mode 100644 index 0000000000..691908547b --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h @@ -0,0 +1,8 @@ +#ifndef BUGSNAG_ANDROID_TEST_BSG_EVENT_H +#define BUGSNAG_ANDROID_TEST_BSG_EVENT_H + +#include + +bugsnag_event *init_event(); + +#endif // BUGSNAG_ANDROID_TEST_BSG_EVENT_H 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 cd1835580d..2565b47185 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 @@ -7,150 +7,13 @@ #include #include -#include #include -#define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/foo.crash" - -bugsnag_breadcrumb *init_breadcrumb(const char *name, const char *message, bugsnag_breadcrumb_type type); - -bool bsg_report_header_write(bsg_report_header *header, int fd); - -void generate_basic_report(bugsnag_event *event) { - strcpy(event->grouping_hash, "foo-hash"); - strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); - strcpy(event->context, "SomeActivity"); - strcpy(event->error.errorClass, "SIGBUS"); - strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); - event->error.stacktrace[0].frame_address = 454379; - event->error.stacktrace[1].frame_address = 342334; - event->error.frame_count = 2; - strcpy(event->error.type, "C"); - strcpy(event->error.stacktrace[0].method, "makinBacon"); - strcpy(event->app.id, "com.example.PhotoSnapPlus"); - strcpy(event->app.release_stage, "リリース"); - strcpy(event->app.version, "2.0.52"); - event->app.version_code = 57; - strcpy(event->app.build_uuid, "1234-9876-adfe"); - strcpy(event->device.manufacturer, "HI-TEC™"); - strcpy(event->device.model, "Rasseur"); - strcpy(event->device.locale, "en_AU#Melbun"); - strcpy(event->device.os_name, "android"); - strcpy(event->user.email, "fenton@io.example.com"); - strcpy(event->user.id, "fex"); - event->device.total_memory = 234678100; - event->app.duration = 6502; - 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; - bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º", BSG_CRUMB_STATE); - bsg_event_add_breadcrumb(event, crumb1); - - bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.", BSG_CRUMB_USER); - bsg_event_add_breadcrumb(event, crumb2); - - event->handled_events = 1; - event->unhandled_events = 1; - strcpy(event->session_id, "f1ab"); - strcpy(event->session_start, "2019-03-19T12:58:19+00:00"); - - strcpy(event->notifier.version, "1.0"); - strcpy(event->notifier.url, "bugsnag.com"); - strcpy(event->notifier.name, "Test Notifier"); -} - -bugsnag_event *bsg_generate_event(void) { - bugsnag_event *report = calloc(1, sizeof(bugsnag_event)); - report->max_crumb_count = 50; - report->breadcrumbs = - calloc(report->max_crumb_count, sizeof(bugsnag_breadcrumb)); - strcpy(report->grouping_hash, "foo-hash"); - strcpy(report->api_key, "5d1e5fbd39a74caa1200142706a90b20"); - strcpy(report->context, "SomeActivity"); - strcpy(report->error.errorClass, "SIGBUS"); - strcpy(report->error.errorMessage, "POSIX is serious about oncoming traffic"); - report->error.stacktrace[0].frame_address = 454379; - report->error.stacktrace[1].frame_address = 342334; - report->error.frame_count = 2; - strcpy(report->error.type, "C"); - strcpy(report->error.stacktrace[0].method, "makinBacon"); - strcpy(report->app.id, "com.example.PhotoSnapPlus"); - strcpy(report->app.release_stage, "リリース"); - strcpy(report->app.version, "2.0.52"); - report->app.version_code = 57; - strcpy(report->app.build_uuid, "1234-9876-adfe"); - strcpy(report->device.manufacturer, "HI-TEC™"); - strcpy(report->device.model, "Rasseur"); - strcpy(report->device.locale, "en_AU#Melbun"); - strcpy(report->device.os_name, "android"); - strcpy(report->user.email, "fenton@io.example.com"); - strcpy(report->user.id, "fex"); - report->device.total_memory = 234678100; - report->app.duration = 6502; - bugsnag_event_add_metadata_bool(report, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(report, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(report, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(report, "metrics", "counter", 47.8); - - report->crumb_count = 0; - report->crumb_first_index = 0; - bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º", - BSG_CRUMB_STATE); - bsg_event_add_breadcrumb(report, crumb1); - - bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.", - BSG_CRUMB_USER); - bsg_event_add_breadcrumb(report, crumb2); - - report->handled_events = 1; - report->unhandled_events = 1; - strcpy(report->session_id, "f1ab"); - strcpy(report->session_start, "2019-03-19T12:58:19+00:00"); - - strcpy(report->notifier.version, "1.0"); - strcpy(report->notifier.url, "bugsnag.com"); - strcpy(report->notifier.name, "Test Notifier"); - report->unhandled_events = 2; - return report; -} - +#define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/" void bsg_update_next_run_info(bsg_environment *env); -char *test_read_last_run_info(const bsg_environment *env) { - int fd = open(SERIALIZE_TEST_FILE, O_RDONLY); - size_t size = sizeof(env->next_last_run_info); - char *buf = calloc(1, size); - read(fd, buf, size); - return buf; -} - TEST test_last_run_info_serialization(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); strcpy(env->last_run_info_path, SERIALIZE_TEST_FILE); @@ -171,252 +34,6 @@ TEST test_last_run_info_serialization(void) { PASS(); } -TEST test_report_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)); - 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->breadcrumbs); - free(report); - free(env); - PASS(); -} - -TEST test_report_with_feature_flags_to_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - 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, SERIALIZE_TEST_FILE); - ASSERT(bsg_serialize_event_to_file(env)); - free(report->breadcrumbs); - free(report); - free(env); - PASS(); -} - -TEST test_file_to_report(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - 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(); - memcpy(&env->next_event, generated_report, sizeof(bugsnag_event)); - strcpy(env->next_event_path, SERIALIZE_TEST_FILE); - bsg_serialize_event_to_file(env); - - bugsnag_event *report = bsg_deserialize_event_from_file(SERIALIZE_TEST_FILE); - ASSERT(report != NULL); - ASSERT(strcmp("SIGBUS", report->error.errorClass) == 0); - ASSERT(strcmp("POSIX is serious about oncoming traffic", report->error.errorMessage) == 0); - free(generated_report->breadcrumbs); - free(generated_report); - free(env); - free(report); - PASS(); -} - -TEST test_report_with_feature_flags_from_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - 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->breadcrumbs); - free(report); - free(env); - free(event); - PASS(); -} - -TEST test_report_with_opaque_metadata_from_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - env->report_header.big_endian = 1; - bugsnag_event *report = bsg_generate_event(); - memcpy(&env->next_event, report, sizeof(bugsnag_event)); - bsg_add_metadata_value_opaque(&env->next_event.metadata, "opaque", "map", "{\"user\": \"Bobby Tables\"}"); - bsg_add_metadata_value_opaque(&env->next_event.metadata, "opaque", "list", "[1,2,3,4]"); - 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(6, event->metadata.value_count); - - ASSERT_EQ(BSG_METADATA_OPAQUE_VALUE, bugsnag_event_has_metadata(event, "opaque", "map")); - ASSERT_EQ(BSG_METADATA_OPAQUE_VALUE, bugsnag_event_has_metadata(event, "opaque", "list")); - - free(report->breadcrumbs); - free(report); - free(env); - free(event); - PASS(); -} - -// helper function -JSON_Value *bsg_generate_json(void) { - bugsnag_event *event = bsg_generate_event(); - char *json = bsg_serialize_event_to_json_string(event); - JSON_Value *root_value = json_parse_string(json); - free(json); - free(event->breadcrumbs); - free(event); - return root_value; -} - -TEST test_app_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(strcmp("2.0.52", json_object_dotget_string(event, "app.version")) == 0); - ASSERT(strcmp( "リリース", json_object_dotget_string(event, "app.releaseStage")) == 0); - ASSERT_EQ(57, json_object_dotget_number(event, "app.versionCode")); - ASSERT(strcmp( "1234-9876-adfe", json_object_dotget_string(event, "app.buildUUID")) == 0); - json_value_free(root_value); - PASS(); -} - -TEST test_session_handled_counts(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(strcmp("f1ab", json_object_dotget_string(event, "session.id")) == 0); - ASSERT(strcmp("2019-03-19T12:58:19+00:00", json_object_dotget_string(event, "session.startedAt")) == 0); - ASSERT_EQ(1, json_object_dotget_number(event, "session.events.handled")); - ASSERT_EQ(2, json_object_dotget_number(event, "session.events.unhandled")); - PASS(); -} - -TEST test_grouping_hash_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT_STR_EQ("foo-hash", json_object_get_string(event, "groupingHash")); - PASS(); -} - -TEST test_context_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "SomeActivity", json_object_get_string(event, "context")) == 0); - PASS(); -} - -TEST test_device_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "HI-TEC™", json_object_dotget_string(event, "device.manufacturer")) == 0); - ASSERT(strcmp( "Rasseur", json_object_dotget_string(event, "device.model")) == 0); - ASSERT(strcmp( "en_AU#Melbun", json_object_dotget_string(event, "device.locale")) == 0); - json_value_free(root_value); - PASS(); -} - -TEST test_user_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "fex", json_object_dotget_string(event, "user.id")) == 0); - ASSERT(strcmp( "fenton@io.example.com", json_object_dotget_string(event, "user.email")) == 0); - ASSERT(json_object_dotget_string(event, "user.name") == NULL); - json_value_free(root_value); - PASS(); -} - -TEST test_custom_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "percy", json_object_dotget_string(event, "metaData.metrics.subject")) == 0); - ASSERT(strcmp( "rain", json_object_dotget_string(event, "metaData.app.weather")) == 0); - ASSERT(json_object_dotget_boolean(event, "metaData.app.experimentX") == -1); - ASSERT(json_object_dotget_number(event, "metaData.app.counter") - 47.8 < 0.01); - json_value_free(root_value); - PASS(); -} - -TEST test_exception_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - JSON_Array *exceptions = json_object_get_array(event, "exceptions"); - ASSERT(exceptions != NULL); - JSON_Object *exception = json_array_get_object(exceptions, 0); - ASSERT(exception != NULL); - ASSERT(strcmp("SIGBUS", json_object_get_string(exception, "errorClass")) == 0); - ASSERT(strcmp("POSIX is serious about oncoming traffic", json_object_get_string(exception, "message")) == 0); - ASSERT(strcmp("c", json_object_get_string(exception, "type")) == 0); - JSON_Array *stacktrace = json_object_get_array(exception, "stacktrace"); - ASSERT(stacktrace != NULL); - ASSERT_EQ(2, json_array_get_count(stacktrace)); - ASSERT(strcmp("makinBacon", json_object_get_string(json_array_get_object(stacktrace, 0), "method")) == 0); - ASSERT_STR_EQ("0x6eeeb", json_object_get_string(json_array_get_object(stacktrace, 0), "frameAddress")); - ASSERT(strcmp("0x5393e", json_object_get_string(json_array_get_object(stacktrace, 1), "method")) == 0); - ASSERT_STR_EQ("0x5393e", json_object_get_string(json_array_get_object(stacktrace, 1), "frameAddress")); - json_value_free(root_value); - PASS(); -} - -TEST test_breadcrumbs_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - JSON_Array *breadcrumbs = json_object_get_array(event, "breadcrumbs"); - ASSERT(breadcrumbs != NULL); - ASSERT_EQ(2, json_array_get_count(breadcrumbs)); - - JSON_Object *crumb1 = json_array_get_object(breadcrumbs, 0); - ASSERT_STR_EQ("decrease torque", json_object_get_string(crumb1, "name")); - ASSERT_STR_EQ("state", json_object_get_string(crumb1, "type")); - ASSERT_EQ(1, json_object_get_count(json_object_get_object(crumb1, "metaData"))); - ASSERT_STR_EQ("Moving laterally 26º", json_object_get_string(json_object_get_object(crumb1, "metaData"), "message")); - - JSON_Object *crumb2 = json_array_get_object(breadcrumbs, 1); - ASSERT_STR_EQ("enable blasters", json_object_get_string(crumb2, "name")); - ASSERT_STR_EQ("user", json_object_get_string(crumb2, "type")); - ASSERT_EQ(1, json_object_get_count(json_object_get_object(crumb2, "metaData"))); - ASSERT_STR_EQ("this is a drill.", json_object_get_string(json_object_get_object(crumb2, "metaData"), "message")); - PASS(); -} - - SUITE(suite_json_serialization) { RUN_TEST(test_last_run_info_serialization); - RUN_TEST(test_session_handled_counts); - RUN_TEST(test_context_to_json); - RUN_TEST(test_grouping_hash_to_json); - RUN_TEST(test_app_info_to_json); - RUN_TEST(test_device_info_to_json); - RUN_TEST(test_user_info_to_json); - RUN_TEST(test_custom_info_to_json); - RUN_TEST(test_exception_to_json); - RUN_TEST(test_breadcrumbs_to_json); -} - -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); - RUN_TEST(test_report_with_opaque_metadata_from_file); } From 6d744326b8671f31b535585d3b176c9aaa1645af Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 9 May 2024 10:37:13 +0100 Subject: [PATCH 07/28] fix(ndk): fixed a bug where the breadcrumb metadata was double-nested --- .../src/main/jni/utils/serializer/event_writer.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index d434d86d7b..89d964cd43 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -121,9 +121,14 @@ bool bsg_write_event_file(bsg_environment *env, const char *filename) { json, "context", event->context, strnlen(event->context, sizeof(event->context)))); - if (!bsg_write_metadata(json, &event->metadata)) { - goto error; + CHECKED(bsg_ksjsonbeginObject(json, "metaData")); + { + if (!bsg_write_metadata(json, &event->metadata)) { + goto error; + } } + CHECKED(bsg_ksjsonendContainer(json)); + if (!bsg_write_severity_reason(json, event)) { goto error; } @@ -218,7 +223,6 @@ static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, // helps explain it). bool written[value_count]; - CHECKED(bsg_ksjsonbeginObject(json, "metaData")); memset(written, 0, sizeof(bool) * value_count); for (int i = 0; i < value_count; i++) { bsg_metadata_value *value = &metadata->values[i]; @@ -251,7 +255,6 @@ static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, } CHECKED(bsg_ksjsonendContainer(json)); } - CHECKED(bsg_ksjsonendContainer(json)); return true; error: From 6f06a86afd1ab6770a94754f5ceea43fa1640c4c Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 9 May 2024 16:44:53 +0100 Subject: [PATCH 08/28] fix(ndk): moved the NDK JSON payloads back to their own directory so that enabledReleaseStage & discardClasses can be checked before delivery --- .../api/bugsnag-android-core.api | 1 + .../com/bugsnag/android/NativeInterface.java | 30 ++++- .../bugsnag/android/NativeInterfaceApiTest.kt | 7 +- .../detekt-baseline.xml | 3 +- .../resources/event_serialization.json | 80 ++++++++++- .../com/bugsnag/android/ndk/NativeBridge.kt | 13 ++ .../android/ndk/ReportDiscardScanner.kt | 125 ++++++++++++++++++ .../main/jni/utils/serializer/event_writer.c | 9 +- 8 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index ecca487084..f9dcc4e2a7 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -476,6 +476,7 @@ public class com/bugsnag/android/NativeInterface { public static fun addMetadata (Ljava/lang/String;Ljava/util/Map;)V public static fun clearMetadata (Ljava/lang/String;Ljava/lang/String;)V public static fun createEvent (Ljava/lang/Throwable;Lcom/bugsnag/android/Client;Lcom/bugsnag/android/SeverityReason;)Lcom/bugsnag/android/Event; + public static fun deliverReport (Ljava/io/File;)V public static fun deliverReport ([B[B[BLjava/lang/String;Z)V public static fun getApp ()Ljava/util/Map; public static fun getAppVersion ()Ljava/lang/String; 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 64e59d4161..de0da4a21c 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 @@ -21,6 +21,7 @@ import java.util.Map; import java.util.regex.Pattern; + /** * Used as the entry point for native code to allow proguard to obfuscate other areas if needed */ @@ -81,7 +82,15 @@ public static String getContext() { */ @NonNull public static File getNativeReportPath() { - return getClient().getEventStore().getStorageDir(); + return getNativeReportPath(getPersistenceDirectory()); + } + + private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) { + return new File(persistenceDirectory, "bugsnag/native"); + } + + private static @NonNull File getPersistenceDirectory() { + return getClient().getConfig().getPersistenceDirectory().getValue(); } /** @@ -422,6 +431,25 @@ public static void deliverReport(@Nullable byte[] releaseStageBytes, } } + /** + * Attempt to deliver an existing event file that is not current enqueued for delivery. The + * filename is expected to be in the standard {@link EventFilenameInfo} format, and the file + * should contain a correctly formatted {@link Event} object. This method will attempt to + * move the file into place, and flush the queue asynchronously. If the file cannot be moved + * into the queue directory, the file is deleted before returning. + * + * @param reportFile the file to enqueue for delivery + */ + public static void deliverReport(@NonNull File reportFile) { + EventStore eventStore = getClient().eventStore; + File eventFile = new File(eventStore.getStorageDir(), reportFile.getName()); + if (reportFile.renameTo(eventFile)) { + eventStore.flushAsync(); + } else { + reportFile.delete(); + } + } + /** * Notifies using the Android SDK * 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 f768c9176f..dce80a6b79 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 @@ -7,7 +7,6 @@ import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -22,6 +21,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner +import java.io.File import java.nio.file.Files /** @@ -87,9 +87,10 @@ internal class NativeInterfaceApiTest { @Test fun getNativeReportPathPersistenceDirectory() { val customDir = Files.createTempDirectory("custom").toFile() - `when`(eventStore.storageDir).thenReturn(customDir) + `when`(immutableConfig.persistenceDirectory).thenReturn(lazy { customDir }) val observed = NativeInterface.getNativeReportPath() - assertSame(customDir, observed) + val expected = File(customDir, "bugsnag/native") + assertEquals(expected, observed) } @Test diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index ce2169a8d2..af4ed869eb 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -3,9 +3,8 @@ CyclomaticComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) - LongMethod:EventOnDiskTests.kt$EventOnDiskTests$@Test fun testEvent() LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, eventUUID: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int, maxBreadcrumbs: Int, ) - NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() + SwallowedException:ReportDiscardScanner.kt$ReportDiscardScanner$ex: Exception TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver UseCheckOrError:ResourceUtils.kt$throw IllegalStateException("Failed to read JSON from $resourceName") diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json index ec3576f15a..46734e04a0 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json +++ b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json @@ -1 +1,79 @@ -{"context":"Foo","metaData":{},"severity":"info","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGSEGV"}},"exceptions":[{"errorClass":"SIGSEGV","message":"Whoops!","type":"c","stacktrace":[{"frameAddress":"0x0","symbolAddress":"0x0","loadAddress":"0x0","lineNumber":58,"isPC":true,"file":"Something.c","method":"foo()"}]}],"user":{"id":"123","name":"Bob Bobbiton","email":"bob@example.com"},"app":{"version":"1.0","id":"fa02","type":"C","releaseStage":"dev","versionCode":55,"buildUUID":"123","binaryArch":"x86","duration":9019,"durationInForeground":7017,"inForeground":true,"isLaunching":true},"device":{"osName":"android","id":"my-id-123","locale":"en","osVersion":"9.1","manufacturer":"Google","model":"Nexus","orientation":"portrait","runtimeVersions":{"androidApiLevel":0,"osBuild":""},"cpuAbi":[],"totalMemory":1095092340,"jailbroken":true,"time":"1970-01-01T02:06:49Z"},"breadcrumbs":[],"groupingHash":"Bar","usage":{"callbacks":{}},"session":{"id":"","startedAt":"","events":{"handled":0,"unhandled":0}}} \ No newline at end of file +{ + "context": "Foo", + "metaData": {}, + "severity": "info", + "unhandled": true, + "severityReason": { + "unhandledOverridden": false, + "type": "signal", + "attributes": { + "signalType": "SIGSEGV" + } + }, + "exceptions": [ + { + "errorClass": "SIGSEGV", + "message": "Whoops!", + "type": "c", + "stacktrace": [ + { + "frameAddress": "0x0", + "symbolAddress": "0x0", + "loadAddress": "0x0", + "lineNumber": 58, + "isPC": true, + "file": "Something.c", + "method": "foo()" + } + ] + } + ], + "user": { + "id": "123", + "name": "Bob Bobbiton", + "email": "bob@example.com" + }, + "app": { + "version": "1.0", + "id": "fa02", + "type": "C", + "releaseStage": "dev", + "versionCode": 55, + "buildUUID": "123", + "binaryArch": "x86", + "duration": 9019, + "durationInForeground": 7017, + "inForeground": true, + "isLaunching": true + }, + "device": { + "osName": "android", + "id": "my-id-123", + "locale": "en", + "osVersion": "9.1", + "manufacturer": "Google", + "model": "Nexus", + "orientation": "portrait", + "runtimeVersions": { + "androidApiLevel": 0, + "osBuild": "" + }, + "cpuAbi": [], + "totalMemory": 1095092340, + "jailbroken": true, + "time": "1970-01-01T02:06:49Z" + }, + "breadcrumbs": [], + "groupingHash": "Bar", + "usage": { + "callbacks": {} + }, + "session": { + "id": "", + "startedAt": "", + "events": { + "handled": 0, + "unhandled": 0 + } + } +} \ No newline at end of file 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 89bca859a5..793eec21ef 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 @@ -8,6 +8,7 @@ import com.bugsnag.android.StateEvent.AddBreadcrumb import com.bugsnag.android.StateEvent.AddMetadata import com.bugsnag.android.StateEvent.ClearMetadataSection import com.bugsnag.android.StateEvent.ClearMetadataValue +import com.bugsnag.android.StateEvent.DeliverPending import com.bugsnag.android.StateEvent.Install import com.bugsnag.android.StateEvent.NotifyHandled import com.bugsnag.android.StateEvent.NotifyUnhandled @@ -107,6 +108,7 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse when (event) { is Install -> handleInstallMessage(event) + is DeliverPending -> deliverPendingReports() is AddMetadata -> handleAddMetadata(event) is ClearMetadataSection -> clearMetadataTab(event.section) is ClearMetadataValue -> removeMetadata( @@ -169,6 +171,17 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse } } + private fun deliverPendingReports() { + val discardScanner = ReportDiscardScanner(logger) + reportDirectory.listFiles()?.forEach { reportFile -> + if (discardScanner.shouldDiscard(reportFile)) { + reportFile.delete() + } else { + NativeInterface.deliverReport(reportFile) + } + } + } + private fun isInvalidMessage(msg: Any?): Boolean { if (msg == null || msg !is StateEvent) { return true diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt new file mode 100644 index 0000000000..8395e741a0 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt @@ -0,0 +1,125 @@ +package com.bugsnag.android.ndk + +import android.util.JsonReader +import android.util.JsonToken +import com.bugsnag.android.Logger +import com.bugsnag.android.NativeInterface +import java.io.File + +internal class ReportDiscardScanner(private val logger: Logger) { + /** + * Checks whether a given report file should be discarded due to its `releaseStage` or any of + * the configured `discardClasses`. + * + * @return true if the report should be discarded instead of being sent + */ + fun shouldDiscard(report: File): Boolean { + return try { + report.bufferedReader().use { reader -> + JsonReader(reader).use { json -> shouldDiscard(json) } + } + } catch (ex: Exception) { + false + } + } + + private fun shouldDiscard(json: JsonReader): Boolean { + json.beginObject() + var pendingAppCheck = true + var pendingExceptionsCheck = true + + while (json.hasNext() && (pendingAppCheck || pendingExceptionsCheck)) { + val nextName = json.nextName() + val discard: Boolean = when (nextName) { + "app" -> { + pendingAppCheck = false + shouldDiscardForApp(json) + } + + "exceptions" -> { + pendingExceptionsCheck = false + shouldDiscardForExceptions(json) + } + + else -> { + json.skipValue() + false + } + } + + if (discard) { + return true + } + } + + return false + } + + private fun shouldDiscardForApp(json: JsonReader): Boolean { + val enabledReleaseStages = NativeInterface.getEnabledReleaseStages() + if (enabledReleaseStages.isNullOrEmpty()) { + json.skipValue() + return false + } + + json.beginObject() + while (json.peek() != JsonToken.END_OBJECT) { + val nextName = json.nextName() + when (nextName) { + "releaseStage" -> { + val releaseStage = json.nextString() + if (releaseStage !in enabledReleaseStages) { + logger.d("Discarding native report due to releaseStage") + return true + } + // do not early exit, make sure the entire "app" object is consumed + // before returning + } + + else -> json.skipValue() + } + } + json.endObject() + + return false + } + + private fun shouldDiscardForExceptions(json: JsonReader): Boolean { + json.beginArray() + + while (json.peek() != JsonToken.END_ARRAY) { + if (shouldDiscardException(json)) { + logger.d("Discarding native report due to errorClass") + return true + } + } + + json.endArray() + + return false + } + + private fun shouldDiscardException(json: JsonReader): Boolean { + json.beginObject() + + while (json.peek() != JsonToken.END_OBJECT) { + val name = json.nextName() + when (name) { + "errorClass" -> { + val errorClass = json.nextString() + if (NativeInterface.isDiscardErrorClass(errorClass)) { + return true + } + // do not early exit, make sure the entire "exception" object is consumed + // before returning + } + + else -> json.skipValue() + } + } + + json.endObject() + + return false + } +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 89d964cd43..5d92bdda4d 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -193,8 +193,13 @@ static bool bsg_write_metadata_value(BSG_KSJSONEncodeContext *json, CHECKED(JSON_LIMITED_STRING_ELEMENT(value->name, value->char_value)); break; case BSG_METADATA_NUMBER_VALUE: - CHECKED(bsg_ksjsonaddFloatingPointElement(json, value->name, - value->double_value)); + if (value->double_value == (double)((long long)value->double_value)) { + CHECKED(bsg_ksjsonaddIntegerElement(json, value->name, + (long long)value->double_value)); + } else { + CHECKED(bsg_ksjsonaddFloatingPointElement(json, value->name, + value->double_value)); + } break; case BSG_METADATA_OPAQUE_VALUE: CHECKED(bsg_ksjsonbeginElement(json, value->name)); From a1882f65be8882896c5068e5c189572aced84f88 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 13 May 2024 15:59:54 +0100 Subject: [PATCH 09/28] fix(ndk): BugsnagEventMapper now handles date formats in the "t{epoch millis}" format --- .../main/java/com/bugsnag/android/BugsnagEventMapper.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index c3af9864b5..4887792924 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -244,6 +244,14 @@ internal class BugsnagEventMapper( } private fun String.toDate(): Date { + if (isNotEmpty() && this[0] == 't') { + // date is in the format 't{epoch millis}' + val timestamp = substring(1) + timestamp.toLongOrNull()?.let { + return Date(it) + } + } + return try { DateUtils.fromIso8601(this) } catch (pe: IllegalArgumentException) { From dd2f56a13cab9bb5aab69ece6e63286c1b8ed398 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 13 May 2024 16:31:18 +0100 Subject: [PATCH 10/28] fix(ndk): added thread.type to ndk events, and removed invalid / paused sessions --- .../src/main/java/com/bugsnag/android/EventStore.kt | 1 + .../src/main/jni/utils/serializer/event_writer.c | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt index 6eb471d22d..e6d8401ae9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt @@ -196,6 +196,7 @@ internal class EventStore( return null } } catch (ioe: Exception) { + logger.w("could not parse event payload", ioe) eventSource.clear() } val processedEvent = eventSource.event diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 5d92bdda4d..3073037abd 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -25,6 +25,7 @@ strnlen((value), sizeof((value)))) #define STRING_NOT_EMPTY(s) (*(s) != 0) +#define STRING_IS_EMPTY(s) (*(s) == 0) #define STR_CONST_CAT(dst, src) bsg_strncpy((dst), (src), sizeof(src)) static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, @@ -601,6 +602,7 @@ static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, CHECKED(bsg_ksjsonaddUIntegerElement(json, "id", thread->id)); CHECKED(JSON_LIMITED_STRING_ELEMENT("name", thread->name)); CHECKED(JSON_LIMITED_STRING_ELEMENT("state", thread->state)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); } CHECKED(bsg_ksjsonendContainer(json)); } @@ -643,6 +645,11 @@ static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, static bool bsg_write_session(BSG_KSJSONEncodeContext *json, bugsnag_event *event) { + + if (STRING_IS_EMPTY(event->session_id)) { + return true; + } + CHECKED(bsg_ksjsonbeginObject(json, "session")); { CHECKED(JSON_LIMITED_STRING_ELEMENT("id", event->session_id)); From 09751aa99097c23df7a5d36fe530531d1f6c916c Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 13 May 2024 17:49:09 +0100 Subject: [PATCH 11/28] fix(ndk): fixed NDK thread writing behaviour --- .../src/main/jni/utils/serializer/event_writer.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 3073037abd..22ba5352d3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -589,17 +589,16 @@ static bool bsg_write_breadcrumbs(BSG_KSJSONEncodeContext *json, static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, bsg_thread *threads, int thread_count) { - if (thread_count == 0) { - return true; - } - CHECKED(bsg_ksjsonbeginArray(json, "threads")); { for (int i = 0; i < thread_count; i++) { bsg_thread *thread = &threads[i]; + char id_string[30]; + bsg_uint64_to_string(thread->id, id_string); + CHECKED(bsg_ksjsonbeginObject(json, NULL)); { - CHECKED(bsg_ksjsonaddUIntegerElement(json, "id", thread->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", id_string)); CHECKED(JSON_LIMITED_STRING_ELEMENT("name", thread->name)); CHECKED(JSON_LIMITED_STRING_ELEMENT("state", thread->state)); CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); From ece18258642be01be51f0535ece245497a017cb7 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 14 May 2024 11:01:46 +0100 Subject: [PATCH 12/28] fix(ndk): added the static usage metrics data to the native events --- .../android/internal/InternalMetricsImpl.kt | 2 +- .../main/jni/utils/serializer/event_writer.c | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt index 6e648e5f48..0900b9166c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt @@ -53,7 +53,7 @@ class InternalMetricsImpl(source: Map? = null) : InternalMetrics { // This is currently the only place where we set static data. // When that changes in future, we'll need a StaticData object to properly merge data // coming from multiple sources. - NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences))) + NdkPluginCaller.setStaticData(mapOf("config" to configDifferences)) } override fun setCallbackCounts(newCallbackCounts: Map) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 22ba5352d3..1885dc3192 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -48,7 +48,7 @@ static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, static bool bsg_write_session(BSG_KSJSONEncodeContext *json, bugsnag_event *event); static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, - bugsnag_event *event); + bsg_environment *env); static int bsg_write(const char *data, size_t length, void *userData) { bsg_buffered_writer *writer = userData; @@ -160,7 +160,7 @@ bool bsg_write_event_file(bsg_environment *env, const char *filename) { CHECKED( JSON_LIMITED_STRING_ELEMENT("groupingHash", event->grouping_hash)); } - if (!bsg_write_usage(json, event)) { + if (!bsg_write_usage(json, env)) { goto error; } if (!bsg_write_threads(json, event->threads, event->thread_count)) { @@ -671,7 +671,8 @@ static bool bsg_write_session(BSG_KSJSONEncodeContext *json, } static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, - bugsnag_event *event) { + bsg_environment *env) { + bugsnag_event *event = &env->next_event; CHECKED(bsg_ksjsonbeginObject(json, "usage")); { CHECKED(bsg_ksjsonbeginObject(json, "callbacks")); @@ -695,6 +696,16 @@ static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, } } CHECKED(bsg_ksjsonendContainer(json)); + + if (env->static_json_data != NULL) { + const size_t length = strlen(env->static_json_data); + // the static_json_data *must* be more than simply "{}" + if (length > 2) { + CHECKED(bsg_ksjsonaddRawJSONData(json, ",", 1)); + CHECKED(bsg_ksjsonaddRawJSONData(json, &(env->static_json_data[1]), + length - 2)); + } + } } CHECKED(bsg_ksjsonendContainer(json)); From 0abb887035ef4b664109fbd1a3d48ee35d5093e3 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 15 May 2024 09:50:28 +0100 Subject: [PATCH 13/28] test(ndk): added a basic ReportDiscardScanner test --- .../android/ndk/ReportDiscardScannerTest.kt | 38 +++++++++++++++++++ .../android/ndk/ReportDiscardScanner.kt | 20 +++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt new file mode 100644 index 0000000000..889cc207c6 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt @@ -0,0 +1,38 @@ +package com.bugsnag.android.ndk + +import com.bugsnag.android.Client +import com.bugsnag.android.Logger +import com.bugsnag.android.NativeInterface +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import java.io.File + +@RunWith(MockitoJUnitRunner::class) +class ReportDiscardScannerTest { + @Mock + lateinit var client: Client + + @Before + fun setupNativeInterface() { + NativeInterface.setClient(client) + } + + @Test + fun discardStaticData() { + val discardScanner = ReportDiscardScanner(object : Logger {}, emptySet()) + assertTrue(discardScanner.shouldDiscard(File("/data/data/something/there_is_some.static_data.json"))) + } + + @Test + fun discardNonJson() { + val discardScanner = ReportDiscardScanner(object : Logger {}, emptySet()) + assertTrue(discardScanner.shouldDiscard(File("/data/data/683c6b92-b325-4987-80ad-77086509ca1e.dump"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/683c6b92-b325-4987-80ad-77086509ca1e.binary"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/something_not_quite.static_data.binary"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/data.binary"))) + } +} diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt index 8395e741a0..cb48661821 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt @@ -2,11 +2,16 @@ package com.bugsnag.android.ndk import android.util.JsonReader import android.util.JsonToken +import androidx.annotation.VisibleForTesting import com.bugsnag.android.Logger import com.bugsnag.android.NativeInterface import java.io.File -internal class ReportDiscardScanner(private val logger: Logger) { +internal class ReportDiscardScanner( + private val logger: Logger, + private val enabledReleaseStages: Collection = + NativeInterface.getEnabledReleaseStages() ?: emptySet(), +) { /** * Checks whether a given report file should be discarded due to its `releaseStage` or any of * the configured `discardClasses`. @@ -14,6 +19,12 @@ internal class ReportDiscardScanner(private val logger: Logger) { * @return true if the report should be discarded instead of being sent */ fun shouldDiscard(report: File): Boolean { + if (!report.name.endsWith(".json") || + report.name.endsWith(".static_data.json") + ) { + return true + } + return try { report.bufferedReader().use { reader -> JsonReader(reader).use { json -> shouldDiscard(json) } @@ -23,7 +34,8 @@ internal class ReportDiscardScanner(private val logger: Logger) { } } - private fun shouldDiscard(json: JsonReader): Boolean { + @VisibleForTesting + internal fun shouldDiscard(json: JsonReader): Boolean { json.beginObject() var pendingAppCheck = true var pendingExceptionsCheck = true @@ -56,8 +68,7 @@ internal class ReportDiscardScanner(private val logger: Logger) { } private fun shouldDiscardForApp(json: JsonReader): Boolean { - val enabledReleaseStages = NativeInterface.getEnabledReleaseStages() - if (enabledReleaseStages.isNullOrEmpty()) { + if (enabledReleaseStages.isEmpty()) { json.skipValue() return false } @@ -69,7 +80,6 @@ internal class ReportDiscardScanner(private val logger: Logger) { "releaseStage" -> { val releaseStage = json.nextString() if (releaseStage !in enabledReleaseStages) { - logger.d("Discarding native report due to releaseStage") return true } // do not early exit, make sure the entire "app" object is consumed From 6c175399e78892a2ae82ea008f42ed460e95c5f5 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 21 May 2024 16:23:38 +0100 Subject: [PATCH 14/28] refactor(ndk event): replaced STRING_**_EMPTY macros with inline functions --- .../main/jni/utils/serializer/event_writer.c | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 1885dc3192..badc66252b 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -24,8 +24,6 @@ bsg_ksjsonaddStringElement(json, (name), (value), \ strnlen((value), sizeof((value)))) -#define STRING_NOT_EMPTY(s) (*(s) != 0) -#define STRING_IS_EMPTY(s) (*(s) == 0) #define STR_CONST_CAT(dst, src) bsg_strncpy((dst), (src), sizeof(src)) static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, @@ -50,6 +48,14 @@ static bool bsg_write_session(BSG_KSJSONEncodeContext *json, static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, bsg_environment *env); +static inline bool string_is_not_empty(const char *restrict s) { + return (*(s) != 0); +} + +static inline bool string_is_empty(const char *restrict s) { + return (*(s) == 0); +} + static int bsg_write(const char *data, size_t length, void *userData) { bsg_buffered_writer *writer = userData; return writer->write(writer, data, length) ? BSG_KSJSON_OK @@ -156,7 +162,7 @@ bool bsg_write_event_file(bsg_environment *env, const char *filename) { event->max_crumb_count)) { goto error; } - if (STRING_NOT_EMPTY(event->grouping_hash)) { + if (string_is_not_empty(event->grouping_hash)) { CHECKED( JSON_LIMITED_STRING_ELEMENT("groupingHash", event->grouping_hash)); } @@ -339,18 +345,18 @@ static bool bsg_write_stackframe(BSG_KSJSONEncodeContext *json, CHECKED(bsg_ksjsonaddBooleanElement(json, "isPC", true)); } - if (STRING_NOT_EMPTY(frame->filename)) { + if (string_is_not_empty(frame->filename)) { CHECKED(JSON_LIMITED_STRING_ELEMENT("file", frame->filename)); } - if (STRING_NOT_EMPTY(frame->method)) { + if (string_is_not_empty(frame->method)) { CHECKED(JSON_LIMITED_STRING_ELEMENT("method", frame->method)); } else { bsg_uint64_to_hex(frame->symbol_address, hex_output_buffer, 1); CHECKED(JSON_LIMITED_STRING_ELEMENT("method", hex_str)); } - if (STRING_NOT_EMPTY(frame->code_identifier)) { + if (string_is_not_empty(frame->code_identifier)) { CHECKED(JSON_LIMITED_STRING_ELEMENT("codeIdentifier", frame->code_identifier)); } @@ -411,9 +417,9 @@ static bool bsg_write_error(BSG_KSJSONEncodeContext *json, bsg_error *error) { } static bool bsg_write_user(BSG_KSJSONEncodeContext *json, bugsnag_user *user) { - const bool has_id = STRING_NOT_EMPTY(user->id); - const bool has_name = STRING_NOT_EMPTY(user->name); - const bool has_email = STRING_NOT_EMPTY(user->email); + const bool has_id = string_is_not_empty(user->id); + const bool has_name = string_is_not_empty(user->name); + const bool has_email = string_is_not_empty(user->email); const bool has_user = has_id || has_name || has_email; if (has_user) { @@ -448,7 +454,7 @@ static bool bsg_write_app(BSG_KSJSONEncodeContext *json, bsg_app_info *app) { CHECKED(JSON_LIMITED_STRING_ELEMENT("releaseStage", app->release_stage)); CHECKED( bsg_ksjsonaddIntegerElement(json, "versionCode", app->version_code)); - if (STRING_NOT_EMPTY(app->build_uuid)) { + if (string_is_not_empty(app->build_uuid)) { CHECKED(JSON_LIMITED_STRING_ELEMENT("buildUUID", app->build_uuid)); } @@ -645,7 +651,7 @@ static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, static bool bsg_write_session(BSG_KSJSONEncodeContext *json, bugsnag_event *event) { - if (STRING_IS_EMPTY(event->session_id)) { + if (string_is_empty(event->session_id)) { return true; } From 74b60016c8c5823656516f862fa2a08afc046d40 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 21 May 2024 16:40:31 +0100 Subject: [PATCH 15/28] refactor(ndk event): copy the entire event_path instead of relying on it fitting into a preallocated buffer --- bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c | 5 +---- bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h | 2 +- .../src/main/jni/utils/serializer/event_writer.c | 2 +- bugsnag-plugin-android-ndk/src/test/cpp/main.c | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) 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 eb0abe2beb..13278dfceb 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -22,8 +22,6 @@ extern "C" { #endif -#define STATIC_DATA_FILENAME_EXTENSION ".static_data.json" - static bsg_environment *bsg_global_env; static pthread_mutex_t bsg_global_env_write_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -178,8 +176,7 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( if (event_path == NULL) { goto error; } - bsg_strncpy(bugsnag_env->event_path, event_path, - sizeof(bugsnag_env->event_path)); + bugsnag_env->event_path = strdup(event_path); bsg_safe_release_string_utf_chars(env, _event_path, event_path); // copy the event UUID to the env struct 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 aff3673033..411c7fffdc 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -24,7 +24,7 @@ typedef struct { /** * File path on disk where the next crash report will be written if needed. */ - char event_path[384]; + char *event_path; /** * The pre-generated UUID of the next crash report. */ diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index badc66252b..451982d800 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -70,7 +70,7 @@ static size_t build_filename(bsg_environment *env, char *out) { time_t now; time(&now); - int length = strnlen(env->event_path, sizeof(env->event_path)); + int length = strnlen(env->event_path, 4096); memcpy(out, env->event_path, length); out[length++] = '/'; diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/main.c b/bugsnag-plugin-android-ndk/src/test/cpp/main.c index 600d5a46b0..2ab71894fb 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -71,7 +71,7 @@ JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run( bugsnag_event *event = init_event(); memcpy(&env.next_event, event, sizeof(bugsnag_event)); - strcpy(env.event_path, dir); + env.event_path = strdup(dir); strcpy(env.event_uuid, "test-uuid"); bsg_event_write(&env); From 3161e39df656958f47c67de9e81d79c9e0bf75d3 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 22 May 2024 08:34:43 +0100 Subject: [PATCH 16/28] refactor(ndk event): NaN & Inf values are encoded to JSON as 'null' --- .../src/main/jni/utils/serializer/BSG_KSJSONCodec.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c index a543178136..be03d36e5b 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c @@ -324,6 +324,9 @@ int bsg_ksjsonaddFloatingPointElement(BSG_KSJSONEncodeContext *const context, const char *const name, double value) { int result = bsg_ksjsonbeginElement(context, name); unlikely_if(result != BSG_KSJSON_OK) { return result; } + unlikely_if(isnan(value) || isinf(value)) { + return addJSONData(context, "null", 4); + } char buff[30]; bsg_double_to_string(value, buff, MAX_SIGNIFICANT_DIGITS); return addJSONData(context, buff, strlen(buff)); From 6c2964101d1eff45535b6ba71b4bf583c69e7baf Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Wed, 22 May 2024 10:18:22 +0100 Subject: [PATCH 17/28] Update AGP and related versions for mazerunner (#2030) * feat(gradle) update AGP and related versions for mazerunner * feat(gradle) update AGP and related versions for mazerunner --- .buildkite/pipeline.full.yml | 6 +++++- .buildkite/pipeline.yml | 6 ++++++ Makefile | 18 +++++++++--------- features/fixtures/mazerunner/app/build.gradle | 3 ++- .../mazerunner/app/detekt-baseline.xml | 4 ++-- .../fixtures/mazerunner/app/proguard-rules.pro | 2 ++ .../app/src/main/AndroidManifest.xml | 3 +-- features/fixtures/mazerunner/build.gradle | 6 +++--- .../cxx-scenarios-bugsnag/build.gradle | 1 + .../cxx-scenarios-bugsnag/detekt-baseline.xml | 4 ++-- .../src/main/AndroidManifest.xml | 2 +- .../mazerunner/cxx-scenarios/build.gradle | 1 + .../cxx-scenarios/detekt-baseline.xml | 4 ++-- .../cxx-scenarios/src/main/AndroidManifest.xml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../mazerunner/jvm-scenarios/build.gradle | 3 ++- .../jvm-scenarios/detekt-baseline.xml | 10 +++++----- .../jvm-scenarios/src/main/AndroidManifest.xml | 2 +- features/smoke_tests/02_handled.feature | 8 ++++---- features/smoke_tests/04_unhandled.feature | 4 ++-- 20 files changed, 53 insertions(+), 38 deletions(-) diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index eb0a5deac6..88b261970f 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -6,14 +6,16 @@ steps: queue: macos-14 artifact_paths: build/fixture-minimal.apk command: make fixture-minimal + env: + JAVA_VERSION: 17 - label: ':android: Build Example App' timeout_in_minutes: 5 agents: queue: macos-14 + command: 'make example-app' env: JAVA_VERSION: 17 - command: 'make example-app' - label: ':android: Build debug fixture APK' key: "fixture-debug" @@ -24,6 +26,8 @@ steps: - "build/fixture-debug.apk" - "build/fixture-debug/*" command: make fixture-debug + env: + JAVA_VERSION: 17 - label: ':android: Build Scan' timeout_in_minutes: 10 diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 17e408882d..04b5d7d0c1 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -21,6 +21,8 @@ steps: - bundle install - make fixture-r19 - bundle exec upload-app --farm=bb --app=./build/fixture-r19.apk --app-id-file=build/fixture-r19-url.txt + env: + JAVA_VERSION: 17 - label: ':android: Build fixture APK r21' key: "fixture-r21" @@ -35,6 +37,8 @@ steps: - bundle install - make fixture-r21 - bundle exec upload-app --farm=bb --app=./build/fixture-r21.apk --app-id-file=build/fixture-r21-url.txt + env: + JAVA_VERSION: 17 - label: ':android: Coding standards checks' timeout_in_minutes: 20 @@ -67,6 +71,8 @@ steps: commands: - cd features/fixtures/mazerunner - ./gradlew ktlintCheck detekt checkstyle + env: + JAVA_VERSION: 17 - label: ':android: Android size reporting' timeout_in_minutes: 10 diff --git a/Makefile b/Makefile index 0e5bba7b8f..2ee1038902 100644 --- a/Makefile +++ b/Makefile @@ -34,31 +34,31 @@ notifier: fixture-r19: notifier # Build the r19 test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=19.2.5345600 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=19.2.5345600 \ -PTEST_FIXTURE_NAME=fixture-r19.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release r19 fixture-r21: notifier # Build the r21 test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=21.4.7075529 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=21.4.7075529 \ -PTEST_FIXTURE_NAME=fixture-r21.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release r21 fixture-minimal: notifier # Build the minimal test fixture - @./gradlew -PMINIMAL_FIXTURE=true \ + @cd ./features/fixtures/mazerunner && ./gradlew -PMINIMAL_FIXTURE=true \ -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ -PTEST_FIXTURE_NAME=fixture-minimal.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release minimal fixture-debug: notifier # Build the minimal test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ -PTEST_FIXTURE_NAME=fixture-debug.apk \ - -p=features/fixtures/mazerunner assembleDebug -x check + assembleDebug -x check @ruby scripts/copy-build-files.rb debug debug example-app: @@ -86,4 +86,4 @@ endif check: @./gradlew lint detekt ktlintCheck checkstyle @./scripts/run-cpp-check.sh - @./scripts/run-clang-format-ci-check.sh + @./scripts/run-clang-format-ci-check.sh \ No newline at end of file diff --git a/features/fixtures/mazerunner/app/build.gradle b/features/fixtures/mazerunner/app/build.gradle index aab4b30864..2534707394 100644 --- a/features/fixtures/mazerunner/app/build.gradle +++ b/features/fixtures/mazerunner/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { minSdkVersion 17 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 34 versionName "1.1.14" manifestPlaceholders = [ @@ -79,6 +79,7 @@ android { kotlinOptions { jvmTarget = "1.8" } + namespace 'com.bugsnag.android.mazerunner' } dependencies { diff --git a/features/fixtures/mazerunner/app/detekt-baseline.xml b/features/fixtures/mazerunner/app/detekt-baseline.xml index a708a443f8..1cca4dc6c5 100644 --- a/features/fixtures/mazerunner/app/detekt-baseline.xml +++ b/features/fixtures/mazerunner/app/detekt-baseline.xml @@ -1,6 +1,6 @@ - + - + MagicNumber:MainActivity.kt$MainActivity$1000 MagicNumber:MainActivity.kt$MainActivity$250 diff --git a/features/fixtures/mazerunner/app/proguard-rules.pro b/features/fixtures/mazerunner/app/proguard-rules.pro index ba642cad1e..759bdb97ed 100644 --- a/features/fixtures/mazerunner/app/proguard-rules.pro +++ b/features/fixtures/mazerunner/app/proguard-rules.pro @@ -1,2 +1,4 @@ -keep class com.bugsnag.android.mazerunner.** {*;} -keep class com.bugsnag.android.DeliveryDelegate {*;} +-keepattributes LineNumberTable,SourceFile +-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml index d0439114c9..a152aba337 100644 --- a/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index 09adc72ba8..0076b5b5f7 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -18,16 +18,16 @@ buildscript { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } - ext.kotlin_version = "1.4.32" + ext.kotlin_version = "1.8.20" dependencies { def agpVersion = project.hasProperty("USE_AGP_VERSION") ? project.property("USE_AGP_VERSION") - : "7.1.0" + : "8.3.2" project.logger.lifecycle("Using AGP $agpVersion") classpath "com.android.tools.build:gradle:$agpVersion" - classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.1.0" + classpath "com.bugsnag:bugsnag-android-gradle-plugin:8.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.2.0" diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle index 3d2486d0c1..1cc184d0e3 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle @@ -37,6 +37,7 @@ android { buildFeatures.prefab = true packagingOptions.jniLibs.pickFirsts += ["**/libbugsnag-ndk.so"] + namespace 'com.bugsnag.android.mazerunner.cxxscenariosbugsnag' } dependencies { diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml index 1c1387a874..a4f1610554 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml @@ -1,6 +1,6 @@ - + - + MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$500 MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$999 diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml index 855e977fee..8072ee00db 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/fixtures/mazerunner/cxx-scenarios/build.gradle b/features/fixtures/mazerunner/cxx-scenarios/build.gradle index ca3600068b..a9723680de 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/build.gradle +++ b/features/fixtures/mazerunner/cxx-scenarios/build.gradle @@ -34,6 +34,7 @@ android { path "CMakeLists.txt" } } + namespace 'com.bugsnag.android.mazerunner.cxxscenarios' } dependencies { diff --git a/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml index 104065a6ca..63419479af 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml @@ -1,6 +1,6 @@ - + - + MagicNumber:CXXDelayedCrashScenario.kt$CXXDelayedCrashScenario$405 MagicNumber:CXXIgnoredSigabrtScenario.kt$CXXIgnoredSigabrtScenario$2726 diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml index bdb6ed5246..8072ee00db 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties b/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties index d2880ba800..e411586a54 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.3.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/features/fixtures/mazerunner/jvm-scenarios/build.gradle b/features/fixtures/mazerunner/jvm-scenarios/build.gradle index c14c318d52..5ac7256c16 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/build.gradle +++ b/features/fixtures/mazerunner/jvm-scenarios/build.gradle @@ -32,6 +32,7 @@ android { lintOptions { tasks.lint.enabled = false } + namespace 'com.bugsnag.android.mazerunner.jvmscenarios' } dependencies { @@ -42,7 +43,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:3.12.0" } else { project.logger.lifecycle("Using OkHttp 4 dependency in test fixture") - implementation "com.squareup.okhttp3:okhttp:4.9.1" + implementation "com.squareup.okhttp3:okhttp:4.12.0" } implementation "com.bugsnag:bugsnag-android-performance:1.2.2" } diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index d18bb02cbd..cbe2ac2438 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -1,9 +1,9 @@ - + - + MagicNumber:AnrHelper.kt$1000 - MagicNumber:AnrHelper.kt$<no name provided>$60000 + MagicNumber:AnrHelper.kt$<no name provided>$60000 MagicNumber:AutoDetectAnrsFalseScenario.kt$AutoDetectAnrsFalseScenario$100000 MagicNumber:AutoDetectAnrsTrueScenario.kt$AutoDetectAnrsTrueScenario$100000 MagicNumber:BugsnagInitScenario.kt$BugsnagInitScenario$25 @@ -22,11 +22,11 @@ MagicNumber:Scenario.kt$Scenario$100 MagicNumber:Scenario.kt$Scenario$1000 MagicNumber:StartupCrashFlushScenario.kt$StartupCrashFlushScenario$6000 - MagicNumber:TestHarnessHooks.kt$<no name provided>$500 + MagicNumber:TestHarnessHooks.kt$<no name provided>$500 MagicNumber:TrimmedStacktraceScenario.kt$TrimmedStacktraceScenario$100000 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123456 - ThrowingExceptionsWithoutMessageOrCause:AnrHelper.kt$<no name provided>$IllegalStateException() + ThrowingExceptionsWithoutMessageOrCause:AnrHelper.kt$<no name provided>$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:BugsnagInitScenario.kt$BugsnagInitScenario$RuntimeException() ThrowingExceptionsWithoutMessageOrCause:CustomPluginNotifierDescriptionScenario.kt$CustomPluginNotifierDescriptionScenario$RuntimeException() ThrowingExceptionsWithoutMessageOrCause:TestHarnessHooks.kt$RuntimeException() diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml index 15312f0fe5..8072ee00db 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/smoke_tests/02_handled.feature b/features/smoke_tests/02_handled.feature index f14f3ec91a..5f2d5d7a51 100644 --- a/features/smoke_tests/02_handled.feature +++ b/features/smoke_tests/02_handled.feature @@ -22,9 +22,9 @@ Feature: Handled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "HandledJavaSmokeScenario.startScenario" - And the exception "stacktrace.0.file" equals "HandledJavaSmokeScenario.java" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 8 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 56 And the event "exceptions.0.stacktrace.0.inProject" is true And the error payload field "events.0.projectPackages" is a non-empty array And the event "projectPackages.0" equals "com.bugsnag.android.mazerunner" @@ -133,9 +133,9 @@ Feature: Handled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "generateException" - And the exception "stacktrace.0.file" equals "Scenario.kt" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 1 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 11 And the event "exceptions.0.stacktrace.0.inProject" is true # Overwritten App data diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature index 31d183d5c1..d25c411172 100644 --- a/features/smoke_tests/04_unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -25,9 +25,9 @@ Feature: Unhandled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "UnhandledJavaLoadedConfigScenario.startScenario" - And the exception "stacktrace.0.file" equals "UnhandledJavaLoadedConfigScenario.java" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 7 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 41 And the event "exceptions.0.stacktrace.0.inProject" is true And the thread with name "main" contains the error reporting flag From 9f62d7adb345f9d07ce53d3aa8f821e2d19222ad Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Fri, 24 May 2024 09:29:32 +0100 Subject: [PATCH 18/28] fix(Session Tracker) fixed logic of discarding session (#2033) --- CHANGELOG.md | 7 +++++++ .../src/main/java/com/bugsnag/android/SessionTracker.java | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e501cbd60..bb2b1d9094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## TBD + +### Bug fixes + +* Corrected the behavior when `Bugsnag.startSession` is called when `config.autoTrackSessions=true`, the first automatic session will now be correctly discarded + [#2033](https://github.com/bugsnag/bugsnag-android/pull/2033) + ## 6.5.0 (2024-05-15) ### Enhancements diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java index 9fc2747f5b..25fb7058c4 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java @@ -36,7 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi private volatile Session currentSession = null; final BackgroundTaskService backgroundTaskService; final Logger logger; - private boolean shouldSuppressFirstAutoSession = false; + private boolean shouldSuppressFirstAutoSession = true; SessionTracker(ImmutableConfig configuration, CallbackState callbackState, @@ -108,9 +108,13 @@ private boolean shouldDiscardSession(boolean autoCaptured) { && existingSession != null && !existingSession.isAutoCaptured() && shouldSuppressFirstAutoSession) { - shouldSuppressFirstAutoSession = true; + shouldSuppressFirstAutoSession = false; return true; } + + if (autoCaptured) { + shouldSuppressFirstAutoSession = false; + } } return false; } From 5bc8d99973f613db012ee35525345cdbbf222e8a Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Fri, 31 May 2024 09:21:07 +0100 Subject: [PATCH 19/28] fix(errorCallback) separating throw exception out (#2036) --- bugsnag-android-core/detekt-baseline.xml | 1 - .../mazerunner/app/detekt-baseline.xml | 1 + ...ndledExceptionEventDetailChangeScenario.kt | 15 +++-- .../error_callback_alters_fields.feature | 60 ++++++++++++++++++- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index a5e99e257b..1200bc7906 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -55,7 +55,6 @@ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$exception: Exception SwallowedException:DeviceIdFilePersistence.kt$DeviceIdFilePersistence$exc: OverlappingFileLockException SwallowedException:EventStore.kt$EventStore$exception: RejectedExecutionException - SwallowedException:EventStore.kt$EventStore$ioe: Exception SwallowedException:ForegroundDetector.kt$ForegroundDetector$e: Exception SwallowedException:ImmutableConfig.kt$e: Exception SwallowedException:JsonHelperTest.kt$JsonHelperTest$e: IllegalArgumentException diff --git a/features/fixtures/mazerunner/app/detekt-baseline.xml b/features/fixtures/mazerunner/app/detekt-baseline.xml index 1cca4dc6c5..c54e54cfd0 100644 --- a/features/fixtures/mazerunner/app/detekt-baseline.xml +++ b/features/fixtures/mazerunner/app/detekt-baseline.xml @@ -2,6 +2,7 @@ + ComplexMethod:MainActivity.kt$MainActivity$// Starts a thread to poll for Maze Runner actions to perform private fun startCommandRunner() MagicNumber:MainActivity.kt$MainActivity$1000 MagicNumber:MainActivity.kt$MainActivity$250 SwallowedException:NetworkStatus.kt$catch (e: Exception) { NetworkStatus.NO_INTERNET } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt index fdacbf4c3e..2cac1f5885 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt @@ -17,7 +17,6 @@ internal class UnhandledExceptionEventDetailChangeScenario( ) : Scenario(config, context, eventMetadata) { init { - config.addOnError( OnErrorCallback { event -> event.apiKey = "0000111122223333aaaabbbbcccc9999" @@ -75,12 +74,18 @@ internal class UnhandledExceptionEventDetailChangeScenario( Bugsnag.addMetadata("custom_data1", "data", "hello") Bugsnag.addMetadata("custom_data2", "data", "hello") - Bugsnag.addMetadata("custom_data3", "test data", "divert all available power to the crash reporter") - + Bugsnag.addMetadata( + "custom_data3", + "test data", + "divert all available power to the crash reporter" + ) Bugsnag.addFeatureFlag("test1") Bugsnag.addFeatureFlag("test2") - Bugsnag.notify(RuntimeException("UnhandledExceptionEventDetailChangeScenario")) - throw NullPointerException("something broke") + if (eventMetadata == "notify") { + Bugsnag.notify(RuntimeException("UnhandledExceptionEventDetailChangeScenario")) + } else { + throw NullPointerException("something broke") + } } } diff --git a/features/full_tests/error_callback_alters_fields.feature b/features/full_tests/error_callback_alters_fields.feature index 2748be9376..d851efd911 100644 --- a/features/full_tests/error_callback_alters_fields.feature +++ b/features/full_tests/error_callback_alters_fields.feature @@ -3,11 +3,67 @@ Feature: When the api key is altered in an Event the JSON payload reflects this Background: Given I clear all persistent data - Scenario: Unhandled exception with altered event details - When I run "UnhandledExceptionEventDetailChangeScenario" and relaunch the crashed app + Scenario: Crash exception with altered event details + When I configure the app to run in the "crash" state + And I run "UnhandledExceptionEventDetailChangeScenario" and relaunch the crashed app And I configure Bugsnag for "UnhandledExceptionApiKeyChangeScenario" And I wait to receive an error And the error payload field "events" is an array with 1 elements + And the exception "message" equals "something broke" + And the error payload field "apiKey" equals "0000111122223333aaaabbbbcccc9999" + And the error "Bugsnag-Api-Key" header equals "0000111122223333aaaabbbbcccc9999" + And the event "severity" equals "error" + And the event "context" equals "new-context" + And the event "groupingHash" equals "groupingHash1" + And the event "user.id" equals "abc" + And the event "user.email" equals "joe@test.com" + And the event "user.name" equals "Joe" + And the event "metaData.custom_data2.test_data" equals "this is test" + And the event "metaData.custom_data1" is null + And the event "metaData.custom_data2.data" is null + And the event "metaData.custom_data3.test data" equals "divert all available power to the crash reporter" + And event 0 contains the feature flag "beta" with variant "b" + And event 0 does not contain the feature flag "alpha" + And event 0 contains the feature flag "gamma" with no variant + And event 0 does not contain the feature flag "test1" + And event 0 contains the feature flag "test2" with no variant + + # app fields + And the event "unhandled" is false + And the event "app.binaryArch" equals "x86" + And the event "app.id" equals "12345" + And the event "app.releaseStage" equals "custom" + And the event "app.version" equals "1.2.3" + And the event "app.buildUUID" equals "12345678" + And the event "app.type" equals "android_custom" + And the event "app.versionCode" equals 123 + And the event "app.duration" equals 123456 + And the event "app.durationInForeground" equals 123456 + And the event "app.inForeground" is false + And the event "app.isLaunching" is false + + # device fields + And the event "device.id" equals "12345" + And the event "device.jailbroken" is true + And the event "device.locale" equals "en-UK" + And the event "device.totalMemory" equals 123456 + And the event "device.runtimeVersions.androidApiLevel" equals "30" + And the event "device.freeDisk" equals 123456 + And the event "device.freeMemory" equals 123456 + And the event "device.orientation" equals "portrait" + + # breadcrumbs fields + And the event "breadcrumbs.0.type" equals "error" + And the event "breadcrumbs.0.name" equals "new breadcrumb message" + And the event "breadcrumbs.0.metaData.foo" equals "data" + And the event "breadcrumbs.1.type" equals "error" + And the event "breadcrumbs.1.name" equals "Second breadcrumb message" + + Scenario: Unhandled exception with altered event details + When I configure the app to run in the "notify" state + And I run "UnhandledExceptionEventDetailChangeScenario" + And I wait to receive an error + And the error payload field "events" is an array with 1 elements And the exception "message" equals "UnhandledExceptionEventDetailChangeScenario" And the error payload field "apiKey" equals "0000111122223333aaaabbbbcccc9999" And the error "Bugsnag-Api-Key" header equals "0000111122223333aaaabbbbcccc9999" From 2201a506e2f5a0f198679f86cfb46ed29ccaf79e Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 30 May 2024 09:53:20 +0100 Subject: [PATCH 20/28] refactor(ndk event): iterate directly over metadata elements in NDK instead of using ArrayList --- .../src/main/jni/metadata.c | 210 +++++++----------- 1 file changed, 77 insertions(+), 133 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index f466322c9a..0968a3d380 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -376,101 +376,71 @@ static void populate_metadata_value(JNIEnv *env, bugsnag_metadata *dst, } 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_jni_cache->initialized) { - goto exit; - } - - section_key = bsg_safe_call_object_method( - env, section_keylist, bsg_jni_cache->ArrayList_get, (jint)index); - if (section_key == NULL) { - goto exit; - } + const char *section, jobject section_map) { + jobject entryset = NULL; + jobject entries = NULL; - _value = bsg_safe_call_object_method(env, section, bsg_jni_cache->Map_get, - section_key); - name = bsg_safe_get_string_utf_chars(env, section_key); - if (name == NULL) { + if (section_map == 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); -} - -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; - if (!bsg_jni_cache->initialized) { goto exit; } - _key = bsg_safe_call_object_method(env, keylist, bsg_jni_cache->ArrayList_get, - (jint)i); - if (_key == NULL) { - goto exit; - } - section = bsg_safe_get_string_utf_chars(env, _key); - if (section == NULL) { - goto exit; - } - _section = - bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_get, _key); - if (_section == NULL) { + // get size of metadata map + jint map_size = + bsg_safe_call_int_method(env, section_map, bsg_jni_cache->Map_size); + if (map_size <= 0) { goto exit; } - jint section_size = - bsg_safe_call_int_method(env, _section, bsg_jni_cache->Map_size); - if (section_size == -1) { + + // create a list of metadata keys + entryset = bsg_safe_call_object_method(env, section_map, + bsg_jni_cache->Map_entrySet); + if (entryset == NULL) { goto exit; } - section_keyset = - bsg_safe_call_object_method(env, _section, bsg_jni_cache->Map_keySet); - if (section_keyset == NULL) { + entries = + bsg_safe_call_object_method(env, entryset, bsg_jni_cache->Set_iterator); + if (entries == NULL) { goto exit; } - section_keylist = bsg_safe_new_object( - env, bsg_jni_cache->ArrayList, - bsg_jni_cache->ArrayList_constructor_collection, section_keyset); - if (section_keylist == NULL) { - goto exit; - } - for (int j = 0; j < section_size; j++) { - populate_metadata_obj(env, dst, _section, section_keylist, j); + // we iterate against the size of the Map, avoiding calling Iterator.hasNext() + // on each loop (avoiding the small JNI overhead). Any concurrent modification + // will be caught as an exception and handled by bsg_safe_call_object_method + for (int i = 0; i < map_size; i++) { + (*env)->PushLocalFrame(env, 3); + { + jobject entry = bsg_safe_call_object_method(env, entries, + bsg_jni_cache->Iterator_next); + jstring _key = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getKey); + jobject _value = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getValue); + + if (_key != NULL && _value != NULL) { + const char *key = bsg_safe_get_string_utf_chars(env, _key); + if (key != NULL) { + populate_metadata_value(env, dst, section, key, _value); + bsg_safe_release_string_utf_chars(env, _key, key); + } + } + } + (*env)->PopLocalFrame(env, NULL); } - 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, entries); + bsg_safe_delete_local_ref(env, entryset); } // Internal API - void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, jobject metadata) { jobject _metadata = NULL; - jobject keyset = NULL; - jobject keylist = NULL; + jobject entrySet = NULL; + jobject entryIterator = NULL; if (!bsg_jni_cache->initialized) { goto exit; @@ -489,90 +459,64 @@ void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, } int size = bsg_safe_call_int_method(env, metadata, bsg_jni_cache->Map_size); - if (size == -1) { + if (size <= 0) { goto exit; } - // create a list of metadata keys - keyset = bsg_safe_call_static_object_method(env, metadata, - bsg_jni_cache->Map_keySet); - if (keyset == NULL) { + // retrieve the Map.Entry set + entrySet = + bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_entrySet); + if (entrySet == NULL) { goto exit; } - keylist = bsg_safe_new_object(env, bsg_jni_cache->ArrayList, - bsg_jni_cache->ArrayList_constructor_collection, - keyset); - if (keylist == NULL) { + + // retrieve an Iterator + entryIterator = + bsg_safe_call_object_method(env, entrySet, bsg_jni_cache->Set_iterator); + if (entryIterator == NULL) { goto exit; } + // we iterate against the size of the Map, avoiding calling Iterator.hasNext() + // on each loop (avoiding the small JNI overhead). Any concurrent modification + // will be caught as an exception and handled by bsg_safe_call_object_method for (int i = 0; i < size; i++) { - populate_metadata_section(env, dst, metadata, keylist, i); + (*env)->PushLocalFrame(env, 3); + { + jobject entry = bsg_safe_call_object_method(env, entryIterator, + bsg_jni_cache->Iterator_next); + jobject _key = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getKey); + jobject _value = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getValue); + + const char *section_name = bsg_safe_get_string_utf_chars(env, _key); + + if (section_name != NULL && _value != NULL) { + populate_metadata_obj(env, dst, section_name, _value); + bsg_safe_release_string_utf_chars(env, _key, section_name); + } + } + (*env)->PopLocalFrame(env, NULL); } exit: bsg_safe_delete_local_ref(env, _metadata); - bsg_safe_delete_local_ref(env, keyset); - bsg_safe_delete_local_ref(env, keylist); + bsg_safe_delete_local_ref(env, entrySet); + bsg_safe_delete_local_ref(env, entryIterator); } void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, jobject metadata) { - jobject entryset = NULL; - jobject entries = NULL; if (metadata == NULL) { - goto exit; + return; } if (!bsg_jni_cache->initialized) { - goto exit; - } - - // get size of metadata map - jint map_size = - bsg_safe_call_int_method(env, metadata, bsg_jni_cache->Map_size); - if (map_size <= 0) { - goto exit; - } - - // create a list of metadata keys - entryset = - bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_entrySet); - if (entryset == NULL) { - goto exit; - } - entries = - bsg_safe_call_object_method(env, entryset, bsg_jni_cache->Set_iterator); - if (entries == NULL) { - goto exit; - } - - while (bsg_safe_call_boolean_method(env, entries, - bsg_jni_cache->Iterator_hasNext)) { - (*env)->PushLocalFrame(env, 3); - { - jobject entry = bsg_safe_call_object_method(env, entries, - bsg_jni_cache->Iterator_next); - jstring _key = bsg_safe_call_object_method( - env, entry, bsg_jni_cache->MapEntry_getKey); - jobject _value = bsg_safe_call_object_method( - env, entry, bsg_jni_cache->MapEntry_getValue); - - 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); - } - } - } - (*env)->PopLocalFrame(env, NULL); + return; } -exit: - bsg_safe_delete_local_ref(env, entries); - bsg_safe_delete_local_ref(env, entryset); + populate_metadata_obj(env, &crumb->metadata, "metaData", metadata); } void bsg_populate_event(JNIEnv *env, bugsnag_event *event) { From 9a6104c60b2de7f1d2c3c898a32e1be98ea98efd Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 30 May 2024 09:54:14 +0100 Subject: [PATCH 21/28] test(ndk): fixed event_serialization.json to align with the expected event data --- .../resources/event_serialization.json | 80 +------------------ 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json index 46734e04a0..1e3189a138 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json +++ b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json @@ -1,79 +1 @@ -{ - "context": "Foo", - "metaData": {}, - "severity": "info", - "unhandled": true, - "severityReason": { - "unhandledOverridden": false, - "type": "signal", - "attributes": { - "signalType": "SIGSEGV" - } - }, - "exceptions": [ - { - "errorClass": "SIGSEGV", - "message": "Whoops!", - "type": "c", - "stacktrace": [ - { - "frameAddress": "0x0", - "symbolAddress": "0x0", - "loadAddress": "0x0", - "lineNumber": 58, - "isPC": true, - "file": "Something.c", - "method": "foo()" - } - ] - } - ], - "user": { - "id": "123", - "name": "Bob Bobbiton", - "email": "bob@example.com" - }, - "app": { - "version": "1.0", - "id": "fa02", - "type": "C", - "releaseStage": "dev", - "versionCode": 55, - "buildUUID": "123", - "binaryArch": "x86", - "duration": 9019, - "durationInForeground": 7017, - "inForeground": true, - "isLaunching": true - }, - "device": { - "osName": "android", - "id": "my-id-123", - "locale": "en", - "osVersion": "9.1", - "manufacturer": "Google", - "model": "Nexus", - "orientation": "portrait", - "runtimeVersions": { - "androidApiLevel": 0, - "osBuild": "" - }, - "cpuAbi": [], - "totalMemory": 1095092340, - "jailbroken": true, - "time": "1970-01-01T02:06:49Z" - }, - "breadcrumbs": [], - "groupingHash": "Bar", - "usage": { - "callbacks": {} - }, - "session": { - "id": "", - "startedAt": "", - "events": { - "handled": 0, - "unhandled": 0 - } - } -} \ No newline at end of file +{"context":"Foo","metaData":{},"severity":"info","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGSEGV"}},"exceptions":[{"errorClass":"SIGSEGV","message":"Whoops!","type":"c","stacktrace":[{"frameAddress":"0x0","symbolAddress":"0x0","loadAddress":"0x0","lineNumber":58,"isPC":true,"file":"Something.c","method":"foo()"}]}],"user":{"id":"123","name":"Bob Bobbiton","email":"bob@example.com"},"app":{"version":"1.0","id":"fa02","type":"C","releaseStage":"dev","versionCode":55,"buildUUID":"123","binaryArch":"x86","duration":9019,"durationInForeground":7017,"inForeground":true,"isLaunching":true},"device":{"osName":"android","id":"my-id-123","locale":"en","osVersion":"9.1","manufacturer":"Google","model":"Nexus","orientation":"portrait","runtimeVersions":{"androidApiLevel":0,"osBuild":""},"cpuAbi":[],"totalMemory":1095092340,"jailbroken":true,"time":"1970-01-01T02:06:49Z"},"breadcrumbs":[],"groupingHash":"Bar","usage":{"callbacks":{}},"threads":[]} \ No newline at end of file From af3f5bcbc820485fa8d0ea2275e9939d30c48fcc Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 30 May 2024 10:38:15 +0100 Subject: [PATCH 22/28] test(ndk): manually cause a stack-overflow in CXXStackoverflowScenario to avoid strcnpy_chk --- .../src/main/cpp/CXXStackoverflowScenario.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp index fbe5fce359..1a4b7ddd65 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp @@ -5,8 +5,13 @@ extern "C" { int __attribute__((optnone)) __attribute__((noinline)) crash_stack_overflow(int counter, char *input) { char stack[7]; + char *output = stack; - strcpy(stack, input); + while (*input) { + *output = *input; + input++; + output++; + } return 4 / counter; } From d5ac07d8775ab8e9416346e2e8b1652e76abd481 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Thu, 6 Jun 2024 10:59:51 +0100 Subject: [PATCH 23/28] Reinstate instrumentation tests using MacOS test boxes as a platform (#2034) * Add script to run instrumentation test against an emulator * Add debugging * more debugging * Use correct darn box * Use appropriate sys_arch * Upload build reports * Upload build reports * Only upload the html test reports * Introduce error to test failstate * Update to use macos 14 queue and remove test failure * Compress updated reports * Incorrect artifacts syntax? * Refer to the directory instead of globbing * Remove compression into zip --- .buildkite/pipeline.yml | 10 +++++ scripts/run-connected-checks.rb | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100755 scripts/run-connected-checks.rb diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 04b5d7d0c1..fc336dedd3 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -86,6 +86,16 @@ steps: queue: macos-14 command: './gradlew test' + - label: ':android: Instrumentation tests' + timeout_in_minutes: 10 + plugins: + artifacts#v1.9.0: + upload: "bugsnag-*/build/reports/androidTests/connected/**/*.html" + agents: + queue: macos-14 + command: './scripts/run-connected-checks.rb' + env: + API_LEVEL: 30 # # BitBar steps # diff --git a/scripts/run-connected-checks.rb b/scripts/run-connected-checks.rb new file mode 100755 index 0000000000..bac5605ba8 --- /dev/null +++ b/scripts/run-connected-checks.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby +require 'pty' +require 'open3' + +# Ensure API_LEVEL is set +raise('API_LEVEL environment variable must be set') unless ENV['API_LEVEL'] +target_api_level = ENV['API_LEVEL'] + +# Check if the appropriate AVD exists based on given API level +avd_exists = `avdmanager list avd -c | grep test-sdk-#{ENV['API_LEVEL']}`.strip + +if avd_exists.empty? + puts "AVD test-sdk-#{target_api_level} does not exist, creating it now" + # Determine if we're running on x86 or ARM + sys_arch = `uname -m`.strip + sys_arch = 'arm64-v8a' if sys_arch.eql?('arm64') + # Check to see if the appropriate SDK is installed + sdk_installed = `sdkmanager --list_installed | grep "system-images;android-#{target_api_level};google_apis;#{sys_arch}"`.strip + + if sdk_installed.empty? + # If not, install it + puts "The system image for API level #{target_api_level} is not installed, installing it now" + `sdkmanager "system-images;android-#{target_api_level};google_apis;#{sys_arch}"` + end + # Create the AVD + `avdmanager -s create avd -n test-sdk-#{target_api_level} -k "system-images;android-#{target_api_level};google_apis;#{sys_arch}"` +else + puts "AVD test-sdk-#{target_api_level} already exists, skipping creation" +end + +begin + emulator_pid = nil + emulator_lines = [] + emulator_thread = Thread.new do + PTY.spawn('emulator', '-avd', "test-sdk-#{target_api_level}", '-no-window', '-gpu', 'swiftshader_indirect', '-noaudio', '-no-boot-anim', '-camera-back', 'none', '-no-snapshot-load') do |stdout, _stdin, pid| + emulator_pid = pid + stdout.each do |line| + emulator_lines << line + puts line + end + end + end + + # Wait for the emulator to boot + start_time = Time.now + until emulator_lines.any? { |line| line.include?('Boot completed') } + if Time.now - start_time > 60 + raise 'Emulator did not boot in 60 seconds' + end + end + + puts 'Emulator booted successfully' + + # Run the connectedCheck tests + exit_status = nil + Open3.popen2e('./gradlew connectedCheck -x :bugsnag-benchmarks:connectedCheck') do |_stdin, stdout_stderr, wait_thr| + stdout_stderr.each { |line| puts line } + exit_status = wait_thr.value + end +ensure + # Stop the emulator + puts 'Stopping emulator process' + Process.kill('INT', emulator_pid) if emulator_pid + emulator_thread.join +end + +unless exit_status.success? + exit(exit_status.exitstatus) +end From 504010175fe9a30fdc35b7c660872c782cec02b7 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 13 Jun 2024 09:05:56 +0100 Subject: [PATCH 24/28] feat(native): support kernels running with a 16kB page size --- CHANGELOG.md | 5 ++ bugsnag-android-core/src/main/CMakeLists.txt | 24 ++--- .../src/main/CMakeLists.txt | 36 ++++---- .../src/main/CMakeLists.txt | 90 ++++++++++--------- examples/sdk-app-example/app/CMakeLists.txt | 9 ++ scripts/run-cpp-check.sh | 4 +- 6 files changed, 97 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2b1d9094..cfe7928ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## TBD +### Enhancements + +* Support for Android Kernels with a 16kB page size + []() + ### Bug fixes * Corrected the behavior when `Bugsnag.startSession` is called when `config.autoTrackSessions=true`, the first automatic session will now be correctly discarded diff --git a/bugsnag-android-core/src/main/CMakeLists.txt b/bugsnag-android-core/src/main/CMakeLists.txt index c2cebef409..281c8ee11a 100644 --- a/bugsnag-android-core/src/main/CMakeLists.txt +++ b/bugsnag-android-core/src/main/CMakeLists.txt @@ -1,15 +1,19 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-root-detection - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - jni/root_detection.c - ) + bugsnag-root-detection + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + jni/root_detection.c +) include_directories(jni) -set_target_properties(bugsnag-root-detection - PROPERTIES - COMPILE_OPTIONS - -Werror -Wall -pedantic) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + +set_target_properties( + bugsnag-root-detection + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt index f4bc87c22e..3777341157 100644 --- a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt @@ -1,23 +1,27 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-plugin-android-anr - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - jni/anr_google.c - jni/anr_handler.c - jni/bugsnag_anr.c - jni/utils/string.c - ) + bugsnag-plugin-android-anr + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + jni/anr_google.c + jni/anr_handler.c + jni/bugsnag_anr.c + jni/utils/string.c +) include_directories(jni) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + target_link_libraries( # Specifies the target library. - bugsnag-plugin-android-anr - # Links the log library to the target library. - log) + bugsnag-plugin-android-anr + # Links the log library to the target library. + log) -set_target_properties(bugsnag-plugin-android-anr - PROPERTIES - COMPILE_OPTIONS - -Werror -Wall -pedantic) +set_target_properties( + bugsnag-plugin-android-anr + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 959c790255..73d98025d4 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -1,58 +1,62 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-ndk - - # Sets the library as a shared library. - SHARED - - # Provides a relative path to your source file(s). - jni/bugsnag_ndk.c - jni/bugsnag.c - jni/metadata.c - jni/safejni.c - jni/jni_cache.c - jni/event.c - jni/featureflags.c - jni/internal_metrics.c - jni/handlers/signal_handler.c - jni/handlers/cpp_handler.cpp - jni/utils/crash_info.c - jni/utils/serializer/buffered_writer.c - jni/utils/serializer/event_writer.c - jni/utils/serializer/BSG_KSJSONCodec.c - jni/utils/serializer/BSG_KSCrashStringConversion.c - jni/utils/stack_unwinder.cpp - jni/utils/seqlock.c - jni/utils/serializer.c - jni/utils/string.c - jni/utils/threads.c - jni/utils/memory.c - jni/deps/parson/parson.c + bugsnag-ndk + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + jni/bugsnag_ndk.c + jni/bugsnag.c + jni/metadata.c + jni/safejni.c + jni/jni_cache.c + jni/event.c + jni/featureflags.c + jni/internal_metrics.c + jni/handlers/signal_handler.c + jni/handlers/cpp_handler.cpp + jni/utils/crash_info.c + jni/utils/serializer/buffered_writer.c + jni/utils/serializer/event_writer.c + jni/utils/serializer/BSG_KSJSONCodec.c + jni/utils/serializer/BSG_KSCrashStringConversion.c + jni/utils/stack_unwinder.cpp + jni/utils/seqlock.c + jni/utils/serializer.c + jni/utils/string.c + jni/utils/threads.c + jni/utils/memory.c + jni/deps/parson/parson.c ) include_directories( - jni - jni/deps - jni/external/libunwindstack-ndk/include + jni + jni/deps + jni/external/libunwindstack-ndk/include ) target_include_directories(bugsnag-ndk PRIVATE ${BUGSNAG_DIR}/assets/include) -target_link_libraries( # Specifies the target library. - bugsnag-ndk - # Links the log library to the target library. - log) +target_link_libraries( + # Specifies the target library. + bugsnag-ndk + # Links the log library to the target library. + log +) # Avoid exporting symbols in release mode to keep internals private # More symbols are exported in debug mode for the sake of unit testing -set(EXTRA_LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_LIST_DIR}/exported_native_symbols-${CMAKE_BUILD_TYPE}.txt") - -set_target_properties(bugsnag-ndk - PROPERTIES - COMPILE_OPTIONS -Werror -Wall -pedantic - LINK_FLAGS "${EXTRA_LINK_FLAGS}" - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED YES) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384,--version-script=${CMAKE_CURRENT_LIST_DIR}/exported_native_symbols-${CMAKE_BUILD_TYPE}.txt") + +set_target_properties( + bugsnag-ndk + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES +) add_subdirectory(jni/external/libunwindstack-ndk/cmake) target_link_libraries(bugsnag-ndk unwindstack) diff --git a/examples/sdk-app-example/app/CMakeLists.txt b/examples/sdk-app-example/app/CMakeLists.txt index c803a1ddd6..f9aa3ccd01 100644 --- a/examples/sdk-app-example/app/CMakeLists.txt +++ b/examples/sdk-app-example/app/CMakeLists.txt @@ -5,5 +5,14 @@ find_package(bugsnag-plugin-android-ndk REQUIRED CONFIG) add_library(entrypoint SHARED src/main/cpp/entrypoint.cpp) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + target_include_directories(entrypoint PRIVATE ${BUGSNAG_INCLUDE_DIR}) target_link_libraries(entrypoint bugsnag-plugin-android-ndk::bugsnag-ndk) + +set_target_properties( + entrypoint + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/scripts/run-cpp-check.sh b/scripts/run-cpp-check.sh index 35213af7b5..1ce30903ac 100755 --- a/scripts/run-cpp-check.sh +++ b/scripts/run-cpp-check.sh @@ -1,2 +1,2 @@ -cppcheck --error-exitcode=2 --enable=warning,performance bugsnag-plugin-android-anr/src/main/jni && \ -cppcheck --error-exitcode=2 --enable=warning,performance bugsnag-plugin-android-ndk/src/main/jni -i bugsnag-plugin-android-ndk/src/main/jni/deps -i bugsnag-plugin-android-ndk/src/main/jni/external +cppcheck --error-exitcode=2 --enable=warning,performance --check-level=exhaustive bugsnag-plugin-android-anr/src/main/jni && \ +cppcheck --error-exitcode=2 --enable=warning,performance --check-level=exhaustive bugsnag-plugin-android-ndk/src/main/jni -i bugsnag-plugin-android-ndk/src/main/jni/deps -i bugsnag-plugin-android-ndk/src/main/jni/external From 341930e8290926383a86c0530e9ac1eadafc20b8 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 13 Jun 2024 11:54:38 +0100 Subject: [PATCH 25/28] feat(event correlation): changed the casing of the `traceid`->`traceId` and `spanid`->`spanId` --- .../src/main/java/com/bugsnag/android/BugsnagEventMapper.kt | 4 ++-- .../src/main/java/com/bugsnag/android/TraceCorrelation.kt | 4 ++-- .../src/test/resources/event_serialization_8.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index 94c7586163..e44902d423 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -96,8 +96,8 @@ internal class BugsnagEventMapper( // populate correlation (map["correlation"] as? Map)?.let { - val traceId = parseTraceId(it["traceid"]) - val spanId = it["spanid"]?.parseUnsignedLong() + val traceId = parseTraceId(it["traceId"]) + val spanId = it["spanId"]?.parseUnsignedLong() if (traceId != null && spanId != null) { event.traceCorrelation = TraceCorrelation(traceId, spanId) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt index 0909406b6e..4fc01ac60f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt @@ -5,8 +5,8 @@ import java.util.UUID internal data class TraceCorrelation(val traceId: UUID, val spanId: Long) : JsonStream.Streamable { override fun toStream(writer: JsonStream) { writer.beginObject() - .name("traceid").value(traceId.toHexString()) - .name("spanid").value(spanId.toHexString()) + .name("traceId").value(traceId.toHexString()) + .name("spanId").value(spanId.toHexString()) writer.endObject() } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_8.json b/bugsnag-android-core/src/test/resources/event_serialization_8.json index 8d0da45a80..982656fdb9 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_8.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_8.json @@ -35,7 +35,7 @@ "threads": [], "featureFlags": [], "correlation": { - "traceid": "24b8b82900d34da39a6bcb48b5a46124", - "spanid": "3dbe7c7ae84945b9" + "traceId": "24b8b82900d34da39a6bcb48b5a46124", + "spanId": "3dbe7c7ae84945b9" } } From 1f49c06f527ad48425271b0961af1ce022871757 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 13 Jun 2024 12:21:31 +0100 Subject: [PATCH 26/28] chore(changelog): fixed the CHANGELOG entry for PR#2040 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe7928ec4..e318c6b26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Enhancements * Support for Android Kernels with a 16kB page size - []() + [#2040](https://github.com/bugsnag/bugsnag-android/pull/2040) ### Bug fixes From 3f93a0474a77241a2f4e2a76148e5e131365874d Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 12 Jun 2024 16:42:59 +0100 Subject: [PATCH 27/28] fix(anr): only set ANR unwind function if the bugsnag-plugin-android-anr.so was loaded successfully --- CHANGELOG.md | 2 ++ .../java/com/bugsnag/android/AnrPlugin.kt | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e318c6b26b..ffc5269c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * Corrected the behavior when `Bugsnag.startSession` is called when `config.autoTrackSessions=true`, the first automatic session will now be correctly discarded [#2033](https://github.com/bugsnag/bugsnag-android/pull/2033) +* Avoid a possible crash in the ANR plugin when the native ANR library failed to load. + [#2039](https://github.com/bugsnag/bugsnag-android/pull/2039) ## 6.5.0 (2024-05-15) diff --git a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt index f2bcb924c2..5745b3e2fe 100644 --- a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt +++ b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt @@ -67,22 +67,25 @@ internal class AnrPlugin : Plugin { } private fun performOneTimeSetup(client: Client) { - libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { + val isLoaded = libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { val error = it.errors[0] error.errorClass = "AnrLinkError" error.errorMessage = LOAD_ERR_MSG true } - @Suppress("UNCHECKED_CAST") - val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? - if (clz != null) { - val ndkPlugin = client.getPlugin(clz) - if (ndkPlugin != null) { - val method = ndkPlugin.javaClass.getMethod("getSignalUnwindStackFunction") - - @Suppress("UNCHECKED_CAST") - val function = method.invoke(ndkPlugin) as Long - setUnwindFunction(function) + + if (isLoaded) { + @Suppress("UNCHECKED_CAST") + val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? + if (clz != null) { + val ndkPlugin = client.getPlugin(clz) + if (ndkPlugin != null) { + val method = ndkPlugin.javaClass.getMethod("getSignalUnwindStackFunction") + + @Suppress("UNCHECKED_CAST") + val function = method.invoke(ndkPlugin) as Long + setUnwindFunction(function) + } } } } From 6544f3922d2005017797efbceef8ce2a86b941c9 Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Wed, 19 Jun 2024 11:22:00 +0100 Subject: [PATCH 28/28] release v6.6.0 --- CHANGELOG.md | 2 +- .../src/main/java/com/bugsnag/android/Notifier.kt | 2 +- examples/sdk-app-example/app/build.gradle | 4 ++-- gradle.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc5269c28..42901e967d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## TBD +## 6.6.0 (2024-06-19) ### Enhancements diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index 4cc446d679..f89f73b880 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "6.5.0", + var version: String = "6.6.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index 28a225a86b..5def256fa5 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -42,8 +42,8 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:6.5.0" - implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.5.0" + implementation "com.bugsnag:bugsnag-android:6.6.0" + implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.6.1" implementation "com.google.android.material:material:1.11.0" diff --git a/gradle.properties b/gradle.properties index f27b1d23a3..a3007565d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=6.5.0 +VERSION_NAME=6.6.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git