Skip to content

Commit a354f4b

Browse files
committed
Added user feedback dialog support via static API
Added dialog configurator without context added Compose button for feedback
1 parent 2bfacef commit a354f4b

File tree

20 files changed

+289
-27
lines changed

20 files changed

+289
-27
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app
402402
public class io/sentry/android/core/SentryUserFeedbackDialog$Builder {
403403
public fun <init> (Landroid/content/Context;)V
404404
public fun <init> (Landroid/content/Context;I)V
405-
public fun <init> (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
405+
public fun <init> (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V
406+
public fun <init> (Landroid/content/Context;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V
406407
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
407408
public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog;
408409
}

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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
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;
@@ -609,4 +612,23 @@ public boolean isEnableAutoTraceIdGeneration() {
609612
public void setEnableAutoTraceIdGeneration(final boolean enableAutoTraceIdGeneration) {
610613
this.enableAutoTraceIdGeneration = enableAutoTraceIdGeneration;
611614
}
615+
616+
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
617+
@Override
618+
public void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
619+
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
620+
if (activity == null) {
621+
Sentry.getCurrentScopes()
622+
.getOptions()
623+
.getLogger()
624+
.log(
625+
SentryLevel.ERROR,
626+
"Cannot show user feedback dialog, no activity is available. "
627+
+ "Make sure to call SentryAndroid.init() in your Application.onCreate() method.");
628+
return;
629+
}
630+
631+
new SentryUserFeedbackDialog.Builder(activity, configurator).create().show();
632+
}
633+
}
612634
}

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ public final class SentryUserFeedbackDialog extends AlertDialog {
2828
private @Nullable OnDismissListener delegate;
2929

3030
private final @Nullable OptionsConfiguration configuration;
31+
private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
3132

3233
SentryUserFeedbackDialog(
3334
final @NotNull Context context,
3435
final int themeResId,
35-
final @Nullable OptionsConfiguration configuration) {
36+
final @Nullable OptionsConfiguration configuration,
37+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
3638
super(context, themeResId);
3739
this.configuration = configuration;
40+
this.configurator = configurator;
3841
SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget");
3942
}
4043

@@ -56,6 +59,9 @@ protected void onCreate(Bundle savedInstanceState) {
5659
if (configuration != null) {
5760
configuration.configure(getContext(), feedbackOptions);
5861
}
62+
if (configurator != null) {
63+
configurator.configure(feedbackOptions);
64+
}
5965
final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title);
6066
final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo);
6167
final @NotNull TextView lblName = findViewById(R.id.sentry_dialog_user_feedback_txt_name);
@@ -226,6 +232,7 @@ public void show() {
226232
public static class Builder {
227233

228234
@Nullable OptionsConfiguration configuration;
235+
@Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
229236
final @NotNull Context context;
230237
final int themeResId;
231238

@@ -264,7 +271,7 @@ public Builder(final @NotNull Context context) {
264271
* {@code 0} to use the parent {@code context}'s default alert dialog theme
265272
*/
266273
public Builder(Context context, int themeResId) {
267-
this(context, themeResId, null);
274+
this(context, themeResId, null, null);
268275
}
269276

270277
/**
@@ -281,7 +288,25 @@ public Builder(Context context, int themeResId) {
281288
*/
282289
public Builder(
283290
final @NotNull Context context, final @Nullable OptionsConfiguration configuration) {
284-
this(context, 0, configuration);
291+
this(context, 0, configuration, null);
292+
}
293+
294+
/**
295+
* Creates a builder for a {@link SentryUserFeedbackDialog} that uses the default alert dialog
296+
* theme. The {@code configuration} can be used to configure the feedback options for this
297+
* specific dialog.
298+
*
299+
* <p>The default alert dialog theme is defined by {@link android.R.attr#alertDialogTheme}
300+
* within the parent {@code context}'s theme.
301+
*
302+
* @param context the parent context
303+
* @param configurator the configuration for the feedback options, can be {@code null} to use
304+
* the global feedback options.
305+
*/
306+
public Builder(
307+
final @NotNull Context context,
308+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
309+
this(context, 0, null, configurator);
285310
}
286311

287312
/**
@@ -311,10 +336,12 @@ public Builder(
311336
public Builder(
312337
final @NotNull Context context,
313338
final int themeResId,
314-
final @Nullable OptionsConfiguration configuration) {
339+
final @Nullable OptionsConfiguration configuration,
340+
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
315341
this.context = context;
316342
this.themeResId = themeResId;
317343
this.configuration = configuration;
344+
this.configurator = configurator;
318345
}
319346

320347
/**
@@ -324,7 +351,7 @@ public Builder(
324351
* @return a new instance of {@link SentryUserFeedbackDialog}
325352
*/
326353
public SentryUserFeedbackDialog create() {
327-
return new SentryUserFeedbackDialog(context, themeResId, configuration);
354+
return new SentryUserFeedbackDialog(context, themeResId, configuration, configurator);
328355
}
329356
}
330357

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: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ 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
1314
import kotlin.test.AfterTest
1415
import kotlin.test.BeforeTest
@@ -52,8 +53,10 @@ class SentryUserFeedbackDialogTest {
5253
}
5354

5455
fun getSut(
55-
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null
56-
): SentryUserFeedbackDialog = SentryUserFeedbackDialog(application, 0, configuration)
56+
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null,
57+
configurator: SentryFeedbackOptions.OptionsConfigurator? = null,
58+
): SentryUserFeedbackDialog =
59+
SentryUserFeedbackDialog(application, 0, configuration, configurator)
5760
}
5861

5962
private val fixture = Fixture()
@@ -98,7 +101,23 @@ class SentryUserFeedbackDialogTest {
98101
@Test
99102
fun `when configuration is passed, it is applied to the current dialog only`() {
100103
fixture.options.isEnabled = true
101-
val sut = fixture.getSut { context, options -> options.formTitle = "custom title" }
104+
val sut =
105+
fixture.getSut(configuration = { context, options -> options.formTitle = "custom title" })
106+
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
107+
sut.show()
108+
// After showing the dialog, the title should be set
109+
assertEquals(
110+
"custom title",
111+
sut.findViewById<TextView>(R.id.sentry_dialog_user_feedback_title).text,
112+
)
113+
// And the original options should not be modified
114+
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
115+
}
116+
117+
@Test
118+
fun `when configurator is passed, it is applied to the current dialog only`() {
119+
fixture.options.isEnabled = true
120+
val sut = fixture.getSut(configurator = { options -> options.formTitle = "custom title" })
102121
assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle)
103122
sut.show()
104123
// 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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,23 @@ class UserFeedbackUiTest : BaseUiTest() {
4949
launchActivity<EmptyActivity>().onActivity {
5050
SentryUserFeedbackDialog.Builder(it).create().show()
5151
}
52-
onView(withId(R.id.sentry_dialog_user_feedback_title)).check(doesNotExist())
52+
onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist())
53+
}
54+
55+
@Test
56+
fun userFeedbackNotShownWhenSdkDisabledViaApi() {
57+
launchActivity<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }
58+
onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist())
59+
}
60+
61+
@Test
62+
fun userFeedbackShownViaApi() {
63+
initSentry()
64+
launchActivity<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }
65+
66+
onView(withId(R.id.sentry_dialog_user_feedback_layout))
67+
.inRoot(isDialog())
68+
.check(matches(isDisplayed()))
5369
}
5470

5571
@Test

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ public final class io/sentry/compose/BuildConfig {
66
public fun <init> ()V
77
}
88

9+
public final class io/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt {
10+
public static final field INSTANCE Lio/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt;
11+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
12+
public fun <init> ()V
13+
public final fun getLambda-1$sentry_compose_release ()Lkotlin/jvm/functions/Function3;
14+
}
15+
916
public final class io/sentry/compose/SentryComposeHelperKt {
1017
public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect;
1118
}
@@ -26,6 +33,10 @@ public final class io/sentry/compose/SentryNavigationIntegrationKt {
2633
public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController;
2734
}
2835

36+
public final class io/sentry/compose/SentryUserFeedbackButtonKt {
37+
public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Landroidx/compose/runtime/Composer;II)V
38+
}
39+
2940
public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator {
3041
public static final field $stable I
3142
public static final field Companion Lio/sentry/compose/gestures/ComposeGestureTargetLocator$Companion;

sentry-compose/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ kotlin {
4343
dependencies {
4444
api(projects.sentry)
4545
api(projects.sentryAndroidNavigation)
46+
implementation(libs.androidx.compose.material3)
4647

4748
compileOnly(libs.androidx.navigation.compose)
4849
implementation(libs.androidx.lifecycle.common.java8)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.sentry.compose
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.Button
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.material3.Text
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Alignment
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.res.painterResource
14+
import androidx.compose.ui.unit.dp
15+
import io.sentry.Sentry
16+
import io.sentry.SentryFeedbackOptions
17+
18+
@Composable
19+
public fun SentryUserFeedbackButton(
20+
modifier: Modifier = Modifier,
21+
configurator: SentryFeedbackOptions.OptionsConfigurator? = null,
22+
) {
23+
Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) {
24+
Row(
25+
verticalAlignment = Alignment.CenterVertically,
26+
horizontalArrangement = Arrangement.Center,
27+
) {
28+
Icon(
29+
painter = painterResource(id = R.drawable.sentry_user_feedback_compose_button_logo_24),
30+
contentDescription = "Vector Icon",
31+
)
32+
Spacer(Modifier.padding(horizontal = 4.dp))
33+
Text(text = "Report a Bug")
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)