Skip to content

Commit

Permalink
[SR] GA Session replay (#4017)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Jan 2, 2025
1 parent 90d524d commit 4267ac9
Show file tree
Hide file tree
Showing 29 changed files with 141 additions and 117 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

## Unreleased

### Features

- Session Replay GA ([#4017](https://github.com/getsentry/sentry-java/pull/4017))

To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onErrorSampleRate` options.

```kotlin
import io.sentry.SentryReplayOptions
import io.sentry.android.core.SentryAndroid

SentryAndroid.init(context) { options ->

options.sessionReplay.sessionSampleRate = 1.0
options.sessionReplay.onErrorSampleRate = 1.0

// To change default redaction behavior (defaults to true)
options.sessionReplay.redactAllImages = true
options.sessionReplay.redactAllText = true

// To change quality of the recording (defaults to MEDIUM)
options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality.MEDIUM // (LOW|MEDIUM|HIGH)
}
```

### Fixes

- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937))
Expand All @@ -13,6 +37,10 @@
- Session Replay: Allow overriding `SdkVersion` for replay events ([#4014](https://github.com/getsentry/sentry-java/pull/4014))
- Session Replay: Send replay options as tags ([#4015](https://github.com/getsentry/sentry-java/pull/4015))

### Breaking changes

- Session Replay options were moved from under `experimental` to the main `options` object ([#4017](https://github.com/getsentry/sentry-java/pull/4017))

## 7.19.1

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,28 +399,26 @@ static void applyMetadata(
options.setEnableMetrics(
readBool(metadata, logger, ENABLE_METRICS, options.isEnableMetrics()));

if (options.getExperimental().getSessionReplay().getSessionSampleRate() == null) {
if (options.getSessionReplay().getSessionSampleRate() == null) {
final Double sessionSampleRate =
readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE);
if (sessionSampleRate != -1) {
options.getExperimental().getSessionReplay().setSessionSampleRate(sessionSampleRate);
options.getSessionReplay().setSessionSampleRate(sessionSampleRate);
}
}

if (options.getExperimental().getSessionReplay().getOnErrorSampleRate() == null) {
if (options.getSessionReplay().getOnErrorSampleRate() == null) {
final Double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE);
if (onErrorSampleRate != -1) {
options.getExperimental().getSessionReplay().setOnErrorSampleRate(onErrorSampleRate);
options.getSessionReplay().setOnErrorSampleRate(onErrorSampleRate);
}
}

options
.getExperimental()
.getSessionReplay()
.setMaskAllText(readBool(metadata, logger, REPLAYS_MASK_ALL_TEXT, true));

options
.getExperimental()
.getSessionReplay()
.setMaskAllImages(readBool(metadata, logger, REPLAYS_MASK_ALL_IMAGES, true));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,22 +1459,22 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate)
}

@Test
fun `applyMetadata does not override replays onErrorSampleRate from options`() {
// Arrange
val expectedSampleRate = 0.99f
fixture.options.experimental.sessionReplay.onErrorSampleRate = expectedSampleRate.toDouble()
fixture.options.sessionReplay.onErrorSampleRate = expectedSampleRate.toDouble()
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f)
val context = fixture.getContext(metaData = bundle)

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

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate)
}

@Test
Expand All @@ -1486,7 +1486,7 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertNull(fixture.options.experimental.sessionReplay.onErrorSampleRate)
assertNull(fixture.options.sessionReplay.onErrorSampleRate)
}

@Test
Expand All @@ -1499,8 +1499,8 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
assertTrue(fixture.options.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}

@Test
Expand All @@ -1512,8 +1512,8 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
assertTrue(fixture.options.sessionReplay.maskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}

@Test
Expand Down Expand Up @@ -1562,7 +1562,7 @@ class ManifestMetadataReaderTest {
assertEquals(expectedSampleRate.toDouble(), fixture.options.sampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.tracesSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.sessionSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.sessionSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ class SentryAndroidTest {
options.release = "prod"
options.dsn = "https://[email protected]/123"
options.isEnableAutoSessionTracking = true
options.experimental.sessionReplay.onErrorSampleRate = 1.0
options.sessionReplay.onErrorSampleRate = 1.0
optionsConfig(options)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ReplayTest : BaseUiTest() {
activityScenario.moveToState(Lifecycle.State.RESUMED)

initSentry {
it.experimental.sessionReplay.sessionSampleRate = 1.0
it.sessionReplay.sessionSampleRate = 1.0

it.beforeSendReplay =
SentryOptions.BeforeSendReplayCallback { event, _ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public class ReplayCache(
it.createNewFile()
}
screenshot.outputStream().use {
bitmap.compress(JPEG, options.experimental.sessionReplay.quality.screenshotQuality, it)
bitmap.compress(JPEG, options.sessionReplay.quality.screenshotQuality, it)
it.flush()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ public class ReplayIntegration(
return
}

if (!options.experimental.sessionReplay.isSessionReplayEnabled &&
!options.experimental.sessionReplay.isSessionReplayForErrorsEnabled
if (!options.sessionReplay.isSessionReplayEnabled &&
!options.sessionReplay.isSessionReplayForErrorsEnabled
) {
options.logger.log(INFO, "Session replay is disabled, no sample rate specified")
return
Expand All @@ -132,7 +132,7 @@ public class ReplayIntegration(

options.connectionStatusProvider.addConnectionStatusObserver(this)
hub.rateLimiter?.addRateLimitObserver(this)
if (options.experimental.sessionReplay.isTrackOrientationChange) {
if (options.sessionReplay.isTrackOrientationChange) {
try {
context.registerComponentCallbacks(this)
} catch (e: Throwable) {
Expand Down Expand Up @@ -167,13 +167,13 @@ public class ReplayIntegration(
return
}

val isFullSession = random.sample(options.experimental.sessionReplay.sessionSampleRate)
if (!isFullSession && !options.experimental.sessionReplay.isSessionReplayForErrorsEnabled) {
val isFullSession = random.sample(options.sessionReplay.sessionSampleRate)
if (!isFullSession && !options.sessionReplay.isSessionReplayForErrorsEnabled) {
options.logger.log(INFO, "Session replay is not started, full session was not sampled and onErrorSampleRate is not specified")
return
}

val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) {
SessionCaptureStrategy(options, hub, dateProvider, replayExecutor, replayCacheProvider)
} else {
Expand Down Expand Up @@ -264,7 +264,7 @@ public class ReplayIntegration(

options.connectionStatusProvider.removeConnectionStatusObserver(this)
hub?.rateLimiter?.removeRateLimitObserver(this)
if (options.experimental.sessionReplay.isTrackOrientationChange) {
if (options.sessionReplay.isTrackOrientationChange) {
try {
context.unregisterComponentCallbacks(this)
} catch (ignored: Throwable) {
Expand All @@ -285,7 +285,7 @@ public class ReplayIntegration(
recorder?.stop()

// refresh config based on new device configuration
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay)
captureStrategy?.onConfigurationChanged(recorderConfig)

recorder?.start(recorderConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ internal class BufferCaptureStrategy(
isTerminating: Boolean,
onSegmentSent: (Date) -> Unit
) {
val sampled = random.sample(options.experimental.sessionReplay.onErrorSampleRate)
val sampled = random.sample(options.sessionReplay.onErrorSampleRate)

if (!sampled) {
options.logger.log(INFO, "Replay wasn't sampled by onErrorSampleRate, not capturing for event")
Expand Down Expand Up @@ -107,7 +107,7 @@ internal class BufferCaptureStrategy(
cache?.store(frameTimestamp)

val now = dateProvider.currentTimeMillis
val bufferLimit = now - options.experimental.sessionReplay.errorReplayDuration
val bufferLimit = now - options.sessionReplay.errorReplayDuration
screenAtStart = cache?.rotate(bufferLimit)
bufferedSegments.rotate(bufferLimit)
}
Expand Down Expand Up @@ -137,7 +137,7 @@ internal class BufferCaptureStrategy(

override fun onTouchEvent(event: MotionEvent) {
super.onTouchEvent(event)
val bufferLimit = dateProvider.currentTimeMillis - options.experimental.sessionReplay.errorReplayDuration
val bufferLimit = dateProvider.currentTimeMillis - options.sessionReplay.errorReplayDuration
rotateEvents(currentEvents, bufferLimit)
}

Expand Down Expand Up @@ -189,7 +189,7 @@ internal class BufferCaptureStrategy(
}

private fun createCurrentSegment(taskName: String, onSegmentCreated: (ReplaySegment) -> Unit) {
val errorReplayDuration = options.experimental.sessionReplay.errorReplayDuration
val errorReplayDuration = options.sessionReplay.errorReplayDuration
val now = dateProvider.currentTimeMillis
val currentSegmentTimestamp = if (cache?.frames?.isNotEmpty() == true) {
// in buffer mode we have to set the timestamp of the first frame as the actual start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ internal class SessionCaptureStrategy(
}

val now = dateProvider.currentTimeMillis
if ((now - currentSegmentTimestamp.time >= options.experimental.sessionReplay.sessionSegmentDuration)) {
if ((now - currentSegmentTimestamp.time >= options.sessionReplay.sessionSegmentDuration)) {
val segment =
createSegmentInternal(
options.experimental.sessionReplay.sessionSegmentDuration,
options.sessionReplay.sessionSegmentDuration,
currentSegmentTimestamp,
currentReplayId,
currentSegment,
Expand All @@ -110,7 +110,7 @@ internal class SessionCaptureStrategy(
}
}

if ((now - replayStartTimestamp.get() >= options.experimental.sessionReplay.sessionDuration)) {
if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
options.replayController.stop()
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ internal object ComposeViewHierarchyNode {
}

val className = getProxyClassName(isImage)
if (options.experimental.sessionReplay.unmaskViewClasses.contains(className)) {
if (options.sessionReplay.unmaskViewClasses.contains(className)) {
return false
}

return options.experimental.sessionReplay.maskViewClasses.contains(className)
return options.sessionReplay.maskViewClasses.contains(className)
}

private var _rootCoordinates: WeakReference<LayoutCoordinates>? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,22 +269,22 @@ sealed class ViewHierarchyNode(
return false
}

if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.unmaskViewClasses)) {
if (this.javaClass.isAssignableFrom(options.sessionReplay.unmaskViewClasses)) {
return false
}

return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.maskViewClasses)
return this.javaClass.isAssignableFrom(options.sessionReplay.maskViewClasses)
}

private fun ViewParent.isUnmaskContainer(options: SentryOptions): Boolean {
val unmaskContainer =
options.experimental.sessionReplay.unmaskViewContainerClass ?: return false
options.sessionReplay.unmaskViewContainerClass ?: return false
return this.javaClass.name == unmaskContainer
}

private fun View.isMaskContainer(options: SentryOptions): Boolean {
val maskContainer =
options.experimental.sessionReplay.maskViewContainerClass ?: return false
options.sessionReplay.maskViewContainerClass ?: return false
return this.javaClass.name == maskContainer
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class AnrWithReplayIntegrationTest {
it.cacheDirPath = cacheDir
it.isDebug = true
it.setLogger(SystemOutLogger())
it.experimental.sessionReplay.onErrorSampleRate = 1.0
it.sessionReplay.onErrorSampleRate = 1.0
// beforeSend is called after event processors are applied, so we can assert here
// against the enriched ANR event
it.beforeSend = SentryOptions.BeforeSendCallback { event, _ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ class ReplayIntegrationTest {
dateProvider: ICurrentDateProvider = CurrentDateProvider.getInstance()
): ReplayIntegration {
options.run {
experimental.sessionReplay.onErrorSampleRate = onErrorSampleRate
experimental.sessionReplay.sessionSampleRate = sessionSampleRate
sessionReplay.onErrorSampleRate = onErrorSampleRate
sessionReplay.sessionSampleRate = sessionSampleRate
connectionStatusProvider = mock {
on { connectionStatus }.thenReturn(if (isOffline) DISCONNECTED else CONNECTED)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ class ReplayIntegrationWithRecorderTest {
// fake current time to trigger segment creation, CurrentDateProvider.getInstance() should
// be used in prod
val dateProvider = ICurrentDateProvider {
System.currentTimeMillis() + fixture.options.experimental.sessionReplay.sessionSegmentDuration
System.currentTimeMillis() + fixture.options.sessionReplay.sessionSegmentDuration
}

fixture.options.experimental.sessionReplay.sessionSampleRate = 1.0
fixture.options.sessionReplay.sessionSampleRate = 1.0
fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath

val replay: ReplayIntegration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class ReplaySmokeTest {
captured.set(true)
}

fixture.options.experimental.sessionReplay.sessionSampleRate = 1.0
fixture.options.sessionReplay.sessionSampleRate = 1.0
fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath

val replay: ReplayIntegration = fixture.getSut(context)
Expand Down Expand Up @@ -155,7 +155,7 @@ class ReplaySmokeTest {
captured.set(true)
}

fixture.options.experimental.sessionReplay.onErrorSampleRate = 1.0
fixture.options.sessionReplay.onErrorSampleRate = 1.0
fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath

val replay: ReplayIntegration = fixture.getSut(context)
Expand Down Expand Up @@ -204,7 +204,7 @@ class ReplaySmokeTest {

@Test
fun `works when double inited`() {
fixture.options.experimental.sessionReplay.sessionSampleRate = 1.0
fixture.options.sessionReplay.sessionSampleRate = 1.0
fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath

// first init + close
Expand Down
Loading

0 comments on commit 4267ac9

Please sign in to comment.