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 replay integration #3272

Merged
merged 19 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading