diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5267e17c5..8fe20403456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,14 @@ - Allow hybrid SDK to set replay options tags information (#4710) - Add threshold to always log fatal logs (#4707) +### Improvements + +- Add error logging for invalid `cacheDirectoryPath` (#4693) + +### Internal + +- Change macros TEST and TESTCI to SENTRY_TEST and SENTRY_TEST_CI (#4712) +- Deserializing SentryEvents with Decodable (#4724) ### Internal - Change macros TEST and TESTCI to SENTRY_TEST and SENTRY_TEST_CI (#4712) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index cf7deb29899..d88e448372e 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -78,9 +78,13 @@ 33EB2A922C341300004FED3D /* Sentry.h in Headers */ = {isa = PBXBuildFile; fileRef = 63AA76931EB9C1C200D153DE /* Sentry.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift */; }; 51B15F802BE88D510026A2F2 /* URLSessionTaskHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7F2BE88D510026A2F2 /* URLSessionTaskHelperTests.swift */; }; + 620078722D38F00D0022CB67 /* SentryGeoCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620078712D38F00D0022CB67 /* SentryGeoCodable.swift */; }; + 620078742D38F0DF0022CB67 /* SentryCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620078732D38F0DF0022CB67 /* SentryCodable.swift */; }; + 620078782D3906BF0022CB67 /* SentryCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620078772D3906BF0022CB67 /* SentryCodableTests.swift */; }; 620203B22C59025E0008317C /* SentryFileContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620203B12C59025E0008317C /* SentryFileContents.swift */; }; 620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */ = {isa = PBXBuildFile; fileRef = 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */; }; 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */ = {isa = PBXBuildFile; fileRef = 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */; }; + 620467AC2D3FFD230025F06C /* SentryNSErrorCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620467AB2D3FFD1C0025F06C /* SentryNSErrorCodable.swift */; }; 6205B4A42CE73AA100744684 /* TestDebugImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85790282976A69F00C6AC1F /* TestDebugImageProvider.swift */; }; 621AE74B2C626C230012E730 /* SentryANRTrackerV2.h in Headers */ = {isa = PBXBuildFile; fileRef = 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */; }; 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; }; @@ -92,16 +96,23 @@ 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, ); }; }; 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 */; }; 62872B632BA1B86100A4FA7D /* NSLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62872B622BA1B86100A4FA7D /* NSLockTests.swift */; }; 62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62885DA629E946B100554F38 /* TestConncurrentModifications.swift */; }; + 6293F5752D422A95002BC3BD /* SentryStacktraceCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6293F5742D422A8A002BC3BD /* SentryStacktraceCodable.swift */; }; 629428802CB3BF69002C454C /* SwizzleClassNameExclude.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */; }; 6294774C2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */; }; 62950F1029E7FE0100A42624 /* SentryTransactionContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */; }; @@ -131,6 +142,8 @@ 62F05D2B2C0DB1F100916E3F /* SentryLogTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F05D2A2C0DB1F100916E3F /* SentryLogTestHelper.m */; }; 62F226B729A37C120038080D /* SentryBooleanSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F226B629A37C120038080D /* SentryBooleanSerialization.m */; }; 62F4DDA12C04CB9700588890 /* SentryBaggageSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F4DDA02C04CB9700588890 /* SentryBaggageSerializationTests.swift */; }; + 62F70E932D4234B800634054 /* SentryMechanismMetaCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F70E922D4234B100634054 /* SentryMechanismMetaCodable.swift */; }; + 62F70E952D423BCD00634054 /* SentryMechanismCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F70E942D423BCA00634054 /* SentryMechanismCodable.swift */; }; 62FC18AF2C9D5FAC008803CD /* SentryANRTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FC18AE2C9D5FAC008803CD /* SentryANRTracker.swift */; }; 62FC69362BEDFF18002D3EF2 /* SentryLogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FC69352BEDFF18002D3EF2 /* SentryLogExtensions.swift */; }; 630435FE1EBCA9D900C4D3FA /* SentryNSURLRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 630435FC1EBCA9D900C4D3FA /* SentryNSURLRequest.h */; }; @@ -788,10 +801,10 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; - D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; - D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; + D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; + D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; @@ -1106,9 +1119,13 @@ 33EB2A8F2C3411AE004FED3D /* SentryWithoutUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryWithoutUIKit.h; path = Public/SentryWithoutUIKit.h; sourceTree = ""; }; 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskHelper.swift; sourceTree = ""; }; 51B15F7F2BE88D510026A2F2 /* URLSessionTaskHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskHelperTests.swift; sourceTree = ""; }; + 620078712D38F00D0022CB67 /* SentryGeoCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryGeoCodable.swift; sourceTree = ""; }; + 620078732D38F0DF0022CB67 /* SentryCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCodable.swift; sourceTree = ""; }; + 620078772D3906BF0022CB67 /* SentryCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCodableTests.swift; sourceTree = ""; }; 620203B12C59025E0008317C /* SentryFileContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFileContents.swift; sourceTree = ""; }; 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBuildAppStartSpans.h; path = include/SentryBuildAppStartSpans.h; sourceTree = ""; }; 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBuildAppStartSpans.m; sourceTree = ""; }; + 620467AB2D3FFD1C0025F06C /* SentryNSErrorCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSErrorCodable.swift; sourceTree = ""; }; 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackerV2.h; path = include/SentryANRTrackerV2.h; sourceTree = ""; }; 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackerV2.m; sourceTree = ""; }; 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = ""; }; @@ -1121,16 +1138,23 @@ 62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDependencyContainerTests.swift; sourceTree = ""; }; 623C45AE2A651C4500D9E88B /* SentryCoreDataTracker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCoreDataTracker+Test.h"; sourceTree = ""; }; 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryCoreDataTracker+Test.m"; sourceTree = ""; }; + 623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFrameCodable.swift; sourceTree = ""; }; + 623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapper.swift; sourceTree = ""; }; + 623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapperTests.swift; sourceTree = ""; }; 624688182C048EF10006179C /* SentryBaggageSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageSerialization.swift; sourceTree = ""; }; 626E2D4B2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilderTests.swift; sourceTree = ""; }; 6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalSerializable.h; path = include/SentryInternalSerializable.h; sourceTree = ""; }; 6273513E2CBD14970021D100 /* SentryDebugImageProvider+HybridSDKs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryDebugImageProvider+HybridSDKs.h"; path = "include/HybridPublic/SentryDebugImageProvider+HybridSDKs.h"; sourceTree = ""; }; 627E7588299F6FE40085504D /* SentryInternalDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalDefines.h; path = include/SentryInternalDefines.h; sourceTree = ""; }; + 628094732D39584700B3F18B /* SentryUserCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserCodable.swift; sourceTree = ""; }; + 6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodeArbitraryData.swift; sourceTree = ""; }; + 6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryDataTests.swift; sourceTree = ""; }; 62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDelayedFrame.h; path = include/SentryDelayedFrame.h; sourceTree = ""; }; 62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDelayedFrame.m; sourceTree = ""; }; 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLock.swift; sourceTree = ""; }; 62872B622BA1B86100A4FA7D /* NSLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLockTests.swift; sourceTree = ""; }; 62885DA629E946B100554F38 /* TestConncurrentModifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConncurrentModifications.swift; sourceTree = ""; }; + 6293F5742D422A8A002BC3BD /* SentryStacktraceCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStacktraceCodable.swift; sourceTree = ""; }; 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwizzleClassNameExclude.swift; sourceTree = ""; }; 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Delegate.swift; sourceTree = ""; }; 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionContextTests.swift; sourceTree = ""; }; @@ -1161,6 +1185,8 @@ 62F226B629A37C120038080D /* SentryBooleanSerialization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBooleanSerialization.m; sourceTree = ""; }; 62F226B829A37C270038080D /* SentryBooleanSerialization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryBooleanSerialization.h; sourceTree = ""; }; 62F4DDA02C04CB9700588890 /* SentryBaggageSerializationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageSerializationTests.swift; sourceTree = ""; }; + 62F70E922D4234B100634054 /* SentryMechanismMetaCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMechanismMetaCodable.swift; sourceTree = ""; }; + 62F70E942D423BCA00634054 /* SentryMechanismCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMechanismCodable.swift; sourceTree = ""; }; 62FC18AE2C9D5FAC008803CD /* SentryANRTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTracker.swift; sourceTree = ""; }; 62FC69352BEDFF18002D3EF2 /* SentryLogExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogExtensions.swift; sourceTree = ""; }; 630435FC1EBCA9D900C4D3FA /* SentryNSURLRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLRequest.h; path = include/SentryNSURLRequest.h; sourceTree = ""; }; @@ -1892,10 +1918,10 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; - D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; - D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; + D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; + D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; @@ -2162,6 +2188,33 @@ path = ViewHierarchy; sourceTree = ""; }; + 620078752D38F1110022CB67 /* Codable */ = { + isa = PBXGroup; + children = ( + 623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */, + 6281C5712D3E4F06009D0978 /* DecodeArbitraryData.swift */, + 623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */, + 6293F5742D422A8A002BC3BD /* SentryStacktraceCodable.swift */, + 62F70E922D4234B100634054 /* SentryMechanismMetaCodable.swift */, + 62F70E942D423BCA00634054 /* SentryMechanismCodable.swift */, + 620467AB2D3FFD1C0025F06C /* SentryNSErrorCodable.swift */, + 620078712D38F00D0022CB67 /* SentryGeoCodable.swift */, + 628094732D39584700B3F18B /* SentryUserCodable.swift */, + 620078732D38F0DF0022CB67 /* SentryCodable.swift */, + ); + path = Codable; + sourceTree = ""; + }; + 620078762D3906AD0022CB67 /* Codable */ = { + isa = PBXGroup; + children = ( + 623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */, + 6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */, + 620078772D3906BF0022CB67 /* SentryCodableTests.swift */, + ); + path = Codable; + sourceTree = ""; + }; 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( @@ -2934,6 +2987,7 @@ 7B3D0474249A3D5800E106B6 /* Protocol */ = { isa = PBXGroup; children = ( + 620078762D3906AD0022CB67 /* Codable */, 7BC6EBF7255C05060059822A /* TestData.swift */, 7B869EBB249B91D8004F4FDB /* SentryDebugMetaEquality.swift */, 7B869EBD249B964D004F4FDB /* SentryThreadEquality.swift */, @@ -3634,8 +3688,6 @@ 8ECC674325C23A1F000E2BF6 /* SentrySpanContext.m */, 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */, 8E4E7C7325DAAB49006AB9E2 /* SentrySpanProtocol.h */, - 7B3B83712833832B0001FDEB /* SentrySpanOperations.h */, - 622C08D729E546F4002571D4 /* SentryTraceOrigins.h */, 8E4E7C6C25DAAAFE006AB9E2 /* SentrySpan.h */, 84A789092C0E9F5800FF0803 /* SentrySpan+Private.h */, 8EC3AE7925CA23B600E7591A /* SentrySpan.m */, @@ -4044,6 +4096,7 @@ D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( + 620078752D38F1110022CB67 /* Codable */, 64F9571C2D12DA1800324652 /* SentryViewControllerBreadcrumbTracking.swift */, D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, @@ -4685,6 +4738,7 @@ D48724DD2D354939005DE483 /* SentrySpanOperation.swift 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 */, @@ -4717,6 +4771,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 */, @@ -4724,10 +4779,12 @@ 7B7A30C824B48389005A4C6E /* SentryCrashWrapper.m in Sources */, D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */, D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */, + 620078742D38F0DF0022CB67 /* SentryCodable.swift in Sources */, 63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */, 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 */, @@ -4746,6 +4803,7 @@ 8459FCC02BD73EB20038E9C9 /* SentryProfilerSerialization.mm in Sources */, 621C884A2CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm in Sources */, 63EED6C02237923600E02400 /* SentryOptions.m in Sources */, + 620078722D38F00D0022CB67 /* SentryGeoCodable.swift in Sources */, D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, @@ -4774,6 +4832,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 */, @@ -4800,9 +4859,11 @@ 62FC18AF2C9D5FAC008803CD /* SentryANRTracker.swift in Sources */, 63FE711120DA4C1000CDBAE8 /* SentryCrashDebug.c in Sources */, 7B883F49253D714C00879E62 /* SentryCrashUUIDConversion.c in Sources */, + 62F70E932D4234B800634054 /* SentryMechanismMetaCodable.swift in Sources */, 843FB3232D0CD04D00558F18 /* SentryUserAccess.m in Sources */, 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, + 6293F5752D422A95002BC3BD /* SentryStacktraceCodable.swift in Sources */, 63FE712120DA4C1000CDBAE8 /* SentryCrashSymbolicator.c in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, @@ -4832,6 +4893,7 @@ 7DB3A687238EA75E00A2D442 /* SentryHttpTransport.m in Sources */, 63FE70D520DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.m in Sources */, D80CD8D12B751442002F710B /* HTTPHeaderSanitizer.swift in Sources */, + 62F70E952D423BCD00634054 /* SentryMechanismCodable.swift in Sources */, 0AAE201E28ED9B9400D0CD80 /* SentryReachability.m in Sources */, 7B0A54282521C22C00A71716 /* SentryFrameRemover.m in Sources */, 7BC63F0A28081288009D9E37 /* SentrySwizzleWrapper.m in Sources */, @@ -4869,6 +4931,7 @@ 64F9571D2D12DA1A00324652 /* SentryViewControllerBreadcrumbTracking.swift in Sources */, 63FE70D320DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.c in Sources */, 849B8F9D2C6E906900148E1F /* SentryUserFeedbackWidgetConfiguration.swift in Sources */, + 620467AC2D3FFD230025F06C /* SentryNSErrorCodable.swift in Sources */, 639FCFA51EBC809A00778193 /* SentryStacktrace.m in Sources */, D84D2CC32C29AD120011AF8A /* SentrySessionReplay.swift in Sources */, 849B8F9B2C6E906900148E1F /* SentryUserFeedbackIntegrationDriver.swift in Sources */, @@ -5119,6 +5182,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 */, @@ -5162,6 +5226,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 */, @@ -5246,6 +5311,7 @@ D8CB742B294B1DD000A5F964 /* SentryUIApplicationTests.swift in Sources */, 63FE720920DA66EC00CDBAE8 /* XCTestCase+SentryCrash.m in Sources */, D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */, + 620078782D3906BF0022CB67 /* SentryCodableTests.swift in Sources */, 7B85BD8E24C5C3A6000A4225 /* SentryFileManagerTestExtension.swift in Sources */, 7B0002342477F52D0035FEF1 /* SentrySessionTests.swift in Sources */, ); diff --git a/Sources/Sentry/SentryGeo.m b/Sources/Sentry/SentryGeo.m index 91d73818a8c..699ad8c7346 100644 --- a/Sources/Sentry/SentryGeo.m +++ b/Sources/Sentry/SentryGeo.m @@ -1,5 +1,5 @@ #import "SentryGeo.h" - +#import "SentrySwift.h" #import NS_ASSUME_NONNULL_BEGIN @@ -21,7 +21,21 @@ - (id)copyWithZone:(nullable NSZone *)zone - (NSDictionary *)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 diff --git a/Sources/Sentry/include/SentryDateUtils.h b/Sources/Sentry/include/SentryDateUtils.h index 1a2d16f19ca..ee8dd4d76dd 100644 --- a/Sources/Sentry/include/SentryDateUtils.h +++ b/Sources/Sentry/include/SentryDateUtils.h @@ -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); diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index c1415fe1481..c4d7a99b8ea 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -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" diff --git a/Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift b/Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift new file mode 100644 index 00000000000..98003fc2bdf --- /dev/null +++ b/Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift @@ -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 + } +} + +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 + } +} diff --git a/Sources/Swift/Protocol/Codable/NSNumberDecodableWrapper.swift b/Sources/Swift/Protocol/Codable/NSNumberDecodableWrapper.swift new file mode 100644 index 00000000000..ef6384d8950 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/NSNumberDecodableWrapper.swift @@ -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 + } + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryCodable.swift b/Sources/Swift/Protocol/Codable/SentryCodable.swift new file mode 100644 index 00000000000..ada271754da --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryCodable.swift @@ -0,0 +1,19 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +func decodeFromJSONData(jsonData: Data) -> T? { + if jsonData.isEmpty { + return nil + } + + 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 +} diff --git a/Sources/Swift/Protocol/Codable/SentryFrameCodable.swift b/Sources/Swift/Protocol/Codable/SentryFrameCodable.swift new file mode 100644 index 00000000000..e0ebd03676b --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryFrameCodable.swift @@ -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 + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryGeoCodable.swift b/Sources/Swift/Protocol/Codable/SentryGeoCodable.swift new file mode 100644 index 00000000000..e4c7a6ce71c --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryGeoCodable.swift @@ -0,0 +1,19 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension Geo: Decodable { + + private enum CodingKeys: String, CodingKey { + case city + case countryCode = "country_code" + case region + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.init() + self.city = try container.decodeIfPresent(String.self, forKey: .city) + self.countryCode = try container.decodeIfPresent(String.self, forKey: .countryCode) + self.region = try container.decodeIfPresent(String.self, forKey: .region) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryMechanismCodable.swift b/Sources/Swift/Protocol/Codable/SentryMechanismCodable.swift new file mode 100644 index 00000000000..235e5506c93 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryMechanismCodable.swift @@ -0,0 +1,31 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension Mechanism: Decodable { + + enum CodingKeys: String, CodingKey { + case type + case handled + case synthetic + case desc = "description" + case data + case helpLink = "help_link" + case meta + } + + required convenience public init(from decoder: any Decoder) throws { + + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .type) + self.init(type: type) + self.desc = try container.decodeIfPresent(String.self, forKey: .desc) + self.data = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .data) + } + self.handled = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .handled)?.value + self.synthetic = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .synthetic)?.value + self.helpLink = try container.decodeIfPresent(String.self, forKey: .helpLink) + self.meta = try container.decodeIfPresent(MechanismMeta.self, forKey: .meta) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryMechanismMetaCodable.swift b/Sources/Swift/Protocol/Codable/SentryMechanismMetaCodable.swift new file mode 100644 index 00000000000..7bf13939cb0 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryMechanismMetaCodable.swift @@ -0,0 +1,24 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension MechanismMeta: Decodable { + + enum CodingKeys: String, CodingKey { + case signal + case machException = "mach_exception" + case error = "ns_error" + } + + required convenience public init(from decoder: any Decoder) throws { + self.init() + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.signal = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .signal) + } + self.machException = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .machException) + } + self.error = try container.decodeIfPresent(SentryNSError.self, forKey: .error) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift b/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift new file mode 100644 index 00000000000..9148ead6f84 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift @@ -0,0 +1,19 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryNSError: Decodable { + + enum CodingKeys: String, CodingKey { + case domain + case code + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let domain = try container.decode(String.self, forKey: .domain) + let code = try container.decode(Int.self, forKey: .code) + + self.init(domain: domain, code: code) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryStacktraceCodable.swift b/Sources/Swift/Protocol/Codable/SentryStacktraceCodable.swift new file mode 100644 index 00000000000..807dc3fa9ae --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryStacktraceCodable.swift @@ -0,0 +1,22 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryStacktrace: Decodable { + + enum CodingKeys: String, CodingKey { + case frames + case registers + case snapshot + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let frames = try container.decodeIfPresent([Frame].self, forKey: .frames) ?? [] + let registers = try container.decodeIfPresent([String: String].self, forKey: .registers) ?? [:] + self.init(frames: frames, registers: registers) + + let snapshot = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .snapshot) + self.snapshot = snapshot?.value + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryUserCodable.swift b/Sources/Swift/Protocol/Codable/SentryUserCodable.swift new file mode 100644 index 00000000000..25ebd887eba --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryUserCodable.swift @@ -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) + } + } +} diff --git a/Tests/SentryTests/Protocol/Codable/ArbitraryDataTests.swift b/Tests/SentryTests/Protocol/Codable/ArbitraryDataTests.swift new file mode 100644 index 00000000000..b004c65fcfe --- /dev/null +++ b/Tests/SentryTests/Protocol/Codable/ArbitraryDataTests.swift @@ -0,0 +1,259 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class ArbitraryDataTests: XCTestCase { + + func testDecode_StringValues() throws { + // Arrange + let jsonData = #""" + { + "data": { + "some": "value", + "empty": "", + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual("value", actual.data?["some"] as? String) + XCTAssertEqual("", actual.data?["empty"] as? String) + } + + func testDecode_IntValues() throws { + // Arrange + let jsonData = """ + { + "data": { + "positive": 1, + "zero": 0, + "negative": -1, + "max": \(Int.max), + "min": \(Int.min) + } + } + """.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual(1, actual.data?["positive"] as? Int) + XCTAssertEqual(0, actual.data?["zero"] as? Int) + XCTAssertEqual(-1, actual.data?["negative"] as? Int) + XCTAssertEqual(Int.max, actual.data?["max"] as? Int) + XCTAssertEqual(Int.min, actual.data?["min"] as? Int) + } + + func testDecode_DoubleValues() throws { + // Arrange + let jsonData = """ + { + "data": { + "positive": 0.1, + "negative": -0.1, + "max": \(Double.greatestFiniteMagnitude), + "min": \(Double.leastNormalMagnitude) + } + } + """.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual(0.1, actual.data?["positive"] as? Double) + XCTAssertEqual(-0.1, actual.data?["negative"] as? Double) + XCTAssertEqual(Double.greatestFiniteMagnitude, actual.data?["max"] as? Double) + XCTAssertEqual(Double.leastNormalMagnitude, actual.data?["min"] as? Double) + } + + func testDecode_DoubleWithoutFractionalPart_IsDecodedAsInt() throws { + // Arrange + let jsonData = """ + { + "data": { + "zero": 0.0, + "one": 1.0, + "minus_one": -1.0, + + } + } + """.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual(0, actual.data?["zero"] as? Int) + XCTAssertEqual(1, actual.data?["one"] as? Int) + XCTAssertEqual(-1, actual.data?["minus_one"] as? Int) + } + + func testDecode_BoolValues() throws { + // Arrange + let jsonData = #""" + { + "data": { + "true": true, + "false": false + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual(true, actual.data?["true"] as? Bool) + XCTAssertEqual(false, actual.data?["false"] as? Bool) + } + + func testDecode_DateValue() throws { + // Arrange + let date = TestCurrentDateProvider().date().addingTimeInterval(0.001) + let jsonData = #""" + { + "data": { + "date": "\#(sentry_toIso8601String(date))" + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let actualDate = try XCTUnwrap( actual.data?["date"] as? Date) + XCTAssertEqual(date.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 0.0001) + } + + func testDecode_Dict() throws { + // Arrange + let jsonData = #""" + { + "data": { + "dict": { + "string": "value", + "true": true, + "number": 10, + }, + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let dict = try XCTUnwrap(actual.data?["dict"] as? [String: Any]) + XCTAssertEqual("value", dict["string"] as? String) + XCTAssertEqual(true, dict["true"] as? Bool) + XCTAssertEqual(10, dict["number"] as? Int) + } + + func testDecode_IntArray() throws { + // Arrange + let jsonData = #""" + { + "data": { + "array": [1, 2, 3] + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual([1, 2, 3], actual.data?["array"] as? [Int]) + } + + func testDecode_ArrayOfDicts() throws { + // Arrange + let date = TestCurrentDateProvider().date().addingTimeInterval(0.001) + let jsonData = #""" + { + "data": { + "array": [ + { + "dict1_string": "value", + "dict1_int": 1, + }, + { + "dict2_number": 0.1, + "dict2_date": "\#(sentry_toIso8601String(date))" + }, + ] + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let array = try XCTUnwrap(actual.data?["array"] as? [Any]) + XCTAssertEqual(2, array.count) + + let dict1 = try XCTUnwrap(array[0] as? [String: Any]) + + XCTAssertEqual("value", dict1["dict1_string"] as? String) + XCTAssertEqual(1, dict1["dict1_int"] as? Int) + + let dict2 = try XCTUnwrap(array[1] as? [String: Any]) + XCTAssertEqual(0.1, dict2["dict2_number"] as? Double) + let actualDate = try XCTUnwrap(dict2["dict2_date"] as? Date) + XCTAssertEqual(date.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 0.0001) + } + + func testDecode_NullValue() throws { + // Arrange + let jsonData = #""" + { + "data": { "null": null } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertEqual(NSNull(), actual.data?["null"] as? NSNull) + } + + func testDecode_GarbageJSON() { + // Arrange + let jsonData = #""" + { + "data": { + 1: "garbage" + } + } + """#.data(using: .utf8)! + + // Act & Assert + XCTAssertNil(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + } +} + +class DataWrapper: Decodable { + + var data: [String: Any]? + + enum CodingKeys: String, CodingKey { + case data + } + + required convenience public init(from decoder: any Decoder) throws { + self.init() + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.data = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .data) as [String: ArbitraryData]? + } + } +} diff --git a/Tests/SentryTests/Protocol/Codable/NSNumberDecodableWrapperTests.swift b/Tests/SentryTests/Protocol/Codable/NSNumberDecodableWrapperTests.swift new file mode 100644 index 00000000000..2b06a6d74f3 --- /dev/null +++ b/Tests/SentryTests/Protocol/Codable/NSNumberDecodableWrapperTests.swift @@ -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 + } +} diff --git a/Tests/SentryTests/Protocol/Codable/SentryCodableTests.swift b/Tests/SentryTests/Protocol/Codable/SentryCodableTests.swift new file mode 100644 index 00000000000..1ab40fcf20b --- /dev/null +++ b/Tests/SentryTests/Protocol/Codable/SentryCodableTests.swift @@ -0,0 +1,28 @@ +@testable import Sentry +import XCTest + +class SentryCodableTests: XCTestCase { + + func testDecodeWithEmptyData_ReturnsNil() { + XCTAssertNil(decodeFromJSONData(jsonData: Data()) as Geo?) + } + + func testDecodeWithGarbageData_ReturnsNil() { + let data = "garbage".data(using: .utf8)! + XCTAssertNil(decodeFromJSONData(jsonData: data) as Geo?) + } + + func testDecodeWithWrongJSON_ReturnsEmptyObject() { + let wrongJSON = "{\"wrong\": \"json\"}".data(using: .utf8)! + let actual = decodeFromJSONData(jsonData: wrongJSON) as Geo? + let expected = Geo() + + XCTAssertEqual(expected, actual) + } + + func testDecodeWithBrokenJSON_ReturnsNil() { + let brokenJSON = "{\"broken\": \"json\"".data(using: .utf8)! + XCTAssertNil(decodeFromJSONData(jsonData: brokenJSON) as Geo?) + } + +} diff --git a/Tests/SentryTests/Protocol/SentryFrameTests.swift b/Tests/SentryTests/Protocol/SentryFrameTests.swift index 3583304aee5..0aaf6c20f46 100644 --- a/Tests/SentryTests/Protocol/SentryFrameTests.swift +++ b/Tests/SentryTests/Protocol/SentryFrameTests.swift @@ -1,12 +1,16 @@ +@testable import Sentry import XCTest class SentryFrameTests: XCTestCase { func testSerialize() { + // Arrange let frame = TestData.mainFrame + // Act let actual = frame.serialize() + // Assert XCTAssertEqual(frame.symbolAddress, actual["symbol_addr"] as? String) XCTAssertEqual(frame.fileName, actual["filename"] as? String) XCTAssertEqual(frame.function, actual["function"] as? String) @@ -20,6 +24,51 @@ class SentryFrameTests: XCTestCase { XCTAssertEqual(frame.inApp, actual["in_app"] as? NSNumber) XCTAssertEqual(frame.stackStart, actual["stack_start"] as? NSNumber) } + + func testDecode_WithAllProperties() throws { + // Arrange + let frame = TestData.mainFrame + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: frame.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Frame?) + + // Assert + XCTAssertEqual(frame.symbolAddress, decoded.symbolAddress) + XCTAssertEqual(frame.fileName, decoded.fileName) + XCTAssertEqual(frame.function, decoded.function) + XCTAssertEqual(frame.module, decoded.module) + XCTAssertEqual(frame.lineNumber, decoded.lineNumber) + XCTAssertEqual(frame.columnNumber, decoded.columnNumber) + XCTAssertEqual(frame.package, decoded.package) + XCTAssertEqual(frame.imageAddress, decoded.imageAddress) + XCTAssertEqual(frame.platform, decoded.platform) + XCTAssertEqual(frame.inApp, decoded.inApp) + XCTAssertEqual(frame.stackStart, decoded.stackStart) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let frame = Frame() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: frame.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Frame?) + + // Assert + XCTAssertNil(decoded.symbolAddress) + XCTAssertNil(decoded.fileName) + XCTAssertEqual("", decoded.function) + XCTAssertNil(decoded.module) + XCTAssertNil(decoded.lineNumber) + XCTAssertNil(decoded.columnNumber) + XCTAssertNil(decoded.package) + XCTAssertNil(decoded.imageAddress) + XCTAssertNil(decoded.instructionAddress) + XCTAssertNil(decoded.platform) + XCTAssertNil(decoded.inApp) + XCTAssertNil(decoded.stackStart) + } func testSerialize_Bools() { SentryBooleanSerialization.test(Frame(), property: "inApp", serializedProperty: "in_app") diff --git a/Tests/SentryTests/Protocol/SentryGeoTests.swift b/Tests/SentryTests/Protocol/SentryGeoTests.swift index fd759f1c739..7ed65269613 100644 --- a/Tests/SentryTests/Protocol/SentryGeoTests.swift +++ b/Tests/SentryTests/Protocol/SentryGeoTests.swift @@ -1,6 +1,8 @@ +@testable import Sentry import XCTest class SentryGeoTests: XCTestCase { + func testSerializationWithAllProperties() throws { let geo = try XCTUnwrap(TestData.geo.copy() as? Geo) let actual = geo.serialize() @@ -15,6 +17,47 @@ class SentryGeoTests: XCTestCase { XCTAssertEqual(TestData.geo.region, actual["region"] as? String) } + func testSerialization_WithAllPropertiesNil() throws { + let geo = Geo() + + let actual = geo.serialize() + + XCTAssertNil(actual["city"]) + XCTAssertNil(actual["country_code"]) + XCTAssertNil(actual["region"]) + } + + func testSerialization_WithEmptyString() throws { + let geo = Geo() + geo.city = "" + + let actual = geo.serialize() + + XCTAssertEqual("", actual["city"] as? String) + XCTAssertNil(actual["country_code"]) + XCTAssertNil(actual["region"]) + } + + func testDecodeWithAllProperties() throws { + let geo = TestData.geo + let actual = geo.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + let decoded = decodeFromJSONData(jsonData: data) as Geo? + + XCTAssertEqual(geo, decoded) + } + + func testDecode_WithAllPropertiesNil() throws { + let geo = Geo() + let actual = geo.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + let decoded = decodeFromJSONData(jsonData: data) as Geo? + + XCTAssertEqual(geo, decoded) + } + func testHash() { XCTAssertEqual(TestData.geo.hash(), TestData.geo.hash()) diff --git a/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift b/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift index bb7f576271b..4bb87aab629 100644 --- a/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift +++ b/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift @@ -1,9 +1,10 @@ +@testable import Sentry import SentryTestUtils import XCTest class SentryMechanismMetaTests: XCTestCase { - func testSerialize() { + func testSerialize() throws { let sut = TestData.mechanismMeta let actual = sut.serialize() @@ -15,31 +16,15 @@ class SentryMechanismMetaTests: XCTestCase { let expected = TestData.mechanismMeta - guard let error = actual["ns_error"] as? [String: Any] else { - XCTFail("The serialization doesn't contain ns_error") - return - } - let nsError = expected.error! as SentryNSError - XCTAssertEqual(Dynamic(nsError).domain, error["domain"] as? String) - XCTAssertEqual(Dynamic(nsError).code, error["code"] as? Int) - - guard let signal = actual["signal"] as? [String: Any] else { - XCTFail("The serialization doesn't contain signal") - return - } - XCTAssertEqual(try XCTUnwrap(expected.signal?["number"] as? Int), try XCTUnwrap(signal["number"] as? Int)) - XCTAssertEqual(try XCTUnwrap(expected.signal?["code"] as? Int), try XCTUnwrap(signal["code"] as? Int)) - XCTAssertEqual(try XCTUnwrap(expected.signal?["name"] as? String), try XCTUnwrap(signal["name"] as? String)) - XCTAssertEqual(try XCTUnwrap(expected.signal?["code_name"] as? String), try XCTUnwrap(signal["code_name"] as? String)) - - guard let machException = actual["mach_exception"] as? [String: Any] else { - XCTFail("The serialization doesn't contain mach_exception") - return - } - XCTAssertEqual(try XCTUnwrap(expected.machException?["name"] as? String), try XCTUnwrap(machException["name"] as? String)) - XCTAssertEqual(try XCTUnwrap(expected.machException?["exception"] as? Int), try XCTUnwrap(machException["exception"] as? Int)) - XCTAssertEqual(try XCTUnwrap(expected.machException?["subcode"] as? Int), try XCTUnwrap(machException["subcode"] as? Int)) - XCTAssertEqual(try XCTUnwrap(expected.machException?["code"] as? Int), try XCTUnwrap(machException["code"] as? Int)) + let error = try XCTUnwrap(actual["ns_error"] as? [String: Any]) + + let nsError = try XCTUnwrap(expected.error) + XCTAssertEqual(nsError.domain, error["domain"] as? String) + XCTAssertEqual(nsError.code, error["code"] as? Int) + + try assertSignal(actual: actual["signal"] as? [String: Any], expected: expected.signal) + + try assertMachException(actual: actual["mach_exception"] as? [String: Any], expected: expected.machException) } func testSerialize_CallsSanitize() { @@ -57,5 +42,57 @@ class SentryMechanismMetaTests: XCTestCase { let signal = actual["signal"] as? [String: Any] XCTAssertEqual(self.description, try XCTUnwrap(signal?["a"] as? String)) } + + func testDecode_WithAllProperties() throws { + // Arrange + let sut = TestData.mechanismMeta + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: sut.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as MechanismMeta?) + + // Assert + try assertSignal(actual: decoded.signal, expected: sut.signal) + try assertMachException(actual: decoded.machException, expected: sut.machException) + XCTAssertEqual(sut.error?.code, decoded.error?.code) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let sut = TestData.mechanismMeta + sut.signal = nil + sut.machException = nil + sut.error = nil + + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: sut.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as MechanismMeta?) + + // Assert + XCTAssertNil(decoded.signal) + XCTAssertNil(decoded.machException) + XCTAssertNil(decoded.error) + } + + private func assertSignal(actual: [String: Any]?, expected: [String: Any]?) throws { + let actualNonNil = try XCTUnwrap(actual) + let expectedNonNil = try XCTUnwrap(expected) + + XCTAssertEqual(expectedNonNil["number"] as? Int, actualNonNil["number"] as? Int) + XCTAssertEqual(expectedNonNil["code"] as? Int, actualNonNil["code"] as? Int) + XCTAssertEqual(expectedNonNil["name"] as? String, actualNonNil["name"] as? String) + XCTAssertEqual(expectedNonNil["code_name"] as? String, actualNonNil["code_name"] as? String) + } + + private func assertMachException(actual: [String: Any]?, expected: [String: Any]?) throws { + let actualNonNil = try XCTUnwrap(actual) + let expectedNonNil = try XCTUnwrap(expected) + + XCTAssertEqual(expectedNonNil["name"] as? String, actualNonNil["name"] as? String) + XCTAssertEqual(expectedNonNil["exception"] as? Int, actualNonNil["exception"] as? Int) + XCTAssertEqual(expectedNonNil["subcode"] as? Int, actualNonNil["subcode"] as? Int) + XCTAssertEqual(expectedNonNil["code"] as? Int, actualNonNil["code"] as? Int) + } } diff --git a/Tests/SentryTests/Protocol/SentryMechanismTests.swift b/Tests/SentryTests/Protocol/SentryMechanismTests.swift index 30cbf8bc439..432cd13bbb7 100644 --- a/Tests/SentryTests/Protocol/SentryMechanismTests.swift +++ b/Tests/SentryTests/Protocol/SentryMechanismTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import SentryTestUtils import XCTest @@ -43,4 +44,40 @@ class SentryMechanismTests: XCTestCase { func testSerialize_Bools() { SentryBooleanSerialization.test(Mechanism(type: ""), property: "handled") } + + func testDecode_WithAllProperties() throws { + // Arrange + let expected = TestData.mechanism + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: expected.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Mechanism?) + + // Assert + XCTAssertEqual(expected.type, decoded.type) + XCTAssertEqual(expected.desc, decoded.desc) + XCTAssertEqual(expected.handled, decoded.handled) + XCTAssertEqual(expected.synthetic, decoded.synthetic) + XCTAssertEqual(expected.helpLink, decoded.helpLink) + + XCTAssertEqual(expected.meta?.error?.code, decoded.meta?.error?.code) + XCTAssertEqual(expected.meta?.error?.domain, decoded.meta?.error?.domain) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let expected = Mechanism(type: "type") + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: expected.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Mechanism?) + + // Assert + XCTAssertEqual(expected.type, decoded.type) + XCTAssertNil(decoded.desc) + XCTAssertNil(decoded.handled) + XCTAssertNil(decoded.synthetic) + XCTAssertNil(decoded.helpLink) + XCTAssertNil(decoded.meta?.error) + } } diff --git a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift index 95757c8544c..f0e33ec1ead 100644 --- a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift +++ b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryNSErrorTests: XCTestCase { @@ -10,6 +11,30 @@ class SentryNSErrorTests: XCTestCase { XCTAssertEqual(error.domain, actual["domain"] as? String) XCTAssertEqual(error.code, actual["code"] as? Int) } + + func testDecode_WithAllProperties() throws { + // Arrange + let error = SentryNSError(domain: "domain", code: 10) + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: error.serialize())) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryNSError?) + + // Assert + XCTAssertEqual(error.code, decoded.code) + XCTAssertEqual(error.domain, decoded.domain) + } + + func testDecode_WithRemovedDomain_ReturnsNil() throws { + // Arrange + let error = SentryNSError(domain: "domain", code: 10) + var serialized = error.serialize() + serialized.removeValue(forKey: "domain") + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: serialized)) + + // Act & Assert + XCTAssertNil(decodeFromJSONData(jsonData: data) as SentryNSError?) + } func testSerializeWithUnderlyingNSError() { let inputUnderlyingErrorCode = 5_123 diff --git a/Tests/SentryTests/Protocol/SentryStacktraceTests.swift b/Tests/SentryTests/Protocol/SentryStacktraceTests.swift index 1f2e7334a2e..02718dc3c85 100644 --- a/Tests/SentryTests/Protocol/SentryStacktraceTests.swift +++ b/Tests/SentryTests/Protocol/SentryStacktraceTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryStacktraceTests: XCTestCase { @@ -36,4 +37,67 @@ class SentryStacktraceTests: XCTestCase { func testSerialize_Bools() { SentryBooleanSerialization.test(SentryStacktrace(frames: [], registers: [:]), property: "snapshot") } + + func testDecode_WithAllProperties() throws { + // Arrange + let stacktrace = TestData.stacktrace + let serialized = stacktrace.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: serialized)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryStacktrace?) + + // Assert + XCTAssertEqual(stacktrace.frames.count, decoded.frames.count) + XCTAssertEqual(stacktrace.registers, decoded.registers) + XCTAssertEqual(stacktrace.snapshot, decoded.snapshot) + } + + func testDecode_MissingSnapshot() throws { + // Arrange + let stacktrace = TestData.stacktrace + stacktrace.snapshot = nil + let serialized = stacktrace.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: serialized)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryStacktrace?) + + // Assert + XCTAssertEqual(stacktrace.frames.count, decoded.frames.count) + XCTAssertEqual(stacktrace.registers, decoded.registers) + XCTAssertNil(decoded.snapshot) + } + + func testDecode_EmptyFrames() throws { + // Arrange + let stacktrace = TestData.stacktrace + stacktrace.frames = [] + let serialized = stacktrace.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: serialized)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryStacktrace?) + + // Assert + XCTAssertEqual(stacktrace.frames.count, decoded.frames.count) + XCTAssertEqual(stacktrace.registers, decoded.registers) + XCTAssertEqual(stacktrace.snapshot, decoded.snapshot) + } + + func testDecode_EmptyRegisters() throws { + // Arrange + let stacktrace = TestData.stacktrace + stacktrace.registers = [:] + let serialized = stacktrace.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: serialized)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryStacktrace?) + + // Assert + XCTAssertEqual(stacktrace.frames.count, decoded.frames.count) + XCTAssertEqual(stacktrace.registers, decoded.registers) + XCTAssertEqual(stacktrace.snapshot, decoded.snapshot) + } } diff --git a/Tests/SentryTests/Protocol/SentryUserTests.swift b/Tests/SentryTests/Protocol/SentryUserTests.swift index 2dd3d9fff75..50b6b15dc07 100644 --- a/Tests/SentryTests/Protocol/SentryUserTests.swift +++ b/Tests/SentryTests/Protocol/SentryUserTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest @available(*, deprecated) @@ -73,6 +74,32 @@ class SentryUserTests: XCTestCase { XCTAssertNil(actual["id"] as? String) } + func testDecode_WithAllProperties() throws { + // Arrange + let user = TestData.user + let actual = user.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = decodeFromJSONData(jsonData: data) as User? + + // Assert + XCTAssertEqual(user, decoded) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let user = User() + let actual = user.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = decodeFromJSONData(jsonData: data) as User? + + // Assert + XCTAssertEqual(user, decoded) + } + func testHash() { XCTAssertEqual(TestData.user.hash(), TestData.user.hash()) diff --git a/develop-docs/DECISIONS.md b/develop-docs/DECISIONS.md index 1d1b0c8d5ab..c518c696f1b 100644 --- a/develop-docs/DECISIONS.md +++ b/develop-docs/DECISIONS.md @@ -236,3 +236,153 @@ To enable visionOS support with the Sentry static framework, you need to set the However, C functions can still be accessed from code that is conditionally compiled using directives, such as `#if os(iOS)`. +## Deserializing Events + +Date: January 16, 2025 +Contributors: @brustolin, @philipphofmann, @armcknight, @philprime + +Decision: Mutual Agreement on Option B + +Comments: + +1. @philprime: I would prefer to manually (because automatically is not possible without external tools) write extensions of existing Objective-C classes to conform to Decodable, then use the JSONDecoder. If the variables of the classes/structs do not match the JSON spec (i.e. ipAddress in Swift, but ip_address serialized), we might have to implement custom CodingKeys anyways. +2. @brustolin: I agree with @philprime , manually writing the Decodable extensions for ObjC classes seems to be the best approach right now. +3. @armcknight: I think the best bet to get the actual work done that is needed is to go with option B, vs all the refactors that would be needed to use Codable to go with A. Then, protocol APIs could be migrated from ObjC to Swift as-needed and converted to Codable. +4. @philipphofmann: I think Option B/ manually deserializing is the way to go for now. I actually tried it and it seemed a bit wrong. I evaluated the other options and with all your input, I agree with doing it manually. We do it once and then all good. Thanks everyone. + +### Background +To report fatal app hangs and measure how long an app hangs lasts ([GH Epic](https://github.com/getsentry/sentry-cocoa/issues/4261)), we need to serialize events to disk, deserialize, modify, and send them to Sentry. As of January 14, 2025, the Cocoa SDK doesn’t support deserializing events. As the fatal app hangs must go through beforeSend, we can’t simply modify the serialized JSON stored on disk. Instead, we must deserialize the event JSON and initialize a SentryEvent so that it can go through beforeSend. + +As of January 14, 2025, all the serialization is custom-made with the [SentrySerializable](https://github.com/getsentry/sentry-cocoa/blob/main/Sources/Sentry/Public/SentrySerializable.h) protocol: + +```objectivec +@protocol SentrySerializable + +- (NSDictionary *)serialize; + +@end +``` + +The SDK manually creates a JSON-like dict: + +```objectivec +- (NSDictionary *)serialize +{ + return @{ @"city" : self.city, @"country_code" : self.countryCode, @"region" : self.region }; +} +``` + +And then the [SentryEnvelope](https://github.com/getsentry/sentry-cocoa/blob/72e34fae44b817d8c12490bbc5c1ce7540f86762/Sources/Sentry/SentryEnvelope.m#L70-L90) calls serialize on the event and then converts it to JSON data. + +```objectivec + NSData *json = [SentrySerialization dataWithJSONObject:[event serialize]]; +``` + +To implement a deserialized method, we would need to manually implement the counterparts, which is plenty of code. As ObjC is slowly dying out and the future is Swift, we would like to avoid writing plenty of ObjC code that we will convert to Swift in the future. + +### Option A: Use Swifts Built In Codable and convert Serializable Classes to Swift + +As Swift has a built-in [Decoding and Encoding](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) mechanisms it makes sense to explore this option. + +Serializing a struct in Swift to JSON is not much code: + +```objectivec +struct Language: Codable { + var name: String + var version: Int +} + +let swift = Language(name: "Swift", version: 6) + +let encoder = JSONEncoder() +if let encoded = try? encoder.encode(swift) { + // save `encoded` somewhere +} +``` + +The advantage is that we don’t have to manually create the dictionaries in serialize and a potential deserialize method. The problem is that this only works with Swift structs and classes. We can’t use Swift structs, as they’re not working in ObjC. So, we need to convert the classes to serialize and deserialize to Swift. + +The major challenge is that doing this without breaking changes for both Swift and ObjC is extremely hard to achieve. One major problem is that some existing classes such as SentryUser overwrite the `- (NSUInteger)hash` method, which is `var hash: [Int](https://developer.apple.com/documentation/swift/int) { get }` in Swift. When converting SentryUser to Swift, calling `user.hash()` converts to `user.hash`. While most of our users don’t call this method, it still is a breaking change. And that’s only one issue we found when converting classes to Swift. + +To do this conversion safely, we should do it in a major release. We need to convert all public protocol classes to Swift. Maybe it even makes sense to convert all public classes to Swift to avoid issues with our package managers that get confused when there is a mix of public classes of Swift and ObjC. SPM, for example, doesn’t allow this, and we need to precompile our SDK to be compatible. + +The [SentryEnvelope](https://github.com/getsentry/sentry-cocoa/blob/72e34fae44b817d8c12490bbc5c1ce7540f86762/Sources/Sentry/SentryEnvelope.m#L70-L90) first creates a JSON dict and then converts it to JSON data. Instead, we could directly use the Swift JSONEncoder to save one step in between. This would convert the classes to JSON data directly. + +```objectivec + NSData *json = [SentrySerialization dataWithJSONObject:[event serialize]]; +``` + +All that said, I suggest converting all public protocol classes to Swift and switching to Swift Codable for serialization, cause it will be less code and more future-proof. Of course, we will run into problems and challenges on the way, but it’s worth doing it. + +#### Pros + +1. Less code. +2. More Swift code is more future-proof. + +#### Cons + +- Major release +- Risk of adding bugs + +### Option B: Add Deserialize in Swift + +We could implement all deserializing code in Swift without requiring a major version. The implementation would be the counterpart of ObjC serialize implementations, but written in Swift. + +#### Pros + +1. No major +2. Low risk of introducing bugs +3. Full control of serializing and deserializing + +#### Cons + +1. Potentially slightly higher maintenance effort, which is negligible as we hardly change the protocol classes. + +*Sample for implementation of Codable:* + +```swift +@_implementationOnly import _SentryPrivate +import Foundation + +// User is the Swift name of SentryUser +extension User: Codable { + private enum CodingKeys: String, CodingKey { + case id + case email + case username + case ipAddress + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(userId, forKey: .id) + try container.encode(email, forKey: .email) + try container.encode(username, forKey: .username) + try container.encode(ipAddress, forKey: .ipAddress) + } + + public required convenience init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.init(userId: try container.decode(String.self, forKey: .id)) + email = try container.decode(String.self, forKey: .email) + username = try container.decode(String.self, forKey: .username) + ipAddress = try container.decode(String.self, forKey: .ipAddress) + } +} +``` + +### Option C: Duplicate protocol classes and use Swift Codable + +We do option A, but we keep the public ObjC classes, duplicate them in Swift, and use the internal Swift classes only for serializing and deserializing. Once we have a major release, we replace the ObjC classes with the internal Swift classes. + +We can also start with this option to evaluate Swift Codable and switch to option A once we’re confident it’s working correctly. + +#### Pros + +1. No major. +2. We can refactor it step by step. +3. The risk of introducing bugs can be distributed across multiple releases. + +#### Cons + +1. Duplicate code.