Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
07b758b
Get rid of the lock on touch events
romtsn Dec 17, 2024
e3df539
pre-allocate some things for gesture converter
romtsn Dec 17, 2024
fe15278
have one less thread switch for re]play
romtsn Dec 18, 2024
c65f9b6
update
romtsn Dec 18, 2024
14e8155
Merge branch 'main' into rz/fix/session-replay-anr-ontouchevent
romtsn Dec 18, 2024
bc2e9d6
Changelog
romtsn Dec 18, 2024
fb832b2
Add option to disable orientation change tracking for session replay
romtsn Dec 19, 2024
55dbeec
Make RecorderConfig lazier
romtsn Dec 20, 2024
01b5a88
Fix tests
romtsn Dec 20, 2024
e963707
Changelog
romtsn Dec 20, 2024
c48094f
Allow overriding SdkVersion for replay events only
romtsn Dec 30, 2024
94586a7
Send replay recording options
romtsn Dec 30, 2024
26ccb69
Merge branch 'main' into rz/feat/session-replay-override-sdk-version
romtsn Dec 30, 2024
870fdd8
Changelog
romtsn Dec 30, 2024
fdcae96
Merge branch 'rz/feat/session-replay-override-sdk-version' into rz/fe…
romtsn Dec 30, 2024
cba3c72
Changelog
romtsn Dec 30, 2024
4bf8acd
Add a comment
romtsn Dec 30, 2024
8869e77
Merge branch 'rz/feat/session-replay-override-sdk-version' into rz/fe…
romtsn Dec 30, 2024
35313ba
Add a test
romtsn Dec 30, 2024
fa37ca3
Change joinToString to serializable list for options
romtsn Dec 30, 2024
a5f1ba5
Reduce heap allocations in screenshot recorder
romtsn Jan 2, 2025
c709c50
Formatting
romtsn Jan 2, 2025
cd14384
Add test
romtsn Jan 2, 2025
fa3ae2e
Merge branch 'main' into rz/fix/delete-corrupted-frames
romtsn Jan 2, 2025
179088d
Fix
romtsn Jan 2, 2025
036e373
Changelog
romtsn Jan 2, 2025
d812f11
GA Session replay
romtsn Jan 2, 2025
6649e9b
Format code
getsentry-bot Jan 2, 2025
c468058
Changelog
romtsn Jan 2, 2025
c6257b8
Api dump
romtsn Jan 2, 2025
6805ced
Update CHANGELOG.md
romtsn Jan 2, 2025
665d47e
Update CHANGELOG.md
romtsn Jan 2, 2025
78746fb
Merge branch 'main' into rz/feat/session-replay-ga
romtsn Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading