From c74fa58b23024fae99deed003b065d4ec89ac1f5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 9 Oct 2026 06:25:08 +0200 Subject: [PATCH 1/4] fix(android): Handle feedback configurator failures Keep the feedback form usable when an options configuration callback throws. Apply callback changes transactionally so partially mutated feedback options are discarded. Co-Authored-By: Claude --- .../android/core/SentryUserFeedbackForm.java | 31 ++++++++++-- .../core/SentryUserFeedbackFormTest.kt | 50 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java index 611db3c1345..7da33575de4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java @@ -62,14 +62,37 @@ public class SentryUserFeedbackForm extends AlertDialog { final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { super(context, themeResId); this.associatedEventId = associatedEventId; - this.resolvedFeedbackOptions = - new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions()); + final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions(); + @NotNull + SentryFeedbackOptions feedbackOptions = new SentryFeedbackOptions(options.getFeedbackOptions()); if (configuration != null) { - configuration.configure(context, resolvedFeedbackOptions); + final @NotNull SentryFeedbackOptions configuredOptions = + new SentryFeedbackOptions(feedbackOptions); + try { + configuration.configure(context, configuredOptions); + feedbackOptions = configuredOptions; + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + options + .getLogger() + .log( + SentryLevel.ERROR, "Feedback OptionsConfiguration callback threw an exception.", e); + } } if (configurator != null) { - configurator.configure(resolvedFeedbackOptions); + final @NotNull SentryFeedbackOptions configuredOptions = + new SentryFeedbackOptions(feedbackOptions); + try { + configurator.configure(configuredOptions); + feedbackOptions = configuredOptions; + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + options + .getLogger() + .log(SentryLevel.ERROR, "Feedback OptionsConfigurator callback threw an exception.", e); + } } + this.resolvedFeedbackOptions = feedbackOptions; SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget"); maybeStartShakeDetection(context); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt index 95e50a85b14..b28cb9197cc 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt @@ -164,6 +164,56 @@ class SentryUserFeedbackFormTest { assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle) } + @Test + fun `when configuration throws, its partial changes are ignored`() { + fixture.options.isEnabled = true + val originalTitle = fixture.options.feedbackOptions.formTitle + val failure = IllegalStateException("configuration failed") + + val sut = + fixture.getSut( + configuration = { _, options -> + options.formTitle = "partial title" + throw failure + } + ) + sut.show() + + assertThat(sut.findViewById(R.id.sentry_dialog_user_feedback_title).text) + .isEqualTo(originalTitle) + } + + @Test + fun `when configuration throws a fatal error, it is not swallowed`() { + val failure = OutOfMemoryError("configuration failed") + + val thrown = + assertFailsWith { + fixture.getSut(configuration = { _, _ -> throw failure }) + } + + assertThat(thrown).isSameInstanceAs(failure) + } + + @Test + fun `when configurator throws, its partial changes are ignored`() { + fixture.options.isEnabled = true + val failure = IllegalStateException("configurator failed") + + val sut = + fixture.getSut( + configuration = { _, options -> options.formTitle = "configured title" }, + configurator = { options -> + options.formTitle = "partial title" + throw failure + }, + ) + sut.show() + + assertThat(sut.findViewById(R.id.sentry_dialog_user_feedback_title).text) + .isEqualTo("configured title") + } + @Test fun `dialog window does not have FLAG_ALT_FOCUSABLE_IM so soft keyboard can appear`() { fixture.options.isEnabled = true From 6f75a53b4a031522373122b650007afce62f8791 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 9 Oct 2026 06:25:31 +0200 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc054e0be18..5a778c8b032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Keep the Android feedback form available when an options configurator throws ([#6239](https://github.com/getsentry/sentry-java/pull/6239)) + ### Features - Add support for Android Navigation 3 through the new `sentry-android-navigation3` library ([#6233](https://github.com/getsentry/sentry-java/pull/6233)) From e32efe0d3f76d7432656d40b3b3415ae84ac820a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 13 Oct 2026 09:42:25 +0200 Subject: [PATCH 3/4] test(android): Verify configurator runs after configuration failure Ensure a failed legacy configuration does not prevent the configurator from receiving the original feedback options and applying its changes. Co-Authored-By: Claude --- .../android/core/SentryUserFeedbackFormTest.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt index b28cb9197cc..6115ac335f9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt @@ -165,22 +165,31 @@ class SentryUserFeedbackFormTest { } @Test - fun `when configuration throws, its partial changes are ignored`() { + fun `when configuration throws, configurator still runs with original options`() { fixture.options.isEnabled = true val originalTitle = fixture.options.feedbackOptions.formTitle + val customNameLabel = "custom name label" val failure = IllegalStateException("configuration failed") + var titleReceivedByConfigurator: CharSequence? = null val sut = fixture.getSut( configuration = { _, options -> options.formTitle = "partial title" throw failure - } + }, + configurator = { options -> + titleReceivedByConfigurator = options.formTitle + options.nameLabel = customNameLabel + }, ) sut.show() + assertThat(titleReceivedByConfigurator).isEqualTo(originalTitle) assertThat(sut.findViewById(R.id.sentry_dialog_user_feedback_title).text) .isEqualTo(originalTitle) + assertThat(sut.findViewById(R.id.sentry_dialog_user_feedback_txt_name).text) + .isEqualTo(customNameLabel) } @Test From 08377ad4e4a92023676015d56024d37562f1e7be Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 13 Oct 2026 09:46:18 +0200 Subject: [PATCH 4/4] test(android): Verify configurator propagates fatal errors Ensure fatal errors from the feedback options configurator are rethrown instead of being handled as recoverable callback failures. Co-Authored-By: Claude --- .../sentry/android/core/SentryUserFeedbackFormTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt index 6115ac335f9..19414515b87 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt @@ -223,6 +223,16 @@ class SentryUserFeedbackFormTest { .isEqualTo("configured title") } + @Test + fun `when configurator throws a fatal error, it is not swallowed`() { + val failure = OutOfMemoryError("configurator failed") + + val thrown = + assertFailsWith { fixture.getSut(configurator = { throw failure }) } + + assertThat(thrown).isSameInstanceAs(failure) + } + @Test fun `dialog window does not have FLAG_ALT_FOCUSABLE_IM so soft keyboard can appear`() { fixture.options.isEnabled = true