From f1c4417665bbc5a88aca19da835d7439fe12050a Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Mon, 10 Nov 2025 10:57:09 +0000 Subject: [PATCH 01/17] feat(event) implement ErrorOptions and CaptureOptions class --- .../api/bugsnag-android-core.api | 44 ++++ bugsnag-android-core/detekt-baseline.xml | 3 +- .../java/com/bugsnag/android/Bugsnag.java | 14 ++ .../main/java/com/bugsnag/android/Client.java | 227 ++++++++++++++---- .../main/java/com/bugsnag/android/Error.java | 6 +- .../java/com/bugsnag/android/ErrorInternal.kt | 9 +- .../java/com/bugsnag/android/ErrorOptions.kt | 82 +++++++ .../main/java/com/bugsnag/android/Event.java | 25 ++ .../java/com/bugsnag/android/EventInternal.kt | 23 +- .../java/com/bugsnag/android/ErrorTest.kt | 4 +- .../android/RecursiveThrowableCauseTest.kt | 2 +- .../bugsnag/android/ExampleApplication.kt | 1 + 12 files changed, 379 insertions(+), 61 deletions(-) create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 692f6d1675..5d212ef7e4 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -82,6 +82,7 @@ public final class com/bugsnag/android/Bugsnag { public static fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public static fun markLaunchCompleted ()V public static fun notify (Ljava/lang/Throwable;)V + public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public static fun pauseSession ()V public static fun removeOnBreadcrumb (Lcom/bugsnag/android/OnBreadcrumbCallback;)V @@ -111,6 +112,40 @@ public class com/bugsnag/android/BugsnagVmViolationListener : android/os/StrictM public fun onVmViolation (Landroid/os/strictmode/Violation;)V } +public final class com/bugsnag/android/CaptureOptions { + public static final field CAPTURE_BREADCRUMBS I + public static final field CAPTURE_FEATURE_FLAGS I + public static final field CAPTURE_STACKTRACE I + public static final field CAPTURE_THREADS I + public static final field CAPTURE_USER I + public static final field Companion Lcom/bugsnag/android/CaptureOptions$Companion; + public fun ()V + public fun (ZZLjava/util/Set;ZZZ)V + public synthetic fun (ZZLjava/util/Set;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun captureNothing ()Lcom/bugsnag/android/CaptureOptions; + public static final fun captureOnly (I)Lcom/bugsnag/android/CaptureOptions; + public static final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/CaptureOptions; + public final fun getBreadcrumbs ()Z + public final fun getFeatureFlags ()Z + public final fun getMetadata ()Ljava/util/Set; + public final fun getStacktrace ()Z + public final fun getThreads ()Z + public final fun getUser ()Z + public final fun setBreadcrumbs (Z)V + public final fun setFeatureFlags (Z)V + public final fun setMetadata (Ljava/util/Set;)V + public final fun setStacktrace (Z)V + public final fun setThreads (Z)V + public final fun setUser (Z)V +} + +public final class com/bugsnag/android/CaptureOptions$Companion { + public final fun captureNothing ()Lcom/bugsnag/android/CaptureOptions; + public final fun captureOnly (I)Lcom/bugsnag/android/CaptureOptions; + public final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/CaptureOptions; + public static synthetic fun captureOnly$default (Lcom/bugsnag/android/CaptureOptions$Companion;ILjava/util/Set;ILjava/lang/Object;)Lcom/bugsnag/android/CaptureOptions; +} + public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com/bugsnag/android/FeatureFlagAware, com/bugsnag/android/MetadataAware, com/bugsnag/android/UserAware { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Lcom/bugsnag/android/Configuration;)V @@ -139,6 +174,7 @@ public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com public fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public fun markLaunchCompleted ()V public fun notify (Ljava/lang/Throwable;)V + public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public fun pauseSession ()V public fun removeOnBreadcrumb (Lcom/bugsnag/android/OnBreadcrumbCallback;)V @@ -355,6 +391,14 @@ public class com/bugsnag/android/Error : com/bugsnag/android/JsonStream$Streamab public fun toStream (Lcom/bugsnag/android/JsonStream;)V } +public final class com/bugsnag/android/ErrorOptions { + public fun ()V + public fun (Lcom/bugsnag/android/CaptureOptions;)V + public synthetic fun (Lcom/bugsnag/android/CaptureOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapture ()Lcom/bugsnag/android/CaptureOptions; + public final fun setCapture (Lcom/bugsnag/android/CaptureOptions;)V +} + public final class com/bugsnag/android/ErrorType : java/lang/Enum { public static final field ANDROID Lcom/bugsnag/android/ErrorType; public static final field C Lcom/bugsnag/android/ErrorType; diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index 7a176d6815..ce0cd6894f 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -22,6 +22,7 @@ LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap<String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? ) LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null ) LongParameterList:EventInternal.kt$EventInternal$( apiKey: String, logger: Logger, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<Pattern> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<Pattern>? = null, isAttemptDeliveryOnCrash: Boolean = false ) + LongParameterList:EventInternal.kt$EventInternal$( originalError: Throwable? = null, config: ImmutableConfig, severityReason: SeverityReason, data: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), captureStacktrace: Boolean = true, captureThreads: Boolean = true, excludeStacktrace: Boolean? = null ) LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier, callbackState: CallbackState ) LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null, /** * 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 ) @@ -64,9 +65,9 @@ MagicNumber:JsonHelper.kt$JsonHelper$8 MagicNumber:JsonStream.kt$JsonStream$128 MagicNumber:JsonStream.kt$JsonStream$32 - MagicNumber:JsonStream.kt$JsonStream.Companion$128 MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3 MagicNumber:SessionStore.kt$SessionStore$60 + MaxLineLength:ErrorTest.kt$ErrorTest$val err = Error.createError(IllegalStateException("Some err", RuntimeException("Whoops")), setOf(), NoopLogger, false) MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)" MaxLineLength:ThreadState.kt$ThreadState$"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]" NestedBlockDepth:FileStore.kt$FileStore$fun deleteStoredFiles(storedFiles: Collection<File>?) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index 80be7e8dd8..17309aa84a 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -255,6 +255,20 @@ public static void notify(@NonNull final Throwable exception, getClient().notify(exception, onError); } + /** + * Notify Bugsnag of a handled exception + * + * @param exception the exception to send to Bugsnag + * @param onError callback invoked on the generated error report for + * additional modification + * @param options the error options + */ + public static void notify(@NonNull final Throwable exception, + @Nullable final ErrorOptions options, + @Nullable final OnErrorCallback onError) { + getClient().notify(exception, options, onError); + } + /** * Adds a map of multiple metadata key-value pairs to the specified section. */ diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 5d40e15da3..13e34f1bd0 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -147,7 +147,6 @@ public Unit invoke(Boolean hasConnection, String networkState) { } }); - // set sensible defaults for delivery/project packages etc if not set ConfigModule configModule = new ConfigModule( contextModule, configuration, @@ -169,11 +168,9 @@ public Unit invoke(Boolean hasConnection, String networkState) { + "https://docs.bugsnag.com/platforms/android/#basic-configuration"); } - // setup storage as soon as possible final StorageModule storageModule = new StorageModule(appContext, immutableConfig, bgTaskService); - // setup state trackers for bugsnag BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(immutableConfig, configuration); clientObservable = bugsnagStateModule.getClientObservable(); @@ -183,11 +180,9 @@ public Unit invoke(Boolean hasConnection, String networkState) { metadataState = bugsnagStateModule.getMetadataState(); featureFlagState = bugsnagStateModule.getFeatureFlagState(); - // lookup system services final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule, bgTaskService); - // setup further state trackers and data collection TrackerModule trackerModule = new TrackerModule(configModule, storageModule, this, bgTaskService, callbackState); @@ -196,7 +191,6 @@ public Unit invoke(Boolean hasConnection, String networkState) { bgTaskService, connectivity, storageModule.getDeviceIdStore(), memoryTrimState); - // load the device + user information userState = storageModule.loadUser(configuration.getUser()); EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, @@ -210,7 +204,6 @@ public Unit invoke(Boolean hasConnection, String networkState) { exceptionHandler = new ExceptionHandler(this, logger); - // load last run info lastRunInfoStore = storageModule.getLastRunInfoStore().getOrNull(); lastRunInfo = storageModule.getLastRunInfo().getOrNull(); @@ -289,7 +282,6 @@ private void start() { exceptionHandler.install(); } - // Initialise plugins before attempting anything else NativeInterface.setClient(Client.this); pluginClient.loadPlugins(Client.this); NdkPluginCaller.INSTANCE.setNdkPlugin(pluginClient.getNdkPlugin()); @@ -297,21 +289,17 @@ private void start() { NdkPluginCaller.INSTANCE.setInternalMetricsEnabled(true); } - // Flush any on-disk errors and sessions eventStore.get().flushOnLaunch(); eventStore.get().flushAsync(); sessionTracker.flushAsync(); - // These call into NdkPluginCaller to sync with the native side, so they must happen later internalMetrics.setConfigDifferences(configDifferences); callbackState.setInternalMetrics(internalMetrics); - // Register listeners for system events in the background registerLifecycleCallbacks(); registerComponentCallbacks(); registerListenersInBackground(); - // Leave auto breadcrumb Map data = new HashMap<>(); leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data); @@ -396,21 +384,21 @@ public Unit invoke(String oldOrientation, String newOrientation) { } }, new Function2() { @Override - public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { + public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { leaveAutoBreadcrumb( - "Trim Memory", - BreadcrumbType.STATE, - Collections.singletonMap( - "trimLevel", memoryTrimState.getTrimLevelDescription() - ) + "Trim Memory", + BreadcrumbType.STATE, + Collections.singletonMap( + "trimLevel", memoryTrimState.getTrimLevelDescription() + ) ); } memoryTrimState.emitObservableEvent(); return null; - } + } } )); } @@ -745,7 +733,7 @@ public void removeOnSession(@NonNull OnSessionCallback onSession) { * @param exception the exception to send to Bugsnag */ public void notify(@NonNull Throwable exception) { - notify(exception, null); + notify(exception, null, null); } /** @@ -756,15 +744,28 @@ public void notify(@NonNull Throwable exception) { * additional modification */ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { + notify(exc, null, onError); + } + + /** + * Notify Bugsnag of a handled exception + * + * @param exc the exception to send to Bugsnag + * @param options the error options + * @param onError callback invoked on the generated error report for + * additional modification + */ + public void notify( + @NonNull Throwable exc, + @Nullable ErrorOptions options, + @Nullable OnErrorCallback onError + ) { if (exc != null) { if (immutableConfig.shouldDiscardError(exc)) { return; } SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); - Metadata metadata = metadataState.getMetadata(); - FeatureFlags featureFlags = featureFlagState.getFeatureFlags(); - Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags, - logger); + Event event = createEventWithOptions(exc, severityReason, options); event.setGroupingDiscriminator(getGroupingDiscriminator()); populateAndNotifyAndroidEvent(event, onError); } else { @@ -772,6 +773,82 @@ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { } } + private Event createEventWithOptions( + @NonNull Throwable exc, + @NonNull SeverityReason severityReason, + @NonNull ErrorOptions options + ) { + if (options == null) { + return new Event( + exc, + immutableConfig, + severityReason, + metadataState.getMetadata(), + featureFlagState.getFeatureFlags(), + logger + ); + } + + + final CaptureOptions capture = options != null ? options.getCapture() : null; + final Metadata metadata = capture == null || capture.getMetadata() == null + ? metadataState.getMetadata() + : captureSelectedMetadata(capture.getMetadata()); + final FeatureFlags featureFlags = capture == null || capture.getMetadata() == null + ? featureFlagState.getFeatureFlags() + : new FeatureFlags(); + final Boolean Stacktrace = capture == null || capture.getMetadata() == null + ? null + : capture.getStacktrace(); + final Boolean Threads = capture == null || capture.getMetadata() == null + ? null + : capture.getThreads(); + + Event event; + if (options != null || capture != null) { + event = new Event( + exc, + immutableConfig, + severityReason, + metadata, + featureFlags, + true, + logger + ); + } else { + event = new Event( + exc, + immutableConfig, + severityReason, + metadataState.getMetadata(), + featureFlagState.getFeatureFlags(), + Stacktrace, + Threads, + logger); + } + return event; + } + + private Metadata captureSelectedMetadata(@Nullable Set metadata) { + Map> all = snapshotAllMetadataTabsExcludingAppDevice(); + Metadata selectedMetadataTabs = null; + if (metadata != null || !metadata.isEmpty()) { + for (Map.Entry> e : all.entrySet()) { + if (metadata.contains(e.getKey())) { + selectedMetadataTabs.addMetadata(e.getKey(), e.getValue()); + } + } + } + + for (String tab : metadata) { + Object value = all.get(tab); + if (value != null) { + selectedMetadataTabs.addMetadata("", tab, value); + } + } + return selectedMetadataTabs; + } + /** * Caches an error then attempts to notify. * @@ -786,9 +863,8 @@ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, Event event = new Event(exc, immutableConfig, handledState, data, featureFlagState.getFeatureFlags(), logger); event.setGroupingDiscriminator(getGroupingDiscriminator()); - populateAndNotifyAndroidEvent(event, null); + populateAndNotifyAndroidEvent(event, null, null); - // persist LastRunInfo so that on relaunch users can check the app crashed int consecutiveLaunchCrashes = lastRunInfo == null ? 0 : lastRunInfo.getConsecutiveLaunchCrashes(); boolean launching = launchCrashTracker.isLaunching(); @@ -798,46 +874,108 @@ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, LastRunInfo runInfo = new LastRunInfo(consecutiveLaunchCrashes, true, launching); persistRunInfo(runInfo); - // suspend execution of any further background tasks, waiting for previously - // submitted ones to complete. bgTaskService.shutdown(); } void populateAndNotifyAndroidEvent(@NonNull Event event, @Nullable OnErrorCallback onError) { - // Capture the state of the app and device and attach diagnostics to the event + populateAndNotifyAndroidEvent(event, null, onError); + } + + void populateAndNotifyAndroidEvent(@NonNull Event event, + @Nullable ErrorOptions options, + @Nullable OnErrorCallback onError + ) { + populateDeviceAndAppData(event); + + if (options == null) { + populateAllEventData(event); + } else { + populateConditionalEventData(event, options); + } + + event.setContext(contextState.getContext()); + event.setInternalMetrics(internalMetrics); + event.setGroupingDiscriminator(getGroupingDiscriminator()); + + notifyInternal(event, onError); + } + + private void populateDeviceAndAppData(@NonNull Event event) { event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime())); event.addMetadata("device", deviceDataCollector.getDeviceMetadata()); - - // add additional info that belongs in metadata - // generate new object each time, as this can be mutated by end-users event.setApp(appDataCollector.generateAppWithState()); event.addMetadata("app", appDataCollector.getAppDataMetadata()); + } - // Attach breadcrumbState to the event + private void populateAllEventData(@NonNull Event event) { event.setBreadcrumbs(breadcrumbState.copy()); - // Attach user info to the event User user = userState.get().getUser(); event.setUser(user.getId(), user.getEmail(), user.getName()); + } - // Attach context to the event - event.setContext(contextState.getContext()); + private void populateConditionalEventData(@NonNull Event event, @NonNull ErrorOptions options) { + final CaptureOptions capture = + options.getCapture() != null ? options.getCapture() : null; - event.setInternalMetrics(internalMetrics); - event.setGroupingDiscriminator(getGroupingDiscriminator()); + if (capture != null) { + if (capture.getBreadcrumbs()) { + event.setBreadcrumbs(breadcrumbState.copy()); + } - notifyInternal(event, onError); + if (capture.getUser()) { + User user = userState.get().getUser(); + event.setUser(user.getId(), user.getEmail(), user.getName()); + } + + copySelectedMetadataTabs(event, capture.getMetadata()); + } else { + populateAllEventData(event); + } + } + + + /** + * Copies only the selected metadata tabs from state into the event. + */ + private void copySelectedMetadataTabs(@NonNull Event event, @Nullable Set allow) { + Map> all = snapshotAllMetadataTabsExcludingAppDevice(); + if (allow != null) { + for (Map.Entry> e : all.entrySet()) { + if (allow.contains(e.getKey())) { + event.addMetadata(e.getKey(), e.getValue()); + } + } + return; + } + if (allow.isEmpty()) { + return; + } + for (String tab : allow) { + Object value = all.get(tab); + if (value != null) { + event.addMetadata("", tab, value); + } + } + } + + /** + * Returns a shallow snapshot of all metadata tabs except "app" and "device". + * Implement using existing MetadataState APIs; if none exist, add a safe accessor there. + */ + private Map> snapshotAllMetadataTabsExcludingAppDevice() { + Map> snapshot = metadataState.getMetadata().toMap(); + snapshot.remove("app"); + snapshot.remove("device"); + return snapshot; } void notifyInternal(@NonNull Event event, @Nullable OnErrorCallback onError) { - // set the redacted keys on the event as this - // will not have been set for RN/Unity events Collection redactedKeys = metadataState.getMetadata().getRedactedKeys(); event.setRedactedKeys(redactedKeys); - // get session for event Session currentSession = sessionTracker.getCurrentSession(); if (currentSession != null @@ -845,7 +983,6 @@ void notifyInternal(@NonNull Event event, event.setSession(currentSession); } - // Run on error tasks, don't notify if any return false if (!callbackState.runOnErrorTasks(event, logger) || (onError != null && !onError.onError(event))) { @@ -853,7 +990,6 @@ void notifyInternal(@NonNull Event event, return; } - // leave an error breadcrumb of this event - for the next event leaveErrorBreadcrumb(event); setGroupingDiscriminator(getGroupingDiscriminator()); @@ -965,8 +1101,6 @@ public Object getMetadata(@NonNull String section, @NonNull String key) { } } - // cast map to retain original signature until next major version bump, as this - // method signature is used by Unity/React native @NonNull @SuppressWarnings({"unchecked", "rawtypes"}) Map getMetadata() { @@ -1022,7 +1156,6 @@ void leaveAutoBreadcrumb(@NonNull String message, } private void leaveErrorBreadcrumb(@NonNull Event event) { - // Add a breadcrumb for this event occurring List errors = event.getErrors(); if (errors.size() > 0) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java index d711e4d609..ee8119c497 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java @@ -105,7 +105,9 @@ public void toStream(@NonNull JsonStream stream) throws IOException { static List createError(@NonNull Throwable exc, @NonNull Collection projectPackages, - @NonNull Logger logger) { - return ErrorInternal.Companion.createError(exc, projectPackages, logger); + @NonNull Logger logger, + @Nullable Boolean excludeStacktrace + ) { + return ErrorInternal.Companion.createError(exc, projectPackages, logger, excludeStacktrace); } } \ No newline at end of file diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt index 9e716bbd62..98917a8115 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt @@ -19,12 +19,17 @@ internal class ErrorInternal @JvmOverloads internal constructor( fun createError( exc: Throwable, projectPackages: Collection, - logger: Logger + logger: Logger, + excludeStacktrace: Boolean ): MutableList { return exc.safeUnrollCauses() .mapTo(mutableListOf()) { currentEx -> // Somehow it's possible for stackTrace to be null in rare cases - val stacktrace = currentEx.stackTrace ?: arrayOf() + val stacktrace: Array = if (excludeStacktrace) { + emptyArray() + } else { + currentEx.stackTrace ?: arrayOf() + } val trace = Stacktrace(stacktrace, projectPackages, logger) val errorInternal = ErrorInternal( currentEx.javaClass.name, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt new file mode 100644 index 0000000000..bc14c1f85f --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt @@ -0,0 +1,82 @@ +package com.bugsnag.android + +/** + * Options for controlling how handled errors are reported. + */ +class ErrorOptions @JvmOverloads constructor( + /** Controls which data fields are captured during Event creation. */ + var capture: CaptureOptions = CaptureOptions() +) + +/** + * Granular flags for controlling data capture at notify time. + */ +class CaptureOptions( + /** Whether to capture breadcrumbs. Defaults to true. */ + var breadcrumbs: Boolean = true, + + /** Whether to capture feature flags. Defaults to true. */ + var featureFlags: Boolean = true, + + /** + * Controls which custom metadata tabs are included. + * - null: all metadata tabs captured + * - empty set: only app and device tabs captured + * - set of names: app, device, and specified tabs captured + * + * Note: app and device tabs are always captured. + */ + var metadata: Set? = null, + + /** Whether to capture stacktrace. Defaults to true. */ + var stacktrace: Boolean = true, + + /** Whether to capture thread state. Defaults to true. */ + var threads: Boolean = true, + + /** Whether to capture user information. Defaults to true. */ + var user: Boolean = true, +) { + /** + * Create a CaptureOptions with all of the default capturing behaviour (capture everything). + */ + constructor() : + // we specify one arg to avoid repeating all of the defaults here + this(breadcrumbs = true) + + companion object { + const val CAPTURE_STACKTRACE = 1 + const val CAPTURE_BREADCRUMBS = 2 + const val CAPTURE_FEATURE_FLAGS = 4 + const val CAPTURE_THREADS = 8 + const val CAPTURE_USER = 16 + + /** + * A convenience method to capture only selected event fields using a bit-mask of field + * names (a mask of [CAPTURE_STACKTRACE], [CAPTURE_BREADCRUMBS], etc.). + * + * @param fields a bit mask of the fields to capture + * @param metadata the metadata tabs to capture (or `null` to capture all) + */ + @JvmStatic + @JvmOverloads + fun captureOnly(fields: Int, metadata: Set? = null): CaptureOptions { + return captureNothing().apply { + stacktrace = (fields and CAPTURE_STACKTRACE) != 0 + breadcrumbs = (fields and CAPTURE_BREADCRUMBS) != 0 + featureFlags = (fields and CAPTURE_FEATURE_FLAGS) != 0 + threads = (fields and CAPTURE_THREADS) != 0 + user = (fields and CAPTURE_USER) != 0 + this.metadata = metadata + } + } + + /** + * Return CaptureOptions that will not capture any optional fields. + */ + @JvmStatic + fun captureNothing(): CaptureOptions { + return captureOnly(0) + } + } +} 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 2c32fe84f7..fed0ba50d6 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 @@ -43,6 +43,31 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F logger); } + Event(@Nullable Throwable originalError, + @NonNull ImmutableConfig config, + @NonNull SeverityReason severityReason, + @NonNull Metadata metadata, + @NonNull FeatureFlags featureFlags, + @Nullable Boolean excludeStacktrace, + @NonNull Logger logger) { + this(new EventInternal(originalError, config, severityReason, metadata, featureFlags, + excludeStacktrace), + logger); + } + + Event(@Nullable Throwable originalError, + @NonNull ImmutableConfig config, + @NonNull SeverityReason severityReason, + @NonNull Metadata metadata, + @NonNull FeatureFlags featureFlags, + boolean captureStacktrace, + boolean captureThreads, + @NonNull Logger logger) { + this(new EventInternal(originalError, config, severityReason, metadata, featureFlags, + captureStacktrace, captureThreads), + logger); + } + Event(@NonNull EventInternal impl, @NonNull Logger logger) { this.impl = impl; this.logger = logger; 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 2a4d57f142..2d81d42faf 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 @@ -17,22 +17,33 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata config: ImmutableConfig, severityReason: SeverityReason, data: Metadata = Metadata(), - featureFlags: FeatureFlags = FeatureFlags() + featureFlags: FeatureFlags = FeatureFlags(), + captureStacktrace: Boolean = true, + captureThreads: Boolean = true, + excludeStacktrace: Boolean? = null ) : this( config.apiKey, config.logger, mutableListOf(), config.discardClasses.toSet(), - when (originalError) { - null -> mutableListOf() - else -> Error.createError(originalError, config.projectPackages, config.logger) + when { + originalError == null -> mutableListOf() + !captureStacktrace -> { + val error = Error.createError(originalError, config.projectPackages, config.logger, excludeStacktrace)[0] + mutableListOf(error) + } + else -> Error.createError(originalError, config.projectPackages, config.logger, false) }, data.copy(), featureFlags.copy(), originalError, config.projectPackages, severityReason, - ThreadState(originalError, severityReason.unhandled, config).threads, + if (captureThreads) { + ThreadState(originalError, severityReason.unhandled, config).threads + } else { + mutableListOf() + }, User(), config.redactedKeys.toSet(), config.attemptDeliveryOnCrash @@ -343,7 +354,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata errors.add(newError) return newError } else { - val newErrors = Error.createError(thrownError, projectPackages, logger) + val newErrors = Error.createError(thrownError, projectPackages, logger, false) errors.addAll(newErrors) return newErrors.first() } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt index b3cf182c19..f019be98a6 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt @@ -8,7 +8,7 @@ class ErrorTest { @Test fun createError() { - val err = Error.createError(RuntimeException("Whoops"), setOf(), NoopLogger) + val err = Error.createError(RuntimeException("Whoops"), setOf(), NoopLogger, false) assertEquals(1, err.size) assertEquals("Whoops", err[0].errorMessage) assertFalse(err[0].stacktrace.isEmpty()) @@ -16,7 +16,7 @@ class ErrorTest { @Test fun createNestedError() { - val err = Error.createError(IllegalStateException("Some err", RuntimeException("Whoops")), setOf(), NoopLogger) + val err = Error.createError(IllegalStateException("Some err", RuntimeException("Whoops")), setOf(), NoopLogger, false) assertEquals(2, err.size) assertEquals("Some err", err[0].errorMessage) assertFalse(err[0].stacktrace.isEmpty()) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt index 313135e5bf..09a6555f48 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt @@ -16,7 +16,7 @@ class RecursiveThrowableCauseTest { @Test(timeout = 100L) fun testCreateEvent() { - Error.createError(createRecursiveThrowableChain(), emptyList(), NoopLogger) + Error.createError(createRecursiveThrowableChain(), emptyList(), NoopLogger, false) } private fun createRecursiveThrowableChain(): Throwable { diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt index e43650bba6..86daadf5d5 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt @@ -36,6 +36,7 @@ class ExampleApplication : Application() { config.setUser("123456", "joebloggs@example.com", "Joe Bloggs") config.addMetadata("user", "age", 31) config.addPlugin(bugsnagOkHttpPlugin) + config.isAttemptDeliveryOnCrash = true // Configure the persistence directory when running MultiProcessActivity in a separate // process to ensure the two Bugsnag clients are independent From fcc3561cb952202589355252e9e84ef324889bae Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Tue, 25 Nov 2025 01:04:45 +0000 Subject: [PATCH 02/17] test(event) error options end to end tests --- bugsnag-android-core/detekt-baseline.xml | 2 +- .../java/com/bugsnag/android/Bugsnag.java | 2 +- .../main/java/com/bugsnag/android/Client.java | 22 +++---- .../java/com/bugsnag/android/ErrorOptions.kt | 20 +++---- .../main/java/com/bugsnag/android/Event.java | 12 ---- .../java/com/bugsnag/android/EventInternal.kt | 3 +- .../com/bugsnag/android/CaptureOptionsTest.kt | 57 +++++++++++++++++++ .../bugsnag/android/BaseCrashyActivity.kt | 11 +++- .../bugsnag/android/ExampleApplication.kt | 1 - .../scenarios/ErrorOptionsScenario.kt | 24 ++++++++ .../scenarios/NullErrorOptionsScenario.kt | 17 ++++++ features/full_tests/auto_notify.feature | 2 - .../full_tests/null_error_options.feature | 14 +++++ features/smoke_tests/02_handled.feature | 11 ++++ 14 files changed, 155 insertions(+), 43 deletions(-) create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullErrorOptionsScenario.kt create mode 100644 features/full_tests/null_error_options.feature diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index ce0cd6894f..f925d09fd1 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -22,7 +22,7 @@ LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap<String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? ) LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null ) LongParameterList:EventInternal.kt$EventInternal$( apiKey: String, logger: Logger, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<Pattern> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<Pattern>? = null, isAttemptDeliveryOnCrash: Boolean = false ) - LongParameterList:EventInternal.kt$EventInternal$( originalError: Throwable? = null, config: ImmutableConfig, severityReason: SeverityReason, data: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), captureStacktrace: Boolean = true, captureThreads: Boolean = true, excludeStacktrace: Boolean? = null ) + LongParameterList:EventInternal.kt$EventInternal$( originalError: Throwable? = null, config: ImmutableConfig, severityReason: SeverityReason, data: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), captureStacktrace: Boolean = true, captureThreads: Boolean = true, ) LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier, callbackState: CallbackState ) LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null, /** * 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 ) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index 17309aa84a..d09b946f0c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -259,9 +259,9 @@ public static void notify(@NonNull final Throwable exception, * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag + * @param options additional options to adjust the reporting of the exception * @param onError callback invoked on the generated error report for * additional modification - * @param options the error options */ public static void notify(@NonNull final Throwable exception, @Nullable final ErrorOptions options, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 13e34f1bd0..963f459e34 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -767,7 +767,7 @@ public void notify( SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Event event = createEventWithOptions(exc, severityReason, options); event.setGroupingDiscriminator(getGroupingDiscriminator()); - populateAndNotifyAndroidEvent(event, onError); + populateAndNotifyAndroidEvent(event,options, onError); } else { logNull("notify"); } @@ -789,20 +789,15 @@ private Event createEventWithOptions( ); } - final CaptureOptions capture = options != null ? options.getCapture() : null; final Metadata metadata = capture == null || capture.getMetadata() == null ? metadataState.getMetadata() : captureSelectedMetadata(capture.getMetadata()); - final FeatureFlags featureFlags = capture == null || capture.getMetadata() == null + final FeatureFlags featureFlags = capture == null || capture.getFeatureFlags() ? featureFlagState.getFeatureFlags() : new FeatureFlags(); - final Boolean Stacktrace = capture == null || capture.getMetadata() == null - ? null - : capture.getStacktrace(); - final Boolean Threads = capture == null || capture.getMetadata() == null - ? null - : capture.getThreads(); + final boolean stacktrace = capture == null || capture.getStacktrace(); + final boolean threads = capture == null || capture.getThreads(); Event event; if (options != null || capture != null) { @@ -812,7 +807,8 @@ private Event createEventWithOptions( severityReason, metadata, featureFlags, - true, + stacktrace, + threads, logger ); } else { @@ -822,8 +818,8 @@ private Event createEventWithOptions( severityReason, metadataState.getMetadata(), featureFlagState.getFeatureFlags(), - Stacktrace, - Threads, + stacktrace, + threads, logger); } return event; @@ -831,7 +827,7 @@ private Event createEventWithOptions( private Metadata captureSelectedMetadata(@Nullable Set metadata) { Map> all = snapshotAllMetadataTabsExcludingAppDevice(); - Metadata selectedMetadataTabs = null; + Metadata selectedMetadataTabs = new Metadata(); if (metadata != null || !metadata.isEmpty()) { for (Map.Entry> e : all.entrySet()) { if (metadata.contains(e.getKey())) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt index bc14c1f85f..8f3ca6c441 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt @@ -21,7 +21,7 @@ class CaptureOptions( /** * Controls which custom metadata tabs are included. * - null: all metadata tabs captured - * - empty set: only app and device tabs captured + * - empty set: no custom metadata captured * - set of names: app, device, and specified tabs captured * * Note: app and device tabs are always captured. @@ -61,14 +61,14 @@ class CaptureOptions( @JvmStatic @JvmOverloads fun captureOnly(fields: Int, metadata: Set? = null): CaptureOptions { - return captureNothing().apply { - stacktrace = (fields and CAPTURE_STACKTRACE) != 0 - breadcrumbs = (fields and CAPTURE_BREADCRUMBS) != 0 - featureFlags = (fields and CAPTURE_FEATURE_FLAGS) != 0 - threads = (fields and CAPTURE_THREADS) != 0 - user = (fields and CAPTURE_USER) != 0 - this.metadata = metadata - } + return CaptureOptions( + stacktrace = (fields and CAPTURE_STACKTRACE) != 0, + breadcrumbs = (fields and CAPTURE_BREADCRUMBS) != 0, + featureFlags = (fields and CAPTURE_FEATURE_FLAGS) != 0, + threads = (fields and CAPTURE_THREADS) != 0, + user = (fields and CAPTURE_USER) != 0, + metadata = metadata + ) } /** @@ -76,7 +76,7 @@ class CaptureOptions( */ @JvmStatic fun captureNothing(): CaptureOptions { - return captureOnly(0) + return captureOnly(0, emptySet()) } } } 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 fed0ba50d6..8b52c8b506 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 @@ -43,18 +43,6 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F logger); } - Event(@Nullable Throwable originalError, - @NonNull ImmutableConfig config, - @NonNull SeverityReason severityReason, - @NonNull Metadata metadata, - @NonNull FeatureFlags featureFlags, - @Nullable Boolean excludeStacktrace, - @NonNull Logger logger) { - this(new EventInternal(originalError, config, severityReason, metadata, featureFlags, - excludeStacktrace), - logger); - } - Event(@Nullable Throwable originalError, @NonNull ImmutableConfig config, @NonNull SeverityReason severityReason, 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 2d81d42faf..f6d9923320 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 @@ -20,7 +20,6 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata featureFlags: FeatureFlags = FeatureFlags(), captureStacktrace: Boolean = true, captureThreads: Boolean = true, - excludeStacktrace: Boolean? = null ) : this( config.apiKey, config.logger, @@ -29,7 +28,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata when { originalError == null -> mutableListOf() !captureStacktrace -> { - val error = Error.createError(originalError, config.projectPackages, config.logger, excludeStacktrace)[0] + val error = Error.createError(originalError, config.projectPackages, config.logger, !captureStacktrace)[0] mutableListOf(error) } else -> Error.createError(originalError, config.projectPackages, config.logger, false) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt new file mode 100644 index 0000000000..e953683593 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt @@ -0,0 +1,57 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class CaptureOptionsTest { + + @Test + fun testDefaultValues() { + val options = CaptureOptions() + assertTrue(options.breadcrumbs) + assertTrue(options.featureFlags) + assertNull(options.metadata) + assertTrue(options.stacktrace) + assertTrue(options.threads) + assertTrue(options.user) + } + + @Test + fun testCaptureOnlyStacktraceAndUser() { + val fields = CaptureOptions.CAPTURE_STACKTRACE or CaptureOptions.CAPTURE_USER + val options = CaptureOptions.captureOnly(fields) + assertTrue(options.stacktrace) + assertFalse(options.breadcrumbs) + assertFalse(options.featureFlags) + assertFalse(options.threads) + assertTrue(options.user) + assertNull(options.metadata) + } + + @Test + fun testCaptureOnlyWithMetadata() { + val fields = CaptureOptions.CAPTURE_BREADCRUMBS or CaptureOptions.CAPTURE_FEATURE_FLAGS + val metadataTabs = setOf("customTab") + val options = CaptureOptions.captureOnly(fields, metadataTabs) + assertFalse(options.stacktrace) + assertTrue(options.breadcrumbs) + assertTrue(options.featureFlags) + assertFalse(options.threads) + assertFalse(options.user) + assertEquals(metadataTabs, options.metadata) + } + + @Test + fun testCaptureNothing() { + val options = CaptureOptions.captureNothing() + assertFalse(options.stacktrace) + assertFalse(options.breadcrumbs) + assertFalse(options.featureFlags) + assertFalse(options.threads) + assertFalse(options.user) + assertEquals(emptySet(), options.metadata) + } +} diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt index 66d8da4c62..0bf8eaf0b9 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt @@ -9,7 +9,9 @@ import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.bugsnag.android.BreadcrumbType import com.bugsnag.android.Bugsnag +import com.bugsnag.android.CaptureOptions import com.bugsnag.android.Configuration +import com.bugsnag.android.ErrorOptions import com.bugsnag.android.Severity import com.example.foo.CrashyClass import com.google.android.material.snackbar.Snackbar @@ -93,7 +95,14 @@ open class BaseCrashyActivity : AppCompatActivity() { try { throw RuntimeException("Non-Fatal Crash") } catch (e: RuntimeException) { - Bugsnag.notify(e) { + + + val errorOptions: ErrorOptions = ErrorOptions(CaptureOptions.captureNothing()) + Bugsnag.addMetadata("custom", "key", "value") + Bugsnag.leaveBreadcrumb("Test breadcrumb") + Bugsnag.addFeatureFlag("testFeatureFlag", "variantA") + Bugsnag.setUser("123", "jane@doe.com", "Jane Doe") + Bugsnag.notify(e, errorOptions){ showSnackbar() true } diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt index 86daadf5d5..e43650bba6 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt @@ -36,7 +36,6 @@ class ExampleApplication : Application() { config.setUser("123456", "joebloggs@example.com", "Joe Bloggs") config.addMetadata("user", "age", 31) config.addPlugin(bugsnagOkHttpPlugin) - config.isAttemptDeliveryOnCrash = true // Configure the persistence directory when running MultiProcessActivity in a separate // process to ensure the two Bugsnag clients are independent diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt new file mode 100644 index 0000000000..1408a447d6 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt @@ -0,0 +1,24 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.CaptureOptions +import com.bugsnag.android.Configuration +import com.bugsnag.android.ErrorOptions + +class ErrorOptionsScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + private val errorOptions = ErrorOptions(CaptureOptions.captureNothing()) + + override fun startScenario() { + super.startScenario() + Bugsnag.addMetadata("custom", "key", "value") + Bugsnag.leaveBreadcrumb("Test breadcrumb") + Bugsnag.addFeatureFlag("testFeatureFlag", "variantA") + Bugsnag.setUser("123", "jane@doe.com", "Jane Doe") + Bugsnag.notify(generateException(), errorOptions, null) + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullErrorOptionsScenario.kt new file mode 100644 index 0000000000..6fef19ef73 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/NullErrorOptionsScenario.kt @@ -0,0 +1,17 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration + +class NullErrorOptionsScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + override fun startScenario() { + super.startScenario() + Bugsnag.notify(generateException(), null, null) + } +} diff --git a/features/full_tests/auto_notify.feature b/features/full_tests/auto_notify.feature index 2bbe799a5d..3591348127 100644 --- a/features/full_tests/auto_notify.feature +++ b/features/full_tests/auto_notify.feature @@ -43,5 +43,3 @@ Feature: Switching automatic error detection on/off for Unity And the event "severity" equals "error" And the event "severityReason.type" equals "signal" And the event "severityReason.unhandledOverridden" is false - - diff --git a/features/full_tests/null_error_options.feature b/features/full_tests/null_error_options.feature new file mode 100644 index 0000000000..832e4910df --- /dev/null +++ b/features/full_tests/null_error_options.feature @@ -0,0 +1,14 @@ +Feature: Error options notify scenarios + + Background: + Given I clear all persistent data + + Scenario: Handled exceptions with null error options + When I run "NullErrorOptionsScenario" + 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 event "breadcrumbs" is not null + And the event "featureFlags" is not null + And the event "threads" is not null + And the event "user" is not null + And the event "metaData" is not null \ No newline at end of file diff --git a/features/smoke_tests/02_handled.feature b/features/smoke_tests/02_handled.feature index 6f2fc022ff..6636a14e44 100644 --- a/features/smoke_tests/02_handled.feature +++ b/features/smoke_tests/02_handled.feature @@ -249,3 +249,14 @@ Feature: Handled smoke tests And the event "threads.0.stacktrace.0.method" is not null And the event "threads.0.stacktrace.0.file" is not null And the event "threads.0.stacktrace.0.lineNumber" is not null + + Scenario: Handled exceptions with negative options + When I run "ErrorOptionsScenario" + 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 event "stacktrace" is null + And the event "breadcrumbs.0" is null + And the event "featureFlags.0" is null + And the event "threads.0" is null + And the event "user.id" is null + And the event "user.name" is null \ No newline at end of file From ab7b1eedff0846d99016180e8251aeb6c9b5a7d8 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 1 Dec 2025 17:09:32 +0000 Subject: [PATCH 03/17] refactor(CaptureOptions): inverted and simplified the `excludeStacktrace` option in Error --- .../main/java/com/bugsnag/android/Error.java | 6 +++--- .../java/com/bugsnag/android/ErrorInternal.kt | 15 ++++++++------- .../java/com/bugsnag/android/EventInternal.kt | 17 +++++++++-------- .../test/java/com/bugsnag/android/ErrorTest.kt | 9 +++++++-- .../android/RecursiveThrowableCauseTest.kt | 2 +- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java index ee8119c497..f6e6f152d0 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java @@ -104,10 +104,10 @@ public void toStream(@NonNull JsonStream stream) throws IOException { } static List createError(@NonNull Throwable exc, + boolean captureStacktrace, @NonNull Collection projectPackages, - @NonNull Logger logger, - @Nullable Boolean excludeStacktrace + @NonNull Logger logger ) { - return ErrorInternal.Companion.createError(exc, projectPackages, logger, excludeStacktrace); + return ErrorInternal.Companion.createError(exc, projectPackages, captureStacktrace, logger); } } \ No newline at end of file diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt index 98917a8115..adadbac99f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorInternal.kt @@ -19,17 +19,18 @@ internal class ErrorInternal @JvmOverloads internal constructor( fun createError( exc: Throwable, projectPackages: Collection, - logger: Logger, - excludeStacktrace: Boolean + captureStacktrace: Boolean, + logger: Logger ): MutableList { return exc.safeUnrollCauses() .mapTo(mutableListOf()) { currentEx -> // Somehow it's possible for stackTrace to be null in rare cases - val stacktrace: Array = if (excludeStacktrace) { - emptyArray() - } else { - currentEx.stackTrace ?: arrayOf() - } + val stacktrace: Array = + if (captureStacktrace) { + currentEx.stackTrace ?: emptyArray() + } else { + emptyArray() + } val trace = Stacktrace(stacktrace, projectPackages, logger) val errorInternal = ErrorInternal( currentEx.javaClass.name, 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 f6d9923320..f604c29880 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 @@ -25,13 +25,14 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata config.logger, mutableListOf(), config.discardClasses.toSet(), - when { - originalError == null -> mutableListOf() - !captureStacktrace -> { - val error = Error.createError(originalError, config.projectPackages, config.logger, !captureStacktrace)[0] - mutableListOf(error) - } - else -> Error.createError(originalError, config.projectPackages, config.logger, false) + when (originalError) { + null -> mutableListOf() + else -> Error.createError( + originalError, + captureStacktrace, + config.projectPackages, + config.logger + ) }, data.copy(), featureFlags.copy(), @@ -353,7 +354,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata errors.add(newError) return newError } else { - val newErrors = Error.createError(thrownError, projectPackages, logger, false) + val newErrors = Error.createError(thrownError, true, projectPackages, logger) errors.addAll(newErrors) return newErrors.first() } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt index f019be98a6..3d871887ef 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt @@ -8,7 +8,7 @@ class ErrorTest { @Test fun createError() { - val err = Error.createError(RuntimeException("Whoops"), setOf(), NoopLogger, false) + val err = Error.createError(RuntimeException("Whoops"), true, setOf(), NoopLogger) assertEquals(1, err.size) assertEquals("Whoops", err[0].errorMessage) assertFalse(err[0].stacktrace.isEmpty()) @@ -16,7 +16,12 @@ class ErrorTest { @Test fun createNestedError() { - val err = Error.createError(IllegalStateException("Some err", RuntimeException("Whoops")), setOf(), NoopLogger, false) + val err = Error.createError( + IllegalStateException("Some err", RuntimeException("Whoops")), + true, + setOf(), + NoopLogger + ) assertEquals(2, err.size) assertEquals("Some err", err[0].errorMessage) assertFalse(err[0].stacktrace.isEmpty()) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt index 09a6555f48..30f9dba2df 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt @@ -16,7 +16,7 @@ class RecursiveThrowableCauseTest { @Test(timeout = 100L) fun testCreateEvent() { - Error.createError(createRecursiveThrowableChain(), emptyList(), NoopLogger, false) + Error.createError(createRecursiveThrowableChain(), false, emptyList(), NoopLogger) } private fun createRecursiveThrowableChain(): Throwable { From 7dde678f9ece463e19ee4ca4925f81bd373c53a4 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 08:21:26 +0000 Subject: [PATCH 04/17] refactor(CaptureOptions): renamed `CaptureOptions` to `ErrorCaptureOptions` for clarity --- .../src/main/java/com/bugsnag/android/Client.java | 4 ++-- .../main/java/com/bugsnag/android/ErrorOptions.kt | 10 +++++----- ...reOptionsTest.kt => ErrorCaptureOptionsTest.kt} | 14 +++++++------- .../mazerunner/scenarios/ErrorOptionsScenario.kt | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) rename bugsnag-android-core/src/test/java/com/bugsnag/android/{CaptureOptionsTest.kt => ErrorCaptureOptionsTest.kt} (74%) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 963f459e34..99a1a78e3f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -789,7 +789,7 @@ private Event createEventWithOptions( ); } - final CaptureOptions capture = options != null ? options.getCapture() : null; + final ErrorCaptureOptions capture = options != null ? options.getCapture() : null; final Metadata metadata = capture == null || capture.getMetadata() == null ? metadataState.getMetadata() : captureSelectedMetadata(capture.getMetadata()); @@ -912,7 +912,7 @@ private void populateAllEventData(@NonNull Event event) { } private void populateConditionalEventData(@NonNull Event event, @NonNull ErrorOptions options) { - final CaptureOptions capture = + final ErrorCaptureOptions capture = options.getCapture() != null ? options.getCapture() : null; if (capture != null) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt index 8f3ca6c441..fc7de90a6a 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt @@ -5,13 +5,13 @@ package com.bugsnag.android */ class ErrorOptions @JvmOverloads constructor( /** Controls which data fields are captured during Event creation. */ - var capture: CaptureOptions = CaptureOptions() + var capture: ErrorCaptureOptions = ErrorCaptureOptions() ) /** * Granular flags for controlling data capture at notify time. */ -class CaptureOptions( +class ErrorCaptureOptions( /** Whether to capture breadcrumbs. Defaults to true. */ var breadcrumbs: Boolean = true, @@ -60,8 +60,8 @@ class CaptureOptions( */ @JvmStatic @JvmOverloads - fun captureOnly(fields: Int, metadata: Set? = null): CaptureOptions { - return CaptureOptions( + fun captureOnly(fields: Int, metadata: Set? = null): ErrorCaptureOptions { + return ErrorCaptureOptions( stacktrace = (fields and CAPTURE_STACKTRACE) != 0, breadcrumbs = (fields and CAPTURE_BREADCRUMBS) != 0, featureFlags = (fields and CAPTURE_FEATURE_FLAGS) != 0, @@ -75,7 +75,7 @@ class CaptureOptions( * Return CaptureOptions that will not capture any optional fields. */ @JvmStatic - fun captureNothing(): CaptureOptions { + fun captureNothing(): ErrorCaptureOptions { return captureOnly(0, emptySet()) } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt similarity index 74% rename from bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt rename to bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt index e953683593..4c7f0a01a4 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/CaptureOptionsTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt @@ -6,11 +6,11 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test -class CaptureOptionsTest { +class ErrorCaptureOptionsTest { @Test fun testDefaultValues() { - val options = CaptureOptions() + val options = ErrorCaptureOptions() assertTrue(options.breadcrumbs) assertTrue(options.featureFlags) assertNull(options.metadata) @@ -21,8 +21,8 @@ class CaptureOptionsTest { @Test fun testCaptureOnlyStacktraceAndUser() { - val fields = CaptureOptions.CAPTURE_STACKTRACE or CaptureOptions.CAPTURE_USER - val options = CaptureOptions.captureOnly(fields) + val fields = ErrorCaptureOptions.CAPTURE_STACKTRACE or ErrorCaptureOptions.CAPTURE_USER + val options = ErrorCaptureOptions.captureOnly(fields) assertTrue(options.stacktrace) assertFalse(options.breadcrumbs) assertFalse(options.featureFlags) @@ -33,9 +33,9 @@ class CaptureOptionsTest { @Test fun testCaptureOnlyWithMetadata() { - val fields = CaptureOptions.CAPTURE_BREADCRUMBS or CaptureOptions.CAPTURE_FEATURE_FLAGS + val fields = ErrorCaptureOptions.CAPTURE_BREADCRUMBS or ErrorCaptureOptions.CAPTURE_FEATURE_FLAGS val metadataTabs = setOf("customTab") - val options = CaptureOptions.captureOnly(fields, metadataTabs) + val options = ErrorCaptureOptions.captureOnly(fields, metadataTabs) assertFalse(options.stacktrace) assertTrue(options.breadcrumbs) assertTrue(options.featureFlags) @@ -46,7 +46,7 @@ class CaptureOptionsTest { @Test fun testCaptureNothing() { - val options = CaptureOptions.captureNothing() + val options = ErrorCaptureOptions.captureNothing() assertFalse(options.stacktrace) assertFalse(options.breadcrumbs) assertFalse(options.featureFlags) diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt index 1408a447d6..8c31ffdd19 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt @@ -2,7 +2,7 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag -import com.bugsnag.android.CaptureOptions +import com.bugsnag.android.ErrorCaptureOptions import com.bugsnag.android.Configuration import com.bugsnag.android.ErrorOptions @@ -11,7 +11,7 @@ class ErrorOptionsScenario( context: Context, eventMetadata: String? ) : Scenario(config, context, eventMetadata) { - private val errorOptions = ErrorOptions(CaptureOptions.captureNothing()) + private val errorOptions = ErrorOptions(ErrorCaptureOptions.captureNothing()) override fun startScenario() { super.startScenario() From 697444e9daa0ee892d0edc0f76f6f25f0af64b15 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 08:22:33 +0000 Subject: [PATCH 05/17] refactor(ErrorCaptureOptions): Changed the order of the ErrorCaptureOptions constructor arguments keep the boolean flags grouped --- .../api/bugsnag-android-core.api | 76 +++++++++---------- .../java/com/bugsnag/android/ErrorOptions.kt | 18 ++--- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 5d212ef7e4..19b363207d 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -112,40 +112,6 @@ public class com/bugsnag/android/BugsnagVmViolationListener : android/os/StrictM public fun onVmViolation (Landroid/os/strictmode/Violation;)V } -public final class com/bugsnag/android/CaptureOptions { - public static final field CAPTURE_BREADCRUMBS I - public static final field CAPTURE_FEATURE_FLAGS I - public static final field CAPTURE_STACKTRACE I - public static final field CAPTURE_THREADS I - public static final field CAPTURE_USER I - public static final field Companion Lcom/bugsnag/android/CaptureOptions$Companion; - public fun ()V - public fun (ZZLjava/util/Set;ZZZ)V - public synthetic fun (ZZLjava/util/Set;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public static final fun captureNothing ()Lcom/bugsnag/android/CaptureOptions; - public static final fun captureOnly (I)Lcom/bugsnag/android/CaptureOptions; - public static final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/CaptureOptions; - public final fun getBreadcrumbs ()Z - public final fun getFeatureFlags ()Z - public final fun getMetadata ()Ljava/util/Set; - public final fun getStacktrace ()Z - public final fun getThreads ()Z - public final fun getUser ()Z - public final fun setBreadcrumbs (Z)V - public final fun setFeatureFlags (Z)V - public final fun setMetadata (Ljava/util/Set;)V - public final fun setStacktrace (Z)V - public final fun setThreads (Z)V - public final fun setUser (Z)V -} - -public final class com/bugsnag/android/CaptureOptions$Companion { - public final fun captureNothing ()Lcom/bugsnag/android/CaptureOptions; - public final fun captureOnly (I)Lcom/bugsnag/android/CaptureOptions; - public final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/CaptureOptions; - public static synthetic fun captureOnly$default (Lcom/bugsnag/android/CaptureOptions$Companion;ILjava/util/Set;ILjava/lang/Object;)Lcom/bugsnag/android/CaptureOptions; -} - public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com/bugsnag/android/FeatureFlagAware, com/bugsnag/android/MetadataAware, com/bugsnag/android/UserAware { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Lcom/bugsnag/android/Configuration;)V @@ -391,12 +357,46 @@ public class com/bugsnag/android/Error : com/bugsnag/android/JsonStream$Streamab public fun toStream (Lcom/bugsnag/android/JsonStream;)V } +public final class com/bugsnag/android/ErrorCaptureOptions { + public static final field CAPTURE_BREADCRUMBS I + public static final field CAPTURE_FEATURE_FLAGS I + public static final field CAPTURE_STACKTRACE I + public static final field CAPTURE_THREADS I + public static final field CAPTURE_USER I + public static final field Companion Lcom/bugsnag/android/ErrorCaptureOptions$Companion; + public fun ()V + public fun (ZZZZZLjava/util/Set;)V + public synthetic fun (ZZZZZLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun captureNothing ()Lcom/bugsnag/android/ErrorCaptureOptions; + public static final fun captureOnly (I)Lcom/bugsnag/android/ErrorCaptureOptions; + public static final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/ErrorCaptureOptions; + public final fun getBreadcrumbs ()Z + public final fun getFeatureFlags ()Z + public final fun getMetadata ()Ljava/util/Set; + public final fun getStacktrace ()Z + public final fun getThreads ()Z + public final fun getUser ()Z + public final fun setBreadcrumbs (Z)V + public final fun setFeatureFlags (Z)V + public final fun setMetadata (Ljava/util/Set;)V + public final fun setStacktrace (Z)V + public final fun setThreads (Z)V + public final fun setUser (Z)V +} + +public final class com/bugsnag/android/ErrorCaptureOptions$Companion { + public final fun captureNothing ()Lcom/bugsnag/android/ErrorCaptureOptions; + public final fun captureOnly (I)Lcom/bugsnag/android/ErrorCaptureOptions; + public final fun captureOnly (ILjava/util/Set;)Lcom/bugsnag/android/ErrorCaptureOptions; + public static synthetic fun captureOnly$default (Lcom/bugsnag/android/ErrorCaptureOptions$Companion;ILjava/util/Set;ILjava/lang/Object;)Lcom/bugsnag/android/ErrorCaptureOptions; +} + public final class com/bugsnag/android/ErrorOptions { public fun ()V - public fun (Lcom/bugsnag/android/CaptureOptions;)V - public synthetic fun (Lcom/bugsnag/android/CaptureOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapture ()Lcom/bugsnag/android/CaptureOptions; - public final fun setCapture (Lcom/bugsnag/android/CaptureOptions;)V + public fun (Lcom/bugsnag/android/ErrorCaptureOptions;)V + public synthetic fun (Lcom/bugsnag/android/ErrorCaptureOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapture ()Lcom/bugsnag/android/ErrorCaptureOptions; + public final fun setCapture (Lcom/bugsnag/android/ErrorCaptureOptions;)V } public final class com/bugsnag/android/ErrorType : java/lang/Enum { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt index fc7de90a6a..b0a7906565 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorOptions.kt @@ -18,6 +18,15 @@ class ErrorCaptureOptions( /** Whether to capture feature flags. Defaults to true. */ var featureFlags: Boolean = true, + /** Whether to capture stacktrace. Defaults to true. */ + var stacktrace: Boolean = true, + + /** Whether to capture thread state. Defaults to true. */ + var threads: Boolean = true, + + /** Whether to capture user information. Defaults to true. */ + var user: Boolean = true, + /** * Controls which custom metadata tabs are included. * - null: all metadata tabs captured @@ -27,15 +36,6 @@ class ErrorCaptureOptions( * Note: app and device tabs are always captured. */ var metadata: Set? = null, - - /** Whether to capture stacktrace. Defaults to true. */ - var stacktrace: Boolean = true, - - /** Whether to capture thread state. Defaults to true. */ - var threads: Boolean = true, - - /** Whether to capture user information. Defaults to true. */ - var user: Boolean = true, ) { /** * Create a CaptureOptions with all of the default capturing behaviour (capture everything). From 4f7e7751459093a3202f385f2661b772cd34eb3c Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 09:02:43 +0000 Subject: [PATCH 06/17] feat(Client): Added notify(Throwable, ErrorOptions) to Client & Bugsnag --- .../api/bugsnag-android-core.api | 2 + .../java/com/bugsnag/android/Bugsnag.java | 11 ++ .../main/java/com/bugsnag/android/Client.java | 130 +++++++++--------- 3 files changed, 76 insertions(+), 67 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 19b363207d..7936f57e2a 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -82,6 +82,7 @@ public final class com/bugsnag/android/Bugsnag { public static fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public static fun markLaunchCompleted ()V public static fun notify (Ljava/lang/Throwable;)V + public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;)V public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public static fun pauseSession ()V @@ -140,6 +141,7 @@ public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com public fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public fun markLaunchCompleted ()V public fun notify (Ljava/lang/Throwable;)V + public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;)V public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public fun pauseSession ()V diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index d09b946f0c..97981d2d2b 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -255,6 +255,17 @@ public static void notify(@NonNull final Throwable exception, getClient().notify(exception, onError); } + /** + * Notify Bugsnag of a handled exception + * + * @param exception the exception to send to Bugsnag + * @param options additional options to adjust the reporting of the exception + */ + public static void notify(@NonNull final Throwable exception, + @Nullable final ErrorOptions options) { + getClient().notify(exception, options); + } + /** * Notify Bugsnag of a handled exception * diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 99a1a78e3f..e3654c9738 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -2,6 +2,13 @@ import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import com.bugsnag.android.internal.BackgroundTaskService; import com.bugsnag.android.internal.ForegroundDetector; import com.bugsnag.android.internal.ImmutableConfig; @@ -15,16 +22,6 @@ import com.bugsnag.android.internal.dag.Provider; import com.bugsnag.android.internal.dag.SystemServiceModule; -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - import java.io.File; import java.util.Collection; import java.util.Collections; @@ -37,6 +34,9 @@ import java.util.concurrent.RejectedExecutionException; import java.util.regex.Pattern; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. * Typically you'd instead use the static access provided in the Bugsnag class. @@ -383,23 +383,23 @@ public Unit invoke(String oldOrientation, String newOrientation) { return null; } }, new Function2() { - @Override + @Override public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { - memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); - if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { - leaveAutoBreadcrumb( - "Trim Memory", - BreadcrumbType.STATE, - Collections.singletonMap( + memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); + if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { + leaveAutoBreadcrumb( + "Trim Memory", + BreadcrumbType.STATE, + Collections.singletonMap( "trimLevel", memoryTrimState.getTrimLevelDescription() ) - ); - } - - memoryTrimState.emitObservableEvent(); - return null; - } + ); } + + memoryTrimState.emitObservableEvent(); + return null; + } + } )); } @@ -536,7 +536,7 @@ public boolean resumeSession() { /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - * + *

* In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @@ -548,7 +548,7 @@ public String getContext() { /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - * + *

* In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @@ -603,15 +603,15 @@ public User getUser() { /** * Add a "on error" callback, to execute code at the point where an error report is * captured in Bugsnag. - * + *

* You can use this to add or modify information attached to an Event * before it is sent to your dashboard. You can also return * false from any callback to prevent delivery. "on error" * callbacks do not run before reports generated in the event * of immediate app termination from crashes in C/C++ code. - * + *

* For example: - * + *

* Bugsnag.addOnError(new OnErrorCallback() { * public boolean run(Event event) { * event.setSeverity(Severity.INFO); @@ -648,12 +648,12 @@ public void removeOnError(@NonNull OnErrorCallback onError) { /** * Add an "on breadcrumb" callback, to execute code before every * breadcrumb captured by Bugsnag. - * + *

* You can use this to modify breadcrumbs before they are stored by Bugsnag. * You can also return false from any callback to ignore a breadcrumb. - * + *

* For example: - * + *

* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { * public boolean run(Breadcrumb breadcrumb) { * return false; // ignore the breadcrumb @@ -689,12 +689,12 @@ public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { /** * Add an "on session" callback, to execute code before every * session captured by Bugsnag. - * + *

* You can use this to modify sessions before they are stored by Bugsnag. * You can also return false from any callback to ignore a session. - * + *

* For example: - * + *

* Bugsnag.onSession(new OnSessionCallback() { * public boolean run(Session session) { * return false; // ignore the session @@ -747,6 +747,16 @@ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { notify(exc, null, onError); } + /** + * Notify Bugsnag of a handled exception + * + * @param exc the exception to send to Bugsnag + * @param options the error options + */ + public void notify(@NonNull Throwable exc, @Nullable ErrorOptions options) { + notify(exc, options, null); + } + /** * Notify Bugsnag of a handled exception * @@ -767,7 +777,7 @@ public void notify( SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Event event = createEventWithOptions(exc, severityReason, options); event.setGroupingDiscriminator(getGroupingDiscriminator()); - populateAndNotifyAndroidEvent(event,options, onError); + populateAndNotifyAndroidEvent(event, options, onError); } else { logNull("notify"); } @@ -796,33 +806,19 @@ private Event createEventWithOptions( final FeatureFlags featureFlags = capture == null || capture.getFeatureFlags() ? featureFlagState.getFeatureFlags() : new FeatureFlags(); - final boolean stacktrace = capture == null || capture.getStacktrace(); - final boolean threads = capture == null || capture.getThreads(); - - Event event; - if (options != null || capture != null) { - event = new Event( - exc, - immutableConfig, - severityReason, - metadata, - featureFlags, - stacktrace, - threads, - logger - ); - } else { - event = new Event( - exc, - immutableConfig, - severityReason, - metadataState.getMetadata(), - featureFlagState.getFeatureFlags(), - stacktrace, - threads, - logger); - } - return event; + final boolean captureStacktrace = capture == null || capture.getStacktrace(); + final boolean captureThreads = capture == null || capture.getThreads(); + + return new Event( + exc, + immutableConfig, + severityReason, + metadata, + featureFlags, + captureStacktrace, + captureThreads, + logger + ); } private Metadata captureSelectedMetadata(@Nullable Set metadata) { @@ -847,7 +843,7 @@ private Metadata captureSelectedMetadata(@Nullable Set metadata) { /** * Caches an error then attempts to notify. - * + *

* Should only ever be called from the {@link ExceptionHandler}. */ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, @@ -881,7 +877,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event, void populateAndNotifyAndroidEvent(@NonNull Event event, @Nullable ErrorOptions options, @Nullable OnErrorCallback onError - ) { + ) { populateDeviceAndAppData(event); if (options == null) { @@ -996,7 +992,7 @@ void notifyInternal(@NonNull Event event, * Returns the current buffer of breadcrumbs that will be sent with captured events. This * ordered list represents the most recent breadcrumbs to be captured up to the limit * set in {@link Configuration#getMaxBreadcrumbs()}. - * + *

* The returned collection is readonly and mutating the list will cause no effect on the * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and @@ -1226,7 +1222,7 @@ public void clearFeatureFlags() { /** * Retrieves information about the last launch of the application, if it has been run before. - * + *

* For example, this allows checking whether the app crashed on its last launch, which could * be used to perform conditional behaviour to recover from crashes, such as clearing the * app data cache. @@ -1240,7 +1236,7 @@ public LastRunInfo getLastRunInfo() { * Informs Bugsnag that the application has finished launching. Once this has been called * {@link AppWithState#isLaunching()} will always be false in any new error reports, * and synchronous delivery will not be attempted on the next launch for any fatal crashes. - * + *

* By default this method will be called after Bugsnag is initialized when * {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually * has precedence over the value supplied via the launchDurationMillis configuration option. From 75cf5ae14b7b8b2965f1c79e64b3052b684ec99b Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 09:46:25 +0000 Subject: [PATCH 07/17] test(ErrorOptions): added a more complete e2e test suite for ErrorCaptureOptions --- .../scenarios/ErrorOptionsScenario.kt | 23 +++++- .../full_tests/error_capture_options.feature | 78 +++++++++++++++++++ features/smoke_tests/02_handled.feature | 10 +-- 3 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 features/full_tests/error_capture_options.feature diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt index 8c31ffdd19..e0296eb2ef 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt @@ -13,12 +13,31 @@ class ErrorOptionsScenario( ) : Scenario(config, context, eventMetadata) { private val errorOptions = ErrorOptions(ErrorCaptureOptions.captureNothing()) + init { + val enabledCapture = eventMetadata?.splitToSequence(' ') + ?.map { it.trim() } + ?.toMutableSet() + ?: mutableSetOf() + + // remove each of the options - if they were present set the associated capture option + errorOptions.capture.stacktrace = enabledCapture.remove("stacktrace") + errorOptions.capture.breadcrumbs = enabledCapture.remove("breadcrumbs") + errorOptions.capture.featureFlags = enabledCapture.remove("featureFlags") + errorOptions.capture.threads = enabledCapture.remove("threads") + errorOptions.capture.user = enabledCapture.remove("user") + + // any remaining options are used for metadata tabs + errorOptions.capture.metadata = enabledCapture + } + override fun startScenario() { super.startScenario() - Bugsnag.addMetadata("custom", "key", "value") + Bugsnag.addMetadata("custom", "key1", "value") + Bugsnag.addMetadata("custom2", "testKey2", "value") Bugsnag.leaveBreadcrumb("Test breadcrumb") Bugsnag.addFeatureFlag("testFeatureFlag", "variantA") + Bugsnag.addFeatureFlag("featureFlag2") Bugsnag.setUser("123", "jane@doe.com", "Jane Doe") - Bugsnag.notify(generateException(), errorOptions, null) + Bugsnag.notify(generateException(), errorOptions) } } diff --git a/features/full_tests/error_capture_options.feature b/features/full_tests/error_capture_options.feature new file mode 100644 index 0000000000..4f4485cff8 --- /dev/null +++ b/features/full_tests/error_capture_options.feature @@ -0,0 +1,78 @@ +Feature: ErrorCaptureOptions + + Background: + Given I clear all persistent data + + Scenario: Capture only stacktrace + When I configure the app to run in the "stacktrace" state + And I run "ErrorOptionsScenario" + 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 + # 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 "ErrorOptionsScenario.startScenario" + And the exception "stacktrace.0.file" equals "SourceFile" + + And the event "user.id" is null + And the event "user.name" is null + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.featureFlags" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements + + Scenario: Capture only user + When I configure the app to run in the "user" state + And I run "ErrorOptionsScenario" + 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 event "user.id" equals "123" + And the event "user.email" equals "jane@doe.com" + And the event "user.name" equals "Jane Doe" + + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.featureFlags" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements + + Scenario: Capture only breadcrumbs + When I configure the app to run in the "breadcrumbs" state + And I run "ErrorOptionsScenario" + 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 event "user.id" equals "123" + And the event "user.email" equals "jane@doe.com" + And the event "user.name" equals "Jane Doe" + + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.featureFlags" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements + + Scenario: Capture only feature flags + When I configure the app to run in the "user" state + And I run "ErrorOptionsScenario" + 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 event "user.id" is null + And the event "user.name" is null + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements + + And the error payload field "events.0.featureFlags" is an array with 2 elements + And event 0 contains the feature flag "testFeatureFlag" with variant "variantA" + And event 0 contains the feature flag "featureFlag2" with no variant + + Scenario: Capture selected metadata and stacktrace + When I configure the app to run in the "stacktrace custom2" state + And I run "ErrorOptionsScenario" + 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 + # 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 "ErrorOptionsScenario.startScenario" + And the exception "stacktrace.0.file" equals "SourceFile" + + And the event "user.id" is null + And the event "user.name" is null + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.featureFlags" is an array with 0 elements + And the event "metaData.custom2.testKey2" equals "value" diff --git a/features/smoke_tests/02_handled.feature b/features/smoke_tests/02_handled.feature index 6636a14e44..c8112a4446 100644 --- a/features/smoke_tests/02_handled.feature +++ b/features/smoke_tests/02_handled.feature @@ -250,13 +250,13 @@ Feature: Handled smoke tests And the event "threads.0.stacktrace.0.file" is not null And the event "threads.0.stacktrace.0.lineNumber" is not null - Scenario: Handled exceptions with negative options + Scenario: Handled exceptions with captureNone options When I run "ErrorOptionsScenario" 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 event "stacktrace" is null - And the event "breadcrumbs.0" is null - And the event "featureFlags.0" is null - And the event "threads.0" is null And the event "user.id" is null - And the event "user.name" is null \ No newline at end of file + And the event "user.name" is null + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.featureFlags" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements From c6ed145f2435f8f46deedc5f7d3bac2ba72fc6dd Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 10:31:36 +0000 Subject: [PATCH 08/17] chore(lint): fixed lint errors --- .../main/java/com/bugsnag/android/Client.java | 52 +++++++++---------- .../scenarios/ErrorOptionsScenario.kt | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index e3654c9738..fccc1e16de 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -2,13 +2,6 @@ import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - import com.bugsnag.android.internal.BackgroundTaskService; import com.bugsnag.android.internal.ForegroundDetector; import com.bugsnag.android.internal.ImmutableConfig; @@ -22,6 +15,16 @@ import com.bugsnag.android.internal.dag.Provider; import com.bugsnag.android.internal.dag.SystemServiceModule; +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + import java.io.File; import java.util.Collection; import java.util.Collections; @@ -34,9 +37,6 @@ import java.util.concurrent.RejectedExecutionException; import java.util.regex.Pattern; -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. * Typically you'd instead use the static access provided in the Bugsnag class. @@ -383,23 +383,23 @@ public Unit invoke(String oldOrientation, String newOrientation) { return null; } }, new Function2() { - @Override - public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { - memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); - if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { - leaveAutoBreadcrumb( - "Trim Memory", - BreadcrumbType.STATE, - Collections.singletonMap( - "trimLevel", memoryTrimState.getTrimLevelDescription() - ) - ); - } + @Override + public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { + memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); + if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { + leaveAutoBreadcrumb( + "Trim Memory", + BreadcrumbType.STATE, + Collections.singletonMap( + "trimLevel", memoryTrimState.getTrimLevelDescription() + ) + ); + } - memoryTrimState.emitObservableEvent(); - return null; - } - } + memoryTrimState.emitObservableEvent(); + return null; + } + } )); } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt index e0296eb2ef..d0a498e4ca 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt @@ -2,8 +2,8 @@ package com.bugsnag.android.mazerunner.scenarios import android.content.Context import com.bugsnag.android.Bugsnag -import com.bugsnag.android.ErrorCaptureOptions import com.bugsnag.android.Configuration +import com.bugsnag.android.ErrorCaptureOptions import com.bugsnag.android.ErrorOptions class ErrorOptionsScenario( From 81992057b60bfeae0268c8a5af93d2cf21a313a5 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 10:54:16 +0000 Subject: [PATCH 09/17] refactor(Client): removed the `notify(Throwable, ErrorOptions)` from Client / Bugsnag to avoid breaking source compatibility for `notify(error, null)` (ambiguous call) --- bugsnag-android-core/api/bugsnag-android-core.api | 2 -- .../src/main/java/com/bugsnag/android/Bugsnag.java | 11 ----------- .../src/main/java/com/bugsnag/android/Client.java | 10 ---------- .../mazerunner/scenarios/ErrorOptionsScenario.kt | 2 +- 4 files changed, 1 insertion(+), 24 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 7936f57e2a..19b363207d 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -82,7 +82,6 @@ public final class com/bugsnag/android/Bugsnag { public static fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public static fun markLaunchCompleted ()V public static fun notify (Ljava/lang/Throwable;)V - public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;)V public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public static fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public static fun pauseSession ()V @@ -141,7 +140,6 @@ public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com public fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V public fun markLaunchCompleted ()V public fun notify (Ljava/lang/Throwable;)V - public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;)V public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/ErrorOptions;Lcom/bugsnag/android/OnErrorCallback;)V public fun notify (Ljava/lang/Throwable;Lcom/bugsnag/android/OnErrorCallback;)V public fun pauseSession ()V diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java index 97981d2d2b..d09b946f0c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java @@ -255,17 +255,6 @@ public static void notify(@NonNull final Throwable exception, getClient().notify(exception, onError); } - /** - * Notify Bugsnag of a handled exception - * - * @param exception the exception to send to Bugsnag - * @param options additional options to adjust the reporting of the exception - */ - public static void notify(@NonNull final Throwable exception, - @Nullable final ErrorOptions options) { - getClient().notify(exception, options); - } - /** * Notify Bugsnag of a handled exception * diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index fccc1e16de..6bf3941021 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -747,16 +747,6 @@ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { notify(exc, null, onError); } - /** - * Notify Bugsnag of a handled exception - * - * @param exc the exception to send to Bugsnag - * @param options the error options - */ - public void notify(@NonNull Throwable exc, @Nullable ErrorOptions options) { - notify(exc, options, null); - } - /** * Notify Bugsnag of a handled exception * diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt index d0a498e4ca..58d14a61df 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ErrorOptionsScenario.kt @@ -38,6 +38,6 @@ class ErrorOptionsScenario( Bugsnag.addFeatureFlag("testFeatureFlag", "variantA") Bugsnag.addFeatureFlag("featureFlag2") Bugsnag.setUser("123", "jane@doe.com", "Jane Doe") - Bugsnag.notify(generateException(), errorOptions) + Bugsnag.notify(generateException(), errorOptions, null) } } From b4d19ee148d2bf9160d5469186d370c5ae1734a1 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 11:51:02 +0000 Subject: [PATCH 10/17] test(ErrorOptions): fixed the stacktrace check in End-to-end tests --- features/full_tests/error_capture_options.feature | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/features/full_tests/error_capture_options.feature b/features/full_tests/error_capture_options.feature index 4f4485cff8..1a8ea5a77a 100644 --- a/features/full_tests/error_capture_options.feature +++ b/features/full_tests/error_capture_options.feature @@ -10,7 +10,7 @@ Feature: ErrorCaptureOptions And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier # 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 "ErrorOptionsScenario.startScenario" + And the event "exceptions.0.stacktrace.0.method" ends with "com.bugsnag.android.mazerunner.scenarios.Scenario.generateException" And the exception "stacktrace.0.file" equals "SourceFile" And the event "user.id" is null @@ -39,16 +39,16 @@ Feature: ErrorCaptureOptions 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 event "user.id" equals "123" - And the event "user.email" equals "jane@doe.com" - And the event "user.name" equals "Jane Doe" + And the event has 2 breadcrumbs + And the event "breadcrumbs.1.name" equals "Test breadcrumb" - And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the event "user.id" is null + And the event "user.name" is null And the error payload field "events.0.featureFlags" is an array with 0 elements And the error payload field "events.0.threads" is an array with 0 elements Scenario: Capture only feature flags - When I configure the app to run in the "user" state + When I configure the app to run in the "featureFlags" state And I run "ErrorOptionsScenario" 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 @@ -68,7 +68,7 @@ Feature: ErrorCaptureOptions And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier # 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 "ErrorOptionsScenario.startScenario" + And the event "exceptions.0.stacktrace.0.method" ends with "com.bugsnag.android.mazerunner.scenarios.Scenario.generateException" And the exception "stacktrace.0.file" equals "SourceFile" And the event "user.id" is null From d56bbfe94d2f86377ef4b09ee3539533053433d9 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 14:06:03 +0000 Subject: [PATCH 11/17] chore(gem): add openssl to Gemfile --- Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 23149cdcfa..03ce809b59 100644 --- a/Gemfile +++ b/Gemfile @@ -10,3 +10,5 @@ gem 'bugsnag-maze-runner', '~>10.0' #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner' gem "license_finder", "~> 7.0" + +gem "openssl", "~> 3.3" From f9e1af4a23bfdf0388ced5408a45cd582456f5ba Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 14:30:13 +0000 Subject: [PATCH 12/17] chore(example): removed the ErrorOptions from the example app --- .../example/bugsnag/android/BaseCrashyActivity.kt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt index 0bf8eaf0b9..531dc39a76 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/BaseCrashyActivity.kt @@ -9,9 +9,7 @@ import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.bugsnag.android.BreadcrumbType import com.bugsnag.android.Bugsnag -import com.bugsnag.android.CaptureOptions import com.bugsnag.android.Configuration -import com.bugsnag.android.ErrorOptions import com.bugsnag.android.Severity import com.example.foo.CrashyClass import com.google.android.material.snackbar.Snackbar @@ -23,7 +21,6 @@ import okhttp3.internal.notify import java.io.IOException import java.util.Date - open class BaseCrashyActivity : AppCompatActivity() { companion object { @@ -95,14 +92,7 @@ open class BaseCrashyActivity : AppCompatActivity() { try { throw RuntimeException("Non-Fatal Crash") } catch (e: RuntimeException) { - - - val errorOptions: ErrorOptions = ErrorOptions(CaptureOptions.captureNothing()) - Bugsnag.addMetadata("custom", "key", "value") - Bugsnag.leaveBreadcrumb("Test breadcrumb") - Bugsnag.addFeatureFlag("testFeatureFlag", "variantA") - Bugsnag.setUser("123", "jane@doe.com", "Jane Doe") - Bugsnag.notify(e, errorOptions){ + Bugsnag.notify(e) { showSnackbar() true } From 37d8be86535c1c9e663b4dd58f1de33353d1cbcc Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 16:38:08 +0000 Subject: [PATCH 13/17] test(ErrorCaptureOptions): added a test for all possible bit fields used in `captureOnly` --- bugsnag-android-core/detekt-baseline.xml | 1 - .../android/ErrorCaptureOptionsTest.kt | 49 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index f925d09fd1..398e5c8809 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -67,7 +67,6 @@ MagicNumber:JsonStream.kt$JsonStream$32 MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3 MagicNumber:SessionStore.kt$SessionStore$60 - MaxLineLength:ErrorTest.kt$ErrorTest$val err = Error.createError(IllegalStateException("Some err", RuntimeException("Whoops")), setOf(), NoopLogger, false) MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)" MaxLineLength:ThreadState.kt$ThreadState$"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]" NestedBlockDepth:FileStore.kt$FileStore$fun deleteStoredFiles(storedFiles: Collection<File>?) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt index 4c7f0a01a4..81699e3858 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorCaptureOptionsTest.kt @@ -1,5 +1,10 @@ package com.bugsnag.android +import com.bugsnag.android.ErrorCaptureOptions.Companion.CAPTURE_BREADCRUMBS +import com.bugsnag.android.ErrorCaptureOptions.Companion.CAPTURE_FEATURE_FLAGS +import com.bugsnag.android.ErrorCaptureOptions.Companion.CAPTURE_STACKTRACE +import com.bugsnag.android.ErrorCaptureOptions.Companion.CAPTURE_THREADS +import com.bugsnag.android.ErrorCaptureOptions.Companion.CAPTURE_USER import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -21,7 +26,7 @@ class ErrorCaptureOptionsTest { @Test fun testCaptureOnlyStacktraceAndUser() { - val fields = ErrorCaptureOptions.CAPTURE_STACKTRACE or ErrorCaptureOptions.CAPTURE_USER + val fields = CAPTURE_STACKTRACE or CAPTURE_USER val options = ErrorCaptureOptions.captureOnly(fields) assertTrue(options.stacktrace) assertFalse(options.breadcrumbs) @@ -33,7 +38,7 @@ class ErrorCaptureOptionsTest { @Test fun testCaptureOnlyWithMetadata() { - val fields = ErrorCaptureOptions.CAPTURE_BREADCRUMBS or ErrorCaptureOptions.CAPTURE_FEATURE_FLAGS + val fields = CAPTURE_BREADCRUMBS or CAPTURE_FEATURE_FLAGS val metadataTabs = setOf("customTab") val options = ErrorCaptureOptions.captureOnly(fields, metadataTabs) assertFalse(options.stacktrace) @@ -54,4 +59,44 @@ class ErrorCaptureOptionsTest { assertFalse(options.user) assertEquals(emptySet(), options.metadata) } + + @Test + fun captureOnlyBitFields() { + val bitMappings = mapOf Boolean>( + CAPTURE_STACKTRACE to ErrorCaptureOptions::stacktrace, + CAPTURE_BREADCRUMBS to ErrorCaptureOptions::breadcrumbs, + CAPTURE_FEATURE_FLAGS to ErrorCaptureOptions::featureFlags, + CAPTURE_THREADS to ErrorCaptureOptions::threads, + CAPTURE_USER to ErrorCaptureOptions::user, + ) + + // test every possible combination of bit fields -> Boolean mappings + val allBits = bitMappings.keys.toIntArray() + val totalCombinations = 1 shl allBits.size + + for (combination in 0 until totalCombinations) { + var fields = 0 + val expectedEnabled = mutableSetOf() + + // Build the bit field based on the combination + for (i in allBits.indices) { + if ((combination and (1 shl i)) != 0) { + fields = fields or allBits[i] + expectedEnabled.add(allBits[i]) + } + } + + val options = ErrorCaptureOptions.captureOnly(fields) + + // Verify each field matches expectations + for ((bit, getter) in bitMappings) { + val shouldBeEnabled = bit in expectedEnabled + assertEquals( + "Combination $combination: bit $bit should be $shouldBeEnabled", + shouldBeEnabled, + getter(options) + ) + } + } + } } From 3c876a4b8f2732832eb7119b14a3b36a1ad245a3 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 2 Dec 2025 17:01:11 +0000 Subject: [PATCH 14/17] refactor(Client.notify): further simplified the `notify(Throwable, ErrorOption, ...)` implementation --- .../main/java/com/bugsnag/android/Client.java | 138 ++++-------------- .../java/com/bugsnag/android/MetadataState.kt | 8 + 2 files changed, 35 insertions(+), 111 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 6bf3941021..75f594b699 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -536,7 +536,7 @@ public boolean resumeSession() { /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - *

+ * * In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @@ -548,7 +548,7 @@ public String getContext() { /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - *

+ * * In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @@ -603,15 +603,15 @@ public User getUser() { /** * Add a "on error" callback, to execute code at the point where an error report is * captured in Bugsnag. - *

+ * * You can use this to add or modify information attached to an Event * before it is sent to your dashboard. You can also return * false from any callback to prevent delivery. "on error" * callbacks do not run before reports generated in the event * of immediate app termination from crashes in C/C++ code. - *

+ * * For example: - *

+ * * Bugsnag.addOnError(new OnErrorCallback() { * public boolean run(Event event) { * event.setSeverity(Severity.INFO); @@ -648,12 +648,12 @@ public void removeOnError(@NonNull OnErrorCallback onError) { /** * Add an "on breadcrumb" callback, to execute code before every * breadcrumb captured by Bugsnag. - *

+ * * You can use this to modify breadcrumbs before they are stored by Bugsnag. * You can also return false from any callback to ignore a breadcrumb. - *

+ * * For example: - *

+ * * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { * public boolean run(Breadcrumb breadcrumb) { * return false; // ignore the breadcrumb @@ -689,12 +689,12 @@ public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { /** * Add an "on session" callback, to execute code before every * session captured by Bugsnag. - *

+ * * You can use this to modify sessions before they are stored by Bugsnag. * You can also return false from any callback to ignore a session. - *

+ * * For example: - *

+ * * Bugsnag.onSession(new OnSessionCallback() { * public boolean run(Session session) { * return false; // ignore the session @@ -776,23 +776,12 @@ public void notify( private Event createEventWithOptions( @NonNull Throwable exc, @NonNull SeverityReason severityReason, - @NonNull ErrorOptions options + @Nullable ErrorOptions options ) { - if (options == null) { - return new Event( - exc, - immutableConfig, - severityReason, - metadataState.getMetadata(), - featureFlagState.getFeatureFlags(), - logger - ); - } - final ErrorCaptureOptions capture = options != null ? options.getCapture() : null; final Metadata metadata = capture == null || capture.getMetadata() == null ? metadataState.getMetadata() - : captureSelectedMetadata(capture.getMetadata()); + : metadataState.selectMetadata(capture.getMetadata()); final FeatureFlags featureFlags = capture == null || capture.getFeatureFlags() ? featureFlagState.getFeatureFlags() : new FeatureFlags(); @@ -811,29 +800,9 @@ private Event createEventWithOptions( ); } - private Metadata captureSelectedMetadata(@Nullable Set metadata) { - Map> all = snapshotAllMetadataTabsExcludingAppDevice(); - Metadata selectedMetadataTabs = new Metadata(); - if (metadata != null || !metadata.isEmpty()) { - for (Map.Entry> e : all.entrySet()) { - if (metadata.contains(e.getKey())) { - selectedMetadataTabs.addMetadata(e.getKey(), e.getValue()); - } - } - } - - for (String tab : metadata) { - Object value = all.get(tab); - if (value != null) { - selectedMetadataTabs.addMetadata("", tab, value); - } - } - return selectedMetadataTabs; - } - /** * Caches an error then attempts to notify. - *

+ * * Should only ever be called from the {@link ExceptionHandler}. */ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, @@ -869,12 +838,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event, @Nullable OnErrorCallback onError ) { populateDeviceAndAppData(event); - - if (options == null) { - populateAllEventData(event); - } else { - populateConditionalEventData(event, options); - } + populateEventData(event, options); event.setContext(contextState.getContext()); event.setInternalMetrics(internalMetrics); @@ -890,67 +854,19 @@ private void populateDeviceAndAppData(@NonNull Event event) { event.addMetadata("app", appDataCollector.getAppDataMetadata()); } - private void populateAllEventData(@NonNull Event event) { - event.setBreadcrumbs(breadcrumbState.copy()); + private void populateEventData(@NonNull Event event, @Nullable ErrorOptions options) { + final ErrorCaptureOptions capture = options != null && options.getCapture() != null + ? options.getCapture() + : null; - User user = userState.get().getUser(); - event.setUser(user.getId(), user.getEmail(), user.getName()); - } - - private void populateConditionalEventData(@NonNull Event event, @NonNull ErrorOptions options) { - final ErrorCaptureOptions capture = - options.getCapture() != null ? options.getCapture() : null; - - if (capture != null) { - if (capture.getBreadcrumbs()) { - event.setBreadcrumbs(breadcrumbState.copy()); - } - - if (capture.getUser()) { - User user = userState.get().getUser(); - event.setUser(user.getId(), user.getEmail(), user.getName()); - } - - copySelectedMetadataTabs(event, capture.getMetadata()); - } else { - populateAllEventData(event); + if (capture == null || capture.getBreadcrumbs()) { + event.setBreadcrumbs(breadcrumbState.copy()); } - } - - /** - * Copies only the selected metadata tabs from state into the event. - */ - private void copySelectedMetadataTabs(@NonNull Event event, @Nullable Set allow) { - Map> all = snapshotAllMetadataTabsExcludingAppDevice(); - if (allow != null) { - for (Map.Entry> e : all.entrySet()) { - if (allow.contains(e.getKey())) { - event.addMetadata(e.getKey(), e.getValue()); - } - } - return; - } - if (allow.isEmpty()) { - return; + if (capture == null || capture.getUser()) { + User user = userState.get().getUser(); + event.setUser(user.getId(), user.getEmail(), user.getName()); } - for (String tab : allow) { - Object value = all.get(tab); - if (value != null) { - event.addMetadata("", tab, value); - } - } - } - - /** - * Returns a shallow snapshot of all metadata tabs except "app" and "device". - * Implement using existing MetadataState APIs; if none exist, add a safe accessor there. - */ - private Map> snapshotAllMetadataTabsExcludingAppDevice() { - Map> snapshot = metadataState.getMetadata().toMap(); - snapshot.remove("app"); - snapshot.remove("device"); - return snapshot; } void notifyInternal(@NonNull Event event, @@ -982,7 +898,7 @@ void notifyInternal(@NonNull Event event, * Returns the current buffer of breadcrumbs that will be sent with captured events. This * ordered list represents the most recent breadcrumbs to be captured up to the limit * set in {@link Configuration#getMaxBreadcrumbs()}. - *

+ * * The returned collection is readonly and mutating the list will cause no effect on the * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and @@ -1212,7 +1128,7 @@ public void clearFeatureFlags() { /** * Retrieves information about the last launch of the application, if it has been run before. - *

+ * * For example, this allows checking whether the app crashed on its last launch, which could * be used to perform conditional behaviour to recover from crashes, such as clearing the * app data cache. @@ -1226,7 +1142,7 @@ public LastRunInfo getLastRunInfo() { * Informs Bugsnag that the application has finished launching. Once this has been called * {@link AppWithState#isLaunching()} will always be false in any new error reports, * and synchronous delivery will not be attempted on the next launch for any fatal crashes. - *

+ * * By default this method will be called after Bugsnag is initialized when * {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually * has precedence over the value supplied via the launchDurationMillis configuration option. diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt index d305b6b6e4..7ab7f51761 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/MetadataState.kt @@ -26,6 +26,14 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : notifyClear(section, key) } + fun selectMetadata(sections: Set): Metadata { + return Metadata( + metadata.store.filterTo(HashMap()) { (key, _) -> + key in sections || key == "device" || key == "app" + } + ) + } + private fun notifyClear(section: String, key: String?) { when (key) { null -> updateState { StateEvent.ClearMetadataSection(section) } From 78b1c00086b6f6ab12e4be3af3b8a5414569d0bd Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 3 Dec 2025 10:54:31 +0000 Subject: [PATCH 15/17] test(e2e): added stacktrace null checks to all non-stacktrace scenarios, and improved readability --- .../full_tests/error_capture_options.feature | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/features/full_tests/error_capture_options.feature b/features/full_tests/error_capture_options.feature index 1a8ea5a77a..9d41ac51b5 100644 --- a/features/full_tests/error_capture_options.feature +++ b/features/full_tests/error_capture_options.feature @@ -8,11 +8,13 @@ Feature: ErrorCaptureOptions And I run "ErrorOptionsScenario" 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 + # 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 "com.bugsnag.android.mazerunner.scenarios.Scenario.generateException" And the exception "stacktrace.0.file" equals "SourceFile" + # Everything else is null/empty And the event "user.id" is null And the event "user.name" is null And the error payload field "events.0.breadcrumbs" is an array with 0 elements @@ -25,10 +27,13 @@ Feature: ErrorCaptureOptions 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 + # User validation And the event "user.id" equals "123" And the event "user.email" equals "jane@doe.com" And the event "user.name" equals "Jane Doe" + # Everything else is null/empty + And the event "stacktrace" is null And the error payload field "events.0.breadcrumbs" is an array with 0 elements And the error payload field "events.0.featureFlags" is an array with 0 elements And the error payload field "events.0.threads" is an array with 0 elements @@ -39,9 +44,12 @@ Feature: ErrorCaptureOptions 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 + # Breadcrumb validation And the event has 2 breadcrumbs And the event "breadcrumbs.1.name" equals "Test breadcrumb" + # Everything else is null/empty + And the event "stacktrace" is null And the event "user.id" is null And the event "user.name" is null And the error payload field "events.0.featureFlags" is an array with 0 elements @@ -52,27 +60,35 @@ Feature: ErrorCaptureOptions And I run "ErrorOptionsScenario" 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 event "user.id" is null - And the event "user.name" is null - And the error payload field "events.0.breadcrumbs" is an array with 0 elements - And the error payload field "events.0.threads" is an array with 0 elements + # Feature flag validation And the error payload field "events.0.featureFlags" is an array with 2 elements And event 0 contains the feature flag "testFeatureFlag" with variant "variantA" And event 0 contains the feature flag "featureFlag2" with no variant + # Everything else is null/empty + And the event "stacktrace" is null + And the event "user.id" is null + And the event "user.name" is null + And the error payload field "events.0.breadcrumbs" is an array with 0 elements + And the error payload field "events.0.threads" is an array with 0 elements + Scenario: Capture selected metadata and stacktrace When I configure the app to run in the "stacktrace custom2" state And I run "ErrorOptionsScenario" 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 - # Stacktrace validation + + # 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 "com.bugsnag.android.mazerunner.scenarios.Scenario.generateException" And the exception "stacktrace.0.file" equals "SourceFile" + # Metadata validation + And the event "metaData.custom2.testKey2" equals "value" + + # Everything else is null/empty And the event "user.id" is null And the event "user.name" is null And the error payload field "events.0.breadcrumbs" is an array with 0 elements And the error payload field "events.0.featureFlags" is an array with 0 elements - And the event "metaData.custom2.testKey2" equals "value" From ebb31a8d5b43b9e4fcb14dd3bf91530d176d793b Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 3 Dec 2025 11:04:07 +0000 Subject: [PATCH 16/17] chore(Error): refactored `Error.createError` to follow the same order as `ErrorInternal.createError` --- .../src/main/java/com/bugsnag/android/Error.java | 2 +- .../src/main/java/com/bugsnag/android/EventInternal.kt | 4 ++-- .../src/test/java/com/bugsnag/android/ErrorTest.kt | 4 ++-- .../java/com/bugsnag/android/RecursiveThrowableCauseTest.kt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java index f6e6f152d0..8a65db2849 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java @@ -104,8 +104,8 @@ public void toStream(@NonNull JsonStream stream) throws IOException { } static List createError(@NonNull Throwable exc, - boolean captureStacktrace, @NonNull Collection projectPackages, + boolean captureStacktrace, @NonNull Logger logger ) { return ErrorInternal.Companion.createError(exc, projectPackages, captureStacktrace, logger); 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 f604c29880..54a944b7b6 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 @@ -29,8 +29,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata null -> mutableListOf() else -> Error.createError( originalError, - captureStacktrace, config.projectPackages, + captureStacktrace, config.logger ) }, @@ -354,7 +354,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata errors.add(newError) return newError } else { - val newErrors = Error.createError(thrownError, true, projectPackages, logger) + val newErrors = Error.createError(thrownError, projectPackages, true, logger) errors.addAll(newErrors) return newErrors.first() } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt index 3d871887ef..fb07549c8b 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ErrorTest.kt @@ -8,7 +8,7 @@ class ErrorTest { @Test fun createError() { - val err = Error.createError(RuntimeException("Whoops"), true, setOf(), NoopLogger) + val err = Error.createError(RuntimeException("Whoops"), setOf(), true, NoopLogger) assertEquals(1, err.size) assertEquals("Whoops", err[0].errorMessage) assertFalse(err[0].stacktrace.isEmpty()) @@ -18,8 +18,8 @@ class ErrorTest { fun createNestedError() { val err = Error.createError( IllegalStateException("Some err", RuntimeException("Whoops")), - true, setOf(), + true, NoopLogger ) assertEquals(2, err.size) diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt index 30f9dba2df..58087577dd 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/RecursiveThrowableCauseTest.kt @@ -16,7 +16,7 @@ class RecursiveThrowableCauseTest { @Test(timeout = 100L) fun testCreateEvent() { - Error.createError(createRecursiveThrowableChain(), false, emptyList(), NoopLogger) + Error.createError(createRecursiveThrowableChain(), emptyList(), false, NoopLogger) } private fun createRecursiveThrowableChain(): Throwable { From 0a646e5a55a22b18eb1215d32cf9e3c0c837318b Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 3 Dec 2025 11:17:22 +0000 Subject: [PATCH 17/17] test(e2e): additional custom metadata capturing checks --- features/full_tests/error_capture_options.feature | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/full_tests/error_capture_options.feature b/features/full_tests/error_capture_options.feature index 9d41ac51b5..c8c732305c 100644 --- a/features/full_tests/error_capture_options.feature +++ b/features/full_tests/error_capture_options.feature @@ -9,7 +9,7 @@ Feature: ErrorCaptureOptions 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 - # Stacktrace validation + # 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 "com.bugsnag.android.mazerunner.scenarios.Scenario.generateException" And the exception "stacktrace.0.file" equals "SourceFile" @@ -86,6 +86,9 @@ Feature: ErrorCaptureOptions # Metadata validation And the event "metaData.custom2.testKey2" equals "value" + And the event "metaData.custom" is null + And the event "metaData.app" is not null + And the event "metaData.device" is not null # Everything else is null/empty And the event "user.id" is null