Skip to content

Commit a2de9ab

Browse files
runningcodeclaude
andcommitted
ref(core): Keep the creation tick on deserialized breadcrumbs
Always fill the tick and let the timestamp comparison carry the fix, so ordering no longer depends on every caller using a stable sort. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 162e99e commit a2de9ab

2 files changed

Lines changed: 27 additions & 36 deletions

File tree

‎sentry/src/main/java/io/sentry/Breadcrumb.java‎

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,7 @@ public final class Breadcrumb implements JsonUnknown, JsonSerializable, Comparab
2626
/** A timestamp representing when the breadcrumb occurred as java.util.Date. */
2727
private @Nullable Date timestamp;
2828

29-
/**
30-
* The tick this breadcrumb was created at, used to order breadcrumbs that share a timestamp.
31-
*
32-
* <p>Null for a breadcrumb rebuilt from a serialized one. A tick is a reading of a counter whose
33-
* origin is this process run, so a tick from an earlier run is a number from an unrelated origin
34-
* rather than a position in this run's order.
35-
*/
36-
private final @Nullable Long creationTick;
29+
private final @NotNull Long nanos;
3730

3831
/** If a message is provided, its rendered as text and the whitespace is preserved. */
3932
private @Nullable String message;
@@ -66,33 +59,23 @@ public final class Breadcrumb implements JsonUnknown, JsonSerializable, Comparab
6659
*
6760
* @param timestamp the timestamp
6861
*/
69-
public Breadcrumb(final @NotNull Date timestamp) {
70-
this(timestamp, System.nanoTime());
71-
}
72-
7362
@SuppressWarnings("JavaUtilDate")
74-
private Breadcrumb(final @NotNull Date timestamp, final @Nullable Long creationTick) {
75-
this.creationTick = creationTick;
63+
public Breadcrumb(final @NotNull Date timestamp) {
64+
this.nanos = System.nanoTime();
7665
this.timestamp = timestamp;
7766
this.timestampMs = null;
7867
}
7968

80-
/**
81-
* A breadcrumb rebuilt from a serialized one — read back from disk, or handed over by a hybrid
82-
* SDK. It carries the timestamp it was serialized with and no creation tick.
83-
*/
84-
static @NotNull Breadcrumb deserialized(final @NotNull Date timestamp) {
85-
return new Breadcrumb(timestamp, null);
86-
}
87-
8869
public Breadcrumb(final long timestamp) {
89-
this.creationTick = System.nanoTime();
70+
this.nanos = System.nanoTime();
9071
this.timestampMs = timestamp;
9172
this.timestamp = null;
9273
}
9374

9475
Breadcrumb(final @NotNull Breadcrumb breadcrumb) {
95-
this.creationTick = breadcrumb.creationTick;
76+
// A clone stands in for the breadcrumb it was copied from, so it inherits its tie-breaker
77+
// instead of taking a fresh one and sorting after everything recorded since.
78+
this.nanos = breadcrumb.nanos;
9679
this.timestamp = breadcrumb.timestamp;
9780
this.timestampMs = breadcrumb.timestampMs;
9881
this.message = breadcrumb.message;
@@ -189,7 +172,7 @@ public static Breadcrumb fromMap(
189172
}
190173
}
191174

192-
final Breadcrumb breadcrumb = Breadcrumb.deserialized(timestamp);
175+
final Breadcrumb breadcrumb = new Breadcrumb(timestamp);
193176
breadcrumb.message = message;
194177
breadcrumb.type = type;
195178
if (data != null) {
@@ -850,21 +833,14 @@ public void setUnknown(@Nullable Map<String, Object> unknown) {
850833

851834
@Override
852835
@SuppressWarnings("JavaUtilDate")
853-
public int compareTo(final @NotNull Breadcrumb o) {
836+
public int compareTo(@NotNull Breadcrumb o) {
854837
final int byTimestamp = getTimestamp().compareTo(o.getTimestamp());
855838
if (byTimestamp != 0) {
856839
return byTimestamp;
857840
}
858841
// Timestamps are millisecond-granular, so breadcrumbs recorded in the same millisecond tie.
859-
// Creation ticks break the tie in the order they were actually recorded.
860-
if (creationTick == null) {
861-
// Deserialized, so from an earlier process run than anything holding a tick.
862-
return o.creationTick == null ? 0 : -1;
863-
}
864-
if (o.creationTick == null) {
865-
return 1;
866-
}
867-
return creationTick.compareTo(o.creationTick);
842+
// nanos is only meaningful within a process run, which is all a tie-breaker has to cover.
843+
return nanos.compareTo(o.nanos);
868844
}
869845

870846
public static final class JsonKeys {
@@ -973,7 +949,7 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
973949
}
974950
}
975951

976-
Breadcrumb breadcrumb = Breadcrumb.deserialized(timestamp);
952+
Breadcrumb breadcrumb = new Breadcrumb(timestamp);
977953
breadcrumb.message = message;
978954
breadcrumb.type = type;
979955
if (data != null) {

‎sentry/src/test/java/io/sentry/BreadcrumbTest.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry
22

33
import com.google.common.truth.Truth.assertThat
4+
import java.io.StringReader
45
import java.util.Date
56
import java.util.concurrent.CountDownLatch
67
import java.util.concurrent.Executors
@@ -395,6 +396,20 @@ class BreadcrumbTest {
395396
assertThat(sorted).containsExactly("restored", "live").inOrder()
396397
}
397398

399+
@Test
400+
fun `a breadcrumb read back from JSON is ordered by its own timestamp, not by when it was parsed`() {
401+
val live = Breadcrumb(Date(1_600_000_000_000)).apply { message = "live" }
402+
val json =
403+
"""{"timestamp":"${DateUtils.getTimestamp(Date(1_500_000_000_000))}","message":"restored"}"""
404+
val restored =
405+
Breadcrumb.Deserializer()
406+
.deserialize(JsonObjectReader(StringReader(json)), NoOpLogger.getInstance())
407+
408+
val sorted = listOf(live, restored).sorted().map { it.message }
409+
410+
assertThat(sorted).containsExactly("restored", "live").inOrder()
411+
}
412+
398413
@Test
399414
fun `cloning a breadcrumb keeps its position among breadcrumbs sharing its timestamp`() {
400415
val timestamp = Date(1_600_000_000_000)

0 commit comments

Comments
 (0)