Skip to content

Commit 70671a5

Browse files
authored
Add Compose user feedback button (#4559)
* Added user feedback dialog support via static API * Added dialog configurator without context * Added Compose button for feedback * Add support for associated event ID in user feedback dialog * Cleaned SentryUserFeedbackDialog.Builder methods * SentryFeedbackOptions is not Internal anymore
1 parent 485c1a6 commit 70671a5

File tree

22 files changed

+339
-27
lines changed

22 files changed

+339
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Add `SentryUserFeedbackButton` Composable ([#4559](https://github.com/getsentry/sentry-java/pull/4559))
8+
- Also added `Sentry.showUserFeedbackDialog` static method
79
- Add deadlineTimeout option ([#4555](https://github.com/getsentry/sentry-java/pull/4555))
810

911
### Fixes

sentry-android-core/api/sentry-android-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ public class io/sentry/android/core/SentryUserFeedbackDialog$Builder {
404404
public fun <init> (Landroid/content/Context;I)V
405405
public fun <init> (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
406406
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
407+
public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder;
408+
public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder;
407409
public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog;
408410
}
409411

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@ static void installDefaultIntegrations(
389389
options.addIntegration(replay);
390390
options.setReplayController(replay);
391391
}
392+
options
393+
.getFeedbackOptions()
394+
.setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler());
392395
}
393396

394397
/**

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
package io.sentry.android.core;
22

3+
import android.app.Activity;
34
import android.app.ActivityManager;
45
import android.app.ApplicationExitInfo;
56
import io.sentry.Hint;
67
import io.sentry.IScope;
78
import io.sentry.ISpan;
89
import io.sentry.Sentry;
910
import io.sentry.SentryEvent;
11+
import io.sentry.SentryFeedbackOptions;
12+
import io.sentry.SentryLevel;
1013
import io.sentry.SentryOptions;
1114
import io.sentry.SpanStatus;
1215
import io.sentry.android.core.internal.util.RootChecker;
1316
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
1417
import io.sentry.protocol.Mechanism;
1518
import io.sentry.protocol.SdkVersion;
19+
import io.sentry.protocol.SentryId;
1620
import org.jetbrains.annotations.ApiStatus;
1721
import org.jetbrains.annotations.NotNull;
1822
import org.jetbrains.annotations.Nullable;
@@ -609,4 +613,29 @@ public boolean isEnableAutoTraceIdGeneration() {
609613
public void setEnableAutoTraceIdGeneration(final boolean enableAutoTraceIdGeneration) {
610614
this.enableAutoTraceIdGeneration = enableAutoTraceIdGeneration;
611615
}
616+
617+
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
618+
@Override
619+
public void showDialog(
620+
final @Nullable SentryId associatedEventId,
621+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
622+
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
623+
if (activity == null) {
624+
Sentry.getCurrentScopes()
625+
.getOptions()
626+
.getLogger()
627+
.log(
628+
SentryLevel.ERROR,
629+
"Cannot show user feedback dialog, no activity is available. "
630+
+ "Make sure to call SentryAndroid.init() in your Application.onCreate() method.");
631+
return;
632+
}
633+
634+
new SentryUserFeedbackDialog.Builder(activity)
635+
.associatedEventId(associatedEventId)
636+
.configurator(configurator)
637+
.create()
638+
.show();
639+
}
640+
}
612641
}

sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,22 @@ public final class SentryUserFeedbackDialog extends AlertDialog {
2525

2626
private boolean isCancelable = false;
2727
private @Nullable SentryId currentReplayId;
28+
private final @Nullable SentryId associatedEventId;
2829
private @Nullable OnDismissListener delegate;
2930

3031
private final @Nullable OptionsConfiguration configuration;
32+
private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
3133

3234
SentryUserFeedbackDialog(
3335
final @NotNull Context context,
3436
final int themeResId,
35-
final @Nullable OptionsConfiguration configuration) {
37+
final @Nullable SentryId associatedEventId,
38+
final @Nullable OptionsConfiguration configuration,
39+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
3640
super(context, themeResId);
41+
this.associatedEventId = associatedEventId;
3742
this.configuration = configuration;
43+
this.configurator = configurator;
3844
SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget");
3945
}
4046

@@ -56,6 +62,9 @@ protected void onCreate(Bundle savedInstanceState) {
5662
if (configuration != null) {
5763
configuration.configure(getContext(), feedbackOptions);
5864
}
65+
if (configurator != null) {
66+
configurator.configure(feedbackOptions);
67+
}
5968
final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title);
6069
final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo);
6170
final @NotNull TextView lblName = findViewById(R.id.sentry_dialog_user_feedback_txt_name);
@@ -145,6 +154,9 @@ protected void onCreate(Bundle savedInstanceState) {
145154
final @NotNull Feedback feedback = new Feedback(message);
146155
feedback.setName(name);
147156
feedback.setContactEmail(email);
157+
if (associatedEventId != null) {
158+
feedback.setAssociatedEventId(associatedEventId);
159+
}
148160
if (currentReplayId != null) {
149161
feedback.setReplayId(currentReplayId);
150162
}
@@ -226,6 +238,8 @@ public void show() {
226238
public static class Builder {
227239

228240
@Nullable OptionsConfiguration configuration;
241+
@Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
242+
@Nullable SentryId associatedEventId;
229243
final @NotNull Context context;
230244
final int themeResId;
231245

@@ -317,14 +331,38 @@ public Builder(
317331
this.configuration = configuration;
318332
}
319333

334+
/**
335+
* Sets the configuration for the feedback options.
336+
*
337+
* @param configurator the configuration for the feedback options, can be {@code null} to use
338+
* the global feedback options.
339+
*/
340+
public Builder configurator(
341+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
342+
this.configurator = configurator;
343+
return this;
344+
}
345+
346+
/**
347+
* Sets the associated event ID for the feedback.
348+
*
349+
* @param associatedEventId the associated event ID for the feedback, can be {@code null} to
350+
* avoid associating the feedback to an event.
351+
*/
352+
public Builder associatedEventId(final @Nullable SentryId associatedEventId) {
353+
this.associatedEventId = associatedEventId;
354+
return this;
355+
}
356+
320357
/**
321358
* Builds a new {@link SentryUserFeedbackDialog} with the specified context, theme, and
322359
* configuration.
323360
*
324361
* @return a new instance of {@link SentryUserFeedbackDialog}
325362
*/
326363
public SentryUserFeedbackDialog create() {
327-
return new SentryUserFeedbackDialog(context, themeResId, configuration);
364+
return new SentryUserFeedbackDialog(
365+
context, themeResId, associatedEventId, configuration, configurator);
328366
}
329367
}
330368

sentry-android-core/src/main/res/drawable/sentry_user_feedback_button_logo_24.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- Taken from https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-main:android/resources/images/material/icons/materialicons/campaign/baseline_campaign_24.xml -->
12
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?android:attr/colorForeground" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
23

34
<path android:fillColor="?android:attr/colorForeground" android:pathData="M18,11v2h4v-2h-4zM16,17.61c0.96,0.71 2.21,1.65 3.2,2.39 0.4,-0.53 0.8,-1.07 1.2,-1.6 -0.99,-0.74 -2.24,-1.68 -3.2,-2.4 -0.4,0.54 -0.8,1.08 -1.2,1.61zM20.4,5.6c-0.4,-0.53 -0.8,-1.07 -1.2,-1.6 -0.99,0.74 -2.24,1.68 -3.2,2.4 0.4,0.53 0.8,1.07 1.2,1.6 0.96,-0.72 2.21,-1.65 3.2,-2.4zM4,9c-1.1,0 -2,0.9 -2,2v2c0,1.1 0.9,2 2,2h1v4h2v-4h1l5,3L13,6L8,9L4,9zM15.5,12c0,-1.33 -0.58,-2.53 -1.5,-3.35v6.69c0.92,-0.81 1.5,-2.01 1.5,-3.34z"/>

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.sentry.MainEventProcessor
1717
import io.sentry.NoOpContinuousProfiler
1818
import io.sentry.NoOpTransactionProfiler
1919
import io.sentry.SentryOptions
20+
import io.sentry.android.core.SentryAndroidOptions.AndroidUserFeedbackIDialogHandler
2021
import io.sentry.android.core.cache.AndroidEnvelopeCache
2122
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader
2223
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator
@@ -836,6 +837,12 @@ class AndroidOptionsInitializerTest {
836837
assertNull(anrv1Integration)
837838
}
838839

840+
@Test
841+
fun `AndroidUserFeedbackIDialogHandler is set as feedback dialog handler`() {
842+
fixture.initSut()
843+
assertIs<AndroidUserFeedbackIDialogHandler>(fixture.sentryOptions.feedbackOptions.dialogHandler)
844+
}
845+
839846
@Test
840847
fun `PersistingScopeObserver is no-op, if scope persistence is disabled`() {
841848
fixture.initSut(configureOptions = { isEnableScopePersistence = false })

sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import io.sentry.IScope
99
import io.sentry.IScopes
1010
import io.sentry.ReplayController
1111
import io.sentry.Sentry
12+
import io.sentry.SentryFeedbackOptions
1213
import io.sentry.SentryLevel
14+
import io.sentry.protocol.SentryId
1315
import kotlin.test.AfterTest
1416
import kotlin.test.BeforeTest
1517
import kotlin.test.Test
@@ -52,8 +54,11 @@ class SentryUserFeedbackDialogTest {
5254
}
5355

5456
fun getSut(
55-
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null
56-
): SentryUserFeedbackDialog = SentryUserFeedbackDialog(application, 0, configuration)
57+
associatedEventId: SentryId? = null,
58+
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null,
59+
configurator: SentryFeedbackOptions.OptionsConfigurator? = null,
60+
): SentryUserFeedbackDialog =
61+
SentryUserFeedbackDialog(application, 0, associatedEventId, configuration, configurator)
5762
}
5863

5964
private val fixture = Fixture()
@@ -98,7 +103,23 @@ class SentryUserFeedbackDialogTest {
98103
@Test
99104
fun `when configuration is passed, it is applied to the current dialog only`() {
100105
fixture.options.isEnabled = true
101-
val sut = fixture.getSut { context, options -> options.formTitle = "custom title" }
106+
val sut =
107+
fixture.getSut(configuration = { context, options -> options.formTitle = "custom title" })
108+
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
109+
sut.show()
110+
// After showing the dialog, the title should be set
111+
assertEquals(
112+
"custom title",
113+
sut.findViewById<TextView>(R.id.sentry_dialog_user_feedback_title).text,
114+
)
115+
// And the original options should not be modified
116+
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
117+
}
118+
119+
@Test
120+
fun `when configurator is passed, it is applied to the current dialog only`() {
121+
fixture.options.isEnabled = true
122+
val sut = fixture.getSut(configurator = { options -> options.formTitle = "custom title" })
102123
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
103124
sut.show()
104125
// After showing the dialog, the title should be set

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import io.sentry.android.core.R
2929
import io.sentry.android.core.SentryUserFeedbackButton
3030
import io.sentry.android.core.SentryUserFeedbackDialog
3131
import io.sentry.assertEnvelopeFeedback
32+
import io.sentry.protocol.SentryId
3233
import io.sentry.protocol.User
3334
import io.sentry.test.getProperty
3435
import kotlin.test.Test
@@ -49,7 +50,23 @@ class UserFeedbackUiTest : BaseUiTest() {
4950
launchActivity<EmptyActivity>().onActivity {
5051
SentryUserFeedbackDialog.Builder(it).create().show()
5152
}
52-
onView(withId(R.id.sentry_dialog_user_feedback_title)).check(doesNotExist())
53+
onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist())
54+
}
55+
56+
@Test
57+
fun userFeedbackNotShownWhenSdkDisabledViaApi() {
58+
launchActivity<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }
59+
onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist())
60+
}
61+
62+
@Test
63+
fun userFeedbackShownViaApi() {
64+
initSentry()
65+
launchActivity<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }
66+
67+
onView(withId(R.id.sentry_dialog_user_feedback_layout))
68+
.inRoot(isDialog())
69+
.check(matches(isDisplayed()))
5370
}
5471

5572
@Test
@@ -461,7 +478,9 @@ class UserFeedbackUiTest : BaseUiTest() {
461478
}
462479
}
463480

464-
showDialogAndCheck {
481+
val sentryId = SentryId()
482+
483+
showDialogAndCheck(sentryId) {
465484
// Send the feedback
466485
fillFormAndSend()
467486
}
@@ -481,6 +500,7 @@ class UserFeedbackUiTest : BaseUiTest() {
481500
assertEquals("Description filled", feedback.message)
482501
// The screen name should be set in the url
483502
assertEquals("io.sentry.uitest.android.EmptyActivity", feedback.url)
503+
assertEquals(sentryId, feedback.associatedEventId)
484504

485505
if (enableReplay) {
486506
// The current replay should be set in the replayId
@@ -613,11 +633,14 @@ class UserFeedbackUiTest : BaseUiTest() {
613633
onView(withId(R.id.sentry_dialog_user_feedback_btn_send)).perform(click())
614634
}
615635

616-
private fun showDialogAndCheck(checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}) {
636+
private fun showDialogAndCheck(
637+
associatedEventId: SentryId? = null,
638+
checker: (dialog: SentryUserFeedbackDialog) -> Unit = {},
639+
) {
617640
lateinit var dialog: SentryUserFeedbackDialog
618641
val feedbackScenario = launchActivity<EmptyActivity>()
619642
feedbackScenario.onActivity {
620-
dialog = SentryUserFeedbackDialog.Builder(it).create()
643+
dialog = SentryUserFeedbackDialog.Builder(it).associatedEventId(associatedEventId).create()
621644
dialog.show()
622645
}
623646

sentry-compose/api/android/sentry-compose.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public final class io/sentry/compose/SentryNavigationIntegrationKt {
2626
public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController;
2727
}
2828

29+
public final class io/sentry/compose/SentryUserFeedbackButtonKt {
30+
public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Landroidx/compose/runtime/Composer;II)V
31+
}
32+
2933
public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator {
3034
public static final field $stable I
3135
public static final field Companion Lio/sentry/compose/gestures/ComposeGestureTargetLocator$Companion;

0 commit comments

Comments
 (0)