diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java index 074a4a6ea51..4ca850ea5f5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java @@ -7,11 +7,11 @@ import io.sentry.ITransaction; import io.sentry.NoOpSpan; import io.sentry.NoOpTransaction; -import io.sentry.SentryDate; -import io.sentry.SentryNanotimeDate; import io.sentry.SpanDataConvention; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.MeasurementValue; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import io.sentry.util.AutoClosableReentrantLock; import java.util.Iterator; import java.util.SortedSet; @@ -32,7 +32,6 @@ public class SpanFrameMetricsCollector // grow indefinitely in case of a long running span private static final int MAX_FRAMES_COUNT = 3600; private static final long ONE_SECOND_NANOS = TimeUnit.SECONDS.toNanos(1); - private static final SentryNanotimeDate EMPTY_NANO_TIME = new SentryNanotimeDate(0, 0); private final boolean enabled; protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); @@ -47,7 +46,8 @@ public class SpanFrameMetricsCollector if (o1 == o2) { return 0; } - int timeDiff = o1.getStartDate().compareTo(o2.getStartDate()); + int timeDiff = + Long.compare(o1.startTimestamp().epochNanos(), o2.startTimestamp().epochNanos()); if (timeDiff != 0) { return timeDiff; } @@ -126,7 +126,7 @@ public void onSpanFinished(final @NotNull ISpan span) { } else { // otherwise only remove old/irrelevant frames final @NotNull ISpan oldestSpan = runningSpans.first(); - frames.headSet(new Frame(toNanoTime(oldestSpan.getStartDate()))).clear(); + frames.headSet(new Frame(toNanoTime(oldestSpan, oldestSpan.startTimestamp()))).clear(); } } } @@ -139,13 +139,13 @@ private void captureFrameMetrics(@NotNull final ISpan span) { return; } - final @Nullable SentryDate spanFinishDate = span.getFinishDate(); - if (spanFinishDate == null) { + final @Nullable Timestamp spanFinish = span.endTimestamp(); + if (spanFinish == null) { return; } - final long spanStartNanos = toNanoTime(span.getStartDate()); - final long spanEndNanos = toNanoTime(spanFinishDate); + final long spanStartNanos = toNanoTime(span, span.startTimestamp()); + final long spanEndNanos = toNanoTime(span, spanFinish); final long spanDurationNanos = spanEndNanos - spanStartNanos; if (spanDurationNanos <= 0) { return; @@ -306,23 +306,38 @@ private static int addPendingFrameDelay( } /** - * Because {@link SentryNanotimeDate#nanoTimestamp()} only gives you millisecond precision, but - * diff does ¯\_(ツ)_/¯ + * Places a span instant on the frame timeline, which {@link android.view.Choreographer} reports + * in the {@link System#nanoTime()} timebase. * - * @param date the input date - * @return a non-unix timestamp in nano precision, similar to {@link System#nanoTime()}. + *

An anchored span's instant inverts to the exact tick it was projected from. That tick is on + * {@link io.sentry.time.MonotonicClock} — {@code CLOCK_BOOTTIME} on Android — while frames are on + * {@code CLOCK_MONOTONIC}, so crossing between them costs one offset read. The offset changes + * whenever the device suspends, so a span that spanned deep sleep lands wrong by the sleep; there + * are no frames during sleep, so the honest fix is to skip such a span rather than to guess. That + * is the follow-up this method's caller wants, not a reason to keep projecting from the wall + * clock. + * + * @return a non-unix timestamp in nano precision, in the {@link System#nanoTime()} timebase. */ - private static long toNanoTime(final @NotNull SentryDate date) { - // SentryNanotimeDate nanotime is based on System.nanotime(), like EMPTY_NANO_TIME, - // thus diff will simply return the System.nanotime() value of date - if (date instanceof SentryNanotimeDate) { - return date.diff(EMPTY_NANO_TIME); + private static long toNanoTime(final @NotNull ISpan span, final @NotNull Timestamp timestamp) { + final @Nullable AnchoredClock anchor = span.anchor(); + if (anchor != null) { + // TODO [MAJOR] Cross into the frame timebase before comparing. + // The tick is on io.sentry.time.MonotonicClock, which is CLOCK_BOOTTIME on Android, while + // Choreographer reports frames on CLOCK_MONOTONIC; the two differ by however long the device + // has been suspended. The offset is System.nanoTime() - SystemClock.elapsedRealtimeNanos(), + // but reading it from statics here is untestable and it changes on every suspend, so it needs + // an injectable seam and a decision about what to do with a span that spanned deep sleep — + // there are no frames during sleep, so skipping such a span is more honest than shifting it. + // Left unbridged deliberately: this demonstrates the span/anchor integration, not the + // timebase fix. + return anchor.tickOf(timestamp); } - // e.g. SentryLongDate is unix time based - upscaled to nanos, - // we need to project it back to System.nanotime() format - long nowUnixInNanos = DateUtils.millisToNanos(System.currentTimeMillis()); - long shiftInNanos = nowUnixInNanos - date.nanoTimestamp(); + // A stated instant — an OTel span, an app-start projection, a SQLite driver span — carries no + // tick at all, so there is nothing to invert and the wall clock is all we have. + final long nowUnixInNanos = DateUtils.millisToNanos(System.currentTimeMillis()); + final long shiftInNanos = nowUnixInNanos - timestamp.epochNanos(); return System.nanoTime() - shiftInNanos; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index c79d418efe5..840a97316b9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -1489,7 +1489,7 @@ class ActivityLifecycleIntegrationTest { uiLoadTransaction.children.single { it.operation == ActivityLifecycleIntegration.TTID_OP } assertTrue(ttidSpan.isFinished) assertTrue(appStartTransaction.isFinished) - assertEquals(ttidSpan.finishDate, appStartTransaction.finishDate) + assertEquals(ttidSpan.endTimestamp(), appStartTransaction.endTimestamp()) assertEquals( ttidSpan.measurements[MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY]!!.value, AppStartMetrics.getInstance().appStartTimeSpan.durationMs, @@ -2077,7 +2077,7 @@ class ActivityLifecycleIntegrationTest { runFirstDraw(view) assertTrue(ttidSpan.isFinished) assertTrue(ttfdSpan.isFinished) - assertEquals(ttfdSpan.finishDate, ttidSpan.finishDate) + assertEquals(ttfdSpan.endTimestamp(), ttidSpan.endTimestamp()) sut.onActivityDestroyed(activity) @@ -2131,8 +2131,8 @@ class ActivityLifecycleIntegrationTest { assertNotNull(ttidSpan) assertNotNull(ttfdSpan) - assertEquals(ttidSpan.startDate, fixture.transaction.startDate) - assertEquals(ttfdSpan.startDate, fixture.transaction.startDate) + assertEquals(ttidSpan.startTimestamp(), fixture.transaction.startTimestamp()) + assertEquals(ttfdSpan.startTimestamp(), fixture.transaction.startTimestamp()) } @Test @@ -2168,7 +2168,7 @@ class ActivityLifecycleIntegrationTest { // the ttfd span should be trimmed to be equal to the ttid span, and the description should end // with "-exceeded" assertEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) - assertEquals(ttidSpan.finishDate, ttfdSpan.finishDate) + assertEquals(ttidSpan.endTimestamp(), ttfdSpan.endTimestamp()) assertEquals(ttfdSpan.description, "Activity full display - Deadline Exceeded") } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt index 2b6f19a8d31..14bae4bbe49 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.core +import io.sentry.DateUtils import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.NoOpSpan @@ -8,6 +9,10 @@ import io.sentry.SentryNanotimeDate import io.sentry.SpanContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector import io.sentry.protocol.MeasurementValue +import io.sentry.time.AnchoredClock +import io.sentry.time.EpochClock +import io.sentry.time.MonotonicClock +import io.sentry.time.Timestamp import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.test.Test @@ -49,16 +54,7 @@ class SpanFrameMetricsCollectorTest { val span = mock() val spanContext = SpanContext("op.fake") whenever(span.spanContext).thenReturn(spanContext) - whenever(span.startDate) - .thenReturn(SentryNanotimeDate(System.currentTimeMillis(), startTimeStampNanos)) - whenever(span.finishDate) - .thenReturn( - if (endTimeStampNanos != null) { - SentryNanotimeDate(System.currentTimeMillis(), endTimeStampNanos) - } else { - null - } - ) + stubAnchoredTimes(span, startTimeStampNanos, endTimeStampNanos) return span } @@ -69,19 +65,39 @@ class SpanFrameMetricsCollectorTest { val span = mock() val spanContext = SpanContext("op.fake") whenever(span.spanContext).thenReturn(spanContext) - whenever(span.startDate) - .thenReturn(SentryNanotimeDate(System.currentTimeMillis(), startTimeStampNanos)) - whenever(span.finishDate) - .thenReturn( - if (endTimeStampNanos != null) { - SentryNanotimeDate(System.currentTimeMillis(), endTimeStampNanos) - } else { - null - } - ) + stubAnchoredTimes(span, startTimeStampNanos, endTimeStampNanos) return span } + /** + * Gives a mocked span the timing shape a real one now has: two instants projected from one + * anchor, whose ticks are the values this test feeds the frame collector. + */ + private fun stubAnchoredTimes(span: ISpan, startTick: Long, endTick: Long?) { + val clock = FakeClock(startTick) + val epoch = EpochClock { + Timestamp.ofEpochNanos(DateUtils.millisToNanos(System.currentTimeMillis())) + } + val anchor = AnchoredClock.create(epoch, clock) + val start = anchor.start() + val end = endTick?.let { + clock.setNanos(it) + anchor.now() + } + + whenever(span.anchor()).thenReturn(anchor) + whenever(span.startTimestamp()).thenReturn(start) + whenever(span.endTimestamp()).thenReturn(end) + } + + private class FakeClock(private var nanos: Long) : MonotonicClock { + override fun tickNanos(): Long = nanos + + fun setNanos(value: Long) { + nanos = value + } + } + private val fixture = Fixture() @Test diff --git a/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt b/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt index b3c0eb7c713..9637d9dc9a5 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt @@ -4,21 +4,13 @@ import io.sentry.IScopes import io.sentry.ISpan import io.sentry.Instrumenter import io.sentry.ScopesAdapter -import io.sentry.SentryDate import io.sentry.SentryLongDate -import io.sentry.SentryNanotimeDate import io.sentry.SentryStackTraceFactory import io.sentry.SpanDataConvention import io.sentry.SpanStatus private const val SQLITE_TRACE_ORIGIN = "auto.db.sqlite" -/** - * Sentinel for extracting a [SentryNanotimeDate]'s underlying [System.nanoTime] value via - * [SentryDate.diff]. - */ -private val EMPTY_NANO_TIME = SentryNanotimeDate(0, 0L) - /** Span instrumentation for [SentrySQLiteDriver]. */ internal class DriverSpans(private val scopes: IScopes, private val dbMetadata: DbMetadata) { @@ -26,7 +18,7 @@ internal class DriverSpans(private val scopes: IScopes, private val dbMetadata: /** * Returns a timestamp in nanoseconds for use with [record]. Timestamp is ns-precise if the active - * parent span uses a [SentryNanotimeDate] (the ordinary case); otherwise it's ms-precise. + * parent span is anchored (the ordinary case); otherwise it's ms-precise. * * Note: Internalizing the start time in [record] would shift spans to end-of-work on the trace * timeline, which is less desirable; callers capture the start before doing database work and @@ -104,14 +96,9 @@ internal class DriverSpans(private val scopes: IScopes, private val dbMetadata: * END TRANSACTION ├███┤ 0.33 ms * ``` */ -internal fun ISpan.computeNanoStartTimestampForChild(): Long? { - if (startDate !is SentryNanotimeDate) { - return null - } - - val parentWallClockNanos = startDate.nanoTimestamp() - val parentMonotonicNanos = startDate.diff(EMPTY_NANO_TIME) - val elapsedSinceParentStart = System.nanoTime() - parentMonotonicNanos - // Return the child's absolute start time. - return parentWallClockNanos + elapsedSinceParentStart -} +internal fun ISpan.computeNanoStartTimestampForChild(): Long? = + // An anchored span projects nanosecond instants from the transaction's single wall-clock + // reading, so "now" on its own timeline is exactly where a child span should start. No + // reconstruction, and no silent drop to millisecond precision when the parent's date happens + // not to be anchored. + anchor()?.now()?.epochNanos() diff --git a/sentry-android-sqlite/src/test/java/io/sentry/sqlite/ComputeNanoStartTimestampForChildTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/sqlite/ComputeNanoStartTimestampForChildTest.kt index 13ae1389b77..5122f4f49fa 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/sqlite/ComputeNanoStartTimestampForChildTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/sqlite/ComputeNanoStartTimestampForChildTest.kt @@ -2,8 +2,11 @@ package io.sentry.sqlite import io.sentry.DateUtils import io.sentry.ISpan -import io.sentry.SentryLongDate -import io.sentry.SentryNanotimeDate +import io.sentry.time.AnchoredClock +import io.sentry.time.EpochClock +import io.sentry.time.MonotonicClock +import io.sentry.time.Timestamp +import java.util.concurrent.TimeUnit.MILLISECONDS import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -14,86 +17,72 @@ import org.mockito.kotlin.whenever class ComputeNanoStartTimestampForChildTest { @Test - fun `returns parent wall clock plus elapsed monotonic time since parent started`() { - val wallClockMillis = 1_000_000L - val elapsedNanos = 500_000L - val parentMonotonicNanos = System.nanoTime() - elapsedNanos - val span = spanWithNanotimeStart(wallClockMillis, parentMonotonicNanos) + fun `returns the parent's own timeline projected to now`() { + val clock = FakeClock() + val span = anchoredSpan(WALL_CLOCK_MILLIS, clock) - val timestamp = span.computeNanoStartTimestampForChild()!! + clock.advanceNanos(500_000L) - val elapsedSinceParentStart = timestamp - DateUtils.millisToNanos(wallClockMillis) - assertTrue(elapsedSinceParentStart >= elapsedNanos) - assertTrue(elapsedSinceParentStart < elapsedNanos + TEST_SLACK_NANOS) + assertEquals( + DateUtils.millisToNanos(WALL_CLOCK_MILLIS) + 500_000L, + span.computeNanoStartTimestampForChild(), + ) } @Test - fun `same millisecond wall clocks with different monotonic offsets produce distinct ordered timestamps`() { - val wallClockMillis = 1_000_000L - val wallClockNanos = DateUtils.millisToNanos(wallClockMillis) - val earlierParentMonotonicNanos = System.nanoTime() - 200_000L - val laterParentMonotonicNanos = System.nanoTime() - 800_000L - val earlierSpan = spanWithNanotimeStart(wallClockMillis, earlierParentMonotonicNanos) - val laterSpan = spanWithNanotimeStart(wallClockMillis, laterParentMonotonicNanos) + fun `returns the parent's start when no time has elapsed since it started`() { + val span = anchoredSpan(WALL_CLOCK_MILLIS, FakeClock()) assertEquals( - earlierSpan.startDate.nanoTimestamp(), - laterSpan.startDate.nanoTimestamp(), - "Raw parent timestamps share the same ms-quantized value", + DateUtils.millisToNanos(WALL_CLOCK_MILLIS), + span.computeNanoStartTimestampForChild(), ) - - val earlier = earlierSpan.computeNanoStartTimestampForChild()!! - val later = laterSpan.computeNanoStartTimestampForChild()!! - - assertTrue(earlier > wallClockNanos) - assertTrue(later > wallClockNanos) - assertTrue(earlier < later) - assertTrue(later - earlier >= 500_000L) - } - - @Test - fun `returns parent wall clock when no monotonic time has elapsed since parent started`() { - val wallClockMillis = 1_000_000L - val parentMonotonicNanos = System.nanoTime() - val span = spanWithNanotimeStart(wallClockMillis, parentMonotonicNanos) - - val elapsedSinceParentStart = - span.computeNanoStartTimestampForChild()!! - DateUtils.millisToNanos(wallClockMillis) - assertTrue(elapsedSinceParentStart >= 0L) - assertTrue(elapsedSinceParentStart < TEST_SLACK_NANOS) } @Test - fun `works when parent wall clock differs from millisecond baseline`() { - val wallClockMillis = 1_000_001L - val elapsedNanos = 1_500_000L - val parentMonotonicNanos = System.nanoTime() - elapsedNanos - val span = spanWithNanotimeStart(wallClockMillis, parentMonotonicNanos) - - val elapsedSinceParentStart = - span.computeNanoStartTimestampForChild()!! - DateUtils.millisToNanos(wallClockMillis) - assertTrue(elapsedSinceParentStart >= elapsedNanos) - assertTrue(elapsedSinceParentStart < elapsedNanos + TEST_SLACK_NANOS) + fun `keeps nanosecond resolution even though the wall anchor is millisecond-quantized`() { + val clock = FakeClock() + val span = anchoredSpan(WALL_CLOCK_MILLIS, clock) + val wallClockNanos = DateUtils.millisToNanos(WALL_CLOCK_MILLIS) + + clock.advanceNanos(200_000L) + val earlier = span.computeNanoStartTimestampForChild()!! + clock.advanceNanos(600_000L) + val later = span.computeNanoStartTimestampForChild()!! + + // Both fall inside the same wall-clock millisecond, yet stay distinct and ordered — the + // resolution comes off the monotonic clock, not off the anchor. + assertTrue(earlier > wallClockNanos) + assertTrue(later - earlier == 600_000L) + assertTrue(later - wallClockNanos < MILLISECONDS.toNanos(1)) } @Test - fun `returns null when start date is not SentryNanotimeDate`() { + fun `returns null when the parent span is not anchored`() { val span = mock() - whenever(span.startDate).thenReturn(SentryLongDate(DateUtils.millisToNanos(1_000_000L))) + whenever(span.anchor()).thenReturn(null) assertNull(span.computeNanoStartTimestampForChild()) } - private fun spanWithNanotimeStart(wallClockMillis: Long, parentMonotonicNanos: Long): ISpan { - val startDate = SentryNanotimeDate(wallClockMillis, parentMonotonicNanos) + private fun anchoredSpan(wallClockMillis: Long, clock: FakeClock): ISpan { + val epoch = EpochClock { Timestamp.ofEpochNanos(DateUtils.millisToNanos(wallClockMillis)) } val span = mock() - whenever(span.startDate).thenReturn(startDate) + whenever(span.anchor()).thenReturn(AnchoredClock.create(epoch, clock)) return span } - companion object { + private class FakeClock : MonotonicClock { + private var nanos = 0L + + override fun tickNanos(): Long = nanos - // Upper bound for monotonic drift while the test body runs. - private const val TEST_SLACK_NANOS = 50_000_000L + fun advanceNanos(amount: Long) { + nanos += amount + } + } + + companion object { + private const val WALL_CLOCK_MILLIS = 1_000_000L } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 1f81e4324d4..a25855bce7a 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -44,6 +44,8 @@ public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFact public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { public fun (Lio/opentelemetry/api/trace/Span;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -89,6 +91,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; @@ -99,6 +102,8 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public final class io/sentry/opentelemetry/OtelTransactionSpanForwarder : io/sentry/ITransaction { public fun (Lio/sentry/opentelemetry/IOtelSpanWrapper;)V public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -144,6 +149,7 @@ public final class io/sentry/opentelemetry/OtelTransactionSpanForwarder : io/sen public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java index 907d71a278b..f33378cfb20 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java @@ -20,6 +20,8 @@ import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; @@ -321,4 +323,19 @@ public void setContext(@Nullable String key, @Nullable Object context) { public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { delegate.addFeatureFlag(flag, result); } + + @Override + public @NotNull Timestamp startTimestamp() { + return delegate.startTimestamp(); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return delegate.endTimestamp(); + } + + @Override + public @Nullable AnchoredClock anchor() { + return delegate.anchor(); + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java index e3cdfc4be3b..7a2657d26af 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java @@ -19,6 +19,8 @@ import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.List; @@ -314,4 +316,19 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource nameSou public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { rootSpan.addFeatureFlag(flag, result); } + + @Override + public @NotNull Timestamp startTimestamp() { + return rootSpan.startTimestamp(); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return rootSpan.endTimestamp(); + } + + @Override + public @Nullable AnchoredClock anchor() { + return rootSpan.anchor(); + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api index 3ed25d1a9cf..137ffa00dbf 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -58,6 +58,8 @@ public final class io/sentry/opentelemetry/OtelSpanUtils { public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { public fun (Lio/opentelemetry/sdk/trace/ReadWriteSpan;Lio/sentry/IScopes;Lio/sentry/SentryDate;Lio/sentry/TracesSamplingDecision;Lio/sentry/opentelemetry/IOtelSpanWrapper;Lio/sentry/SpanId;Lio/sentry/Baggage;)V public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -103,6 +105,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java index 80da51f9db7..f5d28034cee 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -28,6 +28,8 @@ import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.lang.ref.WeakReference; @@ -554,4 +556,27 @@ public void close() { otelScope.close(); } } + + /** + * OTel hands us epoch nanos at both ends and no tick, so these instants are stated rather than + * projected and {@link #anchor()} is null. A duration taken across them is a wall-clock + * difference — the one case the anchored design cannot improve, because the input carries nothing + * else. + */ + @Override + public @NotNull Timestamp startTimestamp() { + return Timestamp.ofEpochNanos(startTimestamp.nanoTimestamp()); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return finishedTimestamp == null + ? null + : Timestamp.ofEpochNanos(finishedTimestamp.nanoTimestamp()); + } + + @Override + public @Nullable AnchoredClock anchor() { + return null; + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8193d61651d..33ffa89e8d8 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1177,6 +1177,8 @@ public abstract interface class io/sentry/ISocketTagger { public abstract interface class io/sentry/ISpan { public abstract fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public abstract fun anchor ()Lio/sentry/time/AnchoredClock; + public abstract fun endTimestamp ()Lio/sentry/time/Timestamp; public abstract fun finish ()V public abstract fun finish (Lio/sentry/SpanStatus;)V public abstract fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -1211,6 +1213,7 @@ public abstract interface class io/sentry/ISpan { public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public abstract fun startTimestamp ()Lio/sentry/time/Timestamp; public abstract fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public abstract fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public abstract fun traceContext ()Lio/sentry/TraceContext; @@ -1927,6 +1930,8 @@ public final class io/sentry/NoOpSocketTagger : io/sentry/ISocketTagger { public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -1962,6 +1967,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1976,6 +1982,8 @@ public final class io/sentry/NoOpSpanFactory : io/sentry/ISpanFactory { public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -2022,6 +2030,7 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -4243,6 +4252,8 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun (Lio/sentry/TransactionContext;Lio/sentry/IScopes;)V public fun (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;)V public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -4292,6 +4303,7 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -4388,6 +4400,8 @@ public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration, ja public final class io/sentry/Span : io/sentry/ISpan { public fun (Lio/sentry/TransactionContext;Lio/sentry/SentryTracer;Lio/sentry/IScopes;Lio/sentry/SpanOptions;)V public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun anchor ()Lio/sentry/time/AnchoredClock; + public fun endTimestamp ()Lio/sentry/time/Timestamp; public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -4429,6 +4443,7 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startTimestamp ()Lio/sentry/time/Timestamp; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -7641,6 +7656,7 @@ public final class io/sentry/time/SystemEpochClock : io/sentry/time/EpochClock { } public final class io/sentry/time/Timestamp { + public fun anchor ()Lio/sentry/time/AnchoredClock; public fun epochNanos ()J public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index 9c55cdc3201..ab3910ac2eb 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -1,6 +1,8 @@ package io.sentry; import io.sentry.protocol.Contexts; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -279,6 +281,33 @@ default ISpan startChild( @Nullable SentryDate getFinishDate(); + /** + * When this span started, as an instant projected from {@link #anchor()}. + * + *

Replaces {@link #getStartDate()}. The difference that matters is not the type: two instants + * from one anchor are images of the same tick origin, so subtracting them reports measured time, + * whereas two {@link SentryDate}s may or may not, depending on the runtime class of each. + */ + @ApiStatus.Internal + @NotNull + Timestamp startTimestamp(); + + /** When this span ended, or null while it is still running. */ + @ApiStatus.Internal + @Nullable + Timestamp endTimestamp(); + + /** + * The clock this span's instants were projected from, or null when they were stated from outside + * the process — an OTel span, or a caller-supplied {@code startTimestamp}. + * + *

Exposed so that in-process consumers can recover the tick a span was measured at, via {@link + * AnchoredClock#tickOf}, instead of reverse-engineering it out of a {@link SentryNanotimeDate}. + */ + @ApiStatus.Internal + @Nullable + AnchoredClock anchor(); + /** * Whether this span instance is a NOOP that doesn't collect information * diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 676c539942d..a72285ce3ae 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -2,6 +2,8 @@ import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -200,4 +202,19 @@ public void setContext(@Nullable String key, @Nullable Object context) {} @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} + + @Override + public @NotNull Timestamp startTimestamp() { + return Timestamp.ofEpochNanos(0); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return null; + } + + @Override + public @Nullable AnchoredClock anchor() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index 1f34870c9ad..9526f74cb00 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -3,6 +3,8 @@ import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.ApiStatus; @@ -255,4 +257,19 @@ public boolean isNoOp() { @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} + + @Override + public @NotNull Timestamp startTimestamp() { + return Timestamp.ofEpochNanos(0); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return null; + } + + @Override + public @Nullable AnchoredClock anchor() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index d60187cb4d5..6ec64b016f9 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -5,6 +5,8 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.TransactionNameSource; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; @@ -25,6 +27,14 @@ @ApiStatus.Internal public final class SentryTracer implements ITransaction { private final @NotNull SentryId eventId = new SentryId(); + + /** + * The one wall-clock reading this transaction makes. Declared before {@link #root} so that the + * root span, constructed inside this class's constructor, already sees a usable anchor — Java + * initialises fields in declaration order. + */ + private final @NotNull AnchoredClock anchor; + private final @NotNull Span root; private final @NotNull List children = new CopyOnWriteArrayList<>(); private final @NotNull IScopes scopes; @@ -74,6 +84,9 @@ public SentryTracer( Objects.requireNonNull(context, "context is required"); Objects.requireNonNull(scopes, "scopes are required"); + this.anchor = + AnchoredClock.create( + scopes.getOptions().getEpochClock(), scopes.getOptions().getMonotonicClock()); this.root = new Span(context, this, scopes, transactionOptions); this.name = context.getName(); @@ -160,7 +173,7 @@ private void onDeadlineTimeoutReached() { return; } - final @NotNull SentryDate finishTimestamp = scopes.getOptions().getDateProvider().now(); + final @NotNull Timestamp finishTimestamp = anchor.now(); // abort all child-spans first, this ensures the transaction can be finished, // even if waitForChildren is true @@ -182,8 +195,20 @@ public void finish( @Nullable SentryDate finishDate, boolean dropIfNoChildren, @Nullable Hint hint) { + finish( + status, + finishDate == null ? null : Timestamp.ofEpochNanos(finishDate.nanoTimestamp()), + dropIfNoChildren, + hint); + } + + void finish( + @Nullable SpanStatus status, + @Nullable Timestamp finishDate, + boolean dropIfNoChildren, + @Nullable Hint hint) { // try to get the high precision timestamp from the root span - SentryDate finishTimestamp = root.getFinishDate(); + @Nullable Timestamp finishTimestamp = root.endTimestamp(); // if a finishDate was passed in, use that instead if (finishDate != null) { @@ -192,7 +217,7 @@ public void finish( // if it's not set -> fallback to the current time if (finishTimestamp == null) { - finishTimestamp = scopes.getOptions().getDateProvider().now(); + finishTimestamp = anchor.now(); } // auto-finish any idle spans first @@ -344,11 +369,31 @@ private void cancelDeadlineTimer() { return children; } + @NotNull + AnchoredClock getAnchor() { + return anchor; + } + @Override public @NotNull SentryDate getStartDate() { return this.root.getStartDate(); } + @Override + public @NotNull Timestamp startTimestamp() { + return this.root.startTimestamp(); + } + + @Override + public @Nullable Timestamp endTimestamp() { + return this.root.endTimestamp(); + } + + @Override + public @Nullable AnchoredClock anchor() { + return this.root.anchor(); + } + @Override public @Nullable SentryDate getFinishDate() { return this.root.getFinishDate(); @@ -557,7 +602,7 @@ private void setDefaultSpanData(final @NotNull ISpan span) { * that no profile was recorded, e.g. when the OS rate limits profiling requests. Spans that no * profile covers drop the reference here, so they don't point to a profile that never arrives. */ - private void dropUnrecordedProfilerIds(final @NotNull SentryDate finishTimestamp) { + private void dropUnrecordedProfilerIds(final @NotNull Timestamp finishTimestamp) { final @NotNull IContinuousProfiler continuousProfiler = scopes.getOptions().getContinuousProfiler(); if (continuousProfiler instanceof NoOpContinuousProfiler) { @@ -591,7 +636,7 @@ private void dropUnrecordedProfilerIds(final @NotNull SentryDate finishTimestamp private boolean isProfileMissing( final @NotNull IContinuousProfiler continuousProfiler, final @NotNull Span span, - final @NotNull SentryDate finishTimestamp) { + final @NotNull Timestamp finishTimestamp) { final @Nullable Object data = span.getData(SpanDataConvention.PROFILER_ID); if (!(data instanceof String)) { return false; @@ -604,10 +649,13 @@ private boolean isProfileMissing( return false; } final @Nullable SentryDate spanFinishDate = span.getFinishDate(); + // IContinuousProfiler is public API and still speaks SentryDate; convert at the boundary return continuousProfiler.getProfileRecordingState( profilerId, span.getStartDate(), - spanFinishDate != null ? spanFinishDate : finishTimestamp) + spanFinishDate != null + ? spanFinishDate + : new SentryLongDate(finishTimestamp.epochNanos())) == ProfileRecordingState.NOT_RECORDED; } diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 7ee7eed1900..a62b3950093 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -3,6 +3,8 @@ import io.sentry.protocol.Contexts; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; +import io.sentry.time.AnchoredClock; +import io.sentry.time.Timestamp; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.Iterator; @@ -17,11 +19,16 @@ @ApiStatus.Internal public final class Span implements ISpan { - /** The moment in time when span was started. */ - private @NotNull SentryDate startTimestamp; + /** + * When the span started and ended, projected from the transaction's {@link AnchoredClock} — or + * stated directly, when a caller supplied them. + * + *

One anchor per transaction is what makes {@code end - start} a tick difference rather than + * the difference of two independent wall-clock readings. + */ + private @NotNull Timestamp start; - /** The moment in time when span has ended. */ - private @Nullable SentryDate timestamp; + private @Nullable Timestamp end; private final @NotNull SpanContext context; @@ -61,12 +68,7 @@ public final class Span implements ISpan { this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = options; this.spanFinishedCallback = spanFinishedCallback; - final @Nullable SentryDate startTimestamp = options.getStartTimestamp(); - if (startTimestamp != null) { - this.startTimestamp = startTimestamp; - } else { - this.startTimestamp = scopes.getOptions().getDateProvider().now(); - } + this.start = resolveStart(options, transaction); } public Span( @@ -79,23 +81,45 @@ public Span( this.transaction = Objects.requireNonNull(sentryTracer, "sentryTracer is required"); this.scopes = Objects.requireNonNull(scopes, "scopes are required"); this.spanFinishedCallback = null; - final @Nullable SentryDate startTimestamp = options.getStartTimestamp(); - if (startTimestamp != null) { - this.startTimestamp = startTimestamp; - } else { - this.startTimestamp = scopes.getOptions().getDateProvider().now(); - } + this.start = resolveStart(options, sentryTracer); this.options = options; } + private static @NotNull Timestamp resolveStart( + final @NotNull SpanOptions options, final @NotNull SentryTracer transaction) { + final @Nullable SentryDate stated = options.getStartTimestamp(); + return stated != null + ? Timestamp.ofEpochNanos(stated.nanoTimestamp()) + : transaction.getAnchor().now(); + } + @Override public @NotNull SentryDate getStartDate() { - return startTimestamp; + return new SentryLongDate(start.epochNanos()); } @Override public @Nullable SentryDate getFinishDate() { - return timestamp; + return end == null ? null : new SentryLongDate(end.epochNanos()); + } + + @Override + public @NotNull Timestamp startTimestamp() { + return start; + } + + @Override + public @Nullable Timestamp endTimestamp() { + return end; + } + + @Override + public @Nullable AnchoredClock anchor() { + // Both endpoints have to come from the anchor for a tick to mean anything; a stated one does + // not, and a projected end paired with a stated start would place the span wrongly. + final boolean anchored = + start.anchor() != null && (end == null || end.anchor() == start.anchor()); + return anchored ? start.anchor() : null; } @Override @@ -180,7 +204,7 @@ public void finish() { @Override public void finish(@Nullable SpanStatus status) { - finish(status, scopes.getOptions().getDateProvider().now()); + finish(status, (Timestamp) null); } /** @@ -191,16 +215,25 @@ public void finish(@Nullable SpanStatus status) { */ @Override public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate timestamp) { + finish(status, timestamp == null ? null : Timestamp.ofEpochNanos(timestamp.nanoTimestamp())); + } + + /** + * The anchored counterpart of {@link #finish(SpanStatus, SentryDate)}, used by {@link + * SentryTracer} so that a span it stamps keeps the transaction's anchor rather than being demoted + * to a stated instant. + */ + void finish(final @Nullable SpanStatus status, final @Nullable Timestamp end) { // the span can be finished only once if (finished || !isFinishing.compareAndSet(false, true)) { return; } this.context.setStatus(status); - this.timestamp = timestamp == null ? scopes.getOptions().getDateProvider().now() : timestamp; + this.end = end == null ? transaction.getAnchor().now() : end; if (options.isTrimStart() || options.isTrimEnd()) { - @Nullable SentryDate minChildStart = null; - @Nullable SentryDate maxChildEnd = null; + @Nullable Timestamp minChildStart = null; + @Nullable Timestamp maxChildEnd = null; // The root span should be trimmed based on all children, but the other spans, like the // jetpack composition should be trimmed based on its direct children only @@ -209,23 +242,25 @@ public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate ? transaction.getChildren() : getDirectChildren(); for (final Span child : children) { - if (minChildStart == null || child.getStartDate().isBefore(minChildStart)) { - minChildStart = child.getStartDate(); + final @NotNull Timestamp childStart = child.startTimestamp(); + if (minChildStart == null || childStart.epochNanos() < minChildStart.epochNanos()) { + minChildStart = childStart; } - if (maxChildEnd == null - || (child.getFinishDate() != null && child.getFinishDate().isAfter(maxChildEnd))) { - maxChildEnd = child.getFinishDate(); + final @Nullable Timestamp childEnd = child.endTimestamp(); + if (childEnd != null + && (maxChildEnd == null || childEnd.epochNanos() > maxChildEnd.epochNanos())) { + maxChildEnd = childEnd; } } if (options.isTrimStart() && minChildStart != null - && startTimestamp.isBefore(minChildStart)) { - updateStartDate(minChildStart); + && start.epochNanos() < minChildStart.epochNanos()) { + this.start = minChildStart; } if (options.isTrimEnd() && maxChildEnd != null - && (this.timestamp == null || this.timestamp.isAfter(maxChildEnd))) { - updateEndDate(maxChildEnd); + && this.end.epochNanos() > maxChildEnd.epochNanos()) { + this.end = maxChildEnd; } } @@ -406,8 +441,12 @@ public Map getMeasurements() { @Override public boolean updateEndDate(final @NotNull SentryDate date) { - if (this.timestamp != null) { - this.timestamp = date; + return updateEndDate(Timestamp.ofEpochNanos(date.nanoTimestamp())); + } + + boolean updateEndDate(final @NotNull Timestamp date) { + if (this.end != null) { + this.end = date; return true; } return false; @@ -437,10 +476,6 @@ SpanFinishedCallback getSpanFinishedCallback() { return spanFinishedCallback; } - private void updateStartDate(@NotNull SentryDate date) { - this.startTimestamp = date; - } - @NotNull SpanOptions getOptions() { return options; diff --git a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java index 58930ec1a87..059a48dad13 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java +++ b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java @@ -65,14 +65,15 @@ public SentrySpan(final @NotNull Span span, final @Nullable Map final Map measurementsCopy = CollectionUtils.newConcurrentHashMap(span.getMeasurements()); this.measurements = measurementsCopy != null ? measurementsCopy : new ConcurrentHashMap<>(); - // we lose precision here, from potential nanosecond precision down to 10 microsecond precision + // Both endpoints are projections of the transaction's one anchor, so the server subtracting + // them yields the time this span measured. No derivation needed: the end is not a second + // wall-clock reading, it is the anchor plus a tick difference. + // We lose precision here, from nanosecond precision down to 10 microsecond precision. this.timestamp = - span.getFinishDate() == null + span.endTimestamp() == null ? null - : DateUtils.nanosToSeconds( - span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate())); - // we lose precision here, from potential nanosecond precision down to 10 microsecond precision - this.startTimestamp = DateUtils.nanosToSeconds(span.getStartDate().nanoTimestamp()); + : DateUtils.nanosToSeconds(span.endTimestamp().epochNanos()); + this.startTimestamp = DateUtils.nanosToSeconds(span.startTimestamp().epochNanos()); this.data = data; final @NotNull IFeatureFlagBuffer featureFlagBuffer = span.getSpanContext().getFeatureFlagBuffer(); diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index ab6ff10a4d8..4d9639d9446 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -58,14 +58,13 @@ public final class SentryTransaction extends SentryBaseEvent public SentryTransaction(final @NotNull SentryTracer sentryTracer) { super(sentryTracer.getEventId()); Objects.requireNonNull(sentryTracer, "sentryTracer is required"); - // we lose precision here, from potential nanosecond precision down to 10 microsecond precision - this.startTimestamp = DateUtils.nanosToSeconds(sentryTracer.getStartDate().nanoTimestamp()); - // we lose precision here, from potential nanosecond precision down to 10 microsecond precision + // Anchored, like the child spans: see SentrySpan's constructor. + // We lose precision here, from nanosecond precision down to 10 microsecond precision. + this.startTimestamp = DateUtils.nanosToSeconds(sentryTracer.startTimestamp().epochNanos()); + final @Nullable io.sentry.time.Timestamp end = sentryTracer.endTimestamp(); this.timestamp = DateUtils.nanosToSeconds( - sentryTracer - .getStartDate() - .laterDateNanosTimestampByDiff(sentryTracer.getFinishDate())); + end != null ? end.epochNanos() : sentryTracer.startTimestamp().epochNanos()); this.transaction = sentryTracer.getName(); for (final Span span : sentryTracer.getChildren()) { if (Boolean.TRUE.equals(span.isSampled())) { diff --git a/sentry/src/main/java/io/sentry/time/Timestamp.java b/sentry/src/main/java/io/sentry/time/Timestamp.java index 0c476db7c8e..7d8c2dfc6e5 100644 --- a/sentry/src/main/java/io/sentry/time/Timestamp.java +++ b/sentry/src/main/java/io/sentry/time/Timestamp.java @@ -47,8 +47,7 @@ public long epochNanos() { } /** The clock that projected this instant, or null if it was read or stated directly. */ - @Nullable - AnchoredClock anchor() { + public @Nullable AnchoredClock anchor() { return anchor; } diff --git a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index d259100853c..8c9b6da77a7 100644 --- a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -190,7 +190,9 @@ class DefaultCompositePerformanceCollectorTest { SentryNanotimeDate(TimeUnit.SECONDS.toMillis(100), TimeUnit.SECONDS.toNanos(100)), SentryNanotimeDate(TimeUnit.SECONDS.toMillis(131), TimeUnit.SECONDS.toNanos(131)), ) - whenever(mockDateProvider.now()).thenReturn(dates[0], dates[0], dates[0], dates[1]) + // The first reading anchors the collector, every later one is a sampling tick. Stubbing an + // exact call sequence coupled this test to how often unrelated code read the provider. + whenever(mockDateProvider.now()).thenReturn(dates[0], dates[1]) val collector = fixture.getSut { it.dateProvider = mockDateProvider it.addPerformanceCollector(mockCollector) @@ -221,7 +223,9 @@ class DefaultCompositePerformanceCollectorTest { SentryNanotimeDate(TimeUnit.SECONDS.toMillis(100), TimeUnit.SECONDS.toNanos(100)), SentryNanotimeDate(TimeUnit.SECONDS.toMillis(130), TimeUnit.SECONDS.toNanos(130)), ) - whenever(mockDateProvider.now()).thenReturn(dates[0], dates[0], dates[0], dates[1]) + // The first reading anchors the collector, every later one is a sampling tick. Stubbing an + // exact call sequence coupled this test to how often unrelated code read the provider. + whenever(mockDateProvider.now()).thenReturn(dates[0], dates[1]) val collector = fixture.getSut { it.dateProvider = mockDateProvider } collector.start(fixture.transaction1) verify(fixture.mockTimer, never())!!.cancel() diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index eecacc95bd2..5a02767ae41 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -4,6 +4,8 @@ import io.sentry.cache.EnvelopeCache import io.sentry.hints.Retryable import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction +import io.sentry.time.JavaMonotonicClock +import io.sentry.time.SystemEpochClock import io.sentry.util.HintUtils import io.sentry.util.thread.NoOpThreadChecker import java.io.File @@ -38,6 +40,8 @@ class OutboxSenderTest { init { whenever(options.dsn).thenReturn("https://key@sentry.io/proj") whenever(options.dateProvider).thenReturn(SentryNanotimeDateProvider()) + whenever(options.epochClock).thenReturn(SystemEpochClock.getInstance()) + whenever(options.monotonicClock).thenReturn(JavaMonotonicClock.getInstance()) whenever(options.threadChecker).thenReturn(NoOpThreadChecker.getInstance()) whenever(options.continuousProfiler).thenReturn(NoOpContinuousProfiler.getInstance()) whenever(scopes.options).thenReturn(this.options) diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 7c6324db0a7..32ac3d8443c 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -25,7 +25,6 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.check -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy @@ -142,13 +141,13 @@ class SentryTracerTest { @Test fun `when transaction is created, startTimestamp is set`() { val tracer = fixture.getSut() - assertNotNull(tracer.startDate) + assertNotNull(tracer.startTimestamp()) } @Test fun `when transaction is created, timestamp is not set`() { val tracer = fixture.getSut() - assertNull(tracer.finishDate) + assertNull(tracer.endTimestamp()) } @Test @@ -173,14 +172,14 @@ class SentryTracerTest { fun `when transaction is finished, timestamp is set`() { val tracer = fixture.getSut() tracer.finish() - assertNotNull(tracer.finishDate) + assertNotNull(tracer.endTimestamp()) } @Test fun `when transaction is finished with status, timestamp and status are set`() { val tracer = fixture.getSut() tracer.finish(SpanStatus.ABORTED) - assertNotNull(tracer.finishDate) + assertNotNull(tracer.endTimestamp()) assertEquals(SpanStatus.ABORTED, tracer.status) } @@ -193,7 +192,7 @@ class SentryTracerTest { 0, ) tracer.finish(SpanStatus.ABORTED, date) - assertEquals(tracer.finishDate!!.nanoTimestamp(), date.nanoTimestamp()) + assertEquals(tracer.endTimestamp()!!.epochNanos(), date.nanoTimestamp()) assertEquals(SpanStatus.ABORTED, tracer.status) } @@ -357,7 +356,10 @@ class SentryTracerTest { coveredSpan.finish() whenever(continuousProfiler.getProfileRecordingState(any(), any(), any())).thenAnswer { invocation -> - if (invocation.getArgument(1) === uncoveredSpan.startDate) + if ( + invocation.getArgument(1).nanoTimestamp() == + uncoveredSpan.startTimestamp().epochNanos() + ) ProfileRecordingState.NOT_RECORDED else ProfileRecordingState.RECORDED } @@ -410,10 +412,15 @@ class SentryTracerTest { tracer.finish() + val startTimes = argumentCaptor() val endTimes = argumentCaptor() verify(continuousProfiler, atLeastOnce()) - .getProfileRecordingState(any(), eq(unfinishedSpan.startDate), endTimes.capture()) - assertThat(endTimes.lastValue.isAfter(unfinishedSpan.startDate)).isTrue() + .getProfileRecordingState(any(), startTimes.capture(), endTimes.capture()) + // SentryDate has no value equality, so match on the instant rather than with eq() + assertThat(startTimes.allValues.map { it.nanoTimestamp() }) + .contains(unfinishedSpan.startTimestamp().epochNanos()) + assertThat(endTimes.lastValue.nanoTimestamp()) + .isGreaterThan(unfinishedSpan.startTimestamp().epochNanos()) } @Test @@ -612,7 +619,7 @@ class SentryTracerTest { val span = tracer.startChild("op") as Span assertNotNull(span) assertNotNull(span.spanId) - assertNotNull(span.startDate) + assertNotNull(span.startTimestamp()) } @Test @@ -643,7 +650,7 @@ class SentryTracerTest { val span = tracer.startChild("op", "description") as Span assertNotNull(span) assertNotNull(span.spanId) - assertNotNull(span.startDate) + assertNotNull(span.startTimestamp()) assertEquals("op", span.operation) assertEquals("description", span.description) } @@ -677,10 +684,10 @@ class SentryTracerTest { val span = tracer.startChild("op", "description", sentryDate) as Span assertNotNull(span) assertNotNull(span.spanId) - assertNotNull(span.startDate) + assertNotNull(span.startTimestamp()) assertEquals("op", span.operation) assertEquals("description", span.description) - assertEquals(sentryDate, span.startDate) + assertEquals(sentryDate.nanoTimestamp(), span.startTimestamp().epochNanos()) } @Test @@ -735,7 +742,7 @@ class SentryTracerTest { transaction.throwable = ex transaction.finish(SpanStatus.OK) - val timestamp = transaction.finishDate + val timestamp = transaction.endTimestamp() transaction.finish(SpanStatus.UNKNOWN_ERROR) @@ -750,7 +757,7 @@ class SentryTracerTest { ) assertEquals(SpanStatus.OK, transaction.status) - assertEquals(timestamp, transaction.finishDate) + assertEquals(timestamp, transaction.endTimestamp()) } @Test @@ -808,14 +815,14 @@ class SentryTracerTest { val date = SentryNanotimeDate(0, 0) val transaction = fixture.getSut(startTimestamp = date) - assertSame(date, transaction.startDate) + assertEquals(date.nanoTimestamp(), transaction.startTimestamp().epochNanos()) } @Test fun `when startTimestamp is nullable, set it automatically`() { val transaction = fixture.getSut(startTimestamp = null) - assertNotNull(transaction.startDate) + assertNotNull(transaction.startTimestamp()) } @Test @@ -1227,7 +1234,7 @@ class SentryTracerTest { .captureTransaction( check { assertEquals(2, it.spans.size) - assertEquals(transaction.root.finishDate, span2.finishDate) + assertEquals(transaction.root.endTimestamp(), span2.endTimestamp()) }, anyOrNull(), anyOrNull(), @@ -1426,10 +1433,7 @@ class SentryTracerTest { .captureTransaction( check { assertEquals(1, it.spans.size) - assertEquals( - transaction.root.finishDate!!.nanoTimestamp(), - span.finishDate!!.nanoTimestamp(), - ) + assertEquals(transaction.root.endTimestamp(), span.endTimestamp()) }, anyOrNull(), anyOrNull(), @@ -1464,7 +1468,10 @@ class SentryTracerTest { .captureTransaction( check { assertEquals(1, it.spans.size) - assertEquals(transactionFinishDate, span.finishDate) + assertEquals( + transactionFinishDate.nanoTimestamp(), + span.endTimestamp()!!.epochNanos(), + ) }, anyOrNull(), anyOrNull(), @@ -1497,14 +1504,14 @@ class SentryTracerTest { parentSpan.finish() - val expectedParentStartDate = child1.startDate - val expectedParentEndDate = parentSpan.finishDate + val expectedParentStartDate = child1.startTimestamp() + val expectedParentEndDate = parentSpan.endTimestamp() transaction.finish() assertTrue(parentSpan.isFinished) - assertEquals(expectedParentStartDate, parentSpan.startDate) - assertEquals(expectedParentEndDate, parentSpan.finishDate) + assertEquals(expectedParentStartDate, parentSpan.startTimestamp()) + assertEquals(expectedParentEndDate, parentSpan.endTimestamp()) verify(fixture.scopes) .captureTransaction( @@ -1540,14 +1547,14 @@ class SentryTracerTest { parentSpan.finish() - val expectedParentStartDate = parentSpan.startDate - val expectedParentEndDate = child2.finishDate + val expectedParentStartDate = parentSpan.startTimestamp() + val expectedParentEndDate = child2.endTimestamp() transaction.finish() assertTrue(parentSpan.isFinished) - assertEquals(expectedParentStartDate, parentSpan.startDate) - assertEquals(expectedParentEndDate, parentSpan.finishDate) + assertEquals(expectedParentStartDate, parentSpan.startTimestamp()) + assertEquals(expectedParentEndDate, parentSpan.endTimestamp()) verify(fixture.scopes) .captureTransaction( @@ -1591,20 +1598,20 @@ class SentryTracerTest { fun `updateEndDate is ignored and returns false if span is not finished`() { val transaction = fixture.getSut() assertFalse(transaction.isFinished) - assertNull(transaction.finishDate) + assertNull(transaction.endTimestamp()) assertFalse(transaction.updateEndDate(mock())) - assertNull(transaction.finishDate) + assertNull(transaction.endTimestamp()) } @Test fun `updateEndDate updates finishDate and returns true if span is finished`() { val transaction = fixture.getSut() - val endDate: SentryDate = mock() + val endDate = SentryLongDate(1_700_000_000_000_000_000) transaction.finish() assertTrue(transaction.isFinished) - assertNotNull(transaction.finishDate) + assertNotNull(transaction.endTimestamp()) assertTrue(transaction.updateEndDate(endDate)) - assertEquals(endDate, transaction.finishDate) + assertEquals(endDate.nanoTimestamp(), transaction.endTimestamp()!!.epochNanos()) } @Test @@ -1637,19 +1644,19 @@ class SentryTracerTest { // and one span is finished but not the other, and the transaction is force-finished span0.finish(SpanStatus.OK) - val span0FinishDate = span0.finishDate + val span0FinishDate = span0.endTimestamp() transaction.forceFinish(SpanStatus.ABORTED, false, null) // then the first span should keep it's status assertTrue(span0.isFinished) assertEquals(SpanStatus.OK, span0.status) - assertEquals(span0FinishDate, span0.finishDate) + assertEquals(span0FinishDate, span0.endTimestamp()) // and the second span should have the same status as the transaction assertTrue(span1.isFinished) assertEquals(SpanStatus.ABORTED, span1.status) - assertEquals(transaction.finishDate, span1.finishDate) + assertEquals(transaction.endTimestamp(), span1.endTimestamp()) // and the transaction should be captured with both spans verify(fixture.scopes) @@ -1768,11 +1775,11 @@ class SentryTracerTest { fixture.getSut( transactionFinishedCallback = { assertFalse(it.isFinished) - assertNotNull(it.finishDate) + assertNotNull(it.endTimestamp()) } ) assertFalse(transaction.isFinished) - assertNull(transaction.finishDate) + assertNull(transaction.endTimestamp()) transaction.finish() } diff --git a/sentry/src/test/java/io/sentry/SpanTest.kt b/sentry/src/test/java/io/sentry/SpanTest.kt index b61e12ad87f..0799ff98939 100644 --- a/sentry/src/test/java/io/sentry/SpanTest.kt +++ b/sentry/src/test/java/io/sentry/SpanTest.kt @@ -55,7 +55,7 @@ class SpanTest { val span = fixture.getSut() span.finish() - assertNotNull(span.finishDate) + assertNotNull(span.endTimestamp()) } @Test @@ -63,7 +63,7 @@ class SpanTest { val span = fixture.getSut() span.finish(SpanStatus.CANCELLED) - assertNotNull(span.finishDate) + assertNotNull(span.endTimestamp()) assertEquals(SpanStatus.CANCELLED, span.status) } @@ -105,7 +105,7 @@ class SpanTest { assertThat(child.parentSpanId).isEqualTo(parent.spanContext.spanId) assertThat(child.operation).isEqualTo("child-op") assertThat(child.description).isEqualTo("description") - assertThat(child.startDate).isSameInstanceAs(timestamp) + assertThat(child.startTimestamp().epochNanos()).isEqualTo(timestamp.nanoTimestamp()) } @Test @@ -237,14 +237,14 @@ class SpanTest { span.throwable = ex span.finish(SpanStatus.OK) - val timestamp = span.finishDate + val timestamp = span.endTimestamp() span.finish(SpanStatus.UNKNOWN_ERROR) // call only once verify(fixture.scopes).setSpanContext(any(), any(), any()) assertEquals(SpanStatus.OK, span.status) - assertEquals(timestamp, span.finishDate) + assertEquals(timestamp, span.endTimestamp()) } @Test @@ -298,22 +298,22 @@ class SpanTest { // then the span start should match the child // but the finish date should be kept the same - assertEquals(child1.startDate, span.startDate) - assertEquals(finishDate, span.finishDate) + assertEquals(child1.startTimestamp(), span.startTimestamp()) + assertEquals(finishDate.nanoTimestamp(), span.endTimestamp()!!.epochNanos()) } @Test fun `when span trim-start is enabled, do not trim to start of child span if it started earlier`() { // when trim start is enabled val span = fixture.getSut(SpanOptions().apply { isTrimStart = true }) - val startDate = span.startDate + val startDate = span.startTimestamp() // and a child span is created but has an earlier timestamp val child1 = span.startChild( "op1", "desc", - SentryLongDate(span.startDate.nanoTimestamp() - 1000L), + SentryLongDate(span.startTimestamp().epochNanos() - 1000L), Instrumenter.SENTRY, SpanOptions(), ) as Span @@ -321,7 +321,7 @@ class SpanTest { span.finish(SpanStatus.OK) // then the span start should remain unchanged - assertEquals(startDate, span.startDate) + assertEquals(startDate, span.startTimestamp()) } @Test @@ -329,7 +329,7 @@ class SpanTest { // when trim end is enabled val span = fixture.getSut(SpanOptions().apply { isTrimEnd = true }) - val startDate = span.startDate + val startDate = span.startTimestamp() // and a child span is created Thread.sleep(1) @@ -340,8 +340,8 @@ class SpanTest { // then the start should be left unchanged // but the end should match the child - assertEquals(startDate, span.startDate) - assertEquals(child1.finishDate, span.finishDate) + assertEquals(startDate, span.startTimestamp()) + assertEquals(child1.endTimestamp(), span.endTimestamp()) } @Test @@ -349,20 +349,20 @@ class SpanTest { // when trim end is enabled val span = fixture.getSut(SpanOptions().apply { isTrimEnd = true }) - val startDate = span.startDate + val startDate = span.startTimestamp() // and a child span is created, but finished later than the parent val child1 = span.startChild("op1") as Span span.finish(SpanStatus.OK) - val finishDate = span.finishDate!! + val finishDate = span.endTimestamp()!! Thread.sleep(1) child1.finish() // then both start and finish date should be left unchanged - assertEquals(startDate, span.startDate) - assertEquals(finishDate, span.finishDate) + assertEquals(startDate, span.startTimestamp()) + assertEquals(finishDate, span.endTimestamp()) } @Test @@ -395,9 +395,9 @@ class SpanTest { assertTrue(span.isFinished) // then the span start/finish should match its direct children only - assertEquals(child1.startDate, span.startDate) - assertEquals(child2.finishDate, span.finishDate) - assertNotEquals(subChild.finishDate, span.finishDate) + assertEquals(child1.startTimestamp(), span.startTimestamp()) + assertEquals(child2.endTimestamp(), span.endTimestamp()) + assertNotEquals(subChild.endTimestamp(), span.endTimestamp()) } @Test @@ -430,9 +430,9 @@ class SpanTest { assertTrue(span.isFinished) // then the root span start/finish should match first/last of its direct and indirect children - assertEquals(child1.startDate, span.startDate) - assertNotEquals(child2.finishDate, span.finishDate) - assertEquals(subChild.finishDate, span.finishDate) + assertEquals(child1.startTimestamp(), span.startTimestamp()) + assertNotEquals(child2.endTimestamp(), span.endTimestamp()) + assertEquals(subChild.endTimestamp(), span.endTimestamp()) } @Test @@ -467,20 +467,20 @@ class SpanTest { fun `updateEndDate is ignored and returns false if span is not finished`() { val span = fixture.getSut() assertFalse(span.isFinished) - assertNull(span.finishDate) - assertFalse(span.updateEndDate(mock())) - assertNull(span.finishDate) + assertNull(span.endTimestamp()) + assertFalse(span.updateEndDate(mock())) + assertNull(span.endTimestamp()) } @Test fun `updateEndDate updates finishDate and returns true if span is finished`() { val span = fixture.getSut() - val endDate: SentryDate = mock() + val endDate = SentryLongDate(1_700_000_000_000_000_000) span.finish() assertTrue(span.isFinished) - assertNotNull(span.finishDate) + assertNotNull(span.endTimestamp()) assertTrue(span.updateEndDate(endDate)) - assertEquals(endDate, span.finishDate) + assertEquals(endDate.nanoTimestamp(), span.endTimestamp()!!.epochNanos()) } @Test @@ -532,10 +532,10 @@ class SpanTest { val span = fixture.getSut() span.setSpanFinishedCallback { assertFalse(span.isFinished) - assertNotNull(span.finishDate) + assertNotNull(span.endTimestamp()) } assertFalse(span.isFinished) - assertNull(span.finishDate) + assertNull(span.endTimestamp()) span.finish() }