diff --git a/CHANGELOG.md b/CHANGELOG.md index c76ada1b14..44df2feb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Provide API for attaching custom measurements to transactions ([#2260](https://github.com/getsentry/sentry-java/pull/2260)) + ## 6.5.0-beta.2 ### Features 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 d71cab5213..c69369132e 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.MeasurementUnit; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import java.util.HashMap; @@ -104,9 +103,9 @@ 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, MeasurementUnit.NONE); + final MeasurementValue sfValues = new MeasurementValue(slowFrames, MeasurementUnit.NONE); + final MeasurementValue ffValues = new MeasurementValue(frozenFrames, MeasurementUnit.NONE); 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 a3fca88156..669f34c80e 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,10 +3,10 @@ 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.MeasurementUnit; import io.sentry.SentryEvent; import io.sentry.SpanContext; import io.sentry.protocol.MeasurementValue; @@ -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, MeasurementUnit.Duration.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/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index 0e3b0e437b..d48185d744 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -41,7 +41,7 @@ class ActivityFramesTrackerTest { val metrics = sut.takeMetrics(fixture.sentryId) val totalFrames = metrics!!["frames_total"] - assertEquals(totalFrames!!.value, 1f) + assertEquals(totalFrames!!.value, 1) assertEquals(totalFrames.unit, "none") } @@ -57,7 +57,7 @@ class ActivityFramesTrackerTest { val metrics = sut.takeMetrics(fixture.sentryId) val frozenFrames = metrics!!["frames_frozen"] - assertEquals(frozenFrames!!.value, 5f) + assertEquals(frozenFrames!!.value, 5) assertEquals(frozenFrames.unit, "none") } @@ -73,7 +73,7 @@ class ActivityFramesTrackerTest { val metrics = sut.takeMetrics(fixture.sentryId) val slowFrames = metrics!!["frames_slow"] - assertEquals(slowFrames!!.value, 5f) + assertEquals(slowFrames!!.value, 5) assertEquals(slowFrames.unit, "none") } @@ -93,13 +93,13 @@ class ActivityFramesTrackerTest { val metrics = sut.takeMetrics(fixture.sentryId) val totalFrames = metrics!!["frames_total"] - assertEquals(totalFrames!!.value, 111f) + assertEquals(totalFrames!!.value, 111) val frozenFrames = metrics["frames_frozen"] - assertEquals(frozenFrames!!.value, 6f) + assertEquals(frozenFrames!!.value, 6) val slowFrames = metrics["frames_slow"] - assertEquals(slowFrames!!.value, 5f) + assertEquals(slowFrames!!.value, 5) } @Test 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 df595b6a35..3cb65a46fd 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.MeasurementUnit 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, MeasurementUnit.Duration.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 b337973bcf..6f3c401633 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 @@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity; import io.sentry.Attachment; import io.sentry.ISpan; +import io.sentry.MeasurementUnit; import io.sentry.Sentry; import io.sentry.SpanStatus; import io.sentry.UserFeedback; @@ -27,6 +28,7 @@ public class MainActivity extends AppCompatActivity { private int crashCount = 0; + private int screenLoadCount = 0; @Override protected void onCreate(Bundle savedInstanceState) { @@ -200,8 +202,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, new MeasurementUnit.Custom("test")); 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 76d528f585..0eb67e8578 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 e3bac05f63..70d4d0c11f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -485,6 +485,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;Ljava/lang/Number;)V + public abstract fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)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 @@ -599,6 +601,56 @@ 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 abstract interface class io/sentry/MeasurementUnit { + public static final field NONE Ljava/lang/String; + public fun apiName ()Ljava/lang/String; + public abstract fun name ()Ljava/lang/String; +} + +public final class io/sentry/MeasurementUnit$Custom : io/sentry/MeasurementUnit { + public fun (Ljava/lang/String;)V + public fun name ()Ljava/lang/String; +} + +public final class io/sentry/MeasurementUnit$Duration : java/lang/Enum, io/sentry/MeasurementUnit { + public static final field DAY Lio/sentry/MeasurementUnit$Duration; + public static final field HOUR Lio/sentry/MeasurementUnit$Duration; + public static final field MICROSECOND Lio/sentry/MeasurementUnit$Duration; + public static final field MILLISECOND Lio/sentry/MeasurementUnit$Duration; + public static final field MINUTE Lio/sentry/MeasurementUnit$Duration; + public static final field NANOSECOND Lio/sentry/MeasurementUnit$Duration; + public static final field SECOND Lio/sentry/MeasurementUnit$Duration; + public static final field WEEK Lio/sentry/MeasurementUnit$Duration; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Duration; + public static fun values ()[Lio/sentry/MeasurementUnit$Duration; +} + +public final class io/sentry/MeasurementUnit$Fraction : java/lang/Enum, io/sentry/MeasurementUnit { + public static final field PERCENT Lio/sentry/MeasurementUnit$Fraction; + public static final field RATIO Lio/sentry/MeasurementUnit$Fraction; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Fraction; + public static fun values ()[Lio/sentry/MeasurementUnit$Fraction; +} + +public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/sentry/MeasurementUnit { + public static final field BIT Lio/sentry/MeasurementUnit$Information; + public static final field BYTE Lio/sentry/MeasurementUnit$Information; + public static final field EXABYTE Lio/sentry/MeasurementUnit$Information; + public static final field EXBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field GIBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field GIGABYTE Lio/sentry/MeasurementUnit$Information; + public static final field KIBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field KILOBYTE Lio/sentry/MeasurementUnit$Information; + public static final field MEBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field MEGABYTE Lio/sentry/MeasurementUnit$Information; + public static final field PEBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field PETABYTE Lio/sentry/MeasurementUnit$Information; + public static final field TEBIBYTE Lio/sentry/MeasurementUnit$Information; + public static final field TERABYTE Lio/sentry/MeasurementUnit$Information; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Information; + public static fun values ()[Lio/sentry/MeasurementUnit$Information; +} + 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; @@ -670,6 +722,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;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)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 @@ -705,6 +759,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;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)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 @@ -1497,6 +1553,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;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)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 @@ -1603,6 +1661,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;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)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 @@ -2449,13 +2509,11 @@ 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 (Ljava/lang/Number;Ljava/lang/String;)V + public fun (Ljava/lang/Number;Ljava/lang/String;Ljava/util/Map;)V public fun getUnit ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; - public fun getValue ()F + public fun getValue ()Ljava/lang/Number; public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setUnknown (Ljava/util/Map;)V } diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index 537d72de02..ab4bfdffe5 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -169,4 +169,29 @@ ISpan startChild( */ @Nullable Object getData(@NotNull String key); + + /** + * Set a measurement without unit. When setting the measurement without the unit, no formatting + * will be applied to the measurement value in the Sentry product, and the value will be shown as + * is. + * + *

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, @NotNull Number 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, @NotNull Number value, @NotNull MeasurementUnit unit); } diff --git a/sentry/src/main/java/io/sentry/MeasurementUnit.java b/sentry/src/main/java/io/sentry/MeasurementUnit.java new file mode 100644 index 0000000000..5dc00261d1 --- /dev/null +++ b/sentry/src/main/java/io/sentry/MeasurementUnit.java @@ -0,0 +1,132 @@ +package io.sentry; + +import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * The unit of measurement of a metric value. + * + *

Units augment metric values by giving them a magnitude and semantics. There are certain types + * of units that are subdivided in their precision, such as the {@link MeasurementUnit.Duration} for + * time measurements. + * + *

When using the units to custom measurements, Sentry will apply formatting to display + * measurement values in the UI. + * + * @see Develop + * Docs + */ +public interface MeasurementUnit { + + /** Untyped value. */ + @ApiStatus.Internal String NONE = "none"; + + /** A time duration. */ + enum Duration implements MeasurementUnit { + /** 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; + } + + /** Size of information derived from bytes. */ + enum Information implements MeasurementUnit { + /** Bit (`"bit"`), corresponding to 1/8 of a byte. */ + BIT, + + /** Byte (`"byte"`). */ + BYTE, + + /** Kilobyte (`"kilobyte"`), 10^3 bytes. */ + KILOBYTE, + + /** Kibibyte (`"kibibyte"`), 2^10 bytes. */ + KIBIBYTE, + + /** Megabyte (`"megabyte"`), 10^6 bytes. */ + MEGABYTE, + + /** Mebibyte (`"mebibyte"`), 2^20 bytes. */ + MEBIBYTE, + + /** Gigabyte (`"gigabyte"`), 10^9 bytes. */ + GIGABYTE, + + /** Gibibyte (`"gibibyte"`), 2^30 bytes. */ + GIBIBYTE, + + /** Terabyte (`"terabyte"`), 10^12 bytes. */ + TERABYTE, + + /** Tebibyte (`"tebibyte"`), 2^40 bytes. */ + TEBIBYTE, + + /** Petabyte (`"petabyte"`), 10^15 bytes. */ + PETABYTE, + + /** Pebibyte (`"pebibyte"`), 2^50 bytes. */ + PEBIBYTE, + + /** Exabyte (`"exabyte"`), 10^18 bytes. */ + EXABYTE, + + /** Exbibyte (`"exbibyte"`), 2^60 bytes. */ + EXBIBYTE; + } + + /** Fractions such as percentages. */ + enum Fraction implements MeasurementUnit { + /** Floating point fraction of `1`. */ + RATIO, + + /** Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. */ + PERCENT; + } + + /** + * Custom units without builtin conversion. No formatting will be applied to the measurement value + * in the Sentry product, and the value with the unit will be shown as is. + */ + final class Custom implements MeasurementUnit { + + private final @NotNull String name; + + public Custom(@NotNull String name) { + this.name = name; + } + + @Override + public @NotNull String name() { + return name; + } + } + + @NotNull + String name(); + + /** Unit adhering to the API spec. */ + @ApiStatus.Internal + default @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 21294bf63f..08388f8368 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, @NotNull Number value) {} + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index ed5ac8bc72..a3ca631a3b 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, @NotNull Number value) {} + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) {} } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index c40a74ecd7..37eecd101c 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,27 @@ public void setData(@NotNull String key, @NotNull Object value) { return this.root.getData(key); } + @Override + public void setMeasurement(final @NotNull String name, final @NotNull Number value) { + if (root.isFinished()) { + return; + } + + this.measurements.put(name, new MeasurementValue(value, null)); + } + + @Override + public void setMeasurement( + final @NotNull String name, + final @NotNull Number value, + final @NotNull MeasurementUnit unit) { + if (root.isFinished()) { + return; + } + + this.measurements.put(name, new MeasurementValue(value, unit.apiName())); + } + public @Nullable Map getData() { return this.root.getData(); } @@ -601,6 +629,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 8ae6d0cbb0..b149fac379 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, @NotNull Number value) { + this.transaction.setMeasurement(name, value); + } + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit 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 c80b3a47ac..40c9b557ab 100644 --- a/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java +++ b/sentry/src/main/java/io/sentry/protocol/MeasurementValue.java @@ -6,6 +6,7 @@ import io.sentry.JsonObjectWriter; import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; +import io.sentry.SentryLevel; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -18,32 +19,31 @@ @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; + private final @NotNull Number value; private final @Nullable String unit; /** the unknown fields of breadcrumbs, internal usage only */ private @Nullable Map unknown; - public MeasurementValue(final float value, final @Nullable String unit) { + public MeasurementValue(final @NotNull Number value, final @Nullable String unit) { this.value = value; this.unit = unit; } @TestOnly public MeasurementValue( - final float value, final @Nullable String unit, final @Nullable Map unknown) { + final @NotNull Number value, + final @Nullable String unit, + final @Nullable Map unknown) { this.value = value; this.unit = unit; this.unknown = unknown; } @TestOnly - public float getValue() { + public @NotNull Number getValue() { return value; } @@ -97,14 +97,14 @@ public static final class Deserializer implements JsonDeserializer unknown = null; while (reader.peek() == JsonToken.NAME) { final String nextName = reader.nextName(); switch (nextName) { case JsonKeys.VALUE: - value = reader.nextFloat(); + value = (Number) reader.nextObjectOrNull(); break; case JsonKeys.UNIT: unit = reader.nextStringOrNull(); @@ -119,6 +119,14 @@ public static final class Deserializer implements JsonDeserializer() // float cannot represent 0.3 correctly https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html - fun getSut(value: Float = 0.30000001192092896f, unit: String = "test") = MeasurementValue(value, unit, mapOf("new_type" to "newtype")) + fun getSut(value: Number = 0.30000001192092896, unit: String = "test") = MeasurementValue(value, unit, mapOf("new_type" to "newtype")) } private val fixture = Fixture() @Test - fun serialize() { - val expected = sanitizedFile("json/measurement_value.json") + fun `serialize double`() { + val expected = sanitizedFile("json/measurement_value_double.json") val actual = serialize(fixture.getSut()) assertEquals(expected, actual) } @Test - fun deserialize() { - val expectedJson = sanitizedFile("json/measurement_value.json") + fun `deserialize double`() { + val expectedJson = sanitizedFile("json/measurement_value_double.json") val actual = deserialize(expectedJson) val actualJson = serialize(actual) assertEquals(expectedJson, actualJson) } + @Test + fun `serialize int`() { + val expected = sanitizedFile("json/measurement_value_int.json") + val actual = serialize(fixture.getSut(4)) + assertEquals(expected, actual) + } + + @Test + fun `deserialize int`() { + val expectedJson = sanitizedFile("json/measurement_value_int.json") + val actual = deserialize(expectedJson) + val actualJson = serialize(actual) + assertEquals(expectedJson, actualJson) + } + + @Test(expected = IllegalStateException::class) + fun `deserialize missing value`() { + val expectedJson = sanitizedFile("json/measurement_value_missing.json") + deserialize(expectedJson) + } + // Helper private fun sanitizedFile(path: String): String { diff --git a/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt index 51cbd5ce71..ec068376a2 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt @@ -26,7 +26,7 @@ class SentryTransactionSerializationTest { ), mapOf( "386384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(), - "186384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(0.4000000059604645f, "test2") + "186384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(0.4000000059604645, "test2") ), TransactionInfo(TransactionNameSource.CUSTOM.apiName()) ).apply { diff --git a/sentry/src/test/resources/json/measurement_value.json b/sentry/src/test/resources/json/measurement_value_double.json similarity index 100% rename from sentry/src/test/resources/json/measurement_value.json rename to sentry/src/test/resources/json/measurement_value_double.json diff --git a/sentry/src/test/resources/json/measurement_value_int.json b/sentry/src/test/resources/json/measurement_value_int.json new file mode 100644 index 0000000000..944cee02d3 --- /dev/null +++ b/sentry/src/test/resources/json/measurement_value_int.json @@ -0,0 +1,5 @@ +{ + "value": 4, + "unit": "test", + "new_type": "newtype" +} diff --git a/sentry/src/test/resources/json/measurement_value_missing.json b/sentry/src/test/resources/json/measurement_value_missing.json new file mode 100644 index 0000000000..7fefbe2fa9 --- /dev/null +++ b/sentry/src/test/resources/json/measurement_value_missing.json @@ -0,0 +1,4 @@ +{ + "unit": "test", + "new_type": "newtype" +}