Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
- Adds GraphQL integration ([#5299](https://github.com/getsentry/sentry-react-native/pull/5299))
- Adds Supabase integration ([#5296](https://github.com/getsentry/sentry-react-native/pull/5296))

### Fix

- Sync `user.geo` from `SetUser` to the native layer ([#5302](https://github.com/getsentry/sentry-react-native/pull/5302))

### Dependencies

- Bump Bundler Plugins from v4.4.0 to v4.5.0 ([#5283](https://github.com/getsentry/sentry-react-native/pull/5283))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,90 @@ class RNSentryModuleImplTest {
val regex = Regex(options.ignoredErrors!![0].filterString)
assertTrue(regex.matches("Error*WithStar"))
}

@Test
fun `setUser with geo data creates user with correct geo properties`() {
val userKeys =
JavaOnlyMap.of(
"id",
"123",
"email",
"test@example.com",
"username",
"testuser",
"geo",
JavaOnlyMap.of(
"city",
"San Francisco",
"country_code",
"US",
"region",
"California",
),
)
val userDataKeys = JavaOnlyMap.of("customField", "customValue")

module.setUser(userKeys, userDataKeys)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the assertions are missing on the added tests. An alternative approach might be to add Android tests checking the scope like we do for breadcrumbs

}

@Test
fun `setUser with partial geo data creates user with available geo properties`() {
val userKeys =
JavaOnlyMap.of(
"id",
"123",
"geo",
JavaOnlyMap.of(
"city",
"New York",
"country_code",
"US",
),
)
val userDataKeys = JavaOnlyMap.of()

module.setUser(userKeys, userDataKeys)
}

@Test
fun `setUser with empty geo data handles empty geo object`() {
val userKeys =
JavaOnlyMap.of(
"id",
"123",
"geo",
JavaOnlyMap.of(),
)
val userDataKeys = JavaOnlyMap.of()

module.setUser(userKeys, userDataKeys)
}

@Test
fun `setUser with null geo data handles null geo gracefully`() {
val userKeys =
JavaOnlyMap.of(
"id",
"123",
"geo",
null,
)
val userDataKeys = JavaOnlyMap.of()

module.setUser(userKeys, userDataKeys)
}

@Test
fun `setUser with invalid geo data handles non-map geo gracefully`() {
val userKeys =
JavaOnlyMap.of(
"id",
"123",
"geo",
"invalid_geo_data",
)
val userDataKeys = JavaOnlyMap.of()

module.setUser(userKeys, userDataKeys)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
#import <RNSentry/RNSentry.h>

@class SentryOptions;
@class SentryUser;

@interface SentrySDKInternal (PrivateTests)

+ (nullable SentryOptions *)options;
@end

@interface RNSentry (PrivateTests)

+ (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
otherUserKeys:(NSDictionary *)userDataKeys;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -859,4 +859,90 @@ - (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmen
@"enableSessionReplayInUnreliableEnvironment should be disabled");
}

- (void)testCreateUserWithGeoDataCreatesSentryGeoObject
{
NSDictionary *userKeys = @{
@"id" : @"123",
@"email" : @"test@example.com",
@"username" : @"testuser",
@"geo" :
@ { @"city" : @"San Francisco", @"country_code" : @"US", @"region" : @"California" }
};

NSDictionary *userDataKeys = @{ @"customField" : @"customValue" };

SentryUser *user = [RNSentry userFrom:userKeys otherUserKeys:userDataKeys];

XCTAssertNotNil(user, @"User should not be nil");
XCTAssertEqual(user.userId, @"123", @"User ID should match");
XCTAssertEqual(user.email, @"test@example.com", @"Email should match");
XCTAssertEqual(user.username, @"testuser", @"Username should match");

// Test that geo data is properly converted to SentryGeo object
XCTAssertNotNil(user.geo, @"Geo should not be nil");
XCTAssertTrue([user.geo isKindOfClass:[SentryGeo class]], @"Geo should be SentryGeo object");
XCTAssertEqual(user.geo.city, @"San Francisco", @"City should match");
XCTAssertEqual(user.geo.countryCode, @"US", @"Country code should match");
XCTAssertEqual(user.geo.region, @"California", @"Region should match");

// Test that custom data is preserved
XCTAssertNotNil(user.data, @"User data should not be nil");
XCTAssertEqual(user.data[@"customField"], @"customValue", @"Custom field should be preserved");
}

- (void)testCreateUserWithPartialGeoDataCreatesSentryGeoObject
{
NSDictionary *userKeys =
@{ @"id" : @"456", @"geo" : @ { @"city" : @"New York", @"country_code" : @"US" } };

NSDictionary *userDataKeys = @{};

SentryUser *user = [RNSentry userFrom:userKeys otherUserKeys:userDataKeys];

XCTAssertNotNil(user, @"User should not be nil");
XCTAssertEqual(user.userId, @"456", @"User ID should match");

// Test that partial geo data is properly converted to SentryGeo object
XCTAssertNotNil(user.geo, @"Geo should not be nil");
XCTAssertTrue([user.geo isKindOfClass:[SentryGeo class]], @"Geo should be SentryGeo object");
XCTAssertEqual(user.geo.city, @"New York", @"City should match");
XCTAssertEqual(user.geo.countryCode, @"US", @"Country code should match");
XCTAssertNil(user.geo.region, @"Region should be nil when not provided");
}

- (void)testCreateUserWithEmptyGeoDataCreatesSentryGeoObject
{
NSDictionary *userKeys = @{ @"id" : @"789", @"geo" : @ {} };

NSDictionary *userDataKeys = @{};

SentryUser *user = [RNSentry userFrom:userKeys otherUserKeys:userDataKeys];

XCTAssertNotNil(user, @"User should not be nil");
XCTAssertEqual(user.userId, @"789", @"User ID should match");

// Test that empty geo data is properly converted to SentryGeo object
XCTAssertNotNil(user.geo, @"Geo should not be nil");
XCTAssertTrue([user.geo isKindOfClass:[SentryGeo class]], @"Geo should be SentryGeo object");
XCTAssertNil(user.geo.city, @"City should be nil when not provided");
XCTAssertNil(user.geo.countryCode, @"Country code should be nil when not provided");
XCTAssertNil(user.geo.region, @"Region should be nil when not provided");
}

- (void)testCreateUserWithoutGeoDataDoesNotCreateGeoObject
{
NSDictionary *userKeys = @{ @"id" : @"999", @"email" : @"test@example.com" };

NSDictionary *userDataKeys = @{};

SentryUser *user = [RNSentry userFrom:userKeys otherUserKeys:userDataKeys];

XCTAssertNotNil(user, @"User should not be nil");
XCTAssertEqual(user.userId, @"999", @"User ID should match");
XCTAssertEqual(user.email, @"test@example.com", @"Email should match");

// Test that no geo object is created when geo data is not provided
XCTAssertNil(user.geo, @"Geo should be nil when not provided");
}

@end
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "RNSentry+Test.h"
#import "RNSentryTests.h"
#import <Sentry/SentryGeo.h>
#import <XCTest/XCTest.h>

@interface RNSentryUserTests : XCTestCase
Expand Down Expand Up @@ -102,4 +103,73 @@ - (void)testNullValuesUser
XCTAssertTrue([actual isEqualToUser:expected]);
}

- (void)testUserWithGeo
{
SentryUser *expected = [SentryUser alloc];
[expected setUserId:@"123"];
[expected setEmail:@"test@example.com"];
[expected setUsername:@"testuser"];

SentryGeo *expectedGeo = [SentryGeo alloc];
[expectedGeo setCity:@"San Francisco"];
[expectedGeo setCountryCode:@"US"];
[expectedGeo setRegion:@"California"];
[expected setGeo:expectedGeo];

SentryUser *actual = [RNSentry userFrom:@{
@"id" : @"123",
@"email" : @"test@example.com",
@"username" : @"testuser",
@"geo" :
@ { @"city" : @"San Francisco", @"country_code" : @"US", @"region" : @"California" }
}
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
}

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

SentryGeo *expectedGeo = [SentryGeo alloc];
[expectedGeo setCity:@"New York"];
[expectedGeo setCountryCode:@"US"];
[expected setGeo:expectedGeo];

SentryUser *actual = [RNSentry userFrom:@{
@"id" : @"123",
@"geo" : @ { @"city" : @"New York", @"country_code" : @"US" }
}
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
}

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

// Empty geo dictionary creates an empty SentryGeo object
SentryGeo *expectedGeo = [SentryGeo alloc];
[expected setGeo:expectedGeo];

SentryUser *actual = [RNSentry userFrom:@{ @"id" : @"123", @"geo" : @ {} } otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
}

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

SentryUser *actual = [RNSentry userFrom:@{ @"id" : @"123", @"geo" : @"invalid_geo_data" }
otherUserKeys:nil];

XCTAssertTrue([actual isEqualToUser:expected]);
}

@end
Binary file modified packages/core/android/libs/replay-stubs.jar
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think we don't need to change the stub.

Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.protocol.Geo;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryPackage;
Expand Down Expand Up @@ -722,6 +723,23 @@ public void setUser(final ReadableMap userKeys, final ReadableMap userDataKeys)
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"));
}
if (geoMap.hasKey("region")) {
geoData.setRegion(geoMap.getString("region"));
}
userInstance.setGeo(geoData);
}
}
}

if (userDataKeys != null) {
Expand Down
24 changes: 24 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#import <Sentry/SentryEvent.h>
#import <Sentry/SentryException.h>
#import <Sentry/SentryFormatter.h>
#import <Sentry/SentryGeo.h>
#import <Sentry/SentryOptions.h>
#import <Sentry/SentryOptionsInternal.h>
#import <Sentry/SentryScreenFrames.h>
Expand Down Expand Up @@ -722,6 +723,29 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
[userInstance setUsername:username];
}

id geo = [userKeys valueForKey:@"geo"];
if ([geo isKindOfClass:NSDictionary.class]) {
NSDictionary *geoDict = (NSDictionary *)geo;
SentryGeo *sentryGeo = [SentryGeo alloc];

id city = [geoDict valueForKey:@"city"];
if ([city isKindOfClass:NSString.class]) {
[sentryGeo setCity:city];
}

id countryCode = [geoDict valueForKey:@"country_code"];
if ([countryCode isKindOfClass:NSString.class]) {
[sentryGeo setCountryCode:countryCode];
}

id region = [geoDict valueForKey:@"region"];
if ([region isKindOfClass:NSString.class]) {
[sentryGeo setRegion:region];
}

[userInstance setGeo:sentryGeo];
}

if ([userDataKeys isKindOfClass:NSDictionary.class]) {
[userInstance setData:userDataKeys];
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,13 @@ export const NATIVE: SentryNativeWrapper = {
let userKeys = null;
let userDataKeys = null;
if (user) {
const { id, ip_address, email, username, ...otherKeys } = user;
// TODO: Update native impl to use geo
const requiredUser: Omit<RequiredKeysUser, 'geo'> = {
const { id, ip_address, email, username, geo, ...otherKeys } = user;
const requiredUser: RequiredKeysUser = {
id,
ip_address,
email,
username,
geo,
};
userKeys = this._serializeObject(requiredUser);
userDataKeys = this._serializeObject(otherKeys);
Expand Down
Loading
Loading