diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b5b7d1686..4d1a581769c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Features + +- Add `Sentry.feedback()` API for `show()` and `capture()` ([#5349](https://github.com/getsentry/sentry-java/pull/5349)) + - `Sentry.showUserFeedbackDialog()` is deprecated in favor of `Sentry.feedback().show()` + - `Sentry.captureFeedback()` is deprecated in favor of `Sentry.feedback().capture()` + - `Sentry.captureUserFeedback()` and `UserFeedback` are deprecated in favor of `Sentry.feedback().capture()` with the new `Feedback` type + - `SentryUserFeedbackDialog` is deprecated in favor of `SentryUserFeedbackForm` + - All deprecated APIs will be removed in the next major version + ### Dependencies - Bump Native SDK from v0.13.7 to v0.13.8 ([#5334](https://github.com/getsentry/sentry-java/pull/5334)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 0d83082548f..4623d6e1316 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -496,23 +496,44 @@ public class io/sentry/android/core/SentryUserFeedbackButton : android/widget/Bu public fun setOnClickListener (Landroid/view/View$OnClickListener;)V } -public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog { - public fun setCancelable (Z)V - public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V - public fun show ()V +public final class io/sentry/android/core/SentryUserFeedbackDialog : io/sentry/android/core/SentryUserFeedbackForm { } -public class io/sentry/android/core/SentryUserFeedbackDialog$Builder { +public class io/sentry/android/core/SentryUserFeedbackDialog$Builder : io/sentry/android/core/SentryUserFeedbackForm$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;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder; + public synthetic fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder; public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder; + public synthetic fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder; public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog; + public synthetic fun create ()Lio/sentry/android/core/SentryUserFeedbackForm; +} + +public abstract interface class io/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration : io/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration { +} + +public class io/sentry/android/core/SentryUserFeedbackForm : android/app/AlertDialog { + protected fun onCreate (Landroid/os/Bundle;)V + protected fun onStart ()V + public fun setCancelable (Z)V + public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V + public fun show ()V +} + +public class io/sentry/android/core/SentryUserFeedbackForm$Builder { + public fun (Landroid/content/Context;)V + public fun (Landroid/content/Context;I)V + public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration;)V + public fun (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration;)V + public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder; + public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder; + public fun create ()Lio/sentry/android/core/SentryUserFeedbackForm; } -public abstract interface class io/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration { +public abstract interface class io/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration { public abstract fun configure (Landroid/content/Context;Lio/sentry/SentryFeedbackOptions;)V } 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 5f7fad69b5d..5704cf7d7d4 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 @@ -441,7 +441,7 @@ static void installDefaultIntegrations( } options .getFeedbackOptions() - .setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler()); + .setFormHandler(new SentryAndroidOptions.AndroidUserFeedbackFormHandler()); } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java index b845b6ed8c4..fc34f18152f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/FeedbackShakeIntegration.java @@ -178,7 +178,7 @@ private void startShakeDetection(final @NotNull Activity activity) { } previousOnFormClose = null; }); - new SentryUserFeedbackDialog.Builder(active).create().show(); + new SentryUserFeedbackForm.Builder(active).create().show(); } catch (Throwable e) { isDialogShowing = false; options.getFeedbackOptions().setOnFormClose(previousOnFormClose); 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 054e43322a2..8fe702aad50 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 @@ -741,9 +741,9 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) { this.enableAnrFingerprinting = enableAnrFingerprinting; } - static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler { + static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler { @Override - public void showDialog( + public void showForm( final @Nullable SentryId associatedEventId, final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity(); @@ -758,7 +758,7 @@ public void showDialog( return; } - new SentryUserFeedbackDialog.Builder(activity) + new SentryUserFeedbackForm.Builder(activity) .associatedEventId(associatedEventId) .configurator(configurator) .create() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackButton.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackButton.java index eedafd8f001..729dfd0b4e7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackButton.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackButton.java @@ -104,7 +104,7 @@ private void init( } } - // Set the default ClickListener to open the SentryUserFeedbackDialog + // Set the default ClickListener to open the SentryUserFeedbackForm setOnClickListener(delegate); } @@ -113,7 +113,7 @@ public void setOnClickListener(final @Nullable OnClickListener listener) { delegate = listener; super.setOnClickListener( v -> { - new SentryUserFeedbackDialog.Builder(getContext()).create().show(); + new SentryUserFeedbackForm.Builder(getContext()).create().show(); if (delegate != null) { delegate.onClick(v); } 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 542a7027a4f..155464b7b73 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 @@ -1,380 +1,114 @@ 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.SentryIntegrationPackageStorage; -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 final @Nullable SentryId associatedEventId; - private @Nullable OnDismissListener delegate; - - private final @Nullable OptionsConfiguration configuration; - private final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; +/** + * @deprecated Use {@link SentryUserFeedbackForm} instead. + */ +@Deprecated +public final class SentryUserFeedbackDialog extends SentryUserFeedbackForm { SentryUserFeedbackDialog( final @NotNull Context context, final int themeResId, final @Nullable SentryId associatedEventId, - final @Nullable OptionsConfiguration configuration, + final @Nullable SentryUserFeedbackForm.OptionsConfiguration configuration, final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { - super(context, themeResId); - this.associatedEventId = associatedEventId; - this.configuration = configuration; - this.configurator = configurator; - SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget"); - } - - @Override - public void setCancelable(boolean cancelable) { - super.setCancelable(cancelable); - isCancelable = cancelable; - } - - @Override - @SuppressWarnings("deprecation") - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.sentry_dialog_user_feedback); - setCancelable(isCancelable); - - final @NotNull SentryFeedbackOptions feedbackOptions = - new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions()); - 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); - 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.getUsername()); - 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 (associatedEventId != null) { - feedback.setAssociatedEventId(associatedEventId); - } - 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); + super(context, themeResId, associatedEventId, configuration, configurator); } - @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(); - } - - public static class Builder { - - @Nullable OptionsConfiguration configuration; - @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; - @Nullable SentryId associatedEventId; - final @NotNull Context context; - final int themeResId; + /** + * @deprecated Use {@link SentryUserFeedbackForm.Builder} instead. + */ + @Deprecated + public static class Builder extends SentryUserFeedbackForm.Builder { /** * Creates a builder for a {@link SentryUserFeedbackDialog} that uses the default alert dialog * theme. * - *

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 + * @deprecated Use {@link SentryUserFeedbackForm.Builder#Builder(Context)} instead. */ + @Deprecated public Builder(final @NotNull Context context) { - this(context, 0); + super(context); } /** * Creates a builder for a {@link SentryUserFeedbackDialog} that uses an explicit theme * resource. * - *

The specified theme resource ({@code themeResId}) is applied on top of the parent {@code - * context}'s theme. It may be specified as a style resource containing a fully-populated theme, - * such as {@link android.R.style#Theme_Material_Dialog}, to replace all attributes in the - * parent {@code context}'s theme including primary and accent colors. - * - *

To preserve attributes such as primary and accent colors, the {@code themeResId} may - * instead be specified as an overlay theme such as {@link - * android.R.style#ThemeOverlay_Material_Dialog}. This will override only the window attributes - * necessary to style the alert window as a dialog. - * - *

Alternatively, the {@code themeResId} may be specified as {@code 0} to use the parent - * {@code context}'s resolved value for {@link android.R.attr#alertDialogTheme}. - * * @param context the parent context - * @param themeResId the resource ID of the theme against which to inflate this dialog, or - * {@code 0} to use the parent {@code context}'s default alert dialog theme + * @param themeResId the resource ID of the theme + * @deprecated Use {@link SentryUserFeedbackForm.Builder#Builder(Context, int)} instead. */ + @Deprecated public Builder(Context context, int themeResId) { - this(context, themeResId, null); + super(context, themeResId); } /** - * 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. + * Creates a builder for a {@link SentryUserFeedbackDialog} with a configuration. * * @param context the parent context - * @param configuration the configuration for the feedback options, can be {@code null} to use - * the global feedback options. + * @param configuration the configuration for the feedback options + * @deprecated Use {@link SentryUserFeedbackForm.Builder#Builder(Context, + * SentryUserFeedbackForm.OptionsConfiguration)} instead. */ + @Deprecated public Builder( final @NotNull Context context, final @Nullable OptionsConfiguration configuration) { - this(context, 0, configuration); + super(context, configuration); } /** - * Creates a builder for a {@link SentryUserFeedbackDialog} that uses an explicit theme - * resource. The {@code configuration} can be used to configure the feedback options for this - * specific dialog. - * - *

The specified theme resource ({@code themeResId}) is applied on top of the parent {@code - * context}'s theme. It may be specified as a style resource containing a fully-populated theme, - * such as {@link android.R.style#Theme_Material_Dialog}, to replace all attributes in the - * parent {@code context}'s theme including primary and accent colors. - * - *

To preserve attributes such as primary and accent colors, the {@code themeResId} may - * instead be specified as an overlay theme such as {@link - * android.R.style#ThemeOverlay_Material_Dialog}. This will override only the window attributes - * necessary to style the alert window as a dialog. - * - *

Alternatively, the {@code themeResId} may be specified as {@code 0} to use the parent - * {@code context}'s resolved value for {@link android.R.attr#alertDialogTheme}. + * Creates a builder for a {@link SentryUserFeedbackDialog} with a theme and configuration. * * @param context the parent context - * @param themeResId the resource ID of the theme against which to inflate this dialog, or - * {@code 0} to use the parent {@code context}'s default alert dialog theme - * @param configuration the configuration for the feedback options, can be {@code null} to use - * the global feedback options. + * @param themeResId the resource ID of the theme + * @param configuration the configuration for the feedback options + * @deprecated Use {@link SentryUserFeedbackForm.Builder#Builder(Context, int, + * SentryUserFeedbackForm.OptionsConfiguration)} instead. */ + @Deprecated public Builder( final @NotNull Context context, final int themeResId, final @Nullable OptionsConfiguration configuration) { - this.context = context; - this.themeResId = themeResId; - this.configuration = configuration; + super(context, themeResId, 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. - */ + @Deprecated + @Override public Builder configurator( final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { - this.configurator = configurator; + super.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. - */ + @Deprecated + @Override public Builder associatedEventId(final @Nullable SentryId associatedEventId) { - this.associatedEventId = associatedEventId; + super.associatedEventId(associatedEventId); return this; } - /** - * Builds a new {@link SentryUserFeedbackDialog} with the specified context, theme, and - * configuration. - * - * @return a new instance of {@link SentryUserFeedbackDialog} - */ + @Deprecated + @Override public SentryUserFeedbackDialog create() { return new SentryUserFeedbackDialog( context, themeResId, associatedEventId, configuration, configurator); } } - /** Configuration callback for feedback options. */ - public interface OptionsConfiguration { - - /** - * configure the feedback options - * - * @param context the context of the feedback dialog - * @param options the feedback options - */ - void configure(final @NotNull Context context, final @NotNull SentryFeedbackOptions options); - } + /** + * @deprecated Use {@link SentryUserFeedbackForm.OptionsConfiguration} instead. + */ + @Deprecated + public interface OptionsConfiguration extends SentryUserFeedbackForm.OptionsConfiguration {} } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java new file mode 100644 index 00000000000..0babe475491 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java @@ -0,0 +1,379 @@ +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.SentryIntegrationPackageStorage; +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 class SentryUserFeedbackForm 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; + + SentryUserFeedbackForm( + 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"); + } + + @Override + public void setCancelable(boolean cancelable) { + super.setCancelable(cancelable); + isCancelable = cancelable; + } + + @Override + @SuppressWarnings("deprecation") + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.sentry_dialog_user_feedback); + setCancelable(isCancelable); + + final @NotNull SentryFeedbackOptions feedbackOptions = + new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions()); + 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); + 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.getUsername()); + 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 (associatedEventId != null) { + feedback.setAssociatedEventId(associatedEventId); + } + 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.feedback().capture(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(); + } + + public static class Builder { + + @Nullable OptionsConfiguration configuration; + @Nullable SentryFeedbackOptions.OptionsConfigurator configurator; + @Nullable SentryId associatedEventId; + final @NotNull Context context; + final int themeResId; + + /** + * Creates a builder for a {@link SentryUserFeedbackForm} that uses the default alert dialog + * theme. + * + *

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 + */ + public Builder(final @NotNull Context context) { + this(context, 0); + } + + /** + * Creates a builder for a {@link SentryUserFeedbackForm} that uses an explicit theme resource. + * + *

The specified theme resource ({@code themeResId}) is applied on top of the parent {@code + * context}'s theme. It may be specified as a style resource containing a fully-populated theme, + * such as {@link android.R.style#Theme_Material_Dialog}, to replace all attributes in the + * parent {@code context}'s theme including primary and accent colors. + * + *

To preserve attributes such as primary and accent colors, the {@code themeResId} may + * instead be specified as an overlay theme such as {@link + * android.R.style#ThemeOverlay_Material_Dialog}. This will override only the window attributes + * necessary to style the alert window as a dialog. + * + *

Alternatively, the {@code themeResId} may be specified as {@code 0} to use the parent + * {@code context}'s resolved value for {@link android.R.attr#alertDialogTheme}. + * + * @param context the parent context + * @param themeResId the resource ID of the theme against which to inflate this dialog, or + * {@code 0} to use the parent {@code context}'s default alert dialog theme + */ + public Builder(Context context, int themeResId) { + this(context, themeResId, null); + } + + /** + * Creates a builder for a {@link SentryUserFeedbackForm} 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 configuration the configuration for the feedback options, can be {@code null} to use + * the global feedback options. + */ + public Builder( + final @NotNull Context context, final @Nullable OptionsConfiguration configuration) { + this(context, 0, configuration); + } + + /** + * Creates a builder for a {@link SentryUserFeedbackForm} that uses an explicit theme resource. + * The {@code configuration} can be used to configure the feedback options for this specific + * dialog. + * + *

The specified theme resource ({@code themeResId}) is applied on top of the parent {@code + * context}'s theme. It may be specified as a style resource containing a fully-populated theme, + * such as {@link android.R.style#Theme_Material_Dialog}, to replace all attributes in the + * parent {@code context}'s theme including primary and accent colors. + * + *

To preserve attributes such as primary and accent colors, the {@code themeResId} may + * instead be specified as an overlay theme such as {@link + * android.R.style#ThemeOverlay_Material_Dialog}. This will override only the window attributes + * necessary to style the alert window as a dialog. + * + *

Alternatively, the {@code themeResId} may be specified as {@code 0} to use the parent + * {@code context}'s resolved value for {@link android.R.attr#alertDialogTheme}. + * + * @param context the parent context + * @param themeResId the resource ID of the theme against which to inflate this dialog, or + * {@code 0} to use the parent {@code context}'s default alert dialog theme + * @param configuration the configuration for the feedback options, can be {@code null} to use + * the global feedback options. + */ + public Builder( + final @NotNull Context context, + final int themeResId, + 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; + } + + /** + * Builds a new {@link SentryUserFeedbackForm} with the specified context, theme, and + * configuration. + * + * @return a new instance of {@link SentryUserFeedbackForm} + */ + public SentryUserFeedbackForm create() { + return new SentryUserFeedbackForm( + context, themeResId, associatedEventId, configuration, configurator); + } + } + + /** Configuration callback for feedback options. */ + public interface OptionsConfiguration { + + /** + * configure the feedback options + * + * @param context the context of the feedback dialog + * @param options the feedback options + */ + void configure(final @NotNull Context context, final @NotNull SentryFeedbackOptions options); + } +} 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 be54bf7768b..f8724d286f8 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 @@ -18,7 +18,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.SentryAndroidOptions.AndroidUserFeedbackFormHandler import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator @@ -882,9 +882,9 @@ class AndroidOptionsInitializerTest { } @Test - fun `AndroidUserFeedbackIDialogHandler is set as feedback dialog handler`() { + fun `AndroidUserFeedbackFormHandler is set as feedback form handler`() { fixture.initSut() - assertIs(fixture.sentryOptions.feedbackOptions.dialogHandler) + assertIs(fixture.sentryOptions.feedbackOptions.formHandler) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/FeedbackShakeIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/FeedbackShakeIntegrationTest.kt index cb940686c30..bddc9395c0d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/FeedbackShakeIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/FeedbackShakeIntegrationTest.kt @@ -22,10 +22,10 @@ class FeedbackShakeIntegrationTest { val scopes = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } val activity = mock() - val dialogHandler = mock() + val formHandler = mock() init { - options.feedbackOptions.setDialogHandler(dialogHandler) + options.feedbackOptions.setFormHandler(formHandler) } fun getSut(useShakeGesture: Boolean = true): FeedbackShakeIntegration { 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/SentryUserFeedbackFormTest.kt similarity index 94% rename from sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackDialogTest.kt rename to sentry-android-core/src/test/java/io/sentry/android/core/SentryUserFeedbackFormTest.kt index bd60859c608..04f6a35716b 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/SentryUserFeedbackFormTest.kt @@ -26,7 +26,7 @@ import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) -class SentryUserFeedbackDialogTest { +class SentryUserFeedbackFormTest { class Fixture { val application: Context = ApplicationProvider.getApplicationContext() private val mockDsn = "http://key@localhost/proj" @@ -55,10 +55,10 @@ class SentryUserFeedbackDialogTest { fun getSut( associatedEventId: SentryId? = null, - configuration: SentryUserFeedbackDialog.OptionsConfiguration? = null, + configuration: SentryUserFeedbackForm.OptionsConfiguration? = null, configurator: SentryFeedbackOptions.OptionsConfigurator? = null, - ): SentryUserFeedbackDialog = - SentryUserFeedbackDialog(application, 0, associatedEventId, configuration, configurator) + ): SentryUserFeedbackForm = + SentryUserFeedbackForm(application, 0, associatedEventId, configuration, configurator) } private val fixture = Fixture() 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 39dfae40203..bfcaf2845b3 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 @@ -28,7 +28,7 @@ import io.sentry.SentryOptions import io.sentry.android.core.AndroidLogger import io.sentry.android.core.R import io.sentry.android.core.SentryUserFeedbackButton -import io.sentry.android.core.SentryUserFeedbackDialog +import io.sentry.android.core.SentryUserFeedbackForm import io.sentry.assertEnvelopeFeedback import io.sentry.protocol.SentryId import io.sentry.protocol.User @@ -49,21 +49,21 @@ class UserFeedbackUiTest : BaseUiTest() { @Test fun userFeedbackNotShownWhenSdkDisabled() { launchActivity().onActivity { - SentryUserFeedbackDialog.Builder(it).create().show() + SentryUserFeedbackForm.Builder(it).create().show() } onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist()) } @Test fun userFeedbackNotShownWhenSdkDisabledViaApi() { - launchActivity().onActivity { Sentry.showUserFeedbackDialog() } + launchActivity().onActivity { Sentry.feedback().show() } onView(withId(R.id.sentry_dialog_user_feedback_layout)).check(doesNotExist()) } @Test fun userFeedbackShownViaApi() { initSentry() - launchActivity().onActivity { Sentry.showUserFeedbackDialog() } + launchActivity().onActivity { Sentry.feedback().show() } onView(withId(R.id.sentry_dialog_user_feedback_layout)) .inRoot(isDialog()) @@ -639,12 +639,12 @@ class UserFeedbackUiTest : BaseUiTest() { private fun showDialogAndCheck( associatedEventId: SentryId? = null, - checker: (dialog: SentryUserFeedbackDialog) -> Unit = {}, + checker: (dialog: SentryUserFeedbackForm) -> Unit = {}, ) { - lateinit var dialog: SentryUserFeedbackDialog + lateinit var dialog: SentryUserFeedbackForm val feedbackScenario = launchActivity() feedbackScenario.onActivity { - dialog = SentryUserFeedbackDialog.Builder(it).associatedEventId(associatedEventId).create() + dialog = SentryUserFeedbackForm.Builder(it).associatedEventId(associatedEventId).create() dialog.show() } 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 93460b88893..0836c826d32 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryUserFeedbackButton.kt @@ -21,7 +21,7 @@ public fun SentryUserFeedbackButton( text: String = "Report a Bug", configurator: SentryFeedbackOptions.OptionsConfigurator? = null, ) { - Button(modifier = modifier, onClick = { Sentry.showUserFeedbackDialog(configurator) }) { + Button(modifier = modifier, onClick = { Sentry.feedback().show(configurator) }) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b9cbb2ae1b2..8bd1e90e094 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -664,6 +664,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -740,6 +741,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -834,6 +836,15 @@ public abstract interface class io/sentry/IEnvelopeSender { public abstract fun processEnvelopeFile (Ljava/lang/String;Lio/sentry/Hint;)V } +public abstract interface class io/sentry/IFeedbackApi { + public abstract fun capture (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public abstract fun capture (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public abstract fun capture (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public abstract fun show ()V + public abstract fun show (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public abstract fun show (Lio/sentry/protocol/SentryId;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V +} + public abstract interface class io/sentry/IHub : io/sentry/IScopes { } @@ -1012,6 +1023,7 @@ public abstract interface class io/sentry/IScopes { public abstract fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public abstract fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public abstract fun endSession ()V + public abstract fun feedback ()Lio/sentry/IFeedbackApi; public abstract fun flush (J)V public abstract fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public abstract fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -1569,6 +1581,16 @@ public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } +public final class io/sentry/NoOpFeedbackApi : io/sentry/IFeedbackApi { + public fun capture (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public fun capture (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun capture (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public static fun getInstance ()Lio/sentry/NoOpFeedbackApi; + public fun show ()V + public fun show (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V + public fun show (Lio/sentry/protocol/SentryId;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V +} + public final class io/sentry/NoOpHub : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V @@ -1595,6 +1617,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -1781,6 +1804,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -2517,6 +2541,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -2596,6 +2621,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun configureScope (Lio/sentry/ScopeType;Lio/sentry/ScopeCallback;)V public fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public fun endSession ()V + public fun feedback ()Lio/sentry/IFeedbackApi; public fun flush (J)V public fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -2720,6 +2746,7 @@ public final class io/sentry/Sentry { public static fun continueTrace (Ljava/lang/String;Ljava/util/List;)Lio/sentry/TransactionContext; public static fun distribution ()Lio/sentry/IDistributionApi; public static fun endSession ()V + public static fun feedback ()Lio/sentry/IFeedbackApi; public static fun flush (J)V public static fun forkedCurrentScope (Ljava/lang/String;)Lio/sentry/IScopes; public static fun forkedRootScopes (Ljava/lang/String;)Lio/sentry/IScopes; @@ -3151,12 +3178,11 @@ public final class io/sentry/SentryExecutorService : io/sentry/ISentryExecutorSe } public final class io/sentry/SentryFeedbackOptions { - 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 getFormHandler ()Lio/sentry/SentryFeedbackOptions$IFormHandler; public fun getFormTitle ()Ljava/lang/CharSequence; public fun getIsRequiredLabel ()Ljava/lang/CharSequence; public fun getMessageLabel ()Ljava/lang/CharSequence; @@ -3177,10 +3203,10 @@ public final class io/sentry/SentryFeedbackOptions { public fun isUseSentryUser ()Z public fun isUseShakeGesture ()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 + public fun setFormHandler (Lio/sentry/SentryFeedbackOptions$IFormHandler;)V public fun setFormTitle (Ljava/lang/CharSequence;)V public fun setIsRequiredLabel (Ljava/lang/CharSequence;)V public fun setMessageLabel (Ljava/lang/CharSequence;)V @@ -3202,8 +3228,8 @@ 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/protocol/SentryId;Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)V +public abstract interface class io/sentry/SentryFeedbackOptions$IFormHandler { + public abstract fun showForm (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/FeedbackApi.java b/sentry/src/main/java/io/sentry/FeedbackApi.java new file mode 100644 index 00000000000..b8b8a3c9b9a --- /dev/null +++ b/sentry/src/main/java/io/sentry/FeedbackApi.java @@ -0,0 +1,51 @@ +package io.sentry; + +import io.sentry.protocol.Feedback; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class FeedbackApi implements IFeedbackApi { + + private final @NotNull IScopes scopes; + + FeedbackApi(final @NotNull IScopes scopes) { + this.scopes = scopes; + } + + @Override + public void show() { + show(null, null); + } + + @Override + public void show(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + show(null, configurator); + } + + @Override + public void show( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { + final @NotNull SentryOptions options = scopes.getOptions(); + options.getFeedbackOptions().getFormHandler().showForm(associatedEventId, configurator); + } + + @Override + public @NotNull SentryId capture(final @NotNull Feedback feedback) { + return scopes.captureFeedback(feedback); + } + + @Override + public @NotNull SentryId capture(final @NotNull Feedback feedback, final @Nullable Hint hint) { + return scopes.captureFeedback(feedback, hint); + } + + @Override + public @NotNull SentryId capture( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback callback) { + return scopes.captureFeedback(feedback, hint, callback); + } +} diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index cf90eb1fe65..5e2d91a9ae8 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -395,6 +395,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().metrics(); } + @Override + public @NotNull IFeedbackApi feedback() { + return Sentry.getCurrentScopes().feedback(); + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) { Sentry.setAttribute(key, value); diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 66a34b4dc36..00395292fd5 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -380,6 +380,11 @@ public void reportFullyDisplayed() { return scopes.metrics(); } + @Override + public @NotNull IFeedbackApi feedback() { + return scopes.feedback(); + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) { scopes.setAttribute(key, value); diff --git a/sentry/src/main/java/io/sentry/IFeedbackApi.java b/sentry/src/main/java/io/sentry/IFeedbackApi.java new file mode 100644 index 00000000000..5bab630fa8b --- /dev/null +++ b/sentry/src/main/java/io/sentry/IFeedbackApi.java @@ -0,0 +1,29 @@ +package io.sentry; + +import io.sentry.protocol.Feedback; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface IFeedbackApi { + + void show(); + + void show(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); + + void show( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); + + @NotNull + SentryId capture(final @NotNull Feedback feedback); + + @NotNull + SentryId capture(final @NotNull Feedback feedback, final @Nullable Hint hint); + + @NotNull + SentryId capture( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback callback); +} diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index b1b437f72e5..26ea0dcc3ea 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -217,7 +217,10 @@ SentryId captureException( * Captures a manually created user feedback and sends it to Sentry. * * @param userFeedback The user feedback to send to Sentry. + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#capture(io.sentry.protocol.Feedback) + * capture(feedback)} with the new {@link io.sentry.protocol.Feedback} type instead. */ + @Deprecated void captureUserFeedback(@NotNull UserFeedback userFeedback); /** Starts a new session. If there's a running session, it ends it before starting the new one. */ @@ -748,6 +751,9 @@ default boolean isNoOp() { @NotNull IMetricsApi metrics(); + @NotNull + IFeedbackApi feedback(); + /** * Sets an attribute. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 98b6034bb78..2a1df15f812 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -174,7 +174,10 @@ SentryId captureReplayEvent( * Captures a manually created user feedback and sends it to Sentry. * * @param userFeedback The user feedback to send to Sentry. + * @deprecated Use {@link IFeedbackApi#capture(io.sentry.protocol.Feedback)} with the new {@link + * io.sentry.protocol.Feedback} type instead. */ + @Deprecated void captureUserFeedback(@NotNull UserFeedback userFeedback); /** diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index a0fa80879aa..2b24090d0cc 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -72,6 +72,7 @@ public final class JsonSerializer implements ISerializer { /** * All our custom deserializers need to be registered to be used with the deserializer instance. * */ + @SuppressWarnings("deprecation") public JsonSerializer(@NotNull SentryOptions options) { this.options = options; diff --git a/sentry/src/main/java/io/sentry/NoOpFeedbackApi.java b/sentry/src/main/java/io/sentry/NoOpFeedbackApi.java new file mode 100644 index 00000000000..bdef5d37590 --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpFeedbackApi.java @@ -0,0 +1,46 @@ +package io.sentry; + +import io.sentry.protocol.Feedback; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NoOpFeedbackApi implements IFeedbackApi { + + private static final NoOpFeedbackApi instance = new NoOpFeedbackApi(); + + private NoOpFeedbackApi() {} + + public static NoOpFeedbackApi getInstance() { + return instance; + } + + @Override + public void show() {} + + @Override + public void show(final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {} + + @Override + public void show( + final @Nullable SentryId associatedEventId, + final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {} + + @Override + public @NotNull SentryId capture(final @NotNull Feedback feedback) { + return SentryId.EMPTY_ID; + } + + @Override + public @NotNull SentryId capture(final @NotNull Feedback feedback, final @Nullable Hint hint) { + return SentryId.EMPTY_ID; + } + + @Override + public @NotNull SentryId capture( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback callback) { + return SentryId.EMPTY_ID; + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 4a02be1bd40..d5ef143d342 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -80,6 +80,7 @@ public boolean isEnabled() { return SentryId.EMPTY_ID; } + @Deprecated @Override public void captureUserFeedback(@NotNull UserFeedback userFeedback) {} @@ -338,6 +339,11 @@ public boolean isNoOp() { return NoOpMetricsApi.getInstance(); } + @Override + public @NotNull IFeedbackApi feedback() { + return NoOpFeedbackApi.getInstance(); + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) {} diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 1ae357d502e..345c74cc0ee 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -77,6 +77,7 @@ public boolean isEnabled() { return SentryId.EMPTY_ID; } + @Deprecated @Override public void captureUserFeedback(@NotNull UserFeedback userFeedback) {} @@ -336,6 +337,11 @@ public boolean isNoOp() { return NoOpMetricsApi.getInstance(); } + @Override + public @NotNull IFeedbackApi feedback() { + return NoOpFeedbackApi.getInstance(); + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) {} diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 961ef9031be..ac9ff2344bf 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -44,6 +44,7 @@ public void flush(long timeoutMillis) {} return SentryId.EMPTY_ID; } + @Deprecated @Override public void captureUserFeedback(@NotNull UserFeedback userFeedback) {} diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 82c03feac4b..3b67b94916e 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -34,6 +34,7 @@ public final class Scopes implements IScopes { private final @NotNull CombinedScopeView combinedScope; private final @NotNull ILoggerApi logger; private final @NotNull IMetricsApi metrics; + private final @NotNull IFeedbackApi feedbackApi; public Scopes( final @NotNull IScope scope, @@ -61,6 +62,7 @@ private Scopes( this.compositePerformanceCollector = options.getCompositePerformanceCollector(); this.logger = new LoggerApi(this); this.metrics = new MetricsApi(this); + this.feedbackApi = new FeedbackApi(this); } public @NotNull String getCreator() { @@ -344,6 +346,7 @@ private void assignTraceContext(final @NotNull SentryEvent event) { return sentryId; } + @Deprecated @Override public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { if (!isEnabled()) { @@ -1245,6 +1248,11 @@ public void reportFullyDisplayed() { return metrics; } + @Override + public @NotNull IFeedbackApi feedback() { + return feedbackApi; + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) { if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index b66b681a332..b697a950501 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -51,18 +51,18 @@ public boolean isEnabled() { @Override public @NotNull SentryId captureFeedback(@NotNull Feedback feedback) { - return Sentry.captureFeedback(feedback); + return Sentry.feedback().capture(feedback); } @Override public @NotNull SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint) { - return Sentry.captureFeedback(feedback, hint); + return Sentry.feedback().capture(feedback, hint); } @Override public @NotNull SentryId captureFeedback( @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { - return Sentry.captureFeedback(feedback, hint, callback); + return Sentry.feedback().capture(feedback, hint, callback); } @ApiStatus.Internal @@ -82,6 +82,7 @@ public boolean isEnabled() { return Sentry.captureException(throwable, hint, callback); } + @Deprecated @Override public void captureUserFeedback(@NotNull UserFeedback userFeedback) { Sentry.captureUserFeedback(userFeedback); @@ -392,6 +393,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().metrics(); } + @Override + public @NotNull IFeedbackApi feedback() { + return Sentry.getCurrentScopes().feedback(); + } + @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) { Sentry.setAttribute(key, value); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index fee19dc4d09..919607e5879 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -827,40 +827,37 @@ public static void close() { } /** - * Captures the feedback. - * - * @param feedback The feedback to send. - * @return The Id (SentryId object) of the event + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#capture(Feedback) capture(feedback)} + * instead. */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static @NotNull SentryId captureFeedback(final @NotNull Feedback feedback) { - return getCurrentScopes().captureFeedback(feedback); + return feedback().capture(feedback); } /** - * Captures the feedback. - * - * @param feedback The feedback to send. - * @param hint An optional hint to be applied to the event. - * @return The Id (SentryId object) of the event + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#capture(Feedback, Hint) + * capture(feedback, hint)} instead. */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static @NotNull SentryId captureFeedback( final @NotNull Feedback feedback, final @Nullable Hint hint) { - return getCurrentScopes().captureFeedback(feedback, hint); + return feedback().capture(feedback, hint); } /** - * Captures the feedback. - * - * @param feedback The feedback to send. - * @param hint An optional hint to be applied to the event. - * @param callback The callback to configure the scope for a single invocation. - * @return The Id (SentryId object) of the event + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#capture(Feedback, Hint, ScopeCallback) + * capture(feedback, hint, callback)} instead. */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static @NotNull SentryId captureFeedback( final @NotNull Feedback feedback, final @Nullable Hint hint, final @Nullable ScopeCallback callback) { - return getCurrentScopes().captureFeedback(feedback, hint, callback); + return feedback().capture(feedback, hint, callback); } /** @@ -916,7 +913,11 @@ public static void close() { * Captures a manually created user feedback and sends it to Sentry. * * @param userFeedback The user feedback to send to Sentry. + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#capture(Feedback) capture(feedback)} + * with the new {@link Feedback} type instead. */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static void captureUserFeedback(final @NotNull UserFeedback userFeedback) { getCurrentScopes().captureUserFeedback(userFeedback); } @@ -1355,20 +1356,41 @@ public static IMetricsApi metrics() { return getCurrentScopes().metrics(); } + @NotNull + public static IFeedbackApi feedback() { + return getCurrentScopes().feedback(); + } + + /** + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#show() show()} instead. + */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static void showUserFeedbackDialog() { - showUserFeedbackDialog(null); + feedback().show(); } + /** + * @deprecated Use {@link #feedback()}.{@link + * IFeedbackApi#show(SentryFeedbackOptions.OptionsConfigurator) show(configurator)} instead. + */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static void showUserFeedbackDialog( final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { - showUserFeedbackDialog(null, configurator); + feedback().show(configurator); } + /** + * @deprecated Use {@link #feedback()}.{@link IFeedbackApi#show(SentryId, + * SentryFeedbackOptions.OptionsConfigurator) show(associatedEventId, configurator)} instead. + */ + @Deprecated + @SuppressWarnings("InlineMeSuggester") public static void showUserFeedbackDialog( final @Nullable SentryId associatedEventId, final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) { - final @NotNull SentryOptions options = getCurrentScopes().getOptions(); - options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); + feedback().show(associatedEventId, configurator); } /** diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index b8178e35517..c99fcaeaa2f 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -688,6 +688,7 @@ private SentryEvent processFeedbackEvent( return feedbackEvent; } + @Deprecated @Override public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { Objects.requireNonNull(userFeedback, "SentryEvent is required."); @@ -714,6 +715,7 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { } } + @Deprecated private @NotNull SentryEnvelope buildEnvelope(final @NotNull UserFeedback userFeedback) { final List envelopeItems = new ArrayList<>(); diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index dd47d2b99d0..dbbc36524db 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -169,6 +169,7 @@ public final class SentryEnvelopeItem { } } + @Deprecated public static SentryEnvelopeItem fromUserFeedback( final @NotNull ISerializer serializer, final @NotNull UserFeedback userFeedback) { Objects.requireNonNull(serializer, "ISerializer is required."); diff --git a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java index 2a0ead54234..a72b352317e 100644 --- a/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java +++ b/sentry/src/main/java/io/sentry/SentryFeedbackOptions.java @@ -91,10 +91,10 @@ public final class SentryFeedbackOptions { /** Callback called when there is an error submitting feedback via the prepared form. */ private @Nullable SentryFeedbackCallback onSubmitError; - private @NotNull IDialogHandler iDialogHandler; + private @NotNull IFormHandler iFormHandler; - public SentryFeedbackOptions(@NotNull IDialogHandler iDialogHandler) { - this.iDialogHandler = iDialogHandler; + SentryFeedbackOptions(@NotNull IFormHandler iFormHandler) { + this.iFormHandler = iFormHandler; } /** Creates a copy of the passed {@link SentryFeedbackOptions}. */ @@ -121,7 +121,7 @@ public SentryFeedbackOptions(final @NotNull SentryFeedbackOptions other) { this.onFormClose = other.onFormClose; this.onSubmitSuccess = other.onSubmitSuccess; this.onSubmitError = other.onSubmitError; - this.iDialogHandler = other.iDialogHandler; + this.iFormHandler = other.iFormHandler; } /** @@ -535,23 +535,23 @@ public void setOnSubmitError(final @Nullable SentryFeedbackCallback onSubmitErro } /** - * Sets the dialog handler to be used to show the feedback form. + * Sets the form handler to be used to show the feedback form. * - * @param iDialogHandler the dialog handler to be used to show the feedback form + * @param iFormHandler the form handler to be used to show the feedback form */ @ApiStatus.Internal - public void setDialogHandler(final @NotNull IDialogHandler iDialogHandler) { - this.iDialogHandler = iDialogHandler; + public void setFormHandler(final @NotNull IFormHandler iFormHandler) { + this.iFormHandler = iFormHandler; } /** - * Gets the dialog handler to be used to show the feedback form. + * Gets the form handler to be used to show the feedback form. * - * @return the dialog handler to be used to show the feedback form + * @return the form handler to be used to show the feedback form */ @ApiStatus.Internal - public @NotNull IDialogHandler getDialogHandler() { - return iDialogHandler; + public @NotNull IFormHandler getFormHandler() { + return iFormHandler; } @Override @@ -609,8 +609,8 @@ public interface SentryFeedbackCallback { } @ApiStatus.Internal - public interface IDialogHandler { - void showDialog( + public interface IFormHandler { + void showForm( final @Nullable SentryId associatedEventId, final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator); } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 86086f8816b..a6f78cfad9c 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3397,7 +3397,7 @@ private SentryOptions(final boolean empty) { feedbackOptions = new SentryFeedbackOptions( (associatedEventId, configurator) -> - logger.log(SentryLevel.WARNING, "showDialog() can only be called in Android.")); + logger.log(SentryLevel.WARNING, "showForm() can only be called in Android.")); if (!empty) { setSpanFactory(SpanFactoryFactory.create(new LoadClass(), NoOpLogger.getInstance())); diff --git a/sentry/src/main/java/io/sentry/UserFeedback.java b/sentry/src/main/java/io/sentry/UserFeedback.java index b580744ee77..b9d0ade0f9d 100644 --- a/sentry/src/main/java/io/sentry/UserFeedback.java +++ b/sentry/src/main/java/io/sentry/UserFeedback.java @@ -8,7 +8,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Adds additional information about what happened to an event. */ +/** + * Adds additional information about what happened to an event. + * + * @deprecated Use {@link io.sentry.protocol.Feedback} with {@link Sentry#feedback()}.{@link + * IFeedbackApi#capture(io.sentry.protocol.Feedback) capture(feedback)} instead. + */ +@Deprecated public final class UserFeedback implements JsonUnknown, JsonSerializable { private final SentryId eventId; diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 9b97d8935f7..0dbb6a43c11 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -14,6 +14,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class HubAdapterTest { val scopes: IScopes = mock() @@ -63,6 +64,7 @@ class HubAdapterTest { val hint = Hint() val scopeCallback = mock() val feedback = Feedback("message") + whenever(scopes.feedback()).thenReturn(FeedbackApi(scopes)) HubAdapter.getInstance().captureFeedback(feedback) verify(scopes).captureFeedback(eq(feedback)) diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index 43cb5b155a7..1de22cfd3c3 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -14,6 +14,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.reset import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever class ScopesAdapterTest { val scopes: IScopes = mock() @@ -63,6 +64,7 @@ class ScopesAdapterTest { val scopeCallback = mock() val hint = mock() val feedback = Feedback("message") + whenever(scopes.feedback()).thenReturn(FeedbackApi(scopes)) ScopesAdapter.getInstance().captureFeedback(feedback) verify(scopes).captureFeedback(eq(feedback)) diff --git a/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt index a50aff02dc5..e4b96cb17d0 100644 --- a/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryFeedbackOptionsTest.kt @@ -1,6 +1,6 @@ package io.sentry -import io.sentry.SentryFeedbackOptions.IDialogHandler +import io.sentry.SentryFeedbackOptions.IFormHandler import kotlin.test.Test import kotlin.test.assertEquals import org.mockito.kotlin.mock @@ -8,7 +8,7 @@ import org.mockito.kotlin.mock class SentryFeedbackOptionsTest { @Test fun `feedback options is initialized with default values`() { - val options = SentryFeedbackOptions(mock()) + val options = SentryFeedbackOptions(mock()) assertEquals(false, options.isNameRequired) assertEquals(true, options.isShowName) assertEquals(false, options.isEmailRequired) @@ -35,7 +35,7 @@ class SentryFeedbackOptionsTest { @Test fun `feedback options copy constructor`() { val options = - SentryFeedbackOptions(mock()).apply { + SentryFeedbackOptions(mock()).apply { isNameRequired = true isShowName = false isEmailRequired = true @@ -80,6 +80,6 @@ class SentryFeedbackOptionsTest { assertEquals(options.onFormClose, optionsCopy.onFormClose) assertEquals(options.onSubmitSuccess, optionsCopy.onSubmitSuccess) assertEquals(options.onSubmitError, optionsCopy.onSubmitError) - assertEquals(options.dialogHandler, optionsCopy.dialogHandler) + assertEquals(options.formHandler, optionsCopy.formHandler) } } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index da014b30f74..e08d0ed8f72 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -907,8 +907,8 @@ class SentryOptionsTest { setLogger(logger) isDebug = true } - options.feedbackOptions.dialogHandler.showDialog(mock(), mock()) - verify(logger).log(eq(SentryLevel.WARNING), eq("showDialog() can only be called in Android.")) + options.feedbackOptions.formHandler.showForm(mock(), mock()) + verify(logger).log(eq(SentryLevel.WARNING), eq("showForm() can only be called in Android.")) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 25f45816b74..3712b083de7 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1,6 +1,6 @@ package io.sentry -import io.sentry.SentryFeedbackOptions.IDialogHandler +import io.sentry.SentryFeedbackOptions.IFormHandler import io.sentry.SentryOptions.ProfilesSamplerCallback import io.sentry.SentryOptions.TracesSamplerCallback import io.sentry.backpressure.BackpressureMonitor @@ -1504,39 +1504,39 @@ class SentryTest { } @Test - fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler`() { - val mockDialogHandler = mock() + fun `feedback show forwards to feedbackOptions_formHandler`() { + val mockFormHandler = mock() initForTest { it.dsn = dsn - it.feedbackOptions.dialogHandler = mockDialogHandler + it.feedbackOptions.setFormHandler(mockFormHandler) } - Sentry.showUserFeedbackDialog() - verify(mockDialogHandler).showDialog(eq(null), eq(null)) + Sentry.feedback().show() + verify(mockFormHandler).showForm(eq(null), eq(null)) } @Test - fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler with configurator`() { - val mockDialogHandler = mock() + fun `feedback show forwards to feedbackOptions_formHandler with configurator`() { + val mockFormHandler = mock() val configurator = mock() initForTest { it.dsn = dsn - it.feedbackOptions.dialogHandler = mockDialogHandler + it.feedbackOptions.setFormHandler(mockFormHandler) } - Sentry.showUserFeedbackDialog(configurator) - verify(mockDialogHandler).showDialog(eq(null), eq(configurator)) + Sentry.feedback().show(configurator) + verify(mockFormHandler).showForm(eq(null), eq(configurator)) } @Test - fun `showUserFeedbackDialog forwards to feedbackOptions_dialogHandler with associatedEventId and configurator`() { - val mockDialogHandler = mock() + fun `feedback show forwards to feedbackOptions_formHandler with associatedEventId and configurator`() { + val mockFormHandler = mock() val configurator = mock() val associatedEventId = SentryId() initForTest { it.dsn = dsn - it.feedbackOptions.dialogHandler = mockDialogHandler + it.feedbackOptions.setFormHandler(mockFormHandler) } - Sentry.showUserFeedbackDialog(associatedEventId, configurator) - verify(mockDialogHandler).showDialog(eq(associatedEventId), eq(configurator)) + Sentry.feedback().show(associatedEventId, configurator) + verify(mockFormHandler).showForm(eq(associatedEventId), eq(configurator)) } @Test