Skip to content

Commit

Permalink
Create User and Breadcrumb from map (#2614)
Browse files Browse the repository at this point in the history
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
denrase and getsentry-bot authored Apr 17, 2023
1 parent e5871b9 commit c9d0787
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Attach Trace Context when an ANR is detected (ANRv1) ([#2583](https://github.com/getsentry/sentry-java/pull/2583))
- Make log4j2 integration compatible with log4j 3.0 ([#2634](https://github.com/getsentry/sentry-java/pull/2634))
- Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
- Create `User` and `Breadcrumb` from map ([#2614](https://github.com/getsentry/sentry-java/pull/2614))

### Fixes

Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public final class io/sentry/Breadcrumb : io/sentry/JsonSerializable, io/sentry/
public fun <init> (Ljava/util/Date;)V
public static fun debug (Ljava/lang/String;)Lio/sentry/Breadcrumb;
public static fun error (Ljava/lang/String;)Lio/sentry/Breadcrumb;
public static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/Breadcrumb;
public fun getCategory ()Ljava/lang/String;
public fun getData ()Ljava/util/Map;
public fun getData (Ljava/lang/String;)Ljava/lang/Object;
Expand Down Expand Up @@ -641,6 +642,7 @@ public final class io/sentry/JsonObjectDeserializer {

public final class io/sentry/JsonObjectReader : io/sentry/vendor/gson/stream/JsonReader {
public fun <init> (Ljava/io/Reader;)V
public static fun dateOrNull (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/util/Date;
public fun nextBooleanOrNull ()Ljava/lang/Boolean;
public fun nextDateOrNull (Lio/sentry/ILogger;)Ljava/util/Date;
public fun nextDoubleOrNull ()Ljava/lang/Double;
Expand Down Expand Up @@ -2891,6 +2893,7 @@ public final class io/sentry/protocol/Device$JsonKeys {
public final class io/sentry/protocol/Geo : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public fun <init> ()V
public fun <init> (Lio/sentry/protocol/Geo;)V
public static fun fromMap (Ljava/util/Map;)Lio/sentry/protocol/Geo;
public fun getCity ()Ljava/lang/String;
public fun getCountryCode ()Ljava/lang/String;
public fun getRegion ()Ljava/lang/String;
Expand Down Expand Up @@ -3583,6 +3586,7 @@ public final class io/sentry/protocol/TransactionNameSource : java/lang/Enum {
public final class io/sentry/protocol/User : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public fun <init> ()V
public fun <init> (Lio/sentry/protocol/User;)V
public static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/protocol/User;
public fun getData ()Ljava/util/Map;
public fun getEmail ()Ljava/lang/String;
public fun getGeo ()Lio/sentry/protocol/Geo;
Expand Down
85 changes: 85 additions & 0 deletions sentry/src/main/java/io/sentry/Breadcrumb.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,91 @@ public Breadcrumb(final @NotNull Date timestamp) {
this.level = breadcrumb.level;
}

/**
* Creates breadcrumb from a map.
*
* @param map - The breadcrumb data as map
* @param options - the sentry options
* @return the breadcrumb
*/
@SuppressWarnings("unchecked")
public static Breadcrumb fromMap(
@NotNull Map<String, Object> map, @NotNull SentryOptions options) {

@NotNull Date timestamp = DateUtils.getCurrentDateTime();
String message = null;
String type = null;
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
String category = null;
SentryLevel level = null;
Map<String, Object> unknown = null;

for (Map.Entry<String, Object> entry : map.entrySet()) {
Object value = entry.getValue();
switch (entry.getKey()) {
case JsonKeys.TIMESTAMP:
if (value instanceof String) {
Date deserializedDate =
JsonObjectReader.dateOrNull((String) value, options.getLogger());
if (deserializedDate != null) {
timestamp = deserializedDate;
}
}
break;
case JsonKeys.MESSAGE:
message = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.TYPE:
type = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.DATA:
final Map<Object, Object> untypedData =
(value instanceof Map) ? (Map<Object, Object>) value : null;
if (untypedData != null) {
for (Map.Entry<Object, Object> dataEntry : untypedData.entrySet()) {
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
data.put((String) dataEntry.getKey(), dataEntry.getValue());
} else {
options
.getLogger()
.log(SentryLevel.WARNING, "Invalid key or null value in data map.");
}
}
}
break;
case JsonKeys.CATEGORY:
category = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.LEVEL:
String levelString = (value instanceof String) ? (String) value : null;
if (levelString != null) {
try {
level = SentryLevel.valueOf(levelString.toUpperCase(Locale.ROOT));
} catch (Exception exception) {
// Stub
}
}
break;
default:
if (unknown == null) {
unknown = new ConcurrentHashMap<>();
}
unknown.put(entry.getKey(), entry.getValue());
break;
}
}

final Breadcrumb breadcrumb = new Breadcrumb(timestamp);
breadcrumb.message = message;
breadcrumb.type = type;
breadcrumb.data = data;
breadcrumb.category = category;
breadcrumb.level = level;

breadcrumb.setUnknown(unknown);
return breadcrumb;
}

/**
* Creates HTTP breadcrumb.
*
Expand Down
8 changes: 7 additions & 1 deletion sentry/src/main/java/io/sentry/JsonObjectReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ public void nextUnknown(ILogger logger, Map<String, Object> unknown, String name
nextNull();
return null;
}
String dateString = nextString();
return JsonObjectReader.dateOrNull(nextString(), logger);
}

public static @Nullable Date dateOrNull(@Nullable String dateString, ILogger logger) {
if (dateString == null) {
return null;
}
try {
return DateUtils.getDateTime(dateString);
} catch (Exception e) {
Expand Down
27 changes: 27 additions & 0 deletions sentry/src/main/java/io/sentry/protocol/Geo.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ public Geo(final @NotNull Geo geo) {
this.region = geo.region;
}

/**
* Creates geo from a map.
*
* @param map - The geo data as map
* @return the geo
*/
public static Geo fromMap(@NotNull Map<String, Object> map) {
final Geo geo = new Geo();
for (Map.Entry<String, Object> entry : map.entrySet()) {
Object value = entry.getValue();
switch (entry.getKey()) {
case JsonKeys.CITY:
geo.city = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.COUNTRY_CODE:
geo.countryCode = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.REGION:
geo.region = (value instanceof String) ? (String) value : null;
break;
default:
break;
}
}
return geo;
}

/**
* Gets the human readable city name.
*
Expand Down
100 changes: 100 additions & 0 deletions sentry/src/main/java/io/sentry/protocol/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.sentry.JsonObjectWriter;
import io.sentry.JsonSerializable;
import io.sentry.JsonUnknown;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.util.CollectionUtils;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
Expand Down Expand Up @@ -65,6 +67,104 @@ public User(final @NotNull User user) {
this.unknown = CollectionUtils.newConcurrentHashMap(user.unknown);
}

/**
* Creates user from a map.
*
* <p>The values `data` and `value` expect a {@code Map<String, String>} type. If other object
* types are in the map `toString()` will be called on them.
*
* @param map - The user data as map
* @param options - the sentry options
* @return the user
*/
@SuppressWarnings("unchecked")
public static User fromMap(@NotNull Map<String, Object> map, @NotNull SentryOptions options) {
final User user = new User();
Map<String, Object> unknown = null;

for (Map.Entry<String, Object> entry : map.entrySet()) {
Object value = entry.getValue();
switch (entry.getKey()) {
case JsonKeys.EMAIL:
user.email = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.ID:
user.id = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.USERNAME:
user.username = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.SEGMENT:
user.segment = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.IP_ADDRESS:
user.ipAddress = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.NAME:
user.name = (value instanceof String) ? (String) value : null;
break;
case JsonKeys.GEO:
final Map<Object, Object> geo =
(value instanceof Map) ? (Map<Object, Object>) value : null;
if (geo != null) {
final ConcurrentHashMap<String, Object> geoData = new ConcurrentHashMap<>();
for (Map.Entry<Object, Object> geoEntry : geo.entrySet()) {
if (geoEntry.getKey() instanceof String && geoEntry.getValue() != null) {
geoData.put((String) geoEntry.getKey(), geoEntry.getValue());
} else {
options.getLogger().log(SentryLevel.WARNING, "Invalid key type in gep map.");
}
}
user.geo = Geo.fromMap(geoData);
}
break;
case JsonKeys.DATA:
final Map<Object, Object> data =
(value instanceof Map) ? (Map<Object, Object>) value : null;
if (data != null) {
final ConcurrentHashMap<String, String> userData = new ConcurrentHashMap<>();
for (Map.Entry<Object, Object> dataEntry : data.entrySet()) {
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
userData.put((String) dataEntry.getKey(), dataEntry.getValue().toString());
} else {
options
.getLogger()
.log(SentryLevel.WARNING, "Invalid key or null value in data map.");
}
}
user.data = userData;
}
break;
case JsonKeys.OTHER:
final Map<Object, Object> other =
(value instanceof Map) ? (Map<Object, Object>) value : null;
// restore `other` from legacy JSON
if (other != null && (user.data == null || user.data.isEmpty())) {
final ConcurrentHashMap<String, String> userData = new ConcurrentHashMap<>();
for (Map.Entry<Object, Object> otherEntry : other.entrySet()) {
if (otherEntry.getKey() instanceof String && otherEntry.getValue() != null) {
userData.put((String) otherEntry.getKey(), otherEntry.getValue().toString());
} else {
options
.getLogger()
.log(SentryLevel.WARNING, "Invalid key or null value in other map.");
}
}
user.data = userData;
}
break;
default:
if (unknown == null) {
unknown = new ConcurrentHashMap<>();
}
unknown.put(entry.getKey(), entry.getValue());
break;
}
}
user.unknown = unknown;
return user;
}

/**
* Gets the e-mail address of the user.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import io.sentry.JsonObjectReader
import io.sentry.JsonObjectWriter
import io.sentry.JsonSerializable
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import org.junit.Test
import org.mockito.kotlin.mock
import java.io.StringReader
import java.io.StringWriter
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class BreadcrumbSerializationTest {

Expand Down Expand Up @@ -47,6 +49,51 @@ class BreadcrumbSerializationTest {
assertEquals(expectedJson, actualJson)
}

@Test
fun deserializeFromMap() {
val map: Map<String, Any?> = mapOf(
"timestamp" to "2009-11-16T01:08:47.000Z",
"message" to "46f233c0-7c2d-488a-b05a-7be559173e16",
"type" to "ace57e2e-305e-4048-abf0-6c8538ea7bf4",
"data" to mapOf(
"6607d106-d426-462b-af74-f29fce978e48" to "149bb94a-1387-4484-90be-2df15d1322ab"
),
"category" to "b6eea851-5ae5-40ed-8fdd-5e1a655a879c",
"level" to "debug"
)
val actual = Breadcrumb.fromMap(map, SentryOptions())
val expected = fixture.getSut()

assertEquals(expected.timestamp, actual?.timestamp)
assertEquals(expected.message, actual?.message)
assertEquals(expected.type, actual?.type)
assertEquals(expected.data, actual?.data)
assertEquals(expected.category, actual?.category)
assertEquals(expected.level, actual?.level)
}

@Test
fun deserializeDataWithInvalidKey() {
val map: Map<String, Any?> = mapOf(
"data" to mapOf(
123 to 456 // Invalid key type
)
)
val actual = Breadcrumb.fromMap(map, SentryOptions())
assertTrue(actual.data.isEmpty())
}

@Test
fun deserializeDataWithNullKey() {
val map: Map<String, Any?> = mapOf(
"data" to mapOf(
"null" to null
)
)
val actual = Breadcrumb.fromMap(map, SentryOptions())
assertEquals(null, actual?.data?.get("null"))
}

// Helper

private fun sanitizedFile(path: String): String {
Expand Down
Loading

0 comments on commit c9d0787

Please sign in to comment.