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

feat: Deserialize User #4732

Merged
merged 7 commits into from
Jan 21, 2025
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
12 changes: 12 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
6271ADF32BA06D9B0098D2E9 /* SentryInternalSerializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */; settings = {ATTRIBUTES = (Private, ); }; };
6273513F2CBD14970021D100 /* SentryDebugImageProvider+HybridSDKs.h in Headers */ = {isa = PBXBuildFile; fileRef = 6273513E2CBD14970021D100 /* SentryDebugImageProvider+HybridSDKs.h */; settings = {ATTRIBUTES = (Private, ); }; };
627E7589299F6FE40085504D /* SentryInternalDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 627E7588299F6FE40085504D /* SentryInternalDefines.h */; };
628094742D39584C00B3F18B /* SentryUserCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628094732D39584700B3F18B /* SentryUserCodable.swift */; };
6281C5722D3E4F12009D0978 /* DecodeArbitraryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */; };
6281C5742D3E50DF009D0978 /* ArbitraryDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */; };
62862B1C2B1DDBC8009B16E3 /* SentryDelayedFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */; };
62862B1E2B1DDC35009B16E3 /* SentryDelayedFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */; };
62872B5F2BA1B7F300A4FA7D /* NSLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */; };
Expand Down Expand Up @@ -1116,6 +1119,9 @@
6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalSerializable.h; path = include/SentryInternalSerializable.h; sourceTree = "<group>"; };
6273513E2CBD14970021D100 /* SentryDebugImageProvider+HybridSDKs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryDebugImageProvider+HybridSDKs.h"; path = "include/HybridPublic/SentryDebugImageProvider+HybridSDKs.h"; sourceTree = "<group>"; };
627E7588299F6FE40085504D /* SentryInternalDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalDefines.h; path = include/SentryInternalDefines.h; sourceTree = "<group>"; };
628094732D39584700B3F18B /* SentryUserCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserCodable.swift; sourceTree = "<group>"; };
6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodeArbitraryData.swift; sourceTree = "<group>"; };
6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryDataTests.swift; sourceTree = "<group>"; };
62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDelayedFrame.h; path = include/SentryDelayedFrame.h; sourceTree = "<group>"; };
62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDelayedFrame.m; sourceTree = "<group>"; };
62872B5E2BA1B7F300A4FA7D /* NSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2140,7 +2146,9 @@
620078752D38F1110022CB67 /* Codable */ = {
isa = PBXGroup;
children = (
6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */,
620078712D38F00D0022CB67 /* SentryGeoCodable.swift */,
628094732D39584700B3F18B /* SentryUserCodable.swift */,
620078732D38F0DF0022CB67 /* SentryCodable.swift */,
);
path = Codable;
Expand All @@ -2149,6 +2157,7 @@
620078762D3906AD0022CB67 /* Codable */ = {
isa = PBXGroup;
children = (
6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */,
620078772D3906BF0022CB67 /* SentryCodableTests.swift */,
);
path = Codable;
Expand Down Expand Up @@ -4634,6 +4643,7 @@
0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */,
620203B22C59025E0008317C /* SentryFileContents.swift in Sources */,
7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */,
628094742D39584C00B3F18B /* SentryUserCodable.swift in Sources */,
7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */,
D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */,
631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */,
Expand Down Expand Up @@ -4725,6 +4735,7 @@
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */,
630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */,
6281C5722D3E4F12009D0978 /* DecodeArbitraryData.swift in Sources */,
62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */,
D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */,
7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */,
Expand Down Expand Up @@ -5112,6 +5123,7 @@
D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */,
7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */,
8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */,
6281C5742D3E50DF009D0978 /* ArbitraryDataTests.swift in Sources */,
7BE0DC2F272ABAF6004FA8B7 /* SentryAutoBreadcrumbTrackingIntegrationTests.swift in Sources */,
7B869EBE249B964D004F4FDB /* SentryThreadEquality.swift in Sources */,
7BC6EBF8255C05060059822A /* TestData.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryDateUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

NS_ASSUME_NONNULL_BEGIN

SENTRY_EXTERN NSDateFormatter *sentryGetIso8601FormatterWithMillisecondPrecision(void);

SENTRY_EXTERN NSDate *sentry_fromIso8601String(NSString *string);

SENTRY_EXTERN NSString *sentry_toIso8601String(NSDate *date);
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// Headers that also import SentryDefines should be at the end of this list
// otherwise it wont compile
#import "SentryDateUtil.h"
#import "SentryDateUtils.h"
#import "SentryDisplayLinkWrapper.h"
#import "SentryLevelHelper.h"
#import "SentryLogC.h"
Expand Down
105 changes: 105 additions & 0 deletions Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@_implementationOnly import _SentryPrivate

/// Represents arbitrary data that can be decoded from JSON with Decodable.
///
/// - Note: Some classes on the protocol allow adding extra data in a dictionary of type String:Any.
/// Users can put anything in there that can be serialized to JSON. The SDK uses JSONSerialization to
/// serialize these dictionaries. At first glance, you could assume that we can use JSONSerialization.jsonObject(with:options)
/// to deserialize these dictionaries, but we can't. When using Decodable, you don't have access to the raw
/// data of the JSON. The Decoder and the DecodingContainers don't offer methods to access the underlying
/// data. The Swift Decodable converts the raw data to a JSON object and then casts the JSON object to the
/// class that implements the Decodable protocol, see:
/// https://github.com/swiftlang/swift-foundation/blob/e9d59b6065ad909fee15f174bd5ca2c580490388/Sources/FoundationEssentials/JSON/JSONDecoder.swift#L360-L386
/// https://github.com/swiftlang/swift-foundation/blob/e9d59b6065ad909fee15f174bd5ca2c580490388/Sources/FoundationEssentials/JSON/JSONScanner.swift#L343-L383

/// Therefore, we have to implement decoding the arbitrary dictionary manually.
///
/// A discarded option is to decode the JSON raw data twice: once with Decodable and once with the JSONSerialization.
/// This has two significant downsides: First, we deserialize the JSON twice, which is a performance overhead. Second,
/// we don't conform to the Decodable protocol, which could lead to unwanted hard-to-detect problems in the future.
enum ArbitraryData: Decodable {
case string(String)
case int(Int)
case number(Double)
case boolean(Bool)
case date(Date)
case dict([String: ArbitraryData])
case array([ArbitraryData])
case null

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let dateValue = try? container.decode(Date.self) {
self = .date(dateValue)
} else if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let intValue = try? container.decode(Int.self) {
self = .int(intValue)
} else if let numberValue = try? container.decode(Double.self) {
self = .number(numberValue)
} else if let boolValue = try? container.decode(Bool.self) {
self = .boolean(boolValue)
} else if let objectValue = try? container.decode([String: ArbitraryData].self) {
self = .dict(objectValue)
} else if let arrayValue = try? container.decode([ArbitraryData].self) {
self = .array(arrayValue)
} else if container.decodeNil() {
self = .null
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid JSON value"
)
}
}
}

func decodeArbitraryData(decode: () throws -> [String: ArbitraryData]?) -> [String: Any]? {
do {
let rawData = try decode()
return unwrapArbitraryDict(rawData)
} catch {
SentryLog.error("Failed to decode raw data: \(error)")
return nil

Check warning on line 64 in Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift#L63-L64

Added lines #L63 - L64 were not covered by tests
}
}

private func unwrapArbitraryDict(_ dict: [String: ArbitraryData]?) -> [String: Any]? {
guard let nonNullDict = dict else {
return nil
}

return nonNullDict.mapValues { unwrapArbitraryValue($0) as Any }
}

private func unwrapArbitraryArray(_ array: [ArbitraryData]?) -> [Any]? {
guard let nonNullArray = array else {
return nil

Check warning on line 78 in Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift#L78

Added line #L78 was not covered by tests
}

return nonNullArray.map { unwrapArbitraryValue($0) as Any }
}

private func unwrapArbitraryValue(_ value: ArbitraryData?) -> Any? {
switch value {
case .string(let stringValue):
return stringValue
case .number(let numberValue):
return numberValue
case .int(let intValue):
return intValue
case .boolean(let boolValue):
return boolValue
case .date(let dateValue):
return dateValue
case .dict(let dictValue):
return unwrapArbitraryDict(dictValue)
case .array(let arrayValue):
return unwrapArbitraryArray(arrayValue)
case .null:
return NSNull()
case .none:
return nil
}
}
6 changes: 5 additions & 1 deletion Sources/Swift/Protocol/Codable/SentryCodable.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@_implementationOnly import _SentryPrivate
import Foundation

func decodeFromJSONData<T: Decodable>(jsonData: Data) -> T? {
Expand All @@ -7,9 +8,12 @@ func decodeFromJSONData<T: Decodable>(jsonData: Data) -> T? {

do {
let decoder = JSONDecoder()
let formatter = sentryGetIso8601FormatterWithMillisecondPrecision()
decoder.dateDecodingStrategy = .formatted(formatter)
return try decoder.decode(T.self, from: jsonData)
} catch {
SentryLog.error("Could not decode object of type \(T.self) from JSON data due to error: \(error)")
return nil
}

return nil
}
41 changes: 41 additions & 0 deletions Sources/Swift/Protocol/Codable/SentryUserCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@_implementationOnly import _SentryPrivate
import Foundation

extension User: Decodable {

enum CodingKeys: String, CodingKey {
case userId = "id"
case email
case username
case ipAddress = "ip_address"
case segment
case name
case geo
case data
}

@available(*, deprecated, message: """
This method is only deprecated to silence the deprecation warning of the property \
segment. Our Xcode project has deprecations as warnings and warnings as errors \
configured. Therefore, compilation fails without marking this init method as \
deprecated. It is safe to use this deprecated init method. Instead of turning off \
deprecation warnings for the whole project, we accept the tradeoff of marking this \
init method as deprecated because we don't expect many users to use it. Sadly, \
Swift doesn't offer a better way of silencing a deprecation warning.
""")
required convenience public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.init()
self.userId = try container.decodeIfPresent(String.self, forKey: .userId)
self.email = try container.decodeIfPresent(String.self, forKey: .email)
self.username = try container.decodeIfPresent(String.self, forKey: .username)
self.ipAddress = try container.decodeIfPresent(String.self, forKey: .ipAddress)
self.segment = try container.decodeIfPresent(String.self, forKey: .segment)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
self.geo = try container.decodeIfPresent(Geo.self, forKey: .geo)

self.data = decodeArbitraryData {
try container.decodeIfPresent([String: ArbitraryData].self, forKey: .data)
}
}
}
Loading
Loading