From 0d1d4c9f5279e391106a8e586a421cef026442bd Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 3 Sep 2026 17:29:03 +0200 Subject: [PATCH] ref(checkin): Measure check-in durations with Stopwatch (JAVA-576) All four check-in paths kept a `long startTime = System.nanoTime()` and subtracted it in a finally block: CheckInUtils and the SentryCheckInAdvice in sentry-spring, sentry-spring-jakarta and sentry-spring-7. The clock now comes from the options, so a check-in measures on whatever the SDK measures on: System.nanoTime() on the JVM, unchanged to the bit, and SystemClock.elapsedRealtimeNanos() on Android. A cron job that spans a suspend therefore reports the duration a user would measure rather than the time the CPU happened to be awake, which is what a check-in duration is meant to mean. An uninitialised SDK still reaches a clock: NoOpScopes.getOptions() hands back empty options, whose clock is the JVM one. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + .../java/io/sentry/spring7/checkin/SentryCheckInAdvice.java | 6 ++++-- .../sentry/spring/jakarta/checkin/SentryCheckInAdvice.java | 6 ++++-- .../java/io/sentry/spring/checkin/SentryCheckInAdvice.java | 6 ++++-- sentry/src/main/java/io/sentry/util/CheckInUtils.java | 6 ++++-- sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt | 1 + 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1348e68f114..84e85b5dac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Behavioral Changes - Measure HTTP rate-limit backoff on a monotonic clock instead of the wall clock, so that a device time change no longer lifts or extends an active rate limit ([#6030](https://github.com/getsentry/sentry-java/pull/6030)) +- Measure check-in durations on the monotonic clock, so a cron job that spans device sleep reports the time a user would measure instead of the time the CPU was awake ([#6032](https://github.com/getsentry/sentry-java/pull/6032)) ### Fixes diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java index d2c164b9a6e..d6730602de2 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java @@ -9,6 +9,7 @@ import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; +import io.sentry.time.Stopwatch; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import java.lang.reflect.Method; @@ -91,7 +92,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl TracingUtils.startNewTrace(scopes); @Nullable SentryId checkInId = null; - final long startTime = System.nanoTime(); + final @NotNull Stopwatch stopwatch = + Stopwatch.started(scopes.getOptions().getMonotonicClock()); boolean didError = false; try { @@ -105,7 +107,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl } finally { final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); - checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime)); + checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos())); scopes.captureCheckIn(checkIn); } } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java index fa64ac0e3e4..8507f1d42f6 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -9,6 +9,7 @@ import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; +import io.sentry.time.Stopwatch; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import java.lang.reflect.Method; @@ -91,7 +92,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl TracingUtils.startNewTrace(scopes); @Nullable SentryId checkInId = null; - final long startTime = System.nanoTime(); + final @NotNull Stopwatch stopwatch = + Stopwatch.started(scopes.getOptions().getMonotonicClock()); boolean didError = false; try { @@ -105,7 +107,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl } finally { final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); - checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime)); + checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos())); scopes.captureCheckIn(checkIn); } } diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java index a96e9e29808..ebbc55d40f0 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java @@ -9,6 +9,7 @@ import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; +import io.sentry.time.Stopwatch; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import java.lang.reflect.Method; @@ -94,7 +95,8 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl TracingUtils.startNewTrace(scopes); @Nullable SentryId checkInId = null; - final long startTime = System.nanoTime(); + final @NotNull Stopwatch stopwatch = + Stopwatch.started(scopes.getOptions().getMonotonicClock()); boolean didError = false; try { @@ -108,7 +110,7 @@ public Object invoke(final @NotNull MethodInvocation invocation) throws Throwabl } finally { final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); - checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime)); + checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos())); scopes.captureCheckIn(checkIn); } } diff --git a/sentry/src/main/java/io/sentry/util/CheckInUtils.java b/sentry/src/main/java/io/sentry/util/CheckInUtils.java index 3deea093142..67a1c89ab76 100644 --- a/sentry/src/main/java/io/sentry/util/CheckInUtils.java +++ b/sentry/src/main/java/io/sentry/util/CheckInUtils.java @@ -9,6 +9,7 @@ import io.sentry.MonitorConfig; import io.sentry.Sentry; import io.sentry.protocol.SentryId; +import io.sentry.time.Stopwatch; import java.util.List; import java.util.concurrent.Callable; import org.jetbrains.annotations.ApiStatus; @@ -37,7 +38,8 @@ public static U withCheckIn( try (final @NotNull ISentryLifecycleToken ignored = Sentry.forkedScopes("CheckInUtils").makeCurrent()) { final @NotNull IScopes scopes = Sentry.getCurrentScopes(); - final long startTime = System.nanoTime(); + final @NotNull Stopwatch stopwatch = + Stopwatch.started(scopes.getOptions().getMonotonicClock()); boolean didError = false; TracingUtils.startNewTrace(scopes); @@ -61,7 +63,7 @@ public static U withCheckIn( if (environment != null) { checkIn.setEnvironment(environment); } - checkIn.setDuration(DateUtils.nanosToSeconds(System.nanoTime() - startTime)); + checkIn.setDuration(DateUtils.nanosToSeconds(stopwatch.elapsedNanos())); scopes.captureCheckIn(checkIn); } } diff --git a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt index d4831c829e4..03a3e1bf69e 100644 --- a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt @@ -143,6 +143,7 @@ class CheckInUtilsTest { sentry.`when` { Sentry.forkedScopes(any()) }.then { scopes.forkedScopes("test") } whenever(scopes.forkedScopes(any())).thenReturn(scopes) whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + whenever(scopes.options).thenReturn(SentryOptions()) try { CheckInUtils.withCheckIn("monitor-1") { throw RuntimeException("thrown on purpose") }