Skip to content

Commit 864c8cb

Browse files
runningcodeclaude
andcommitted
docs(time): Steer new measurements at the io.sentry.time clock APIs
Document on each singleton clock's getInstance() that callers should read the ticker or wall clock off the options object instead, so they get the per-platform implementation rather than pinning themselves to one. Calling JavaMonotonicTicker.getInstance() on Android is the trap worth naming: it stops counting through a device suspend, so intervals spanning a deep sleep come out short. Add an AGENTS.md section so new measurement code reaches for io.sentry.time rather than DateUtils.getCurrentDateTime() or a SentryDateProvider, picks the abstraction by what the number is for, and treats adding a new abstraction as a deliberate step rather than a way around an awkward call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5c1467e commit 864c8cb

4 files changed

Lines changed: 64 additions & 0 deletions

File tree

‎AGENTS.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,43 @@ Apply that pattern only where a broad catch is genuinely unavoidable — an entr
155155
arbitrary user code or third-party callbacks. Everywhere else, name the exception types. Say in the
156156
PR description why the broad catch is necessary.
157157

158+
### Measuring Time
159+
160+
**All new code that reads a clock uses `io.sentry.time`.** Do not reach for
161+
`DateUtils.getCurrentDateTime()`, `System.currentTimeMillis()`, `System.nanoTime()`,
162+
`SystemClock`, or any `SentryDateProvider` implementation (`SentryAutoDateProvider`,
163+
`SentryNanotimeDateProvider`, `SentryInstantDateProvider`, `SentryAndroidDateProvider`). Those
164+
remain only because the `SentryDate` protocol types still flow through the event pipeline; they
165+
are legacy, not a precedent to follow.
166+
167+
Pick the abstraction by what the number is *for*, not by what is convenient to call:
168+
169+
| You need | Use | Notes |
170+
|---|---|---|
171+
| An instant that leaves the process (event, breadcrumb, session) | `EpochClock` → `Timestamp` | Serialize it. Never subtract two of them |
172+
| How long something took | `Stopwatch` | |
173+
| Whether a window has elapsed (TTL, cache expiry, backoff, timeout) | `Deadline` | Also gives you `remaining(unit)` for scheduling |
174+
| 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 |
175+
| A raw monotonic tick, when none of the above fits | `MonotonicTicker` | |
176+
177+
Two rules on top of that:
178+
179+
1. **Get the clock from the options object** — `options.getEpochClock()` and
180+
`options.getMonotonicTicker()` — not from `SystemEpochClock.getInstance()`,
181+
`JavaMonotonicTicker.getInstance()`, or `AndroidMonotonicTicker.getInstance()`. The accessor is
182+
what resolves to the correct per-platform implementation: on Android the ticker is
183+
`CLOCK_BOOTTIME`, which keeps counting through a device suspend, while `System.nanoTime()` does
184+
not. Take the clock as a constructor parameter and keep it in a field; `sentry-test-support`
185+
provides `TestMonotonicTicker` so tests advance time instead of sleeping.
186+
2. **If no abstraction fits, stop and think hard before adding one.** Adding a type here is a
187+
deliberate act, not a shortcut around an awkward call site. First re-read the existing types —
188+
most "missing" cases turn out to be a `Deadline` or a `Stopwatch` described in different words.
189+
If one is genuinely missing, work out what invariant it exists to enforce (each of these types
190+
exists to make one class of clock bug unrepresentable — negative durations, unit mix-ups,
191+
wrap-unsafe comparisons, a tick escaping into serialized output), name that invariant in its
192+
Javadoc, and propose it before writing call sites against it. Do not inline raw tick arithmetic
193+
at a call site as a stopgap.
194+
158195
### Testing Requirements
159196
- Write comprehensive unit tests for new features
160197
- Android modules require both unit tests and instrumented tests where applicable

‎sentry-android-core/src/main/java/io/sentry/android/core/internal/time/AndroidMonotonicTicker.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ public final class AndroidMonotonicTicker implements MonotonicTicker {
1616

1717
private static final AndroidMonotonicTicker instance = new AndroidMonotonicTicker();
1818

19+
/**
20+
* Prefer {@link io.sentry.SentryOptions#getMonotonicTicker()} over this: on Android it already
21+
* returns this ticker, and it keeps the call site compiling on the JVM too.
22+
*
23+
* <p>Call this directly only where there is no options object to ask — the {@code
24+
* SentryAndroidOptions} override that supplies it, or a test that means this implementation
25+
* specifically.
26+
*/
1927
public static @NotNull MonotonicTicker getInstance() {
2028
return instance;
2129
}

‎sentry/src/main/java/io/sentry/time/JavaMonotonicTicker.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ public final class JavaMonotonicTicker implements MonotonicTicker {
99

1010
private static final JavaMonotonicTicker instance = new JavaMonotonicTicker();
1111

12+
/**
13+
* Prefer {@link io.sentry.SentryOptions#getMonotonicTicker()} over this: it resolves to the
14+
* ticker that is right for the platform the SDK is running on.
15+
*
16+
* <p>Android overrides that accessor with a {@code CLOCK_BOOTTIME}-backed ticker. This one stops
17+
* counting while the device is suspended, so measuring against it there silently under-reports
18+
* every interval that spans a deep sleep.
19+
*
20+
* <p>Call this directly only where there is no options object to ask — the default that {@code
21+
* SentryOptions} itself returns, or a test that means this implementation specifically.
22+
*/
1223
public static @NotNull MonotonicTicker getInstance() {
1324
return instance;
1425
}

‎sentry/src/main/java/io/sentry/time/SystemEpochClock.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public final class SystemEpochClock implements EpochClock {
2424

2525
private static final SystemEpochClock instance = new SystemEpochClock();
2626

27+
/**
28+
* Prefer {@link io.sentry.SentryOptions#getEpochClock()} over this: that accessor is where a
29+
* platform-specific wall clock would be substituted, the way {@code SentryAndroidOptions} already
30+
* substitutes the ticker. Reading it keeps a call site from being pinned to this implementation.
31+
*
32+
* <p>Call this directly only where there is no options object to ask — the default that {@code
33+
* SentryOptions} itself returns, or a test that means this implementation specifically.
34+
*/
2735
public static @NotNull EpochClock getInstance() {
2836
return instance;
2937
}

0 commit comments

Comments
 (0)