Skip to content

Commit 6f83386

Browse files
committed
Merge branch 'rz/feat/session-replay-touch-events' into rz/feat/session-replay-breadcrumbs-v2
2 parents 9596bb9 + 647822c commit 6f83386

36 files changed

+460
-131
lines changed

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
# Changelog
22

3-
## 7.8.0-alpha.0
3+
## 7.9.0-alpha.1
44

5-
- No documented changes.
5+
- Session Replay for Android ([#3339](https://github.com/getsentry/sentry-java/pull/3339))
6+
7+
We released our second Alpha version of the SDK with support. To get access, it requires adding your Sentry org to our feature flag. Please let us know on the [waitlist](https://sentry.io/lp/mobile-replay-beta/) if you're interested
8+
9+
### Features
10+
11+
- Add start_type to app context ([#3379](https://github.com/getsentry/sentry-java/pull/3379))
12+
13+
### Fixes
14+
15+
- Fix Frame measurements in app start transactions ([#3382](https://github.com/getsentry/sentry-java/pull/3382))
16+
- Fix timing metric value different from span duration ([#3368](https://github.com/getsentry/sentry-java/pull/3368))
617

718
## 7.8.0
819

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ android.useAndroidX=true
1010
android.defaults.buildfeatures.buildconfig=true
1111

1212
# Release information
13-
versionName=7.8.0-alpha.0
13+
versionName=7.9.0-alpha.1
1414

1515
# Override the SDK name on native crashes on Android
1616
sentryAndroidSdkName=sentry.native.android

sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
1717
import io.sentry.android.core.performance.AppStartMetrics;
1818
import io.sentry.android.core.performance.TimeSpan;
19+
import io.sentry.protocol.App;
1920
import io.sentry.protocol.MeasurementValue;
2021
import io.sentry.protocol.SentryId;
2122
import io.sentry.protocol.SentrySpan;
@@ -79,27 +80,40 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
7980

8081
// the app start measurement is only sent once and only if the transaction has
8182
// the app.start span, which is automatically created by the SDK.
82-
if (!sentStartMeasurement && hasAppStartSpan(transaction)) {
83-
final @NotNull TimeSpan appStartTimeSpan =
84-
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
85-
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();
86-
87-
// if appStartUpDurationMs is 0, metrics are not ready to be sent
88-
if (appStartUpDurationMs != 0) {
89-
final MeasurementValue value =
90-
new MeasurementValue(
91-
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());
92-
93-
final String appStartKey =
94-
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
95-
? MeasurementValue.KEY_APP_START_COLD
96-
: MeasurementValue.KEY_APP_START_WARM;
97-
98-
transaction.getMeasurements().put(appStartKey, value);
99-
100-
attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction);
101-
sentStartMeasurement = true;
83+
if (hasAppStartSpan(transaction)) {
84+
if (!sentStartMeasurement) {
85+
final @NotNull TimeSpan appStartTimeSpan =
86+
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
87+
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();
88+
89+
// if appStartUpDurationMs is 0, metrics are not ready to be sent
90+
if (appStartUpDurationMs != 0) {
91+
final MeasurementValue value =
92+
new MeasurementValue(
93+
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());
94+
95+
final String appStartKey =
96+
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
97+
? MeasurementValue.KEY_APP_START_COLD
98+
: MeasurementValue.KEY_APP_START_WARM;
99+
100+
transaction.getMeasurements().put(appStartKey, value);
101+
102+
attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction);
103+
sentStartMeasurement = true;
104+
}
105+
}
106+
107+
@Nullable App appContext = transaction.getContexts().getApp();
108+
if (appContext == null) {
109+
appContext = new App();
110+
transaction.getContexts().setApp(appContext);
102111
}
112+
final String appStartType =
113+
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
114+
? "cold"
115+
: "warm";
116+
appContext.setStartType(appStartType);
103117
}
104118

105119
final SentryId eventId = transaction.getEventId();

sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.sentry.NoOpTransaction;
88
import io.sentry.SentryDate;
99
import io.sentry.SentryNanotimeDate;
10+
import io.sentry.SentryTracer;
1011
import io.sentry.SpanDataConvention;
1112
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
1213
import io.sentry.protocol.MeasurementValue;
@@ -135,11 +136,15 @@ private void captureFrameMetrics(@NotNull final ISpan span) {
135136
return;
136137
}
137138

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

145150
final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics();

sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import io.sentry.DateUtils;
55
import io.sentry.SentryDate;
66
import io.sentry.SentryLongDate;
7+
import io.sentry.SentryNanotimeDate;
8+
import java.util.concurrent.TimeUnit;
79
import org.jetbrains.annotations.ApiStatus;
810
import org.jetbrains.annotations.NotNull;
911
import org.jetbrains.annotations.Nullable;
@@ -21,6 +23,7 @@ public class TimeSpan implements Comparable<TimeSpan> {
2123

2224
private @Nullable String description;
2325

26+
private long startSystemNanos;
2427
private long startUnixTimeMs;
2528
private long startUptimeMs;
2629
private long stopUptimeMs;
@@ -29,6 +32,7 @@ public class TimeSpan implements Comparable<TimeSpan> {
2932
public void start() {
3033
startUptimeMs = SystemClock.uptimeMillis();
3134
startUnixTimeMs = System.currentTimeMillis();
35+
startSystemNanos = System.nanoTime();
3236
}
3337

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

4145
final long shiftMs = SystemClock.uptimeMillis() - startUptimeMs;
4246
startUnixTimeMs = System.currentTimeMillis() - shiftMs;
47+
startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs);
4348
}
4449

4550
/** Stops the time span */
@@ -90,7 +95,8 @@ public long getStartTimestampMs() {
9095
*/
9196
public @Nullable SentryDate getStartTimestamp() {
9297
if (hasStarted()) {
93-
return new SentryLongDate(DateUtils.millisToNanos(getStartTimestampMs()));
98+
return new SentryNanotimeDate(
99+
DateUtils.nanosToDate(DateUtils.millisToNanos(getStartTimestampMs())), startSystemNanos);
94100
}
95101
return null;
96102
}
@@ -162,6 +168,7 @@ public void reset() {
162168
startUptimeMs = 0;
163169
stopUptimeMs = 0;
164170
startUnixTimeMs = 0;
171+
startSystemNanos = 0;
165172
}
166173

167174
@Override

sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import kotlin.test.BeforeTest
2727
import kotlin.test.Test
2828
import kotlin.test.assertEquals
2929
import kotlin.test.assertFalse
30+
import kotlin.test.assertNull
3031
import kotlin.test.assertTrue
3132

3233
@RunWith(AndroidJUnit4::class)
@@ -464,6 +465,60 @@ class PerformanceAndroidEventProcessorTest {
464465
}
465466
}
466467

468+
@Test
469+
fun `does not set start_type field for txns without app start span`() {
470+
// given some ui.load txn
471+
setAppStart(fixture.options, coldStart = true)
472+
473+
val sut = fixture.getSut(enablePerformanceV2 = true)
474+
val context = TransactionContext("Activity", UI_LOAD_OP)
475+
val tracer = SentryTracer(context, fixture.hub)
476+
var tr = SentryTransaction(tracer)
477+
478+
// when it contains no app start span and is processed
479+
tr = sut.process(tr, Hint())
480+
481+
// start_type should not be set
482+
assertNull(tr.contexts.app?.startType)
483+
}
484+
485+
@Test
486+
fun `sets start_type field for app context`() {
487+
// given some cold app start
488+
setAppStart(fixture.options, coldStart = true)
489+
490+
val sut = fixture.getSut(enablePerformanceV2 = true)
491+
val context = TransactionContext("Activity", UI_LOAD_OP)
492+
val tracer = SentryTracer(context, fixture.hub)
493+
var tr = SentryTransaction(tracer)
494+
495+
val appStartSpan = SentrySpan(
496+
0.0,
497+
1.0,
498+
tr.contexts.trace!!.traceId,
499+
SpanId(),
500+
null,
501+
APP_START_COLD,
502+
"App Start",
503+
SpanStatus.OK,
504+
null,
505+
emptyMap(),
506+
emptyMap(),
507+
null,
508+
null
509+
)
510+
tr.spans.add(appStartSpan)
511+
512+
// when the processor attaches the app start spans
513+
tr = sut.process(tr, Hint())
514+
515+
// start_type should be set as well
516+
assertEquals(
517+
"cold",
518+
tr.contexts.app!!.startType
519+
)
520+
}
521+
467522
private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) {
468523
AppStartMetrics.getInstance().apply {
469524
appStartType = when (coldStart) {

sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ class SpanFrameMetricsCollectorTest {
4040
options.frameMetricsCollector = frameMetricsCollector
4141
options.isEnableFramesTracking = enabled
4242
options.isEnablePerformanceV2 = enabled
43-
options.setDateProvider {
44-
SentryLongDate(timeNanos)
45-
}
43+
options.dateProvider = SentryAndroidDateProvider()
4644

4745
return SpanFrameMetricsCollector(options, frameMetricsCollector)
4846
}

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ package io.sentry.uitest.android
22

33
import androidx.lifecycle.Lifecycle
44
import androidx.test.core.app.launchActivity
5+
import androidx.test.espresso.Espresso
6+
import androidx.test.espresso.IdlingRegistry
7+
import androidx.test.espresso.action.ViewActions
8+
import androidx.test.espresso.matcher.ViewMatchers
59
import androidx.test.ext.junit.runners.AndroidJUnit4
610
import io.sentry.Sentry
711
import io.sentry.SentryLevel
12+
import io.sentry.android.core.AndroidLogger
813
import io.sentry.android.core.SentryAndroidOptions
14+
import io.sentry.assertEnvelopeTransaction
15+
import io.sentry.protocol.MeasurementValue
916
import io.sentry.protocol.SentryTransaction
17+
import org.junit.Assume
1018
import org.junit.runner.RunWith
1119
import kotlin.test.Test
20+
import kotlin.test.assertEquals
21+
import kotlin.test.assertNotEquals
1222
import kotlin.test.assertTrue
1323

1424
@RunWith(AndroidJUnit4::class)
@@ -49,4 +59,84 @@ class AutomaticSpansTest : BaseUiTest() {
4959
}
5060
}
5161
}
62+
63+
@Test
64+
fun checkAppStartFramesMeasurements() {
65+
initSentry(true) { options: SentryAndroidOptions ->
66+
options.tracesSampleRate = 1.0
67+
options.isEnableTimeToFullDisplayTracing = true
68+
options.isEnablePerformanceV2 = false
69+
}
70+
71+
IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource)
72+
val sampleScenario = launchActivity<ProfilingSampleActivity>()
73+
swipeList(3)
74+
Sentry.reportFullyDisplayed()
75+
sampleScenario.moveToState(Lifecycle.State.DESTROYED)
76+
IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource)
77+
relayIdlingResource.increment()
78+
79+
relay.assert {
80+
findEnvelope {
81+
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity"
82+
}.assert {
83+
val transactionItem: SentryTransaction = it.assertTransaction()
84+
it.assertNoOtherItems()
85+
val measurements = transactionItem.measurements
86+
val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0
87+
val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0
88+
val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0
89+
assertEquals("ProfilingSampleActivity", transactionItem.transaction)
90+
// AGP matrix tests have no frames
91+
Assume.assumeTrue(totalFrames > 0)
92+
assertNotEquals(totalFrames, 0)
93+
assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.")
94+
}
95+
assertNoOtherEnvelopes()
96+
}
97+
}
98+
99+
@Test
100+
fun checkAppStartFramesMeasurementsPerfV2() {
101+
initSentry(true) { options: SentryAndroidOptions ->
102+
options.tracesSampleRate = 1.0
103+
options.isEnableTimeToFullDisplayTracing = true
104+
options.isEnablePerformanceV2 = true
105+
}
106+
107+
IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource)
108+
val sampleScenario = launchActivity<ProfilingSampleActivity>()
109+
swipeList(3)
110+
Sentry.reportFullyDisplayed()
111+
sampleScenario.moveToState(Lifecycle.State.DESTROYED)
112+
IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource)
113+
relayIdlingResource.increment()
114+
115+
relay.assert {
116+
findEnvelope {
117+
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity"
118+
}.assert {
119+
val transactionItem: SentryTransaction = it.assertTransaction()
120+
it.assertNoOtherItems()
121+
val measurements = transactionItem.measurements
122+
val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0
123+
val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0
124+
val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0
125+
assertEquals("ProfilingSampleActivity", transactionItem.transaction)
126+
// AGP matrix tests have no frames
127+
Assume.assumeTrue(totalFrames > 0)
128+
assertNotEquals(totalFrames, 0)
129+
assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.")
130+
}
131+
assertNoOtherEnvelopes()
132+
}
133+
}
134+
135+
private fun swipeList(times: Int) {
136+
repeat(times) {
137+
Thread.sleep(100)
138+
Espresso.onView(ViewMatchers.withId(R.id.profiling_sample_list)).perform(ViewActions.swipeUp())
139+
Espresso.onIdle()
140+
}
141+
}
52142
}

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class EnvelopeTests : BaseUiTest() {
157157
// Timestamps of measurements should differ at least 10 milliseconds from each other
158158
(1 until values.size).forEach { i ->
159159
assertTrue(
160-
values[i].relativeStartNs.toLong() > values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos(
160+
values[i].relativeStartNs.toLong() >= values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos(
161161
10
162162
),
163163
"Measurement value timestamp for '$name' does not differ at least 10ms"

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ internal abstract class BaseCaptureStrategy(
7272
override val currentSegment = AtomicInteger(0)
7373
override val replayCacheDir: File? get() = cache?.replayCacheDir
7474

75-
private val currentEvents = CopyOnWriteArrayList<RRWebEvent>()
75+
protected val currentEvents = CopyOnWriteArrayList<RRWebEvent>()
7676
private val currentPositions = mutableListOf<Position>()
7777
private var touchMoveBaseline = 0L
7878
private var lastCapturedMoveEvent = 0L
@@ -319,7 +319,6 @@ internal abstract class BaseCaptureStrategy(
319319
}
320320

321321
override fun onTouchEvent(event: MotionEvent) {
322-
// TODO: rotate in buffer mode
323322
val rrwebEvent = event.toRRWebIncrementalSnapshotEvent()
324323
if (rrwebEvent != null) {
325324
currentEvents += rrwebEvent

0 commit comments

Comments
 (0)