Skip to content

Commit

Permalink
Merge 1269a0c into c66aad1
Browse files Browse the repository at this point in the history
  • Loading branch information
philipphofmann authored Feb 10, 2025
2 parents c66aad1 + 1269a0c commit 4cb0e69
Show file tree
Hide file tree
Showing 48 changed files with 2,447 additions and 37 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

- Fix missing `sample_rate` in baggage (#4751)

### Internal
### Internal

- Deserializing SentryEvents with Decodable (#4724)
- Remove internal unknown dict for Breadcrumbs (#4803) This potentially only impacts hybrid SDKs.

## 8.44.0
Expand Down
102 changes: 101 additions & 1 deletion Sentry.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Sources/Sentry/Public/SentryBreadcrumb.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ NS_SWIFT_NAME(Breadcrumb)
*/
@property (nonatomic, copy, nullable) NSString *message;

/**
* Origin of the breadcrumb that is used to identify source of the breadcrumb
* For example hybrid SDKs can identify native breadcrumbs from JS or Flutter
*/
@property (nonatomic, copy, nullable) NSString *origin;

/**
* Arbitrary additional data that will be sent with the breadcrumb
*/
Expand Down
20 changes: 20 additions & 0 deletions Sources/Sentry/Public/SentryEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,24 @@ NS_SWIFT_NAME(Event)

@end

/**
* Subclass of SentryEvent so we can add the Decodable implementation via a Swift extension. We need
* this due to our mixed use of public Swift and ObjC classes. We could avoid this class by
* converting SentryReplayEvent back to ObjC, but we rather accept this tradeoff as we want to
* convert all public classes to Swift in the future. This class needs to be public as we can't add
* the Decodable extension implementation to a class that is not public.
*
* @note: We can’t add the extension for Decodable directly on SentryEvent, because we get an error
* in SentryReplayEvent: 'required' initializer 'init(from:)' must be provided by subclass of
* 'Event' Once we add the initializer with required convenience public init(from decoder: any
* Decoder) throws { fatalError("init(from:) has not been implemented")
* }
* we get the error initializer 'init(from:)' is declared in extension of 'Event' and cannot be
* overridden. Therefore, we add the Decodable implementation not on the Event, but to a subclass of
* the event.
*/
@interface SentryEventDecodable : SentryEvent

@end

NS_ASSUME_NONNULL_END
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,8 @@ - (BOOL)isAppHangEvent

@end

@implementation SentryEventDecodable

@end

NS_ASSUME_NONNULL_END
17 changes: 16 additions & 1 deletion Sources/Sentry/SentryGeo.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "SentryGeo.h"
#import "SentrySwift.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -19,7 +20,21 @@ - (id)copyWithZone:(nullable NSZone *)zone

- (NSDictionary<NSString *, id> *)serialize
{
return @{ @"city" : self.city, @"country_code" : self.countryCode, @"region" : self.region };
NSMutableDictionary *serializedData = [[NSMutableDictionary alloc] init];

if (self.city) {
[serializedData setValue:self.city forKey:@"city"];
}

if (self.countryCode) {
[serializedData setValue:self.countryCode forKey:@"country_code"];
}

if (self.region) {
[serializedData setValue:self.region forKey:@"region"];
}

return serializedData;
}

- (BOOL)isEqual:(id _Nullable)other
Expand Down
12 changes: 12 additions & 0 deletions Sources/Sentry/SentryLevelHelper.m
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
#import "SentryLevelHelper.h"
#import "SentryBreadcrumb+Private.h"
#import "SentryEvent.h"

@implementation SentryLevelBridge : NSObject
+ (NSUInteger)breadcrumbLevel:(SentryBreadcrumb *)breadcrumb
{
return breadcrumb.level;
}

+ (void)setBreadcrumbLevel:(SentryBreadcrumb *)breadcrumb level:(NSUInteger)level
{
breadcrumb.level = level;
}

+ (void)setBreadcrumbLevelOnEvent:(SentryEvent *)event level:(NSUInteger)level
{
event.level = level;
}

@end
10 changes: 4 additions & 6 deletions Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@
# import "SentryBreadcrumb.h"
#endif

@interface SentryBreadcrumb ()
NS_ASSUME_NONNULL_BEGIN

/**
* Origin of the breadcrumb that is used to identify source of the breadcrumb
* For example hybrid SDKs can identify native breadcrumbs from JS or Flutter
*/
@property (nonatomic, copy, nullable) NSString *origin;
@interface SentryBreadcrumb ()

/**
* Initializes a SentryBreadcrumb from a JSON object.
Expand All @@ -19,3 +15,5 @@
*/
- (instancetype _Nonnull)initWithDictionary:(NSDictionary *_Nonnull)dictionary;
@end

NS_ASSUME_NONNULL_END
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryDateUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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
3 changes: 3 additions & 0 deletions Sources/Sentry/include/SentryLevelHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
NS_ASSUME_NONNULL_BEGIN

@class SentryBreadcrumb;
@class SentryEvent;

/**
* This is a workaround to access SentryLevel value from swift
*/
@interface SentryLevelBridge : NSObject
+ (NSUInteger)breadcrumbLevel:(SentryBreadcrumb *)breadcrumb;
+ (void)setBreadcrumbLevel:(SentryBreadcrumb *)breadcrumb level:(NSUInteger)level;
+ (void)setBreadcrumbLevelOnEvent:(SentryEvent *)event level:(NSUInteger)level;
@end

NS_ASSUME_NONNULL_END
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
134 changes: 134 additions & 0 deletions Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
@_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()

// The order here matters as we're dealing with arbitrary data.
// We have to check the double before the Date, because otherwise
// a double value could turn into a Date. So only ISO 8601 string formatted
// dates work, which sanitizeArray and sentry_sanitize use.
// We must check String after Date, because otherwise we would turn a ISO 8601
// string into a string and not a date.
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 dateValue = try? container.decode(Date.self) {
self = .date(dateValue)
} else if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} 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()
if rawData == nil {
return nil
}

return unwrapArbitraryDict(rawData)
} catch {
SentryLog.error("Failed to decode raw data: \(error)")
return nil
}
}

func decodeArbitraryData(decode: () throws -> [String: [String: ArbitraryData]]?) -> [String: [String: Any]]? {
do {
let rawData = try decode()
if rawData == nil {
return nil
}

var newData = [String: [String: Any]]()
for (key, value) in rawData ?? [:] {
newData[key] = unwrapArbitraryDict(value)
}

return newData
} catch {
SentryLog.error("Failed to decode raw data: \(error)")
return nil
}
}

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
}

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
}
}
22 changes: 22 additions & 0 deletions Sources/Swift/Protocol/Codable/NSNumberDecodableWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
struct NSNumberDecodableWrapper: Decodable {
let value: NSNumber?

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intValue = try? container.decode(Int.self) {
value = NSNumber(value: intValue)
}
// On 32-bit platforms UInt is UInt32, so we use UInt64 to cover all platforms.
// We don't need UInt128 because NSNumber doesn't support it.
else if let uint64Value = try? container.decode(UInt64.self) {
value = NSNumber(value: uint64Value)
} else if let doubleValue = try? container.decode(Double.self) {
value = NSNumber(value: doubleValue)
} else if let boolValue = try? container.decode(Bool.self) {
value = NSNumber(value: boolValue)
} else {
SentryLog.warning("Failed to decode NSNumber from container for key: \(container.codingPath.last?.stringValue ?? "unknown")")
value = nil
}
}
}
35 changes: 35 additions & 0 deletions Sources/Swift/Protocol/Codable/SentryBreadcrumbCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@_implementationOnly import _SentryPrivate
import Foundation

extension Breadcrumb: Decodable {

private enum CodingKeys: String, CodingKey {
case level
case category
case timestamp
case type
case message
case data
case origin
}

required convenience public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.init()

let rawLevel = try container.decode(String.self, forKey: .level)
let level = SentryLevelHelper.levelForName(rawLevel)
SentryLevelBridge.setBreadcrumbLevel(self, level: level.rawValue)

self.category = try container.decode(String.self, forKey: .category)
self.timestamp = try container.decodeIfPresent(Date.self, forKey: .timestamp)
self.type = try container.decodeIfPresent(String.self, forKey: .type)
self.message = try container.decodeIfPresent(String.self, forKey: .message)
self.origin = try container.decodeIfPresent(String.self, forKey: .origin)

self.data = decodeArbitraryData {
try container.decodeIfPresent([String: ArbitraryData].self, forKey: .data)
}
}
}
41 changes: 41 additions & 0 deletions Sources/Swift/Protocol/Codable/SentryCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@_implementationOnly import _SentryPrivate
import Foundation

func decodeFromJSONData<T: Decodable>(jsonData: Data) -> T? {
if jsonData.isEmpty {
return nil
}

do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()

// We prefer a Double/TimeInterval because it allows nano second precision.
// The ISO8601 formatter only supports millisecond precision.
if let timeIntervalSince1970 = try? container.decode(Double.self) {
return Date(timeIntervalSince1970: timeIntervalSince1970)
}

if let dateString = try? container.decode(String.self) {
let formatter = sentryGetIso8601FormatterWithMillisecondPrecision()
guard let date = formatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format. The following string doesn't represent a valid ISO 8601 date string: '\(dateString)'")
}

return date
}

throw DecodingError.typeMismatch(Date.self, DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Invalid date format. The Date must either be a Double/TimeInterval representing the timeIntervalSince1970 or it can be a ISO 8601 formatted String."
))

}
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
}
Loading

0 comments on commit 4cb0e69

Please sign in to comment.