Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package io.sentry.rnsentryandroidtester

import com.facebook.react.bridge.JavaOnlyMap
import io.sentry.ILogger
import io.sentry.SentryLevel
import io.sentry.react.RNSentryBreadcrumb
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito

@RunWith(JUnit4::class)
class RNSentryBreadcrumbTest {
private val logger = Mockito.mock(ILogger::class.java)

@Test
fun generatesSentryBreadcrumbFromMap() {
val testData =
Expand All @@ -32,7 +37,7 @@ class RNSentryBreadcrumbTest {
"data",
testData,
)
val actual = RNSentryBreadcrumb.fromMap(map)
val actual = RNSentryBreadcrumb.fromMap(map, logger)
assertEquals(SentryLevel.ERROR, actual.level)
assertEquals("testCategory", actual.category)
assertEquals("testOrigin", actual.origin)
Expand All @@ -41,14 +46,36 @@ class RNSentryBreadcrumbTest {
assertEquals(testData.toHashMap(), actual.data)
}

@Test
fun defaultsToInfoLevelWhenMissing() {
val map =
JavaOnlyMap.of(
"message",
"testMessage",
)
val actual = RNSentryBreadcrumb.fromMap(map, logger)
assertEquals(SentryLevel.INFO, actual.level)
}

@Test
fun setsTimestamp() {
val map =
JavaOnlyMap.of(
"message",
"testMessage",
)
val actual = RNSentryBreadcrumb.fromMap(map, logger)
assertNotNull(actual.timestamp)
}

@Test
fun reactNativeForMissingOrigin() {
val map =
JavaOnlyMap.of(
"message",
"testMessage",
)
val actual = RNSentryBreadcrumb.fromMap(map)
val actual = RNSentryBreadcrumb.fromMap(map, logger)
assertEquals("testMessage", actual.message)
assertEquals("react-native", actual.origin)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ final class RNSentryBreadcrumbTests: XCTestCase {
XCTAssertEqual(actualCrumb!.origin, "someOrigin")
}

func testSetsTimestamp() {
let actualCrumb = RNSentryBreadcrumb.from([
"message": "testMessage"
])

XCTAssertNotNil(actualCrumb!.timestamp)
}

func testUsesInfoAsDefaultSentryLevel() {
let actualCrumb = RNSentryBreadcrumb.from([
"message": "testMessage"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@ - (void)testNullUser

- (void)testEmptyUser
{
SentryUser *expected = [[SentryUser alloc] init];
[expected setData:@{ }];

SentryUser *actual = [RNSentry userFrom:@{ } otherUserKeys:@{ }];
XCTAssertTrue([actual isEqualToUser:expected]);
XCTAssertNotNil(actual);
XCTAssertNil(actual.userId);
XCTAssertNil(actual.email);
XCTAssertNil(actual.username);
XCTAssertNil(actual.ipAddress);
XCTAssertEqualObjects(actual.data, @{ });
}

- (void)testInvalidUser
{
SentryUser *expected = [[SentryUser alloc] init];

SentryUser *actual = [RNSentry userFrom:@{
@"id" : @123,
@"ip_address" : @ { },
Expand All @@ -69,14 +69,16 @@ - (void)testInvalidUser
}
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
// initWithDictionary: ignores non-string values for known string fields
XCTAssertNotNil(actual);
XCTAssertNil(actual.userId);
XCTAssertNil(actual.email);
XCTAssertNil(actual.username);
XCTAssertNil(actual.ipAddress);
}

- (void)testPartiallyInvalidUser
{
SentryUser *expected = [[SentryUser alloc] init];
[expected setUserId:@"123"];

SentryUser *actual = [RNSentry userFrom:@{
@"id" : @"123",
@"ip_address" : @ { },
Expand All @@ -85,13 +87,15 @@ - (void)testPartiallyInvalidUser
}
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
XCTAssertNotNil(actual);
XCTAssertEqualObjects(actual.userId, @"123");
XCTAssertNil(actual.email);
XCTAssertNil(actual.username);
XCTAssertNil(actual.ipAddress);
}

- (void)testNullValuesUser
{
SentryUser *expected = [[SentryUser alloc] init];

SentryUser *actual = [RNSentry userFrom:@{
@"id" : [NSNull null],
@"ip_address" : [NSNull null],
Expand All @@ -100,7 +104,12 @@ - (void)testNullValuesUser
}
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
// initWithDictionary: ignores non-string values for known string fields
XCTAssertNotNil(actual);
XCTAssertNil(actual.userId);
XCTAssertNil(actual.email);
XCTAssertNil(actual.username);
XCTAssertNil(actual.ipAddress);
}

- (void)testUserWithGeo
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package io.sentry.react;

import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import io.sentry.Breadcrumb;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.util.MapObjectReader;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -36,59 +40,41 @@ public static String getCurrentScreenFrom(ReadableMap from) {
}

@NotNull
public static Breadcrumb fromMap(ReadableMap from) {
final @NotNull Breadcrumb breadcrumb = new Breadcrumb();

if (from.hasKey("message")) {
breadcrumb.setMessage(from.getString("message"));
}

if (from.hasKey("type")) {
breadcrumb.setType(from.getString("type"));
}

if (from.hasKey("category")) {
breadcrumb.setCategory(from.getString("category"));
}

if (from.hasKey("origin")) {
breadcrumb.setOrigin(from.getString("origin"));
} else {
breadcrumb.setOrigin("react-native");
}
public static Breadcrumb fromMap(ReadableMap from, @NotNull ILogger logger) {
try {
final @NotNull MapObjectReader reader = new MapObjectReader(toDeepHashMap(from));
final @NotNull Breadcrumb breadcrumb =
new Breadcrumb.Deserializer().deserialize(reader, logger);

if (from.hasKey("level")) {
switch (from.getString("level")) {
case "fatal":
breadcrumb.setLevel(SentryLevel.FATAL);
break;
case "warning":
breadcrumb.setLevel(SentryLevel.WARNING);
break;
case "debug":
breadcrumb.setLevel(SentryLevel.DEBUG);
break;
case "error":
breadcrumb.setLevel(SentryLevel.ERROR);
break;
case "info":
default:
breadcrumb.setLevel(SentryLevel.INFO);
break;
if (breadcrumb.getLevel() == null) {
breadcrumb.setLevel(SentryLevel.INFO);
}
if (breadcrumb.getOrigin() == null) {
breadcrumb.setOrigin("react-native");
}

return breadcrumb;
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Failed to deserialize breadcrumb from map.", e);
final Breadcrumb fallback = new Breadcrumb();
fallback.setOrigin("react-native");
return fallback;
}
}

if (from.hasKey("data")) {
final ReadableMap data = from.getMap("data");
for (final Map.Entry<String, Object> entry : data.toHashMap().entrySet()) {
final Object value = entry.getValue();
// data is ConcurrentHashMap and can't have null values
if (value != null) {
breadcrumb.setData(entry.getKey(), entry.getValue());
@NotNull
static Map<String, Object> toDeepHashMap(@NotNull ReadableMap from) {
final Map<String, Object> map = from.toHashMap();
final ReadableMapKeySetIterator iterator = from.keySetIterator();
while (iterator.hasNextKey()) {
final String key = iterator.nextKey();
if (from.getType(key) == ReadableType.Map) {
final ReadableMap nested = from.getMap(key);
if (nested != null) {
map.put(key, toDeepHashMap(nested));
}
}
}

return breadcrumb;
return map;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.profilemeasurements.ProfileMeasurement;
import io.sentry.profilemeasurements.ProfileMeasurementValue;
import io.sentry.protocol.Geo;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
Expand All @@ -62,6 +61,7 @@
import io.sentry.util.FileUtils;
import io.sentry.util.JsonSerializationUtils;
import io.sentry.util.LoadClass;
import io.sentry.util.MapObjectReader;
import io.sentry.vendor.Base64;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
Expand Down Expand Up @@ -575,68 +575,43 @@ public void setUser(final ReadableMap userKeys, final ReadableMap userDataKeys)
if (userKeys == null && userDataKeys == null) {
scope.setUser(null);
} else {
User userInstance = new User();

if (userKeys != null) {
if (userKeys.hasKey("email")) {
userInstance.setEmail(userKeys.getString("email"));
}

if (userKeys.hasKey("id")) {
userInstance.setId(userKeys.getString("id"));
}

if (userKeys.hasKey("username")) {
userInstance.setUsername(userKeys.getString("username"));
}

if (userKeys.hasKey("ip_address")) {
userInstance.setIpAddress(userKeys.getString("ip_address"));
}

if (userKeys.hasKey("geo")) {
ReadableMap geoMap = userKeys.getMap("geo");
if (geoMap != null) {
Geo geoData = new Geo();
if (geoMap.hasKey("city")) {
geoData.setCity(geoMap.getString("city"));
}
if (geoMap.hasKey("country_code")) {
geoData.setCountryCode(geoMap.getString("country_code"));
Comment thread
antonis marked this conversation as resolved.
try {
final MapObjectReader reader =
new MapObjectReader(
userKeys != null
? RNSentryBreadcrumb.toDeepHashMap(userKeys)
: new HashMap<>());
final User userInstance = new User.Deserializer().deserialize(reader, logger);

if (userDataKeys != null) {
Map<String, String> userDataMap = new HashMap<>();
ReadableMapKeySetIterator it = userDataKeys.keySetIterator();
while (it.hasNextKey()) {
String key = it.nextKey();
String value = userDataKeys.getString(key);

// other is ConcurrentHashMap and can't have null values
if (value != null) {
userDataMap.put(key, value);
}
if (geoMap.hasKey("region")) {
geoData.setRegion(geoMap.getString("region"));
}
userInstance.setGeo(geoData);
}
}
}

if (userDataKeys != null) {
Map<String, String> userDataMap = new HashMap<>();
ReadableMapKeySetIterator it = userDataKeys.keySetIterator();
while (it.hasNextKey()) {
String key = it.nextKey();
String value = userDataKeys.getString(key);

// other is ConcurrentHashMap and can't have null values
if (value != null) {
userDataMap.put(key, value);
}
userInstance.setData(userDataMap);
}

Comment thread
antonis marked this conversation as resolved.
userInstance.setData(userDataMap);
scope.setUser(userInstance);
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Failed to deserialize user from map.", e);
scope.setUser(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userData error clears scope user

Medium Severity

The new setUser try/catch wraps both User.Deserializer and userDataKeys parsing. If deserialization succeeds but userDataKeys.getString throws on a non-string value, the catch calls scope.setUser(null), wiping the scope user instead of leaving the previous user or applying the deserialized core fields.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d8dfaa8. Configure here.

}

scope.setUser(userInstance);
}
});
}

public void addBreadcrumb(final ReadableMap breadcrumb) {
Sentry.configureScope(
scope -> {
scope.addBreadcrumb(RNSentryBreadcrumb.fromMap(breadcrumb));
scope.addBreadcrumb(RNSentryBreadcrumb.fromMap(breadcrumb, logger));

final @Nullable String screen = RNSentryBreadcrumb.getCurrentScreenFrom(breadcrumb);
if (screen != null) {
Expand Down
Loading
Loading