Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR] Add buffer mode and link replays with events/transactions #3291

Merged
merged 15 commits into from
Apr 4, 2024
Merged
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://key@sentry.io/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
Loading