Skip to content

Commit

Permalink
Merge pull request #3272 from getsentry/rz/feat/session-replay-integr…
Browse files Browse the repository at this point in the history
…ation

[SR] Add replay integration
  • Loading branch information
romtsn authored Apr 3, 2024
2 parents 79151e9 + 27b15d7 commit b04aaf2
Show file tree
Hide file tree
Showing 31 changed files with 1,407 additions and 261 deletions.
4 changes: 4 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ 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
2 changes: 2 additions & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ dependencies {
api(projects.sentry)
compileOnly(projects.sentryAndroidFragment)
compileOnly(projects.sentryAndroidTimber)
compileOnly(projects.sentryAndroidReplay)
compileOnly(projects.sentryCompose)

// lifecycle processor, session tracking
Expand Down Expand Up @@ -103,6 +104,7 @@ dependencies {
testImplementation(projects.sentryTestSupport)
testImplementation(projects.sentryAndroidFragment)
testImplementation(projects.sentryAndroidTimber)
testImplementation(projects.sentryAndroidReplay)
testImplementation(projects.sentryComposeHelper)
testImplementation(projects.sentryAndroidNdk)
testRuntimeOnly(Config.Libs.composeUi)
Expand Down
6 changes: 6 additions & 0 deletions sentry-android-core/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@
-keepnames class io.sentry.exception.SentryHttpClientException

##---------------End: proguard configuration for sentry-okhttp ----------

##---------------Begin: proguard configuration for sentry-android-replay ----------
-dontwarn io.sentry.android.replay.ReplayIntegration
-dontwarn io.sentry.android.replay.ReplayIntegrationKt
-keepnames class io.sentry.android.replay.ReplayIntegration
##---------------End: proguard configuration for sentry-android-replay ----------
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.replay.ReplayIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.cache.PersistingOptionsObserver;
import io.sentry.cache.PersistingScopeObserver;
import io.sentry.compose.gestures.ComposeGestureTargetLocator;
import io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.LazyEvaluator;
import io.sentry.util.Objects;
Expand Down Expand Up @@ -230,7 +232,8 @@ static void installDefaultIntegrations(
final @NotNull LoadClass loadClass,
final @NotNull ActivityFramesTracker activityFramesTracker,
final boolean isFragmentAvailable,
final boolean isTimberAvailable) {
final boolean isTimberAvailable,
final boolean isReplayAvailable) {

// Integration MUST NOT cache option values in ctor, as they will be configured later by the
// user
Expand Down Expand Up @@ -295,6 +298,9 @@ static void installDefaultIntegrations(
new NetworkBreadcrumbsIntegration(context, buildInfoProvider, options.getLogger()));
options.addIntegration(new TempSensorBreadcrumbsIntegration(context));
options.addIntegration(new PhoneStateBreadcrumbsIntegration(context));
if (isReplayAvailable) {
options.addIntegration(new ReplayIntegration(context, CurrentDateProvider.getInstance()));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.sentry.transport.ICurrentDateProvider;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -19,11 +20,12 @@
final class LifecycleWatcher implements DefaultLifecycleObserver {

private final AtomicLong lastUpdatedSession = new AtomicLong(0L);
private final AtomicBoolean isFreshSession = new AtomicBoolean(false);

private final long sessionIntervalMillis;

private @Nullable TimerTask timerTask;
private final @Nullable Timer timer;
private final @NotNull Timer timer = new Timer(true);
private final @NotNull Object timerLock = new Object();
private final @NotNull IHub hub;
private final boolean enableSessionTracking;
Expand Down Expand Up @@ -55,11 +57,6 @@ final class LifecycleWatcher implements DefaultLifecycleObserver {
this.enableAppLifecycleBreadcrumbs = enableAppLifecycleBreadcrumbs;
this.hub = hub;
this.currentDateProvider = currentDateProvider;
if (enableSessionTracking) {
timer = new Timer(true);
} else {
timer = null;
}
}

// App goes to foreground
Expand All @@ -74,41 +71,45 @@ public void onStart(final @NotNull LifecycleOwner owner) {
}

private void startSession() {
if (enableSessionTracking) {
cancelTask();
cancelTask();

final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();

hub.configureScope(
scope -> {
if (lastUpdatedSession.get() == 0L) {
final @Nullable Session currentSession = scope.getSession();
if (currentSession != null && currentSession.getStarted() != null) {
lastUpdatedSession.set(currentSession.getStarted().getTime());
}
hub.configureScope(
scope -> {
if (lastUpdatedSession.get() == 0L) {
final @Nullable Session currentSession = scope.getSession();
if (currentSession != null && currentSession.getStarted() != null) {
lastUpdatedSession.set(currentSession.getStarted().getTime());
isFreshSession.set(true);
}
});
}
});

final long lastUpdatedSession = this.lastUpdatedSession.get();
if (lastUpdatedSession == 0L
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
final long lastUpdatedSession = this.lastUpdatedSession.get();
if (lastUpdatedSession == 0L
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
if (enableSessionTracking) {
addSessionBreadcrumb("start");
hub.startSession();
}
this.lastUpdatedSession.set(currentTimeMillis);
SentryAndroid.startReplay();
} else if (!isFreshSession.getAndSet(false)) {
// only resume if it's not a fresh session, which has been started in SentryAndroid.init
SentryAndroid.resumeReplay();
}
this.lastUpdatedSession.set(currentTimeMillis);
}

// App went to background and triggered this callback after 700ms
// as no new screen was shown
@Override
public void onStop(final @NotNull LifecycleOwner owner) {
if (enableSessionTracking) {
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
this.lastUpdatedSession.set(currentTimeMillis);
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
this.lastUpdatedSession.set(currentTimeMillis);

scheduleEndSession();
}
SentryAndroid.pauseReplay();
scheduleEndSession();

AppState.getInstance().setInBackground(true);
addAppBreadcrumb("background");
Expand All @@ -122,8 +123,11 @@ private void scheduleEndSession() {
new TimerTask() {
@Override
public void run() {
addSessionBreadcrumb("end");
hub.endSession();
if (enableSessionTracking) {
addSessionBreadcrumb("end");
hub.endSession();
}
SentryAndroid.stopReplay();
}
};

Expand Down Expand Up @@ -164,7 +168,7 @@ TimerTask getTimerTask() {
}

@TestOnly
@Nullable
@NotNull
Timer getTimer() {
return timer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
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 All @@ -33,6 +35,11 @@ public final class SentryAndroid {
static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME =
"io.sentry.android.timber.SentryTimberIntegration";

static final String SENTRY_REPLAY_INTEGRATION_CLASS_NAME =
"io.sentry.android.replay.ReplayIntegration";

private static boolean isReplayAvailable = false;

private static final String TIMBER_CLASS_NAME = "timber.log.Timber";
private static final String FRAGMENT_CLASS_NAME =
"androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks";
Expand Down Expand Up @@ -99,6 +106,8 @@ public static synchronized void init(
final boolean isTimberAvailable =
(isTimberUpstreamAvailable
&& classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options));
isReplayAvailable =
classLoader.isClassAvailable(SENTRY_REPLAY_INTEGRATION_CLASS_NAME, options);

final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger);
final LoadClass loadClass = new LoadClass();
Expand All @@ -118,7 +127,8 @@ public static synchronized void init(
loadClass,
activityFramesTracker,
isFragmentAvailable,
isTimberAvailable);
isTimberAvailable,
isReplayAvailable);

configuration.configure(options);

Expand All @@ -145,9 +155,12 @@ public static synchronized void init(
true);

final @NotNull IHub hub = Sentry.getCurrentHub();
if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) {
hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start"));
hub.startSession();
if (ContextUtils.isForegroundImportance()) {
if (hub.getOptions().isEnableAutoSessionTracking()) {
hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start"));
hub.startSession();
}
startReplay();
}
} catch (IllegalAccessException e) {
logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e);
Expand Down Expand Up @@ -212,4 +225,59 @@ 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 @@ -15,6 +15,7 @@ import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator
import io.sentry.android.core.internal.modules.AssetsModulesLoader
import io.sentry.android.core.internal.util.AndroidMainThreadChecker
import io.sentry.android.fragment.FragmentLifecycleIntegration
import io.sentry.android.replay.ReplayIntegration
import io.sentry.android.timber.SentryTimberIntegration
import io.sentry.cache.PersistingOptionsObserver
import io.sentry.cache.PersistingScopeObserver
Expand Down Expand Up @@ -83,6 +84,7 @@ class AndroidOptionsInitializerTest {
loadClass,
activityFramesTracker,
false,
false,
false
)

Expand All @@ -99,7 +101,8 @@ class AndroidOptionsInitializerTest {
minApi: Int = Build.VERSION_CODES.KITKAT,
classesToLoad: List<String> = emptyList(),
isFragmentAvailable: Boolean = false,
isTimberAvailable: Boolean = false
isTimberAvailable: Boolean = false,
isReplayAvailable: Boolean = false
) {
mockContext = ContextUtilsTestHelper.mockMetaData(
mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext = true),
Expand All @@ -126,7 +129,8 @@ class AndroidOptionsInitializerTest {
loadClass,
activityFramesTracker,
isFragmentAvailable,
isTimberAvailable
isTimberAvailable,
isReplayAvailable
)

AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
Expand Down Expand Up @@ -478,6 +482,24 @@ class AndroidOptionsInitializerTest {
assertNull(actual)
}

@Test
fun `ReplayIntegration added to the integration list if available on classpath`() {
fixture.initSutWithClassLoader(isReplayAvailable = true)

val actual =
fixture.sentryOptions.integrations.firstOrNull { it is ReplayIntegration }
assertNotNull(actual)
}

@Test
fun `ReplayIntegration won't be enabled, it throws class not found`() {
fixture.initSutWithClassLoader(isReplayAvailable = false)

val actual =
fixture.sentryOptions.integrations.firstOrNull { it is ReplayIntegration }
assertNull(actual)
}

@Test
fun `AndroidEnvelopeCache is set to options`() {
fixture.initSut()
Expand Down Expand Up @@ -634,6 +656,7 @@ class AndroidOptionsInitializerTest {
mock(),
mock(),
false,
false,
false
)
verify(mockOptions, never()).outboxPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class AndroidProfilerTest {
loadClass,
activityFramesTracker,
false,
false,
false
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class AndroidTransactionProfilerTest {
loadClass,
activityFramesTracker,
false,
false,
false
)

Expand Down
Loading

0 comments on commit b04aaf2

Please sign in to comment.