diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ef92a9d9a..f7e961100fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Features + +- Add `name` and `geo` to User (#2710) ### Fixes - Correctly track and send GPU frame render data in profiles (#2823) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 53fe837d691..7c108c5b6f8 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -692,6 +692,9 @@ 8ED2D28026A6581C00CA8329 /* NSURLProtocolSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ED2D27F26A6581C00CA8329 /* NSURLProtocolSwizzle.m */; }; 8ED3D306264DFE700049393B /* SwiftDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED3D305264DFE700049393B /* SwiftDescriptorTests.swift */; }; 8EE017A126704CD500470616 /* SentryUIViewControllerPerformanceTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA1ED0E2669152F00E62B98 /* SentryUIViewControllerPerformanceTrackerTests.swift */; }; + 9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9286059729A5098900F96038 /* SentryGeo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9286059629A5098900F96038 /* SentryGeo.m */; }; + 9286059929A50BAB00F96038 /* SentryGeoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9286059829A50BAA00F96038 /* SentryGeoTests.swift */; }; A2475E1325FB63A3007D9080 /* fishhook.h in Headers */ = {isa = PBXBuildFile; fileRef = A2475E1225FB63A3007D9080 /* fishhook.h */; }; A2475E1725FB63AF007D9080 /* SentryHook.h in Headers */ = {isa = PBXBuildFile; fileRef = A2475E1625FB63AF007D9080 /* SentryHook.h */; }; A2475E1B25FB63D7007D9080 /* SentryHook.c in Sources */ = {isa = PBXBuildFile; fileRef = A2475E1A25FB63D7007D9080 /* SentryHook.c */; }; @@ -1589,6 +1592,9 @@ 8ED2D27E26A6581C00CA8329 /* NSURLProtocolSwizzle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLProtocolSwizzle.h; sourceTree = ""; }; 8ED2D27F26A6581C00CA8329 /* NSURLProtocolSwizzle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLProtocolSwizzle.m; sourceTree = ""; }; 8ED3D305264DFE700049393B /* SwiftDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptorTests.swift; sourceTree = ""; }; + 9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = ""; }; + 9286059629A5098900F96038 /* SentryGeo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryGeo.m; sourceTree = ""; }; + 9286059829A50BAA00F96038 /* SentryGeoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryGeoTests.swift; sourceTree = ""; }; A2475E1225FB63A3007D9080 /* fishhook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fishhook.h; sourceTree = ""; }; A2475E1625FB63AF007D9080 /* SentryHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SentryHook.h; sourceTree = ""; }; A2475E1A25FB63D7007D9080 /* SentryHook.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SentryHook.c; sourceTree = ""; }; @@ -1836,6 +1842,8 @@ 639FCF9B1EBC7F9500778193 /* SentryThread.m */, 639FCFAA1EBC811400778193 /* SentryUser.h */, 639FCFAB1EBC811400778193 /* SentryUser.m */, + 9286059429A5096600F96038 /* SentryGeo.h */, + 9286059629A5098900F96038 /* SentryGeo.m */, 63B818F71EC34639002FDF4C /* SentryDebugMeta.h */, 63B818F81EC34639002FDF4C /* SentryDebugMeta.m */, 6360850B1ED2AFE100E8599E /* SentryBreadcrumb.h */, @@ -2507,6 +2515,7 @@ 7B6D98E824C6D336005502FA /* SentrySdkInfo+Equality.m */, 7B82D54824E2A2D400EE670F /* SentryIdTests.swift */, 7B04A9AA24EA5F8D00E710B1 /* SentryUserTests.swift */, + 9286059829A50BAA00F96038 /* SentryGeoTests.swift */, 7BB42EEF24F3B7B700D7B39A /* SentrySession+Equality.h */, 7BB42EF024F3B7B700D7B39A /* SentrySession+Equality.m */, 7B0A5451252311CE00A71716 /* SentryBreadcrumbTests.swift */, @@ -3519,6 +3528,7 @@ 63FE714F20DA4C1100CDBAE8 /* NSError+SentrySimpleConstructor.h in Headers */, 7BC5B6FA290BCDE500D99477 /* SentryHttpStatusCodeRange+Private.h in Headers */, 7B04A9AF24EAC02C00E710B1 /* SentryRetryAfterHeaderParser.h in Headers */, + 9286059529A5096600F96038 /* SentryGeo.h in Headers */, 7DC83100239826280043DD9A /* SentryIntegrationProtocol.h in Headers */, 7B98D7BC25FB607300C5A389 /* SentryWatchdogTerminationTracker.h in Headers */, 7BA61CB9247BC57B00C130A8 /* SentryCrashDefaultBinaryImageProvider.h in Headers */, @@ -3974,6 +3984,7 @@ 7B98D7CF25FB650F00C5A389 /* SentryWatchdogTerminationTrackingIntegration.m in Sources */, 8E5D38DD261D4A3E000D363D /* SentryPerformanceTrackingIntegration.m in Sources */, 7B4E23C2251A2C2B00060D68 /* SentrySessionCrashedHandler.m in Sources */, + 9286059729A5098900F96038 /* SentryGeo.m in Sources */, 7B42C48227E08F4B009B58C2 /* SentryDependencyContainer.m in Sources */, 7BA61E9225F21AF80008CAA2 /* SentryLogOutput.m in Sources */, 639FCFAD1EBC811400778193 /* SentryUser.m in Sources */, @@ -4076,6 +4087,7 @@ 0A5370A128A3EC2400B2DCDE /* SentryViewHierarchyTests.swift in Sources */, D8FFE50C2703DBB400607131 /* SwizzlingCallTests.swift in Sources */, 7BFAA6E7297AA16A00E7E02E /* SentryCrashMonitor_CppException_Tests.mm in Sources */, + 9286059929A50BAB00F96038 /* SentryGeoTests.swift in Sources */, D8B76B0828081461000A58C4 /* TestSentryScreenShot.swift in Sources */, A8AFFCD22907DA7600967CD7 /* SentryHttpStatusCodeRangeTests.swift in Sources */, 7BE2C7F8257000A4003B66C7 /* SentryTestIntegration.m in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index df484339c6d..afaa72b0986 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -19,6 +19,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; #import "SentryEvent.h" #import "SentryException.h" #import "SentryFrame.h" +#import "SentryGeo.h" #import "SentryHttpStatusCodeRange.h" #import "SentryHub.h" #import "SentryId.h" diff --git a/Sources/Sentry/Public/SentryDebugMeta.h b/Sources/Sentry/Public/SentryDebugMeta.h index b00133b26a4..0f41f76eae5 100644 --- a/Sources/Sentry/Public/SentryDebugMeta.h +++ b/Sources/Sentry/Public/SentryDebugMeta.h @@ -35,7 +35,7 @@ NS_SWIFT_NAME(DebugMeta) /** * Name of the image. Use @c codeFile when using "macho" as the @c type . */ -@property (nonatomic, copy) NSString *_Nullable name; +@property (nullable, nonatomic, copy) NSString *name; /** * The size of the image in virtual memory. If missing, Sentry will assume that the image spans up diff --git a/Sources/Sentry/Public/SentryGeo.h b/Sources/Sentry/Public/SentryGeo.h new file mode 100644 index 00000000000..ef63b737d49 --- /dev/null +++ b/Sources/Sentry/Public/SentryGeo.h @@ -0,0 +1,43 @@ +#import "SentryDefines.h" +#import "SentrySerializable.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Approximate geographical location of the end user or device. +/// +/// Example of serialized data: +/// { +/// "geo": { +/// "country_code": "US", +/// "city": "Ashburn", +/// "region": "San Francisco" +/// } +/// } +NS_SWIFT_NAME(Geo) +@interface SentryGeo : NSObject + +/** + * Optional: Human readable city name. + */ +@property (nullable, atomic, copy) NSString *city; + +/** + * Optional: Two-letter country code (ISO 3166-1 alpha-2). + */ +@property (nullable, atomic, copy) NSString *countryCode; + +/** + * Optional: Human readable region name or code. + */ +@property (nullable, atomic, copy) NSString *region; + +- (BOOL)isEqual:(id _Nullable)other; + +- (BOOL)isEqualToGeo:(SentryGeo *)geo; + +- (NSUInteger)hash; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryUser.h b/Sources/Sentry/Public/SentryUser.h index 497d6337feb..28651f1925c 100644 --- a/Sources/Sentry/Public/SentryUser.h +++ b/Sources/Sentry/Public/SentryUser.h @@ -1,8 +1,11 @@ #import "SentryDefines.h" +#import "SentryGeo.h" #import "SentrySerializable.h" NS_ASSUME_NONNULL_BEGIN +@class SentryGeo; + NS_SWIFT_NAME(User) @interface SentryUser : NSObject @@ -31,6 +34,16 @@ NS_SWIFT_NAME(User) */ @property (atomic, copy) NSString *_Nullable segment; +/** + * Optional: Human readable name + */ +@property (atomic, copy) NSString *_Nullable name; + +/** + * Optional: Geo location of user + */ +@property (nullable, nonatomic, strong) SentryGeo *geo; + /** * Optional: Additional data */ diff --git a/Sources/Sentry/SentryGeo.m b/Sources/Sentry/SentryGeo.m new file mode 100644 index 00000000000..91d73818a8c --- /dev/null +++ b/Sources/Sentry/SentryGeo.m @@ -0,0 +1,80 @@ +#import "SentryGeo.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryGeo + +- (id)copyWithZone:(nullable NSZone *)zone +{ + SentryGeo *copy = [[[self class] allocWithZone:zone] init]; + + if (copy != nil) { + copy.city = self.city; + copy.countryCode = self.countryCode; + copy.region = self.region; + } + + return copy; +} + +- (NSDictionary *)serialize +{ + return @{ @"city" : self.city, @"country_code" : self.countryCode, @"region" : self.region }; +} + +- (BOOL)isEqual:(id _Nullable)other +{ + if (other == self) { + return YES; + } + if (!other || ![[other class] isEqual:[self class]]) { + return NO; + } + + return [self isEqualToGeo:other]; +} + +- (BOOL)isEqualToGeo:(SentryGeo *)geo +{ + if (self == geo) { + return YES; + } + if (geo == nil) { + return NO; + } + + NSString *otherCity = geo.city; + if (self.city != otherCity && ![self.city isEqualToString:otherCity]) { + return NO; + } + + NSString *otherCountryCode = geo.countryCode; + if (self.countryCode != otherCountryCode + && ![self.countryCode isEqualToString:otherCountryCode]) { + return NO; + } + + NSString *otherRegion = geo.region; + if (self.region != otherRegion && ![self.region isEqualToString:otherRegion]) { + return NO; + } + + return YES; +} + +- (NSUInteger)hash +{ + NSUInteger hash = 17; + + hash = hash * 23 + [self.city hash]; + hash = hash * 23 + [self.countryCode hash]; + hash = hash * 23 + [self.region hash]; + + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryUser.m b/Sources/Sentry/SentryUser.m index f6fc00695bc..5db730f14b6 100644 --- a/Sources/Sentry/SentryUser.m +++ b/Sources/Sentry/SentryUser.m @@ -29,6 +29,8 @@ - (id)copyWithZone:(nullable NSZone *)zone copy.username = self.username; copy.ipAddress = self.ipAddress; copy.segment = self.segment; + copy.name = self.name; + copy.geo = self.geo.copy; copy.data = self.data.copy; } @@ -44,6 +46,8 @@ - (id)copyWithZone:(nullable NSZone *)zone [serializedData setValue:self.username forKey:@"username"]; [serializedData setValue:self.ipAddress forKey:@"ip_address"]; [serializedData setValue:self.segment forKey:@"segment"]; + [serializedData setValue:self.name forKey:@"name"]; + [serializedData setValue:[self.geo serialize] forKey:@"geo"]; [serializedData setValue:[self.data sentry_sanitize] forKey:@"data"]; return serializedData; @@ -96,6 +100,16 @@ - (BOOL)isEqualToUser:(SentryUser *)user return NO; } + NSString *otherName = user.name; + if (self.name != otherName && ![self.name isEqualToString:otherName]) { + return NO; + } + + SentryGeo *otherGeo = user.geo; + if (self.geo != otherGeo && ![self.geo isEqualToGeo:otherGeo]) { + return NO; + } + NSDictionary *otherUserData = user.data; if (self.data != otherUserData && ![self.data isEqualToDictionary:otherUserData]) { return NO; @@ -113,6 +127,8 @@ - (NSUInteger)hash hash = hash * 23 + [self.username hash]; hash = hash * 23 + [self.ipAddress hash]; hash = hash * 23 + [self.segment hash]; + hash = hash * 23 + [self.name hash]; + hash = hash * 23 + [self.geo hash]; hash = hash * 23 + [self.data hash]; return hash; diff --git a/Tests/SentryTests/Protocol/SentryGeoTests.swift b/Tests/SentryTests/Protocol/SentryGeoTests.swift new file mode 100644 index 00000000000..70fa6cab5ff --- /dev/null +++ b/Tests/SentryTests/Protocol/SentryGeoTests.swift @@ -0,0 +1,62 @@ +import XCTest + +class SentryGeoTests: XCTestCase { + func testSerializationWithAllProperties() { + let geo = TestData.geo.copy() as! Geo + let actual = geo.serialize() + + // Changing the original doesn't modify the serialized + geo.city = "" + geo.countryCode = "" + geo.region = "" + + XCTAssertEqual(TestData.geo.city, actual["city"] as? String) + XCTAssertEqual(TestData.geo.countryCode, actual["country_code"] as? String) + XCTAssertEqual(TestData.geo.region, actual["region"] as? String) + } + + func testHash() { + XCTAssertEqual(TestData.geo.hash(), TestData.geo.hash()) + + let geo2 = TestData.geo + geo2.city = "Berlin" + XCTAssertNotEqual(TestData.geo.hash(), geo2.hash()) + } + + func testIsEqualToSelf() { + XCTAssertEqual(TestData.geo, TestData.geo) + XCTAssertTrue(TestData.geo.isEqual(to: TestData.geo)) + } + + func testIsNotEqualToOtherClass() { + XCTAssertFalse(TestData.geo.isEqual(1)) + } + + func testIsEqualToCopy() { + XCTAssertEqual(TestData.geo, TestData.geo.copy() as! Geo) + } + + func testNotIsEqual() { + testIsNotEqual { geo in geo.city = "" } + testIsNotEqual { geo in geo.countryCode = "" } + testIsNotEqual { geo in geo.region = "" } + } + + func testIsNotEqual(block: (Geo) -> Void ) { + let geo = TestData.geo.copy() as! Geo + block(geo) + XCTAssertNotEqual(TestData.geo, geo) + } + + func testCopyWithZone_CopiesDeepCopy() { + let geo = TestData.geo + let copiedGeo = geo.copy() as! Geo + + // Modifying the original does not change the copy + geo.city = "" + geo.countryCode = "" + geo.region = "" + + XCTAssertEqual(TestData.geo, copiedGeo) + } +} diff --git a/Tests/SentryTests/Protocol/SentryUserTests.swift b/Tests/SentryTests/Protocol/SentryUserTests.swift index 54bd77c34f3..6ebd913ae3f 100644 --- a/Tests/SentryTests/Protocol/SentryUserTests.swift +++ b/Tests/SentryTests/Protocol/SentryUserTests.swift @@ -12,6 +12,8 @@ class SentryUserTests: XCTestCase { user.username = "" user.ipAddress = "" user.segment = "" + user.name = "" + user.geo = Geo() user.data?.removeAll() XCTAssertEqual(TestData.user.userId, actual["id"] as? String) @@ -19,7 +21,13 @@ class SentryUserTests: XCTestCase { XCTAssertEqual(TestData.user.username, actual["username"] as? String) XCTAssertEqual(TestData.user.ipAddress, actual["ip_address"] as? String) XCTAssertEqual(TestData.user.segment, actual["segment"] as? String) + XCTAssertEqual(TestData.user.name, actual["name"] as? String) XCTAssertEqual(["some": ["data": "data", "date": TestData.timestampAs8601String]], actual["data"] as? Dictionary) + + let actualGeo = actual["geo"] as? [String: Any] + XCTAssertEqual(TestData.user.geo?.city, actualGeo?["city"] as? String) + XCTAssertEqual(TestData.user.geo?.countryCode, actualGeo?["country_code"] as? String) + XCTAssertEqual(TestData.user.geo?.region, actualGeo?["region"] as? String) } func testSerializationWithOnlyId() { @@ -64,6 +72,8 @@ class SentryUserTests: XCTestCase { testIsNotEqual { user in user.username = "" } testIsNotEqual { user in user.ipAddress = "" } testIsNotEqual { user in user.segment = "" } + testIsNotEqual { user in user.name = "" } + testIsNotEqual { user in user.geo = Geo() } testIsNotEqual { user in user.data?.removeAll() } } @@ -83,6 +93,8 @@ class SentryUserTests: XCTestCase { user.username = "" user.ipAddress = "" user.segment = "" + user.name = "" + user.geo = Geo() user.data = [:] XCTAssertEqual(TestData.user, copiedUser) @@ -116,6 +128,11 @@ class SentryUserTests: XCTestCase { user.username = "\(i)" user.ipAddress = "\(i)" user.segment = "\(i)" + user.name = "\(i)" + + user.geo?.city = "\(i)" + user.geo?.countryCode = "\(i)" + user.geo?.region = "\(i)" user.data?["\(i)"] = "\(i)" diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 3c602183721..5a8f8ceeadf 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -63,11 +63,21 @@ class TestData { user.username = "user123" user.ipAddress = "127.0.0.1" user.segment = "segmentA" + user.name = "User" + user.geo = geo user.data = ["some": ["data": "data", "date": timestamp]] return user } + static var geo: Geo { + let geo = Geo() + geo.city = "Vienna" + geo.countryCode = "at" + geo.region = "Vienna" + return geo + } + static var debugMeta: DebugMeta { let debugMeta = DebugMeta() debugMeta.imageAddress = "0x0000000105705000"