Skip to content

Commit c9d0787

Browse files
Create User and Breadcrumb from map (#2614)
Co-authored-by: Sentry Github Bot <[email protected]>
1 parent e5871b9 commit c9d0787

File tree

8 files changed

+340
-1
lines changed

8 files changed

+340
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Attach Trace Context when an ANR is detected (ANRv1) ([#2583](https://github.com/getsentry/sentry-java/pull/2583))
88
- Make log4j2 integration compatible with log4j 3.0 ([#2634](https://github.com/getsentry/sentry-java/pull/2634))
99
- Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
10+
- Create `User` and `Breadcrumb` from map ([#2614](https://github.com/getsentry/sentry-java/pull/2614))
1011

1112
### Fixes
1213

sentry/api/sentry.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public final class io/sentry/Breadcrumb : io/sentry/JsonSerializable, io/sentry/
8989
public fun <init> (Ljava/util/Date;)V
9090
public static fun debug (Ljava/lang/String;)Lio/sentry/Breadcrumb;
9191
public static fun error (Ljava/lang/String;)Lio/sentry/Breadcrumb;
92+
public static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/Breadcrumb;
9293
public fun getCategory ()Ljava/lang/String;
9394
public fun getData ()Ljava/util/Map;
9495
public fun getData (Ljava/lang/String;)Ljava/lang/Object;
@@ -641,6 +642,7 @@ public final class io/sentry/JsonObjectDeserializer {
641642

642643
public final class io/sentry/JsonObjectReader : io/sentry/vendor/gson/stream/JsonReader {
643644
public fun <init> (Ljava/io/Reader;)V
645+
public static fun dateOrNull (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/util/Date;
644646
public fun nextBooleanOrNull ()Ljava/lang/Boolean;
645647
public fun nextDateOrNull (Lio/sentry/ILogger;)Ljava/util/Date;
646648
public fun nextDoubleOrNull ()Ljava/lang/Double;
@@ -2891,6 +2893,7 @@ public final class io/sentry/protocol/Device$JsonKeys {
28912893
public final class io/sentry/protocol/Geo : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
28922894
public fun <init> ()V
28932895
public fun <init> (Lio/sentry/protocol/Geo;)V
2896+
public static fun fromMap (Ljava/util/Map;)Lio/sentry/protocol/Geo;
28942897
public fun getCity ()Ljava/lang/String;
28952898
public fun getCountryCode ()Ljava/lang/String;
28962899
public fun getRegion ()Ljava/lang/String;
@@ -3583,6 +3586,7 @@ public final class io/sentry/protocol/TransactionNameSource : java/lang/Enum {
35833586
public final class io/sentry/protocol/User : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
35843587
public fun <init> ()V
35853588
public fun <init> (Lio/sentry/protocol/User;)V
3589+
public static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/protocol/User;
35863590
public fun getData ()Ljava/util/Map;
35873591
public fun getEmail ()Ljava/lang/String;
35883592
public fun getGeo ()Lio/sentry/protocol/Geo;

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,91 @@ public Breadcrumb(final @NotNull Date timestamp) {
5959
this.level = breadcrumb.level;
6060
}
6161

62+
/**
63+
* Creates breadcrumb from a map.
64+
*
65+
* @param map - The breadcrumb data as map
66+
* @param options - the sentry options
67+
* @return the breadcrumb
68+
*/
69+
@SuppressWarnings("unchecked")
70+
public static Breadcrumb fromMap(
71+
@NotNull Map<String, Object> map, @NotNull SentryOptions options) {
72+
73+
@NotNull Date timestamp = DateUtils.getCurrentDateTime();
74+
String message = null;
75+
String type = null;
76+
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
77+
String category = null;
78+
SentryLevel level = null;
79+
Map<String, Object> unknown = null;
80+
81+
for (Map.Entry<String, Object> entry : map.entrySet()) {
82+
Object value = entry.getValue();
83+
switch (entry.getKey()) {
84+
case JsonKeys.TIMESTAMP:
85+
if (value instanceof String) {
86+
Date deserializedDate =
87+
JsonObjectReader.dateOrNull((String) value, options.getLogger());
88+
if (deserializedDate != null) {
89+
timestamp = deserializedDate;
90+
}
91+
}
92+
break;
93+
case JsonKeys.MESSAGE:
94+
message = (value instanceof String) ? (String) value : null;
95+
break;
96+
case JsonKeys.TYPE:
97+
type = (value instanceof String) ? (String) value : null;
98+
break;
99+
case JsonKeys.DATA:
100+
final Map<Object, Object> untypedData =
101+
(value instanceof Map) ? (Map<Object, Object>) value : null;
102+
if (untypedData != null) {
103+
for (Map.Entry<Object, Object> dataEntry : untypedData.entrySet()) {
104+
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
105+
data.put((String) dataEntry.getKey(), dataEntry.getValue());
106+
} else {
107+
options
108+
.getLogger()
109+
.log(SentryLevel.WARNING, "Invalid key or null value in data map.");
110+
}
111+
}
112+
}
113+
break;
114+
case JsonKeys.CATEGORY:
115+
category = (value instanceof String) ? (String) value : null;
116+
break;
117+
case JsonKeys.LEVEL:
118+
String levelString = (value instanceof String) ? (String) value : null;
119+
if (levelString != null) {
120+
try {
121+
level = SentryLevel.valueOf(levelString.toUpperCase(Locale.ROOT));
122+
} catch (Exception exception) {
123+
// Stub
124+
}
125+
}
126+
break;
127+
default:
128+
if (unknown == null) {
129+
unknown = new ConcurrentHashMap<>();
130+
}
131+
unknown.put(entry.getKey(), entry.getValue());
132+
break;
133+
}
134+
}
135+
136+
final Breadcrumb breadcrumb = new Breadcrumb(timestamp);
137+
breadcrumb.message = message;
138+
breadcrumb.type = type;
139+
breadcrumb.data = data;
140+
breadcrumb.category = category;
141+
breadcrumb.level = level;
142+
143+
breadcrumb.setUnknown(unknown);
144+
return breadcrumb;
145+
}
146+
62147
/**
63148
* Creates HTTP breadcrumb.
64149
*

sentry/src/main/java/io/sentry/JsonObjectReader.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,13 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
135135
nextNull();
136136
return null;
137137
}
138-
String dateString = nextString();
138+
return JsonObjectReader.dateOrNull(nextString(), logger);
139+
}
140+
141+
public static @Nullable Date dateOrNull(@Nullable String dateString, ILogger logger) {
142+
if (dateString == null) {
143+
return null;
144+
}
139145
try {
140146
return DateUtils.getDateTime(dateString);
141147
} catch (Exception e) {

sentry/src/main/java/io/sentry/protocol/Geo.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ public Geo(final @NotNull Geo geo) {
3535
this.region = geo.region;
3636
}
3737

38+
/**
39+
* Creates geo from a map.
40+
*
41+
* @param map - The geo data as map
42+
* @return the geo
43+
*/
44+
public static Geo fromMap(@NotNull Map<String, Object> map) {
45+
final Geo geo = new Geo();
46+
for (Map.Entry<String, Object> entry : map.entrySet()) {
47+
Object value = entry.getValue();
48+
switch (entry.getKey()) {
49+
case JsonKeys.CITY:
50+
geo.city = (value instanceof String) ? (String) value : null;
51+
break;
52+
case JsonKeys.COUNTRY_CODE:
53+
geo.countryCode = (value instanceof String) ? (String) value : null;
54+
break;
55+
case JsonKeys.REGION:
56+
geo.region = (value instanceof String) ? (String) value : null;
57+
break;
58+
default:
59+
break;
60+
}
61+
}
62+
return geo;
63+
}
64+
3865
/**
3966
* Gets the human readable city name.
4067
*

sentry/src/main/java/io/sentry/protocol/User.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import io.sentry.JsonObjectWriter;
77
import io.sentry.JsonSerializable;
88
import io.sentry.JsonUnknown;
9+
import io.sentry.SentryLevel;
10+
import io.sentry.SentryOptions;
911
import io.sentry.util.CollectionUtils;
1012
import io.sentry.vendor.gson.stream.JsonToken;
1113
import java.io.IOException;
@@ -65,6 +67,104 @@ public User(final @NotNull User user) {
6567
this.unknown = CollectionUtils.newConcurrentHashMap(user.unknown);
6668
}
6769

70+
/**
71+
* Creates user from a map.
72+
*
73+
* <p>The values `data` and `value` expect a {@code Map<String, String>} type. If other object
74+
* types are in the map `toString()` will be called on them.
75+
*
76+
* @param map - The user data as map
77+
* @param options - the sentry options
78+
* @return the user
79+
*/
80+
@SuppressWarnings("unchecked")
81+
public static User fromMap(@NotNull Map<String, Object> map, @NotNull SentryOptions options) {
82+
final User user = new User();
83+
Map<String, Object> unknown = null;
84+
85+
for (Map.Entry<String, Object> entry : map.entrySet()) {
86+
Object value = entry.getValue();
87+
switch (entry.getKey()) {
88+
case JsonKeys.EMAIL:
89+
user.email = (value instanceof String) ? (String) value : null;
90+
break;
91+
case JsonKeys.ID:
92+
user.id = (value instanceof String) ? (String) value : null;
93+
break;
94+
case JsonKeys.USERNAME:
95+
user.username = (value instanceof String) ? (String) value : null;
96+
break;
97+
case JsonKeys.SEGMENT:
98+
user.segment = (value instanceof String) ? (String) value : null;
99+
break;
100+
case JsonKeys.IP_ADDRESS:
101+
user.ipAddress = (value instanceof String) ? (String) value : null;
102+
break;
103+
case JsonKeys.NAME:
104+
user.name = (value instanceof String) ? (String) value : null;
105+
break;
106+
case JsonKeys.GEO:
107+
final Map<Object, Object> geo =
108+
(value instanceof Map) ? (Map<Object, Object>) value : null;
109+
if (geo != null) {
110+
final ConcurrentHashMap<String, Object> geoData = new ConcurrentHashMap<>();
111+
for (Map.Entry<Object, Object> geoEntry : geo.entrySet()) {
112+
if (geoEntry.getKey() instanceof String && geoEntry.getValue() != null) {
113+
geoData.put((String) geoEntry.getKey(), geoEntry.getValue());
114+
} else {
115+
options.getLogger().log(SentryLevel.WARNING, "Invalid key type in gep map.");
116+
}
117+
}
118+
user.geo = Geo.fromMap(geoData);
119+
}
120+
break;
121+
case JsonKeys.DATA:
122+
final Map<Object, Object> data =
123+
(value instanceof Map) ? (Map<Object, Object>) value : null;
124+
if (data != null) {
125+
final ConcurrentHashMap<String, String> userData = new ConcurrentHashMap<>();
126+
for (Map.Entry<Object, Object> dataEntry : data.entrySet()) {
127+
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
128+
userData.put((String) dataEntry.getKey(), dataEntry.getValue().toString());
129+
} else {
130+
options
131+
.getLogger()
132+
.log(SentryLevel.WARNING, "Invalid key or null value in data map.");
133+
}
134+
}
135+
user.data = userData;
136+
}
137+
break;
138+
case JsonKeys.OTHER:
139+
final Map<Object, Object> other =
140+
(value instanceof Map) ? (Map<Object, Object>) value : null;
141+
// restore `other` from legacy JSON
142+
if (other != null && (user.data == null || user.data.isEmpty())) {
143+
final ConcurrentHashMap<String, String> userData = new ConcurrentHashMap<>();
144+
for (Map.Entry<Object, Object> otherEntry : other.entrySet()) {
145+
if (otherEntry.getKey() instanceof String && otherEntry.getValue() != null) {
146+
userData.put((String) otherEntry.getKey(), otherEntry.getValue().toString());
147+
} else {
148+
options
149+
.getLogger()
150+
.log(SentryLevel.WARNING, "Invalid key or null value in other map.");
151+
}
152+
}
153+
user.data = userData;
154+
}
155+
break;
156+
default:
157+
if (unknown == null) {
158+
unknown = new ConcurrentHashMap<>();
159+
}
160+
unknown.put(entry.getKey(), entry.getValue());
161+
break;
162+
}
163+
}
164+
user.unknown = unknown;
165+
return user;
166+
}
167+
68168
/**
69169
* Gets the e-mail address of the user.
70170
*

sentry/src/test/java/io/sentry/protocol/BreadcrumbSerializationTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import io.sentry.JsonObjectReader
88
import io.sentry.JsonObjectWriter
99
import io.sentry.JsonSerializable
1010
import io.sentry.SentryLevel
11+
import io.sentry.SentryOptions
1112
import org.junit.Test
1213
import org.mockito.kotlin.mock
1314
import java.io.StringReader
1415
import java.io.StringWriter
1516
import kotlin.test.assertEquals
17+
import kotlin.test.assertTrue
1618

1719
class BreadcrumbSerializationTest {
1820

@@ -47,6 +49,51 @@ class BreadcrumbSerializationTest {
4749
assertEquals(expectedJson, actualJson)
4850
}
4951

52+
@Test
53+
fun deserializeFromMap() {
54+
val map: Map<String, Any?> = mapOf(
55+
"timestamp" to "2009-11-16T01:08:47.000Z",
56+
"message" to "46f233c0-7c2d-488a-b05a-7be559173e16",
57+
"type" to "ace57e2e-305e-4048-abf0-6c8538ea7bf4",
58+
"data" to mapOf(
59+
"6607d106-d426-462b-af74-f29fce978e48" to "149bb94a-1387-4484-90be-2df15d1322ab"
60+
),
61+
"category" to "b6eea851-5ae5-40ed-8fdd-5e1a655a879c",
62+
"level" to "debug"
63+
)
64+
val actual = Breadcrumb.fromMap(map, SentryOptions())
65+
val expected = fixture.getSut()
66+
67+
assertEquals(expected.timestamp, actual?.timestamp)
68+
assertEquals(expected.message, actual?.message)
69+
assertEquals(expected.type, actual?.type)
70+
assertEquals(expected.data, actual?.data)
71+
assertEquals(expected.category, actual?.category)
72+
assertEquals(expected.level, actual?.level)
73+
}
74+
75+
@Test
76+
fun deserializeDataWithInvalidKey() {
77+
val map: Map<String, Any?> = mapOf(
78+
"data" to mapOf(
79+
123 to 456 // Invalid key type
80+
)
81+
)
82+
val actual = Breadcrumb.fromMap(map, SentryOptions())
83+
assertTrue(actual.data.isEmpty())
84+
}
85+
86+
@Test
87+
fun deserializeDataWithNullKey() {
88+
val map: Map<String, Any?> = mapOf(
89+
"data" to mapOf(
90+
"null" to null
91+
)
92+
)
93+
val actual = Breadcrumb.fromMap(map, SentryOptions())
94+
assertEquals(null, actual?.data?.get("null"))
95+
}
96+
5097
// Helper
5198

5299
private fun sanitizedFile(path: String): String {

0 commit comments

Comments
 (0)