Skip to content

Commit 14060c4

Browse files
authored
Merge 9e87fe8 into 79151e9
2 parents 79151e9 + 9e87fe8 commit 14060c4

File tree

22 files changed

+1005
-208
lines changed

22 files changed

+1005
-208
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: 1 addition & 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

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: 76 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,63 @@ private static void deduplicateIntegrations(
212225
}
213226
}
214227
}
228+
229+
public static synchronized void startReplay() {
230+
performReplayAction(
231+
"starting",
232+
(replay) -> {
233+
replay.start();
234+
});
235+
}
236+
237+
public static synchronized void stopReplay() {
238+
performReplayAction(
239+
"stopping",
240+
(replay) -> {
241+
replay.stop();
242+
});
243+
}
244+
245+
public static synchronized void resumeReplay() {
246+
performReplayAction(
247+
"resuming",
248+
(replay) -> {
249+
replay.resume();
250+
});
251+
}
252+
253+
public static synchronized void pauseReplay() {
254+
performReplayAction(
255+
"pausing",
256+
(replay) -> {
257+
replay.pause();
258+
});
259+
}
260+
261+
private static void performReplayAction(
262+
final @NotNull String actionName, final @NotNull ReplayCallable action) {
263+
final @NotNull IHub hub = Sentry.getCurrentHub();
264+
if (isReplayAvailable) {
265+
final ReplayIntegration replay = ReplayIntegrationKt.getReplayIntegration(hub);
266+
if (replay != null) {
267+
action.call(replay);
268+
} else {
269+
hub.getOptions()
270+
.getLogger()
271+
.log(
272+
SentryLevel.INFO,
273+
"Session Replay wasn't registered yet, not " + actionName + " the replay");
274+
}
275+
} else {
276+
hub.getOptions()
277+
.getLogger()
278+
.log(
279+
SentryLevel.INFO,
280+
"Session Replay wasn't found on classpath, not " + actionName + " the replay");
281+
}
282+
}
283+
284+
private interface ReplayCallable {
285+
void call(final @NotNull ReplayIntegration replay);
286+
}
215287
}

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,30 @@ public final class io/sentry/android/replay/BuildConfig {
66
public fun <init> ()V
77
}
88

9-
public final class io/sentry/android/replay/WindowRecorder {
10-
public fun <init> ()V
11-
public final fun startRecording (Landroid/content/Context;)V
12-
public final fun stopRecording ()V
9+
public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integration, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
10+
public static final field Companion Lio/sentry/android/replay/ReplayIntegration$Companion;
11+
public static final field VIDEO_BUFFER_DURATION J
12+
public static final field VIDEO_SEGMENT_DURATION J
13+
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
14+
public fun close ()V
15+
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
16+
public final fun pause ()V
17+
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
18+
public final fun resume ()V
19+
public final fun start ()V
20+
public final fun stop ()V
21+
}
22+
23+
public final class io/sentry/android/replay/ReplayIntegration$Companion {
24+
}
25+
26+
public final class io/sentry/android/replay/ReplayIntegrationKt {
27+
public static final fun getReplayIntegration (Lio/sentry/IHub;)Lio/sentry/android/replay/ReplayIntegration;
28+
public static final fun gracefullyShutdown (Ljava/util/concurrent/ExecutorService;Lio/sentry/SentryOptions;)V
29+
}
30+
31+
public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallback {
32+
public abstract fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
1333
}
1434

1535
public abstract interface class io/sentry/android/replay/video/SimpleFrameMuxer {

sentry-android-replay/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ android {
1919
targetSdk = Config.Android.targetSdkVersion
2020
minSdk = Config.Android.minSdkVersionReplay
2121

22+
testInstrumentationRunner = Config.TestLibs.androidJUnitRunner
23+
2224
// for AGP 4.1
2325
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
2426
}
@@ -67,7 +69,9 @@ dependencies {
6769

6870
// tests
6971
testImplementation(projects.sentryTestSupport)
72+
testImplementation(Config.TestLibs.robolectric)
7073
testImplementation(Config.TestLibs.kotlinTestJunit)
74+
testImplementation(Config.TestLibs.androidxRunner)
7175
testImplementation(Config.TestLibs.androidxJunit)
7276
testImplementation(Config.TestLibs.mockitoKotlin)
7377
testImplementation(Config.TestLibs.mockitoInline)

0 commit comments

Comments
 (0)