From 42762640b206d1ffd7eadbab52d73b8e6a96c21c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 Apr 2024 00:00:25 +0200 Subject: [PATCH 1/4] Add session deadline of 1h --- .../sentry/android/replay/ReplayIntegration.kt | 9 +++++++++ .../src/main/AndroidManifest.xml | 2 ++ sentry/api/sentry.api | 1 + .../main/java/io/sentry/SentryReplayOptions.java | 16 ++++++++++++---- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index a64353e09a..35eed08675 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -34,6 +34,7 @@ import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference import kotlin.LazyThreadSafetyMode.NONE @@ -54,6 +55,7 @@ class ReplayIntegration( private val isRecording = AtomicBoolean(false) private val currentReplayId = AtomicReference(SentryId.EMPTY_ID) private val segmentTimestamp = AtomicReference() + private val replayStartTimestamp = AtomicLong() private val currentSegment = AtomicInteger(0) // TODO: surround with try-catch on the calling site @@ -136,6 +138,7 @@ class ReplayIntegration( recorder?.startRecording() // TODO: replace it with dateProvider.currentTimeMillis to also test it segmentTimestamp.set(DateUtils.getCurrentDateTime()) + replayStartTimestamp.set(dateProvider.currentTimeMillis) // TODO: finalize old recording if there's some left on disk and send it using the replayId from persisted scope (e.g. for ANRs) } @@ -243,6 +246,7 @@ class ReplayIntegration( recorder?.stopRecording() cache?.close() currentSegment.set(0) + replayStartTimestamp.set(0) segmentTimestamp.set(null) currentReplayId.set(SentryId.EMPTY_ID) hub?.configureScope { it.replayId = SentryId.EMPTY_ID } @@ -276,6 +280,11 @@ class ReplayIntegration( // set next segment timestamp as close to the previous one as possible to avoid gaps segmentTimestamp.set(DateUtils.getDateTime(currentSegmentTimestamp.time + videoDuration)) } + } else if (isFullSession.get() && + (now - replayStartTimestamp.get() >= options.experimental.sessionReplayOptions.sessionDuration) + ) { + stop() + options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording") } else if (!isFullSession.get()) { cache?.rotate(now - options.experimental.sessionReplayOptions.errorReplayDuration) } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index e4fd0aefd9..23b469ff20 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -156,5 +156,7 @@ + + diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3e65667941..2c116ffb21 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2569,6 +2569,7 @@ public final class io/sentry/SentryReplayOptions { public fun getErrorReplayDuration ()J public fun getErrorSampleRate ()Ljava/lang/Double; public fun getFrameRate ()I + public fun getSessionDuration ()J public fun getSessionSampleRate ()Ljava/lang/Double; public fun getSessionSegmentDuration ()J public fun isSessionReplayEnabled ()Z diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index d702d6256b..51ed1a3a72 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -22,22 +22,25 @@ public final class SentryReplayOptions { /** * Defines the quality of the session replay. Higher bit rates have better replay quality, but - * also affect the final payload size to transfer. The default value is 20kbps; + * also affect the final payload size to transfer, defaults to 20kbps. */ private int bitRate = 20_000; /** * Number of frames per second of the replay. The bigger the number, the more accurate the replay - * will be, but also more data to transfer and more CPU load. + * will be, but also more data to transfer and more CPU load, defaults to 1fps. */ private int frameRate = 1; - /** The maximum duration of replays for error events. */ + /** The maximum duration of replays for error events, defaults to 30s. */ private long errorReplayDuration = 30_000L; - /** The maximum duration of the segment of a session replay. */ + /** The maximum duration of the segment of a session replay, defaults to 5s. */ private long sessionSegmentDuration = 5000L; + /** The maximum duration of a full session replay, defaults to 1h. */ + private long sessionDuration = 60 * 60 * 1000L; + public SentryReplayOptions() {} public SentryReplayOptions( @@ -103,4 +106,9 @@ public long getErrorReplayDuration() { public long getSessionSegmentDuration() { return sessionSegmentDuration; } + + @ApiStatus.Internal + public long getSessionDuration() { + return sessionDuration; + } } From 3fe5e0fb1989f19518b9a39962f359a75cd2b5f6 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 Apr 2024 00:17:23 +0200 Subject: [PATCH 2/4] Clean up older replays when starting a new one --- .../android/replay/ReplayIntegration.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 35eed08675..8c944506bf 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -59,7 +59,7 @@ class ReplayIntegration( private val currentSegment = AtomicInteger(0) // TODO: surround with try-catch on the calling site - private val saver by lazy { + private val replayExecutor by lazy { Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory()) } @@ -128,6 +128,18 @@ class ReplayIntegration( currentSegment.set(0) currentReplayId.set(SentryId()) + replayExecutor.submit { + // clean up old replays + options.cacheDirPath?.let { cacheDir -> + File(cacheDir).listFiles { dir, name -> + // TODO: also exclude persisted replay_id from scope when implementing ANRs + if (name.startsWith("replay_") && !name.contains(currentReplayId.get().toString())) { + FileUtils.deleteRecursively(File(dir, name)) + } + false + } + } + } if (isFullSession.get()) { // only set replayId on the scope if it's a full session, otherwise all events will be // tagged with the replay that might never be sent when we're recording in buffer mode @@ -182,7 +194,7 @@ class ReplayIntegration( } val segmentId = currentSegment.get() val replayId = currentReplayId.get() - saver.submit { + replayExecutor.submit { val videoDuration = createAndCaptureSegment(now - currentSegmentTimestamp.time, currentSegmentTimestamp, replayId, segmentId, BUFFER, hint) if (videoDuration != null) { @@ -215,7 +227,7 @@ class ReplayIntegration( val segmentId = currentSegment.get() val duration = now - currentSegmentTimestamp.time val replayId = currentReplayId.get() - saver.submit { + replayExecutor.submit { val videoDuration = createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId) if (videoDuration != null) { @@ -235,7 +247,7 @@ class ReplayIntegration( val duration = now - currentSegmentTimestamp.time val replayId = currentReplayId.get() val replayCacheDir = cache?.replayCacheDir - saver.submit { + replayExecutor.submit { // we don't flush the segment, but we still wanna clean up the folder for buffer mode if (isFullSession.get()) { createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId) @@ -257,7 +269,7 @@ class ReplayIntegration( // have to do it before submitting, otherwise if the queue is busy, the timestamp won't be // reflecting the exact time of when it was captured val frameTimestamp = dateProvider.currentTimeMillis - saver.submit { + replayExecutor.submit { cache?.addFrame(bitmap, frameTimestamp) val now = dateProvider.currentTimeMillis @@ -374,7 +386,7 @@ class ReplayIntegration( } stop() - saver.gracefullyShutdown(options) + replayExecutor.gracefullyShutdown(options) } private class ReplayExecutorServiceThreadFactory : ThreadFactory { From ca9f9d40a06ee955be641fc4435f59a2acc3fb20 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 Apr 2024 00:23:38 +0200 Subject: [PATCH 3/4] Remove unnecessary extension fun --- .../test/java/io/sentry/android/core/SentryAndroidTest.kt | 5 ++--- sentry-android-replay/api/sentry-android-replay.api | 5 ++--- sentry/api/sentry.api | 2 ++ sentry/src/main/java/io/sentry/NoOpReplayController.java | 5 +++++ sentry/src/main/java/io/sentry/ReplayController.java | 2 ++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index a4b213800f..f8f266b149 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -27,7 +27,6 @@ import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.replay.ReplayIntegration -import io.sentry.android.replay.getReplayIntegration import io.sentry.android.timber.SentryTimberIntegration import io.sentry.cache.IEnvelopeCache import io.sentry.cache.PersistingOptionsObserver @@ -319,7 +318,7 @@ class SentryAndroidTest { @Config(sdk = [26]) fun `init starts session replay if app is in foreground`() { initSentryWithForegroundImportance(true) { _ -> - assertTrue(Sentry.getCurrentHub().getReplayIntegration()!!.isRecording()) + assertTrue(Sentry.getCurrentHub().options.replayController.isRecording()) } } @@ -327,7 +326,7 @@ class SentryAndroidTest { @Config(sdk = [26]) fun `init does not start session replay if the app is in background`() { initSentryWithForegroundImportance(false) { _ -> - assertFalse(Sentry.getCurrentHub().getReplayIntegration()!!.isRecording()) + assertFalse(Sentry.getCurrentHub().options.replayController.isRecording()) } } diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 0a064b803f..3b320c91b7 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -33,7 +33,7 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable { public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable { public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V public fun close ()V - public final fun isRecording ()Z + public fun isRecording ()Z public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V public fun pause ()V public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V @@ -44,8 +44,7 @@ public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integr } public final class io/sentry/android/replay/ReplayIntegrationKt { - public static final fun getReplayIntegration (Lio/sentry/IHub;)Lio/sentry/android/replay/ReplayIntegration; - public static final fun gracefullyShutdown (Ljava/util/concurrent/ExecutorService;Lio/sentry/SentryOptions;)V + public static final fun submitSafely (Ljava/util/concurrent/ExecutorService;Lio/sentry/ILogger;Ljava/lang/Runnable;)Ljava/util/concurrent/Future; } public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallback { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2c116ffb21..d3200dc76a 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1206,6 +1206,7 @@ public final class io/sentry/NoOpLogger : io/sentry/ILogger { public final class io/sentry/NoOpReplayController : io/sentry/ReplayController { public static fun getInstance ()Lio/sentry/NoOpReplayController; + public fun isRecording ()Z public fun pause ()V public fun resume ()V public fun sendReplayForEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)V @@ -1604,6 +1605,7 @@ public final class io/sentry/PropagationContext { } public abstract interface class io/sentry/ReplayController { + public abstract fun isRecording ()Z public abstract fun pause ()V public abstract fun resume ()V public abstract fun sendReplayForEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)V diff --git a/sentry/src/main/java/io/sentry/NoOpReplayController.java b/sentry/src/main/java/io/sentry/NoOpReplayController.java index d052fba8b4..1353e01a58 100644 --- a/sentry/src/main/java/io/sentry/NoOpReplayController.java +++ b/sentry/src/main/java/io/sentry/NoOpReplayController.java @@ -24,6 +24,11 @@ public void pause() {} @Override public void resume() {} + @Override + public boolean isRecording() { + return false; + } + @Override public void sendReplayForEvent(@NotNull SentryEvent event, @NotNull Hint hint) {} } diff --git a/sentry/src/main/java/io/sentry/ReplayController.java b/sentry/src/main/java/io/sentry/ReplayController.java index a45a0ecda2..d2b7f7eb16 100644 --- a/sentry/src/main/java/io/sentry/ReplayController.java +++ b/sentry/src/main/java/io/sentry/ReplayController.java @@ -13,5 +13,7 @@ public interface ReplayController { void resume(); + boolean isRecording(); + void sendReplayForEvent(@NotNull SentryEvent event, @NotNull Hint hint); } From ea417e426d48b10b21c85fc5e38bf5c0a21ca5ed Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 Apr 2024 12:12:18 +0200 Subject: [PATCH 4/4] Safe executors --- .../api/sentry-android-replay.api | 4 -- .../android/replay/ReplayIntegration.kt | 42 ++++-------- .../sentry/android/replay/WindowRecorder.kt | 24 ++++--- .../sentry/android/replay/util/Executors.kt | 67 +++++++++++++++++++ 4 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 3b320c91b7..cda49e0fd1 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -43,10 +43,6 @@ public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integr public fun stop ()V } -public final class io/sentry/android/replay/ReplayIntegrationKt { - public static final fun submitSafely (Ljava/util/concurrent/ExecutorService;Lio/sentry/ILogger;Ljava/lang/Runnable;)Ljava/util/concurrent/Future; -} - public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallback { public abstract fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 8c944506bf..3079cb47fd 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -18,6 +18,8 @@ import io.sentry.SentryReplayEvent import io.sentry.SentryReplayEvent.ReplayType import io.sentry.SentryReplayEvent.ReplayType.BUFFER import io.sentry.SentryReplayEvent.ReplayType.SESSION +import io.sentry.android.replay.util.gracefullyShutdown +import io.sentry.android.replay.util.submitSafely import io.sentry.protocol.SentryId import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent @@ -28,10 +30,8 @@ import java.io.Closeable import java.io.File import java.security.SecureRandom import java.util.Date -import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory -import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -43,6 +43,10 @@ class ReplayIntegration( private val dateProvider: ICurrentDateProvider ) : Integration, Closeable, ScreenshotRecorderCallback, ReplayController { + internal companion object { + private const val TAG = "ReplayIntegration" + } + private lateinit var options: SentryOptions private var hub: IHub? = null private var recorder: WindowRecorder? = null @@ -110,7 +114,7 @@ class ReplayIntegration( .addPackage("maven:io.sentry:sentry-android-replay", BuildConfig.VERSION_NAME) } - fun isRecording() = isRecording.get() + override fun isRecording() = isRecording.get() override fun start() { // TODO: add lifecycle state instead and manage it in start/pause/resume/stop @@ -128,7 +132,7 @@ class ReplayIntegration( currentSegment.set(0) currentReplayId.set(SentryId()) - replayExecutor.submit { + replayExecutor.submitSafely(options, "$TAG.replays_cleanup") { // clean up old replays options.cacheDirPath?.let { cacheDir -> File(cacheDir).listFiles { dir, name -> @@ -194,7 +198,7 @@ class ReplayIntegration( } val segmentId = currentSegment.get() val replayId = currentReplayId.get() - replayExecutor.submit { + replayExecutor.submitSafely(options, "$TAG.send_replay_for_event") { val videoDuration = createAndCaptureSegment(now - currentSegmentTimestamp.time, currentSegmentTimestamp, replayId, segmentId, BUFFER, hint) if (videoDuration != null) { @@ -227,7 +231,7 @@ class ReplayIntegration( val segmentId = currentSegment.get() val duration = now - currentSegmentTimestamp.time val replayId = currentReplayId.get() - replayExecutor.submit { + replayExecutor.submitSafely(options, "$TAG.pause") { val videoDuration = createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId) if (videoDuration != null) { @@ -247,7 +251,7 @@ class ReplayIntegration( val duration = now - currentSegmentTimestamp.time val replayId = currentReplayId.get() val replayCacheDir = cache?.replayCacheDir - replayExecutor.submit { + replayExecutor.submitSafely(options, "$TAG.stop") { // we don't flush the segment, but we still wanna clean up the folder for buffer mode if (isFullSession.get()) { createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId) @@ -269,7 +273,7 @@ class ReplayIntegration( // have to do it before submitting, otherwise if the queue is busy, the timestamp won't be // reflecting the exact time of when it was captured val frameTimestamp = dateProvider.currentTimeMillis - replayExecutor.submit { + replayExecutor.submitSafely(options, "$TAG.add_frame") { cache?.addFrame(bitmap, frameTimestamp) val now = dateProvider.currentTimeMillis @@ -398,25 +402,3 @@ class ReplayIntegration( } } } - -/** - * Retrieves the [ReplayIntegration] from the list of integrations in [SentryOptions] - */ -fun IHub.getReplayIntegration(): ReplayIntegration? = - options.integrations.find { it is ReplayIntegration } as? ReplayIntegration - -fun ExecutorService.gracefullyShutdown(options: SentryOptions) { - synchronized(this) { - if (!isShutdown) { - shutdown() - } - try { - if (!awaitTermination(options.shutdownTimeoutMillis, MILLISECONDS)) { - shutdownNow() - } - } catch (e: InterruptedException) { - shutdownNow() - Thread.currentThread().interrupt() - } - } -} diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt index d23222368f..743b5f5d89 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt @@ -2,8 +2,9 @@ package io.sentry.android.replay import android.annotation.TargetApi import android.view.View -import io.sentry.SentryLevel.ERROR import io.sentry.SentryOptions +import io.sentry.android.replay.util.gracefullyShutdown +import io.sentry.android.replay.util.scheduleAtFixedRateSafely import java.io.Closeable import java.lang.ref.WeakReference import java.util.concurrent.Executors @@ -20,6 +21,10 @@ internal class WindowRecorder( private val screenshotRecorderCallback: ScreenshotRecorderCallback ) : Closeable { + internal companion object { + private const val TAG = "WindowRecorder" + } + private val rootViewsSpy by lazy(NONE) { RootViewsSpy.install() } @@ -57,14 +62,15 @@ internal class WindowRecorder( recorder = ScreenshotRecorder(recorderConfig, options, screenshotRecorderCallback) rootViewsSpy.listeners += onRootViewsChangedListener - capturingTask = capturer.scheduleAtFixedRate({ - try { - recorder?.capture() - } catch (e: Throwable) { - options.logger.log(ERROR, "Failed to capture a screenshot with exception:", e) - // TODO: I guess schedule the capturer again, cause it will stop executing the runnable? - } - }, 0L, 1000L / recorderConfig.frameRate, MILLISECONDS) + capturingTask = capturer.scheduleAtFixedRateSafely( + options, + "$TAG.capture", + 0L, + 1000L / recorderConfig.frameRate, + MILLISECONDS + ) { + recorder?.capture() + } } fun resume() = recorder?.resume() diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt new file mode 100644 index 0000000000..093416f9bb --- /dev/null +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt @@ -0,0 +1,67 @@ +package io.sentry.android.replay.util + +import io.sentry.SentryLevel.ERROR +import io.sentry.SentryOptions +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.MILLISECONDS + +internal fun ExecutorService.gracefullyShutdown(options: SentryOptions) { + synchronized(this) { + if (!isShutdown) { + shutdown() + } + try { + if (!awaitTermination(options.shutdownTimeoutMillis, MILLISECONDS)) { + shutdownNow() + } + } catch (e: InterruptedException) { + shutdownNow() + Thread.currentThread().interrupt() + } + } +} + +internal fun ExecutorService.submitSafely( + options: SentryOptions, + taskName: String, + task: Runnable +): Future<*>? { + return try { + submit { + try { + task.run() + } catch (e: Throwable) { + options.logger.log(ERROR, "Failed to execute task $taskName", e) + } + } + } catch (e: Throwable) { + options.logger.log(ERROR, "Failed to submit task $taskName to executor", e) + null + } +} + +internal fun ScheduledExecutorService.scheduleAtFixedRateSafely( + options: SentryOptions, + taskName: String, + initialDelay: Long, + period: Long, + unit: TimeUnit, + task: Runnable +): ScheduledFuture<*>? { + return try { + scheduleAtFixedRate({ + try { + task.run() + } catch (e: Throwable) { + options.logger.log(ERROR, "Failed to execute task $taskName", e) + } + }, initialDelay, period, unit) + } catch (e: Throwable) { + options.logger.log(ERROR, "Failed to submit task $taskName to executor", e) + null + } +}