Skip to content

Commit

Permalink
Merge branch 'main' into feat/version-bumps
Browse files Browse the repository at this point in the history
  • Loading branch information
markushi committed Apr 23, 2024
2 parents bfe02ea + a33b076 commit 6488520
Show file tree
Hide file tree
Showing 21 changed files with 251 additions and 45 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
## Unreleased

### Features
- Add start_type to app context ([#3379](https://github.com/getsentry/sentry-java/pull/3379))
- Internal: Version bump Gradle=8.6, AGP=8.3.0, kotlin=1.9.22, compose=1.5.12 ([#3263](https://github.com/getsentry/sentry-java/pull/3263))

**Breaking changes:**
- The min supported Kotlin language version has been bumped to `1.5`, please consider using previous version of the SDK if you have to support compatibility with earlier Kotlin versions

### Fixes

- Fix Frame measurements in app start transactions ([#3382](https://github.com/getsentry/sentry-java/pull/3382))
- Fix timing metric value different from span duration ([#3368](https://github.com/getsentry/sentry-java/pull/3368))

**Breaking changes:**
- The min supported Kotlin language version has been bumped to `1.5`, please consider using previous version of the SDK if you have to support compatibility with earlier Kotlin versions

## 7.8.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.App;
import io.sentry.protocol.MeasurementValue;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentrySpan;
Expand Down Expand Up @@ -79,27 +80,40 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {

// the app start measurement is only sent once and only if the transaction has
// the app.start span, which is automatically created by the SDK.
if (!sentStartMeasurement && hasAppStartSpan(transaction)) {
final @NotNull TimeSpan appStartTimeSpan =
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();

// if appStartUpDurationMs is 0, metrics are not ready to be sent
if (appStartUpDurationMs != 0) {
final MeasurementValue value =
new MeasurementValue(
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());

final String appStartKey =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
? MeasurementValue.KEY_APP_START_COLD
: MeasurementValue.KEY_APP_START_WARM;

transaction.getMeasurements().put(appStartKey, value);

attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction);
sentStartMeasurement = true;
if (hasAppStartSpan(transaction)) {
if (!sentStartMeasurement) {
final @NotNull TimeSpan appStartTimeSpan =
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();

// if appStartUpDurationMs is 0, metrics are not ready to be sent
if (appStartUpDurationMs != 0) {
final MeasurementValue value =
new MeasurementValue(
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());

final String appStartKey =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
? MeasurementValue.KEY_APP_START_COLD
: MeasurementValue.KEY_APP_START_WARM;

transaction.getMeasurements().put(appStartKey, value);

attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction);
sentStartMeasurement = true;
}
}

@Nullable App appContext = transaction.getContexts().getApp();
if (appContext == null) {
appContext = new App();
transaction.getContexts().setApp(appContext);
}
final String appStartType =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
? "cold"
: "warm";
appContext.setStartType(appStartType);
}

final SentryId eventId = transaction.getEventId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.sentry.NoOpTransaction;
import io.sentry.SentryDate;
import io.sentry.SentryNanotimeDate;
import io.sentry.SentryTracer;
import io.sentry.SpanDataConvention;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.protocol.MeasurementValue;
Expand Down Expand Up @@ -135,11 +136,15 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
return;
}

// ignore spans with no finish date
final @Nullable SentryDate spanFinishDate = span.getFinishDate();
// Ignore spans with no finish date, but SentryTracer is not finished when executing this
// callback, yet, so in that case we use the current timestamp.
final @Nullable SentryDate spanFinishDate =
span instanceof SentryTracer ? new SentryNanotimeDate() : span.getFinishDate();
if (spanFinishDate == null) {
return;
}
// Note: The comparison between two values obtained by realNanos() works only if both are the
// same kind of dates (both are SentryNanotimeDate or both SentryLongDate)
final long spanEndNanos = realNanos(spanFinishDate);

final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import io.sentry.DateUtils;
import io.sentry.SentryDate;
import io.sentry.SentryLongDate;
import io.sentry.SentryNanotimeDate;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -21,6 +23,7 @@ public class TimeSpan implements Comparable<TimeSpan> {

private @Nullable String description;

private long startSystemNanos;
private long startUnixTimeMs;
private long startUptimeMs;
private long stopUptimeMs;
Expand All @@ -29,6 +32,7 @@ public class TimeSpan implements Comparable<TimeSpan> {
public void start() {
startUptimeMs = SystemClock.uptimeMillis();
startUnixTimeMs = System.currentTimeMillis();
startSystemNanos = System.nanoTime();
}

/**
Expand All @@ -40,6 +44,7 @@ public void setStartedAt(final long uptimeMs) {

final long shiftMs = SystemClock.uptimeMillis() - startUptimeMs;
startUnixTimeMs = System.currentTimeMillis() - shiftMs;
startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs);
}

/** Stops the time span */
Expand Down Expand Up @@ -90,7 +95,8 @@ public long getStartTimestampMs() {
*/
public @Nullable SentryDate getStartTimestamp() {
if (hasStarted()) {
return new SentryLongDate(DateUtils.millisToNanos(getStartTimestampMs()));
return new SentryNanotimeDate(
DateUtils.nanosToDate(DateUtils.millisToNanos(getStartTimestampMs())), startSystemNanos);
}
return null;
}
Expand Down Expand Up @@ -162,6 +168,7 @@ public void reset() {
startUptimeMs = 0;
stopUptimeMs = 0;
startUnixTimeMs = 0;
startSystemNanos = 0;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -464,6 +465,60 @@ class PerformanceAndroidEventProcessorTest {
}
}

@Test
fun `does not set start_type field for txns without app start span`() {
// given some ui.load txn
setAppStart(fixture.options, coldStart = true)

val sut = fixture.getSut(enablePerformanceV2 = true)
val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
var tr = SentryTransaction(tracer)

// when it contains no app start span and is processed
tr = sut.process(tr, Hint())

// start_type should not be set
assertNull(tr.contexts.app?.startType)
}

@Test
fun `sets start_type field for app context`() {
// given some cold app start
setAppStart(fixture.options, coldStart = true)

val sut = fixture.getSut(enablePerformanceV2 = true)
val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
var tr = SentryTransaction(tracer)

val appStartSpan = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
APP_START_COLD,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)
tr.spans.add(appStartSpan)

// when the processor attaches the app start spans
tr = sut.process(tr, Hint())

// start_type should be set as well
assertEquals(
"cold",
tr.contexts.app!!.startType
)
}

private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) {
AppStartMetrics.getInstance().apply {
appStartType = when (coldStart) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ class SpanFrameMetricsCollectorTest {
options.frameMetricsCollector = frameMetricsCollector
options.isEnableFramesTracking = enabled
options.isEnablePerformanceV2 = enabled
options.setDateProvider {
SentryLongDate(timeNanos)
}
options.dateProvider = SentryAndroidDateProvider()

return SpanFrameMetricsCollector(options, frameMetricsCollector)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ package io.sentry.uitest.android

import androidx.lifecycle.Lifecycle
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.android.core.AndroidLogger
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.assertEnvelopeTransaction
import io.sentry.protocol.MeasurementValue
import io.sentry.protocol.SentryTransaction
import org.junit.Assume
import org.junit.runner.RunWith
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -49,4 +59,84 @@ class AutomaticSpansTest : BaseUiTest() {
}
}
}

@Test
fun checkAppStartFramesMeasurements() {
initSentry(true) { options: SentryAndroidOptions ->
options.tracesSampleRate = 1.0
options.isEnableTimeToFullDisplayTracing = true
options.isEnablePerformanceV2 = false
}

IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource)
val sampleScenario = launchActivity<ProfilingSampleActivity>()
swipeList(3)
Sentry.reportFullyDisplayed()
sampleScenario.moveToState(Lifecycle.State.DESTROYED)
IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource)
relayIdlingResource.increment()

relay.assert {
findEnvelope {
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity"
}.assert {
val transactionItem: SentryTransaction = it.assertTransaction()
it.assertNoOtherItems()
val measurements = transactionItem.measurements
val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0
val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0
val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0
assertEquals("ProfilingSampleActivity", transactionItem.transaction)
// AGP matrix tests have no frames
Assume.assumeTrue(totalFrames > 0)
assertNotEquals(totalFrames, 0)
assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.")
}
assertNoOtherEnvelopes()
}
}

@Test
fun checkAppStartFramesMeasurementsPerfV2() {
initSentry(true) { options: SentryAndroidOptions ->
options.tracesSampleRate = 1.0
options.isEnableTimeToFullDisplayTracing = true
options.isEnablePerformanceV2 = true
}

IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource)
val sampleScenario = launchActivity<ProfilingSampleActivity>()
swipeList(3)
Sentry.reportFullyDisplayed()
sampleScenario.moveToState(Lifecycle.State.DESTROYED)
IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource)
relayIdlingResource.increment()

relay.assert {
findEnvelope {
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity"
}.assert {
val transactionItem: SentryTransaction = it.assertTransaction()
it.assertNoOtherItems()
val measurements = transactionItem.measurements
val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0
val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0
val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0
assertEquals("ProfilingSampleActivity", transactionItem.transaction)
// AGP matrix tests have no frames
Assume.assumeTrue(totalFrames > 0)
assertNotEquals(totalFrames, 0)
assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.")
}
assertNoOtherEnvelopes()
}
}

private fun swipeList(times: Int) {
repeat(times) {
Thread.sleep(100)
Espresso.onView(ViewMatchers.withId(R.id.profiling_sample_list)).perform(ViewActions.swipeUp())
Espresso.onIdle()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class EnvelopeTests : BaseUiTest() {
// Timestamps of measurements should differ at least 10 milliseconds from each other
(1 until values.size).forEach { i ->
assertTrue(
values[i].relativeStartNs.toLong() > values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos(
values[i].relativeStartNs.toLong() >= values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos(
10
),
"Measurement value timestamp for '$name' does not differ at least 10ms"
Expand Down
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3643,6 +3643,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr
public fun getDeviceAppHash ()Ljava/lang/String;
public fun getInForeground ()Ljava/lang/Boolean;
public fun getPermissions ()Ljava/util/Map;
public fun getStartType ()Ljava/lang/String;
public fun getUnknown ()Ljava/util/Map;
public fun getViewNames ()Ljava/util/List;
public fun hashCode ()I
Expand All @@ -3656,6 +3657,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr
public fun setDeviceAppHash (Ljava/lang/String;)V
public fun setInForeground (Ljava/lang/Boolean;)V
public fun setPermissions (Ljava/util/Map;)V
public fun setStartType (Ljava/lang/String;)V
public fun setUnknown (Ljava/util/Map;)V
public fun setViewNames (Ljava/util/List;)V
}
Expand All @@ -3676,6 +3678,7 @@ public final class io/sentry/protocol/App$JsonKeys {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEVICE_APP_HASH Ljava/lang/String;
public static final field IN_FOREGROUND Ljava/lang/String;
public static final field START_TYPE Ljava/lang/String;
public static final field VIEW_NAMES Ljava/lang/String;
public fun <init> ()V
}
Expand Down

0 comments on commit 6488520

Please sign in to comment.