From 62e71ff8ab0a2e619a3f775603b52aec94776a71 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 14 Mar 2025 12:42:21 +0100 Subject: [PATCH 01/35] added Feedback class and extended Contexts with it --- .../java/io/sentry/protocol/Contexts.java | 13 ++ .../java/io/sentry/protocol/Feedback.java | 172 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/protocol/Feedback.java diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index 123436cfae..563a4f6d97 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -51,6 +51,8 @@ public Contexts(final @NotNull Contexts contexts) { this.setOperatingSystem(new OperatingSystem((OperatingSystem) value)); } else if (SentryRuntime.TYPE.equals(entry.getKey()) && value instanceof SentryRuntime) { this.setRuntime(new SentryRuntime((SentryRuntime) value)); + } else if (Feedback.TYPE.equals(entry.getKey()) && value instanceof Feedback) { + this.setFeedback(new Feedback((Feedback) value)); } else if (Gpu.TYPE.equals(entry.getKey()) && value instanceof Gpu) { this.setGpu(new Gpu((Gpu) value)); } else if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { @@ -120,6 +122,14 @@ public void setRuntime(final @NotNull SentryRuntime runtime) { this.put(SentryRuntime.TYPE, runtime); } + public @Nullable Feedback getFeedback() { + return toContextType(Feedback.TYPE, Feedback.class); + } + + public void setFeedback(final @NotNull Feedback feedback) { + this.put(Feedback.TYPE, feedback); + } + public @Nullable Gpu getGpu() { return toContextType(Gpu.TYPE, Gpu.class); } @@ -302,6 +312,9 @@ public static final class Deserializer implements JsonDeserializer { case SentryRuntime.TYPE: contexts.setRuntime(new SentryRuntime.Deserializer().deserialize(reader, logger)); break; + case Feedback.TYPE: + contexts.setFeedback(new Feedback.Deserializer().deserialize(reader, logger)); + break; case SpanContext.TYPE: contexts.setTrace(new SpanContext.Deserializer().deserialize(reader, logger)); break; diff --git a/sentry/src/main/java/io/sentry/protocol/Feedback.java b/sentry/src/main/java/io/sentry/protocol/Feedback.java new file mode 100644 index 0000000000..f62d4b9dc7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/Feedback.java @@ -0,0 +1,172 @@ +package io.sentry.protocol; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import io.sentry.ILogger; +import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectReader; +import io.sentry.ObjectWriter; +import io.sentry.SentryLevel; +import io.sentry.vendor.gson.stream.JsonToken; + +// Specs can be found at https://develop.sentry.dev/sdk/data-model/envelope-items/#user-feedback + +public class Feedback implements JsonUnknown, JsonSerializable { + public static final String TYPE = "feedback"; + + final @NotNull String message; + final @Nullable String contactEmail; + final @Nullable String name; + final @Nullable SentryId associatedEventId; + final @Nullable SentryId replayId; + final @Nullable String url; + + private @Nullable Map unknown; + + public Feedback( + final @NotNull String message, + final @Nullable String contactEmail, + final @Nullable String name, + final @Nullable SentryId associatedEventId, + final @Nullable SentryId replayId, + final @Nullable String url) { + this.message = message; + this.contactEmail = contactEmail; + this.name = name; + this.associatedEventId = associatedEventId; + this.replayId = replayId; + this.url = url; + } + + public Feedback(final @NotNull Feedback feedback) { + this.message = feedback.message; + this.contactEmail = feedback.contactEmail; + this.name = feedback.name; + this.associatedEventId = feedback.associatedEventId; + this.replayId = feedback.replayId; + this.url = feedback.url; + this.unknown = feedback.unknown; + } + + // JsonKeys + + public static final class JsonKeys { + public static final String MESSAGE = "message"; + public static final String CONTACT_EMAIL = "contact_email"; + public static final String NAME = "name"; + public static final String ASSOCIATED_EVENT_ID = "associated_event_id"; + public static final String REPLAY_ID = "replay_id"; + public static final String URL = "url"; + } + + // JsonUnknown + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + // JsonSerializable + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.MESSAGE).value(message); + if (contactEmail != null) { + writer.name(JsonKeys.CONTACT_EMAIL).value(contactEmail); + } + if (name != null) { + writer.name(JsonKeys.NAME).value(name); + } + if (associatedEventId != null) { + writer.name(JsonKeys.ASSOCIATED_EVENT_ID); + associatedEventId.serialize(writer, logger); + } + if (replayId != null) { + writer.name(JsonKeys.REPLAY_ID); + replayId.serialize(writer, logger); + } + if (url != null) { + writer.name(JsonKeys.URL).value(url); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + // JsonDeserializer + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull Feedback deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + @Nullable String message = null; + @Nullable String contactEmail = null; + @Nullable String name = null; + @Nullable SentryId associatedEventId = null; + @Nullable SentryId replayId = null; + @Nullable String url = null; + @Nullable Map unknown = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.MESSAGE: + message = reader.nextStringOrNull(); + break; + case JsonKeys.CONTACT_EMAIL: + contactEmail = reader.nextStringOrNull(); + break; + case JsonKeys.NAME: + name = reader.nextStringOrNull(); + break; + case JsonKeys.ASSOCIATED_EVENT_ID: + associatedEventId = new SentryId.Deserializer().deserialize(reader, logger); + break; + case JsonKeys.REPLAY_ID: + replayId = new SentryId.Deserializer().deserialize(reader, logger); + break; + case JsonKeys.URL: + url = reader.nextStringOrNull(); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (message == null) { + String errorMessage = "Missing required field \"" + JsonKeys.MESSAGE + "\""; + Exception exception = new IllegalStateException(errorMessage); + logger.log(SentryLevel.ERROR, errorMessage, exception); + throw exception; + } + + Feedback feedback = new Feedback(message, contactEmail, name, associatedEventId, replayId, url); + feedback.setUnknown(unknown); + return feedback; + } + } +} From c9b4850b9e04c50acea04e216644fc00f23031c3 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 25 Mar 2025 11:28:53 +0100 Subject: [PATCH 02/35] Added Sentry.captureFeedback API Added Scopes.captureFeedback API Changed the MainActivity sample app to use the new API --- .../sentry/samples/android/MainActivity.java | 14 ++- sentry/api/sentry.api | 52 +++++++++ .../src/main/java/io/sentry/HubAdapter.java | 7 ++ .../main/java/io/sentry/HubScopesWrapper.java | 7 ++ sentry/src/main/java/io/sentry/IScopes.java | 37 +++++++ .../main/java/io/sentry/ISentryClient.java | 17 +++ .../main/java/io/sentry/JsonSerializer.java | 2 + sentry/src/main/java/io/sentry/NoOpHub.java | 7 ++ .../src/main/java/io/sentry/NoOpScopes.java | 7 ++ sentry/src/main/java/io/sentry/Scopes.java | 32 ++++++ .../main/java/io/sentry/ScopesAdapter.java | 7 ++ sentry/src/main/java/io/sentry/Sentry.java | 38 +++++++ .../src/main/java/io/sentry/SentryClient.java | 3 +- .../main/java/io/sentry/SentryItemType.java | 2 +- .../java/io/sentry/protocol/Feedback.java | 100 ++++++++++++------ 15 files changed, 292 insertions(+), 40 deletions(-) diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index da52c72a68..4b238c76a3 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -8,9 +8,8 @@ import io.sentry.ISpan; import io.sentry.MeasurementUnit; import io.sentry.Sentry; -import io.sentry.UserFeedback; import io.sentry.instrumentation.file.SentryFileOutputStream; -import io.sentry.protocol.SentryId; +import io.sentry.protocol.Feedback; import io.sentry.protocol.User; import io.sentry.samples.android.compose.ComposeActivity; import io.sentry.samples.android.databinding.ActivityMainBinding; @@ -73,13 +72,12 @@ protected void onCreate(Bundle savedInstanceState) { binding.sendUserFeedback.setOnClickListener( view -> { - SentryId sentryId = Sentry.captureException(new Exception("I have feedback")); + Feedback feedback = + new Feedback("It broke on Android. I don't know why, but this happens."); + feedback.setContactEmail("john@me.com"); + feedback.setName("John Me"); - UserFeedback userFeedback = new UserFeedback(sentryId); - userFeedback.setComments("It broke on Android. I don't know why, but this happens."); - userFeedback.setEmail("john@me.com"); - userFeedback.setName("John Me"); - Sentry.captureUserFeedback(userFeedback); + Sentry.captureFeedback(feedback); }); binding.addAttachment.setOnClickListener( diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 5115cace13..8d073677fb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -584,6 +584,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -649,6 +650,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -870,6 +872,9 @@ public abstract interface class io/sentry/IScopes { public abstract fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public abstract fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public abstract fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; @@ -951,6 +956,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -1396,6 +1402,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -1555,6 +1562,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2158,6 +2166,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2222,6 +2231,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2328,6 +2338,9 @@ public final class io/sentry/Sentry { public static fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public static fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public static fun captureException (Ljava/lang/Throwable;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public static fun captureFeedback (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public static fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public static fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public static fun captureMessage (Ljava/lang/String;)Lio/sentry/protocol/SentryId; public static fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public static fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; @@ -4490,6 +4503,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun getApp ()Lio/sentry/protocol/App; public fun getBrowser ()Lio/sentry/protocol/Browser; public fun getDevice ()Lio/sentry/protocol/Device; + public fun getFeedback ()Lio/sentry/protocol/Feedback; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; public fun getResponse ()Lio/sentry/protocol/Response; @@ -4509,6 +4523,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun setApp (Lio/sentry/protocol/App;)V public fun setBrowser (Lio/sentry/protocol/Browser;)V public fun setDevice (Lio/sentry/protocol/Device;)V + public fun setFeedback (Lio/sentry/protocol/Feedback;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V public fun setResponse (Lio/sentry/protocol/Response;)V @@ -4728,6 +4743,43 @@ public final class io/sentry/protocol/Device$JsonKeys { public fun ()V } +public final class io/sentry/protocol/Feedback : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun (Lio/sentry/protocol/Feedback;)V + public fun (Ljava/lang/String;)V + public fun getAssociatedEventId ()Lio/sentry/protocol/SentryId; + public fun getContactEmail ()Ljava/lang/String; + public fun getMessage ()Ljava/lang/String; + public fun getName ()Ljava/lang/String; + public fun getReplayId ()Lio/sentry/protocol/SentryId; + public fun getUnknown ()Ljava/util/Map; + public fun getUrl ()Ljava/lang/String; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setAssociatedEventId (Lio/sentry/protocol/SentryId;)V + public fun setContactEmail (Ljava/lang/String;)V + public fun setMessage (Ljava/lang/String;)V + public fun setName (Ljava/lang/String;)V + public fun setReplayId (Lio/sentry/protocol/SentryId;)V + public fun setUnknown (Ljava/util/Map;)V + public fun setUrl (Ljava/lang/String;)V +} + +public final class io/sentry/protocol/Feedback$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/Feedback; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/Feedback$JsonKeys { + public static final field ASSOCIATED_EVENT_ID Ljava/lang/String; + public static final field CONTACT_EMAIL Ljava/lang/String; + public static final field MESSAGE Ljava/lang/String; + public static final field NAME Ljava/lang/String; + public static final field REPLAY_ID Ljava/lang/String; + public static final field URL Ljava/lang/String; + public fun ()V +} + public final class io/sentry/protocol/Geo : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V public fun (Lio/sentry/protocol/Geo;)V diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 7fba4da99d..a3ec727294 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -50,6 +51,12 @@ public boolean isEnabled() { return Sentry.captureMessage(message, level, callback); } + @Override + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { + return Sentry.captureFeedback(feedback, hint, callback); + } + @ApiStatus.Internal @Override public @NotNull SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint hint) { diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 9ca84df9a1..9ce4a281de 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -46,6 +47,12 @@ public boolean isEnabled() { return scopes.captureMessage(message, level, callback); } + @Override + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { + return scopes.captureFeedback(feedback, hint, callback); + } + @Override public @NotNull SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint hint) { return scopes.captureEnvelope(envelope, hint); diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index c93f558ea5..8f3865c490 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -108,6 +109,42 @@ SentryId captureMessage( return captureMessage(message, SentryLevel.INFO, callback); } + /** + * Captures the feedback. + * + * @param feedback The feedback to send. + * @return The Id (SentryId object) of the event + */ + default @NotNull SentryId captureFeedback(final @NotNull Feedback feedback) { + return captureFeedback(feedback, null); + } + + /** + * 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 + */ + default @NotNull SentryId captureFeedback( + final @NotNull Feedback feedback, final @Nullable Hint hint) { + return captureFeedback(feedback, hint, null); + } + + /** + * 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 + */ + @NotNull + SentryId captureFeedback( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback callback); + /** * Captures an envelope. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 017b283d57..98d97ef98f 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.Message; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -78,6 +79,22 @@ public interface ISentryClient { return captureEvent(event, null, hint); } + /** + * Captures the feedback. + * + * @param feedback The feedback to send. + * @param hint An optional hint to be applied to the event. + * @param scope An optional scope to be applied to the event. + * @return The Id (SentryId object) of the event + */ + default @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable IScope scope) { + SentryEvent event = new SentryEvent(); + event.getContexts().setFeedback(feedback); + + return captureEvent(event, scope, hint); + } + /** * Captures the message. * diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 09773dc617..e7950a9a0f 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -9,6 +9,7 @@ import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.Device; +import io.sentry.protocol.Feedback; import io.sentry.protocol.Geo; import io.sentry.protocol.Gpu; import io.sentry.protocol.MeasurementValue; @@ -84,6 +85,7 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(Device.class, new Device.Deserializer()); deserializersByClass.put( Device.DeviceOrientation.class, new Device.DeviceOrientation.Deserializer()); + deserializersByClass.put(Feedback.class, new Feedback.Deserializer()); deserializersByClass.put(Gpu.class, new Gpu.Deserializer()); deserializersByClass.put(MeasurementValue.class, new MeasurementValue.Deserializer()); deserializersByClass.put(Mechanism.class, new Mechanism.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 9b39ce77a6..e4a7ea16c0 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -53,6 +54,12 @@ public boolean isEnabled() { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint hint) { return SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index a37a730b74..ae8e604c42 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -48,6 +49,12 @@ public boolean isEnabled() { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint hint) { return SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index f34b66680e..2ab4acaf57 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -3,6 +3,7 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.SessionEndHint; import io.sentry.hints.SessionStartHint; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -230,6 +231,37 @@ private IScope buildLocalScope( return sentryId; } + @Override + public @NotNull SentryId captureFeedback( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback scopeCallback) { + SentryId sentryId = SentryId.EMPTY_ID; + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureFeedback' call is a no-op."); + } else if (feedback.getMessage().isEmpty()) { + getOptions() + .getLogger() + .log(SentryLevel.WARNING, "captureFeedback called with empty message."); + } else { + try { + final IScope localScope = buildLocalScope(getCombinedScopeView(), scopeCallback); + + sentryId = getClient().captureFeedback(feedback, hint, localScope); + } catch (Throwable e) { + getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Error while capturing feedback: " + feedback.getMessage(), e); + } + } + updateLastEventId(sentryId); + return sentryId; + } + @ApiStatus.Internal @Override public @NotNull SentryId captureEnvelope( diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index d5d143af52..57b3c02b98 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -46,6 +47,12 @@ public boolean isEnabled() { return Sentry.captureMessage(message, level, callback); } + @Override + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { + return Sentry.captureFeedback(feedback, hint, callback); + } + @ApiStatus.Internal @Override public @NotNull SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint hint) { diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 82459410a4..176365ccbd 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -14,6 +14,7 @@ import io.sentry.internal.modules.NoOpModulesLoader; import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.opentelemetry.OpenTelemetryUtil; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.sentry.transport.NoOpEnvelopeCache; @@ -744,6 +745,43 @@ public static void close() { return getCurrentScopes().captureMessage(message, level, callback); } + /** + * Captures the feedback. + * + * @param feedback The feedback to send. + * @return The Id (SentryId object) of the event + */ + public static @NotNull SentryId captureFeedback(final @NotNull Feedback feedback) { + return getCurrentScopes().captureFeedback(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 + */ + public static @NotNull SentryId captureFeedback( + final @NotNull Feedback feedback, final @Nullable Hint hint) { + return getCurrentScopes().captureFeedback(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 + */ + public static @NotNull SentryId captureFeedback( + final @NotNull Feedback feedback, + final @Nullable Hint hint, + final @Nullable ScopeCallback callback) { + return getCurrentScopes().captureFeedback(feedback, hint, callback); + } + /** * Captures the exception. * diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 277be87aee..466ea7d2b5 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -160,7 +160,8 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul scope != null ? scope.withSession((@Nullable Session session) -> {}) : null; @Nullable Session session = null; - if (event != null) { + // Feedbacks shouldn't be sampled, and they don't affect sessions + if (event != null && event.getContexts().getFeedback() == null) { // https://develop.sentry.dev/sdk/sessions/#terminal-session-states if (sessionBeforeUpdate == null || !sessionBeforeUpdate.isTerminated()) { session = updateSessionData(event, hint, scope); diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index 85acc0aadd..5c2864f449 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -27,7 +27,7 @@ public enum SentryItemType implements JsonSerializable { public static SentryItemType resolve(Object item) { if (item instanceof SentryEvent) { - return Event; + return ((SentryEvent) item).getContexts().getFeedback() == null ? Event : Feedback; } else if (item instanceof SentryTransaction) { return Transaction; } else if (item instanceof Session) { diff --git a/sentry/src/main/java/io/sentry/protocol/Feedback.java b/sentry/src/main/java/io/sentry/protocol/Feedback.java index f62d4b9dc7..22f11bb7e8 100644 --- a/sentry/src/main/java/io/sentry/protocol/Feedback.java +++ b/sentry/src/main/java/io/sentry/protocol/Feedback.java @@ -1,12 +1,5 @@ package io.sentry.protocol; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - import io.sentry.ILogger; import io.sentry.JsonDeserializer; import io.sentry.JsonSerializable; @@ -15,34 +8,28 @@ import io.sentry.ObjectWriter; import io.sentry.SentryLevel; import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; // Specs can be found at https://develop.sentry.dev/sdk/data-model/envelope-items/#user-feedback -public class Feedback implements JsonUnknown, JsonSerializable { +public final class Feedback implements JsonUnknown, JsonSerializable { public static final String TYPE = "feedback"; - final @NotNull String message; - final @Nullable String contactEmail; - final @Nullable String name; - final @Nullable SentryId associatedEventId; - final @Nullable SentryId replayId; - final @Nullable String url; + private @NotNull String message; + private @Nullable String contactEmail; + private @Nullable String name; + private @Nullable SentryId associatedEventId; + private @Nullable SentryId replayId; + private @Nullable String url; private @Nullable Map unknown; - public Feedback( - final @NotNull String message, - final @Nullable String contactEmail, - final @Nullable String name, - final @Nullable SentryId associatedEventId, - final @Nullable SentryId replayId, - final @Nullable String url) { + public Feedback(final @NotNull String message) { this.message = message; - this.contactEmail = contactEmail; - this.name = name; - this.associatedEventId = associatedEventId; - this.replayId = replayId; - this.url = url; } public Feedback(final @NotNull Feedback feedback) { @@ -55,6 +42,54 @@ public Feedback(final @NotNull Feedback feedback) { this.unknown = feedback.unknown; } + public @Nullable String getContactEmail() { + return contactEmail; + } + + public void setContactEmail(final @Nullable String contactEmail) { + this.contactEmail = contactEmail; + } + + public @Nullable String getName() { + return name; + } + + public void setName(final @Nullable String name) { + this.name = name; + } + + public @Nullable SentryId getAssociatedEventId() { + return associatedEventId; + } + + public void setAssociatedEventId(final @NotNull SentryId associatedEventId) { + this.associatedEventId = associatedEventId; + } + + public @Nullable SentryId getReplayId() { + return replayId; + } + + public void setReplayId(final @NotNull SentryId replayId) { + this.replayId = replayId; + } + + public @Nullable String getUrl() { + return url; + } + + public void setUrl(final @Nullable String url) { + this.url = url; + } + + public @NotNull String getMessage() { + return message; + } + + public void setMessage(final @NotNull String message) { + this.message = message; + } + // JsonKeys public static final class JsonKeys { @@ -82,7 +117,7 @@ public void setUnknown(@Nullable Map unknown) { @Override public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) - throws IOException { + throws IOException { writer.beginObject(); writer.name(JsonKeys.MESSAGE).value(message); if (contactEmail != null) { @@ -116,7 +151,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger public static final class Deserializer implements JsonDeserializer { @Override public @NotNull Feedback deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) - throws Exception { + throws Exception { @Nullable String message = null; @Nullable String contactEmail = null; @Nullable String name = null; @@ -164,8 +199,13 @@ public static final class Deserializer implements JsonDeserializer { throw exception; } - Feedback feedback = new Feedback(message, contactEmail, name, associatedEventId, replayId, url); - feedback.setUnknown(unknown); + Feedback feedback = new Feedback(message); + feedback.contactEmail = contactEmail; + feedback.name = name; + feedback.associatedEventId = associatedEventId; + feedback.replayId = replayId; + feedback.url = url; + feedback.unknown = unknown; return feedback; } } From 813ff1d9b80143a5ea38d53a64de2a9add0d2923 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 25 Mar 2025 17:42:15 +0100 Subject: [PATCH 03/35] updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf069a858..477f3d1869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Add new User Feedback API ([#4286](https://github.com/getsentry/sentry-java/pull/4286)) + - We now introduced Sentry.captureFeedback, which supersedes Sentry.captureUserFeedback + ### Fixes - Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181)) From dd2d6a8e8df56bbca407545668dd27ddfff632dc Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 27 Mar 2025 17:05:44 +0100 Subject: [PATCH 04/35] added tests --- sentry/api/sentry.api | 6 ++ .../src/main/java/io/sentry/HubAdapter.java | 10 ++ .../main/java/io/sentry/ScopesAdapter.java | 10 ++ .../java/io/sentry/protocol/Feedback.java | 23 ++++- .../src/test/java/io/sentry/HubAdapterTest.kt | 15 +++ .../test/java/io/sentry/JsonSerializerTest.kt | 37 ++++++++ sentry/src/test/java/io/sentry/NoOpHubTest.kt | 4 + .../test/java/io/sentry/ScopesAdapterTest.kt | 15 +++ sentry/src/test/java/io/sentry/ScopesTest.kt | 95 +++++++++++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 20 ++++ sentry/src/test/java/io/sentry/SentryTest.kt | 18 ++++ .../java/io/sentry/protocol/ContextsTest.kt | 2 + .../java/io/sentry/protocol/FeedbackTest.kt | 44 +++++++++ 13 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8d073677fb..af5e0b5057 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -584,6 +584,8 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -2231,6 +2233,8 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -4747,6 +4751,7 @@ public final class io/sentry/protocol/Feedback : io/sentry/JsonSerializable, io/ public static final field TYPE Ljava/lang/String; public fun (Lio/sentry/protocol/Feedback;)V public fun (Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z public fun getAssociatedEventId ()Lio/sentry/protocol/SentryId; public fun getContactEmail ()Ljava/lang/String; public fun getMessage ()Ljava/lang/String; @@ -4754,6 +4759,7 @@ public final class io/sentry/protocol/Feedback : io/sentry/JsonSerializable, io/ public fun getReplayId ()Lio/sentry/protocol/SentryId; public fun getUnknown ()Ljava/util/Map; public fun getUrl ()Ljava/lang/String; + public fun hashCode ()I public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setAssociatedEventId (Lio/sentry/protocol/SentryId;)V public fun setContactEmail (Ljava/lang/String;)V diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index a3ec727294..622ac2e473 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -51,6 +51,16 @@ public boolean isEnabled() { return Sentry.captureMessage(message, level, callback); } + @Override + public @NotNull SentryId captureFeedback(@NotNull Feedback feedback) { + return Sentry.captureFeedback(feedback); + } + + @Override + public @NotNull SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint) { + return Sentry.captureFeedback(feedback, hint); + } + @Override public @NotNull SentryId captureFeedback( @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 57b3c02b98..63f87b1dc0 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -47,6 +47,16 @@ public boolean isEnabled() { return Sentry.captureMessage(message, level, callback); } + @Override + public @NotNull SentryId captureFeedback(@NotNull Feedback feedback) { + return Sentry.captureFeedback(feedback); + } + + @Override + public @NotNull SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint) { + return Sentry.captureFeedback(feedback, hint); + } + @Override public @NotNull SentryId captureFeedback( @NotNull Feedback feedback, @Nullable Hint hint, @Nullable ScopeCallback callback) { diff --git a/sentry/src/main/java/io/sentry/protocol/Feedback.java b/sentry/src/main/java/io/sentry/protocol/Feedback.java index 22f11bb7e8..9758261f99 100644 --- a/sentry/src/main/java/io/sentry/protocol/Feedback.java +++ b/sentry/src/main/java/io/sentry/protocol/Feedback.java @@ -7,10 +7,12 @@ import io.sentry.ObjectReader; import io.sentry.ObjectWriter; import io.sentry.SentryLevel; +import io.sentry.util.CollectionUtils; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,7 +41,7 @@ public Feedback(final @NotNull Feedback feedback) { this.associatedEventId = feedback.associatedEventId; this.replayId = feedback.replayId; this.url = feedback.url; - this.unknown = feedback.unknown; + this.unknown = CollectionUtils.newConcurrentHashMap(feedback.unknown); } public @Nullable String getContactEmail() { @@ -90,6 +92,25 @@ public void setMessage(final @NotNull String message) { this.message = message; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Feedback)) return false; + Feedback feedback = (Feedback) o; + return Objects.equals(message, feedback.message) + && Objects.equals(contactEmail, feedback.contactEmail) + && Objects.equals(name, feedback.name) + && Objects.equals(associatedEventId, feedback.associatedEventId) + && Objects.equals(replayId, feedback.replayId) + && Objects.equals(url, feedback.url) + && Objects.equals(unknown, feedback.unknown); + } + + @Override + public int hashCode() { + return Objects.hash(message, contactEmail, name, associatedEventId, replayId, url, unknown); + } + // JsonKeys public static final class JsonKeys { diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 76e79b0e48..718e228be5 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.test.createSentryClientMock @@ -57,6 +58,20 @@ class HubAdapterTest { verify(scopes).captureMessage(eq("message"), eq(sentryLevel), eq(scopeCallback)) } + @Test fun `captureFeedback calls Hub`() { + val hint = Hint() + val scopeCallback = mock() + val feedback = Feedback("message") + HubAdapter.getInstance().captureFeedback(feedback) + verify(scopes).captureFeedback(eq(feedback)) + + HubAdapter.getInstance().captureFeedback(feedback, hint) + verify(scopes).captureFeedback(eq(feedback), eq(hint)) + + HubAdapter.getInstance().captureFeedback(feedback, hint, scopeCallback) + verify(scopes).captureFeedback(eq(feedback), eq(hint), eq(scopeCallback)) + } + @Test fun `captureEnvelope calls Hub`() { val envelope = mock() val hint = mock() diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 8e3140faa1..de40906ae2 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -3,6 +3,7 @@ package io.sentry import io.sentry.profilemeasurements.ProfileMeasurement import io.sentry.profilemeasurements.ProfileMeasurementValue import io.sentry.protocol.Device +import io.sentry.protocol.Feedback import io.sentry.protocol.ReplayRecordingSerializationTest import io.sentry.protocol.Request import io.sentry.protocol.SdkVersion @@ -1245,6 +1246,42 @@ class JsonSerializerTest { ) } + @Test + fun `serializes feedback`() { + val replayId = SentryId("00000000-0000-0000-0000-000000000001") + val eventId = SentryId("00000000-0000-0000-0000-000000000002") + val feedback = Feedback("message") + feedback.name = "name" + feedback.contactEmail = "email" + feedback.url = "url" + feedback.setReplayId(replayId) + feedback.setAssociatedEventId(eventId) + val actual = serializeToString(feedback) + val expected = "{\"message\":\"message\",\"contact_email\":\"email\",\"name\":\"name\",\"associated_event_id\":\"00000000000000000000000000000002\",\"replay_id\":\"00000000000000000000000000000001\",\"url\":\"url\"}" + assertEquals(expected, actual) + } + + @Test + fun `deserializes feedback`() { + val json = """{ + "message":"message", + "contact_email":"email", + "name":"name", + "associated_event_id":"00000000000000000000000000000002", + "replay_id":"00000000000000000000000000000001", + "url":"url" + }""" + val feedback = fixture.serializer.deserialize(StringReader(json), Feedback::class.java) + val expected = Feedback("message").apply { + name = "name" + contactEmail = "email" + url = "url" + setReplayId(SentryId("00000000-0000-0000-0000-000000000001")) + setAssociatedEventId(SentryId("00000000-0000-0000-0000-000000000002")) + } + assertEquals(expected, feedback) + } + @Test fun `ser deser replay data`() { val replayEvent = SentryReplayEventSerializationTest.Fixture().getSut() diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index f20257482d..68343b1c91 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -39,6 +39,10 @@ class NoOpHubTest { fun `captureMessage returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureMessage("message")) + @Test + fun `captureFeedback returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureFeedback(mock())) + @Test fun `close does not affect captureEvent`() { sut.close() diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index ea274d438b..bc19038ac5 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.test.createSentryClientMock @@ -57,6 +58,20 @@ class ScopesAdapterTest { verify(scopes).captureMessage(eq("message"), eq(sentryLevel), eq(scopeCallback)) } + @Test fun `captureFeedback calls Scopes`() { + val scopeCallback = mock() + val hint = mock() + val feedback = Feedback("message") + ScopesAdapter.getInstance().captureFeedback(feedback) + verify(scopes).captureFeedback(eq(feedback)) + + ScopesAdapter.getInstance().captureFeedback(feedback, hint) + verify(scopes).captureFeedback(eq(feedback), eq(hint)) + + ScopesAdapter.getInstance().captureFeedback(feedback, hint, scopeCallback) + verify(scopes).captureFeedback(eq(feedback), eq(hint), eq(scopeCallback)) + } + @Test fun `captureEnvelope calls Scopes`() { val envelope = mock() val hint = mock() diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 23d2dcdd94..683f702a2f 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -7,6 +7,7 @@ import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.DiscardedEvent import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User @@ -606,6 +607,100 @@ class ScopesTest { //endregion + //region captureFeedback tests + @Test + fun `when captureFeedback is called and message is empty, lastEventId is empty`() { + val (sut, mockClient) = getEnabledScopes() + sut.captureFeedback(Feedback("")) + assertEquals(SentryId.EMPTY_ID, sut.lastEventId) + verify(mockClient, never()).captureFeedback(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `when captureFeedback is called on disabled client, do nothing`() { + val (sut, mockClient) = getEnabledScopes() + sut.close() + + sut.captureFeedback(mock()) + verify(mockClient, never()).captureFeedback(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `when captureFeedback is called with a valid message, captureFeedback on the client should be called`() { + val (sut, mockClient) = getEnabledScopes() + + sut.captureFeedback(Feedback("test")) + verify(mockClient).captureFeedback(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `when captureFeedback is called with a ScopeCallback then the modified scope is sent to the client`() { + val (sut, mockClient) = getEnabledScopes() + + sut.captureFeedback(Feedback("test"), null) { + it.setTag("test", "testValue") + } + + verify(mockClient).captureFeedback( + any(), + eq(null), + check { + assertEquals("testValue", it.tags["test"]) + } + ) + } + + @Test + fun `when captureFeedback is called with a ScopeCallback then subsequent calls to captureFeedback send the unmodified Scope to the client`() { + val (sut, mockClient) = getEnabledScopes() + val argumentCaptor = argumentCaptor() + + sut.captureFeedback(Feedback("testMessage"), null) { + it.setTag("test", "testValue") + } + + sut.captureFeedback(Feedback("test")) + + verify(mockClient, times(2)).captureFeedback( + any(), + anyOrNull(), + argumentCaptor.capture() + ) + + assertEquals("testValue", argumentCaptor.allValues[0].tags["test"]) + assertNull(argumentCaptor.allValues[1].tags["test"]) + } + + @Test + fun `when captureFeedback is called with a ScopeCallback that crashes then the feedback should still be captured`() { + val (sut, mockClient, logger) = getEnabledScopes() + + val exception = Exception("scope callback exception") + sut.captureFeedback(Feedback("Hello World"), null) { + throw exception + } + + verify(mockClient).captureFeedback( + any(), + anyOrNull(), + anyOrNull() + ) + + verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) + } + + @Test + fun `when captureFeedback is called with a Hint, it is passed to the client`() { + val (sut, mockClient) = getEnabledScopes() + val hint = Hint() + + sut.captureFeedback(Feedback("Hello World"), hint) + + verify(mockClient).captureFeedback(any(), eq(hint), anyOrNull()) + } + + //endregion + //region captureException tests @Test fun `when captureException is called and exception is null, lastEventId is empty`() { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 63052022eb..a193de562e 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -14,6 +14,7 @@ import io.sentry.hints.Cached import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts +import io.sentry.protocol.Feedback import io.sentry.protocol.Mechanism import io.sentry.protocol.Message import io.sentry.protocol.Request @@ -289,6 +290,25 @@ class SentryClientTest { assertEquals(SentryLevel.DEBUG, sentEvent!!.level) } + @Test + fun `when captureFeedback is called, sentry event contains feedback in contexts and header type`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } + val sut = fixture.getSut() + sut.captureFeedback(Feedback("message"), null, null) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + assertEquals("message", sentFeedback.message) + + verify(fixture.transport).send( + check { + assertEquals(SentryItemType.Feedback, it.items.first().header.type) + }, + anyOrNull() + ) + } + @Test fun `when event has release, value from options not applied`() { val event = SentryEvent() diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 28c2fe3367..2e3d4d4bed 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -11,6 +11,7 @@ import io.sentry.internal.debugmeta.ResourcesDebugMetaLoader import io.sentry.internal.modules.CompositeModulesLoader import io.sentry.internal.modules.IModulesLoader import io.sentry.internal.modules.NoOpModulesLoader +import io.sentry.protocol.Feedback import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId import io.sentry.protocol.SentryThread @@ -930,6 +931,23 @@ class SentryTest { assertFalse(previousSessionFile.exists()) } + @Test + fun `captureFeedback gets forwarded to client`() { + initForTest { it.dsn = dsn } + + val client = createSentryClientMock() + Sentry.getCurrentScopes().bindClient(client) + + val feedback = Feedback("message") + val hint = Hint() + + Sentry.captureFeedback(feedback) + Sentry.captureFeedback(feedback, hint) + + verify(client).captureFeedback(eq(feedback), eq(null), anyOrNull()) + verify(client).captureFeedback(eq(feedback), eq(hint), anyOrNull()) + } + @Test fun `captureCheckIn gets forwarded to client`() { initForTest { it.dsn = dsn } diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index e1ffe73c0c..f9d1979c37 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -21,6 +21,7 @@ class ContextsTest { contexts.setResponse(Response()) contexts.setTrace(SpanContext("op")) contexts.setSpring(Spring()) + contexts.setFeedback(Feedback("message")) val clone = Contexts(contexts) @@ -35,6 +36,7 @@ class ContextsTest { assertNotSame(contexts.trace, clone.trace) assertNotSame(contexts.response, clone.response) assertNotSame(contexts.spring, clone.spring) + assertNotSame(contexts.feedback, clone.feedback) } @Test diff --git a/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt b/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt new file mode 100644 index 0000000000..17649ddc96 --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt @@ -0,0 +1,44 @@ +package io.sentry.protocol + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNotSame + +class FeedbackTest { + + @Test + fun `copying feedback wont have the same references`() { + val feedback = Feedback("message") + val unknown = mapOf(Pair("unknown", "unknown")) + feedback.setUnknown(unknown) + + val clone = Feedback(feedback) + + assertNotNull(clone) + assertNotSame(feedback, clone) + assertNotSame(feedback.unknown, clone.unknown) + } + + @Test + fun `copying feedback will have the same values`() { + val feedback = Feedback("message") + feedback.name = "name" + feedback.contactEmail = "contact@email.com" + feedback.url = "url" + feedback.setReplayId(SentryId("00000000-0000-0000-0000-000000000001")) + feedback.setAssociatedEventId(SentryId("00000000-0000-0000-0000-000000000002")) + feedback.unknown = mapOf(Pair("unknown", "unknown")) + + val clone = Feedback(feedback) + assertEquals("message", clone.message) + assertEquals("name", clone.name) + assertEquals("contact@email.com", clone.contactEmail) + assertEquals("url", clone.url) + assertEquals("00000000000000000000000000000001", clone.replayId.toString()) + assertEquals("00000000000000000000000000000002", clone.associatedEventId.toString()) + assertNotNull(clone.unknown) { + assertEquals("unknown", it["unknown"]) + } + } +} From 6428dfabd6d60500b7cd61046bd03540fb549110 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 31 Mar 2025 10:56:20 +0200 Subject: [PATCH 05/35] added scope replay id and screen as url captureFeedback does not update scope lastEventId anymore --- .../main/java/io/sentry/ISentryClient.java | 12 ++++++- sentry/src/main/java/io/sentry/Scopes.java | 5 ++- .../java/io/sentry/protocol/Feedback.java | 2 +- sentry/src/test/java/io/sentry/ScopesTest.kt | 10 +++++- .../test/java/io/sentry/SentryClientTest.kt | 36 ++++++++++++++++++- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 98d97ef98f..b76aac67ff 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -88,10 +88,20 @@ public interface ISentryClient { * @return The Id (SentryId object) of the event */ default @NotNull SentryId captureFeedback( - @NotNull Feedback feedback, @Nullable Hint hint, @Nullable IScope scope) { + @NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { SentryEvent event = new SentryEvent(); event.getContexts().setFeedback(feedback); + if (feedback.getReplayId() == null) { + final @NotNull SentryId replayId = scope.getReplayId(); + if (!replayId.equals(SentryId.EMPTY_ID)) { + feedback.setReplayId(replayId); + } + } + if (feedback.getUrl() == null) { + feedback.setUrl(scope.getScreen()); + } + return captureEvent(event, scope, hint); } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 2ab4acaf57..f314d787c5 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -171,7 +171,7 @@ private void assignTraceContext(final @NotNull SentryEvent event) { getCombinedScopeView().assignTraceContext(event); } - private IScope buildLocalScope( + private @NotNull IScope buildLocalScope( final @NotNull IScope parentScope, final @Nullable ScopeCallback callback) { if (callback != null) { try { @@ -249,7 +249,7 @@ private IScope buildLocalScope( .log(SentryLevel.WARNING, "captureFeedback called with empty message."); } else { try { - final IScope localScope = buildLocalScope(getCombinedScopeView(), scopeCallback); + final @NotNull IScope localScope = buildLocalScope(getCombinedScopeView(), scopeCallback); sentryId = getClient().captureFeedback(feedback, hint, localScope); } catch (Throwable e) { @@ -258,7 +258,6 @@ private IScope buildLocalScope( .log(SentryLevel.ERROR, "Error while capturing feedback: " + feedback.getMessage(), e); } } - updateLastEventId(sentryId); return sentryId; } diff --git a/sentry/src/main/java/io/sentry/protocol/Feedback.java b/sentry/src/main/java/io/sentry/protocol/Feedback.java index 9758261f99..1de2f20370 100644 --- a/sentry/src/main/java/io/sentry/protocol/Feedback.java +++ b/sentry/src/main/java/io/sentry/protocol/Feedback.java @@ -8,11 +8,11 @@ import io.sentry.ObjectWriter; import io.sentry.SentryLevel; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 683f702a2f..97613b442f 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -609,13 +609,21 @@ class ScopesTest { //region captureFeedback tests @Test - fun `when captureFeedback is called and message is empty, lastEventId is empty`() { + fun `when captureFeedback is called and message is empty, client is never called`() { val (sut, mockClient) = getEnabledScopes() sut.captureFeedback(Feedback("")) assertEquals(SentryId.EMPTY_ID, sut.lastEventId) verify(mockClient, never()).captureFeedback(any(), anyOrNull(), anyOrNull()) } + @Test + fun `when captureFeedback is called, lastEventId is not updated`() { + val (sut, mockClient) = getEnabledScopes() + sut.captureFeedback(Feedback("message")) + assertEquals(SentryId.EMPTY_ID, sut.lastEventId) + verify(mockClient).captureFeedback(any(), anyOrNull(), anyOrNull()) + } + @Test fun `when captureFeedback is called on disabled client, do nothing`() { val (sut, mockClient) = getEnabledScopes() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index a193de562e..06b0e9215e 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -295,11 +295,14 @@ class SentryClientTest { var sentEvent: SentryEvent? = null fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } val sut = fixture.getSut() - sut.captureFeedback(Feedback("message"), null, null) + val scope = createScope() + sut.captureFeedback(Feedback("message"), null, scope) val sentFeedback = sentEvent!!.contexts.feedback assertNotNull(sentFeedback) assertEquals("message", sentFeedback.message) + assertNull(sentFeedback.replayId) + assertNull(sentFeedback.url) verify(fixture.transport).send( check { @@ -309,6 +312,37 @@ class SentryClientTest { ) } + @Test + fun `when captureFeedback, scope replay id is attached to feedback`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } + val replayId = SentryId() + val sut = fixture.getSut() + val scope = createScope() + scope.replayId = replayId + sut.captureFeedback(Feedback("message"), null, scope) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + assertEquals(replayId.toString(), sentFeedback.replayId?.toString()) + assertNull(sentFeedback.url) + } + + @Test + fun `when captureFeedback, screen is attached to feedback as url`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } + val sut = fixture.getSut() + val scope = createScope() + scope.screen = "screen" + sut.captureFeedback(Feedback("message"), null, scope) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + assertEquals("screen", sentFeedback.url) + assertNull(sentFeedback.replayId) + } + @Test fun `when event has release, value from options not applied`() { val event = SentryEvent() From 144bcca4d7d87accbd7d647783b3b926ecf08ddc Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Apr 2025 17:42:44 +0200 Subject: [PATCH 06/35] added feedback as DataCategory for rate limit and client report added SentryOptions.beforeSendFeedback implemented SentryClient.captureFeedback --- sentry/api/sentry.api | 8 +- sentry/src/main/java/io/sentry/Baggage.java | 6 +- .../src/main/java/io/sentry/DataCategory.java | 1 + .../main/java/io/sentry/ISentryClient.java | 19 +- .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/SentryClient.java | 259 +++++++++++++++--- .../main/java/io/sentry/SentryOptions.java | 24 ++ .../clientreport/ClientReportRecorder.java | 3 + .../java/io/sentry/transport/RateLimiter.java | 2 + 9 files changed, 262 insertions(+), 66 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index af5e0b5057..a1c0b04602 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -35,7 +35,7 @@ public final class io/sentry/Baggage { public fun (Ljava/util/Map;Ljava/lang/String;ZZLio/sentry/ILogger;)V public fun forceSetSampleRate (Ljava/lang/String;)V public fun freeze ()V - public static fun fromEvent (Lio/sentry/SentryEvent;Lio/sentry/SentryOptions;)Lio/sentry/Baggage; + public static fun fromEvent (Lio/sentry/SentryBaseEvent;Ljava/lang/String;Lio/sentry/SentryOptions;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;Lio/sentry/ILogger;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;ZLio/sentry/ILogger;)Lio/sentry/Baggage; @@ -342,6 +342,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Attachment Lio/sentry/DataCategory; public static final field Default Lio/sentry/DataCategory; public static final field Error Lio/sentry/DataCategory; + public static final field Feedback Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; @@ -958,7 +959,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2526,6 +2527,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; @@ -2868,6 +2870,7 @@ public class io/sentry/SentryOptions { public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback; public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback; public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback; + public fun getBeforeSendFeedback ()Lio/sentry/SentryOptions$BeforeSendCallback; public fun getBeforeSendReplay ()Lio/sentry/SentryOptions$BeforeSendReplayCallback; public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback; public fun getBundleIds ()Ljava/util/Set; @@ -2989,6 +2992,7 @@ public class io/sentry/SentryOptions { public fun setBeforeBreadcrumb (Lio/sentry/SentryOptions$BeforeBreadcrumbCallback;)V public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V + public fun setBeforeSendFeedback (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 4b83b91d40..b734534555 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -148,14 +148,16 @@ public static Baggage fromHeader( @ApiStatus.Internal @NotNull public static Baggage fromEvent( - final @NotNull SentryEvent event, final @NotNull SentryOptions options) { + final @NotNull SentryBaseEvent event, + final @Nullable String transaction, + final @NotNull SentryOptions options) { final Baggage baggage = new Baggage(options.getLogger()); final SpanContext trace = event.getContexts().getTrace(); baggage.setTraceId(trace != null ? trace.getTraceId().toString() : null); baggage.setPublicKey(options.retrieveParsedDsn().getPublicKey()); baggage.setRelease(event.getRelease()); baggage.setEnvironment(event.getEnvironment()); - baggage.setTransaction(event.getTransaction()); + baggage.setTransaction(transaction); // we don't persist sample rate baggage.setSampleRate(null); baggage.setSampled(null); diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index 43181eaeac..03e3ca9307 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -8,6 +8,7 @@ public enum DataCategory { All("__all__"), Default("default"), // same as Error Error("error"), + Feedback("feedback"), Session("session"), Attachment("attachment"), Monitor("monitor"), diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index b76aac67ff..0f966777ff 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -87,23 +87,8 @@ public interface ISentryClient { * @param scope An optional scope to be applied to the event. * @return The Id (SentryId object) of the event */ - default @NotNull SentryId captureFeedback( - @NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { - SentryEvent event = new SentryEvent(); - event.getContexts().setFeedback(feedback); - - if (feedback.getReplayId() == null) { - final @NotNull SentryId replayId = scope.getReplayId(); - if (!replayId.equals(SentryId.EMPTY_ID)) { - feedback.setReplayId(replayId); - } - } - if (feedback.getUrl() == null) { - feedback.setUrl(scope.getScreen()); - } - - return captureEvent(event, scope, hint); - } + @NotNull + SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope); /** * Captures the message. diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 162b1fae5a..4688bfba2c 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.RateLimiter; @@ -37,6 +38,11 @@ public void close() {} @Override public void flush(long timeoutMillis) {} + @Override + public @NotNull SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { + return SentryId.EMPTY_ID; + } + @Override public void captureUserFeedback(@NotNull UserFeedback userFeedback) {} diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 466ea7d2b5..9a91cfab70 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -7,6 +7,7 @@ import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -160,8 +161,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul scope != null ? scope.withSession((@Nullable Session session) -> {}) : null; @Nullable Session session = null; - // Feedbacks shouldn't be sampled, and they don't affect sessions - if (event != null && event.getContexts().getFeedback() == null) { + if (event != null) { // https://develop.sentry.dev/sdk/sessions/#terminal-session-states if (sessionBeforeUpdate == null || !sessionBeforeUpdate.isTerminated()) { session = updateSessionData(event, hint, scope); @@ -207,24 +207,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul } try { - @Nullable TraceContext traceContext = null; - if (isBackfillable) { - // for backfillable hint we synthesize Baggage from event values - if (event != null) { - final Baggage baggage = Baggage.fromEvent(event, options); - traceContext = baggage.toTraceContext(); - } - } else if (scope != null) { - final @Nullable ITransaction transaction = scope.getTransaction(); - if (transaction != null) { - traceContext = transaction.traceContext(); - } else { - final @NotNull PropagationContext propagationContext = - TracingUtils.maybeUpdateBaggage(scope, options); - traceContext = propagationContext.traceContext(); - } - } - + final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); final boolean shouldSendAttachments = event != null; List attachments = shouldSendAttachments ? getAttachments(hint) : null; final @Nullable SentryEnvelope envelope = @@ -303,19 +286,7 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin } try { - // TODO: check if event is Backfillable and backfill traceContext from the event values - @Nullable TraceContext traceContext = null; - if (scope != null) { - final @Nullable ITransaction transaction = scope.getTransaction(); - if (transaction != null) { - traceContext = transaction.traceContext(); - } else { - final @NotNull PropagationContext propagationContext = - TracingUtils.maybeUpdateBaggage(scope, options); - traceContext = propagationContext.traceContext(); - } - } - + final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event, null); final boolean cleanupReplayFolder = HintUtils.hasType(hint, Backfillable.class); final SentryEnvelope envelope = buildEnvelope(event, hint.getReplayRecording(), traceContext, cleanupReplayFolder); @@ -572,6 +543,40 @@ private SentryReplayEvent processReplayEvent( return replayEvent; } + @Nullable + private SentryEvent processFeedbackEvent( + @NotNull SentryEvent feedbackEvent, + final @NotNull Hint hint, + final @NotNull List eventProcessors) { + for (final EventProcessor processor : eventProcessors) { + try { + feedbackEvent = processor.process(feedbackEvent, hint); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + e, + "An exception occurred while processing feedback event by processor: %s", + processor.getClass().getName()); + } + + if (feedbackEvent == null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Feedback event was dropped by a processor: %s", + processor.getClass().getName()); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Feedback); + break; + } + } + return feedbackEvent; + } + @Override public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { Objects.requireNonNull(userFeedback, "SentryEvent is required."); @@ -932,18 +937,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint SentryId sentryId = checkIn.getCheckInId(); try { - @Nullable TraceContext traceContext = null; - if (scope != null) { - final @Nullable ITransaction transaction = scope.getTransaction(); - if (transaction != null) { - traceContext = transaction.traceContext(); - } else { - final @NotNull PropagationContext propagationContext = - TracingUtils.maybeUpdateBaggage(scope, options); - traceContext = propagationContext.traceContext(); - } - } - + final @Nullable TraceContext traceContext = getTraceContext(scope, hint, null); final @NotNull SentryEnvelope envelope = buildEnvelope(checkIn, traceContext); hint.clear(); @@ -957,6 +951,126 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + /** + * Captures the feedback. + * + * @param feedback The feedback to send. + * @param hint An optional hint to be applied to the event. + * @param scope An optional scope to be applied to the event. + * @return The Id (SentryId object) of the event + */ + @Override + public @NotNull SentryId captureFeedback( + final @NotNull Feedback feedback, @Nullable Hint hint, final @NotNull IScope scope) { + SentryEvent event = new SentryEvent(); + event.getContexts().setFeedback(feedback); + + if (hint == null) { + hint = new Hint(); + } + + if (feedback.getUrl() == null) { + feedback.setUrl(scope.getScreen()); + } + + options.getLogger().log(SentryLevel.DEBUG, "Capturing feedback: %s", event.getEventId()); + + if (shouldApplyScopeData(event, hint)) { + // Event has already passed through here before it was cached + // Going through again could be reading data that is no longer relevant + // i.e proguard id, app version, threads + event = applyFeedbackScope(event, scope, hint); + + if (event == null) { + options.getLogger().log(SentryLevel.DEBUG, "Feedback was dropped by applyScope"); + return SentryId.EMPTY_ID; + } + } + + event = processFeedbackEvent(event, hint, options.getEventProcessors()); + + if (event != null) { + event = executeBeforeSendFeedback(event, hint); + + if (event == null) { + options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSend"); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Feedback); + } + } + + if (event == null) { + return SentryId.EMPTY_ID; + } + + SentryId sentryId = SentryId.EMPTY_ID; + if (event.getEventId() != null) { + sentryId = event.getEventId(); + } + + final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); + // if event is backfillable we don't wanna trigger capture replay, because it's an event from + // the past. If feedback already has a replayId, we don't want to overwrite it. + if (feedback.getReplayId() == null && !isBackfillable) { + options.getReplayController().captureReplay(false); + final @NotNull SentryId replayId = scope.getReplayId(); + if (!replayId.equals(SentryId.EMPTY_ID)) { + feedback.setReplayId(replayId); + } + } + + try { + final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); + final List attachments = getAttachments(hint); + final @Nullable SentryEnvelope envelope = + buildEnvelope(event, attachments, null, traceContext, null); + + hint.clear(); + if (envelope != null) { + sentryId = sendEnvelope(envelope, hint); + } + } catch (IOException | SentryEnvelopeException e) { + options.getLogger().log(SentryLevel.WARNING, e, "Capturing feedback %s failed.", sentryId); + + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; + } + + return sentryId; + } + + private @Nullable TraceContext getTraceContext( + final @Nullable IScope scope, final @NotNull Hint hint, final @Nullable SentryEvent event) { + return getTraceContext(scope, hint, event, event != null ? event.getTransaction() : null); + } + + private @Nullable TraceContext getTraceContext( + final @Nullable IScope scope, + final @NotNull Hint hint, + final @Nullable SentryBaseEvent event, + final @Nullable String txn) { + @Nullable TraceContext traceContext = null; + final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); + if (isBackfillable) { + // for backfillable hint we synthesize Baggage from event values + if (event != null) { + final Baggage baggage = Baggage.fromEvent(event, txn, options); + traceContext = baggage.toTraceContext(); + } + } else if (scope != null) { + final @Nullable ITransaction transaction = scope.getTransaction(); + if (transaction != null) { + traceContext = transaction.traceContext(); + } else { + final @NotNull PropagationContext propagationContext = + TracingUtils.maybeUpdateBaggage(scope, options); + traceContext = propagationContext.traceContext(); + } + } + return traceContext; + } + private @Nullable List filterForTransaction(@Nullable List attachments) { if (attachments == null) { return null; @@ -1004,6 +1118,43 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return event; } + private @Nullable SentryEvent applyFeedbackScope( + @NotNull SentryEvent event, final @NotNull IScope scope, final @NotNull Hint hint) { + + if (event.getUser() == null) { + event.setUser(scope.getUser()); + } + if (event.getTags() == null) { + event.setTags(new HashMap<>(scope.getTags())); + } else { + for (Map.Entry item : scope.getTags().entrySet()) { + if (!event.getTags().containsKey(item.getKey())) { + event.getTags().put(item.getKey(), item.getValue()); + } + } + } + final Contexts contexts = event.getContexts(); + for (Map.Entry entry : new Contexts(scope.getContexts()).entrySet()) { + if (!contexts.containsKey(entry.getKey())) { + contexts.put(entry.getKey(), entry.getValue()); + } + } + // Set trace data from active span to connect events with transactions + final ISpan span = scope.getSpan(); + if (event.getContexts().getTrace() == null) { + if (span == null) { + event + .getContexts() + .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); + } else { + event.getContexts().setTrace(span.getSpanContext()); + } + } + + event = processFeedbackEvent(event, hint, scope.getEventProcessors()); + return event; + } + private @NotNull CheckIn applyScope(@NotNull CheckIn checkIn, final @Nullable IScope scope) { if (scope != null) { // Set trace data from active span to connect events with transactions @@ -1157,6 +1308,24 @@ private void sortBreadcrumbsByDate( return transaction; } + private @Nullable SentryEvent executeBeforeSendFeedback( + @NotNull SentryEvent event, final @NotNull Hint hint) { + final SentryOptions.BeforeSendCallback beforeSendFeedback = options.getBeforeSendFeedback(); + if (beforeSendFeedback != null) { + try { + event = beforeSendFeedback.execute(event, hint); + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.ERROR, "The BeforeSendFeedback callback threw an exception.", e); + + // drop feedback in case of an error in beforeSend due to PII concerns + event = null; + } + } + return event; + } + private @Nullable SentryReplayEvent executeBeforeSendReplay( @NotNull SentryReplayEvent event, final @NotNull Hint hint) { final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 1ddb12f077..b8aed7e714 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -153,6 +153,12 @@ public class SentryOptions { */ private @Nullable BeforeSendCallback beforeSend; + /** + * This function is called with an SDK specific feedback object and can return a modified feedback + * object or nothing to skip reporting the feedback + */ + private @Nullable BeforeSendCallback beforeSendFeedback; + /** * This function is called with an SDK specific transaction object and can return a modified * transaction object or nothing to skip reporting the transaction @@ -774,6 +780,24 @@ public void setBeforeSendTransaction( this.beforeSendTransaction = beforeSendTransaction; } + /** + * Returns the BeforeSendFeedback callback + * + * @return the beforeSendFeedback callback or null if not set + */ + public @Nullable BeforeSendCallback getBeforeSendFeedback() { + return beforeSendFeedback; + } + + /** + * Sets the beforeSendFeedback callback + * + * @param beforeSendFeedback the beforeSendFeedback callback + */ + public void setBeforeSendFeedback(@Nullable BeforeSendCallback beforeSendFeedback) { + this.beforeSendFeedback = beforeSendFeedback; + } + /** * Returns the BeforeSendReplay callback * diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index b4d4574aba..1ddb3e900b 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -162,6 +162,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.UserFeedback.equals(itemType)) { return DataCategory.UserReport; } + if (SentryItemType.Feedback.equals(itemType)) { + return DataCategory.Feedback; + } if (SentryItemType.Profile.equals(itemType)) { return DataCategory.Profile; } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 4e667e97a7..62736a051e 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -197,6 +197,8 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Monitor; case "replay_video": return DataCategory.Replay; + case "feedback": + return DataCategory.Feedback; default: return DataCategory.Unknown; } From 3c0a6fbf7204dc0cb03b29a8c42ef616ffe31dc0 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Apr 2025 17:51:59 +0200 Subject: [PATCH 07/35] merged main --- sentry/api/sentry.api | 301 +++++++++++++++--- .../main/java/io/sentry/NoOpSentryClient.java | 3 +- .../src/main/java/io/sentry/SentryClient.java | 3 +- 3 files changed, 261 insertions(+), 46 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a1c0b04602..331ccb7f25 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -11,14 +11,17 @@ public final class io/sentry/Attachment { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V + public fun (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V public fun ([BLjava/lang/String;)V public fun ([BLjava/lang/String;Ljava/lang/String;)V public fun ([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V public fun ([BLjava/lang/String;Ljava/lang/String;Z)V + public static fun fromByteProvider (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/Attachment; public static fun fromScreenshot ([B)Lio/sentry/Attachment; public static fun fromThreadDump ([B)Lio/sentry/Attachment; public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment; public fun getAttachmentType ()Ljava/lang/String; + public fun getByteProvider ()Ljava/util/concurrent/Callable; public fun getBytes ()[B public fun getContentType ()Ljava/lang/String; public fun getFilename ()Ljava/lang/String; @@ -32,8 +35,8 @@ public abstract interface class io/sentry/BackfillingEventProcessor : io/sentry/ public final class io/sentry/Baggage { public fun (Lio/sentry/Baggage;)V public fun (Lio/sentry/ILogger;)V - public fun (Ljava/util/Map;Ljava/lang/String;ZZLio/sentry/ILogger;)V - public fun forceSetSampleRate (Ljava/lang/String;)V + public fun (Ljava/util/concurrent/ConcurrentHashMap;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/String;ZZLio/sentry/ILogger;)V + public fun forceSetSampleRate (Ljava/lang/Double;)V public fun freeze ()V public static fun fromEvent (Lio/sentry/SentryBaseEvent;Ljava/lang/String;Lio/sentry/SentryOptions;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;)Lio/sentry/Baggage; @@ -47,10 +50,8 @@ public final class io/sentry/Baggage { public fun getPublicKey ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getReplayId ()Ljava/lang/String; - public fun getSampleRand ()Ljava/lang/String; - public fun getSampleRandDouble ()Ljava/lang/Double; - public fun getSampleRate ()Ljava/lang/String; - public fun getSampleRateDouble ()Ljava/lang/Double; + public fun getSampleRand ()Ljava/lang/Double; + public fun getSampleRate ()Ljava/lang/Double; public fun getSampled ()Ljava/lang/String; public fun getThirdPartyHeader ()Ljava/lang/String; public fun getTraceId ()Ljava/lang/String; @@ -64,9 +65,8 @@ public final class io/sentry/Baggage { public fun setPublicKey (Ljava/lang/String;)V public fun setRelease (Ljava/lang/String;)V public fun setReplayId (Ljava/lang/String;)V - public fun setSampleRand (Ljava/lang/String;)V - public fun setSampleRandDouble (Ljava/lang/Double;)V - public fun setSampleRate (Ljava/lang/String;)V + public fun setSampleRand (Ljava/lang/Double;)V + public fun setSampleRate (Ljava/lang/Double;)V public fun setSampled (Ljava/lang/String;)V public fun setTraceId (Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V @@ -324,10 +324,20 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V } +public abstract interface class io/sentry/CompositePerformanceCollector { + public abstract fun close ()V + public abstract fun onSpanFinished (Lio/sentry/ISpan;)V + public abstract fun onSpanStarted (Lio/sentry/ISpan;)V + public abstract fun start (Lio/sentry/ITransaction;)V + public abstract fun start (Ljava/lang/String;)V + public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public abstract fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/CpuCollectionData { - public fun (JD)V + public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D - public fun getTimestampMillis ()J + public fun getTimestamp ()Lio/sentry/SentryDate; } public final class io/sentry/CustomSamplingContext { @@ -345,6 +355,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Feedback Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; + public static final field ProfileChunk Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; public static final field Session Lio/sentry/DataCategory; @@ -381,6 +392,17 @@ public final class io/sentry/DeduplicateMultithreadedEventProcessor : io/sentry/ public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public final class io/sentry/DefaultCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun (Lio/sentry/SentryOptions;)V + public fun close ()V + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public fun ()V public fun close ()V @@ -392,16 +414,12 @@ public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public final class io/sentry/DefaultSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } -public final class io/sentry/DefaultTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { +public final class io/sentry/DefaultVersionDetector : io/sentry/IVersionDetector { public fun (Lio/sentry/SentryOptions;)V - public fun close ()V - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun checkForMixedVersions ()Z } public final class io/sentry/DiagnosticLogger : io/sentry/ILogger { @@ -590,6 +608,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -636,8 +655,10 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -656,6 +677,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -701,8 +723,10 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -727,6 +751,15 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V } +public abstract interface class io/sentry/IContinuousProfiler { + public abstract fun close (Z)V + public abstract fun getProfilerId ()Lio/sentry/protocol/SentryId; + public abstract fun isRunning ()Z + public abstract fun reevaluateSampling ()V + public abstract fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public abstract fun stopProfiler (Lio/sentry/ProfileLifecycle;)V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -882,6 +915,7 @@ public abstract interface class io/sentry/IScopes { public fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public abstract fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;)Lio/sentry/protocol/SentryId; @@ -931,11 +965,13 @@ public abstract interface class io/sentry/IScopes { public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setTransaction (Ljava/lang/String;)V public abstract fun setUser (Lio/sentry/protocol/User;)V + public abstract fun startProfiler ()V public abstract fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public abstract fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public abstract fun stopProfiler ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -962,6 +998,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V public abstract fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -1042,7 +1079,7 @@ public abstract interface class io/sentry/ISpan { public abstract interface class io/sentry/ISpanFactory { public abstract fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { @@ -1072,6 +1109,10 @@ public abstract interface class io/sentry/ITransportFactory { public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport; } +public abstract interface class io/sentry/IVersionDetector { + public abstract fun checkForMixedVersions ()Z +} + public final class io/sentry/InitPriority : java/lang/Enum { public static final field HIGH Lio/sentry/InitPriority; public static final field HIGHEST Lio/sentry/InitPriority; @@ -1221,6 +1262,11 @@ public final class io/sentry/MainEventProcessor : io/sentry/EventProcessor, java public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } +public final class io/sentry/ManifestVersionDetector : io/sentry/IVersionDetector { + public fun (Lio/sentry/SentryOptions;)V + public fun checkForMixedVersions ()Z +} + public abstract interface class io/sentry/MeasurementUnit { public static final field NONE Ljava/lang/String; public abstract fun apiName ()Ljava/lang/String; @@ -1276,9 +1322,9 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se } public final class io/sentry/MemoryCollectionData { - public fun (JJ)V - public fun (JJJ)V - public fun getTimestampMillis ()J + public fun (JJLio/sentry/SentryDate;)V + public fun (JLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J } @@ -1382,6 +1428,17 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static fun values ()[Lio/sentry/MonitorScheduleUnit; } +public final class io/sentry/NoOpCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun close ()V + public static fun getInstance ()Lio/sentry/NoOpCompositePerformanceCollector; + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { public fun ()V public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z @@ -1390,6 +1447,16 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V } +public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfiler { + public fun close (Z)V + public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun isRunning ()Z + public fun reevaluateSampling ()V + public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfiler (Lio/sentry/ProfileLifecycle;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -1408,6 +1475,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1455,8 +1523,10 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1568,6 +1638,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1615,8 +1686,10 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1677,7 +1750,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public final class io/sentry/NoOpSpanFactory : io/sentry/ISpanFactory { public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; public static fun getInstance ()Lio/sentry/NoOpSpanFactory; } @@ -1734,15 +1807,6 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun updateEndDate (Lio/sentry/SentryDate;)Z } -public final class io/sentry/NoOpTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { - public fun close ()V - public static fun getInstance ()Lio/sentry/NoOpTransactionPerformanceCollector; - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/NoOpTransactionProfiler : io/sentry/ITransactionProfiler { public fun bindTransaction (Lio/sentry/ITransaction;)V public fun close ()V @@ -1757,6 +1821,11 @@ public final class io/sentry/NoOpTransportFactory : io/sentry/ITransportFactory public static fun getInstance ()Lio/sentry/NoOpTransportFactory; } +public final class io/sentry/NoopVersionDetector : io/sentry/IVersionDetector { + public fun checkForMixedVersions ()Z + public static fun getInstance ()Lio/sentry/NoopVersionDetector; +} + public abstract interface class io/sentry/ObjectReader : java/io/Closeable { public abstract fun beginArray ()V public abstract fun beginObject ()V @@ -1828,6 +1897,87 @@ public final class io/sentry/PerformanceCollectionData { public fun getMemoryData ()Lio/sentry/MemoryCollectionData; } +public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun ()V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Ljava/lang/Double;Lio/sentry/SentryOptions;)V + public fun equals (Ljava/lang/Object;)Z + public fun getChunkId ()Lio/sentry/protocol/SentryId; + public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; + public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; + public fun getEnvironment ()Ljava/lang/String; + public fun getMeasurements ()Ljava/util/Map; + public fun getPlatform ()Ljava/lang/String; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRelease ()Ljava/lang/String; + public fun getSampledProfile ()Ljava/lang/String; + public fun getTimestamp ()D + public fun getTraceFile ()Ljava/io/File; + public fun getUnknown ()Ljava/util/Map; + public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setSampledProfile (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileChunk$Builder { + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;Lio/sentry/SentryDate;)V + public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; +} + +public final class io/sentry/ProfileChunk$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileChunk; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileChunk$JsonKeys { + public static final field CHUNK_ID Ljava/lang/String; + public static final field CLIENT_SDK Ljava/lang/String; + public static final field DEBUG_META Ljava/lang/String; + public static final field ENVIRONMENT Ljava/lang/String; + public static final field MEASUREMENTS Ljava/lang/String; + public static final field PLATFORM Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; + public static final field RELEASE Ljava/lang/String; + public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; + public static final field VERSION Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/ProfileContext;)V + public fun (Lio/sentry/protocol/SentryId;)V + public fun equals (Ljava/lang/Object;)Z + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileContext$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileContext; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileContext$JsonKeys { + public static final field PROFILER_ID Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/ProfileLifecycle : java/lang/Enum { + public static final field MANUAL Lio/sentry/ProfileLifecycle; + public static final field TRACE Lio/sentry/ProfileLifecycle; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/ProfileLifecycle; + public static fun values ()[Lio/sentry/ProfileLifecycle; +} + public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TRUNCATION_REASON_BACKGROUNDED Ljava/lang/String; public static final field TRUNCATION_REASON_NORMAL Ljava/lang/String; @@ -2172,6 +2322,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2218,8 +2369,10 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2239,6 +2392,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2285,8 +2439,10 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2392,12 +2548,14 @@ public final class io/sentry/Sentry { public static fun setTag (Ljava/lang/String;Ljava/lang/String;)V public static fun setTransaction (Ljava/lang/String;)V public static fun setUser (Lio/sentry/protocol/User;)V + public static fun startProfiler ()V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public static fun stopProfiler ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2408,20 +2566,30 @@ public abstract interface class io/sentry/Sentry$OptionsConfiguration { public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSampleRate ()Ljava/lang/Double; public fun getProfilingTracesDirPath ()Ljava/lang/String; public fun getProfilingTracesHz ()I public fun getTraceSampleRate ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; + public fun isContinuousProfileSampled ()Z + public fun isContinuousProfilingEnabled ()Z + public fun isEnableAppStartProfiling ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setContinuousProfileSampled (Z)V + public fun setContinuousProfilingEnabled (Z)V + public fun setEnableAppStartProfiling (Z)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V + public fun setStartProfilerOnAppStart (Z)V public fun setTraceSampleRate (Ljava/lang/Double;)V public fun setTraceSampled (Z)V public fun setUnknown (Ljava/util/Map;)V @@ -2434,7 +2602,12 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; + public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_ENABLE_APP_START_PROFILING Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_START_PROFILER_ON_APP_START Ljava/lang/String; + public static final field PROFILE_LIFECYCLE Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; public static final field PROFILING_TRACES_DIR_PATH Ljava/lang/String; @@ -2528,6 +2701,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; @@ -2607,6 +2781,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromCheckIn (Lio/sentry/ISerializer;Lio/sentry/CheckIn;)Lio/sentry/SentryEnvelopeItem; public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; + public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; @@ -2619,11 +2794,12 @@ public final class io/sentry/SentryEnvelopeItem { } public final class io/sentry/SentryEnvelopeItemHeader : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public fun (Lio/sentry/SentryItemType;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun (Lio/sentry/SentryItemType;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public fun getAttachmentType ()Ljava/lang/String; public fun getContentType ()Ljava/lang/String; public fun getFileName ()Ljava/lang/String; public fun getLength ()I + public fun getPlatform ()Ljava/lang/String; public fun getType ()Lio/sentry/SentryItemType; public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V @@ -2641,6 +2817,7 @@ public final class io/sentry/SentryEnvelopeItemHeader$JsonKeys { public static final field CONTENT_TYPE Ljava/lang/String; public static final field FILENAME Ljava/lang/String; public static final field LENGTH Ljava/lang/String; + public static final field PLATFORM Ljava/lang/String; public static final field TYPE Ljava/lang/String; public fun ()V } @@ -2725,6 +2902,7 @@ public final class io/sentry/SentryInstantDateProvider : io/sentry/SentryDatePro public final class io/sentry/SentryIntegrationPackageStorage { public fun addIntegration (Ljava/lang/String;)V public fun addPackage (Ljava/lang/String;Ljava/lang/String;)V + public fun checkForMixedVersions (Lio/sentry/ILogger;)Z public fun clearStorage ()V public static fun getInstance ()Lio/sentry/SentryIntegrationPackageStorage; public fun getIntegrations ()Ljava/util/Set; @@ -2738,6 +2916,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field Event Lio/sentry/SentryItemType; public static final field Feedback Lio/sentry/SentryItemType; public static final field Profile Lio/sentry/SentryItemType; + public static final field ProfileChunk Lio/sentry/SentryItemType; public static final field ReplayEvent Lio/sentry/SentryItemType; public static final field ReplayRecording Lio/sentry/SentryItemType; public static final field ReplayVideo Lio/sentry/SentryItemType; @@ -2876,9 +3055,11 @@ public class io/sentry/SentryOptions { public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; + public fun getCompositePerformanceCollector ()Lio/sentry/CompositePerformanceCollector; public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; + public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; @@ -2893,6 +3074,7 @@ public class io/sentry/SentryOptions { public fun getEventProcessors ()Ljava/util/List; public fun getExecutorService ()Lio/sentry/ISentryExecutorService; public fun getExperimental ()Lio/sentry/ExperimentalOptions; + public fun getFatalLogger ()Lio/sentry/ILogger; public fun getFlushTimeoutMillis ()J public fun getFullyDisplayedReporter ()Lio/sentry/FullyDisplayedReporter; public fun getGestureTargetLocators ()Ljava/util/List; @@ -2922,6 +3104,8 @@ public class io/sentry/SentryOptions { public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; public fun getProfilingTracesDirPath ()Ljava/lang/String; @@ -2949,15 +3133,16 @@ public class io/sentry/SentryOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracesSampler ()Lio/sentry/SentryOptions$TracesSamplerCallback; - public fun getTransactionPerformanceCollector ()Lio/sentry/TransactionPerformanceCollector; public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; + public fun getVersionDetector ()Lio/sentry/IVersionDetector; public final fun getViewHierarchyExporters ()Ljava/util/List; public fun isAttachServerName ()Z public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z public fun isCaptureOpenTelemetryEvents ()Z + public fun isContinuousProfilingEnabled ()Z public fun isDebug ()Z public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z @@ -2981,6 +3166,7 @@ public class io/sentry/SentryOptions { public fun isSendClientReports ()Z public fun isSendDefaultPii ()Z public fun isSendModules ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z @@ -2997,8 +3183,10 @@ public class io/sentry/SentryOptions { public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V public fun setCaptureOpenTelemetryEvents (Z)V + public fun setCompositePerformanceCollector (Lio/sentry/CompositePerformanceCollector;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V + public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -3027,6 +3215,7 @@ public class io/sentry/SentryOptions { public fun setEnvelopeReader (Lio/sentry/IEnvelopeReader;)V public fun setEnvironment (Ljava/lang/String;)V public fun setExecutorService (Lio/sentry/ISentryExecutorService;)V + public fun setFatalLogger (Lio/sentry/ILogger;)V public fun setFlushTimeoutMillis (J)V public fun setForceInit (Z)V public fun setFullyDisplayedReporter (Lio/sentry/FullyDisplayedReporter;)V @@ -3051,6 +3240,8 @@ public class io/sentry/SentryOptions { public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V public fun setPrintUncaughtStackTrace (Z)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V + public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V public fun setProfilingTracesHz (I)V @@ -3074,6 +3265,7 @@ public class io/sentry/SentryOptions { public fun setSpanFactory (Lio/sentry/ISpanFactory;)V public fun setSpotlightConnectionUrl (Ljava/lang/String;)V public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V + public fun setStartProfilerOnAppStart (Z)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setThreadChecker (Lio/sentry/util/thread/IThreadChecker;)V public fun setTraceOptionsRequests (Z)V @@ -3081,10 +3273,10 @@ public class io/sentry/SentryOptions { public fun setTraceSampling (Z)V public fun setTracesSampleRate (Ljava/lang/Double;)V public fun setTracesSampler (Lio/sentry/SentryOptions$TracesSamplerCallback;)V - public fun setTransactionPerformanceCollector (Lio/sentry/TransactionPerformanceCollector;)V public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V + public fun setVersionDetector (Lio/sentry/IVersionDetector;)V public fun setViewHierarchyExporters (Ljava/util/List;)V } @@ -3370,6 +3562,7 @@ public final class io/sentry/SentryUUID { public final class io/sentry/SentryWrapper { public fun ()V public static fun wrapCallable (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Callable; + public static fun wrapRunnable (Ljava/lang/Runnable;)Ljava/lang/Runnable; public static fun wrapSupplier (Ljava/util/function/Supplier;)Ljava/util/function/Supplier; } @@ -3576,6 +3769,7 @@ public abstract interface class io/sentry/SpanDataConvention { public static final field HTTP_RESPONSE_CONTENT_LENGTH_KEY Ljava/lang/String; public static final field HTTP_START_TIMESTAMP Ljava/lang/String; public static final field HTTP_STATUS_CODE_KEY Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; public static final field THREAD_ID Ljava/lang/String; public static final field THREAD_NAME Ljava/lang/String; } @@ -3711,6 +3905,7 @@ public final class io/sentry/TraceContext$JsonKeys { public final class io/sentry/TracesSampler { public fun (Lio/sentry/SentryOptions;)V public fun sample (Lio/sentry/SamplingContext;)Lio/sentry/TracesSamplingDecision; + public fun sampleSessionProfile (D)Z } public final class io/sentry/TracesSamplingDecision { @@ -3771,14 +3966,6 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun setWaitForChildren (Z)V } -public abstract interface class io/sentry/TransactionPerformanceCollector { - public abstract fun close ()V - public abstract fun onSpanFinished (Lio/sentry/ISpan;)V - public abstract fun onSpanStarted (Lio/sentry/ISpan;)V - public abstract fun start (Lio/sentry/ITransaction;)V - public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/TypeCheckHint { public static final field ANDROID_ACTIVITY Ljava/lang/String; public static final field ANDROID_CONFIGURATION Ljava/lang/String; @@ -4267,6 +4454,20 @@ public final class io/sentry/instrumentation/file/SentryFileWriter : java/io/Out public fun (Ljava/lang/String;Z)V } +public final class io/sentry/internal/ManifestVersionReader { + public static fun getInstance ()Lio/sentry/internal/ManifestVersionReader; + public fun readManifestFiles ()V + public fun readOpenTelemetryVersion ()Lio/sentry/internal/ManifestVersionReader$VersionInfoHolder; +} + +public final class io/sentry/internal/ManifestVersionReader$VersionInfoHolder { + public fun ()V + public fun getIntegrations ()Ljava/util/List; + public fun getPackages ()Ljava/util/List; + public fun getSdkName ()Ljava/lang/String; + public fun getSdkVersion ()Ljava/lang/String; +} + public abstract interface class io/sentry/internal/debugmeta/IDebugMetaLoader { public abstract fun loadDebugMeta ()Ljava/util/List; } @@ -4392,9 +4593,10 @@ public final class io/sentry/profilemeasurements/ProfileMeasurement$JsonKeys { public final class io/sentry/profilemeasurements/ProfileMeasurementValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V - public fun (Ljava/lang/Long;Ljava/lang/Number;)V + public fun (Ljava/lang/Long;Ljava/lang/Number;Lio/sentry/SentryDate;)V public fun equals (Ljava/lang/Object;)Z public fun getRelativeStartNs ()Ljava/lang/String; + public fun getTimestamp ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; public fun getValue ()D public fun hashCode ()I @@ -4410,6 +4612,7 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$Deseria public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKeys { public static final field START_NS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public static final field VALUE Ljava/lang/String; public fun ()V } @@ -4514,6 +4717,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun getFeedback ()Lio/sentry/protocol/Feedback; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; + public fun getProfile ()Lio/sentry/ProfileContext; public fun getResponse ()Lio/sentry/protocol/Response; public fun getRuntime ()Lio/sentry/protocol/SentryRuntime; public fun getSize ()I @@ -4534,6 +4738,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun setFeedback (Lio/sentry/protocol/Feedback;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V + public fun setProfile (Lio/sentry/ProfileContext;)V public fun setResponse (Lio/sentry/protocol/Response;)V public fun setRuntime (Lio/sentry/protocol/SentryRuntime;)V public fun setSpring (Lio/sentry/protocol/Spring;)V @@ -4597,6 +4802,7 @@ public final class io/sentry/protocol/DebugImage$JsonKeys { public final class io/sentry/protocol/DebugMeta : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public static fun buildDebugMeta (Lio/sentry/protocol/DebugMeta;Lio/sentry/SentryOptions;)Lio/sentry/protocol/DebugMeta; public fun getImages ()Ljava/util/List; public fun getSdkInfo ()Lio/sentry/protocol/SdkInfo; public fun getUnknown ()Ljava/util/Map; @@ -5307,6 +5513,7 @@ public final class io/sentry/protocol/SentrySpan$JsonKeys { public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V public fun getAbsPath ()Ljava/lang/String; + public fun getAddrMode ()Ljava/lang/String; public fun getColno ()Ljava/lang/Integer; public fun getContextLine ()Ljava/lang/String; public fun getFilename ()Ljava/lang/String; @@ -5330,6 +5537,7 @@ public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializa public fun isNative ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setAbsPath (Ljava/lang/String;)V + public fun setAddrMode (Ljava/lang/String;)V public fun setColno (Ljava/lang/Integer;)V public fun setContextLine (Ljava/lang/String;)V public fun setFilename (Ljava/lang/String;)V @@ -5361,6 +5569,7 @@ public final class io/sentry/protocol/SentryStackFrame$Deserializer : io/sentry/ public final class io/sentry/protocol/SentryStackFrame$JsonKeys { public static final field ABS_PATH Ljava/lang/String; + public static final field ADDR_MODE Ljava/lang/String; public static final field COLNO Ljava/lang/String; public static final field CONTEXT_LINE Ljava/lang/String; public static final field FILENAME Ljava/lang/String; @@ -6439,6 +6648,7 @@ public final class io/sentry/util/SampleRateUtils { public fun ()V public static fun backfilledSampleRand (Lio/sentry/TracesSamplingDecision;)Lio/sentry/TracesSamplingDecision; public static fun backfilledSampleRand (Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)Ljava/lang/Double; + public static fun isValidContinuousProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z @@ -6523,6 +6733,7 @@ public final class io/sentry/util/UrlUtils$UrlDetails { public abstract interface class io/sentry/util/thread/IThreadChecker { public abstract fun currentThreadSystemId ()J + public abstract fun getCurrentThreadName ()Ljava/lang/String; public abstract fun isMainThread ()Z public abstract fun isMainThread (J)Z public abstract fun isMainThread (Lio/sentry/protocol/SentryThread;)Z @@ -6532,6 +6743,7 @@ public abstract interface class io/sentry/util/thread/IThreadChecker { public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thread/IThreadChecker { public fun ()V public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/NoOpThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z @@ -6541,6 +6753,7 @@ public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thre public final class io/sentry/util/thread/ThreadChecker : io/sentry/util/thread/IThreadChecker { public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/ThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 7cfec90f2d..7050a524d1 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -39,7 +39,8 @@ public void close() {} public void flush(long timeoutMillis) {} @Override - public @NotNull SentryId captureFeedback(@NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { + public @NotNull SentryId captureFeedback( + @NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { return SentryId.EMPTY_ID; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index c9d3e2aa5d..2c8a57e949 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -7,8 +7,8 @@ import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; -import io.sentry.protocol.Feedback; import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -1105,6 +1105,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint final @NotNull PropagationContext propagationContext = TracingUtils.maybeUpdateBaggage(scope, options); traceContext = propagationContext.traceContext(); + } } return traceContext; } From 9e1066ed3fd989752e6d70c113f2c1cdbeb72520 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 17 Apr 2025 17:24:40 +0200 Subject: [PATCH 08/35] added tests --- .../java/io/sentry/NoOpSentryClientTest.kt | 4 + .../test/java/io/sentry/SentryClientTest.kt | 243 ++++++++++++++---- sentry/src/test/java/io/sentry/SentryTest.kt | 12 +- .../sentry/clientreport/ClientReportTest.kt | 12 +- .../protocol/ContextsSerializationTest.kt | 1 + .../java/io/sentry/protocol/FeedbackTest.kt | 28 +- .../io/sentry/transport/RateLimiterTest.kt | 25 +- sentry/src/test/resources/json/contexts.json | 10 + 8 files changed, 266 insertions(+), 69 deletions(-) diff --git a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt index 8f8d76eba2..d39dba4c70 100644 --- a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt @@ -29,6 +29,10 @@ class NoOpSentryClientTest { fun `captureEnvelope returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureEnvelope(mock())) + @Test + fun `captureFeedback returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureFeedback(mock(), mock(), mock())) + @Test fun `close does not affect captureEvent`() { sut.close() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 2b29f06a0c..2ba0b1b2e7 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -293,59 +293,6 @@ class SentryClientTest { assertEquals(SentryLevel.DEBUG, sentEvent!!.level) } - @Test - fun `when captureFeedback is called, sentry event contains feedback in contexts and header type`() { - var sentEvent: SentryEvent? = null - fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } - val sut = fixture.getSut() - val scope = createScope() - sut.captureFeedback(Feedback("message"), null, scope) - - val sentFeedback = sentEvent!!.contexts.feedback - assertNotNull(sentFeedback) - assertEquals("message", sentFeedback.message) - assertNull(sentFeedback.replayId) - assertNull(sentFeedback.url) - - verify(fixture.transport).send( - check { - assertEquals(SentryItemType.Feedback, it.items.first().header.type) - }, - anyOrNull() - ) - } - - @Test - fun `when captureFeedback, scope replay id is attached to feedback`() { - var sentEvent: SentryEvent? = null - fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } - val replayId = SentryId() - val sut = fixture.getSut() - val scope = createScope() - scope.replayId = replayId - sut.captureFeedback(Feedback("message"), null, scope) - - val sentFeedback = sentEvent!!.contexts.feedback - assertNotNull(sentFeedback) - assertEquals(replayId.toString(), sentFeedback.replayId?.toString()) - assertNull(sentFeedback.url) - } - - @Test - fun `when captureFeedback, screen is attached to feedback as url`() { - var sentEvent: SentryEvent? = null - fixture.sentryOptions.setBeforeSend { e, _ -> sentEvent = e; e } - val sut = fixture.getSut() - val scope = createScope() - scope.screen = "screen" - sut.captureFeedback(Feedback("message"), null, scope) - - val sentFeedback = sentEvent!!.contexts.feedback - assertNotNull(sentFeedback) - assertEquals("screen", sentFeedback.url) - assertNull(sentFeedback.replayId) - } - @Test fun `when event has release, value from options not applied`() { val event = SentryEvent() @@ -2804,6 +2751,8 @@ class SentryClientTest { verify(fixture.transport).send(anyOrNull(), anyOrNull()) } + //region Replay + @Test fun `when captureReplayEvent, envelope is sent`() { val sut = fixture.getSut() @@ -3044,6 +2993,194 @@ class SentryClientTest { ) } + //endregion + + //region Feedback + + @Test + fun `when captureFeedback is called, sentry event contains feedback in contexts and header type`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSendFeedback { e, _ -> sentEvent = e; e } + val sut = fixture.getSut() + val scope = createScope() + sut.captureFeedback(Feedback("message"), null, scope) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + assertEquals("message", sentFeedback.message) + assertNull(sentFeedback.replayId) + assertNull(sentFeedback.url) + + verify(fixture.transport).send( + check { + assertEquals(SentryItemType.Feedback, it.items.first().header.type) + }, + anyOrNull() + ) + } + + @Test + fun `when captureFeedback, scope data is attached to feedback`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSendFeedback { e, _ -> sentEvent = e; e } + val sut = fixture.getSut() + val scope = createScope() + val scopeReplayId = SentryId() + scope.contexts.setTrace(SpanContext("test")) + scope.setContexts("context-key", "context-value") + scope.screen = "screen" + scope.replayId = scopeReplayId + sut.captureFeedback(Feedback("message"), null, scope) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + // User, tags and contexts are applied to the feedback + assertEquals(scope.user, sentEvent!!.user) + assertEquals("tags", sentEvent!!.tags!!["tags"]) + assertEquals( + scope.contexts.trace!!.traceId.toString(), + sentEvent!!.contexts.trace!!.traceId.toString() + ) + assertEquals(mapOf("value" to "context-value"), sentEvent!!.contexts["context-key"]) + // currently running replay id set in scope is applied to feedback + assertEquals(scopeReplayId, sentFeedback.replayId) + // screen set to scope is applied as url + assertEquals("screen", sentFeedback.url) + // extras and breadcrumbs are not applied to feedback + assertNull(sentEvent!!.extras) + assertNull(sentEvent!!.breadcrumbs) + } + + @Test + fun `when captureFeedback, replay controller is stopped if no replay id is provided`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSendFeedback { e, _ -> sentEvent = e; e } + val replayController = mock() + val replayId = SentryId() + val scope = createScope() + whenever(replayController.captureReplay(any())).thenAnswer { + run { scope.replayId = replayId } + } + val sut = fixture.getSut { it.setReplayController(replayController) } + // When there is no replay id in the feedback + sut.captureFeedback(Feedback("message"), null, scope) + + // Then the replay controller captures the replay + verify(replayController).captureReplay(eq(false)) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + // And the replay id is set to the one from the scope (coming from the replay controller) + assertEquals(replayId, sentFeedback.replayId) + } + + @Test + fun `when captureFeedback, replay controller is not stopped if replay id is provided`() { + var sentEvent: SentryEvent? = null + fixture.sentryOptions.setBeforeSendFeedback { e, _ -> sentEvent = e; e } + val replayController = mock() + val replayId = SentryId() + val scope = createScope() + whenever(replayController.captureReplay(any())).thenAnswer { + run { scope.replayId = replayId } + } + val sut = fixture.getSut { it.setReplayController(replayController) } + // When there is replay id in the feedback + val feedback = Feedback("message") + feedback.setReplayId(SentryId()) + sut.captureFeedback(feedback, null, scope) + + // Then the replay controller doesn't capture the replay + verify(replayController, never()).captureReplay(any()) + + val sentFeedback = sentEvent!!.contexts.feedback + assertNotNull(sentFeedback) + // And the replay id is set to the one from the scope (coming from the replay controller) + assertNotNull(sentFeedback.replayId) + assertNotEquals(replayId, sentFeedback.replayId) + } + + @Test + fun `when beforeSendFeedback is set, callback is invoked`() { + var invoked = false + fixture.sentryOptions.setBeforeSendFeedback { event: SentryEvent, _: Hint -> invoked = true; event } + fixture.getSut().captureFeedback(Feedback("message"), null, createScope()) + assertTrue(invoked) + } + + @Test + fun `when beforeSendFeedback returns null, feedback is dropped`() { + fixture.sentryOptions.setBeforeSendFeedback { event: SentryEvent, _: Hint -> null } + fixture.getSut().captureFeedback(Feedback("message"), null, createScope()) + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1) + ) + ) + } + + @Test + fun `when beforeSendFeedback returns new instance, new instance is sent`() { + val expected = SentryEvent().apply { contexts.setFeedback(Feedback("expected")) } + fixture.sentryOptions.setBeforeSendFeedback { _, _ -> expected } + + fixture.getSut().captureFeedback(Feedback("sent"), null, Scope(fixture.sentryOptions)) + + verify(fixture.transport).send( + check { + val event = getEventFromData(it.items.first().data) + assertEquals("expected", event.contexts.feedback!!.message) + }, + anyOrNull() + ) + verifyNoMoreInteractions(fixture.transport) + } + + @Test + fun `when beforeSendFeedback throws an exception, feedback is dropped`() { + val exception = Exception("test") + fixture.sentryOptions.setBeforeSendFeedback { _, _ -> throw exception } + val id = fixture.getSut().captureFeedback(Feedback("message"), null, Scope(fixture.sentryOptions)) + assertEquals(SentryId.EMPTY_ID, id) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1) + ) + ) + } + + @Test + fun `when feedback is dropped, captures client report with datacategory feedback`() { + fixture.sentryOptions.addEventProcessor(DropEverythingEventProcessor()) + val sut = fixture.getSut() + sut.captureFeedback(Feedback("message"), null, createScope()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Feedback.category, 1)) + ) + } + + @Test + fun `captureFeedback does not capture replay when backfilled`() { + val replayController = mock() + val sut = fixture.getSut { it.setReplayController(replayController) } + + sut.captureFeedback( + Feedback("message"), + HintUtils.createWithTypeCheckHint(BackfillableHint()), + createScope() + ) + verify(replayController, never()).captureReplay(any()) + } + + //endregion + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope { val scope = createScope(fixture.sentryOptions) scope.startSession() diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index efa22d375b..ffcacf20b6 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -972,9 +972,19 @@ class SentryTest { Sentry.captureFeedback(feedback) Sentry.captureFeedback(feedback, hint) + Sentry.captureFeedback(feedback, hint) { it.setTag("testKey", "testValue") } verify(client).captureFeedback(eq(feedback), eq(null), anyOrNull()) - verify(client).captureFeedback(eq(feedback), eq(hint), anyOrNull()) + verify(client).captureFeedback( + eq(feedback), + eq(hint), + check { assertFalse(it.tags.containsKey("testKey")) } + ) + verify(client).captureFeedback( + eq(feedback), + eq(hint), + check { assertEquals("testValue", it.tags["testKey"]) } + ) } @Test diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 23f086dd04..3a09d2258f 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -26,6 +26,7 @@ import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.UserFeedback import io.sentry.dsnString import io.sentry.hints.Retryable +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User @@ -54,6 +55,7 @@ class ClientReportTest { val scopes = mock() whenever(scopes.options).thenReturn(opts) val transaction = SentryTracer(TransactionContext("name", "op"), scopes) + val feedbackEvent = SentryEvent().apply { contexts.setFeedback(Feedback("message")) } val lostClientReport = ClientReport( DateUtils.getCurrentDateTime(), @@ -73,13 +75,14 @@ class ClientReportTest { SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000), SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer), SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)), - SentryEnvelopeItem.fromReplay(opts.serializer, opts.logger, SentryReplayEvent(), ReplayRecording(), false) + SentryEnvelopeItem.fromReplay(opts.serializer, opts.logger, SentryReplayEvent(), ReplayRecording(), false), + SentryEnvelopeItem.fromEvent(opts.serializer, feedbackEvent) ) clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope) val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() - testHelper.assertTotalCount(15, clientReportAtEnd) + testHelper.assertTotalCount(16, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) @@ -92,6 +95,7 @@ class ClientReportTest { testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Monitor, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Replay, 1, clientReportAtEnd) + testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Feedback, 1, clientReportAtEnd) } @Test @@ -136,7 +140,7 @@ class ClientReportTest { DateUtils.getCurrentDateTime(), listOf( DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 3), - DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 2), + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 2), DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.Transaction.category, 1), DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Profile.category, 2) ) @@ -149,7 +153,7 @@ class ClientReportTest { val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() testHelper.assertTotalCount(8, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) - testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) + testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Profile, 2, clientReportAtEnd) } diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt index 5ea43e377c..1ab6623d90 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt @@ -21,6 +21,7 @@ class ContextsSerializationTest { setOperatingSystem(OperatingSystemSerializationTest.Fixture().getSut()) setRuntime(SentryRuntimeSerializationTest.Fixture().getSut()) setGpu(GpuSerializationTest.Fixture().getSut()) + setFeedback(FeedbackTest.Fixture().getSut()) setResponse(ResponseSerializationTest.Fixture().getSut()) setTrace(SpanContextSerializationTest.Fixture().getSut()) setSpring(SpringSerializationTest.Fixture().getSut()) diff --git a/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt b/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt index 17649ddc96..e092cb9ff2 100644 --- a/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/FeedbackTest.kt @@ -1,5 +1,7 @@ package io.sentry.protocol +import io.sentry.ILogger +import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -7,11 +9,23 @@ import kotlin.test.assertNotSame class FeedbackTest { + class Fixture { + val logger = mock() + + fun getSut() = Feedback("message").apply { + name = "name" + contactEmail = "contact@email.com" + url = "url" + setReplayId(SentryId("00000000-0000-0000-0000-000000000001")) + setAssociatedEventId(SentryId("00000000-0000-0000-0000-000000000002")) + unknown = mapOf(Pair("unknown", "unknown")) + } + } + private val fixture = Fixture() + @Test fun `copying feedback wont have the same references`() { - val feedback = Feedback("message") - val unknown = mapOf(Pair("unknown", "unknown")) - feedback.setUnknown(unknown) + val feedback = fixture.getSut() val clone = Feedback(feedback) @@ -22,13 +36,7 @@ class FeedbackTest { @Test fun `copying feedback will have the same values`() { - val feedback = Feedback("message") - feedback.name = "name" - feedback.contactEmail = "contact@email.com" - feedback.url = "url" - feedback.setReplayId(SentryId("00000000-0000-0000-0000-000000000001")) - feedback.setAssociatedEventId(SentryId("00000000-0000-0000-0000-000000000002")) - feedback.unknown = mapOf(Pair("unknown", "unknown")) + val feedback = fixture.getSut() val clone = Feedback(feedback) assertEquals("message", clone.message) diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 5be5a22802..22b3a27f73 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -26,6 +26,7 @@ import io.sentry.UserFeedback import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.IClientReportRecorder import io.sentry.hints.DiskFlushNotification +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User @@ -206,14 +207,16 @@ class RateLimiterTest { val scopes = mock() whenever(scopes.options).thenReturn(SentryOptions()) val transaction = SentryTracer(TransactionContext("name", "op"), scopes) + val feedbackEvent = SentryEvent().apply { contexts.setFeedback(Feedback("message")) } val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release")) val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val checkInItem = SentryEnvelopeItem.fromCheckIn(fixture.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) + val feedbackEventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, feedbackEvent) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, profileChunkItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, profileChunkItem, feedbackEventItem)) rateLimiter.updateRetryAfterLimits(null, null, 429) val result = rateLimiter.filter(envelope, Hint()) @@ -227,6 +230,7 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(checkInItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(feedbackEventItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } @@ -354,6 +358,25 @@ class RateLimiterTest { verifyNoMoreInteractions(fixture.clientReportRecorder) } + @Test + fun `drop feedback items as lost`() { + val rateLimiter = fixture.getSUT() + + val feedbackEvent = SentryEvent().apply { contexts.setFeedback(Feedback("message")) } + val feedbackEventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, feedbackEvent) + val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(feedbackEventItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:feedback:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(feedbackEventItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + @Test fun `apply rate limits notifies observers`() { val rateLimiter = fixture.getSUT() diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index 8eb4000fc6..6f58243127 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -65,6 +65,16 @@ "processor_frequency": 800.0, "cpu_description": "cpu0" }, + "feedback": + { + "message": "message", + "contact_email": "contact@email.com", + "name": "name", + "associated_event_id": "00000000000000000000000000000002", + "replay_id": "00000000000000000000000000000001", + "url": "url", + "unknown": "unknown" + }, "gpu": { "name": "d623a6b5-e1ab-4402-931b-c06f5a43a5ae", From 8feeadcd9fd08bddbd0b5fd8f10a70a0970d00b9 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 17 Apr 2025 17:49:20 +0200 Subject: [PATCH 09/35] fixed tests --- .../io/sentry/android/core/SessionTrackingIntegrationTest.kt | 5 +++++ .../sentry/protocol/CombinedContextsViewSerializationTest.kt | 1 + 2 files changed, 6 insertions(+) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index b67f8aa288..9e79e79b47 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -21,6 +21,7 @@ import io.sentry.SentryReplayEvent import io.sentry.Session import io.sentry.TraceContext import io.sentry.UserFeedback +import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.transport.RateLimiter @@ -147,6 +148,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureFeedback(feedback: Feedback, hint: Hint?, scope: IScope): SentryId { + TODO("Not yet implemented") + } + override fun captureReplayEvent( event: SentryReplayEvent, scope: IScope?, diff --git a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt index f5e92713eb..80c9db6ce9 100644 --- a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt @@ -24,6 +24,7 @@ class CombinedContextsViewSerializationTest { current.setApp(AppSerializationTest.Fixture().getSut()) current.setBrowser(BrowserSerializationTest.Fixture().getSut()) + current.setFeedback(FeedbackTest.Fixture().getSut()) current.setTrace(SpanContextSerializationTest.Fixture().getSut()) isolation.setDevice(DeviceSerializationTest.Fixture().getSut()) From 986772dd625f41fd091f83b491836d62c28b15ad Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 29 Apr 2025 17:56:13 +0200 Subject: [PATCH 10/35] started adding resources for UF widget --- .../main/res/drawable/edit_text_border.xml | 15 +++ .../drawable/sentry_logo_dark_400x352.webp | Bin 0 -> 2854 bytes .../layout/sentry_dialog_user_feedback.xml | 109 ++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 sentry-android-core/src/main/res/drawable/edit_text_border.xml create mode 100644 sentry-android-core/src/main/res/drawable/sentry_logo_dark_400x352.webp create mode 100644 sentry-android-core/src/main/res/layout/sentry_dialog_user_feedback.xml diff --git a/sentry-android-core/src/main/res/drawable/edit_text_border.xml b/sentry-android-core/src/main/res/drawable/edit_text_border.xml new file mode 100644 index 0000000000..5615e31857 --- /dev/null +++ b/sentry-android-core/src/main/res/drawable/edit_text_border.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/sentry-android-core/src/main/res/drawable/sentry_logo_dark_400x352.webp b/sentry-android-core/src/main/res/drawable/sentry_logo_dark_400x352.webp new file mode 100644 index 0000000000000000000000000000000000000000..3f93b4beda1ccc4427c0ee41d69609365535e3e2 GIT binary patch literal 2854 zcmV+>3)%EiNk&E<3jhFDMM6+kP&iBx3jhEwkHJ?ECm;|Y2=afYgCN^x8`S;HE+8S8 z>HiN21PK3ClnH>=h~92NJCY>3kp!S2z@s4mFgpS?b`=2ER)3-H#p=jH@;)Q_PYbry zuwjJN(r4LkV_B8};bgyu^xL1WuXX+USqAglpKbluc6$HE_xhjPFW+)+>znPeEp=Vr z7T7S9d7KEFXZR_Q_>ccW$#!S$bu% ziaViG{2}1cFOyOHA>ayZUTZ$U6ucAiGFd~&=@Wkh(uI-9CI(17vO>^e4FoOYugx-P zq^*7r!ww9<8Xrdvx1_PhFAPt! z_|?@z%(C`;q?cP(%dqv$PMcHGqt9B{BcxVYJ3EP2);{Rfh_DRz_|(*4N_b_pNs102 zH9~6Ci0FlrOFRQkkyqAEQ^F{#QBu?)XdX#pt6cy*J+eYGv_)3P5cg?B7&Ik-ISHHF zY_i%kCGarJ9U=8-L>MH+W>$#Q9xK?;{#ZHa&xqjQYz0T{1B}!hD>_koV`U#$ z_a}n^Gt885$4ZjAV?{|V8WFafxE#(*gwz+SpuNqpqNap3BLXIM7BHhp{jq{hoGDhx zlkCcfum%P8KW<9cVil0uVik~jGa^hmabWyNHn>`16+us|8d6V2ge_7*utG#?j1_m{ ze6b3gI6Fp!5E{ZVQa`LZ(nsT98yF^C`d;s`>crJGqGl2rAq~D+=@_=?8;OQVYxt#h zzohzicB>20wssY(PTZpZx`gHe*0rbTOo^wzP;K6cyE|V8*O40faxERHpItgT-A^eh zSPYd}W=i-emsuz7SJy7HPFxj#=}rqpISiGTDtzXs6j}Jp55c7hpE)X%+wj?sj#Fm0 z3m3KH6bWguP*UY0*zv73;WJOA*j*8;apgA`p~jWnCF7^B#o_Iq;V zD>bHey%bqsu1fKn1y;k#Zx-0yS#9-|n%1SvzEV@eiVvUpQic<|_pmaY*j=MaKx%8( zY=^X#oa%w3ZM^^oAgs)U&z6-aM^u@CmFa6Ws=Sc?^Py2Ciad*yYMD!;&c}t%n9ICG z>QUh{?gsyqsLKd!Tz9*pejIH|&(y+oSMmb`U-jtORc{Vguo`whC+m zns|x8XQgf9syyG6=vYCe)y0xNY)KneGfQeCZF`T|5>F?sniR-8)^AwShow7Tv?^@) z%tp5J9he%Ylas*2hV8uEO!mvN7<<> zVh1M2>OQ(&Ea}r^omk?6k{bE#)fyUCA4|F-+Y9YPC;Aj;$L#30h2TR;7f7xSVsWQ= zrLQ4x?j3?CX)a7Dlz2v+xKvoJ+A98n=tloA`~ZLp-V`t|>#M)s4a> z0`t~%rE-$+fuuEdw~w&O3D2~6l4K5s3|f>lD=w1E!Ch;1j9D?AsMhf*XOc84t0+-z zBVmoetgO8XgeghsRh>>Xk?>ihwdeNTThJjTAxWARckU`&FMXj-Tt?QJ$%~?Xv&~d3mpDPCoUm=ugHVj zgGm?W$0bR%Nl9PXSVNZ@H16~sO@uTZYe<0yH!*3l8gbgyF0Y>h8YskiriJt7ho#oM;606)dT;NP}G`)L&jhZqnA1 z$|;9iAVEDjY3ooV9HCx156Q!4KRT2eE`bAaQUQ5fk#J@@*f^wj;%Z!}U6sh7k>81{ zVMUH>*n#PtxE5ESt3n+Jlcp*s86;vFW#UOw`3>BP1k)%Zd)-A=VCxR6i1bl5e3olh z2}4oEIS5G8d)9|)$!VrZbL}czFq-LG6HS_H*Gn744Lxh@!bP%OGqxhZ9r%bP;~FH! zV1W*#Nrk(lNd_ZzAZtoUm2xfk#q^faMVboU(Sw!oq{!Hpid68O8(2IEIxr_GnDrry zC+#807(N5DK7{e4Blf7FSzy`J3_ZgqbRrB(HZ=(=?ucX~rPqR$6f-Sy-_L zycwD_TlFNYSYkaCee*%BWQkzViTOxVtOaWh9G(RCh>U|S4g)vHo($frZbbq*F%#)8 zg_@b}cR?p+BJKXR(xG7|@2V4NC$6)3&VpaY1$Dn!e&VpKe9W-m#8CH}m=hDhZS602 z2tsif=0u*f>!}0Eo+;qE1tbmW0zJBk0tCc;UH!K1+a3OW#>%2NSZO?za(k{x&i zd>NjU=&BbRcW1iG$OsRShA!+YXR7$c9q`Grz`!)EU6ELjYmEdga*#GKok$aKJt3+b zqzgCvR;J=+PNe0aMuZv~hElCa%*Y)R{2y*{%QVsziK?I)1%aZ8kr8~*N7acf5DXKL ziXDp`3b-Ov0ZBosO?5tF7ptH;2L&m%ipi}=sAi=t81)w0VoMlj3b@YH#D#~?Kr1Iv zz;!|LBjouAObH-x3*QgbiAO*bq~O|ko`CDcOu%~5uK07Ffa}RjT#Aaq z84-yBu9)!QGce^sm!VmSJ}+2=Xb}jo!WFU4CYZR+K>}y(^31U&aR+KZB&6VPE$QMg ziiv9&B%~mQ;xMAoV-YNF-F;46KbGZlC??=pv+7CJ|BuX!O>VhH&pMk8*@~B8QNp?h-^bXcY zP9&Z>%pg*Q&zdSl;g+}qj6ozaCBXFJB=L(EV-Sf*!9@5ZX7t5~ij*@VlEh(7#;{Y7 z#-ymSz($Q6v~*0g!7p>dL}5k~QdIB+YmhjM<}Npp>>=5NDY;vU-g=9&(vHx9krZj6 z5O*0773tKt#7ShVCR!(r43&@(lqE-RRbBNGXsv_GSVzVxcx($J(VYLLg=|A*ycJ-b)2vOc9yUd<$Q?$%g#=Da_~#aeO!^loV$4 z$$WMd>H8<=@v`cK{7Ym;KUYrednlSuw#a1fm35ZN{3xk!MLPcujc{uOEv%cj0f|uHOe0Vt8Aq zkGuUp5W=l_jQrK@T}!|mJwArqZe)rG)#|A#u7}J15#%2hqCded`1U8 + + + + + + + + + + + + + + + + + + + +