diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec129f56b4..8f73670ddda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # vNext * Enchancement: Improve EventProcessor nullability annotations (#1229). +* Enchancement: Add "data" property to Span (#1235). * Fix: Disable Gson HTML escaping # 4.1.0 diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 831f0bf029d..6d55f96ccb5 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -67,17 +67,6 @@ public static void main(String[] args) throws InterruptedException { // Performance configuration options // Set what percentage of traces should be collected options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces - - // Determine traces sample rate based on the sampling context - options.setTracesSampler( - context -> { - // only 10% of transactions with "/product" prefix will be collected - if (!context.getTransactionContext().getName().startsWith("/products")) { - return 0.1; - } else { - return 0.5; - } - }); }); Sentry.addBreadcrumb( @@ -139,8 +128,11 @@ public static void main(String[] args) throws InterruptedException { // Transactions collect execution time of the piece of code that's executed between the start // and finish of transaction. ITransaction transaction = Sentry.startTransaction("transaction name"); + transaction.setData("data-key", "data-value"); + Sentry.configureScope(scope -> scope.setTransaction(transaction)); // Transactions can contain one or more Spans ISpan outerSpan = transaction.startChild("child"); + outerSpan.setData("span-data-key", "span-data-value"); Thread.sleep(100); // Spans create a tree structure. Each span can have one ore more spans inside. ISpan innerSpan = outerSpan.startChild("jdbc", "select * from product where id = :id"); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3695d304b3e..47352225c9b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -282,12 +282,14 @@ public abstract interface class io/sentry/ISerializer { public abstract interface class io/sentry/ISpan { public abstract fun finish ()V public abstract fun finish (Lio/sentry/SpanStatus;)V + public abstract fun getData (Ljava/lang/String;)Ljava/lang/Object; public abstract fun getDescription ()Ljava/lang/String; public abstract fun getOperation ()Ljava/lang/String; public abstract fun getSpanContext ()Lio/sentry/SpanContext; public abstract fun getStatus ()Lio/sentry/SpanStatus; public abstract fun getThrowable ()Ljava/lang/Throwable; public abstract fun isFinished ()Z + public abstract fun setData (Ljava/lang/String;Ljava/lang/Object;)V public abstract fun setDescription (Ljava/lang/String;)V public abstract fun setOperation (Ljava/lang/String;)V public abstract fun setStatus (Lio/sentry/SpanStatus;)V @@ -348,6 +350,7 @@ public final class io/sentry/NoOpLogger : io/sentry/ILogger { public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/NoOpSpan; public fun getOperation ()Ljava/lang/String; @@ -355,6 +358,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun getStatus ()Lio/sentry/SpanStatus; public fun getThrowable ()Ljava/lang/Throwable; public fun isFinished ()Z + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setOperation (Ljava/lang/String;)V public fun setStatus (Lio/sentry/SpanStatus;)V @@ -369,6 +373,7 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; public fun getEventId ()Lio/sentry/protocol/SentryId; public static fun getInstance ()Lio/sentry/NoOpTransaction; @@ -383,6 +388,7 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun getTransaction ()Ljava/lang/String; public fun isFinished ()Z public fun isSampled ()Ljava/lang/Boolean; + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setName (Ljava/lang/String;)V public fun setOperation (Ljava/lang/String;)V @@ -547,15 +553,19 @@ public abstract class io/sentry/SentryBaseEvent { public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getEnvironment ()Ljava/lang/String; public fun getEventId ()Lio/sentry/protocol/SentryId; + public fun getExtra (Ljava/lang/String;)Ljava/lang/Object; public fun getOriginThrowable ()Ljava/lang/Throwable; public fun getRelease ()Ljava/lang/String; public fun getRequest ()Lio/sentry/protocol/Request; public fun getSdk ()Lio/sentry/protocol/SdkVersion; public fun getTag (Ljava/lang/String;)Ljava/lang/String; public fun getThrowable ()Ljava/lang/Throwable; + public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun setEnvironment (Ljava/lang/String;)V public fun setEventId (Lio/sentry/protocol/SentryId;)V + public fun setExtra (Ljava/lang/String;Ljava/lang/Object;)V + public fun setExtras (Ljava/util/Map;)V public fun setRelease (Ljava/lang/String;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setSdk (Lio/sentry/protocol/SdkVersion;)V @@ -638,7 +648,6 @@ public final class io/sentry/SentryEvent : io/sentry/SentryBaseEvent, io/sentry/ public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; public fun getDist ()Ljava/lang/String; public fun getExceptions ()Ljava/util/List; - public fun getExtra (Ljava/lang/String;)Ljava/lang/Object; public fun getFingerprints ()Ljava/util/List; public fun getLevel ()Lio/sentry/SentryLevel; public fun getLogger ()Ljava/lang/String; @@ -653,14 +662,11 @@ public final class io/sentry/SentryEvent : io/sentry/SentryBaseEvent, io/sentry/ public fun getUser ()Lio/sentry/protocol/User; public fun isCrashed ()Z public fun isErrored ()Z - public fun removeExtra (Ljava/lang/String;)V public fun removeModule (Ljava/lang/String;)V public fun setBreadcrumbs (Ljava/util/List;)V public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V public fun setDist (Ljava/lang/String;)V public fun setExceptions (Ljava/util/List;)V - public fun setExtra (Ljava/lang/String;Ljava/lang/Object;)V - public fun setExtras (Ljava/util/Map;)V public fun setFingerprints (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V public fun setLogger (Ljava/lang/String;)V @@ -842,6 +848,7 @@ public final class io/sentry/SentryTransaction : io/sentry/SentryBaseEvent, io/s public fun (Ljava/lang/String;Lio/sentry/SpanContext;Lio/sentry/IHub;)V public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; public fun getLatestActiveSpan ()Lio/sentry/Span; public fun getName ()Ljava/lang/String; @@ -852,6 +859,7 @@ public final class io/sentry/SentryTransaction : io/sentry/SentryBaseEvent, io/s public fun getTransaction ()Ljava/lang/String; public fun isFinished ()Z public fun isSampled ()Ljava/lang/Boolean; + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setName (Ljava/lang/String;)V public fun setOperation (Ljava/lang/String;)V @@ -911,11 +919,13 @@ public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration { public final class io/sentry/Span : io/sentry/SpanContext, io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getSpanContext ()Lio/sentry/SpanContext; public fun getStartTimestamp ()Ljava/util/Date; public fun getThrowable ()Ljava/lang/Throwable; public fun getTimestamp ()Ljava/util/Date; public fun isFinished ()Z + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index bd78c8dc05b..0e9d9a3dc8a 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -118,6 +118,17 @@ public interface ISpan { */ void setTag(@NotNull String key, @NotNull String value); + /** + * Set extra data on span on transaction + * + * @param key the data key + * @param value the data value + */ + void setData(@NotNull String key, @NotNull Object value); + + @Nullable + Object getData(@NotNull String key); + /** * Returns if span has finished. * diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 8c6530f164c..b040f8969ec 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -76,6 +76,14 @@ public void setThrowable(@Nullable Throwable throwable) {} @Override public void setTag(@NotNull String key, @NotNull String value) {} + @Override + public void setData(@NotNull String key, @NotNull Object value) {} + + @Override + public @Nullable Object getData(@NotNull String key) { + return null; + } + @Override public boolean isFinished() { return false; diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index 071230145f4..72331179431 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -130,4 +130,12 @@ public void setThrowable(@Nullable Throwable throwable) {} @Override public void setTag(@NotNull String key, @NotNull String value) {} + + @Override + public void setData(@NotNull String key, @NotNull Object value) {} + + @Override + public @Nullable Object getData(@NotNull String key) { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/SentryBaseEvent.java b/sentry/src/main/java/io/sentry/SentryBaseEvent.java index 096219b9b37..1ae72181c81 100644 --- a/sentry/src/main/java/io/sentry/SentryBaseEvent.java +++ b/sentry/src/main/java/io/sentry/SentryBaseEvent.java @@ -7,6 +7,7 @@ import io.sentry.protocol.SentryId; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -59,6 +60,13 @@ public abstract class SentryBaseEvent { */ private String environment; + /** + * Arbitrary extra information set by the user. + * + *

```json { "extra": { "my_key": 1, "some_other_value": "foo bar" } }``` + */ + private final Map extra = new ConcurrentHashMap<>(); + /** The captured Throwable */ protected transient @Nullable Throwable throwable; @@ -174,4 +182,34 @@ public String getEnvironment() { public void setEnvironment(String environment) { this.environment = environment; } + + @NotNull + Map getExtras() { + return extra; + } + + /** + * Copies extras from the map given by parameter to extras on the event. If key already exists, + * its value gets overwritten. + * + * @param extra - the extras + */ + public void setExtras(final @NotNull Map extra) { + final Map copy = new HashMap<>(extra); + for (Map.Entry entry : copy.entrySet()) { + this.extra.put(entry.getKey(), entry.getValue()); + } + } + + public void setExtra(final @NotNull String key, final @NotNull Object value) { + extra.put(key, value); + } + + public void removeExtra(final @NotNull String key) { + extra.remove(key); + } + + public @Nullable Object getExtra(final @NotNull String key) { + return extra.get(key); + } } diff --git a/sentry/src/main/java/io/sentry/SentryEvent.java b/sentry/src/main/java/io/sentry/SentryEvent.java index 4d1179f64c9..6ce6a946504 100644 --- a/sentry/src/main/java/io/sentry/SentryEvent.java +++ b/sentry/src/main/java/io/sentry/SentryEvent.java @@ -97,13 +97,6 @@ public final class SentryEvent extends SentryBaseEvent implements IUnknownProper /** List of breadcrumbs recorded before this event. */ private List breadcrumbs; - /** - * Arbitrary extra information set by the user. - * - *

```json { "extra": { "my_key": 1, "some_other_value": "foo bar" } }``` - */ - private Map extra; - private Map unknown; /** * Name and versions of all installed modules/packages/dependencies in the current @@ -261,34 +254,6 @@ public void addBreadcrumb(final @Nullable String message) { this.addBreadcrumb(new Breadcrumb(message)); } - Map getExtras() { - return extra; - } - - public void setExtras(Map extra) { - this.extra = extra; - } - - public void setExtra(String key, Object value) { - if (extra == null) { - extra = new HashMap<>(); - } - extra.put(key, value); - } - - public void removeExtra(@NotNull String key) { - if (extra != null) { - extra.remove(key); - } - } - - public @Nullable Object getExtra(final @NotNull String key) { - if (extra != null) { - return extra.get(key); - } - return null; - } - @ApiStatus.Internal @Override public void acceptUnknownProperties(Map unknown) { diff --git a/sentry/src/main/java/io/sentry/SentryTransaction.java b/sentry/src/main/java/io/sentry/SentryTransaction.java index eb1c2cdb9d3..753af135761 100644 --- a/sentry/src/main/java/io/sentry/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/SentryTransaction.java @@ -24,6 +24,7 @@ public final class SentryTransaction extends SentryBaseEvent implements ITransac /** A list of spans within this transaction. Can be empty. */ private final @NotNull List spans = new CopyOnWriteArrayList<>(); + /** * A hub this transaction is attached to. Marked as transient to be ignored during JSON * serialization. @@ -204,6 +205,16 @@ public void setDescription(@Nullable String description) { return this.context; } + @Override + public void setData(@NotNull String key, @NotNull Object value) { + setExtra(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return getExtra(key); + } + /** * Sets transaction status. * diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 6e9908bff17..81b26ebdcb5 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -3,6 +3,8 @@ import io.sentry.protocol.SentryId; import io.sentry.util.Objects; import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +15,9 @@ public final class Span extends SpanContext implements ISpan { /** The moment in time when span has ended. */ private @Nullable Date timestamp; + /** Arbitrary data associated with the Span. */ + private final Map data = new ConcurrentHashMap<>(); + /** * A transaction this span is attached to. Marked as transient to be ignored during JSON * serialization. @@ -78,6 +83,16 @@ public void finish(@Nullable SpanStatus status) { return this; } + @Override + public void setData(@NotNull String key, @NotNull Object value) { + data.put(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return data.get(key); + } + @Override public boolean isFinished() { return this.timestamp != null; diff --git a/sentry/src/test/java/io/sentry/GsonSerializerTest.kt b/sentry/src/test/java/io/sentry/GsonSerializerTest.kt index af925981e5b..799323d89ec 100644 --- a/sentry/src/test/java/io/sentry/GsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/GsonSerializerTest.kt @@ -46,18 +46,10 @@ class GsonSerializerTest { fixture = Fixture() } - private fun serializeToString(ev: SentryEvent): String { + private fun serializeToString(ev: Any): String { return this.serializeToString { wrt -> fixture.serializer.serialize(ev, wrt) } } - private fun serializeToString(session: Session): String { - return this.serializeToString { wrt -> fixture.serializer.serialize(session, wrt) } - } - - private fun serializeToString(userFeedback: UserFeedback): String { - return this.serializeToString { wrt -> fixture.serializer.serialize(userFeedback, wrt) } - } - private fun serializeToString(serialize: (StringWriter) -> Unit): String { val wrt = StringWriter() serialize(wrt) @@ -77,7 +69,7 @@ class GsonSerializerTest { val actual = serializeToString(sentryEvent) - val expected = "{\"event_id\":\"${sentryEvent.eventId}\",\"contexts\":{}}" + val expected = "{\"event_id\":\"${sentryEvent.eventId}\",\"contexts\":{},\"extra\":{}}" assertEquals(expected, actual) } @@ -98,7 +90,7 @@ class GsonSerializerTest { val sentryEvent = generateEmptySentryEvent(DateUtils.getDateTime(dateIsoFormat)) sentryEvent.eventId = null - val expected = "{\"timestamp\":\"$dateIsoFormat\",\"contexts\":{}}" + val expected = "{\"timestamp\":\"$dateIsoFormat\",\"contexts\":{},\"extra\":{}}" val actual = serializeToString(sentryEvent) @@ -194,7 +186,7 @@ class GsonSerializerTest { val actual = serializeToString(sentryEvent) - val expected = "{\"unknown\":{\"object\":{\"boolean\":true,\"int\":1}},\"contexts\":{}}" + val expected = "{\"unknown\":{\"object\":{\"boolean\":true,\"int\":1}},\"contexts\":{},\"extra\":{}}" assertEquals(expected, actual) } @@ -207,7 +199,7 @@ class GsonSerializerTest { device.timezone = TimeZone.getTimeZone("Europe/Vienna") sentryEvent.contexts.device = device - val expected = "{\"contexts\":{\"device\":{\"timezone\":\"Europe/Vienna\"}}}" + val expected = "{\"contexts\":{\"device\":{\"timezone\":\"Europe/Vienna\"}},\"extra\":{}}" val actual = serializeToString(sentryEvent) @@ -234,7 +226,7 @@ class GsonSerializerTest { device.orientation = Device.DeviceOrientation.LANDSCAPE sentryEvent.contexts.device = device - val expected = "{\"contexts\":{\"device\":{\"orientation\":\"landscape\"}}}" + val expected = "{\"contexts\":{\"device\":{\"orientation\":\"landscape\"}},\"extra\":{}}" val actual = serializeToString(sentryEvent) @@ -259,7 +251,7 @@ class GsonSerializerTest { sentryEvent.eventId = null sentryEvent.level = SentryLevel.DEBUG - val expected = "{\"level\":\"debug\",\"contexts\":{}}" + val expected = "{\"level\":\"debug\",\"contexts\":{},\"extra\":{}}" val actual = serializeToString(sentryEvent) @@ -440,17 +432,18 @@ class GsonSerializerTest { trace.status = SpanStatus.OK trace.setTag("myTag", "myValue") val transaction = SentryTransaction("transaction-name", trace, mock()) + transaction.setData("string-data-key", "data-value") + transaction.setData("integer-data-key", 10) transaction.finish() - val stringWriter = StringWriter() - fixture.serializer.serialize(transaction, stringWriter) - - val element = JsonParser().parse(stringWriter.toString()).asJsonObject + val element = JsonParser().parse(serializeToString(transaction)).asJsonObject assertEquals("transaction-name", element["transaction"].asString) assertEquals("transaction", element["type"].asString) assertNotNull(element["start_timestamp"].asString) assertNotNull(element["event_id"].asString) assertNotNull(element["spans"].asJsonArray) + assertEquals("data-value", element["extra"].asJsonObject["string-data-key"].asString) + assertEquals(10, element["extra"].asJsonObject["integer-data-key"].asInt) val jsonTrace = element["contexts"].asJsonObject["trace"] assertNotNull(jsonTrace.asJsonObject["trace_id"].asString) assertNotNull(jsonTrace.asJsonObject["span_id"].asString) @@ -468,6 +461,10 @@ class GsonSerializerTest { "start_timestamp": "2020-10-23T10:24:01.791Z", "timestamp": "2020-10-23T10:24:02.791Z", "event_id": "3367f5196c494acaae85bbbd535379ac", + "extra": { + "string-data-key": "data-value", + "integer-data-key": 10.5 + }, "contexts": { "trace": { "trace_id": "b156a475de54423d9c1571df97ec7eb6", @@ -486,6 +483,8 @@ class GsonSerializerTest { assertNotNull(transaction.timestamp) assertNotNull(transaction.contexts) assertNotNull(transaction.contexts.trace) + assertEquals("data-value", transaction.getData("string-data-key")) + assertEquals(10.5, transaction.getData("integer-data-key")) assertEquals("b156a475de54423d9c1571df97ec7eb6", transaction.contexts.trace!!.traceId.toString()) assertEquals("0a53026963414893", transaction.contexts.trace!!.spanId.toString()) assertEquals("http", transaction.contexts.trace!!.operation) @@ -493,6 +492,51 @@ class GsonSerializerTest { assertEquals("some-value", (transaction.contexts["custom"] as Map<*, *>)["some-key"]) } + @Test + fun `serializes span`() { + val sentryId = SentryId() + val parentSpanId = SpanId() + val span = Span(sentryId, parentSpanId, SentryTransaction("tx"), NoOpHub.getInstance()) + span.setData("data-key", "data-value") + span.finish() + + val element = JsonParser().parse(serializeToString(span)).asJsonObject + + assertEquals("data-value", element.get("data").asJsonObject.get("data-key").asString) + assertEquals("data-value", element.get("data").asJsonObject.get("data-key").asString) + assertEquals(sentryId.toString(), element.get("trace_id").asString) + assertEquals(parentSpanId.toString(), element.get("parent_span_id").asString) + assertNotNull(element.get("span_id").asString) + assertNotNull(element.get("start_timestamp").asString) + assertNotNull(element.get("timestamp").asString) + } + + @Test + fun `deserializes span`() { + val json = """ + { + "start_timestamp":"2021-02-09T17:19:33.405Z", + "timestamp":"2021-02-09T17:19:33.405Z", + "data":{ + "data-key":"data-value" + }, + "trace_id":"bb185b9ad3ae489eb3645c1d6657b2e0", + "span_id":"f67e9eafb33f444e", + "parent_span_id":"15c304828711435c", + "tags":{} + } + """.trimIndent() + + val span = fixture.serializer.deserialize(StringReader(json), Span::class.java) + assertNotNull(span) + assertNotNull(span.startTimestamp) + assertNotNull(span.timestamp) + assertEquals(SentryId("bb185b9ad3ae489eb3645c1d6657b2e0"), span.traceId) + assertEquals(SpanId("f67e9eafb33f444e"), span.spanId) + assertEquals(SpanId("15c304828711435c"), span.parentSpanId) + assertEquals("data-value", span.getData("data-key")) + } + @Test fun `serializing user feedback`() { val actual = serializeToString(userFeedback)