Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Add `Session.State.Unhandled` for unhandled errors that do not terminate the process ([#5919](https://github.com/getsentry/sentry-java/pull/5919))

### Internal

- Add internal `UptimeClock` and `ElapsedRealtimeClock` abstractions with `Deadline` and `Stopwatch` primitives ([#6028](https://github.com/getsentry/sentry-java/pull/6028))

### Fixes

- Keep dropped tombstone and ANR events dropped, instead of reporting the same app exit again at every app start ([#6002](https://github.com/getsentry/sentry-java/pull/6002))
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader;
public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock;
public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;
public fun getNativeSdkName ()Ljava/lang/String;
public fun getNdkAppHangTimeoutIntervalMillis ()J
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SpanStatus;
import io.sentry.android.core.internal.time.AndroidElapsedRealtimeClock;
import io.sentry.android.core.internal.util.RootChecker;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.time.ElapsedRealtimeClock;
import io.sentry.util.SampleRateUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -864,6 +866,12 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) {
this.enableAnrFingerprinting = enableAnrFingerprinting;
}

@Override
@ApiStatus.Internal
public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() {
return AndroidElapsedRealtimeClock.getInstance();
}

static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler {
@Override
public void showForm(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.android.core.internal.time;

import android.os.SystemClock;
import io.sentry.time.ElapsedRealtimeClock;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* {@link ElapsedRealtimeClock} backed by {@link SystemClock#elapsedRealtimeNanos()}.
*
* <p>That is {@code CLOCK_BOOTTIME}, so it keeps counting while the device is suspended โ€” unlike
* {@link System#nanoTime()}, which the core module falls back to and which stops in deep sleep.
*/
@ApiStatus.Internal
public final class AndroidElapsedRealtimeClock implements ElapsedRealtimeClock {

private static final AndroidElapsedRealtimeClock instance = new AndroidElapsedRealtimeClock();

public static @NotNull ElapsedRealtimeClock getInstance() {
return instance;
}

private AndroidElapsedRealtimeClock() {}

@Override
public long tickNanos() {
return SystemClock.elapsedRealtimeNanos();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.sentry.time

import java.util.concurrent.TimeUnit

/**
* A [Ticker] that only moves when a test tells it to.
*
* Advancing by an amount *and a unit* is the point: a stubbed `thenReturn(1001)` against a
* nanosecond clock is off by a factor of a million and still compiles, whereas `advance(1001,
* MILLISECONDS)` cannot be.
*
* Implements both clock guarantees so a test can inject it wherever either is declared. Production
* code must never do this โ€” the whole purpose of the two interfaces is that one object cannot
* honestly promise both.
*/
class TestTicker(private var nanos: Long = 0) : UptimeClock, ElapsedRealtimeClock {
override fun tickNanos(): Long = nanos

fun advance(amount: Long, unit: TimeUnit) {
nanos += unit.toNanos(amount)
}
}
35 changes: 35 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3698,6 +3698,7 @@ public class io/sentry/SentryOptions {
public fun getDistributionController ()Lio/sentry/IDistributionApi;
public fun getDsn ()Ljava/lang/String;
public fun getEffectiveOrgId ()Ljava/lang/String;
public fun getElapsedRealtimeClock ()Lio/sentry/time/ElapsedRealtimeClock;
public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache;
public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader;
public fun getEnvironment ()Ljava/lang/String;
Expand Down Expand Up @@ -7582,6 +7583,40 @@ public final class io/sentry/rrweb/RRWebVideoEvent$JsonKeys {
public fun <init> ()V
}

public final class io/sentry/time/Deadline {
public static fun after (Lio/sentry/time/Ticker;JLjava/util/concurrent/TimeUnit;)Lio/sentry/time/Deadline;
public fun hasPassed ()Z
public fun isAfter (Lio/sentry/time/Deadline;)Z
public static fun passed (Lio/sentry/time/Ticker;)Lio/sentry/time/Deadline;
public fun remaining (Ljava/util/concurrent/TimeUnit;)J
}

public abstract interface class io/sentry/time/ElapsedRealtimeClock : io/sentry/time/Ticker {
}

public final class io/sentry/time/JavaElapsedRealtimeClock : io/sentry/time/ElapsedRealtimeClock {
public static fun getInstance ()Lio/sentry/time/ElapsedRealtimeClock;
public fun tickNanos ()J
}

public final class io/sentry/time/JavaUptimeClock : io/sentry/time/UptimeClock {
public static fun getInstance ()Lio/sentry/time/UptimeClock;
public fun tickNanos ()J
}

public final class io/sentry/time/Stopwatch {
public fun elapsed (Ljava/util/concurrent/TimeUnit;)J
public fun elapsedNanos ()J
public static fun started (Lio/sentry/time/Ticker;)Lio/sentry/time/Stopwatch;
}

public abstract interface class io/sentry/time/Ticker {
public abstract fun tickNanos ()J
}

public abstract interface class io/sentry/time/UptimeClock : io/sentry/time/Ticker {
}

public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ITransport {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/RequestDetails;)V
public fun <init> (Lio/sentry/transport/QueuedThreadPoolExecutor;Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/transport/HttpConnection;)V
Expand Down
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.sentry.metrics.IMetricsBatchProcessorFactory;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryTransaction;
import io.sentry.time.ElapsedRealtimeClock;
import io.sentry.time.JavaElapsedRealtimeClock;
import io.sentry.transport.ITransport;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.NoOpEnvelopeCache;
Expand Down Expand Up @@ -3059,6 +3061,18 @@ public void setDateProvider(final @NotNull SentryDateProvider dateProvider) {
this.dateProvider.setValue(dateProvider);
}

/**
* Returns the clock used to measure intervals that must include deep sleep, such as rate-limit
* windows and cache expiry.
*
* <p>Android overrides this with a {@code SystemClock.elapsedRealtimeNanos()}-backed clock, which
* this module cannot reference. On the JVM there is no suspend state to account for.
*/
@ApiStatus.Internal
public @NotNull ElapsedRealtimeClock getElapsedRealtimeClock() {
return JavaElapsedRealtimeClock.getInstance();
}

/**
* Adds a ICollector.
*
Expand Down
79 changes: 79 additions & 0 deletions sentry/src/main/java/io/sentry/time/Deadline.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.sentry.time;

import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* A point in the future, measured on a {@link Ticker}.
*
* <p>Exists so that callers never do arithmetic on raw ticks. A tick carries no unit and no epoch,
* so spelling out {@code now - then < ttl} at every call site is where unit mix-ups, sentinels that
* happen to mean "boot", and wrap-unsafe {@code <} comparisons come from. Each of those is decided
* once, here.
*/
@ApiStatus.Internal
public final class Deadline {

private final @NotNull Ticker clock;
private final long deadlineNanos;

private Deadline(final @NotNull Ticker clock, final long deadlineNanos) {
this.clock = clock;
this.deadlineNanos = deadlineNanos;
}

/** A deadline {@code amount} of {@code unit} from now. */
public static @NotNull Deadline after(
final @NotNull Ticker clock, final long amount, final @NotNull TimeUnit unit) {
return new Deadline(clock, clock.tickNanos() + unit.toNanos(amount));
}

/**
* A deadline that has already passed. Use for state that has not been populated yet, so that
* "never set" needs no numeric sentinel and cannot be mistaken for fresh โ€” {@code 0} is a real
* and very recent instant on any boot-relative clock.
*/
public static @NotNull Deadline passed(final @NotNull Ticker clock) {
return new Deadline(clock, clock.tickNanos());
}

public boolean hasPassed() {
// Subtraction rather than `<`: a tick origin is arbitrary, may be negative, and may wrap.
return clock.tickNanos() - deadlineNanos >= 0;
}

/**
* How much time is left, rounded up, or zero once the deadline has passed.
*
* <p>Rounding up matters: callers schedule work for {@code remaining()} and then re-check {@link
* #hasPassed()}. Truncating would wake them a fraction early, to find the deadline still
* standing.
*/
public long remaining(final @NotNull TimeUnit unit) {
final long remainingNanos = deadlineNanos - clock.tickNanos();
if (remainingNanos <= 0) {
return 0;
}
final long unitNanos = unit.toNanos(1);
final long whole = remainingNanos / unitNanos;
return remainingNanos % unitNanos == 0 ? whole : whole + 1;
}

/**
* Whether this deadline falls after {@code other}.
*
* @throws IllegalArgumentException if the two were created from different clocks, whose origins
* are unrelated and whose ticks are therefore not comparable.
*/
public boolean isAfter(final @NotNull Deadline other) {
if (clock != other.clock) {
throw new IllegalArgumentException(
"Cannot compare deadlines from different clocks: "
+ clock.getClass().getName()
+ " and "
+ other.clock.getClass().getName());
}
return deadlineNanos - other.deadlineNanos > 0;
}
}
16 changes: 16 additions & 0 deletions sentry/src/main/java/io/sentry/time/ElapsedRealtimeClock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;

/**
* A {@link Ticker} that <strong>includes</strong> time the device spent suspended in deep sleep.
*
* <p>This is the clock for anything expressed in real elapsed time regardless of what the device
* was doing โ€” a rate-limit window the server asked us to wait out, or a cache entry that should go
* stale on a wall-clock schedule.
*
* <p>On Android this is {@code CLOCK_BOOTTIME}, via {@code SystemClock.elapsedRealtimeNanos()}. On
* the JVM there is no comparable suspend state, so uptime and elapsed real time coincide.
*/
@ApiStatus.Internal
public interface ElapsedRealtimeClock extends Ticker {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* {@link ElapsedRealtimeClock} backed by {@link System#nanoTime()}.
*
* <p>Identical to {@link JavaUptimeClock} โ€” a JVM cannot observe deep sleep โ€” but kept a distinct
* type so that a call site declaring which guarantee it needs documents that intent on every
* platform.
*/
@ApiStatus.Internal
public final class JavaElapsedRealtimeClock implements ElapsedRealtimeClock {

private static final JavaElapsedRealtimeClock instance = new JavaElapsedRealtimeClock();

public static @NotNull ElapsedRealtimeClock getInstance() {
return instance;
}

private JavaElapsedRealtimeClock() {}

@Override
public long tickNanos() {
return System.nanoTime();
}
}
22 changes: 22 additions & 0 deletions sentry/src/main/java/io/sentry/time/JavaUptimeClock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/** {@link UptimeClock} backed by {@link System#nanoTime()}. */
@ApiStatus.Internal
public final class JavaUptimeClock implements UptimeClock {

private static final JavaUptimeClock instance = new JavaUptimeClock();

public static @NotNull UptimeClock getInstance() {
return instance;
}

private JavaUptimeClock() {}

@Override
public long tickNanos() {
return System.nanoTime();
}
}
35 changes: 35 additions & 0 deletions sentry/src/main/java/io/sentry/time/Stopwatch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.sentry.time;

import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* Measures how long something took, on a {@link Ticker}.
*
* <p>The counterpart to {@link Deadline}: it keeps the start tick and the unit conversion in one
* place, so call sites stop repeating {@code System.nanoTime() - startTime}.
*/
@ApiStatus.Internal
public final class Stopwatch {

private final @NotNull Ticker clock;
private final long startNanos;

private Stopwatch(final @NotNull Ticker clock) {
this.clock = clock;
this.startNanos = clock.tickNanos();
}

public static @NotNull Stopwatch started(final @NotNull Ticker clock) {
return new Stopwatch(clock);
}

public long elapsedNanos() {
return clock.tickNanos() - startNanos;
}

public long elapsed(final @NotNull TimeUnit unit) {
return unit.convert(elapsedNanos(), TimeUnit.NANOSECONDS);
}
}
20 changes: 20 additions & 0 deletions sentry/src/main/java/io/sentry/time/Ticker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;

/**
* A monotonically increasing nanosecond counter.
*
* <p>This type deliberately promises very little: a tick is a number that does not go backwards,
* measured from an origin that is arbitrary and may be negative. Only <em>differences</em> between
* two ticks from the same instance are meaningful, and a tick must never be persisted, serialized,
* or compared against a value from another clock.
*
* <p>Do not implement or depend on {@code Ticker} directly. It exists so that {@link Deadline} and
* {@link Stopwatch} can be written once; callers declare {@link UptimeClock} or {@link
* ElapsedRealtimeClock}, whose names state which guarantee they provide.
*/
@ApiStatus.Internal
public interface Ticker {
long tickNanos();
}
16 changes: 16 additions & 0 deletions sentry/src/main/java/io/sentry/time/UptimeClock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.sentry.time;

import org.jetbrains.annotations.ApiStatus;

/**
* A {@link Ticker} that <strong>excludes</strong> time the device spent suspended in deep sleep.
*
* <p>This is the clock for measuring how long the CPU was actually available โ€” most importantly ANR
* detection, where counting suspended time would report a responsive main thread as blocked.
*
* <p>On Android this is {@code CLOCK_MONOTONIC}, the same clock behind {@code
* SystemClock.uptimeMillis()}. On the JVM there is no comparable suspend state, so uptime and
* elapsed real time coincide.
*/
@ApiStatus.Internal
public interface UptimeClock extends Ticker {}
Loading
Loading