From a354f4b7596cd6e98cdfe5fa94dfa8d7c91fdd3f Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Jul 2025 14:04:48 +0200 Subject: [PATCH 1/9] Added user feedback dialog support via static API Added dialog configurator without context added Compose button for feedback --- .../api/sentry-android-core.api | 3 +- .../core/AndroidOptionsInitializer.java | 3 ++ .../android/core/SentryAndroidOptions.java | 22 +++++++++ .../core/SentryUserFeedbackDialog.java | 37 +++++++++++++-- .../core/AndroidOptionsInitializerTest.kt | 7 +++ .../core/SentryUserFeedbackDialogTest.kt | 25 ++++++++-- .../uitest/android/UserFeedbackUiTest.kt | 18 ++++++- sentry-compose/api/android/sentry-compose.api | 11 +++++ sentry-compose/build.gradle.kts | 1 + .../compose/SentryUserFeedbackButton.kt | 36 ++++++++++++++ ...y_user_feedback_compose_button_logo_24.xml | 5 ++ .../android/compose/ComposeActivity.kt | 7 +++ .../src/main/kotlin/io/sentry/test/Init.kt | 1 + sentry/api/sentry.api | 14 +++++- sentry/src/main/java/io/sentry/Sentry.java | 10 ++++ .../java/io/sentry/SentryFeedbackOptions.java | 43 ++++++++++++++++- .../main/java/io/sentry/SentryOptions.java | 6 ++- .../io/sentry/SentryFeedbackOptionsTest.kt | 6 ++- .../test/java/io/sentry/SentryOptionsTest.kt | 14 ++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 47 ++++++++++++++----- 20 files changed, 289 insertions(+), 27 deletions(-) create mode 100644 sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt create mode 100644 sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index ea67ed5bcb7..8e4b43d82e8 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -402,7 +402,8 @@ public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app public class io/sentry/android/core/SentryUserFeedbackDialog$Builder { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;I)V - public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V + public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public fun (Landroid/content/Context;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V public fun (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 302f4627f20..8a564081ea0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -389,6 +389,9 @@ static void installDefaultIntegrations( options.addIntegration(replay); options.setReplayController(replay); } + options + .getFeedbackOptions() + .setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler()); } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 0ac69de3ab9..6adb78e20e3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import android.app.Activity; import android.app.ActivityManager; import android.app.ApplicationExitInfo; import io.sentry.Hint; @@ -7,6 +8,8 @@ import io.sentry.ISpan; import io.sentry.Sentry; import io.sentry.SentryEvent; +import io.sentry.SentryFeedbackOptions; +import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanStatus; import io.sentry.android.core.internal.util.RootChecker; @@ -609,4 +612,23 @@ public boolean isEnableAutoTraceIdGeneration() { public void setEnableAutoTraceIdGeneration(final boolean enableAutoTraceIdGeneration) { this.enableAutoTraceIdGeneration = enableAutoTraceIdGeneration; } + + static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler { + @Override + public void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity(); + if (activity == null) { + Sentry.getCurrentScopes() + .getOptions() + .getLogger() + .log( + SentryLevel.ERROR, + "Cannot show user feedback dialog, no activity is available. " + + "Make sure to call SentryAndroid.init() in your Application.onCreate() method."); + return; + } + + new SentryUserFeedbackDialog.Builder(activity, configurator).create().show(); + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java index 39fff9c7168..dffb4a237ad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java @@ -28,13 +28,16 @@ public final class SentryUserFeedbackDialog extends AlertDialog { private @Nullable OnDismissListener delegate; private final @Nullable OptionsConfiguration configuration; + private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; SentryUserFeedbackDialog( final @NotNull Context context, final int themeResId, - final @Nullable OptionsConfiguration configuration) { + final @Nullable OptionsConfiguration configuration, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { super(context, themeResId); this.configuration = configuration; + this.configurator = configurator; SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget"); } @@ -56,6 +59,9 @@ protected void onCreate(Bundle savedInstanceState) { if (configuration != null) { configuration.configure(getContext(), feedbackOptions); } + if (configurator != null) { + configurator.configure(feedbackOptions); + } final @NotNull TextView lblTitle = findViewById(R.id.sentry_dialog_user_feedback_title); final @NotNull ImageView imgLogo = findViewById(R.id.sentry_dialog_user_feedback_logo); final @NotNull TextView lblName = findViewById(R.id.sentry_dialog_user_feedback_txt_name); @@ -226,6 +232,7 @@ public void show() { public static class Builder { @Nullable OptionsConfiguration configuration; + @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; final @NotNull Context context; final int themeResId; @@ -264,7 +271,7 @@ public Builder(final @NotNull Context context) { * {@code 0} to use the parent {@code context}'s default alert dialog theme */ public Builder(Context context, int themeResId) { - this(context, themeResId, null); + this(context, themeResId, null, null); } /** @@ -281,7 +288,25 @@ public Builder(Context context, int themeResId) { */ public Builder( final @NotNull Context context, final @Nullable OptionsConfiguration configuration) { - this(context, 0, configuration); + this(context, 0, configuration, null); + } + + /** + * Creates a builder for a {@link SentryUserFeedbackDialog} that uses the default alert dialog + * theme. The {@code configuration} can be used to configure the feedback options for this + * specific dialog. + * + *

The default alert dialog theme is defined by {@link android.R.attr#alertDialogTheme} + * within the parent {@code context}'s theme. + * + * @param context the parent context + * @param configurator the configuration for the feedback options, can be {@code null} to use + * the global feedback options. + */ + public Builder( + final @NotNull Context context, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + this(context, 0, null, configurator); } /** @@ -311,10 +336,12 @@ public Builder( public Builder( final @NotNull Context context, final int themeResId, - final @Nullable OptionsConfiguration configuration) { + final @Nullable OptionsConfiguration configuration, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { this.context = context; this.themeResId = themeResId; this.configuration = configuration; + this.configurator = configurator; } /** @@ -324,7 +351,7 @@ public Builder( * @return a new instance of {@link SentryUserFeedbackDialog} */ public SentryUserFeedbackDialog create() { - return new SentryUserFeedbackDialog(context, themeResId, configuration); + return new SentryUserFeedbackDialog(context, themeResId, configuration, configurator); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index a3248d893af..cd1a7cc26de 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -17,6 +17,7 @@ import io.sentry.MainEventProcessor import io.sentry.NoOpContinuousProfiler import io.sentry.NoOpTransactionProfiler import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroidOptions.AndroidUserFeedbackIDialogHandler import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator @@ -836,6 +837,12 @@ class AndroidOptionsInitializerTest { assertNull(anrv1Integration) } + @Test + fun `AndroidUserFeedbackIDialogHandler is set as feedback dialog handler`() { + fixture.initSut() + assertIs(fixture.sentryOptions.feedbackOptions.dialogHandler) + } + @Test fun `PersistingScopeObserver is no-op, if scope persistence is disabled`() { fixture.initSut(configureOptions = { isEnableScopePersistence = false }) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt index 265693eede1..12413ed7edf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt @@ -9,6 +9,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.ReplayController import io.sentry.Sentry +import io.sentry.SentryFeedbackOptions import io.sentry.SentryLevel import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -52,8 +53,10 @@ class SentryUserFeedbackDialogTest { } fun getSut( - configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null - ): SentryUserFeedbackDialog = SentryUserFeedbackDialog(application, 0, configuration) + configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null, + configurator: SentryFeedbackOptions.OptionsConfigurator? = null, + ): SentryUserFeedbackDialog = + SentryUserFeedbackDialog(application, 0, configuration, configurator) } private val fixture = Fixture() @@ -98,7 +101,23 @@ class SentryUserFeedbackDialogTest { @Test fun `when configuration is passed, it is applied to the current dialog only`() { fixture.options.isEnabled = true - val sut = fixture.getSut { context, options -> options.formTitle = "custom title" } + val sut = + fixture.getSut(configuration = { context, options -> options.formTitle = "custom title" }) + assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle) + sut.show() + // After showing the dialog, the title should be set + assertEquals( + "custom title", + sut.findViewById(R.id.sentry_dialog_user_feedback_title).text, + ) + // And the original options should not be modified + assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle) + } + + @Test + fun `when configurator is passed, it is applied to the current dialog only`() { + fixture.options.isEnabled = true + val sut = fixture.getSut(configurator = { options -> options.formTitle = "custom title" }) assertNotEquals("custom title", fixture.options.feedbackOptions.formTitle) sut.show() // After showing the dialog, the title should be set diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt index 823376b5b8d..24244c24e0b 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt @@ -49,7 +49,23 @@ class UserFeedbackUiTest : BaseUiTest() { launchActivity().onActivity { SentryUserFeedbackDialog.Builder(it).create().show() } - onView(withId(R.id.sentry_dialog_user_feedback_title)).check(doesNotExist()) + onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist()) + } + + @Test + fun userFeedbackNotShownWhenSdkDisabledViaApi() { + launchActivity().onActivity { Sentry.showUserFeedbackDialog() } + onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist()) + } + + @Test + fun userFeedbackShownViaApi() { + initSentry() + launchActivity().onActivity { Sentry.showUserFeedbackDialog() } + + onView(withId(R.id.sentry_dialog_user_feedback_layout)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) } @Test diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index 59d4828ec7f..88580cd7ea8 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -6,6 +6,13 @@ public final class io/sentry/compose/BuildConfig { public fun ()V } +public final class io/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt { + public static final field INSTANCE Lio/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$sentry_compose_release ()Lkotlin/jvm/functions/Function3; +} + public final class io/sentry/compose/SentryComposeHelperKt { public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect; } @@ -26,6 +33,10 @@ public final class io/sentry/compose/SentryNavigationIntegrationKt { public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController; } +public final class io/sentry/compose/SentryUserFeedbackButtonKt { + public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Landroidx/compose/runtime/Composer;II)V +} + public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator { public static final field $stable I public static final field Companion Lio/sentry/compose/gestures/ComposeGestureTargetLocator$Companion; diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 0aa0f3d851c..9624bd018b9 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -43,6 +43,7 @@ kotlin { dependencies { api(projects.sentry) api(projects.sentryAndroidNavigation) + implementation(libs.androidx.compose.material3) compileOnly(libs.androidx.navigation.compose) implementation(libs.androidx.lifecycle.common.java8) diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt new file mode 100644 index 00000000000..7ca31c9b281 --- /dev/null +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt @@ -0,0 +1,36 @@ +package io.sentry.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import io.sentry.Sentry +import io.sentry.SentryFeedbackOptions + +@Composable +public fun SentryUserFeedbackButton( + modifier: Modifier = Modifier, + configurator: SentryFeedbackOptions.OptionsConfigurator? = null, +) { + Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.sentry_user_feedback_compose_button_logo_24), + contentDescription = "Vector Icon", + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text(text = "Report a Bug") + } + } +} diff --git a/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml b/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml new file mode 100644 index 00000000000..31efc71058c --- /dev/null +++ b/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt index 9fd433a1c75..b27da13c770 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt @@ -51,6 +51,7 @@ import androidx.navigation.navArgument import coil.compose.AsyncImage import io.sentry.android.replay.sentryReplayUnmask import io.sentry.compose.SentryTraced +import io.sentry.compose.SentryUserFeedbackButton import io.sentry.compose.withSentryObservableEffect import io.sentry.samples.android.GithubAPI import io.sentry.samples.android.R as IR @@ -108,6 +109,12 @@ fun Landing(navigateGithub: () -> Unit, navigateGithubWithArgs: () -> Unit) { Text("Show Dialog", modifier = Modifier.sentryReplayUnmask()) } } + SentryTraced(tag = "button_dialog") { + SentryUserFeedbackButton(modifier = Modifier.padding(top = 32.dp)) { options -> + options.formTitle = "Report a Bug???" + options.messageLabel = "Please provide details about the bug you encountered." + } + } if (showDialog) { BasicAlertDialog( onDismissRequest = { diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Init.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Init.kt index ebf7fb0ee33..5061714d5ed 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Init.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Init.kt @@ -39,4 +39,5 @@ fun initForTest() { fun applyTestOptions(options: SentryOptions) { options.shutdownTimeoutMillis = 0 + options.sessionFlushTimeoutMillis = 0 } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 171cd7eaa88..08939c84c03 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2587,6 +2587,8 @@ public final class io/sentry/Sentry { public static fun setTag (Ljava/lang/String;Ljava/lang/String;)V public static fun setTransaction (Ljava/lang/String;)V public static fun setUser (Lio/sentry/protocol/User;)V + public static fun showUserFeedbackDialog ()V + public static fun showUserFeedbackDialog (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V public static fun startProfiler ()V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; @@ -2961,9 +2963,10 @@ public final class io/sentry/SentryExecutorService : io/sentry/ISentryExecutorSe } public final class io/sentry/SentryFeedbackOptions { - public fun ()V + public fun (Lio/sentry/SentryFeedbackOptions$IDialogHandler;)V public fun (Lio/sentry/SentryFeedbackOptions;)V public fun getCancelButtonLabel ()Ljava/lang/CharSequence; + public fun getDialogHandler ()Lio/sentry/SentryFeedbackOptions$IDialogHandler; public fun getEmailLabel ()Ljava/lang/CharSequence; public fun getEmailPlaceholder ()Ljava/lang/CharSequence; public fun getFormTitle ()Ljava/lang/CharSequence; @@ -2985,6 +2988,7 @@ public final class io/sentry/SentryFeedbackOptions { public fun isShowName ()Z public fun isUseSentryUser ()Z public fun setCancelButtonLabel (Ljava/lang/CharSequence;)V + public fun setDialogHandler (Lio/sentry/SentryFeedbackOptions$IDialogHandler;)V public fun setEmailLabel (Ljava/lang/CharSequence;)V public fun setEmailPlaceholder (Ljava/lang/CharSequence;)V public fun setEmailRequired (Z)V @@ -3008,6 +3012,14 @@ public final class io/sentry/SentryFeedbackOptions { public fun toString ()Ljava/lang/String; } +public abstract interface class io/sentry/SentryFeedbackOptions$IDialogHandler { + public abstract fun showDialog (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V +} + +public abstract interface class io/sentry/SentryFeedbackOptions$OptionsConfigurator { + public abstract fun configure (Lio/sentry/SentryFeedbackOptions;)V +} + public abstract interface class io/sentry/SentryFeedbackOptions$SentryFeedbackCallback { public abstract fun call (Lio/sentry/protocol/Feedback;)V } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 0596f8f2ffb..0214514f99c 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1289,4 +1289,14 @@ public static ILoggerApi logger() { public static IReplayApi replay() { return getCurrentScopes().getScope().getOptions().getReplayController(); } + + public static void showUserFeedbackDialog() { + showUserFeedbackDialog(null); + } + + public static void showUserFeedbackDialog( + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + final @NotNull SentryOptions options = getCurrentScopes().getOptions(); + options.getFeedbackOptions().getDialogHandler().showDialog(configurator); + } } diff --git a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java index 0d821b139ba..79ba24dd3ac 100644 --- a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java +++ b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java @@ -83,12 +83,16 @@ public final class SentryFeedbackOptions { private @Nullable Runnable onFormClose; /** Callback called when feedback is successfully submitted via the prepared form. */ - private @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitSuccess; + private @Nullable SentryFeedbackCallback onSubmitSuccess; /** Callback called when there is an error submitting feedback via the prepared form. */ private @Nullable SentryFeedbackCallback onSubmitError; - public SentryFeedbackOptions() {} + private @NotNull IDialogHandler iDialogHandler; + + public SentryFeedbackOptions(@NotNull IDialogHandler iDialogHandler) { + this.iDialogHandler = iDialogHandler; + } /** Creates a copy of the passed {@link SentryFeedbackOptions}. */ public SentryFeedbackOptions(final @NotNull SentryFeedbackOptions other) { @@ -113,6 +117,7 @@ public SentryFeedbackOptions(final @NotNull SentryFeedbackOptions other) { this.onFormClose = other.onFormClose; this.onSubmitSuccess = other.onSubmitSuccess; this.onSubmitError = other.onSubmitError; + this.iDialogHandler = other.iDialogHandler; } /** @@ -507,6 +512,24 @@ public void setOnSubmitError(final @Nullable SentryFeedbackCallback onSubmitErro this.onSubmitError = onSubmitError; } + /** + * Sets the dialog handler to be used to show the feedback form. + * + * @param iDialogHandler the dialog handler to be used to show the feedback form + */ + public void setDialogHandler(final @NotNull IDialogHandler iDialogHandler) { + this.iDialogHandler = iDialogHandler; + } + + /** + * Gets the dialog handler to be used to show the feedback form. + * + * @return the dialog handler to be used to show the feedback form + */ + public @NotNull IDialogHandler getDialogHandler() { + return iDialogHandler; + } + @Override public String toString() { return "SentryFeedbackOptions{" @@ -558,4 +581,20 @@ public String toString() { public interface SentryFeedbackCallback { void call(final @NotNull Feedback feedback); } + + @ApiStatus.Internal + public interface IDialogHandler { + void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); + } + + /** Configuration callback for feedback options. */ + public interface OptionsConfigurator { + + /** + * configure the feedback options + * + * @param options the feedback options + */ + void configure(final @NotNull SentryFeedbackOptions options); + } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index cb306f0aaf3..79b95151e41 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3035,7 +3035,11 @@ private SentryOptions(final boolean empty) { final @NotNull SdkVersion sdkVersion = createSdkVersion(); experimental = new ExperimentalOptions(empty, sdkVersion); sessionReplay = new SentryReplayOptions(empty, sdkVersion); - feedbackOptions = new SentryFeedbackOptions(); + feedbackOptions = + new SentryFeedbackOptions( + configurator -> + logger.log(SentryLevel.WARNING, "showDialog() can only be called in Android.")); + if (!empty) { setSpanFactory(SpanFactoryFactory.create(new LoadClass(), NoOpLogger.getInstance())); // SentryExecutorService should be initialized before any diff --git a/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt index 410be6aa73d..a50aff02dc5 100644 --- a/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.SentryFeedbackOptions.IDialogHandler import kotlin.test.Test import kotlin.test.assertEquals import org.mockito.kotlin.mock @@ -7,7 +8,7 @@ import org.mockito.kotlin.mock class SentryFeedbackOptionsTest { @Test fun `feedback options is initialized with default values`() { - val options = SentryFeedbackOptions() + val options = SentryFeedbackOptions(mock()) assertEquals(false, options.isNameRequired) assertEquals(true, options.isShowName) assertEquals(false, options.isEmailRequired) @@ -34,7 +35,7 @@ class SentryFeedbackOptionsTest { @Test fun `feedback options copy constructor`() { val options = - SentryFeedbackOptions().apply { + SentryFeedbackOptions(mock()).apply { isNameRequired = true isShowName = false isEmailRequired = true @@ -79,5 +80,6 @@ class SentryFeedbackOptionsTest { assertEquals(options.onFormClose, optionsCopy.onFormClose) assertEquals(options.onSubmitSuccess, optionsCopy.onSubmitSuccess) assertEquals(options.onSubmitError, optionsCopy.onSubmitError) + assertEquals(options.dialogHandler, optionsCopy.dialogHandler) } } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index f132807e428..b6f77c043b0 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -13,7 +13,9 @@ import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.verify class SentryOptionsTest { @Test @@ -815,4 +817,16 @@ class SentryOptionsTest { options.setTag(null, null) assertTrue(options.tags.isEmpty()) } + + @Test + fun `feedback dialog handler logs a warning`() { + val logger = mock() + val options = + SentryOptions.empty().apply { + setLogger(logger) + isDebug = true + } + options.feedbackOptions.dialogHandler.showDialog(mock()) + verify(logger).log(eq(SentryLevel.WARNING), eq("showDialog() can only be called in Android.")) + } } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 6877a3bdebb..91713ffb6f4 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.SentryFeedbackOptions.IDialogHandler import io.sentry.SentryOptions.ProfilesSamplerCallback import io.sentry.SentryOptions.TracesSamplerCallback import io.sentry.backpressure.BackpressureMonitor @@ -131,7 +132,7 @@ class SentryTest { throw IllegalStateException("bad integration") } - Sentry.init { + initForTest { it.dsn = dsn it.integrations.clear() it.integrations.add(badIntegration) @@ -391,7 +392,7 @@ class SentryTest { fun `profilingTracesDirPath should be created and cleared at initialization when continuous profiling is enabled`() { val tempPath = getTempPath() var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.profileSessionSampleRate = 1.0 it.cacheDirPath = tempPath @@ -406,7 +407,7 @@ class SentryTest { fun `profilingTracesDirPath should not be created when no profiling is enabled`() { val tempPath = getTempPath() var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.profileSessionSampleRate = 0.0 it.cacheDirPath = tempPath @@ -923,6 +924,7 @@ class SentryTest { // the scopes is already initialized scopes.startSession() } + it.sessionFlushTimeoutMillis = 100 } assertFalse(previousSessionFile.exists()) @@ -1193,7 +1195,7 @@ class SentryTest { fun `init calls samplers if isStartProfilerOnAppStart is true`() { val mockSampleTracer = mock() val mockProfilesSampler = mock() - Sentry.init { + initForTest { it.dsn = dsn it.tracesSampleRate = 1.0 it.isStartProfilerOnAppStart = true @@ -1306,7 +1308,7 @@ class SentryTest { val appStartProfilingConfigFile = File(path, "app_start_profiling_config") appStartProfilingConfigFile.createNewFile() assertTrue(appStartProfilingConfigFile.exists()) - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = path it.isEnableAppStartProfiling = false @@ -1421,7 +1423,7 @@ class SentryTest { @Test fun `startProfiler starts the continuous profiler`() { val profiler = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setContinuousProfiler(profiler) it.profileSessionSampleRate = 1.0 @@ -1433,7 +1435,7 @@ class SentryTest { @Test fun `startProfiler is ignored when continuous profiling is disabled`() { val profiler = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setContinuousProfiler(profiler) it.profilesSampleRate = 1.0 @@ -1446,7 +1448,7 @@ class SentryTest { fun `startProfiler is ignored when profile lifecycle is TRACE`() { val profiler = mock() val logger = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setContinuousProfiler(profiler) it.profileSessionSampleRate = 1.0 @@ -1467,7 +1469,7 @@ class SentryTest { @Test fun `stopProfiler stops the continuous profiler`() { val profiler = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setContinuousProfiler(profiler) it.profileSessionSampleRate = 1.0 @@ -1479,7 +1481,7 @@ class SentryTest { @Test fun `stopProfiler is ignored when continuous profiling is disabled`() { val profiler = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setContinuousProfiler(profiler) it.profilesSampleRate = 1.0 @@ -1491,7 +1493,7 @@ class SentryTest { @Test fun `replay debug masking is forwarded to replay controller`() { val replayController = mock() - Sentry.init { + initForTest { it.dsn = dsn it.setReplayController(replayController) } @@ -1502,6 +1504,29 @@ class SentryTest { verify(replayController).disableDebugMaskingOverlay() } + @Test + fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler`() { + val mockDialogHandler = mock() + initForTest { + it.dsn = dsn + it.feedbackOptions.dialogHandler = mockDialogHandler + } + Sentry.showUserFeedbackDialog() + verify(mockDialogHandler).showDialog(eq(null)) + } + + @Test + fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler with configurator`() { + val mockDialogHandler = mock() + val configurator = mock() + initForTest { + it.dsn = dsn + it.feedbackOptions.dialogHandler = mockDialogHandler + } + Sentry.showUserFeedbackDialog(configurator) + verify(mockDialogHandler).showDialog(eq(configurator)) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set From d4fe38dc85b860285af82652410c533bd8ed1176 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Jul 2025 14:06:55 +0200 Subject: [PATCH 2/9] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1bb8a6656f..491fe09b113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add Compose user feedback button ([#4559](https://github.com/getsentry/sentry-java/pull/4559)) + ### Fixes - Allow multiple UncaughtExceptionHandlerIntegrations to be active at the same time ([#4462](https://github.com/getsentry/sentry-java/pull/4462)) From 561dee1803ce2e9b5f5c3a7294a8214cfc3ecaff Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 22 Jul 2025 14:50:26 +0200 Subject: [PATCH 3/9] Apply suggestions from code review Co-authored-by: Markus Hintersteiner --- CHANGELOG.md | 2 +- .../kotlin/io/sentry/compose/SentryUserFeedbackButton.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 491fe09b113..9af8eb53a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add Compose user feedback button ([#4559](https://github.com/getsentry/sentry-java/pull/4559)) +- Add `SentryUserFeedbackButton` Composable ([#4559](https://github.com/getsentry/sentry-java/pull/4559)) ### Fixes diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt index 7ca31c9b281..ddd7e31fae5 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt @@ -19,6 +19,7 @@ import io.sentry.SentryFeedbackOptions public fun SentryUserFeedbackButton( modifier: Modifier = Modifier, configurator: SentryFeedbackOptions.OptionsConfigurator? = null, + text: "Report a Bug", ) { Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) { Row( @@ -27,10 +28,10 @@ public fun SentryUserFeedbackButton( ) { Icon( painter = painterResource(id = R.drawable.sentry_user_feedback_compose_button_logo_24), - contentDescription = "Vector Icon", + contentDescription = null, ) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(text = "Report a Bug") + Text(text = text) } } } From 1f321414efe771eae531c7ef2d3221914f1e3c33 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 22 Jul 2025 17:02:58 +0200 Subject: [PATCH 4/9] Add support for associated event ID in user feedback dialog Cleaned SentryUserFeedbackDialog.Builder methods SentryFeedbackOptions is not Internal anymore --- CHANGELOG.md | 1 + .../api/sentry-android-core.api | 5 +- .../android/core/SentryAndroidOptions.java | 11 +++- .../core/SentryUserFeedbackDialog.java | 57 +++++++++++-------- .../uitest/android/UserFeedbackUiTest.kt | 10 +++- sentry-compose/api/android/sentry-compose.api | 9 +-- .../compose/SentryUserFeedbackButton.kt | 4 +- sentry/api/sentry.api | 3 +- sentry/src/main/java/io/sentry/Sentry.java | 8 ++- .../java/io/sentry/SentryFeedbackOptions.java | 6 +- .../main/java/io/sentry/SentryOptions.java | 2 +- .../test/java/io/sentry/SentryOptionsTest.kt | 2 +- sentry/src/test/java/io/sentry/SentryTest.kt | 17 +++++- 13 files changed, 87 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af8eb53a8b..ab9cf067736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add `SentryUserFeedbackButton` Composable ([#4559](https://github.com/getsentry/sentry-java/pull/4559)) + - Also added `Sentry.showUserFeedbackDialog` static method ### Fixes diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 8e4b43d82e8..0bf0d44427d 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -402,9 +402,10 @@ public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app public class io/sentry/android/core/SentryUserFeedbackDialog$Builder { public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;I)V - public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V - public fun (Landroid/content/Context;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V public fun (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V + public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder; + public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder; public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 6adb78e20e3..69ddfe4b0ad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -16,6 +16,7 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -615,7 +616,9 @@ public void setEnableAutoTraceIdGeneration(final boolean enableAutoTraceIdGenera static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler { @Override - public void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + public void showDialog( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity(); if (activity == null) { Sentry.getCurrentScopes() @@ -628,7 +631,11 @@ public void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator return; } - new SentryUserFeedbackDialog.Builder(activity, configurator).create().show(); + new SentryUserFeedbackDialog.Builder(activity) + .associatedEventId(associatedEventId) + .configurator(configurator) + .create() + .show(); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java index dffb4a237ad..542a7027a4f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackDialog.java @@ -25,6 +25,7 @@ public final class SentryUserFeedbackDialog extends AlertDialog { private boolean isCancelable = false; private @Nullable SentryId currentReplayId; + private final @Nullable SentryId associatedEventId; private @Nullable OnDismissListener delegate; private final @Nullable OptionsConfiguration configuration; @@ -33,9 +34,11 @@ public final class SentryUserFeedbackDialog extends AlertDialog { SentryUserFeedbackDialog( final @NotNull Context context, final int themeResId, + final @Nullable SentryId associatedEventId, final @Nullable OptionsConfiguration configuration, final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { super(context, themeResId); + this.associatedEventId = associatedEventId; this.configuration = configuration; this.configurator = configurator; SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget"); @@ -151,6 +154,9 @@ protected void onCreate(Bundle savedInstanceState) { final @NotNull Feedback feedback = new Feedback(message); feedback.setName(name); feedback.setContactEmail(email); + if (associatedEventId != null) { + feedback.setAssociatedEventId(associatedEventId); + } if (currentReplayId != null) { feedback.setReplayId(currentReplayId); } @@ -233,6 +239,7 @@ public static class Builder { @Nullable OptionsConfiguration configuration; @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; + @Nullable SentryId associatedEventId; final @NotNull Context context; final int themeResId; @@ -271,7 +278,7 @@ public Builder(final @NotNull Context context) { * {@code 0} to use the parent {@code context}'s default alert dialog theme */ public Builder(Context context, int themeResId) { - this(context, themeResId, null, null); + this(context, themeResId, null); } /** @@ -288,25 +295,7 @@ public Builder(Context context, int themeResId) { */ public Builder( final @NotNull Context context, final @Nullable OptionsConfiguration configuration) { - this(context, 0, configuration, null); - } - - /** - * Creates a builder for a {@link SentryUserFeedbackDialog} that uses the default alert dialog - * theme. The {@code configuration} can be used to configure the feedback options for this - * specific dialog. - * - *

The default alert dialog theme is defined by {@link android.R.attr#alertDialogTheme} - * within the parent {@code context}'s theme. - * - * @param context the parent context - * @param configurator the configuration for the feedback options, can be {@code null} to use - * the global feedback options. - */ - public Builder( - final @NotNull Context context, - final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { - this(context, 0, null, configurator); + this(context, 0, configuration); } /** @@ -336,12 +325,33 @@ public Builder( public Builder( final @NotNull Context context, final int themeResId, - final @Nullable OptionsConfiguration configuration, - final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + final @Nullable OptionsConfiguration configuration) { this.context = context; this.themeResId = themeResId; this.configuration = configuration; + } + + /** + * Sets the configuration for the feedback options. + * + * @param configurator the configuration for the feedback options, can be {@code null} to use + * the global feedback options. + */ + public Builder configurator( + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { this.configurator = configurator; + return this; + } + + /** + * Sets the associated event ID for the feedback. + * + * @param associatedEventId the associated event ID for the feedback, can be {@code null} to + * avoid associating the feedback to an event. + */ + public Builder associatedEventId(final @Nullable SentryId associatedEventId) { + this.associatedEventId = associatedEventId; + return this; } /** @@ -351,7 +361,8 @@ public Builder( * @return a new instance of {@link SentryUserFeedbackDialog} */ public SentryUserFeedbackDialog create() { - return new SentryUserFeedbackDialog(context, themeResId, configuration, configurator); + return new SentryUserFeedbackDialog( + context, themeResId, associatedEventId, configuration, configurator); } } diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt index 24244c24e0b..6cb6ae00e71 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt @@ -29,6 +29,7 @@ import io.sentry.android.core.R import io.sentry.android.core.SentryUserFeedbackButton import io.sentry.android.core.SentryUserFeedbackDialog import io.sentry.assertEnvelopeFeedback +import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.test.getProperty import kotlin.test.Test @@ -477,7 +478,9 @@ class UserFeedbackUiTest : BaseUiTest() { } } - showDialogAndCheck { + val sentryId = SentryId() + + showDialogAndCheck(sentryId) { // Send the feedback fillFormAndSend() } @@ -497,6 +500,7 @@ class UserFeedbackUiTest : BaseUiTest() { assertEquals("Description filled", feedback.message) // The screen name should be set in the url assertEquals("io.sentry.uitest.android.EmptyActivity", feedback.url) + assertEquals(sentryId, feedback.associatedEventId) if (enableReplay) { // The current replay should be set in the replayId @@ -629,11 +633,11 @@ class UserFeedbackUiTest : BaseUiTest() { onView(withId(R.id.sentry_dialog_user_feedback_btn_send)).perform(click()) } - private fun showDialogAndCheck(checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}) { + private fun showDialogAndCheck(associatedEventId: SentryId? = null, checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}) { lateinit var dialog: SentryUserFeedbackDialog val feedbackScenario = launchActivity() feedbackScenario.onActivity { - dialog = SentryUserFeedbackDialog.Builder(it).create() + dialog = SentryUserFeedbackDialog.Builder(it).associatedEventId(associatedEventId).create() dialog.show() } diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index 88580cd7ea8..6efb0a9a089 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -6,13 +6,6 @@ public final class io/sentry/compose/BuildConfig { public fun ()V } -public final class io/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt { - public static final field INSTANCE Lio/sentry/compose/ComposableSingletons$SentryUserFeedbackButtonKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$sentry_compose_release ()Lkotlin/jvm/functions/Function3; -} - public final class io/sentry/compose/SentryComposeHelperKt { public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect; } @@ -34,7 +27,7 @@ public final class io/sentry/compose/SentryNavigationIntegrationKt { } public final class io/sentry/compose/SentryUserFeedbackButtonKt { - public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Landroidx/compose/runtime/Composer;II)V + public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V } public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator { diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt index ddd7e31fae5..9ccd13a7e37 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt @@ -19,7 +19,7 @@ import io.sentry.SentryFeedbackOptions public fun SentryUserFeedbackButton( modifier: Modifier = Modifier, configurator: SentryFeedbackOptions.OptionsConfigurator? = null, - text: "Report a Bug", + text: String = "Report a Bug", ) { Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) { Row( @@ -31,7 +31,7 @@ public fun SentryUserFeedbackButton( contentDescription = null, ) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(text = text) + Text(text = text) } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 08939c84c03..a1927ba7cd4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2589,6 +2589,7 @@ public final class io/sentry/Sentry { public static fun setUser (Lio/sentry/protocol/User;)V public static fun showUserFeedbackDialog ()V public static fun showUserFeedbackDialog (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public static fun showUserFeedbackDialog (Lio/sentry/protocol/SentryId;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V public static fun startProfiler ()V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; @@ -3013,7 +3014,7 @@ public final class io/sentry/SentryFeedbackOptions { } public abstract interface class io/sentry/SentryFeedbackOptions$IDialogHandler { - public abstract fun showDialog (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public abstract fun showDialog (Lio/sentry/protocol/SentryId;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V } public abstract interface class io/sentry/SentryFeedbackOptions$OptionsConfigurator { diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 0214514f99c..f24ecab9e78 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1296,7 +1296,13 @@ public static void showUserFeedbackDialog() { public static void showUserFeedbackDialog( final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + showUserFeedbackDialog(null, configurator); + } + + public static void showUserFeedbackDialog( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { final @NotNull SentryOptions options = getCurrentScopes().getOptions(); - options.getFeedbackOptions().getDialogHandler().showDialog(configurator); + options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); } } diff --git a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java index 79ba24dd3ac..d43fe7342e1 100644 --- a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java +++ b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java @@ -1,11 +1,11 @@ package io.sentry; import io.sentry.protocol.Feedback; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -@ApiStatus.Internal public final class SentryFeedbackOptions { // User and Form /** Requires the name field on the feedback form to be filled in. Defaults to false. */ @@ -584,7 +584,9 @@ public interface SentryFeedbackCallback { @ApiStatus.Internal public interface IDialogHandler { - void showDialog(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); + void showDialog( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); } /** Configuration callback for feedback options. */ diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 79b95151e41..80547f1a41b 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3037,7 +3037,7 @@ private SentryOptions(final boolean empty) { sessionReplay = new SentryReplayOptions(empty, sdkVersion); feedbackOptions = new SentryFeedbackOptions( - configurator -> + (associatedEventId, configurator) -> logger.log(SentryLevel.WARNING, "showDialog() can only be called in Android.")); if (!empty) { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index b6f77c043b0..c79ee125d03 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -826,7 +826,7 @@ class SentryOptionsTest { setLogger(logger) isDebug = true } - options.feedbackOptions.dialogHandler.showDialog(mock()) + options.feedbackOptions.dialogHandler.showDialog(mock(), mock()) verify(logger).log(eq(SentryLevel.WARNING), eq("showDialog() can only be called in Android.")) } } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 91713ffb6f4..87f4ce17c00 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1512,7 +1512,7 @@ class SentryTest { it.feedbackOptions.dialogHandler = mockDialogHandler } Sentry.showUserFeedbackDialog() - verify(mockDialogHandler).showDialog(eq(null)) + verify(mockDialogHandler).showDialog(eq(null), eq(null)) } @Test @@ -1524,7 +1524,20 @@ class SentryTest { it.feedbackOptions.dialogHandler = mockDialogHandler } Sentry.showUserFeedbackDialog(configurator) - verify(mockDialogHandler).showDialog(eq(configurator)) + verify(mockDialogHandler).showDialog(eq(null), eq(configurator)) + } + + @Test + fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler with associatedEventId and configurator`() { + val mockDialogHandler = mock() + val configurator = mock() + val associatedEventId = SentryId() + initForTest { + it.dsn = dsn + it.feedbackOptions.dialogHandler = mockDialogHandler + } + Sentry.showUserFeedbackDialog(associatedEventId, configurator) + verify(mockDialogHandler).showDialog(eq(associatedEventId), eq(configurator)) } private class InMemoryOptionsObserver : IOptionsObserver { From 0a510802f4835f1f6f080f6d1925726d4474f961 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 22 Jul 2025 17:07:45 +0200 Subject: [PATCH 5/9] format --- .../java/io/sentry/uitest/android/UserFeedbackUiTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt index 6cb6ae00e71..ac434535c13 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/UserFeedbackUiTest.kt @@ -633,7 +633,10 @@ class UserFeedbackUiTest : BaseUiTest() { onView(withId(R.id.sentry_dialog_user_feedback_btn_send)).perform(click()) } - private fun showDialogAndCheck(associatedEventId: SentryId? = null, checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}) { + private fun showDialogAndCheck( + associatedEventId: SentryId? = null, + checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}, + ) { lateinit var dialog: SentryUserFeedbackDialog val feedbackScenario = launchActivity() feedbackScenario.onActivity { From 0b250aa1971ae2ebce0d06512581c01a012bc20c Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 22 Jul 2025 17:19:40 +0200 Subject: [PATCH 6/9] fixed test --- .../io/sentry/android/core/SentryUserFeedbackDialogTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt index 12413ed7edf..bd60859c608 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt @@ -11,6 +11,7 @@ import io.sentry.ReplayController import io.sentry.Sentry import io.sentry.SentryFeedbackOptions import io.sentry.SentryLevel +import io.sentry.protocol.SentryId import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -53,10 +54,11 @@ class SentryUserFeedbackDialogTest { } fun getSut( + associatedEventId: SentryId? = null, configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null, configurator: SentryFeedbackOptions.OptionsConfigurator? = null, ): SentryUserFeedbackDialog = - SentryUserFeedbackDialog(application, 0, configuration, configurator) + SentryUserFeedbackDialog(application, 0, associatedEventId, configuration, configurator) } private val fixture = Fixture() From 7f6bcab7e9ed258e15113b707b60e9501484fd8b Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 22 Jul 2025 17:46:23 +0200 Subject: [PATCH 7/9] fixed test --- .../kotlin/io/sentry/compose/SentryUserFeedbackButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt index 9ccd13a7e37..93460b88893 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt @@ -18,8 +18,8 @@ import io.sentry.SentryFeedbackOptions @Composable public fun SentryUserFeedbackButton( modifier: Modifier = Modifier, - configurator: SentryFeedbackOptions.OptionsConfigurator? = null, text: String = "Report a Bug", + configurator: SentryFeedbackOptions.OptionsConfigurator? = null, ) { Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) { Row( From bf10ebde1881a89238b1dd9c64c0750aeaeb2bbb Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 22 Jul 2025 18:20:51 +0200 Subject: [PATCH 8/9] SentryFeedbackOptions.set/getDialogHandler is now internal --- sentry-compose/api/android/sentry-compose.api | 2 +- sentry/src/main/java/io/sentry/SentryFeedbackOptions.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index 6efb0a9a089..3ae9af627b1 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -27,7 +27,7 @@ public final class io/sentry/compose/SentryNavigationIntegrationKt { } public final class io/sentry/compose/SentryUserFeedbackButtonKt { - public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V + public static final fun SentryUserFeedbackButton (Landroidx/compose/ui/Modifier;Ljava/lang/String;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;Landroidx/compose/runtime/Composer;II)V } public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator { diff --git a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java index d43fe7342e1..77e0741f8d6 100644 --- a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java +++ b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java @@ -517,6 +517,7 @@ public void setOnSubmitError(final @Nullable SentryFeedbackCallback onSubmitErro * * @param iDialogHandler the dialog handler to be used to show the feedback form */ + @ApiStatus.Internal public void setDialogHandler(final @NotNull IDialogHandler iDialogHandler) { this.iDialogHandler = iDialogHandler; } @@ -526,6 +527,7 @@ public void setDialogHandler(final @NotNull IDialogHandler iDialogHandler) { * * @return the dialog handler to be used to show the feedback form */ + @ApiStatus.Internal public @NotNull IDialogHandler getDialogHandler() { return iDialogHandler; } From 21b420255886d01988b7124db325520e458ef5a1 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 23 Jul 2025 11:58:29 +0200 Subject: [PATCH 9/9] added icons attributions --- .../main/res/drawable/sentry_user_feedback_button_logo_24.xml | 1 + .../res/drawable/sentry_user_feedback_compose_button_logo_24.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/sentry-android-core/src/main/res/drawable/sentry_user_feedback_button_logo_24.xml b/sentry-android-core/src/main/res/drawable/sentry_user_feedback_button_logo_24.xml index 52f0a8f044b..4aab5d6c37b 100644 --- a/sentry-android-core/src/main/res/drawable/sentry_user_feedback_button_logo_24.xml +++ b/sentry-android-core/src/main/res/drawable/sentry_user_feedback_button_logo_24.xml @@ -1,3 +1,4 @@ + diff --git a/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml b/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml index 31efc71058c..39807bd7070 100644 --- a/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml +++ b/sentry-compose/src/androidMain/res/drawable/sentry_user_feedback_compose_button_logo_24.xml @@ -1,3 +1,4 @@ +