Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features

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

### Fixes
Expand Down
2 changes: 2 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ public class io/sentry/android/core/SentryUserFeedbackDialog$Builder {
public fun <init> (Landroid/content/Context;I)V
public fun <init> (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
public fun <init> (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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ static void installDefaultIntegrations(
options.addIntegration(replay);
options.setReplayController(replay);
}
options
.getFeedbackOptions()
.setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package io.sentry.android.core;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import io.sentry.Hint;
import io.sentry.IScope;
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;
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;
Expand Down Expand Up @@ -609,4 +613,29 @@ public boolean isEnableAutoTraceIdGeneration() {
public void setEnableAutoTraceIdGeneration(final boolean enableAutoTraceIdGeneration) {
this.enableAutoTraceIdGeneration = enableAutoTraceIdGeneration;
}

static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
@Override
public void showDialog(
final @Nullable SentryId associatedEventId,
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)
.associatedEventId(associatedEventId)
.configurator(configurator)
.create()
.show();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ 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;
private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator;

SentryUserFeedbackDialog(
final @NotNull Context context,
final int themeResId,
final @Nullable OptionsConfiguration configuration) {
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");
}

Expand All @@ -56,6 +62,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);
Expand Down Expand Up @@ -145,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);
}
Expand Down Expand Up @@ -226,6 +238,8 @@ public void show() {
public static class Builder {

@Nullable OptionsConfiguration configuration;
@Nullable SentryFeedbackOptions.OptionsConfigurator configurator;
@Nullable SentryId associatedEventId;
final @NotNull Context context;
final int themeResId;

Expand Down Expand Up @@ -317,14 +331,38 @@ public Builder(
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;
}

/**
* Builds a new {@link SentryUserFeedbackDialog} with the specified context, theme, and
* configuration.
*
* @return a new instance of {@link SentryUserFeedbackDialog}
*/
public SentryUserFeedbackDialog create() {
return new SentryUserFeedbackDialog(context, themeResId, configuration);
return new SentryUserFeedbackDialog(
context, themeResId, associatedEventId, configuration, configurator);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -836,6 +837,12 @@ class AndroidOptionsInitializerTest {
assertNull(anrv1Integration)
}

@Test
fun `AndroidUserFeedbackIDialogHandler is set as feedback dialog handler`() {
fixture.initSut()
assertIs<AndroidUserFeedbackIDialogHandler>(fixture.sentryOptions.feedbackOptions.dialogHandler)
}

@Test
fun `PersistingScopeObserver is no-op, if scope persistence is disabled`() {
fixture.initSut(configureOptions = { isEnableScopePersistence = false })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ 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 io.sentry.protocol.SentryId
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -52,8 +54,11 @@ class SentryUserFeedbackDialogTest {
}

fun getSut(
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null
): SentryUserFeedbackDialog = SentryUserFeedbackDialog(application, 0, configuration)
associatedEventId: SentryId? = null,
configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null,
configurator: SentryFeedbackOptions.OptionsConfigurator? = null,
): SentryUserFeedbackDialog =
SentryUserFeedbackDialog(application, 0, associatedEventId, configuration, configurator)
}

private val fixture = Fixture()
Expand Down Expand Up @@ -98,7 +103,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<TextView>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,7 +50,23 @@ class UserFeedbackUiTest : BaseUiTest() {
launchActivity<EmptyActivity>().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<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }
onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist())
}

@Test
fun userFeedbackShownViaApi() {
initSentry()
launchActivity<EmptyActivity>().onActivity { Sentry.showUserFeedbackDialog() }

onView(withId(R.id.sentry_dialog_user_feedback_layout))
.inRoot(isDialog())
.check(matches(isDisplayed()))
}

@Test
Expand Down Expand Up @@ -461,7 +478,9 @@ class UserFeedbackUiTest : BaseUiTest() {
}
}

showDialogAndCheck {
val sentryId = SentryId()

showDialogAndCheck(sentryId) {
// Send the feedback
fillFormAndSend()
}
Expand All @@ -481,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
Expand Down Expand Up @@ -613,11 +633,14 @@ 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<EmptyActivity>()
feedbackScenario.onActivity {
dialog = SentryUserFeedbackDialog.Builder(it).create()
dialog = SentryUserFeedbackDialog.Builder(it).associatedEventId(associatedEventId).create()
dialog.show()
}

Expand Down
4 changes: 4 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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;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 {
public static final field $stable I
public static final field Companion Lio/sentry/compose/gestures/ComposeGestureTargetLocator$Companion;
Expand Down
1 change: 1 addition & 0 deletions sentry-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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,
text: String = "Report a Bug",
configurator: SentryFeedbackOptions.OptionsConfigurator? = null,
Comment thread
stefanosiano marked this conversation as resolved.
) {
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 = null,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(text = text)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
Comment thread
stefanosiano marked this conversation as resolved.

<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"/>

</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
Loading
Loading