Skip to content

New User Feedback form #4384

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
62e71ff
added Feedback class and extended Contexts with it
stefanosiano Mar 14, 2025
c9b4850
Added Sentry.captureFeedback API
stefanosiano Mar 25, 2025
813ff1d
updated changelog
stefanosiano Mar 25, 2025
dd2d6a8
added tests
stefanosiano Mar 27, 2025
6428dfa
added scope replay id and screen as url
stefanosiano Mar 31, 2025
144bcca
added feedback as DataCategory for rate limit and client report
stefanosiano Apr 16, 2025
509f992
Merge branch 'refs/heads/main' into feat/new-user-feedback-logic
stefanosiano Apr 16, 2025
3c0a6fb
merged main
stefanosiano Apr 16, 2025
9e1066e
added tests
stefanosiano Apr 17, 2025
eadb567
Merge branch 'refs/heads/main' into feat/new-user-feedback-logic
stefanosiano Apr 17, 2025
8feeadc
fixed tests
stefanosiano Apr 17, 2025
986772d
started adding resources for UF widget
stefanosiano Apr 29, 2025
e19d122
started SentryFeedbackOptions
stefanosiano Apr 30, 2025
79052ad
Merge branch 'feat/new-user-feedback-logic' into feat/user-feedback-w…
stefanosiano May 7, 2025
5a83bb8
added all form options
stefanosiano May 9, 2025
ee44b18
Merge branch 'main' into feat/new-user-feedback-logic
stefanosiano May 9, 2025
212ffbf
merged main
stefanosiano May 9, 2025
2bf99b5
Merge branch 'feat/new-user-feedback-logic' into feat/user-feedback-w…
stefanosiano May 9, 2025
6eb876c
user feedback dialog now uses dialogTheme
stefanosiano May 14, 2025
a33ae00
Merge branch 'main' into feat/user-feedback-widget
stefanosiano May 14, 2025
d47f374
merged main
stefanosiano May 14, 2025
1a758a2
removed java options for send button colors. Replaced with theme sett…
stefanosiano May 14, 2025
9315f7b
added tests and UI tests
stefanosiano May 16, 2025
62cea5e
added comments
stefanosiano May 16, 2025
794bd66
added comments
stefanosiano May 16, 2025
f16ac66
added replay capturing on feedback dialog open
stefanosiano May 16, 2025
5388b42
added cancel button ui test
stefanosiano May 19, 2025
8aacd36
added cancel button ui test
stefanosiano May 19, 2025
8750c8d
Merge branch 'refs/heads/main' into feat/user-feedback-widget
stefanosiano May 19, 2025
6d14499
added Feedback.toString() and default values in javadoc of SentryFeed…
stefanosiano May 19, 2025
fb88c1f
merged main
stefanosiano May 19, 2025
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
- Add debug mode for Session Replay masking ([#4357](https://github.com/getsentry/sentry-java/pull/4357))
- Use `Sentry.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks.
- The masks will be invalidated at most once per `frameRate` (default 1 fps).
- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384))
- We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).
For customization options, please check the [User Feedback documentation](https://docs.sentry.io/platforms/android/user-feedback/configuration/).
```java
import io.sentry.android.core.SentryUserFeedbackDialog;

new SentryUserFeedbackDialog(context).show();
```
```kotlin
import io.sentry.android.core.SentryUserFeedbackDialog

SentryUserFeedbackDialog(context).show()
```

## 8.12.0

Expand Down
9 changes: 9 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ public final class io/sentry/android/core/SentryPerformanceProvider {
public fun shutdown ()V
}

public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;I)V
public fun <init> (Landroid/content/Context;ZLandroid/content/DialogInterface$OnCancelListener;)V
public fun setCancelable (Z)V
public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
public fun show ()V
}

public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
Expand Down
3 changes: 2 additions & 1 deletion sentry-android-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>

<application>
<application
android:supportsRtl="true">
<!-- 'android:authorities' must be unique in the device, across all apps -->
<provider
android:name=".SentryInitProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.sentry.ILogger;
import io.sentry.InitPriority;
import io.sentry.ProfileLifecycle;
import io.sentry.SentryFeedbackOptions;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.SentryLevel;
import io.sentry.protocol.SdkVersion;
Expand Down Expand Up @@ -126,6 +127,18 @@ final class ManifestMetadataReader {
static final String ENABLE_AUTO_TRACE_ID_GENERATION =
"io.sentry.traces.enable-auto-id-generation";

static final String FEEDBACK_NAME_REQUIRED = "io.sentry.feedback.is-name-required";

static final String FEEDBACK_SHOW_NAME = "io.sentry.feedback.show-name";

static final String FEEDBACK_EMAIL_REQUIRED = "io.sentry.feedback.is-email-required";

static final String FEEDBACK_SHOW_EMAIL = "io.sentry.feedback.show-email";

static final String FEEDBACK_USE_SENTRY_USER = "io.sentry.feedback.use-sentry-user";

static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -477,6 +490,21 @@ static void applyMetadata(
options
.getLogs()
.setEnabled(readBool(metadata, logger, ENABLE_LOGS, options.getLogs().isEnabled()));

final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions();
feedbackOptions.setNameRequired(
readBool(metadata, logger, FEEDBACK_NAME_REQUIRED, feedbackOptions.isNameRequired()));
feedbackOptions.setShowName(
readBool(metadata, logger, FEEDBACK_SHOW_NAME, feedbackOptions.isShowName()));
feedbackOptions.setEmailRequired(
readBool(metadata, logger, FEEDBACK_EMAIL_REQUIRED, feedbackOptions.isEmailRequired()));
feedbackOptions.setShowEmail(
readBool(metadata, logger, FEEDBACK_SHOW_EMAIL, feedbackOptions.isShowEmail()));
feedbackOptions.setUseSentryUser(
readBool(
metadata, logger, FEEDBACK_USE_SENTRY_USER, feedbackOptions.isUseSentryUser()));
feedbackOptions.setShowBranding(
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));
}
options
.getLogger()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package io.sentry.android.core;

import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import io.sentry.IScopes;
import io.sentry.Sentry;
import io.sentry.SentryFeedbackOptions;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.protocol.Feedback;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class SentryUserFeedbackDialog extends AlertDialog {

private boolean isCancelable = false;
private @Nullable SentryId currentReplayId;
private @Nullable OnDismissListener delegate;

public SentryUserFeedbackDialog(final @NotNull Context context) {
super(context);
}

public SentryUserFeedbackDialog(
final @NotNull Context context,
final boolean cancelable,
@Nullable final OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
isCancelable = cancelable;
}

public SentryUserFeedbackDialog(final @NotNull Context context, final int themeResId) {
super(context, themeResId);
}

@Override
public void setCancelable(boolean cancelable) {
super.setCancelable(cancelable);
isCancelable = cancelable;
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sentry_dialog_user_feedback);
setCancelable(isCancelable);

final @NotNull SentryFeedbackOptions feedbackOptions =
Sentry.getCurrentScopes().getOptions().getFeedbackOptions();
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);
final @NotNull EditText edtName = findViewById(R.id.sentry_dialog_user_feedback_edt_name);
final @NotNull TextView lblEmail = findViewById(R.id.sentry_dialog_user_feedback_txt_email);
final @NotNull EditText edtEmail = findViewById(R.id.sentry_dialog_user_feedback_edt_email);
final @NotNull TextView lblMessage =
findViewById(R.id.sentry_dialog_user_feedback_txt_description);
final @NotNull EditText edtMessage =
findViewById(R.id.sentry_dialog_user_feedback_edt_description);
final @NotNull Button btnSend = findViewById(R.id.sentry_dialog_user_feedback_btn_send);
final @NotNull Button btnCancel = findViewById(R.id.sentry_dialog_user_feedback_btn_cancel);

if (feedbackOptions.isShowBranding()) {
imgLogo.setVisibility(View.VISIBLE);
} else {
imgLogo.setVisibility(View.GONE);
}

// If name is required, ignore showName flag
if (!feedbackOptions.isShowName() && !feedbackOptions.isNameRequired()) {
lblName.setVisibility(View.GONE);
edtName.setVisibility(View.GONE);
} else {
lblName.setVisibility(View.VISIBLE);
edtName.setVisibility(View.VISIBLE);
lblName.setText(feedbackOptions.getNameLabel());
edtName.setHint(feedbackOptions.getNamePlaceholder());
if (feedbackOptions.isNameRequired()) {
lblName.append(feedbackOptions.getIsRequiredLabel());
}
}

// If email is required, ignore showEmail flag
if (!feedbackOptions.isShowEmail() && !feedbackOptions.isEmailRequired()) {
lblEmail.setVisibility(View.GONE);
edtEmail.setVisibility(View.GONE);
} else {
lblEmail.setVisibility(View.VISIBLE);
edtEmail.setVisibility(View.VISIBLE);
lblEmail.setText(feedbackOptions.getEmailLabel());
edtEmail.setHint(feedbackOptions.getEmailPlaceholder());
if (feedbackOptions.isEmailRequired()) {
lblEmail.append(feedbackOptions.getIsRequiredLabel());
}
}

// If Sentry user is set, and useSentryUser is true, populate the name and email
if (feedbackOptions.isUseSentryUser()) {
final @Nullable User user = Sentry.getCurrentScopes().getScope().getUser();
if (user != null) {
edtName.setText(user.getName());
edtEmail.setText(user.getEmail());
}
}

lblMessage.setText(feedbackOptions.getMessageLabel());
lblMessage.append(feedbackOptions.getIsRequiredLabel());
edtMessage.setHint(feedbackOptions.getMessagePlaceholder());
lblTitle.setText(feedbackOptions.getFormTitle());

btnSend.setText(feedbackOptions.getSubmitButtonLabel());
btnSend.setOnClickListener(
v -> {
// Gather fields and trim them
final @NotNull String name = edtName.getText().toString().trim();
final @NotNull String email = edtEmail.getText().toString().trim();
final @NotNull String message = edtMessage.getText().toString().trim();

// If a required field is missing, shows the error label
if (name.isEmpty() && feedbackOptions.isNameRequired()) {
edtName.setError(lblName.getText());
return;
}

if (email.isEmpty() && feedbackOptions.isEmailRequired()) {
edtEmail.setError(lblEmail.getText());
return;
}

if (message.isEmpty()) {
edtMessage.setError(lblMessage.getText());
return;
}

// Create the feedback object
final @NotNull Feedback feedback = new Feedback(message);
feedback.setName(name);
feedback.setContactEmail(email);
if (currentReplayId != null) {
feedback.setReplayId(currentReplayId);
}

// Capture the feedback. If the ID is empty, it means that the feedback was not sent
final @NotNull SentryId id = Sentry.captureFeedback(feedback);
if (!id.equals(SentryId.EMPTY_ID)) {
Toast.makeText(
getContext(), feedbackOptions.getSuccessMessageText(), Toast.LENGTH_SHORT)
.show();
final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitSuccess =
feedbackOptions.getOnSubmitSuccess();
if (onSubmitSuccess != null) {
onSubmitSuccess.call(feedback);
}
} else {
final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitError =
feedbackOptions.getOnSubmitError();
if (onSubmitError != null) {
onSubmitError.call(feedback);
}
}
cancel();
});

btnCancel.setText(feedbackOptions.getCancelButtonLabel());
btnCancel.setOnClickListener(v -> cancel());
setOnDismissListener(delegate);
}

@Override
public void setOnDismissListener(final @Nullable OnDismissListener listener) {
delegate = listener;
// If the user set a custom onDismissListener, we ensure it doesn't override the onFormClose
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
final @Nullable Runnable onFormClose = options.getFeedbackOptions().getOnFormClose();
if (onFormClose != null) {
super.setOnDismissListener(
dialog -> {
onFormClose.run();
currentReplayId = null;
if (delegate != null) {
delegate.onDismiss(dialog);
}
});
} else {
super.setOnDismissListener(delegate);
}
}

@Override
protected void onStart() {
super.onStart();
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions();
final @Nullable Runnable onFormOpen = feedbackOptions.getOnFormOpen();
if (onFormOpen != null) {
onFormOpen.run();
}
options.getReplayController().captureReplay(false);
currentReplayId = options.getReplayController().getReplayId();
}

@Override
public void show() {
// If Sentry is disabled, don't show the dialog, but log a warning
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
final @NotNull SentryOptions options = scopes.getOptions();
if (!scopes.isEnabled() || !options.isEnabled()) {
options
.getLogger()
.log(SentryLevel.WARNING, "Sentry is disabled. Feedback dialog won't be shown.");
return;
}
// Otherwise, show the dialog
super.show();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
15 changes: 15 additions & 0 deletions sentry-android-core/src/main/res/drawable/edit_text_border.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<solid android:color="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="#FFAAAAAA" /> <!-- border color -->
<corners android:radius="4dp" />
<padding
android:left="8dp"
android:top="8dp"
android:right="8dp"
android:bottom="8dp" />
</shape>
Loading
Loading