Skip to content

Commit

Permalink
[SR] Add buffer mode and link replays with events/transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Apr 4, 2024
2 parents 023cb5f + 4d533fb commit 7079d7a
Show file tree
Hide file tree
Showing 27 changed files with 492 additions and 147 deletions.
4 changes: 0 additions & 4 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,6 @@ public final class io/sentry/android/core/SentryAndroid {
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/Sentry$OptionsConfiguration;)V
public static fun init (Landroid/content/Context;Lio/sentry/Sentry$OptionsConfiguration;)V
public static fun pauseReplay ()V
public static fun resumeReplay ()V
public static fun startReplay ()V
public static fun stopReplay ()V
}

public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/SentryDateProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ static void installDefaultIntegrations(
options.addIntegration(new TempSensorBreadcrumbsIntegration(context));
options.addIntegration(new PhoneStateBreadcrumbsIntegration(context));
if (isReplayAvailable) {
options.addIntegration(new ReplayIntegration(context, CurrentDateProvider.getInstance()));
final ReplayIntegration replay =
new ReplayIntegration(context, CurrentDateProvider.getInstance());
options.addIntegration(replay);
options.setReplayController(replay);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,12 @@ private void startSession() {
addSessionBreadcrumb("start");
hub.startSession();
}
SentryAndroid.startReplay();
} else if (!isFreshSession.getAndSet(false)) {
hub.getOptions().getReplayController().start();
} else if (!isFreshSession.get()) {
// only resume if it's not a fresh session, which has been started in SentryAndroid.init
SentryAndroid.resumeReplay();
hub.getOptions().getReplayController().resume();
}
isFreshSession.set(false);
this.lastUpdatedSession.set(currentTimeMillis);
}

Expand All @@ -108,7 +109,7 @@ public void onStop(final @NotNull LifecycleOwner owner) {
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
this.lastUpdatedSession.set(currentTimeMillis);

SentryAndroid.pauseReplay();
hub.getOptions().getReplayController().pause();
scheduleEndSession();

AppState.getInstance().setInBackground(true);
Expand All @@ -127,7 +128,7 @@ public void run() {
addSessionBreadcrumb("end");
hub.endSession();
}
SentryAndroid.stopReplay();
hub.getOptions().getReplayController().stop();
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.replay.ReplayIntegration;
import io.sentry.android.replay.ReplayIntegrationKt;
import io.sentry.android.timber.SentryTimberIntegration;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
Expand Down Expand Up @@ -160,7 +158,7 @@ public static synchronized void init(
hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start"));
hub.startSession();
}
startReplay();
hub.getOptions().getReplayController().start();
}
} catch (IllegalAccessException e) {
logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e);
Expand Down Expand Up @@ -225,59 +223,4 @@ private static void deduplicateIntegrations(
}
}
}

public static synchronized void startReplay() {
if (!ensureReplayIntegration("starting")) {
return;
}
final @NotNull IHub hub = Sentry.getCurrentHub();
ReplayIntegrationKt.getReplayIntegration(hub).start();
}

public static synchronized void stopReplay() {
if (!ensureReplayIntegration("stopping")) {
return;
}
final @NotNull IHub hub = Sentry.getCurrentHub();
ReplayIntegrationKt.getReplayIntegration(hub).stop();
}

public static synchronized void resumeReplay() {
if (!ensureReplayIntegration("resuming")) {
return;
}
final @NotNull IHub hub = Sentry.getCurrentHub();
ReplayIntegrationKt.getReplayIntegration(hub).resume();
}

public static synchronized void pauseReplay() {
if (!ensureReplayIntegration("pausing")) {
return;
}
final @NotNull IHub hub = Sentry.getCurrentHub();
ReplayIntegrationKt.getReplayIntegration(hub).pause();
}

private static boolean ensureReplayIntegration(final @NotNull String actionName) {
final @NotNull IHub hub = Sentry.getCurrentHub();
if (isReplayAvailable) {
final ReplayIntegration replay = ReplayIntegrationKt.getReplayIntegration(hub);
if (replay != null) {
return true;
} else {
hub.getOptions()
.getLogger()
.log(
SentryLevel.INFO,
"Session Replay wasn't registered yet, not " + actionName + " the replay");
}
} else {
hub.getOptions()
.getLogger()
.log(
SentryLevel.INFO,
"Session Replay wasn't found on classpath, not " + actionName + " the replay");
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,13 @@ class AndroidOptionsInitializerTest {
assertNotNull(actual)
}

@Test
fun `ReplayIntegration set as ReplayController if available on classpath`() {
fixture.initSutWithClassLoader(isReplayAvailable = true)

assertTrue(fixture.sentryOptions.replayController is ReplayIntegration)
}

@Test
fun `ReplayIntegration won't be enabled, it throws class not found`() {
fixture.initSutWithClassLoader(isReplayAvailable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import io.sentry.Breadcrumb
import io.sentry.DateUtils
import io.sentry.IHub
import io.sentry.IScope
import io.sentry.ReplayController
import io.sentry.ScopeCallback
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.Session
import io.sentry.Session.State
import io.sentry.transport.ICurrentDateProvider
Expand Down Expand Up @@ -34,6 +36,8 @@ class LifecycleWatcherTest {
val ownerMock = mock<LifecycleOwner>()
val hub = mock<IHub>()
val dateProvider = mock<ICurrentDateProvider>()
val options = SentryOptions()
val replayController = mock<ReplayController>()

fun getSUT(
sessionIntervalMillis: Long = 0L,
Expand All @@ -47,6 +51,8 @@ class LifecycleWatcherTest {
whenever(hub.configureScope(argumentCaptor.capture())).thenAnswer {
argumentCaptor.value.run(scope)
}
options.setReplayController(replayController)
whenever(hub.options).thenReturn(options)

return LifecycleWatcher(
hub,
Expand All @@ -70,6 +76,7 @@ class LifecycleWatcherTest {
val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).startSession()
verify(fixture.replayController).start()
}

@Test
Expand All @@ -79,6 +86,7 @@ class LifecycleWatcherTest {
watcher.onStart(fixture.ownerMock)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub, times(2)).startSession()
verify(fixture.replayController, times(2)).start()
}

@Test
Expand All @@ -88,6 +96,7 @@ class LifecycleWatcherTest {
watcher.onStart(fixture.ownerMock)
watcher.onStart(fixture.ownerMock)
verify(fixture.hub).startSession()
verify(fixture.replayController).start()
}

@Test
Expand All @@ -96,6 +105,7 @@ class LifecycleWatcherTest {
watcher.onStart(fixture.ownerMock)
watcher.onStop(fixture.ownerMock)
verify(fixture.hub, timeout(10000)).endSession()
verify(fixture.replayController, timeout(10000)).stop()
}

@Test
Expand All @@ -110,6 +120,7 @@ class LifecycleWatcherTest {
assertNull(watcher.timerTask)

verify(fixture.hub, never()).endSession()
verify(fixture.replayController, never()).stop()
}

@Test
Expand Down Expand Up @@ -241,6 +252,7 @@ class LifecycleWatcherTest {

watcher.onStart(fixture.ownerMock)
verify(fixture.hub, never()).startSession()
verify(fixture.replayController, never()).start()
}

@Test
Expand All @@ -267,6 +279,7 @@ class LifecycleWatcherTest {

watcher.onStart(fixture.ownerMock)
verify(fixture.hub).startSession()
verify(fixture.replayController).start()
}

@Test
Expand All @@ -282,4 +295,50 @@ class LifecycleWatcherTest {
watcher.onStop(fixture.ownerMock)
assertTrue(AppState.getInstance().isInBackground!!)
}

@Test
fun `if the hub has already a fresh session running, doesn't resume replay`() {
val watcher = fixture.getSUT(
enableAppLifecycleBreadcrumbs = false,
session = Session(
State.Ok,
DateUtils.getCurrentDateTime(),
DateUtils.getCurrentDateTime(),
0,
"abc",
UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"),
true,
0,
10.0,
null,
null,
null,
"release",
null
)
)

watcher.onStart(fixture.ownerMock)
verify(fixture.replayController, never()).resume()
}

@Test
fun `background-foreground replay`() {
whenever(fixture.dateProvider.currentTimeMillis).thenReturn(1L)
val watcher = fixture.getSUT(
sessionIntervalMillis = 2L,
enableAppLifecycleBreadcrumbs = false
)
watcher.onStart(fixture.ownerMock)
verify(fixture.replayController).start()

watcher.onStop(fixture.ownerMock)
verify(fixture.replayController).pause()

watcher.onStart(fixture.ownerMock)
verify(fixture.replayController).resume()

watcher.onStop(fixture.ownerMock)
verify(fixture.replayController, timeout(10000)).stop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ class SentryAndroidTest {
options.release = "prod"
options.dsn = "https://[email protected]/123"
options.isEnableAutoSessionTracking = true
options.experimental.sessionReplayOptions.errorSampleRate = 1.0
}

var session: Session? = null
Expand Down
18 changes: 7 additions & 11 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,20 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public fun close ()V
public final fun createVideoOf (JJILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
public final fun rotate (J)V
}

public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integration, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
public static final field Companion Lio/sentry/android/replay/ReplayIntegration$Companion;
public static final field VIDEO_BUFFER_DURATION J
public static final field VIDEO_SEGMENT_DURATION J
public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
public fun close ()V
public final fun isRecording ()Z
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
public final fun pause ()V
public fun pause ()V
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
public final fun resume ()V
public final fun start ()V
public final fun stop ()V
}

public final class io/sentry/android/replay/ReplayIntegration$Companion {
public fun resume ()V
public fun sendReplayForEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)V
public fun start ()V
public fun stop ()V
}

public final class io/sentry/android/replay/ReplayIntegrationKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,7 @@ public class ReplayCache internal constructor(
encoder = null
}

frames.removeAll {
if (it.timestamp < (from + duration)) {
deleteFile(it.screenshot)
return@removeAll true
}
return@removeAll false
}
rotate(until = (from + duration))

return GeneratedVideo(videoFile, frameCount, videoDuration)
}
Expand Down Expand Up @@ -211,6 +205,21 @@ public class ReplayCache internal constructor(
}
}

/**
* Removes frames from the in-memory and disk cache from start to [until].
*
* @param until value until whose the frames should be removed, represented as unix timestamp
*/
fun rotate(until: Long) {
frames.removeAll {
if (it.timestamp < until) {
deleteFile(it.screenshot)
return@removeAll true
}
return@removeAll false
}
}

override fun close() {
synchronized(encoderLock) {
encoder?.release()
Expand Down
Loading

0 comments on commit 7079d7a

Please sign in to comment.