Skip to content

Commit d7ec9af

Browse files
authored
Merge dfbb992 into 79151e9
2 parents 79151e9 + dfbb992 commit d7ec9af

File tree

31 files changed

+1359
-261
lines changed

31 files changed

+1359
-261
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ public final class io/sentry/android/core/SentryAndroid {
245245
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V
246246
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/Sentry$OptionsConfiguration;)V
247247
public static fun init (Landroid/content/Context;Lio/sentry/Sentry$OptionsConfiguration;)V
248+
public static fun pauseReplay ()V
249+
public static fun resumeReplay ()V
250+
public static fun startReplay ()V
251+
public static fun stopReplay ()V
248252
}
249253

250254
public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/SentryDateProvider {

sentry-android-core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ dependencies {
7676
api(projects.sentry)
7777
compileOnly(projects.sentryAndroidFragment)
7878
compileOnly(projects.sentryAndroidTimber)
79+
compileOnly(projects.sentryAndroidReplay)
7980
compileOnly(projects.sentryCompose)
8081

8182
// lifecycle processor, session tracking
@@ -103,6 +104,7 @@ dependencies {
103104
testImplementation(projects.sentryTestSupport)
104105
testImplementation(projects.sentryAndroidFragment)
105106
testImplementation(projects.sentryAndroidTimber)
107+
testImplementation(projects.sentryAndroidReplay)
106108
testImplementation(projects.sentryComposeHelper)
107109
testImplementation(projects.sentryAndroidNdk)
108110
testRuntimeOnly(Config.Libs.composeUi)

sentry-android-core/proguard-rules.pro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,9 @@
7272
-keepnames class io.sentry.exception.SentryHttpClientException
7373

7474
##---------------End: proguard configuration for sentry-okhttp ----------
75+
76+
##---------------Begin: proguard configuration for sentry-android-replay ----------
77+
-dontwarn io.sentry.android.replay.ReplayIntegration
78+
-dontwarn io.sentry.android.replay.ReplayIntegrationKt
79+
-keepnames class io.sentry.android.replay.ReplayIntegration
80+
##---------------End: proguard configuration for sentry-android-replay ----------

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
2323
import io.sentry.android.core.performance.AppStartMetrics;
2424
import io.sentry.android.fragment.FragmentLifecycleIntegration;
25+
import io.sentry.android.replay.ReplayIntegration;
2526
import io.sentry.android.timber.SentryTimberIntegration;
2627
import io.sentry.cache.PersistingOptionsObserver;
2728
import io.sentry.cache.PersistingScopeObserver;
2829
import io.sentry.compose.gestures.ComposeGestureTargetLocator;
2930
import io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter;
3031
import io.sentry.internal.gestures.GestureTargetLocator;
3132
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
33+
import io.sentry.transport.CurrentDateProvider;
3234
import io.sentry.transport.NoOpEnvelopeCache;
3335
import io.sentry.util.LazyEvaluator;
3436
import io.sentry.util.Objects;
@@ -230,7 +232,8 @@ static void installDefaultIntegrations(
230232
final @NotNull LoadClass loadClass,
231233
final @NotNull ActivityFramesTracker activityFramesTracker,
232234
final boolean isFragmentAvailable,
233-
final boolean isTimberAvailable) {
235+
final boolean isTimberAvailable,
236+
final boolean isReplayAvailable) {
234237

235238
// Integration MUST NOT cache option values in ctor, as they will be configured later by the
236239
// user
@@ -295,6 +298,9 @@ static void installDefaultIntegrations(
295298
new NetworkBreadcrumbsIntegration(context, buildInfoProvider, options.getLogger()));
296299
options.addIntegration(new TempSensorBreadcrumbsIntegration(context));
297300
options.addIntegration(new PhoneStateBreadcrumbsIntegration(context));
301+
if (isReplayAvailable) {
302+
options.addIntegration(new ReplayIntegration(context, CurrentDateProvider.getInstance()));
303+
}
298304
}
299305

300306
/**

sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.sentry.transport.ICurrentDateProvider;
1212
import java.util.Timer;
1313
import java.util.TimerTask;
14+
import java.util.concurrent.atomic.AtomicBoolean;
1415
import java.util.concurrent.atomic.AtomicLong;
1516
import org.jetbrains.annotations.NotNull;
1617
import org.jetbrains.annotations.Nullable;
@@ -19,11 +20,12 @@
1920
final class LifecycleWatcher implements DefaultLifecycleObserver {
2021

2122
private final AtomicLong lastUpdatedSession = new AtomicLong(0L);
23+
private final AtomicBoolean isFreshSession = new AtomicBoolean(false);
2224

2325
private final long sessionIntervalMillis;
2426

2527
private @Nullable TimerTask timerTask;
26-
private final @Nullable Timer timer;
28+
private final @NotNull Timer timer = new Timer(true);
2729
private final @NotNull Object timerLock = new Object();
2830
private final @NotNull IHub hub;
2931
private final boolean enableSessionTracking;
@@ -55,11 +57,6 @@ final class LifecycleWatcher implements DefaultLifecycleObserver {
5557
this.enableAppLifecycleBreadcrumbs = enableAppLifecycleBreadcrumbs;
5658
this.hub = hub;
5759
this.currentDateProvider = currentDateProvider;
58-
if (enableSessionTracking) {
59-
timer = new Timer(true);
60-
} else {
61-
timer = null;
62-
}
6360
}
6461

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

7673
private void startSession() {
77-
if (enableSessionTracking) {
78-
cancelTask();
74+
cancelTask();
7975

80-
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
76+
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
8177

82-
hub.configureScope(
83-
scope -> {
84-
if (lastUpdatedSession.get() == 0L) {
85-
final @Nullable Session currentSession = scope.getSession();
86-
if (currentSession != null && currentSession.getStarted() != null) {
87-
lastUpdatedSession.set(currentSession.getStarted().getTime());
88-
}
78+
hub.configureScope(
79+
scope -> {
80+
if (lastUpdatedSession.get() == 0L) {
81+
final @Nullable Session currentSession = scope.getSession();
82+
if (currentSession != null && currentSession.getStarted() != null) {
83+
lastUpdatedSession.set(currentSession.getStarted().getTime());
84+
isFreshSession.set(true);
8985
}
90-
});
86+
}
87+
});
9188

92-
final long lastUpdatedSession = this.lastUpdatedSession.get();
93-
if (lastUpdatedSession == 0L
94-
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
89+
final long lastUpdatedSession = this.lastUpdatedSession.get();
90+
if (lastUpdatedSession == 0L
91+
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
92+
if (enableSessionTracking) {
9593
addSessionBreadcrumb("start");
9694
hub.startSession();
9795
}
98-
this.lastUpdatedSession.set(currentTimeMillis);
96+
SentryAndroid.startReplay();
97+
} else if (!isFreshSession.getAndSet(false)) {
98+
// only resume if it's not a fresh session, which has been started in SentryAndroid.init
99+
SentryAndroid.resumeReplay();
99100
}
101+
this.lastUpdatedSession.set(currentTimeMillis);
100102
}
101103

102104
// App went to background and triggered this callback after 700ms
103105
// as no new screen was shown
104106
@Override
105107
public void onStop(final @NotNull LifecycleOwner owner) {
106-
if (enableSessionTracking) {
107-
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
108-
this.lastUpdatedSession.set(currentTimeMillis);
108+
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
109+
this.lastUpdatedSession.set(currentTimeMillis);
109110

110-
scheduleEndSession();
111-
}
111+
SentryAndroid.pauseReplay();
112+
scheduleEndSession();
112113

113114
AppState.getInstance().setInBackground(true);
114115
addAppBreadcrumb("background");
@@ -122,8 +123,11 @@ private void scheduleEndSession() {
122123
new TimerTask() {
123124
@Override
124125
public void run() {
125-
addSessionBreadcrumb("end");
126-
hub.endSession();
126+
if (enableSessionTracking) {
127+
addSessionBreadcrumb("end");
128+
hub.endSession();
129+
}
130+
SentryAndroid.stopReplay();
127131
}
128132
};
129133

@@ -164,7 +168,7 @@ TimerTask getTimerTask() {
164168
}
165169

166170
@TestOnly
167-
@Nullable
171+
@NotNull
168172
Timer getTimer() {
169173
return timer;
170174
}

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import io.sentry.android.core.performance.AppStartMetrics;
1616
import io.sentry.android.core.performance.TimeSpan;
1717
import io.sentry.android.fragment.FragmentLifecycleIntegration;
18+
import io.sentry.android.replay.ReplayIntegration;
19+
import io.sentry.android.replay.ReplayIntegrationKt;
1820
import io.sentry.android.timber.SentryTimberIntegration;
1921
import java.lang.reflect.InvocationTargetException;
2022
import java.util.ArrayList;
@@ -33,6 +35,11 @@ public final class SentryAndroid {
3335
static final String SENTRY_TIMBER_INTEGRATION_CLASS_NAME =
3436
"io.sentry.android.timber.SentryTimberIntegration";
3537

38+
static final String SENTRY_REPLAY_INTEGRATION_CLASS_NAME =
39+
"io.sentry.android.replay.ReplayIntegration";
40+
41+
private static boolean isReplayAvailable = false;
42+
3643
private static final String TIMBER_CLASS_NAME = "timber.log.Timber";
3744
private static final String FRAGMENT_CLASS_NAME =
3845
"androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks";
@@ -99,6 +106,8 @@ public static synchronized void init(
99106
final boolean isTimberAvailable =
100107
(isTimberUpstreamAvailable
101108
&& classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options));
109+
isReplayAvailable =
110+
classLoader.isClassAvailable(SENTRY_REPLAY_INTEGRATION_CLASS_NAME, options);
102111

103112
final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger);
104113
final LoadClass loadClass = new LoadClass();
@@ -118,7 +127,8 @@ public static synchronized void init(
118127
loadClass,
119128
activityFramesTracker,
120129
isFragmentAvailable,
121-
isTimberAvailable);
130+
isTimberAvailable,
131+
isReplayAvailable);
122132

123133
configuration.configure(options);
124134

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

147157
final @NotNull IHub hub = Sentry.getCurrentHub();
148-
if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) {
149-
hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start"));
150-
hub.startSession();
158+
if (ContextUtils.isForegroundImportance()) {
159+
if (hub.getOptions().isEnableAutoSessionTracking()) {
160+
hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start"));
161+
hub.startSession();
162+
}
163+
startReplay();
151164
}
152165
} catch (IllegalAccessException e) {
153166
logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e);
@@ -212,4 +225,59 @@ private static void deduplicateIntegrations(
212225
}
213226
}
214227
}
228+
229+
public static synchronized void startReplay() {
230+
if (!ensureReplayIntegration("starting")) {
231+
return;
232+
}
233+
final @NotNull IHub hub = Sentry.getCurrentHub();
234+
ReplayIntegrationKt.getReplayIntegration(hub).start();
235+
}
236+
237+
public static synchronized void stopReplay() {
238+
if (!ensureReplayIntegration("stopping")) {
239+
return;
240+
}
241+
final @NotNull IHub hub = Sentry.getCurrentHub();
242+
ReplayIntegrationKt.getReplayIntegration(hub).stop();
243+
}
244+
245+
public static synchronized void resumeReplay() {
246+
if (!ensureReplayIntegration("resuming")) {
247+
return;
248+
}
249+
final @NotNull IHub hub = Sentry.getCurrentHub();
250+
ReplayIntegrationKt.getReplayIntegration(hub).resume();
251+
}
252+
253+
public static synchronized void pauseReplay() {
254+
if (!ensureReplayIntegration("pausing")) {
255+
return;
256+
}
257+
final @NotNull IHub hub = Sentry.getCurrentHub();
258+
ReplayIntegrationKt.getReplayIntegration(hub).pause();
259+
}
260+
261+
private static boolean ensureReplayIntegration(final @NotNull String actionName) {
262+
final @NotNull IHub hub = Sentry.getCurrentHub();
263+
if (isReplayAvailable) {
264+
final ReplayIntegration replay = ReplayIntegrationKt.getReplayIntegration(hub);
265+
if (replay != null) {
266+
return true;
267+
} else {
268+
hub.getOptions()
269+
.getLogger()
270+
.log(
271+
SentryLevel.INFO,
272+
"Session Replay wasn't registered yet, not " + actionName + " the replay");
273+
}
274+
} else {
275+
hub.getOptions()
276+
.getLogger()
277+
.log(
278+
SentryLevel.INFO,
279+
"Session Replay wasn't found on classpath, not " + actionName + " the replay");
280+
}
281+
return false;
282+
}
215283
}

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator
1515
import io.sentry.android.core.internal.modules.AssetsModulesLoader
1616
import io.sentry.android.core.internal.util.AndroidMainThreadChecker
1717
import io.sentry.android.fragment.FragmentLifecycleIntegration
18+
import io.sentry.android.replay.ReplayIntegration
1819
import io.sentry.android.timber.SentryTimberIntegration
1920
import io.sentry.cache.PersistingOptionsObserver
2021
import io.sentry.cache.PersistingScopeObserver
@@ -83,6 +84,7 @@ class AndroidOptionsInitializerTest {
8384
loadClass,
8485
activityFramesTracker,
8586
false,
87+
false,
8688
false
8789
)
8890

@@ -99,7 +101,8 @@ class AndroidOptionsInitializerTest {
99101
minApi: Int = Build.VERSION_CODES.KITKAT,
100102
classesToLoad: List<String> = emptyList(),
101103
isFragmentAvailable: Boolean = false,
102-
isTimberAvailable: Boolean = false
104+
isTimberAvailable: Boolean = false,
105+
isReplayAvailable: Boolean = false
103106
) {
104107
mockContext = ContextUtilsTestHelper.mockMetaData(
105108
mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext = true),
@@ -126,7 +129,8 @@ class AndroidOptionsInitializerTest {
126129
loadClass,
127130
activityFramesTracker,
128131
isFragmentAvailable,
129-
isTimberAvailable
132+
isTimberAvailable,
133+
isReplayAvailable
130134
)
131135

132136
AndroidOptionsInitializer.initializeIntegrationsAndProcessors(
@@ -478,6 +482,24 @@ class AndroidOptionsInitializerTest {
478482
assertNull(actual)
479483
}
480484

485+
@Test
486+
fun `ReplayIntegration added to the integration list if available on classpath`() {
487+
fixture.initSutWithClassLoader(isReplayAvailable = true)
488+
489+
val actual =
490+
fixture.sentryOptions.integrations.firstOrNull { it is ReplayIntegration }
491+
assertNotNull(actual)
492+
}
493+
494+
@Test
495+
fun `ReplayIntegration won't be enabled, it throws class not found`() {
496+
fixture.initSutWithClassLoader(isReplayAvailable = false)
497+
498+
val actual =
499+
fixture.sentryOptions.integrations.firstOrNull { it is ReplayIntegration }
500+
assertNull(actual)
501+
}
502+
481503
@Test
482504
fun `AndroidEnvelopeCache is set to options`() {
483505
fixture.initSut()
@@ -634,6 +656,7 @@ class AndroidOptionsInitializerTest {
634656
mock(),
635657
mock(),
636658
false,
659+
false,
637660
false
638661
)
639662
verify(mockOptions, never()).outboxPath

sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class AndroidProfilerTest {
118118
loadClass,
119119
activityFramesTracker,
120120
false,
121+
false,
121122
false
122123
)
123124

sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class AndroidTransactionProfilerTest {
125125
loadClass,
126126
activityFramesTracker,
127127
false,
128+
false,
128129
false
129130
)
130131

0 commit comments

Comments
 (0)