Skip to content

Commit 7673c95

Browse files
authored
Merge branch 'main' into fix/app-start-profiler-avoid-stop
2 parents 7016a16 + 014dbef commit 7673c95

File tree

33 files changed

+950
-360
lines changed

33 files changed

+950
-360
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,32 @@
55
### Fixes
66

77
- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
8+
9+
*Breaking changes*:
10+
11+
- `options.experimental.sessionReplay.errorSampleRate` was renamed to `options.experimental.sessionReplay.onErrorSampleRate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
12+
- Manifest option `io.sentry.session-replay.error-sample-rate` was renamed to `io.sentry.session-replay.on-error-sample-rate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
13+
14+
## 7.14.0
15+
16+
### Features
17+
18+
- Session Replay: Gesture/touch support for Flutter ([#3623](https://github.com/getsentry/sentry-java/pull/3623))
19+
20+
### Fixes
21+
22+
- Fix app start spans missing from Pixel devices ([#3634](https://github.com/getsentry/sentry-java/pull/3634))
823
- Avoid ArrayIndexOutOfBoundsException on Android cpu data collection ([#3598](https://github.com/getsentry/sentry-java/pull/3598))
924
- Fix lazy select queries instrumentation ([#3604](https://github.com/getsentry/sentry-java/pull/3604))
25+
- Session Replay: buffer mode improvements ([#3622](https://github.com/getsentry/sentry-java/pull/3622))
26+
- Align next segment timestamp with the end of the buffered segment when converting from buffer mode to session mode
27+
- Persist `buffer` replay type for the entire replay when converting from buffer mode to session mode
28+
- Properly store screen names for `buffer` mode
29+
- Session Replay: fix various crashes and issues ([#3628](https://github.com/getsentry/sentry-java/pull/3628))
30+
- Fix video not being encoded on Pixel devices
31+
- Fix SIGABRT native crashes on Xiaomi devices when encoding a video
32+
- Fix `RejectedExecutionException` when redacting a screenshot
33+
- Fix `FileNotFoundException` when persisting segment values
1034

1135
### Chores
1236

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.13.0
13+
versionName=7.14.0
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/ManifestMetadataReader.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ final class ManifestMetadataReader {
106106

107107
static final String REPLAYS_SESSION_SAMPLE_RATE = "io.sentry.session-replay.session-sample-rate";
108108

109-
static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.error-sample-rate";
109+
static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.on-error-sample-rate";
110110

111111
static final String REPLAYS_REDACT_ALL_TEXT = "io.sentry.session-replay.redact-all-text";
112112

@@ -399,10 +399,10 @@ static void applyMetadata(
399399
}
400400
}
401401

402-
if (options.getExperimental().getSessionReplay().getErrorSampleRate() == null) {
403-
final Double errorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE);
404-
if (errorSampleRate != -1) {
405-
options.getExperimental().getSessionReplay().setErrorSampleRate(errorSampleRate);
402+
if (options.getExperimental().getSessionReplay().getOnErrorSampleRate() == null) {
403+
final Double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE);
404+
if (onErrorSampleRate != -1) {
405+
options.getExperimental().getSessionReplay().setOnErrorSampleRate(onErrorSampleRate);
406406
}
407407
}
408408

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ public void registerApplicationForegroundCheck(final @NotNull Application applic
241241
isCallbackRegistered = true;
242242
appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance();
243243
application.registerActivityLifecycleCallbacks(instance);
244+
// We post on the main thread a task to post a check on the main thread. On Pixel devices
245+
// (possibly others) the first task posted on the main thread is called before the
246+
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
247+
// callback is called before the application one.
248+
new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain(application));
249+
}
250+
251+
private void checkCreateTimeOnMain(final @NotNull Application application) {
244252
new Handler(Looper.getMainLooper())
245253
.post(
246254
() -> {

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ class ManifestMetadataReaderTest {
14221422
}
14231423

14241424
@Test
1425-
fun `applyMetadata reads replays errorSampleRate from metadata`() {
1425+
fun `applyMetadata reads replays onErrorSampleRate from metadata`() {
14261426
// Arrange
14271427
val expectedSampleRate = 0.99f
14281428

@@ -1433,34 +1433,34 @@ class ManifestMetadataReaderTest {
14331433
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
14341434

14351435
// Assert
1436-
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.errorSampleRate)
1436+
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate)
14371437
}
14381438

14391439
@Test
1440-
fun `applyMetadata does not override replays errorSampleRate from options`() {
1440+
fun `applyMetadata does not override replays onErrorSampleRate from options`() {
14411441
// Arrange
14421442
val expectedSampleRate = 0.99f
1443-
fixture.options.experimental.sessionReplay.errorSampleRate = expectedSampleRate.toDouble()
1443+
fixture.options.experimental.sessionReplay.onErrorSampleRate = expectedSampleRate.toDouble()
14441444
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f)
14451445
val context = fixture.getContext(metaData = bundle)
14461446

14471447
// Act
14481448
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
14491449

14501450
// Assert
1451-
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.errorSampleRate)
1451+
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate)
14521452
}
14531453

14541454
@Test
1455-
fun `applyMetadata without specifying replays errorSampleRate, stays null`() {
1455+
fun `applyMetadata without specifying replays onErrorSampleRate, stays null`() {
14561456
// Arrange
14571457
val context = fixture.getContext()
14581458

14591459
// Act
14601460
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
14611461

14621462
// Assert
1463-
assertNull(fixture.options.experimental.sessionReplay.errorSampleRate)
1463+
assertNull(fixture.options.experimental.sessionReplay.onErrorSampleRate)
14641464
}
14651465

14661466
@Test

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ class SentryAndroidTest {
369369
options.release = "prod"
370370
options.dsn = "https://[email protected]/123"
371371
options.isEnableAutoSessionTracking = true
372-
options.experimental.sessionReplay.errorSampleRate = 1.0
372+
options.experimental.sessionReplay.onErrorSampleRate = 1.0
373373
optionsConfig(options)
374374
}
375375

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,20 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos
3636
public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
3737
public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion;
3838
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
39-
public final fun addFrame (Ljava/io/File;J)V
39+
public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V
40+
public static synthetic fun addFrame$default (Lio/sentry/android/replay/ReplayCache;Ljava/io/File;JLjava/lang/String;ILjava/lang/Object;)V
4041
public fun close ()V
4142
public final fun createVideoOf (JJIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
4243
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
4344
public final fun persistSegmentValues (Ljava/lang/String;Ljava/lang/String;)V
44-
public final fun rotate (J)V
45+
public final fun rotate (J)Ljava/lang/String;
4546
}
4647

4748
public final class io/sentry/android/replay/ReplayCache$Companion {
4849
public final fun makeReplayCacheDir (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)Ljava/io/File;
4950
}
5051

51-
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/TouchRecorderCallback, java/io/Closeable {
52+
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, java/io/Closeable {
5253
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
5354
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
5455
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -102,7 +103,18 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion {
102103
public final fun from (Landroid/content/Context;Lio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
103104
}
104105

105-
public abstract interface class io/sentry/android/replay/TouchRecorderCallback {
106+
public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener {
107+
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/android/replay/gestures/TouchRecorderCallback;)V
108+
public fun onRootViewsChanged (Landroid/view/View;Z)V
109+
public final fun stop ()V
110+
}
111+
112+
public final class io/sentry/android/replay/gestures/ReplayGestureConverter {
113+
public fun <init> (Lio/sentry/transport/ICurrentDateProvider;)V
114+
public final fun convert (Landroid/view/MotionEvent;Lio/sentry/android/replay/ScreenshotRecorderConfig;)Ljava/util/List;
115+
}
116+
117+
public abstract interface class io/sentry/android/replay/gestures/TouchRecorderCallback {
106118
public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V
107119
}
108120

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public class ReplayCache(
7676
* @param bitmap the frame screenshot
7777
* @param frameTimestamp the timestamp when the frame screenshot was taken
7878
*/
79-
internal fun addFrame(bitmap: Bitmap, frameTimestamp: Long) {
79+
internal fun addFrame(bitmap: Bitmap, frameTimestamp: Long, screen: String? = null) {
8080
if (replayCacheDir == null || bitmap.isRecycled) {
8181
return
8282
}
@@ -89,7 +89,7 @@ public class ReplayCache(
8989
it.flush()
9090
}
9191

92-
addFrame(screenshot, frameTimestamp)
92+
addFrame(screenshot, frameTimestamp, screen)
9393
}
9494

9595
/**
@@ -101,8 +101,8 @@ public class ReplayCache(
101101
* @param screenshot file containing the frame screenshot
102102
* @param frameTimestamp the timestamp when the frame screenshot was taken
103103
*/
104-
public fun addFrame(screenshot: File, frameTimestamp: Long) {
105-
val frame = ReplayFrame(screenshot, frameTimestamp)
104+
public fun addFrame(screenshot: File, frameTimestamp: Long, screen: String? = null) {
105+
val frame = ReplayFrame(screenshot, frameTimestamp, screen)
106106
frames += frame
107107
}
108108

@@ -233,15 +233,20 @@ public class ReplayCache(
233233
* Removes frames from the in-memory and disk cache from start to [until].
234234
*
235235
* @param until value until whose the frames should be removed, represented as unix timestamp
236+
* @return the first screen in the rotated buffer, if any
236237
*/
237-
fun rotate(until: Long) {
238+
fun rotate(until: Long): String? {
239+
var screen: String? = null
238240
frames.removeAll {
239241
if (it.timestamp < until) {
240242
deleteFile(it.screenshot)
241243
return@removeAll true
244+
} else if (screen == null) {
245+
screen = it.screen
242246
}
243247
return@removeAll false
244248
}
249+
return screen
245250
}
246251

247252
override fun close() {
@@ -426,7 +431,8 @@ internal data class LastSegmentData(
426431

427432
internal data class ReplayFrame(
428433
val screenshot: File,
429-
val timestamp: Long
434+
val timestamp: Long,
435+
val screen: String? = null
430436
)
431437

432438
public data class GeneratedVideo(

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import io.sentry.Integration
1212
import io.sentry.NoOpReplayBreadcrumbConverter
1313
import io.sentry.ReplayBreadcrumbConverter
1414
import io.sentry.ReplayController
15-
import io.sentry.ScopeObserverAdapter
1615
import io.sentry.SentryIntegrationPackageStorage
1716
import io.sentry.SentryLevel.DEBUG
1817
import io.sentry.SentryLevel.INFO
@@ -21,14 +20,15 @@ import io.sentry.android.replay.capture.BufferCaptureStrategy
2120
import io.sentry.android.replay.capture.CaptureStrategy
2221
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
2322
import io.sentry.android.replay.capture.SessionCaptureStrategy
23+
import io.sentry.android.replay.gestures.GestureRecorder
24+
import io.sentry.android.replay.gestures.TouchRecorderCallback
2425
import io.sentry.android.replay.util.MainLooperHandler
2526
import io.sentry.android.replay.util.sample
2627
import io.sentry.android.replay.util.submitSafely
2728
import io.sentry.cache.PersistingScopeObserver
2829
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
2930
import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME
3031
import io.sentry.hints.Backfillable
31-
import io.sentry.protocol.Contexts
3232
import io.sentry.protocol.SentryId
3333
import io.sentry.transport.ICurrentDateProvider
3434
import io.sentry.util.FileUtils
@@ -39,6 +39,7 @@ import java.io.File
3939
import java.security.SecureRandom
4040
import java.util.LinkedList
4141
import java.util.concurrent.atomic.AtomicBoolean
42+
import kotlin.LazyThreadSafetyMode.NONE
4243

4344
public class ReplayIntegration(
4445
private val context: Context,
@@ -64,16 +65,20 @@ public class ReplayIntegration(
6465
recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)?,
6566
replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)?,
6667
replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null,
67-
mainLooperHandler: MainLooperHandler? = null
68+
mainLooperHandler: MainLooperHandler? = null,
69+
gestureRecorderProvider: (() -> GestureRecorder)? = null
6870
) : this(context, dateProvider, recorderProvider, recorderConfigProvider, replayCacheProvider) {
6971
this.replayCaptureStrategyProvider = replayCaptureStrategyProvider
7072
this.mainLooperHandler = mainLooperHandler ?: MainLooperHandler()
73+
this.gestureRecorderProvider = gestureRecorderProvider
7174
}
7275

7376
private lateinit var options: SentryOptions
7477
private var hub: IHub? = null
7578
private var recorder: Recorder? = null
79+
private var gestureRecorder: GestureRecorder? = null
7680
private val random by lazy { SecureRandom() }
81+
private val rootViewsSpy by lazy(NONE) { RootViewsSpy.install() }
7782

7883
// TODO: probably not everything has to be thread-safe here
7984
internal val isEnabled = AtomicBoolean(false)
@@ -83,6 +88,7 @@ public class ReplayIntegration(
8388
private var replayBreadcrumbConverter: ReplayBreadcrumbConverter = NoOpReplayBreadcrumbConverter.getInstance()
8489
private var replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null
8590
private var mainLooperHandler: MainLooperHandler = MainLooperHandler()
91+
private var gestureRecorderProvider: (() -> GestureRecorder)? = null
8692

8793
private lateinit var recorderConfig: ScreenshotRecorderConfig
8894

@@ -102,13 +108,8 @@ public class ReplayIntegration(
102108
}
103109

104110
this.hub = hub
105-
this.options.addScopeObserver(object : ScopeObserverAdapter() {
106-
override fun setContexts(contexts: Contexts) {
107-
// scope screen has fully-qualified name
108-
captureStrategy?.onScreenChanged(contexts.app?.viewNames?.lastOrNull()?.substringAfterLast('.'))
109-
}
110-
})
111-
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, this, mainLooperHandler)
111+
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler)
112+
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
112113
isEnabled.set(true)
113114

114115
try {
@@ -142,7 +143,7 @@ public class ReplayIntegration(
142143

143144
val isFullSession = random.sample(options.experimental.sessionReplay.sessionSampleRate)
144145
if (!isFullSession && !options.experimental.sessionReplay.isSessionReplayForErrorsEnabled) {
145-
options.logger.log(INFO, "Session replay is not started, full session was not sampled and errorSampleRate is not specified")
146+
options.logger.log(INFO, "Session replay is not started, full session was not sampled and onErrorSampleRate is not specified")
146147
return
147148
}
148149

@@ -155,6 +156,7 @@ public class ReplayIntegration(
155156

156157
captureStrategy?.start(recorderConfig)
157158
recorder?.start(recorderConfig)
159+
registerRootViewListeners()
158160
}
159161

160162
override fun resume() {
@@ -176,8 +178,9 @@ public class ReplayIntegration(
176178
return
177179
}
178180

179-
captureStrategy?.captureReplay(isTerminating == true, onSegmentSent = {
181+
captureStrategy?.captureReplay(isTerminating == true, onSegmentSent = { newTimestamp ->
180182
captureStrategy?.currentSegment = captureStrategy?.currentSegment!! + 1
183+
captureStrategy?.segmentTimestamp = newTimestamp
181184
})
182185
captureStrategy = captureStrategy?.convert()
183186
}
@@ -204,16 +207,20 @@ public class ReplayIntegration(
204207
return
205208
}
206209

210+
unregisterRootViewListeners()
207211
recorder?.stop()
212+
gestureRecorder?.stop()
208213
captureStrategy?.stop()
209214
isRecording.set(false)
210215
captureStrategy?.close()
211216
captureStrategy = null
212217
}
213218

214219
override fun onScreenshotRecorded(bitmap: Bitmap) {
220+
var screen: String? = null
221+
hub?.configureScope { screen = it.screen?.substringAfterLast('.') }
215222
captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp ->
216-
addFrame(bitmap, frameTimeStamp)
223+
addFrame(bitmap, frameTimeStamp, screen)
217224
}
218225
}
219226

@@ -257,6 +264,20 @@ public class ReplayIntegration(
257264
captureStrategy?.onTouchEvent(event)
258265
}
259266

267+
private fun registerRootViewListeners() {
268+
if (recorder is OnRootViewsChangedListener) {
269+
rootViewsSpy.listeners += (recorder as OnRootViewsChangedListener)
270+
}
271+
rootViewsSpy.listeners += gestureRecorder
272+
}
273+
274+
private fun unregisterRootViewListeners() {
275+
if (recorder is OnRootViewsChangedListener) {
276+
rootViewsSpy.listeners -= (recorder as OnRootViewsChangedListener)
277+
}
278+
rootViewsSpy.listeners -= gestureRecorder
279+
}
280+
260281
private fun cleanupReplays(unfinishedReplayId: String = "") {
261282
// clean up old replays
262283
options.cacheDirPath?.let { cacheDir ->

0 commit comments

Comments
 (0)