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: Deserializing SentryFrame #4739

Merged
merged 13 commits into from
Jan 23, 2025
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
}
}
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
Loading