From aef95757af512d4971cf291649c27ae3824ba090 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 9 Jun 2026 13:56:45 +0200 Subject: [PATCH 1/2] fix: Support geo in SentryGeo initWithDictionary Add initWithDictionary: to SentryGeo and handle the "geo" key in SentryUser.initWithDictionary: so that geo data is properly deserialized instead of landing in the unknown dict and overwriting the serialized geo. --- Sources/Sentry/SentryGeo.m | 21 ++++++++++++++ Sources/Sentry/SentryUser.m | 4 ++- Sources/Sentry/include/SentryGeo+Private.h | 16 ++++++++++ .../Protocol/SentryUserTests.swift | 29 ++++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Sources/Sentry/include/SentryGeo+Private.h diff --git a/Sources/Sentry/SentryGeo.m b/Sources/Sentry/SentryGeo.m index 7b9f905584c..bf4c18b1c32 100644 --- a/Sources/Sentry/SentryGeo.m +++ b/Sources/Sentry/SentryGeo.m @@ -6,6 +6,27 @@ @implementation SentryGeo +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + if (self = [super init]) { + id city = dictionary[@"city"]; + if ([city isKindOfClass:[NSString class]]) { + self.city = city; + } + + id countryCode = dictionary[@"country_code"]; + if ([countryCode isKindOfClass:[NSString class]]) { + self.countryCode = countryCode; + } + + id region = dictionary[@"region"]; + if ([region isKindOfClass:[NSString class]]) { + self.region = region; + } + } + return self; +} + - (id)copyWithZone:(nullable NSZone *)zone { SentryGeo *copy = [[[self class] allocWithZone:zone] init]; diff --git a/Sources/Sentry/SentryUser.m b/Sources/Sentry/SentryUser.m index 4e12399231e..5a2c4e74f38 100644 --- a/Sources/Sentry/SentryUser.m +++ b/Sources/Sentry/SentryUser.m @@ -1,5 +1,5 @@ #import "SentryUser.h" -#import "SentryGeo.h" +#import "SentryGeo+Private.h" #import "SentryInternalDefines.h" #import "SentrySanitizerUtils.h" @@ -33,6 +33,8 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary self.ipAddress = value; } else if ([key isEqualToString:@"data"] && isDictionary) { self.data = value; + } else if ([key isEqualToString:@"geo"] && isDictionary) { + self.geo = [[SentryGeo alloc] initWithDictionary:value]; } else { unknown[key] = value; } diff --git a/Sources/Sentry/include/SentryGeo+Private.h b/Sources/Sentry/include/SentryGeo+Private.h new file mode 100644 index 00000000000..7d26d3d8261 --- /dev/null +++ b/Sources/Sentry/include/SentryGeo+Private.h @@ -0,0 +1,16 @@ +#if __has_include() +# import +#else +# import "SentryGeo.h" +#endif + +@interface SentryGeo () + +/** + * Initializes a SentryGeo from a dictionary. + * @param dictionary The dictionary containing geo data. + * @return The SentryGeo. + */ +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; + +@end diff --git a/Tests/SentryTests/Protocol/SentryUserTests.swift b/Tests/SentryTests/Protocol/SentryUserTests.swift index f2d13f98ee7..53d7d9b32ec 100644 --- a/Tests/SentryTests/Protocol/SentryUserTests.swift +++ b/Tests/SentryTests/Protocol/SentryUserTests.swift @@ -12,17 +12,44 @@ class SentryUserTests: XCTestCase { "data": [ "fixture-key": "fixture-value" ], + "geo": [ + "city": "San Francisco", + "country_code": "US", + "region": "California" + ], "foo": "bar" // Unknown ] let user = PrivateSentrySDKOnly.user(with: dict) - + XCTAssertEqual(user.userId, "fixture-id") XCTAssertEqual(user.email, "fixture-email") XCTAssertEqual(user.username, "fixture-username") XCTAssertEqual(user.ipAddress, "fixture-ip_address") XCTAssertEqual(user.data?["fixture-key"] as? String, "fixture-value") + XCTAssertEqual(user.geo?.city, "San Francisco") + XCTAssertEqual(user.geo?.countryCode, "US") + XCTAssertEqual(user.geo?.region, "California") XCTAssertEqual(user.value(forKey: "unknown") as? NSDictionary, ["foo": "bar"]) } + + func testInitWithDictionary_GeoRoundTrip() { + let dict: [AnyHashable: Any] = [ + "id": "user-1", + "geo": [ + "city": "San Francisco", + "country_code": "US", + "region": "California" + ] + ] + let user = PrivateSentrySDKOnly.user(with: dict) + let serialized = user.serialize() + + let geo = serialized["geo"] as? [String: Any] + XCTAssertEqual(geo?["city"] as? String, "San Francisco") + XCTAssertEqual(geo?["country_code"] as? String, "US") + XCTAssertEqual(geo?["region"] as? String, "California") + XCTAssertNil(serialized["unknown"]) + } func testSerializationWithAllProperties() throws { let user = try XCTUnwrap(TestData.user.copy() as? User) From 62f8b36d96cca8a15909a07d6c928ed661ef9999 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 9 Jun 2026 14:44:36 +0200 Subject: [PATCH 2/2] test: Add unit tests for SentryGeo initWithDictionary Cover all properties, partial input, empty dict, wrong types, and serialize round-trip. --- .../SentryTests/Protocol/SentryGeoTests.swift | 57 +++++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 2 files changed, 58 insertions(+) diff --git a/Tests/SentryTests/Protocol/SentryGeoTests.swift b/Tests/SentryTests/Protocol/SentryGeoTests.swift index 9c231de4ba0..1e9087ff008 100644 --- a/Tests/SentryTests/Protocol/SentryGeoTests.swift +++ b/Tests/SentryTests/Protocol/SentryGeoTests.swift @@ -3,6 +3,63 @@ import XCTest class SentryGeoTests: XCTestCase { + func testInitWithDictionary_AllProperties() throws { + let dict: [String: Any] = [ + "city": "San Francisco", + "country_code": "US", + "region": "California" + ] + let geo = try XCTUnwrap(Geo(dictionary: dict)) + + XCTAssertEqual(geo.city, "San Francisco") + XCTAssertEqual(geo.countryCode, "US") + XCTAssertEqual(geo.region, "California") + } + + func testInitWithDictionary_PartialProperties() throws { + let geo = try XCTUnwrap(Geo(dictionary: ["city": "Berlin"])) + + XCTAssertEqual(geo.city, "Berlin") + XCTAssertNil(geo.countryCode) + XCTAssertNil(geo.region) + } + + func testInitWithDictionary_EmptyDict() throws { + let geo = try XCTUnwrap(Geo(dictionary: [:])) + + XCTAssertNil(geo.city) + XCTAssertNil(geo.countryCode) + XCTAssertNil(geo.region) + } + + func testInitWithDictionary_WrongTypes() throws { + let dict: [String: Any] = [ + "city": 123, + "country_code": ["not", "a", "string"], + "region": true + ] + let geo = try XCTUnwrap(Geo(dictionary: dict)) + + XCTAssertNil(geo.city) + XCTAssertNil(geo.countryCode) + XCTAssertNil(geo.region) + } + + func testInitWithDictionary_RoundTrip() throws { + let dict: [String: Any] = [ + "city": "San Francisco", + "country_code": "US", + "region": "California" + ] + let geo = try XCTUnwrap(Geo(dictionary: dict)) + let serialized = geo.serialize() + + XCTAssertEqual(serialized["city"] as? String, "San Francisco") + XCTAssertEqual(serialized["country_code"] as? String, "US") + XCTAssertEqual(serialized["region"] as? String, "California") + XCTAssertEqual(serialized.count, 3) + } + func testSerializationWithAllProperties() throws { let geo = try XCTUnwrap(TestData.geo.copy() as? Geo) let actual = geo.serialize() diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index e542bad1649..b1e22263023 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -1,4 +1,5 @@ #import "SentryDefines.h" +#import "SentryGeo+Private.h" #import "SentryLaunchProfiling+Tests.h"