Skip to content

Commit 155fa5a

Browse files
authored
Merge pull request #6239 from getsentry/fix/callback-error-handling-user-feedback
fix(android): Handle feedback configurator failures
2 parents 84e8b86 + 2afa056 commit 155fa5a

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Keep the Android feedback form available when an options configurator throws ([#6239](https://github.com/getsentry/sentry-java/pull/6239))
78
- Keep Spring requests running when a `SentryUserProvider` throws and discard the incomplete user identity ([#6240](https://github.com/getsentry/sentry-java/pull/6240))
89
- Drop Apollo 5 spans when `beforeSpan` throws without disrupting the GraphQL request ([#6238](https://github.com/getsentry/sentry-java/pull/6238))
910

‎sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,37 @@ public class SentryUserFeedbackForm extends AlertDialog {
6262
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
6363
super(context, themeResId);
6464
this.associatedEventId = associatedEventId;
65-
this.resolvedFeedbackOptions =
66-
new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions());
65+
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
66+
@NotNull
67+
SentryFeedbackOptions feedbackOptions = new SentryFeedbackOptions(options.getFeedbackOptions());
6768
if (configuration != null) {
68-
configuration.configure(context, resolvedFeedbackOptions);
69+
final @NotNull SentryFeedbackOptions configuredOptions =
70+
new SentryFeedbackOptions(feedbackOptions);
71+
try {
72+
configuration.configure(context, configuredOptions);
73+
feedbackOptions = configuredOptions;
74+
} catch (Throwable e) {
75+
ExceptionUtils.rethrowIfFatal(e);
76+
options
77+
.getLogger()
78+
.log(
79+
SentryLevel.ERROR, "Feedback OptionsConfiguration callback threw an exception.", e);
80+
}
6981
}
7082
if (configurator != null) {
71-
configurator.configure(resolvedFeedbackOptions);
83+
final @NotNull SentryFeedbackOptions configuredOptions =
84+
new SentryFeedbackOptions(feedbackOptions);
85+
try {
86+
configurator.configure(configuredOptions);
87+
feedbackOptions = configuredOptions;
88+
} catch (Throwable e) {
89+
ExceptionUtils.rethrowIfFatal(e);
90+
options
91+
.getLogger()
92+
.log(SentryLevel.ERROR, "Feedback OptionsConfigurator callback threw an exception.", e);
93+
}
7294
}
95+
this.resolvedFeedbackOptions = feedbackOptions;
7396
SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget");
7497
maybeStartShakeDetection(context);
7598
}

‎sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,75 @@ class SentryUserFeedbackFormTest {
164164
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
165165
}
166166

167+
@Test
168+
fun `when configuration throws, configurator still runs with original options`() {
169+
fixture.options.isEnabled = true
170+
val originalTitle = fixture.options.feedbackOptions.formTitle
171+
val customNameLabel = "custom name label"
172+
val failure = IllegalStateException("configuration failed")
173+
var titleReceivedByConfigurator: CharSequence? = null
174+
175+
val sut =
176+
fixture.getSut(
177+
configuration = { _, options ->
178+
options.formTitle = "partial title"
179+
throw failure
180+
},
181+
configurator = { options ->
182+
titleReceivedByConfigurator = options.formTitle
183+
options.nameLabel = customNameLabel
184+
},
185+
)
186+
sut.show()
187+
188+
assertThat(titleReceivedByConfigurator).isEqualTo(originalTitle)
189+
assertThat(sut.findViewById<TextView>(R.id.sentry_dialog_user_feedback_title).text)
190+
.isEqualTo(originalTitle)
191+
assertThat(sut.findViewById<TextView>(R.id.sentry_dialog_user_feedback_txt_name).text)
192+
.isEqualTo(customNameLabel)
193+
}
194+
195+
@Test
196+
fun `when configuration throws a fatal error, it is not swallowed`() {
197+
val failure = OutOfMemoryError("configuration failed")
198+
199+
val thrown =
200+
assertFailsWith<OutOfMemoryError> {
201+
fixture.getSut(configuration = { _, _ -> throw failure })
202+
}
203+
204+
assertThat(thrown).isSameInstanceAs(failure)
205+
}
206+
207+
@Test
208+
fun `when configurator throws, its partial changes are ignored`() {
209+
fixture.options.isEnabled = true
210+
val failure = IllegalStateException("configurator failed")
211+
212+
val sut =
213+
fixture.getSut(
214+
configuration = { _, options -> options.formTitle = "configured title" },
215+
configurator = { options ->
216+
options.formTitle = "partial title"
217+
throw failure
218+
},
219+
)
220+
sut.show()
221+
222+
assertThat(sut.findViewById<TextView>(R.id.sentry_dialog_user_feedback_title).text)
223+
.isEqualTo("configured title")
224+
}
225+
226+
@Test
227+
fun `when configurator throws a fatal error, it is not swallowed`() {
228+
val failure = OutOfMemoryError("configurator failed")
229+
230+
val thrown =
231+
assertFailsWith<OutOfMemoryError> { fixture.getSut(configurator = { throw failure }) }
232+
233+
assertThat(thrown).isSameInstanceAs(failure)
234+
}
235+
167236
@Test
168237
fun `dialog window does not have FLAG_ALT_FOCUSABLE_IM so soft keyboard can appear`() {
169238
fixture.options.isEnabled = true

0 commit comments

Comments
 (0)