Skip to content

Commit

Permalink
Missing unit fields for Android measurements (#2204)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Dinauer <alexander.dinauer@sentry.io>
Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
4 people committed Sep 8, 2022
1 parent de74337 commit e2ea31c
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Fixed AbstractMethodError when getting Lifecycle ([#2228](https://github.com/getsentry/sentry-java/pull/2228))
- Missing unit fields for Android measurements ([#2204](https://github.com/getsentry/sentry-java/pull/2204))
- Avoid sending empty profiles ([#2232](https://github.com/getsentry/sentry-java/pull/2232))

## 6.4.1
Expand Down
@@ -1,5 +1,7 @@
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;
Expand Down Expand Up @@ -102,9 +104,9 @@ public synchronized void setMetrics(
return;
}

final MeasurementValue tfValues = new MeasurementValue(totalFrames);
final MeasurementValue sfValues = new MeasurementValue(slowFrames);
final MeasurementValue ffValues = new MeasurementValue(frozenFrames);
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 Map<String, @NotNull MeasurementValue> measurements = new HashMap<>();
measurements.put("frames_total", tfValues);
measurements.put("frames_slow", sfValues);
Expand Down
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -65,7 +66,8 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
final Long appStartUpInterval = AppStartState.getInstance().getAppStartInterval();
// if appStartUpInterval is null, metrics are not ready to be sent
if (appStartUpInterval != null) {
final MeasurementValue value = new MeasurementValue((float) appStartUpInterval);
final MeasurementValue value =
new MeasurementValue((float) appStartUpInterval, MILLISECOND_UNIT);

final String appStartKey =
AppStartState.getInstance().isColdStart() ? "app_start_cold" : "app_start_warm";
Expand Down
Expand Up @@ -42,6 +42,7 @@ class ActivityFramesTrackerTest {
val totalFrames = metrics!!["frames_total"]

assertEquals(totalFrames!!.value, 1f)
assertEquals(totalFrames.unit, "none")
}

@Test
Expand All @@ -57,6 +58,7 @@ class ActivityFramesTrackerTest {
val frozenFrames = metrics!!["frames_frozen"]

assertEquals(frozenFrames!!.value, 5f)
assertEquals(frozenFrames.unit, "none")
}

@Test
Expand All @@ -72,6 +74,7 @@ class ActivityFramesTrackerTest {
val slowFrames = metrics!!["frames_slow"]

assertEquals(slowFrames!!.value, 5f)
assertEquals(slowFrames.unit, "none")
}

@Test
Expand Down
Expand Up @@ -10,10 +10,12 @@ 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
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class PerformanceAndroidEventProcessorTest {
Expand Down Expand Up @@ -64,6 +66,19 @@ class PerformanceAndroidEventProcessorTest {
assertTrue(tr.measurements.containsKey("app_start_warm"))
}

@Test
fun `set app cold start unit measurement`() {
val sut = fixture.getSut()

var tr = getTransaction()
setAppStart()

tr = sut.process(tr, Hint())

val measurement = tr.measurements["app_start_cold"]
assertEquals("millisecond", measurement?.unit)
}

@Test
fun `do not add app start metric twice`() {
val sut = fixture.getSut()
Expand Down Expand Up @@ -140,7 +155,7 @@ class PerformanceAndroidEventProcessorTest {
val tracer = SentryTracer(context, fixture.hub)
var tr = SentryTransaction(tracer)

val metrics = mapOf("frames_total" to MeasurementValue(1f))
val metrics = mapOf("frames_total" to MeasurementValue(1f, MILLISECOND_UNIT))
whenever(fixture.activityFramesTracker.takeMetrics(any())).thenReturn(metrics)

tr = sut.process(tr, Hint())
Expand Down
12 changes: 10 additions & 2 deletions sentry/api/sentry.api
Expand Up @@ -517,6 +517,7 @@ public final class io/sentry/JsonObjectReader : io/sentry/vendor/gson/stream/Jso
public fun nextIntegerOrNull ()Ljava/lang/Integer;
public fun nextList (Lio/sentry/ILogger;Lio/sentry/JsonDeserializer;)Ljava/util/List;
public fun nextLongOrNull ()Ljava/lang/Long;
public fun nextMapOrNull (Lio/sentry/ILogger;Lio/sentry/JsonDeserializer;)Ljava/util/Map;
public fun nextObjectOrNull ()Ljava/lang/Object;
public fun nextOrNull (Lio/sentry/ILogger;Lio/sentry/JsonDeserializer;)Ljava/lang/Object;
public fun nextStringOrNull ()Ljava/lang/String;
Expand Down Expand Up @@ -2366,10 +2367,16 @@ public final class io/sentry/protocol/Gpu$JsonKeys {
public fun <init> ()V
}

public final class io/sentry/protocol/MeasurementValue : io/sentry/JsonSerializable {
public fun <init> (F)V
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 <init> (FLjava/lang/String;)V
public fun <init> (FLjava/lang/String;Ljava/util/Map;)V
public fun getUnit ()Ljava/lang/String;
public fun getUnknown ()Ljava/util/Map;
public fun getValue ()F
public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V
public fun setUnknown (Ljava/util/Map;)V
}

public final class io/sentry/protocol/MeasurementValue$Deserializer : io/sentry/JsonDeserializer {
Expand All @@ -2379,6 +2386,7 @@ public final class io/sentry/protocol/MeasurementValue$Deserializer : io/sentry/
}

public final class io/sentry/protocol/MeasurementValue$JsonKeys {
public static final field UNIT Ljava/lang/String;
public static final field VALUE Ljava/lang/String;
public fun <init> ()V
}
Expand Down
22 changes: 22 additions & 0 deletions sentry/src/main/java/io/sentry/JsonObjectReader.java
Expand Up @@ -6,6 +6,7 @@
import java.io.Reader;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
Expand Down Expand Up @@ -99,6 +100,27 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
return list;
}

public <T> @Nullable Map<String, T> nextMapOrNull(
@NotNull ILogger logger, @NotNull JsonDeserializer<T> deserializer) throws IOException {
if (peek() == JsonToken.NULL) {
nextNull();
return null;
}
beginObject();
Map<String, T> map = new HashMap<>();
do {
try {
String key = nextName();
map.put(key, deserializer.deserialize(this, logger));
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Failed to deserialize object in map.", e);
}
} while (peek() == JsonToken.BEGIN_OBJECT || peek() == JsonToken.NAME);

endObject();
return map;
}

public <T> @Nullable T nextOrNull(
@NotNull ILogger logger, @NotNull JsonDeserializer<T> deserializer) throws Exception {
if (peek() == JsonToken.NULL) {
Expand Down
86 changes: 81 additions & 5 deletions sentry/src/main/java/io/sentry/protocol/MeasurementValue.java
Expand Up @@ -5,36 +5,88 @@
import io.sentry.JsonObjectReader;
import io.sentry.JsonObjectWriter;
import io.sentry.JsonSerializable;
import io.sentry.JsonUnknown;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

@ApiStatus.Internal
public final class MeasurementValue implements JsonSerializable {
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;

public MeasurementValue(final float value) {
private final @Nullable String unit;

/** the unknown fields of breadcrumbs, internal usage only */
private @Nullable Map<String, Object> unknown;

public MeasurementValue(final float value, final @Nullable String unit) {
this.value = value;
this.unit = unit;
}

@TestOnly
public MeasurementValue(
final float value, final @Nullable String unit, final @Nullable Map<String, Object> unknown) {
this.value = value;
this.unit = unit;
this.unknown = unknown;
}

@TestOnly
public float getValue() {
return value;
}

public @Nullable String getUnit() {
return unit;
}

@Nullable
@Override
public Map<String, Object> getUnknown() {
return unknown;
}

@Override
public void setUnknown(@Nullable Map<String, Object> unknown) {
this.unknown = unknown;
}

// JsonSerializable

public static final class JsonKeys {
public static final String VALUE = "value";
public static final String UNIT = "unit";
}

@Override
public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger)
throws IOException {
writer.beginObject();
writer.name(JsonKeys.VALUE).value(value);

if (unit != null) {
writer.name(JsonKeys.UNIT).value(unit);
}

if (unknown != null) {
for (final String key : unknown.keySet()) {
final Object value = unknown.get(key);
writer.name(key);
writer.value(logger, value);
}
}

writer.endObject();
}

Expand All @@ -43,10 +95,34 @@ public static final class Deserializer implements JsonDeserializer<MeasurementVa
public @NotNull MeasurementValue deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
reader.beginObject();
reader.nextName();
MeasurementValue measurementValue = new MeasurementValue(reader.nextFloat());

String unit = null;
float value = 0;
Map<String, Object> unknown = null;

while (reader.peek() == JsonToken.NAME) {
final String nextName = reader.nextName();
switch (nextName) {
case JsonKeys.VALUE:
value = reader.nextFloat();
break;
case JsonKeys.UNIT:
unit = reader.nextStringOrNull();
break;
default:
if (unknown == null) {
unknown = new ConcurrentHashMap<>();
}
reader.nextUnknown(logger, unknown, nextName);
break;
}
}

reader.endObject();
return measurementValue;
final MeasurementValue measurement = new MeasurementValue(value, unit);
measurement.setUnknown(unknown);

return measurement;
}
}
}
Expand Up @@ -280,8 +280,8 @@ public static final class Deserializer implements JsonDeserializer<SentryTransac
reader.nextString(); // No need to assign, as it is final.
break;
case JsonKeys.MEASUREMENTS:
Map<String, @NotNull MeasurementValue> deserializedMeasurements =
(Map<String, @NotNull MeasurementValue>) reader.nextObjectOrNull();
Map<String, MeasurementValue> deserializedMeasurements =
reader.nextMapOrNull(logger, new MeasurementValue.Deserializer());
if (deserializedMeasurements != null) {
transaction.measurements.putAll(deserializedMeasurements);
}
Expand Down
29 changes: 29 additions & 0 deletions sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt
Expand Up @@ -146,6 +146,35 @@ class JsonObjectReaderTest {
assertEquals(expected, actual)
}

// nextMap

@Test
fun `returns null for null map`() {
val jsonString = "{\"key\": null}"
val reader = fixture.getSut(jsonString)
reader.beginObject()
reader.nextName()

assertNull(reader.nextMapOrNull(fixture.logger, Deserializable.Deserializer()))
}

@Test
fun `returns map of deserializables`() {
val deserializableA = "{\"foo\": \"foo\", \"bar\": \"bar\"}"
val deserializableB = "{\"foo\": \"fooo\", \"bar\": \"baar\"}"
val jsonString = "{\"deserializable\": { \"a\":$deserializableA,\"b\":$deserializableB}}"
val reader = fixture.getSut(jsonString)
reader.beginObject()
reader.nextName()

val expected = mapOf(
"a" to Deserializable("foo", "bar"),
"b" to Deserializable("fooo", "baar")
)
val actual = reader.nextMapOrNull(fixture.logger, Deserializable.Deserializer())
assertEquals(expected, actual)
}

// nextDateOrNull

@Test
Expand Down
Expand Up @@ -16,7 +16,7 @@ class MeasurementValueSerializationTest {
class Fixture {
val logger = mock<ILogger>()
// float cannot represent 0.3 correctly https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
fun getSut() = MeasurementValue(0.30000001192092896f)
fun getSut(value: Float = 0.30000001192092896f, unit: String = "test") = MeasurementValue(value, unit, mapOf<String, Any>("new_type" to "newtype"))
}
private val fixture = Fixture()

Expand Down
Expand Up @@ -25,7 +25,8 @@ class SentryTransactionSerializationTest {
SentrySpanSerializationTest.Fixture().getSut()
),
mapOf(
"386384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut()
"386384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(),
"186384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(0.4000000059604645f, "test2")
),
TransactionInfo(TransactionNameSource.CUSTOM.apiName())
).apply {
Expand All @@ -49,6 +50,14 @@ class SentryTransactionSerializationTest {
assertEquals(expectedJson, actualJson)
}

@Test
fun `deserialize without measurement unit`() {
val expectedJson = sanitizedFile("json/sentry_transaction_no_measurement_unit.json")
val actual = deserialize(expectedJson)
val actualJson = serialize(actual)
assertEquals(expectedJson, actualJson)
}

@Test
fun `deserialize legacy date format and missing transaction name source`() {
val expectedJson = sanitizedFile("json/sentry_transaction_legacy_date_format.json")
Expand Down

0 comments on commit e2ea31c

Please sign in to comment.