diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b8ae0af67e..58b41ec5b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Make user segment a top level property ([#2257](https://github.com/getsentry/sentry-java/pull/2257)) - Replace user `other` with `data` ([#2258](https://github.com/getsentry/sentry-java/pull/2258)) +- Provide API for attaching custom measurements to transactions ([#2260](https://github.com/getsentry/sentry-java/pull/2260)) ## 6.5.0-beta.1 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index d71cab5213c..46972447784 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -1,11 +1,10 @@ package io.sentry.android.core; -import static io.sentry.protocol.MeasurementValue.NONE_UNIT; - import android.app.Activity; import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; import io.sentry.ILogger; +import io.sentry.SentryMeasurementUnit; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import java.util.HashMap; @@ -104,9 +103,12 @@ public synchronized void setMetrics( return; } - final MeasurementValue tfValues = new MeasurementValue(totalFrames, NONE_UNIT); - final MeasurementValue sfValues = new MeasurementValue(slowFrames, NONE_UNIT); - final MeasurementValue ffValues = new MeasurementValue(frozenFrames, NONE_UNIT); + final MeasurementValue tfValues = + new MeasurementValue(totalFrames, SentryMeasurementUnit.NONE.apiName()); + final MeasurementValue sfValues = + new MeasurementValue(slowFrames, SentryMeasurementUnit.NONE.apiName()); + final MeasurementValue ffValues = + new MeasurementValue(frozenFrames, SentryMeasurementUnit.NONE.apiName()); final Map measurements = new HashMap<>(); measurements.put("frames_total", tfValues); measurements.put("frames_slow", sfValues); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index a3fca88156e..9d5b4447536 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -3,11 +3,11 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD; import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM; import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; -import static io.sentry.protocol.MeasurementValue.MILLISECOND_UNIT; import io.sentry.EventProcessor; import io.sentry.Hint; import io.sentry.SentryEvent; +import io.sentry.SentryMeasurementUnit; import io.sentry.SpanContext; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; @@ -67,7 +67,8 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // if appStartUpInterval is null, metrics are not ready to be sent if (appStartUpInterval != null) { final MeasurementValue value = - new MeasurementValue((float) appStartUpInterval, MILLISECOND_UNIT); + new MeasurementValue( + (float) appStartUpInterval, SentryMeasurementUnit.MILLISECOND.apiName()); final String appStartKey = AppStartState.getInstance().isColdStart() ? "app_start_cold" : "app_start_warm"; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index df595b6a356..99c197f1e01 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -5,12 +5,12 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import io.sentry.Hint import io.sentry.IHub +import io.sentry.SentryMeasurementUnit import io.sentry.SentryTracer import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP import io.sentry.protocol.MeasurementValue -import io.sentry.protocol.MeasurementValue.MILLISECOND_UNIT import io.sentry.protocol.SentryTransaction import java.util.Date import kotlin.test.BeforeTest @@ -156,7 +156,7 @@ class PerformanceAndroidEventProcessorTest { val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val metrics = mapOf("frames_total" to MeasurementValue(1f, MILLISECOND_UNIT)) + val metrics = mapOf("frames_total" to MeasurementValue(1f, SentryMeasurementUnit.MILLISECOND.apiName())) whenever(fixture.activityFramesTracker.takeMetrics(any())).thenReturn(metrics) tr = sut.process(tr, Hint()) 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 b337973bcf8..dc328720f43 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 @@ -27,6 +27,7 @@ public class MainActivity extends AppCompatActivity { private int crashCount = 0; + private int screenLoadCount = 0; @Override protected void onCreate(Bundle savedInstanceState) { @@ -200,8 +201,10 @@ protected void onCreate(Bundle savedInstanceState) { @Override protected void onResume() { super.onResume(); + screenLoadCount++; final ISpan span = Sentry.getSpan(); if (span != null) { + span.setMeasurement("screen_load_count", screenLoadCount); span.finish(SpanStatus.OK); } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonService.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonService.java index 76d528f585c..0eb67e85788 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonService.java @@ -1,5 +1,7 @@ package io.sentry.samples.spring.boot; +import io.sentry.ISpan; +import io.sentry.Sentry; import io.sentry.spring.tracing.SentrySpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,12 +18,19 @@ public class PersonService { private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class); private final JdbcTemplate jdbcTemplate; + private int createCount = 0; public PersonService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } Person create(Person person) { + createCount++; + final ISpan span = Sentry.getSpan(); + if (span != null) { + span.setMeasurement("create_count", createCount); + } + jdbcTemplate.update( "insert into person (firstName, lastName) values (?, ?)", person.getFirstName(), diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a17ca1471c9..2f7a89269bc 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -483,6 +483,8 @@ public abstract interface class io/sentry/ISpan { 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 setMeasurement (Ljava/lang/String;F)V + public abstract fun setMeasurement (Ljava/lang/String;FLio/sentry/SentryMeasurementUnit;)V public abstract fun setOperation (Ljava/lang/String;)V public abstract fun setStatus (Lio/sentry/SpanStatus;)V public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -668,6 +670,8 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun isFinished ()Z public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;F)V + public fun setMeasurement (Ljava/lang/String;FLio/sentry/SentryMeasurementUnit;)V public fun setOperation (Ljava/lang/String;)V public fun setStatus (Lio/sentry/SpanStatus;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -703,6 +707,8 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun scheduleFinish ()V public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;F)V + public fun setMeasurement (Ljava/lang/String;FLio/sentry/SentryMeasurementUnit;)V public fun setName (Ljava/lang/String;)V public fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun setOperation (Ljava/lang/String;)V @@ -1268,6 +1274,21 @@ public final class io/sentry/SentryLevel : java/lang/Enum, io/sentry/JsonSeriali public static fun values ()[Lio/sentry/SentryLevel; } +public final class io/sentry/SentryMeasurementUnit : java/lang/Enum { + public static final field DAY Lio/sentry/SentryMeasurementUnit; + public static final field HOUR Lio/sentry/SentryMeasurementUnit; + public static final field MICROSECOND Lio/sentry/SentryMeasurementUnit; + public static final field MILLISECOND Lio/sentry/SentryMeasurementUnit; + public static final field MINUTE Lio/sentry/SentryMeasurementUnit; + public static final field NANOSECOND Lio/sentry/SentryMeasurementUnit; + public static final field NONE Lio/sentry/SentryMeasurementUnit; + public static final field SECOND Lio/sentry/SentryMeasurementUnit; + public static final field WEEK Lio/sentry/SentryMeasurementUnit; + public fun apiName ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryMeasurementUnit; + public static fun values ()[Lio/sentry/SentryMeasurementUnit; +} + public class io/sentry/SentryOptions { public fun ()V public fun addContextTag (Ljava/lang/String;)V @@ -1492,6 +1513,8 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun scheduleFinish ()V public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;F)V + public fun setMeasurement (Ljava/lang/String;FLio/sentry/SentryMeasurementUnit;)V public fun setName (Ljava/lang/String;)V public fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun setOperation (Ljava/lang/String;)V @@ -1598,6 +1621,8 @@ public final class io/sentry/Span : io/sentry/ISpan { 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 setMeasurement (Ljava/lang/String;F)V + public fun setMeasurement (Ljava/lang/String;FLio/sentry/SentryMeasurementUnit;)V public fun setOperation (Ljava/lang/String;)V public fun setStatus (Lio/sentry/SpanStatus;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -2444,8 +2469,6 @@ public final class io/sentry/protocol/Gpu$JsonKeys { } public final class io/sentry/protocol/MeasurementValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public static final field MILLISECOND_UNIT Ljava/lang/String; - public static final field NONE_UNIT Ljava/lang/String; public fun (FLjava/lang/String;)V public fun (FLjava/lang/String;Ljava/util/Map;)V public fun getUnit ()Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index 537d72de022..3e250631b76 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -169,4 +169,27 @@ ISpan startChild( */ @Nullable Object getData(@NotNull String key); + + /** + * Set a measurement with NONE unit. + * + *

NOTE: Setting a measurement with the same name on the same transaction multiple times only + * keeps the last value. + * + * @param name the name of the measurement + * @param value the value of the measurement + */ + void setMeasurement(@NotNull String name, float value); + + /** + * Set a measurement with specific unit. + * + *

NOTE: Setting a measurement with the same name on the same transaction multiple times only + * keeps the last value. + * + * @param name the name of the measurement + * @param value the value of the measurement + * @param unit the unit the value is measured in + */ + void setMeasurement(@NotNull String name, float value, @NotNull SentryMeasurementUnit unit); } diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 21294bf63f1..ab81a02d4da 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -111,4 +111,11 @@ public void setData(@NotNull String key, @NotNull Object value) {} public @Nullable Object getData(@NotNull String key) { return null; } + + @Override + public void setMeasurement(@NotNull String name, float value) {} + + @Override + public void setMeasurement( + @NotNull String name, float value, @NotNull SentryMeasurementUnit unit) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index ed5ac8bc727..8973e4ae81e 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -164,4 +164,11 @@ public void setData(@NotNull String key, @NotNull Object value) {} public @Nullable Object getData(@NotNull String key) { return null; } + + @Override + public void setMeasurement(@NotNull String name, float value) {} + + @Override + public void setMeasurement( + @NotNull String name, float value, @NotNull SentryMeasurementUnit unit) {} } diff --git a/sentry/src/main/java/io/sentry/SentryMeasurementUnit.java b/sentry/src/main/java/io/sentry/SentryMeasurementUnit.java new file mode 100644 index 00000000000..8a819f8ead8 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryMeasurementUnit.java @@ -0,0 +1,37 @@ +package io.sentry; + +import java.util.Locale; +import org.jetbrains.annotations.NotNull; + +public enum SentryMeasurementUnit { + /** Nanosecond (`"nanosecond"`), 10^-9 seconds. */ + NANOSECOND, + + /** Microsecond (`"microsecond"`), 10^-6 seconds. */ + MICROSECOND, + + /** Millisecond (`"millisecond"`), 10^-3 seconds. */ + MILLISECOND, + + /** Full second (`"second"`). */ + SECOND, + + /** Minute (`"minute"`), 60 seconds. */ + MINUTE, + + /** Hour (`"hour"`), 3600 seconds. */ + HOUR, + + /** Day (`"day"`), 86,400 seconds. */ + DAY, + + /** Week (`"week"`), 604,800 seconds. */ + WEEK, + + /** Untyped value without a unit. */ + NONE; + + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index c40a74ecd71..42f7687a5be 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.TransactionNameSource; @@ -13,6 +14,7 @@ import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -74,6 +76,7 @@ public final class SentryTracer implements ITransaction { private final @NotNull Baggage baggage; private @NotNull TransactionNameSource transactionNameSource; + private final @NotNull Map measurements; public SentryTracer(final @NotNull TransactionContext context, final @NotNull IHub hub) { this(context, hub, null); @@ -104,6 +107,7 @@ public SentryTracer( final @Nullable TransactionFinishedCallback transactionFinishedCallback) { Objects.requireNonNull(context, "context is required"); Objects.requireNonNull(hub, "hub is required"); + this.measurements = new ConcurrentHashMap<>(); this.root = new Span(context, this, hub, startTimestamp); this.name = context.getName(); this.hub = hub; @@ -360,6 +364,9 @@ public void finish(@Nullable SpanStatus status) { // if it's an idle transaction which has no children, we drop it to save user's quota return; } + + transaction.getMeasurements().putAll(measurements); + hub.captureTransaction(transaction, traceContext(), null, profilingTraceData); } } @@ -506,6 +513,25 @@ public void setData(@NotNull String key, @NotNull Object value) { return this.root.getData(key); } + @Override + public void setMeasurement(@NotNull String name, float value) { + if (root.isFinished()) { + return; + } + + this.measurements.put(name, new MeasurementValue(value, SentryMeasurementUnit.NONE.apiName())); + } + + @Override + public void setMeasurement( + @NotNull String name, float value, @NotNull SentryMeasurementUnit unit) { + if (root.isFinished()) { + return; + } + + this.measurements.put(name, new MeasurementValue(value, unit.apiName())); + } + public @Nullable Map getData() { return this.root.getData(); } @@ -601,6 +627,12 @@ AtomicBoolean isFinishTimerRunning() { return isFinishTimerRunning; } + @TestOnly + @NotNull + Map getMeasurements() { + return measurements; + } + private static final class FinishStatus { static final FinishStatus NOT_FINISHED = FinishStatus.notFinished(); diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 8ae6d0cbb06..f7dd49d6853 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -313,6 +313,17 @@ public void setData(@NotNull String key, @NotNull Object value) { return data.get(key); } + @Override + public void setMeasurement(@NotNull String name, float value) { + this.transaction.setMeasurement(name, value); + } + + @Override + public void setMeasurement( + @NotNull String name, float value, @NotNull SentryMeasurementUnit unit) { + this.transaction.setMeasurement(name, value, unit); + } + @Nullable Long getEndNanos() { return endNanos; diff --git a/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java b/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java index c80b3a47acc..99fafe3125c 100644 --- a/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java +++ b/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java @@ -18,9 +18,6 @@ @ApiStatus.Internal public final class MeasurementValue implements JsonUnknown, JsonSerializable { - public static final @NotNull String NONE_UNIT = "none"; - public static final @NotNull String MILLISECOND_UNIT = "millisecond"; - @SuppressWarnings("UnusedVariable") private final float value; diff --git a/sentry/src/test/java/io/sentry/SentryMeasurementUnitTest.kt b/sentry/src/test/java/io/sentry/SentryMeasurementUnitTest.kt new file mode 100644 index 00000000000..19487e28274 --- /dev/null +++ b/sentry/src/test/java/io/sentry/SentryMeasurementUnitTest.kt @@ -0,0 +1,20 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SentryMeasurementUnitTest { + + @Test + fun `apiName converts enum to lowercase`() { + assertEquals("nanosecond", SentryMeasurementUnit.NANOSECOND.apiName()) + assertEquals("microsecond", SentryMeasurementUnit.MICROSECOND.apiName()) + assertEquals("millisecond", SentryMeasurementUnit.MILLISECOND.apiName()) + assertEquals("second", SentryMeasurementUnit.SECOND.apiName()) + assertEquals("minute", SentryMeasurementUnit.MINUTE.apiName()) + assertEquals("hour", SentryMeasurementUnit.HOUR.apiName()) + assertEquals("day", SentryMeasurementUnit.DAY.apiName()) + assertEquals("week", SentryMeasurementUnit.WEEK.apiName()) + assertEquals("none", SentryMeasurementUnit.NONE.apiName()) + } +} diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index db3e7b36b37..8c1fe0f8181 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -8,6 +8,7 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify +import io.sentry.SentryMeasurementUnit.DAY import io.sentry.protocol.User import org.awaitility.kotlin.await import java.util.Date @@ -367,6 +368,7 @@ class SentryTracerTest { transaction.description = "desc" transaction.setTag("myTag", "myValue") transaction.setData("myData", "myValue") + transaction.setMeasurement("myMetric", 1.0f) val ex = RuntimeException() transaction.throwable = ex @@ -383,12 +385,14 @@ class SentryTracerTest { transaction.throwable = RuntimeException() transaction.setData("myData", "myNewValue") transaction.name = "newName" + transaction.setMeasurement("myMetric", 2.0f) assertEquals(SpanStatus.OK, transaction.status) assertEquals("op", transaction.operation) assertEquals("desc", transaction.description) assertEquals("myValue", transaction.getTag("myTag")) assertEquals("myValue", transaction.getData("myData")) + assertEquals(1.0f, transaction.measurements["myMetric"]!!.value) assertEquals("name", transaction.name) assertEquals(ex, transaction.throwable) } @@ -708,7 +712,7 @@ class SentryTracerTest { @Test fun `when idle transaction with children, finishes the transaction after the idle timeout`() { - val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 10) + val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 1000) val span = transaction.startChild("op") span.finish() @@ -804,4 +808,24 @@ class SentryTracerTest { transaction.finish(SpanStatus.OK) assertNull(transaction.timer) } + + @Test + fun `when tracer is finished, puts custom measurements into underlying transaction`() { + val transaction = fixture.getSut() + transaction.setMeasurement("metric1", 1.0f) + transaction.setMeasurement("days", 2f, DAY) + transaction.finish() + + verify(fixture.hub).captureTransaction( + check { + assertEquals(1.0f, it.measurements["metric1"]!!.value) + + assertEquals(2f, it.measurements["days"]!!.value) + assertEquals("day", it.measurements["days"]!!.unit) + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } }