Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create User and Breadcrumb from map #2614

Merged
merged 21 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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