Skip to content
Draft
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
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,43 @@ Apply that pattern only where a broad catch is genuinely unavoidable โ€” an entr
arbitrary user code or third-party callbacks. Everywhere else, name the exception types. Say in the
PR description why the broad catch is necessary.

### Measuring Time

**All new code that reads a clock uses `io.sentry.time`.** Do not reach for
`DateUtils.getCurrentDateTime()`, `System.currentTimeMillis()`, `System.nanoTime()`,
`SystemClock`, or any `SentryDateProvider` implementation (`SentryAutoDateProvider`,
`SentryNanotimeDateProvider`, `SentryInstantDateProvider`, `SentryAndroidDateProvider`). Those
remain only because the `SentryDate` protocol types still flow through the event pipeline; they
are legacy, not a precedent to follow.

Pick the abstraction by what the number is *for*, not by what is convenient to call:

| You need | Use | Notes |
|---|---|---|
| An instant that leaves the process (event, breadcrumb, session) | `EpochClock` โ†’ `Timestamp` | Serialize it. Never subtract two of them |
| How long something took | `Stopwatch` | |
| Whether a window has elapsed (TTL, cache expiry, backoff, timeout) | `Deadline` | Also gives you `remaining(unit)` for scheduling |
| Several instants that will be compared with each other (spans of a transaction, samples of a chunk) | `AnchoredClock` | One wall-clock read, the rest projected off ticks, so gaps are real elapsed time |
| A raw monotonic tick, when none of the above fits | `MonotonicTicker` | |

Two rules on top of that:

1. **Get the clock from the options object** โ€” `options.getEpochClock()` and
`options.getMonotonicTicker()` โ€” not from `SystemEpochClock.getInstance()`,
`JavaMonotonicTicker.getInstance()`, or `AndroidMonotonicTicker.getInstance()`. The accessor is
what resolves to the correct per-platform implementation: on Android the ticker is
`CLOCK_BOOTTIME`, which keeps counting through a device suspend, while `System.nanoTime()` does
not. Take the clock as a constructor parameter and keep it in a field; `sentry-test-support`
provides `TestMonotonicTicker` so tests advance time instead of sleeping.
2. **If no abstraction fits, stop and think hard before adding one.** Adding a type here is a
deliberate act, not a shortcut around an awkward call site. First re-read the existing types โ€”
most "missing" cases turn out to be a `Deadline` or a `Stopwatch` described in different words.
If one is genuinely missing, work out what invariant it exists to enforce (each of these types
exists to make one class of clock bug unrepresentable โ€” negative durations, unit mix-ups,
wrap-unsafe comparisons, a tick escaping into serialized output), name that invariant in its
Javadoc, and propose it before writing call sites against it. Do not inline raw tick arithmetic
at a call site as a stopgap.

### Testing Requirements
- Write comprehensive unit tests for new features
- Android modules require both unit tests and instrumented tests where applicable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public final class AndroidMonotonicTicker implements MonotonicTicker {

private static final AndroidMonotonicTicker instance = new AndroidMonotonicTicker();

/**
* Prefer {@link io.sentry.SentryOptions#getMonotonicTicker()} over this: on Android it already
* returns this ticker, and it keeps the call site compiling on the JVM too.
*
* <p>Call this directly only where there is no options object to ask โ€” the {@code
* SentryAndroidOptions} override that supplies it, or a test that means this implementation
* specifically.
*/
public static @NotNull MonotonicTicker getInstance() {
return instance;
}
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/time/JavaMonotonicTicker.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ public final class JavaMonotonicTicker implements MonotonicTicker {

private static final JavaMonotonicTicker instance = new JavaMonotonicTicker();

/**
* Prefer {@link io.sentry.SentryOptions#getMonotonicTicker()} over this: it resolves to the
* ticker that is right for the platform the SDK is running on.
*
* <p>Android overrides that accessor with a {@code CLOCK_BOOTTIME}-backed ticker. This one stops
* counting while the device is suspended, so measuring against it there silently under-reports
* every interval that spans a deep sleep.
*
* <p>Call this directly only where there is no options object to ask โ€” the default that {@code
* SentryOptions} itself returns, or a test that means this implementation specifically.
*/
public static @NotNull MonotonicTicker getInstance() {
return instance;
}
Expand Down
8 changes: 8 additions & 0 deletions sentry/src/main/java/io/sentry/time/SystemEpochClock.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public final class SystemEpochClock implements EpochClock {

private static final SystemEpochClock instance = new SystemEpochClock();

/**
* Prefer {@link io.sentry.SentryOptions#getEpochClock()} over this: that accessor is where a
* platform-specific wall clock would be substituted, the way {@code SentryAndroidOptions} already
* substitutes the ticker. Reading it keeps a call site from being pinned to this implementation.
*
* <p>Call this directly only where there is no options object to ask โ€” the default that {@code
* SentryOptions} itself returns, or a test that means this implementation specifically.
*/
public static @NotNull EpochClock getInstance() {
return instance;
}
Expand Down
Loading