Skip to content

Commit 2ebf90a

Browse files
adinauerclaude
andauthored
perf(core): SDK Overhead Reduction (#5499)
* collection: SDK Overhead Reduction * perf(core): Skip java.specification.version lookup on Android Android is never Java 9+, so the System.getProperty + Double.valueOf parse in the Platform static initializer is unnecessary overhead on the Android cold-start path. Short-circuit to isJavaNinePlus=false when isAndroid is true. * perf(android): Replace reflective OptionsContainer with direct subclass Replace OptionsContainer.create(SentryAndroidOptions.class) which uses getDeclaredConstructor().newInstance() with a direct SentryAndroidOptionsContainer subclass that returns new SentryAndroidOptions() without reflection. Make OptionsContainer non-final (@OPEN) with a protected no-arg constructor so Android can subclass it. * collection: SDK Overhead reduction for JVM * perf(core): Short-circuit combined scope breadcrumbs Avoid allocating and sorting a merged breadcrumb queue when only one component scope has breadcrumbs. This keeps the full merge path for multi-scope breadcrumbs and returns the default write scope queue when all scopes are empty. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Reduce envelope writer buffer size Use an explicit 512-character BufferedWriter buffer for envelope item and envelope serialization. This avoids allocating the oversized default char buffer for each short-lived serialization writer while preserving the existing OutputStreamWriter-based encoding path. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * perf(core): Remove redundant event map copies Avoid creating temporary maps when applying scope and options tags or scope extras. The event setters already copy these maps, so this preserves snapshot semantics while reducing allocation overhead. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * changelog * perf(core): Short-circuit combined scope collections Avoid allocating merged collection copies when only one combined scope contains values. This extends the breadcrumbs optimization to tags, attributes, extras, and attachments while preserving merge behavior when multiple scopes contribute data. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * perf(android): Use TimeZone.getDefault for device timezone Avoid constructing a Calendar only to read the default device timezone. The locale passed to Calendar does not affect the timezone value, so TimeZone.getDefault returns the same value with less work during device context collection. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Replace Calendar with Date in DateUtils Avoid constructing Calendar instances when DateUtils only needs the current epoch millis or a Date for an existing millis value. Date stores epoch millis without timezone state, so the returned values are unchanged while avoiding unnecessary Calendar allocation and field computation. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Reduce JsonWriter stack allocation Shrink the vendored JsonWriter nesting stack from 32 entries to 8 entries. The stack still grows on demand for deeply nested payloads, while common SDK serialization avoids the larger initial array allocation. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Lazily allocate Breadcrumb data Avoid allocating a ConcurrentHashMap for breadcrumbs that never set data. Initialize the data map on first write while preserving concurrent writes with double-checked locking. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Reduce context serialization allocations Use sorted key arrays when serializing contexts to avoid allocating an ArrayList for each serialization. This preserves deterministic key ordering while keeping the snapshot representation smaller. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Lazily allocate reflection serializer state Defer creation of the reflection serializer visiting set until reflection serialization is actually needed. Normal SDK payload serialization uses explicit serializers, so this avoids an unused HashSet allocation for each writer. * perf(core): Lazily create reflection JSON serializer Defer creation of JsonReflectionObjectSerializer until unknown-object reflection serialization is needed. Normal SDK payloads use explicit serializers, so this avoids allocating unused reflection serializer state for each writer. * fix(android): Preserve locale timezone extension Keep the Calendar-based timezone path for Android 13+ locales that carry a Unicode tz extension. This preserves the existing device timezone behavior while keeping the direct default timezone fast path for normal locales. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Replace ISO8601 timestamp handling Replace the Calendar-backed vendored ISO8601 formatting and parsing path with a small Sentry-specific utility that works directly from epoch milliseconds. This avoids formatter and parser allocations on timestamp-heavy serialization paths while keeping the existing DateUtils API as the facade. Co-Authored-By: Claude <noreply@anthropic.com> * ref(core): Move ISO8601 utility to vendor package Move the Sentry ISO8601 helper under the vendor package and mark it as internal API so the adapted public-domain date conversion code is isolated from core SDK classes. Update attribution metadata to reflect the public-domain dedication source. Co-Authored-By: Claude <noreply@anthropic.com> * perf(core): Avoid cloning Date getters * fix(core): Preserve ISO8601 utility compatibility Match edge-case behavior from the previous vendored ISO8601 utility for date-only timestamps, trailing characters after Z, and Gregorian cutover dates. * fix(core): Preserve mutable breadcrumb data access Initialize the lazy breadcrumb data map when callers request the full map. This keeps getData() mutable for existing callers while preserving lazy allocation for breadcrumbs that only serialize or read individual values. Co-Authored-By: Claude <noreply@anthropic.com> * docs(android): Explain timezone Calendar fallback Document why Android 13+ locales with Unicode timezone extensions keep using Calendar while normal locales use the default timezone directly for performance. Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): Avoid KeySetView in context serialization Use ConcurrentHashMap.keys() when creating sorted context key snapshots so the serialization path stays compatible with Android API 21. Keep the array snapshot optimization without relying on KeySetView, which AnimalSniffer rejects for the SDK's minSdk. Co-Authored-By: Claude <noreply@anthropic.com> * test(core): Add breadcrumb timestamp serialization coverage Cover that breadcrumbs backed by timestamp milliseconds serialize the same timestamp as breadcrumbs backed by Date for the same instant. * fix(core): Parse date-only timestamps with timezones Preserve ISO8601 parser compatibility for date-only values that include a timezone suffix. Keep modern date-only timezone parsing on the fast path and add parity coverage against the previous parser. * docs(core): Add timezone changelog entry * docs(core): Add DateUtils changelog entry * docs(core): Add JsonWriter changelog entry * docs(core): Add breadcrumb changelog entry * docs(core): Add contexts changelog entry * docs(core): Add reflection state changelog entry * docs(core): Add reflection serializer changelog entry * docs(core): Add ISO8601 handling changelog entry * docs(core): Add Date getter changelog entries * changelog --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e0a2a6e commit 2ebf90a

38 files changed

Lines changed: 1252 additions & 114 deletions

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Behavioral Changes
6+
7+
- Collections returned by scope (e.g. `getBreadcrumbs`, `getTags`, `getAttachments`) are shared state and should not be mutated. ([#5541](https://github.com/getsentry/sentry-java/pull/5541))
8+
- Previously, when going through `CombinedScopeView`, we were returning a copy where mutations didn't show up in the underlying scopes.
9+
- This has now changed in order to reduce SDK overhead.
10+
- `Date` objects returned by SDK data model getters are shared state and should not be mutated. ([#5603](https://github.com/getsentry/sentry-java/pull/5603))
11+
- Previously, these getters returned defensive copies for some date fields.
12+
- This has now changed in order to reduce SDK overhead.
13+
14+
### Performance
15+
16+
- Reduce writer buffer size from 8192 to 512 ([#5544](https://github.com/getsentry/sentry-java/pull/5544))
17+
- Remove redundant event map copies ([#5536](https://github.com/getsentry/sentry-java/pull/5536))
18+
- Optimize combined scope by adding an early return if only one scope has data ([#5541](https://github.com/getsentry/sentry-java/pull/5541))
19+
- Reduce model access overhead by avoiding defensive `Date` copies in SDK data model getters. ([#5603](https://github.com/getsentry/sentry-java/pull/5603))
20+
- Reduce timestamp parsing and formatting overhead with Sentry-specific ISO-8601 handling. ([#5602](https://github.com/getsentry/sentry-java/pull/5602))
21+
- Reduce JSON serialization overhead by creating the reflection serializer only when unknown-object fallback serialization is needed. ([#5601](https://github.com/getsentry/sentry-java/pull/5601))
22+
- Reduce JSON serialization overhead by allocating reflection cycle-tracking state only when reflection serialization is used. ([#5600](https://github.com/getsentry/sentry-java/pull/5600))
23+
- Reduce context serialization overhead by sorting key snapshots with arrays instead of temporary lists. ([#5599](https://github.com/getsentry/sentry-java/pull/5599))
24+
- Reduce breadcrumb allocation overhead by creating the `Breadcrumb` data map only when data is added. ([#5598](https://github.com/getsentry/sentry-java/pull/5598))
25+
- Reduce JSON serialization overhead by lowering the initial `JsonWriter` nesting stack size while preserving on-demand growth. ([#5591](https://github.com/getsentry/sentry-java/pull/5591))
26+
- Reduce timestamp helper overhead by replacing unnecessary `Calendar` usage in `DateUtils` with direct `Date` creation. ([#5589](https://github.com/getsentry/sentry-java/pull/5589))
27+
- Reduce Android startup overhead by using the default timezone directly on older devices or when no timezone info is available in the locale. ([#5587](https://github.com/getsentry/sentry-java/pull/5587))
28+
329
## 8.45.0
430

531
### Features

THIRD_PARTY_NOTICES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ limitations under the License.
6262

6363
---
6464

65+
## Howard Hinnant — Date Algorithms (Public Domain)
66+
67+
**Source:** https://howardhinnant.github.io/date_algorithms.html<br>
68+
**License:** Public Domain<br>
69+
**Copyright:** None; public domain dedication by Howard Hinnant
70+
71+
### Scope
72+
73+
The Sentry Java SDK includes adapted civil date conversion algorithms from Howard Hinnant's date algorithms for UTC ISO 8601 timestamp parsing and formatting. The code resides in `io.sentry.vendor.SentryIso8601Utils`.
74+
75+
```
76+
Consider these donated to the public domain.
77+
```
78+
79+
---
80+
6581
## Android Open Source Project — Base64 (Apache 2.0)
6682

6783
**Source:** https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Base64.java<br>

sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,19 @@ private void setDeviceIO(
257257
@SuppressWarnings("NewApi")
258258
@NotNull
259259
private TimeZone getTimeZone() {
260-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) {
260+
// Only use the costly Calendar API on Android 13+ (API Level 33+) when the locale contains a
261+
// Unicode timezone extension (for example "en-US-u-tz-usnyc"), because Calendar honors that
262+
// extension. For all other cases, use the process default timezone directly for performance.
263+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
261264
LocaleList locales = context.getResources().getConfiguration().getLocales();
262265
if (!locales.isEmpty()) {
263266
Locale locale = locales.get(0);
264-
return Calendar.getInstance(locale).getTimeZone();
267+
if (locale.getUnicodeLocaleType("tz") != null) {
268+
return Calendar.getInstance(locale).getTimeZone();
269+
}
265270
}
266271
}
267-
return Calendar.getInstance().getTimeZone();
272+
return TimeZone.getDefault();
268273
}
269274

270275
@SuppressWarnings("JdkObsolete")

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import io.sentry.IScopes;
1010
import io.sentry.ISentryLifecycleToken;
1111
import io.sentry.Integration;
12-
import io.sentry.OptionsContainer;
1312
import io.sentry.Sentry;
1413
import io.sentry.SentryLevel;
1514
import io.sentry.SentryOptions;
@@ -98,7 +97,7 @@ public static void init(
9897
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
9998
try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) {
10099
Sentry.init(
101-
OptionsContainer.create(SentryAndroidOptions.class),
100+
new SentryAndroidOptionsContainer(),
102101
options -> {
103102
final io.sentry.util.LoadClass classLoader = new io.sentry.util.LoadClass();
104103
final boolean isTimberUpstreamAvailable =
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.sentry.android.core;
2+
3+
import io.sentry.OptionsContainer;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
/**
7+
* Direct OptionsContainer for SentryAndroidOptions that avoids reflective
8+
* getDeclaredConstructor().newInstance() on the Android startup path.
9+
*/
10+
final class SentryAndroidOptionsContainer extends OptionsContainer<SentryAndroidOptions> {
11+
12+
@Override
13+
public @NotNull SentryAndroidOptions createInstance() {
14+
return new SentryAndroidOptions();
15+
}
16+
}

sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@ package io.sentry.android.core
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.content.res.Configuration
56
import android.os.BatteryManager
7+
import android.os.Build
8+
import android.os.LocaleList
69
import androidx.test.core.app.ApplicationProvider
710
import androidx.test.ext.junit.runners.AndroidJUnit4
811
import io.sentry.android.core.internal.util.CpuInfoUtils
12+
import java.util.Locale
13+
import java.util.TimeZone
914
import kotlin.test.BeforeTest
1015
import kotlin.test.Test
1116
import kotlin.test.assertEquals
1217
import kotlin.test.assertNotNull
1318
import kotlin.test.assertNull
1419
import org.junit.runner.RunWith
20+
import org.robolectric.annotation.Config
1521

1622
@RunWith(AndroidJUnit4::class)
1723
class DeviceInfoUtilTest {
@@ -47,6 +53,32 @@ class DeviceInfoUtilTest {
4753
assertNotNull(deviceInfo.memorySize)
4854
}
4955

56+
@Test
57+
fun `sets default timezone`() {
58+
val deviceInfoUtil = DeviceInfoUtil.getInstance(context, SentryAndroidOptions())
59+
val deviceInfo = deviceInfoUtil.collectDeviceInformation(false, false)
60+
61+
assertEquals(TimeZone.getDefault(), deviceInfo.timezone)
62+
}
63+
64+
@Test
65+
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
66+
fun `preserves timezone from locale unicode extension`() {
67+
val defaultTimeZone = TimeZone.getDefault()
68+
try {
69+
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
70+
val configuration = Configuration(context.resources.configuration)
71+
configuration.setLocales(LocaleList(Locale.forLanguageTag("en-US-u-tz-usnyc")))
72+
val localizedContext = context.createConfigurationContext(configuration)
73+
val deviceInfoUtil = DeviceInfoUtil(localizedContext, SentryAndroidOptions())
74+
val deviceInfo = deviceInfoUtil.collectDeviceInformation(false, false)
75+
76+
assertEquals("America/New_York", deviceInfo.timezone?.id)
77+
} finally {
78+
TimeZone.setDefault(defaultTimeZone)
79+
}
80+
}
81+
5082
@Test
5183
fun `does include cpu data`() {
5284
CpuInfoUtils.getInstance().setCpuMaxFrequencies(listOf(1024))

sentry/api/sentry.api

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,7 +1312,6 @@ public final class io/sentry/JsonObjectReader : io/sentry/ObjectReader {
13121312

13131313
public final class io/sentry/JsonObjectSerializer {
13141314
public static final field OBJECT_PLACEHOLDER Ljava/lang/String;
1315-
public final field jsonReflectionObjectSerializer Lio/sentry/JsonReflectionObjectSerializer;
13161315
public fun <init> (I)V
13171316
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;Ljava/lang/Object;)V
13181317
}
@@ -2067,7 +2066,8 @@ public abstract interface class io/sentry/ObjectWriter {
20672066
public abstract fun value (Z)Lio/sentry/ObjectWriter;
20682067
}
20692068

2070-
public final class io/sentry/OptionsContainer {
2069+
public class io/sentry/OptionsContainer {
2070+
protected fun <init> ()V
20712071
public static fun create (Ljava/lang/Class;)Lio/sentry/OptionsContainer;
20722072
public fun createInstance ()Ljava/lang/Object;
20732073
}
@@ -7618,6 +7618,7 @@ public final class io/sentry/util/CollectionUtils {
76187618
public static fun newHashMap (Ljava/util/Map;)Ljava/util/Map;
76197619
public static fun reverseListIterator (Ljava/util/concurrent/CopyOnWriteArrayList;)Ljava/util/ListIterator;
76207620
public static fun size (Ljava/lang/Iterable;)I
7621+
public static fun toSortedStringArray (Ljava/util/Enumeration;I)[Ljava/lang/String;
76217622
}
76227623

76237624
public abstract interface class io/sentry/util/CollectionUtils$Mapper {
@@ -8075,6 +8076,11 @@ public class io/sentry/vendor/Base64 {
80758076
public static fun encodeToString ([BIII)Ljava/lang/String;
80768077
}
80778078

8079+
public final class io/sentry/vendor/SentryIso8601Utils {
8080+
public static fun formatTimestamp (J)Ljava/lang/String;
8081+
public static fun parseTimestamp (Ljava/lang/String;)J
8082+
}
8083+
80788084
public class io/sentry/vendor/gson/internal/bind/util/ISO8601Utils {
80798085
public static final field TIMEZONE_UTC Ljava/util/TimeZone;
80808086
public fun <init> ()V

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

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ public final class Breadcrumb implements JsonUnknown, JsonSerializable, Comparab
3434
/** The type of breadcrumb. */
3535
private @Nullable String type;
3636

37+
private static final @NotNull Map<String, @NotNull Object> EMPTY_DATA = Collections.emptyMap();
38+
3739
/** Data associated with this breadcrumb. */
38-
private @NotNull Map<String, @NotNull Object> data = new ConcurrentHashMap<>();
40+
private volatile @NotNull Map<String, @NotNull Object> data = EMPTY_DATA;
3941

4042
/** Dotted strings that indicate what the crumb is or where it comes from. */
4143
private @Nullable String category;
@@ -78,9 +80,11 @@ public Breadcrumb(final long timestamp) {
7880
this.type = breadcrumb.type;
7981
this.category = breadcrumb.category;
8082
this.origin = breadcrumb.origin;
81-
final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
82-
if (dataClone != null) {
83-
this.data = dataClone;
83+
if (!breadcrumb.data.isEmpty()) {
84+
final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
85+
if (dataClone != null) {
86+
this.data = dataClone;
87+
}
8488
}
8589
this.unknown = CollectionUtils.newConcurrentHashMap(breadcrumb.unknown);
8690
this.level = breadcrumb.level;
@@ -100,7 +104,7 @@ public static Breadcrumb fromMap(
100104
@NotNull Date timestamp = DateUtils.getCurrentDateTime();
101105
String message = null;
102106
String type = null;
103-
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
107+
Map<String, Object> data = null;
104108
String category = null;
105109
String origin = null;
106110
SentryLevel level = null;
@@ -129,6 +133,9 @@ public static Breadcrumb fromMap(
129133
if (untypedData != null) {
130134
for (Map.Entry<Object, Object> dataEntry : untypedData.entrySet()) {
131135
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
136+
if (data == null) {
137+
data = new ConcurrentHashMap<>();
138+
}
132139
data.put((String) dataEntry.getKey(), dataEntry.getValue());
133140
} else {
134141
options
@@ -166,7 +173,9 @@ public static Breadcrumb fromMap(
166173
final Breadcrumb breadcrumb = new Breadcrumb(timestamp);
167174
breadcrumb.message = message;
168175
breadcrumb.type = type;
169-
breadcrumb.data = data;
176+
if (data != null) {
177+
breadcrumb.data = data;
178+
}
170179
breadcrumb.category = category;
171180
breadcrumb.origin = origin;
172181
breadcrumb.level = level;
@@ -494,7 +503,7 @@ public static Breadcrumb fromMap(
494503
breadcrumb.setData("view.tag", viewTag);
495504
}
496505
for (final Map.Entry<String, Object> entry : additionalData.entrySet()) {
497-
breadcrumb.getData().put(entry.getKey(), entry.getValue());
506+
breadcrumb.setData(entry.getKey(), entry.getValue());
498507
}
499508
breadcrumb.setLevel(SentryLevel.INFO);
500509
return breadcrumb;
@@ -553,9 +562,9 @@ public Breadcrumb(@Nullable String message) {
553562
@SuppressWarnings("JavaUtilDate")
554563
public @NotNull Date getTimestamp() {
555564
if (timestamp != null) {
556-
return (Date) timestamp.clone();
565+
return timestamp;
557566
} else if (timestampMs != null) {
558-
// we memoize it here into timestamp to avoid instantiating Calendar again and again
567+
// we memoize it here into timestamp to avoid creating a Date again and again
559568
timestamp = DateUtils.getDateTime(timestampMs);
560569
return timestamp;
561570
}
@@ -598,6 +607,20 @@ public void setType(@Nullable String type) {
598607
this.type = type;
599608
}
600609

610+
private @NotNull Map<String, @NotNull Object> getOrCreateData() {
611+
Map<String, @NotNull Object> currentData = data;
612+
if (currentData == EMPTY_DATA) {
613+
synchronized (this) {
614+
currentData = data;
615+
if (currentData == EMPTY_DATA) {
616+
currentData = new ConcurrentHashMap<>();
617+
data = currentData;
618+
}
619+
}
620+
}
621+
return currentData;
622+
}
623+
601624
/**
602625
* Returns the data map
603626
*
@@ -606,7 +629,7 @@ public void setType(@Nullable String type) {
606629
@ApiStatus.Internal
607630
@NotNull
608631
public Map<String, Object> getData() {
609-
return data;
632+
return getOrCreateData();
610633
}
611634

612635
/**
@@ -636,7 +659,7 @@ public void setData(@Nullable String key, @Nullable Object value) {
636659
if (value == null) {
637660
removeData(key);
638661
} else {
639-
data.put(key, value);
662+
getOrCreateData().put(key, value);
640663
}
641664
}
642665

@@ -649,7 +672,10 @@ public void removeData(@Nullable String key) {
649672
if (key == null) {
650673
return;
651674
}
652-
data.remove(key);
675+
final Map<String, @NotNull Object> currentData = data;
676+
if (currentData != EMPTY_DATA) {
677+
currentData.remove(key);
678+
}
653679
}
654680

655681
/**
@@ -823,7 +849,12 @@ public static final class JsonKeys {
823849
public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
824850
throws IOException {
825851
writer.beginObject();
826-
writer.name(JsonKeys.TIMESTAMP).value(logger, getTimestamp());
852+
writer
853+
.name(JsonKeys.TIMESTAMP)
854+
.value(
855+
timestampMs != null
856+
? DateUtils.getTimestampFromMillis(timestampMs)
857+
: DateUtils.getTimestamp(getTimestamp()));
827858
if (message != null) {
828859
writer.name(JsonKeys.MESSAGE).value(message);
829860
}
@@ -859,7 +890,7 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
859890
@NotNull Date timestamp = DateUtils.getCurrentDateTime();
860891
String message = null;
861892
String type = null;
862-
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
893+
Map<String, Object> data = null;
863894
String category = null;
864895
String origin = null;
865896
SentryLevel level = null;
@@ -884,7 +915,7 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
884915
Map<String, Object> deserializedData =
885916
CollectionUtils.newConcurrentHashMap(
886917
(Map<String, Object>) reader.nextObjectOrNull());
887-
if (deserializedData != null) {
918+
if (deserializedData != null && !deserializedData.isEmpty()) {
888919
data = deserializedData;
889920
}
890921
break;
@@ -913,7 +944,9 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
913944
Breadcrumb breadcrumb = new Breadcrumb(timestamp);
914945
breadcrumb.message = message;
915946
breadcrumb.type = type;
916-
breadcrumb.data = data;
947+
if (data != null) {
948+
breadcrumb.data = data;
949+
}
917950
breadcrumb.category = category;
918951
breadcrumb.origin = origin;
919952
breadcrumb.level = level;

0 commit comments

Comments
 (0)