Skip to content

Commit

Permalink
feat: Deserializing SentryFrame (#4739)
Browse files Browse the repository at this point in the history
Add Decodable/Deserializing of SentryFrame, including logic for decoding NSNumbers.
  • Loading branch information
philipphofmann authored Jan 23, 2025
1 parent 7d4acb9 commit aa9e313
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
622C08DB29E554B9002571D4 /* SentrySpanContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */; };
62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */; };
623C45B02A651D8200D9E88B /* SentryCoreDataTracker+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */; };
623FD9022D3FA5E000803EDA /* SentryFrameCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */; };
623FD9042D3FA92700803EDA /* NSNumberDecodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */; };
623FD9062D3FA9C800803EDA /* NSNumberDecodableWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */; };
624688192C048EF10006179C /* SentryBaggageSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624688182C048EF10006179C /* SentryBaggageSerialization.swift */; };
626E2D4C2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626E2D4B2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift */; };
6271ADF32BA06D9B0098D2E9 /* SentryInternalSerializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */; settings = {ATTRIBUTES = (Private, ); }; };
Expand Down Expand Up @@ -1130,6 +1133,9 @@
62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDependencyContainerTests.swift; sourceTree = "<group>"; };
623C45AE2A651C4500D9E88B /* SentryCoreDataTracker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCoreDataTracker+Test.h"; sourceTree = "<group>"; };
623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryCoreDataTracker+Test.m"; sourceTree = "<group>"; };
623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFrameCodable.swift; sourceTree = "<group>"; };
623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapper.swift; sourceTree = "<group>"; };
623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapperTests.swift; sourceTree = "<group>"; };
624688182C048EF10006179C /* SentryBaggageSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageSerialization.swift; sourceTree = "<group>"; };
626E2D4B2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilderTests.swift; sourceTree = "<group>"; };
6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalSerializable.h; path = include/SentryInternalSerializable.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2177,7 +2183,9 @@
620078752D38F1110022CB67 /* Codable */ = {
isa = PBXGroup;
children = (
623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */,
6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */,
623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */,
620078712D38F00D0022CB67 /* SentryGeoCodable.swift */,
628094732D39584700B3F18B /* SentryUserCodable.swift */,
620078732D38F0DF0022CB67 /* SentryCodable.swift */,
Expand All @@ -2188,6 +2196,7 @@
620078762D3906AD0022CB67 /* Codable */ = {
isa = PBXGroup;
children = (
623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */,
6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */,
620078772D3906BF0022CB67 /* SentryCodableTests.swift */,
);
Expand Down Expand Up @@ -4752,6 +4761,7 @@
03F84D3727DD4191008FE43F /* SentrySamplingProfiler.cpp in Sources */,
8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */,
7B08A3472924CF9C0059603A /* SentryMetricKitIntegration.m in Sources */,
623FD9022D3FA5E000803EDA /* SentryFrameCodable.swift in Sources */,
7B63459B280EB9E200CFA05A /* SentryUIEventTrackingIntegration.m in Sources */,
D8AE48AE2C577EAB0092A2A6 /* SentryLog.swift in Sources */,
15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */,
Expand All @@ -4764,6 +4774,7 @@
63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */,
6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */,
7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */,
623FD9042D3FA92700803EDA /* NSNumberDecodableWrapper.swift in Sources */,
D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */,
63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */,
7BA0C0482805600A003E0326 /* SentryTransportAdapter.m in Sources */,
Expand Down Expand Up @@ -5157,6 +5168,7 @@
D8CCFC632A1520C900DE232E /* SentryBinaryImageCacheTests.m in Sources */,
A811D867248E2770008A41EA /* SentrySystemEventBreadcrumbsTest.swift in Sources */,
7B82D54924E2A2D400EE670F /* SentryIdTests.swift in Sources */,
623FD9062D3FA9C800803EDA /* NSNumberDecodableWrapperTests.swift in Sources */,
7B87C916295ECFD700510C52 /* SentryMetricKitEventTests.swift in Sources */,
7B6D98ED24C703F8005502FA /* Async.swift in Sources */,
7BA0C04C28056556003E0326 /* SentryTransportAdapterTests.swift in Sources */,
Expand Down
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
}
}
}
42 changes: 42 additions & 0 deletions Sources/Swift/Protocol/Codable/SentryFrameCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@_implementationOnly import _SentryPrivate
import Foundation

extension Frame: Decodable {

enum CodingKeys: String, CodingKey {
case symbolAddress = "symbol_addr"
case fileName = "filename"
case function
case module
case package
case imageAddress = "image_addr"
case platform
case instructionAddress = "instruction_addr"
// Leaving out instruction on purpose. The event payload does not contain this field
// and SentryFrame.serialize doesn't add it to the serialized dict.
// We will remove the property in the next major see:
// https://github.com/getsentry/sentry-cocoa/issues/4738
case lineNumber = "lineno"
case columnNumber = "colno"
case inApp = "in_app"
case stackStart = "stack_start"
}

required convenience public init(from decoder: any Decoder) throws {
self.init()

let container = try decoder.container(keyedBy: CodingKeys.self)
self.symbolAddress = try container.decodeIfPresent(String.self, forKey: .symbolAddress)
self.fileName = try container.decodeIfPresent(String.self, forKey: .fileName)
self.function = try container.decodeIfPresent(String.self, forKey: .function)
self.module = try container.decodeIfPresent(String.self, forKey: .module)
self.package = try container.decodeIfPresent(String.self, forKey: .package)
self.imageAddress = try container.decodeIfPresent(String.self, forKey: .imageAddress)
self.platform = try container.decodeIfPresent(String.self, forKey: .platform)
self.instructionAddress = try container.decodeIfPresent(String.self, forKey: .instructionAddress)
self.lineNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .lineNumber))?.value
self.columnNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .columnNumber))?.value
self.inApp = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .inApp))?.value
self.stackStart = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .stackStart))?.value
}
}
245 changes: 245 additions & 0 deletions Tests/SentryTests/Protocol/Codable/NSNumberDecodableWrapperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
@testable import Sentry
import SentryTestUtils
import XCTest

class NSNumberDecodableWrapperTests: XCTestCase {

func testDecode_BoolTrue() throws {
// Arrange
let jsonData = #"""
{
"number": true
}
"""#.data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertTrue(number.boolValue)
}

func testDecode_BoolFalse() throws {
// Arrange
let jsonData = #"""
{
"number": false
}
"""#.data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertFalse(number.boolValue)
}

func testDecode_PositiveInt() throws {
// Arrange
let jsonData = #"""
{
"number": 1
}
"""#.data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.intValue, 1)
}

func testDecode_IntMax() throws {
// Arrange
let jsonData = """
{
"number": \(Int.max)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.intValue, Int.max)
}

func testDecode_IntMin() throws {
// Arrange
let jsonData = """
{
"number": \(Int.min)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.intValue, Int.min)
}

func testDecode_UInt32Max() throws {
// Arrange
let jsonData = """
{
"number": \(UInt32.max)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.uint32Value, UInt32.max)
}

func testDecode_UInt64Max() throws {
// Arrange
let jsonData = """
{
"number": \(UInt64.max)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.uint64Value, UInt64.max)
}

// We can't use UInt128.max is only available on iOS 18 and above.
// Still we would like to test if a max value bigger than UInt64.max is decoded correctly.
func testDecode_UInt64MaxPlusOne_UsesDouble() throws {
let UInt64MaxPlusOne = Double(UInt64.max) + 1

// Arrange
let jsonData = """
{
"number": \(UInt64MaxPlusOne)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.doubleValue, UInt64MaxPlusOne)

}

func testDecode_Zero() throws {
// Arrange
let jsonData = """
{
"number": 0.0
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.intValue, 0)
}

func testDecode_Double() throws {
// Arrange
let jsonData = """
{
"number": 0.1
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.doubleValue, 0.1)
}

func testDecode_DoubleMax() throws {
// Arrange
let jsonData = """
{
"number": \(Double.greatestFiniteMagnitude)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.doubleValue, Double.greatestFiniteMagnitude)
}

func testDecode_DoubleMin() throws {
// Arrange
let jsonData = """
{
"number": \(Double.leastNormalMagnitude)
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)

// Assert
let number = try XCTUnwrap(actual.number)
XCTAssertEqual(number.doubleValue, Double.leastNormalMagnitude)
}

func testDecode_Nil() throws {
// Arrange
let jsonData = """
{
"number": null
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)
XCTAssertNil(actual.number)
}

func testDecode_String() throws {
// Arrange
let jsonData = """
{
"number": "hello"
}
""".data(using: .utf8)!

// Act
let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as ClassWithNSNumber?)
XCTAssertNil(actual.number)
}
}

private class ClassWithNSNumber: Decodable {

var number: NSNumber?

enum CodingKeys: String, CodingKey {
case number
}

required convenience public init(from decoder: any Decoder) throws {
self.init()

let container = try decoder.container(keyedBy: CodingKeys.self)
self.number = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .number))?.value
}
}
Loading

0 comments on commit aa9e313

Please sign in to comment.