diff --git a/CHANGELOG.md b/CHANGELOG.md index 778fcd64ee7..ac92441bbbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ - Fix missing `sample_rate` in baggage (#4751) -### Internal +### Internal +- Deserializing SentryEvents with Decodable (#4724) - Remove internal unknown dict for Breadcrumbs (#4803) This potentially only impacts hybrid SDKs. ## 8.44.0 diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 94772880f46..27f574d950b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -78,30 +78,47 @@ 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 */; }; + 6205CF262D549D8A001E6049 /* SentryDateCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6205CF252D549D8A001E6049 /* SentryDateCodableTests.swift */; }; 621AE74B2C626C230012E730 /* SentryANRTrackerV2.h in Headers */ = {isa = PBXBuildFile; fileRef = 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */; }; 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; }; 621C88482CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 621C88472CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h */; }; 621C884A2CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 621C88492CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm */; }; 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; }; 621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; }; + 62212B872D520CB00062C2FA /* SentryEventCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62212B862D520CB00062C2FA /* SentryEventCodable.swift */; }; 6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6221BBC92CAA932100C627CA /* SentryANRType.swift */; }; 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, ); }; }; + 627C77892D50B6840055E966 /* SentryBreadcrumbCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627C77882D50B6840055E966 /* SentryBreadcrumbCodable.swift */; }; 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 */; }; + 6283085F2D50AA8C00EAEF77 /* SentryMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6283085E2D50AA8C00EAEF77 /* SentryMessage.swift */; }; + 628308612D50ADAC00EAEF77 /* SentryRequestCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628308602D50ADAC00EAEF77 /* SentryRequestCodable.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 */; }; + 629194A92D51F976000F7C6B /* SentryDebugMetaCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629194A82D51F976000F7C6B /* SentryDebugMetaCodable.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 */; }; @@ -113,6 +130,7 @@ 62AB8C9E2BF3925700BFC2AC /* WeakReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62AB8C9D2BF3925700BFC2AC /* WeakReference.swift */; }; 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */; }; 62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */; }; + 62BDDD122D51FD540024CCD1 /* SentryThreadCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BDDD112D51FD540024CCD1 /* SentryThreadCodable.swift */; }; 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 62C1AFAA2B7E10EA0038C5F7 /* SentrySpotlightTransport.m */; }; 62C25C862B075F4900C68CBD /* TestOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C25C852B075F4900C68CBD /* TestOptions.swift */; }; 62C316812B1F2E93000D7031 /* SentryDelayedFramesTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */; }; @@ -127,10 +145,13 @@ 62D6B2A72CCA354B004DDBF1 /* SentryUncaughtNSExceptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D6B2A62CCA354B004DDBF1 /* SentryUncaughtNSExceptionsTests.swift */; }; 62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */; }; 62E081AB29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */; }; + 62E300942D5202890037AA3F /* SentryExceptionCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E300932D5202830037AA3F /* SentryExceptionCodable.swift */; }; 62EF86A12C626D39004E058B /* SentryANRTrackerV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */; }; 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 */; }; @@ -1113,9 +1134,14 @@ 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 = ""; }; + 6205CF252D549D8A001E6049 /* SentryDateCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDateCodableTests.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 = ""; }; @@ -1123,21 +1149,33 @@ 621C88492CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryCaptureTransactionWithProfile.mm; sourceTree = ""; }; 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = ""; }; + 62212B862D520CB00062C2FA /* SentryEventCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEventCodable.swift; sourceTree = ""; }; 6221BBC92CAA932100C627CA /* SentryANRType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRType.swift; sourceTree = ""; }; 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySpanContext+Private.h"; path = "include/SentrySpanContext+Private.h"; sourceTree = ""; }; 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 = ""; }; + 627C77882D50B6840055E966 /* SentryBreadcrumbCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbCodable.swift; 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 = ""; }; + 6283085E2D50AA8C00EAEF77 /* SentryMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMessage.swift; sourceTree = ""; }; + 628308602D50ADAC00EAEF77 /* SentryRequestCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestCodable.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 = ""; }; + 629194A82D51F976000F7C6B /* SentryDebugMetaCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDebugMetaCodable.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 = ""; }; @@ -1149,6 +1187,7 @@ 62AB8C9D2BF3925700BFC2AC /* WeakReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakReference.swift; sourceTree = ""; }; 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFramesDelayResult.swift; sourceTree = ""; }; 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTestLogConfig.m; sourceTree = ""; }; + 62BDDD112D51FD540024CCD1 /* SentryThreadCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryThreadCodable.swift; sourceTree = ""; }; 62C1AFA92B7E10D30038C5F7 /* SentrySpotlightTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpotlightTransport.h; path = include/SentrySpotlightTransport.h; sourceTree = ""; }; 62C1AFAA2B7E10EA0038C5F7 /* SentrySpotlightTransport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpotlightTransport.m; sourceTree = ""; }; 62C25C852B075F4900C68CBD /* TestOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOptions.swift; sourceTree = ""; }; @@ -1163,11 +1202,14 @@ 62D6B2A62CCA354B004DDBF1 /* SentryUncaughtNSExceptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUncaughtNSExceptionsTests.swift; sourceTree = ""; }; 62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBreadcrumbDelegate.h; path = include/SentryBreadcrumbDelegate.h; sourceTree = ""; }; 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbTestDelegate.swift; sourceTree = ""; }; + 62E300932D5202830037AA3F /* SentryExceptionCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExceptionCodable.swift; sourceTree = ""; }; 62F05D292C0DB1C800916E3F /* SentryLogTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryLogTestHelper.h; sourceTree = ""; }; 62F05D2A2C0DB1F100916E3F /* SentryLogTestHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLogTestHelper.m; sourceTree = ""; }; 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 = ""; }; @@ -1898,9 +1940,9 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; 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 = ""; }; - D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D41909922D48FFF6002B83D0 /* SentryNSDictionarySanitize+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryNSDictionarySanitize+Tests.h"; sourceTree = ""; }; D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryNSDictionarySanitize+Tests.m"; sourceTree = ""; }; + D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; @@ -2177,6 +2219,41 @@ 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 */, + 627C77882D50B6840055E966 /* SentryBreadcrumbCodable.swift */, + 628308602D50ADAC00EAEF77 /* SentryRequestCodable.swift */, + 620078732D38F0DF0022CB67 /* SentryCodable.swift */, + 6283085E2D50AA8C00EAEF77 /* SentryMessage.swift */, + 62E300932D5202830037AA3F /* SentryExceptionCodable.swift */, + 62BDDD112D51FD540024CCD1 /* SentryThreadCodable.swift */, + 629194A82D51F976000F7C6B /* SentryDebugMetaCodable.swift */, + 62212B862D520CB00062C2FA /* SentryEventCodable.swift */, + ); + path = Codable; + sourceTree = ""; + }; + 620078762D3906AD0022CB67 /* Codable */ = { + isa = PBXGroup; + children = ( + 623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */, + 6281C5732D3E50D8009D0978 /* ArbitraryDataTests.swift */, + 620078772D3906BF0022CB67 /* SentryCodableTests.swift */, + 6205CF252D549D8A001E6049 /* SentryDateCodableTests.swift */, + ); + path = Codable; + sourceTree = ""; + }; 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( @@ -2949,6 +3026,7 @@ 7B3D0474249A3D5800E106B6 /* Protocol */ = { isa = PBXGroup; children = ( + 620078762D3906AD0022CB67 /* Codable */, 7BC6EBF7255C05060059822A /* TestData.swift */, 7B869EBB249B91D8004F4FDB /* SentryDebugMetaEquality.swift */, 7B869EBD249B964D004F4FDB /* SentryThreadEquality.swift */, @@ -4081,6 +4159,7 @@ D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( + 620078752D38F1110022CB67 /* Codable */, 64F9571C2D12DA1800324652 /* SentryViewControllerBreadcrumbTracking.swift */, D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, @@ -4722,6 +4801,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 */, @@ -4740,6 +4820,7 @@ 63AA75EF1EB8B3C400D153DE /* SentryClient.m in Sources */, 7B7D873624864C9D00D2ECFF /* SentryCrashDefaultMachineContextWrapper.m in Sources */, 63FE712F20DA4C1100CDBAE8 /* SentryCrashSysCtl.c in Sources */, + 62212B872D520CB00062C2FA /* SentryEventCodable.swift in Sources */, 7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */, 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */, 84CFA4CA2C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift in Sources */, @@ -4754,6 +4835,8 @@ 03F84D3727DD4191008FE43F /* SentrySamplingProfiler.cpp in Sources */, 8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */, 7B08A3472924CF9C0059603A /* SentryMetricKitIntegration.m in Sources */, + 623FD9022D3FA5E000803EDA /* SentryFrameCodable.swift in Sources */, + 62BDDD122D51FD540024CCD1 /* SentryThreadCodable.swift in Sources */, 7B63459B280EB9E200CFA05A /* SentryUIEventTrackingIntegration.m in Sources */, D8AE48AE2C577EAB0092A2A6 /* SentryLog.swift in Sources */, 15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */, @@ -4761,10 +4844,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 */, @@ -4783,6 +4868,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 */, @@ -4811,6 +4897,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 */, @@ -4837,10 +4924,13 @@ 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 */, + 627C77892D50B6840055E966 /* SentryBreadcrumbCodable.swift in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, @@ -4869,8 +4959,10 @@ 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 */, + 6283085F2D50AA8C00EAEF77 /* SentryMessage.swift in Sources */, 7BC63F0A28081288009D9E37 /* SentrySwizzleWrapper.m in Sources */, 7B6C5EDC264E8DA80010D138 /* SentryFramesTrackingIntegration.m in Sources */, 849B8F9A2C6E906900148E1F /* SentryUserFeedbackConfiguration.swift in Sources */, @@ -4894,6 +4986,7 @@ 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, + 628308612D50ADAC00EAEF77 /* SentryRequestCodable.swift in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, D8739D182BEEA33F007D2F66 /* SentryLevelHelper.m in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, @@ -4906,6 +4999,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 */, @@ -5007,8 +5101,10 @@ 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, 63FE713520DA4C1100CDBAE8 /* SentryCrashMemory.c in Sources */, D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */, + 629194A92D51F976000F7C6B /* SentryDebugMetaCodable.swift in Sources */, 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryAsyncSafeLog.c in Sources */, + 62E300942D5202890037AA3F /* SentryExceptionCodable.swift in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, D8BC28CA2BFF68CA0054DA4D /* NumberExtensions.swift in Sources */, @@ -5159,6 +5255,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 */, @@ -5202,8 +5299,10 @@ 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 */, + 6205CF262D549D8A001E6049 /* SentryDateCodableTests.swift in Sources */, 7BC6EBF8255C05060059822A /* TestData.swift in Sources */, 7B88F30224BC5C6D00ADF90A /* SentrySdkInfoTests.swift in Sources */, 7BC8523B2458849E005A70F0 /* SentryDataCategoryMapperTests.swift in Sources */, @@ -5287,6 +5386,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/Public/SentryBreadcrumb.h b/Sources/Sentry/Public/SentryBreadcrumb.h index 181cf8f9ad0..1d14716a3a8 100644 --- a/Sources/Sentry/Public/SentryBreadcrumb.h +++ b/Sources/Sentry/Public/SentryBreadcrumb.h @@ -39,6 +39,12 @@ NS_SWIFT_NAME(Breadcrumb) */ @property (nonatomic, copy, nullable) NSString *message; +/** + * Origin of the breadcrumb that is used to identify source of the breadcrumb + * For example hybrid SDKs can identify native breadcrumbs from JS or Flutter + */ +@property (nonatomic, copy, nullable) NSString *origin; + /** * Arbitrary additional data that will be sent with the breadcrumb */ diff --git a/Sources/Sentry/Public/SentryEvent.h b/Sources/Sentry/Public/SentryEvent.h index 183fe7ed903..aed5c54fd20 100644 --- a/Sources/Sentry/Public/SentryEvent.h +++ b/Sources/Sentry/Public/SentryEvent.h @@ -196,4 +196,24 @@ NS_SWIFT_NAME(Event) @end +/** + * Subclass of SentryEvent so we can add the Decodable implementation via a Swift extension. We need + * this due to our mixed use of public Swift and ObjC classes. We could avoid this class by + * converting SentryReplayEvent back to ObjC, but we rather accept this tradeoff as we want to + * convert all public classes to Swift in the future. This class needs to be public as we can't add + * the Decodable extension implementation to a class that is not public. + * + * @note: We can’t add the extension for Decodable directly on SentryEvent, because we get an error + * in SentryReplayEvent: 'required' initializer 'init(from:)' must be provided by subclass of + * 'Event' Once we add the initializer with required convenience public init(from decoder: any + * Decoder) throws { fatalError("init(from:) has not been implemented") + * } + * we get the error initializer 'init(from:)' is declared in extension of 'Event' and cannot be + * overridden. Therefore, we add the Decodable implementation not on the Event, but to a subclass of + * the event. + */ +@interface SentryEventDecodable : SentryEvent + +@end + NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryEvent.m b/Sources/Sentry/SentryEvent.m index c5478226b18..8033d1a7e34 100644 --- a/Sources/Sentry/SentryEvent.m +++ b/Sources/Sentry/SentryEvent.m @@ -210,4 +210,8 @@ - (BOOL)isAppHangEvent @end +@implementation SentryEventDecodable + +@end + NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryGeo.m b/Sources/Sentry/SentryGeo.m index c9a5e0625cf..b913713cbf8 100644 --- a/Sources/Sentry/SentryGeo.m +++ b/Sources/Sentry/SentryGeo.m @@ -1,4 +1,5 @@ #import "SentryGeo.h" +#import "SentrySwift.h" NS_ASSUME_NONNULL_BEGIN @@ -19,7 +20,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/SentryLevelHelper.m b/Sources/Sentry/SentryLevelHelper.m index 8455fb4c4bf..aa7a73b0e17 100644 --- a/Sources/Sentry/SentryLevelHelper.m +++ b/Sources/Sentry/SentryLevelHelper.m @@ -1,9 +1,21 @@ #import "SentryLevelHelper.h" #import "SentryBreadcrumb+Private.h" +#import "SentryEvent.h" @implementation SentryLevelBridge : NSObject + (NSUInteger)breadcrumbLevel:(SentryBreadcrumb *)breadcrumb { return breadcrumb.level; } + ++ (void)setBreadcrumbLevel:(SentryBreadcrumb *)breadcrumb level:(NSUInteger)level +{ + breadcrumb.level = level; +} + ++ (void)setBreadcrumbLevelOnEvent:(SentryEvent *)event level:(NSUInteger)level +{ + event.level = level; +} + @end diff --git a/Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h b/Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h index 2430d433ebf..c3a9e56056e 100644 --- a/Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h +++ b/Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h @@ -4,13 +4,9 @@ # import "SentryBreadcrumb.h" #endif -@interface SentryBreadcrumb () +NS_ASSUME_NONNULL_BEGIN -/** - * Origin of the breadcrumb that is used to identify source of the breadcrumb - * For example hybrid SDKs can identify native breadcrumbs from JS or Flutter - */ -@property (nonatomic, copy, nullable) NSString *origin; +@interface SentryBreadcrumb () /** * Initializes a SentryBreadcrumb from a JSON object. @@ -19,3 +15,5 @@ */ - (instancetype _Nonnull)initWithDictionary:(NSDictionary *_Nonnull)dictionary; @end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryDateUtils.h b/Sources/Sentry/include/SentryDateUtils.h index 68914ee41a8..371a6d2d680 100644 --- a/Sources/Sentry/include/SentryDateUtils.h +++ b/Sources/Sentry/include/SentryDateUtils.h @@ -2,6 +2,8 @@ NS_ASSUME_NONNULL_BEGIN +SENTRY_EXTERN NSDateFormatter *sentryGetIso8601FormatterWithMillisecondPrecision(void); + SENTRY_EXTERN NSDate *sentry_fromIso8601String(NSString *string); SENTRY_EXTERN NSString *sentry_toIso8601String(NSDate *date); diff --git a/Sources/Sentry/include/SentryLevelHelper.h b/Sources/Sentry/include/SentryLevelHelper.h index 13368a76d61..23cf1695b13 100644 --- a/Sources/Sentry/include/SentryLevelHelper.h +++ b/Sources/Sentry/include/SentryLevelHelper.h @@ -3,12 +3,15 @@ NS_ASSUME_NONNULL_BEGIN @class SentryBreadcrumb; +@class SentryEvent; /** * This is a workaround to access SentryLevel value from swift */ @interface SentryLevelBridge : NSObject + (NSUInteger)breadcrumbLevel:(SentryBreadcrumb *)breadcrumb; ++ (void)setBreadcrumbLevel:(SentryBreadcrumb *)breadcrumb level:(NSUInteger)level; ++ (void)setBreadcrumbLevelOnEvent:(SentryEvent *)event level:(NSUInteger)level; @end NS_ASSUME_NONNULL_END 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..3a61feb3f36 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/DecodeArbitraryData.swift @@ -0,0 +1,134 @@ +@_implementationOnly import _SentryPrivate + +/// Represents arbitrary data that can be decoded from JSON with Decodable. +/// +/// - Note: Some classes on the protocol allow adding extra data in a dictionary of type String:Any. +/// Users can put anything in there that can be serialized to JSON. The SDK uses JSONSerialization to +/// serialize these dictionaries. At first glance, you could assume that we can use JSONSerialization.jsonObject(with:options) +/// to deserialize these dictionaries, but we can't. When using Decodable, you don't have access to the raw +/// data of the JSON. The Decoder and the DecodingContainers don't offer methods to access the underlying +/// data. The Swift Decodable converts the raw data to a JSON object and then casts the JSON object to the +/// class that implements the Decodable protocol, see: +/// https://github.com/swiftlang/swift-foundation/blob/e9d59b6065ad909fee15f174bd5ca2c580490388/Sources/FoundationEssentials/JSON/JSONDecoder.swift#L360-L386 +/// https://github.com/swiftlang/swift-foundation/blob/e9d59b6065ad909fee15f174bd5ca2c580490388/Sources/FoundationEssentials/JSON/JSONScanner.swift#L343-L383 + +/// Therefore, we have to implement decoding the arbitrary dictionary manually. +/// +/// A discarded option is to decode the JSON raw data twice: once with Decodable and once with the JSONSerialization. +/// This has two significant downsides: First, we deserialize the JSON twice, which is a performance overhead. Second, +/// we don't conform to the Decodable protocol, which could lead to unwanted hard-to-detect problems in the future. +enum ArbitraryData: Decodable { + case string(String) + case int(Int) + case number(Double) + case boolean(Bool) + case date(Date) + case dict([String: ArbitraryData]) + case array([ArbitraryData]) + case null + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + // The order here matters as we're dealing with arbitrary data. + // We have to check the double before the Date, because otherwise + // a double value could turn into a Date. So only ISO 8601 string formatted + // dates work, which sanitizeArray and sentry_sanitize use. + // We must check String after Date, because otherwise we would turn a ISO 8601 + // string into a string and not a date. + if let intValue = try? container.decode(Int.self) { + self = .int(intValue) + } else if let numberValue = try? container.decode(Double.self) { + self = .number(numberValue) + } else if let boolValue = try? container.decode(Bool.self) { + self = .boolean(boolValue) + } else if let dateValue = try? container.decode(Date.self) { + self = .date(dateValue) + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let objectValue = try? container.decode([String: ArbitraryData].self) { + self = .dict(objectValue) + } else if let arrayValue = try? container.decode([ArbitraryData].self) { + self = .array(arrayValue) + } else if container.decodeNil() { + self = .null + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid JSON value" + ) + } + } +} + +func decodeArbitraryData(decode: () throws -> [String: ArbitraryData]?) -> [String: Any]? { + do { + let rawData = try decode() + if rawData == nil { + return nil + } + + return unwrapArbitraryDict(rawData) + } catch { + SentryLog.error("Failed to decode raw data: \(error)") + return nil + } +} + +func decodeArbitraryData(decode: () throws -> [String: [String: ArbitraryData]]?) -> [String: [String: Any]]? { + do { + let rawData = try decode() + if rawData == nil { + return nil + } + + var newData = [String: [String: Any]]() + for (key, value) in rawData ?? [:] { + newData[key] = unwrapArbitraryDict(value) + } + + return newData + } catch { + SentryLog.error("Failed to decode raw data: \(error)") + return nil + } +} + +private func unwrapArbitraryDict(_ dict: [String: ArbitraryData]?) -> [String: Any]? { + guard let nonNullDict = dict else { + return nil + } + + return nonNullDict.mapValues { unwrapArbitraryValue($0) as Any } +} + +private func unwrapArbitraryArray(_ array: [ArbitraryData]?) -> [Any]? { + guard let nonNullArray = array else { + return nil + } + + return nonNullArray.map { unwrapArbitraryValue($0) as Any } +} + +private func unwrapArbitraryValue(_ value: ArbitraryData?) -> Any? { + switch value { + case .string(let stringValue): + return stringValue + case .number(let numberValue): + return numberValue + case .int(let intValue): + return intValue + case .boolean(let boolValue): + return boolValue + case .date(let dateValue): + return dateValue + case .dict(let dictValue): + return unwrapArbitraryDict(dictValue) + case .array(let arrayValue): + return unwrapArbitraryArray(arrayValue) + case .null: + return NSNull() + case .none: + return nil + } +} 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/SentryBreadcrumbCodable.swift b/Sources/Swift/Protocol/Codable/SentryBreadcrumbCodable.swift new file mode 100644 index 00000000000..2c0cdf1e0f8 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryBreadcrumbCodable.swift @@ -0,0 +1,35 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension Breadcrumb: Decodable { + + private enum CodingKeys: String, CodingKey { + case level + case category + case timestamp + case type + case message + case data + case origin + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.init() + + let rawLevel = try container.decode(String.self, forKey: .level) + let level = SentryLevelHelper.levelForName(rawLevel) + SentryLevelBridge.setBreadcrumbLevel(self, level: level.rawValue) + + self.category = try container.decode(String.self, forKey: .category) + self.timestamp = try container.decodeIfPresent(Date.self, forKey: .timestamp) + self.type = try container.decodeIfPresent(String.self, forKey: .type) + self.message = try container.decodeIfPresent(String.self, forKey: .message) + self.origin = try container.decodeIfPresent(String.self, forKey: .origin) + + self.data = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .data) + } + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryCodable.swift b/Sources/Swift/Protocol/Codable/SentryCodable.swift new file mode 100644 index 00000000000..9f158b9b246 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryCodable.swift @@ -0,0 +1,41 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +func decodeFromJSONData(jsonData: Data) -> T? { + if jsonData.isEmpty { + return nil + } + + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + + // We prefer a Double/TimeInterval because it allows nano second precision. + // The ISO8601 formatter only supports millisecond precision. + if let timeIntervalSince1970 = try? container.decode(Double.self) { + return Date(timeIntervalSince1970: timeIntervalSince1970) + } + + if let dateString = try? container.decode(String.self) { + let formatter = sentryGetIso8601FormatterWithMillisecondPrecision() + guard let date = formatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format. The following string doesn't represent a valid ISO 8601 date string: '\(dateString)'") + } + + return date + } + + throw DecodingError.typeMismatch(Date.self, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Invalid date format. The Date must either be a Double/TimeInterval representing the timeIntervalSince1970 or it can be a ISO 8601 formatted String." + )) + + } + return try decoder.decode(T.self, from: jsonData) + } catch { + SentryLog.error("Could not decode object of type \(T.self) from JSON data due to error: \(error)") + } + + return nil +} diff --git a/Sources/Swift/Protocol/Codable/SentryDebugMetaCodable.swift b/Sources/Swift/Protocol/Codable/SentryDebugMetaCodable.swift new file mode 100644 index 00000000000..84340ba3a35 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryDebugMetaCodable.swift @@ -0,0 +1,32 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension DebugMeta: Decodable { + + private enum CodingKeys: String, CodingKey { + case uuid + case debugID = "debug_id" + case type + case name + case imageSize = "image_size" + case imageAddress = "image_addr" + case imageVmAddress = "image_vmaddr" + case codeFile = "code_file" + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.init() + + self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) + self.debugID = try container.decodeIfPresent(String.self, forKey: .debugID) + self.type = try container.decodeIfPresent(String.self, forKey: .type) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.imageSize = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .imageSize))?.value + self.imageAddress = try container.decodeIfPresent(String.self, forKey: .imageAddress) + self.imageVmAddress = try container.decodeIfPresent(String.self, forKey: .imageVmAddress) + self.codeFile = try container.decodeIfPresent(String.self, forKey: .codeFile) + + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryEventCodable.swift b/Sources/Swift/Protocol/Codable/SentryEventCodable.swift new file mode 100644 index 00000000000..5ab245db605 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryEventCodable.swift @@ -0,0 +1,97 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryEventDecodable: Decodable { + + private enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case message + // Leaving out error on purpose, it's not serialized. + case timestamp + case startTimestamp = "start_timestamp" + case level + case platform + case logger + case serverName = "server_name" + case releaseName = "release" + case dist + case environment + case transaction + case type + case tags + case extra + case sdk + case modules + case fingerprint + case user + case context = "contexts" + case threads + case exception + case stacktrace + case debugMeta = "debug_meta" + case breadcrumbs + case request + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.init() + + let eventIdAsString = try container.decode(String.self, forKey: .eventId) + self.eventId = SentryId(uuidString: eventIdAsString) + self.message = try container.decodeIfPresent(SentryMessage.self, forKey: .message) + self.timestamp = try container.decode(Date.self, forKey: .timestamp) + self.startTimestamp = try container.decodeIfPresent(Date.self, forKey: .startTimestamp) + + if let rawLevel = try container.decodeIfPresent(String.self, forKey: .level) { + let level = SentryLevelHelper.levelForName(rawLevel) + SentryLevelBridge.setBreadcrumbLevelOn(self, level: level.rawValue) + } else { + SentryLevelBridge.setBreadcrumbLevelOn(self, level: + SentryLevel.none.rawValue) + } + + self.platform = try container.decode(String.self, forKey: .platform) + self.logger = try container.decodeIfPresent(String.self, forKey: .logger) + self.serverName = try container.decodeIfPresent(String.self, forKey: .serverName) + self.releaseName = try container.decodeIfPresent(String.self, forKey: .releaseName) + self.dist = try container.decodeIfPresent(String.self, forKey: .dist) + self.environment = try container.decodeIfPresent(String.self, forKey: .environment) + self.transaction = try container.decodeIfPresent(String.self, forKey: .transaction) + self.type = try container.decodeIfPresent(String.self, forKey: .type) + self.tags = try container.decodeIfPresent([String: String].self, forKey: .tags) + + self.extra = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .extra) + } + self.sdk = decodeArbitraryData { + try container.decodeIfPresent([String: ArbitraryData].self, forKey: .sdk) + } + + self.modules = try container.decodeIfPresent([String: String].self, forKey: .modules) + self.fingerprint = try container.decodeIfPresent([String].self, forKey: .fingerprint) + self.user = try container.decodeIfPresent(User.self, forKey: .user) + + self.context = decodeArbitraryData { + try container.decodeIfPresent([String: [String: ArbitraryData]].self, forKey: .context) + } + + if let rawThreads = try container.decodeIfPresent([String: [SentryThread]].self, forKey: .threads) { + self.threads = rawThreads["values"] + } + + if let rawExceptions = try container.decodeIfPresent([String: [Exception]].self, forKey: .exception) { + self.exceptions = rawExceptions["values"] + } + + self.stacktrace = try container.decodeIfPresent(SentryStacktrace.self, forKey: .stacktrace) + + if let rawDebugMeta = try container.decodeIfPresent([String: [DebugMeta]].self, forKey: .debugMeta) { + self.debugMeta = rawDebugMeta["images"] + } + + self.breadcrumbs = try container.decodeIfPresent([Breadcrumb].self, forKey: .breadcrumbs) + self.request = try container.decodeIfPresent(SentryRequest.self, forKey: .request) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryExceptionCodable.swift b/Sources/Swift/Protocol/Codable/SentryExceptionCodable.swift new file mode 100644 index 00000000000..70b407453d1 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryExceptionCodable.swift @@ -0,0 +1,28 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension Exception: Decodable { + + private enum CodingKeys: String, CodingKey { + case value + case type + case mechanism + case module + case threadId = "thread_id" + case stacktrace + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let value = try container.decode(String.self, forKey: .value) + let type = try container.decode(String.self, forKey: .type) + + self.init(value: value, type: type) + + self.mechanism = try container.decodeIfPresent(Mechanism.self, forKey: .mechanism) + self.module = try container.decodeIfPresent(String.self, forKey: .module) + self.threadId = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .threadId)?.value + self.stacktrace = try container.decodeIfPresent(SentryStacktrace.self, forKey: .stacktrace) + } +} 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..53d7535aa7d --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryGeoCodable.swift @@ -0,0 +1,20 @@ +@_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..cadbc4e2837 --- /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/SentryMessage.swift b/Sources/Swift/Protocol/Codable/SentryMessage.swift new file mode 100644 index 00000000000..267a14209f1 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryMessage.swift @@ -0,0 +1,21 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryMessage: Decodable { + + private enum CodingKeys: String, CodingKey { + case formatted + case message + case params + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let formatted = try container.decode(String.self, forKey: .formatted) + self.init(formatted: formatted) + + self.message = try container.decodeIfPresent(String.self, forKey: .message) + self.params = try container.decodeIfPresent([String].self, forKey: .params) + } +} diff --git a/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift b/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift new file mode 100644 index 00000000000..088882b878a --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryNSErrorCodable.swift @@ -0,0 +1,18 @@ +@_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/SentryRequestCodable.swift b/Sources/Swift/Protocol/Codable/SentryRequestCodable.swift new file mode 100644 index 00000000000..edd358b7da1 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryRequestCodable.swift @@ -0,0 +1,29 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryRequest: Decodable { + + private enum CodingKeys: String, CodingKey { + case bodySize = "body_size" + case cookies + case headers + case fragment + case method + case queryString = "query_string" + case url + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.init() + + self.bodySize = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .bodySize)?.value + self.cookies = try container.decodeIfPresent(String.self, forKey: .cookies) + self.headers = try container.decodeIfPresent([String: String].self, forKey: .headers) + self.fragment = try container.decodeIfPresent(String.self, forKey: .fragment) + self.method = try container.decodeIfPresent(String.self, forKey: .method) + self.queryString = try container.decodeIfPresent(String.self, forKey: .queryString) + self.url = try container.decodeIfPresent(String.self, forKey: .url) + } +} 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/SentryThreadCodable.swift b/Sources/Swift/Protocol/Codable/SentryThreadCodable.swift new file mode 100644 index 00000000000..cfaad57b8f7 --- /dev/null +++ b/Sources/Swift/Protocol/Codable/SentryThreadCodable.swift @@ -0,0 +1,29 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +extension SentryThread: Decodable { + + private enum CodingKeys: String, CodingKey { + case threadId = "id" + case name + case stacktrace + case crashed + case current + case isMain = "main" + } + + required convenience public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard let threadId = try container.decode(NSNumberDecodableWrapper.self, forKey: .threadId).value else { + throw DecodingError.dataCorruptedError(forKey: .threadId, in: container, debugDescription: "Can't decode SentryThread because couldn't decode threadId.") + } + + self.init(threadId: threadId) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.stacktrace = try container.decodeIfPresent(SentryStacktrace.self, forKey: .stacktrace) + self.crashed = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .crashed)?.value + self.current = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .current)?.value + self.isMain = try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .isMain)?.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..cd683c47c1c --- /dev/null +++ b/Tests/SentryTests/Protocol/Codable/ArbitraryDataTests.swift @@ -0,0 +1,352 @@ +@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?) + } + + func testDecode_Null() throws { + // Arrange + let jsonData = #""" + { + "data": null + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertNil(actual.data) + } + + func testDecodeNestedData_Values() throws { + // Arrange + let jsonData = #""" + { + "nestedData": { + "value": { + "key1": "value1", + "key2": 2, + } + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let nested = try XCTUnwrap(actual.nestedData?["value"] as? [String: Any]) + XCTAssertEqual("value1", nested["key1"] as? String) + XCTAssertEqual(2, nested["key2"] as? Int) + } + + func testDecodeNestedData_Empty() throws { + // Arrange + let jsonData = #""" + { + "nestedData": { + "value": { + } + } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let nested = try XCTUnwrap(actual.nestedData?["value"] as? [String: Any]) + XCTAssertTrue(nested.isEmpty) + } + + func testDecodeNestedData_Null() throws { + // Arrange + let jsonData = #""" + { + "nestedData": { "value": {"nested": null} } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + let nested = try XCTUnwrap(actual.nestedData?["value"] as? [String: Any]) + XCTAssertEqual(NSNull(), nested["nested"] as? NSNull) + } + + func testDecodeNestedData_Garbage() throws { + // Arrange + let jsonData = #""" + { + "nestedData": { "value": "wrong" } + } + """#.data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: jsonData) as DataWrapper?) + + // Assert + XCTAssertNil(actual.nestedData) + } +} + +class DataWrapper: Decodable { + + var data: [String: Any]? + var nestedData: [String: [String: Any]]? + + enum CodingKeys: String, CodingKey { + case data + case nestedData + } + + 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]? + } + self.nestedData = decodeArbitraryData { + try container.decodeIfPresent([String: [String: ArbitraryData]].self, forKey: .nestedData) + } + + } +} 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/Codable/SentryDateCodableTests.swift b/Tests/SentryTests/Protocol/Codable/SentryDateCodableTests.swift new file mode 100644 index 00000000000..dd7b84ac061 --- /dev/null +++ b/Tests/SentryTests/Protocol/Codable/SentryDateCodableTests.swift @@ -0,0 +1,59 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +final class SentryDateCodableTests: XCTestCase { + + func testDecodeDate_WithTimeIntervalSince1970() throws { + //Arrange + let timestamp = 0.012345678 + let date = Date(timeIntervalSince1970: timestamp) + + let json = "{\"date\": \(timestamp)}".data(using: .utf8)! + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: json) as SentryDateTestDecodable?) + + // Assert + XCTAssertEqual(date.timeIntervalSince1970, actual.date.timeIntervalSince1970, accuracy: 0.000000001) + } + + func testDecodeDate_WithTimeISO8601Format() throws { + //Arrange + let timestamp = 0.012345678 + let date = Date(timeIntervalSince1970: timestamp) + let isoString = sentry_toIso8601String(date) + + // The ISO8601 date format only supports milliseconds precision. + // Therefore, we convert the ISO date string back to the date. + let expectedDate = sentry_fromIso8601String(isoString) + + let json = "{\"date\": \"\(isoString)\"}".data(using: .utf8)! + + // Act + let actual = try XCTUnwrap(decodeFromJSONData(jsonData: json) as SentryDateTestDecodable?) + + // Assert + XCTAssertEqual(expectedDate.timeIntervalSince1970, actual.date.timeIntervalSince1970, accuracy: 0.001) + } + + func testDecodeDate_WithWrongDateFormat() throws { + //Arrange + let json = "{\"date\": \"hello\"}".data(using: .utf8)! + + // Act & Assert + XCTAssertNil(decodeFromJSONData(jsonData: json) as SentryDateTestDecodable?) + } + + func testDecodeDate_WithBool() throws { + //Arrange + let json = "{\"date\": true}".data(using: .utf8)! + + // Act & Assert + XCTAssertNil(decodeFromJSONData(jsonData: json) as SentryDateTestDecodable?) + } + +} + +private struct SentryDateTestDecodable: Decodable { + let date: Date +} diff --git a/Tests/SentryTests/Protocol/SentryBreadcrumbTests.swift b/Tests/SentryTests/Protocol/SentryBreadcrumbTests.swift index 79f0cc2045d..c75c3d7d0c0 100644 --- a/Tests/SentryTests/Protocol/SentryBreadcrumbTests.swift +++ b/Tests/SentryTests/Protocol/SentryBreadcrumbTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryBreadcrumbTests: XCTestCase { @@ -131,4 +132,41 @@ class SentryBreadcrumbTests: XCTestCase { XCTAssertEqual(serialaziedString, actual as NSString) } + + func testDecode_WithAllProperties() throws { + // Arrange + let crumb = fixture.breadcrumb + let actual = crumb.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Breadcrumb?) + + // Assert + XCTAssertEqual(crumb.level, decoded.level) + XCTAssertEqual(crumb.category, decoded.category) + XCTAssertEqual(crumb.timestamp, decoded.timestamp) + XCTAssertEqual(crumb.type, decoded.type) + XCTAssertEqual(crumb.message, decoded.message) + + let crumbData = try XCTUnwrap(crumb.data as? NSDictionary) + let decodedData = try XCTUnwrap(decoded.data as? NSDictionary) + + XCTAssertEqual(crumbData, decodedData) + XCTAssertEqual(crumb.origin, decoded.origin) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let crumb = Breadcrumb() + crumb.timestamp = fixture.date + let actual = crumb.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Breadcrumb?) + + // Assert + XCTAssertEqual(crumb, decoded) + } } diff --git a/Tests/SentryTests/Protocol/SentryDebugMetaTests.swift b/Tests/SentryTests/Protocol/SentryDebugMetaTests.swift index 75114e071a7..068b7d7834e 100644 --- a/Tests/SentryTests/Protocol/SentryDebugMetaTests.swift +++ b/Tests/SentryTests/Protocol/SentryDebugMetaTests.swift @@ -1,12 +1,16 @@ +@testable import Sentry import XCTest class SentryDebugMetaTests: XCTestCase { func testSerialize() { + // Arrange let debugMeta = TestData.debugMeta + // Act let actual = debugMeta.serialize() + // Assert XCTAssertEqual(debugMeta.uuid, actual["uuid"] as? String) XCTAssertEqual(debugMeta.debugID, actual["debug_id"] as? String) XCTAssertEqual(debugMeta.type, actual["type"] as? String) @@ -16,4 +20,66 @@ class SentryDebugMetaTests: XCTestCase { XCTAssertEqual(debugMeta.codeFile, actual["code_file"] as? String) XCTAssertEqual(debugMeta.imageVmAddress, actual["image_vmaddr"] as? String) } + + func testDecode_WithAllProperties() throws { + // Arrange + let debugMeta = TestData.debugMeta + let actual = debugMeta.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as DebugMeta?) + + // Assert + XCTAssertEqual(debugMeta.uuid, decoded.uuid) + XCTAssertEqual(debugMeta.debugID, decoded.debugID) + XCTAssertEqual(debugMeta.type, decoded.type) + XCTAssertEqual(debugMeta.imageAddress, decoded.imageAddress) + XCTAssertEqual(debugMeta.imageSize, decoded.imageSize) + XCTAssertEqual((debugMeta.name! as NSString).lastPathComponent, decoded.name) + XCTAssertEqual(debugMeta.codeFile, decoded.codeFile) + XCTAssertEqual(debugMeta.imageVmAddress, decoded.imageVmAddress) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let debugMeta = DebugMeta() + let actual = debugMeta.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as DebugMeta?) + + // Assert + XCTAssertNil(decoded.uuid) + XCTAssertNil(decoded.debugID) + XCTAssertNil(decoded.type) + XCTAssertNil(decoded.imageAddress) + XCTAssertNil(decoded.imageSize) + XCTAssertNil(decoded.name) + XCTAssertNil(decoded.codeFile) + XCTAssertNil(decoded.imageVmAddress) + } + + func testDecode_WithOnlyUuid() throws { + // Arrange + let debugMeta = DebugMeta() + debugMeta.uuid = "123" + let actual = debugMeta.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as DebugMeta?) + + // Assert + XCTAssertEqual(debugMeta.uuid, decoded.uuid) + XCTAssertNil(decoded.debugID) + XCTAssertNil(decoded.type) + XCTAssertNil(decoded.imageAddress) + XCTAssertNil(decoded.imageSize) + XCTAssertNil(decoded.name) + XCTAssertNil(decoded.codeFile) + XCTAssertNil(decoded.imageVmAddress) + } + } diff --git a/Tests/SentryTests/Protocol/SentryEventTests.swift b/Tests/SentryTests/Protocol/SentryEventTests.swift index 1ab3d37fd72..4f1f948a7b6 100644 --- a/Tests/SentryTests/Protocol/SentryEventTests.swift +++ b/Tests/SentryTests/Protocol/SentryEventTests.swift @@ -1,4 +1,4 @@ -import Sentry +@testable import Sentry import SentryTestUtils import XCTest @@ -106,4 +106,174 @@ class SentryEventTests: XCTestCase { func testMessageIsNil() { XCTAssertNil(Event().message) } + + func testDecode_WithAllProperties() throws { + // Arrange + SentryDependencyContainer.sharedInstance().dateProvider = TestCurrentDateProvider() + let event = TestData.event + // Start timestamp is only serialized if event type is transaction + event.type = "transaction" + let actual = event.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryEventDecodable?) + + // Assert + // We don't assert all properties of all objects because we have other tests for that. + XCTAssertEqual(event.eventId, decoded.eventId) + + // Message + let eventMessage = try XCTUnwrap(event.message) + let decodedMessage = try XCTUnwrap(decoded.message) + XCTAssertEqual(eventMessage.formatted, decodedMessage.formatted) + XCTAssertEqual(eventMessage.message, decodedMessage.message) + XCTAssertEqual(eventMessage.params, decodedMessage.params) + + XCTAssertEqual(event.timestamp?.timeIntervalSince1970, decoded.timestamp?.timeIntervalSince1970) + XCTAssertEqual(event.startTimestamp?.timeIntervalSince1970, decoded.startTimestamp?.timeIntervalSince1970) + XCTAssertEqual(event.level, decoded.level) + + XCTAssertEqual(event.platform, decoded.platform) + XCTAssertEqual(event.logger, decoded.logger) + XCTAssertEqual(event.serverName, decoded.serverName) + XCTAssertEqual(event.releaseName, decoded.releaseName) + XCTAssertEqual(event.dist, decoded.dist) + XCTAssertEqual(event.environment, decoded.environment) + XCTAssertEqual(event.transaction, decoded.transaction) + XCTAssertEqual(event.type, decoded.type) + + XCTAssertEqual(event.tags, decoded.tags) + + let eventExtra = try XCTUnwrap(event.extra as? NSDictionary) + let decodedExtra = try XCTUnwrap(decoded.extra as? NSDictionary) + XCTAssertEqual(eventExtra, decodedExtra) + + let eventSdk = try XCTUnwrap(event.sdk as? NSDictionary) + let decodedSdk = try XCTUnwrap(decoded.sdk as? NSDictionary) + XCTAssertEqual(eventSdk, decodedSdk) + + let eventModules = try XCTUnwrap(event.modules as? NSDictionary) + let decodedModules = try XCTUnwrap(decoded.modules as? NSDictionary) + XCTAssertEqual(eventModules, decodedModules) + + XCTAssertEqual(event.fingerprint, decoded.fingerprint) + + XCTAssertEqual(event.user, decoded.user) + + let eventContext = try XCTUnwrap(event.context as? NSDictionary) + let decodedContext = try XCTUnwrap(decoded.context as? NSDictionary) + XCTAssertEqual(eventContext, decodedContext) + + // Threads + let eventThreads = try XCTUnwrap(event.threads) + let decodedThreads = try XCTUnwrap(decoded.threads) + XCTAssertEqual(eventThreads.count, decodedThreads.count) + let firstEventThread = try XCTUnwrap(eventThreads.first) + let decodedFirstThread = try XCTUnwrap(decodedThreads.first) + XCTAssertEqual(firstEventThread.name, decodedFirstThread.name) + XCTAssertEqual(firstEventThread.crashed, decodedFirstThread.crashed) + XCTAssertEqual(firstEventThread.current, decodedFirstThread.current) + + // Exceptions + let eventExceptions = try XCTUnwrap(event.exceptions) + let decodedExceptions = try XCTUnwrap(decoded.exceptions) + XCTAssertEqual(eventExceptions.count, decodedExceptions.count) + let firstEventException = try XCTUnwrap(eventExceptions.first) + let decodedFirstException = try XCTUnwrap(decodedExceptions.first) + XCTAssertEqual(firstEventException.type, decodedFirstException.type) + XCTAssertEqual(firstEventException.value, decodedFirstException.value) + + // Exception Mechanism + let firstEventExceptionMechanism = try XCTUnwrap(firstEventException.mechanism) + let decodedFirstExceptionMechanism = try XCTUnwrap(decodedFirstException.mechanism) + XCTAssertEqual(firstEventExceptionMechanism.type, decodedFirstExceptionMechanism.type) + XCTAssertEqual(firstEventExceptionMechanism.desc, decodedFirstExceptionMechanism.desc) + + // Exception Mechanism Meta + let firstEventExceptionMechanismMeta = try XCTUnwrap(firstEventExceptionMechanism.meta) + let decodedFirstExceptionMechanismMeta = try XCTUnwrap(decodedFirstExceptionMechanism.meta) + XCTAssertEqual(firstEventExceptionMechanismMeta.error?.code, decodedFirstExceptionMechanismMeta.error?.code) + + // Stacktrace + let eventStacktrace = try XCTUnwrap(event.stacktrace) + let decodedStacktrace = try XCTUnwrap(decoded.stacktrace) + XCTAssertEqual(eventStacktrace.frames.count, decodedStacktrace.frames.count) + + // Stacktrace Frames + let firstEventStacktraceFrame = try XCTUnwrap(eventStacktrace.frames.first) + let decodedFirstStacktraceFrame = try XCTUnwrap(decodedStacktrace.frames.first) + XCTAssertEqual(firstEventStacktraceFrame.fileName, decodedFirstStacktraceFrame.fileName) + XCTAssertEqual(firstEventStacktraceFrame.symbolAddress, decodedFirstStacktraceFrame.symbolAddress) + + // Debug Meta + let eventDebugMeta = try XCTUnwrap(event.debugMeta) + let decodedDebugMeta = try XCTUnwrap(decoded.debugMeta) + XCTAssertEqual(eventDebugMeta.count, decodedDebugMeta.count) + + // Debug Meta Frames + let firstEventDebugMeta = try XCTUnwrap(eventDebugMeta.first) + let decodedFirstDebugMeta = try XCTUnwrap(decodedDebugMeta.first) + XCTAssertEqual(firstEventDebugMeta.type, decodedFirstDebugMeta.type) + XCTAssertEqual(firstEventDebugMeta.imageAddress, decodedFirstDebugMeta.imageAddress) + XCTAssertEqual(firstEventDebugMeta.imageSize, decodedFirstDebugMeta.imageSize) + XCTAssertEqual(firstEventDebugMeta.type, decodedFirstDebugMeta.type) + + // Breadcrumbs + let eventBreadcrumbs = try XCTUnwrap(event.breadcrumbs) + let decodedBreadcrumbs = try XCTUnwrap(decoded.breadcrumbs) + XCTAssertEqual(eventBreadcrumbs.count, decodedBreadcrumbs.count) + let firstEventBreadcrumb = try XCTUnwrap(eventBreadcrumbs.first) + let decodedFirstBreadcrumb = try XCTUnwrap(decodedBreadcrumbs.first) + XCTAssertEqual(firstEventBreadcrumb.message, decodedFirstBreadcrumb.message) + XCTAssertEqual(firstEventBreadcrumb.level, decodedFirstBreadcrumb.level) + XCTAssertEqual(firstEventBreadcrumb.timestamp, decodedFirstBreadcrumb.timestamp) + + // Request + let eventRequest = try XCTUnwrap(event.request) + let decodedRequest = try XCTUnwrap(decoded.request) + XCTAssertEqual(eventRequest.url, decodedRequest.url) + XCTAssertEqual(eventRequest.method, decodedRequest.method) + XCTAssertEqual(eventRequest.headers, decodedRequest.headers) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + SentryDependencyContainer.sharedInstance().dateProvider = TestCurrentDateProvider() + let event = Event() + let actual = event.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryEventDecodable?) + + // Assert + XCTAssertEqual(event.eventId, decoded.eventId) + XCTAssertNil(decoded.message) + XCTAssertNil(decoded.error) + XCTAssertEqual(event.timestamp?.timeIntervalSince1970, decoded.timestamp?.timeIntervalSince1970) + XCTAssertNil(decoded.startTimestamp) + XCTAssertEqual(.none, decoded.level) + XCTAssertEqual(event.platform, decoded.platform) + XCTAssertNil(decoded.logger) + XCTAssertNil(decoded.serverName) + XCTAssertNil(decoded.releaseName) + XCTAssertNil(decoded.dist) + XCTAssertNil(decoded.environment) + XCTAssertNil(decoded.transaction) + XCTAssertNil(decoded.type) + XCTAssertNil(decoded.tags) + XCTAssertNil(decoded.extra) + XCTAssertNil(decoded.sdk) + XCTAssertNil(decoded.modules) + XCTAssertNil(decoded.fingerprint) + XCTAssertNil(decoded.user) + XCTAssertNil(decoded.context) + XCTAssertNil(decoded.threads) + XCTAssertNil(decoded.exceptions) + XCTAssertNil(decoded.stacktrace) + XCTAssertNil(decoded.debugMeta) + XCTAssertNil(decoded.breadcrumbs) + XCTAssertNil(decoded.request) + } } diff --git a/Tests/SentryTests/Protocol/SentryExceptionTests.swift b/Tests/SentryTests/Protocol/SentryExceptionTests.swift index 9862e67a3af..0193dfd3d01 100644 --- a/Tests/SentryTests/Protocol/SentryExceptionTests.swift +++ b/Tests/SentryTests/Protocol/SentryExceptionTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryExceptionTests: XCTestCase { @@ -24,4 +25,49 @@ class SentryExceptionTests: XCTestCase { let stacktrace = try XCTUnwrap(actual["stacktrace"] as? [String: Any]) XCTAssertEqual(TestData.stacktrace.registers, stacktrace["registers"] as? [String: String]) } + + func testDecode_WithAllProperties() throws { + // Arrange + let exception = TestData.exception + let actual = exception.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Exception?) + + // Assert + XCTAssertEqual(exception.type, decoded.type) + XCTAssertEqual(exception.value, decoded.value) + XCTAssertEqual(exception.module, decoded.module) + XCTAssertEqual(exception.threadId, decoded.threadId) + + let decodedMechanism = try XCTUnwrap(decoded.mechanism) + let expectedMechanism = try XCTUnwrap(exception.mechanism) + XCTAssertEqual(expectedMechanism.desc, decodedMechanism.desc) + XCTAssertEqual(expectedMechanism.handled, decodedMechanism.handled) + XCTAssertEqual(expectedMechanism.type, decodedMechanism.type) + + let decodedStacktrace = try XCTUnwrap(decoded.stacktrace) + let expectedStacktrace = try XCTUnwrap(exception.stacktrace) + XCTAssertEqual(expectedStacktrace.frames.count, decodedStacktrace.frames.count) + XCTAssertEqual(expectedStacktrace.registers, decodedStacktrace.registers) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let exception = Exception(value: "value", type: "type") + let actual = exception.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Exception?) + + // Assert + XCTAssertEqual(exception.type, decoded.type) + XCTAssertEqual(exception.value, decoded.value) + XCTAssertNil(decoded.mechanism) + XCTAssertNil(decoded.module) + XCTAssertNil(decoded.threadId) + XCTAssertNil(decoded.stacktrace) + } } 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 ec12f8f042e..de8f8cbaa36 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/SentryMessageTests.swift b/Tests/SentryTests/Protocol/SentryMessageTests.swift index 6dfc93e70dc..d08cf90e798 100644 --- a/Tests/SentryTests/Protocol/SentryMessageTests.swift +++ b/Tests/SentryTests/Protocol/SentryMessageTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryMessageTests: XCTestCase { @@ -74,4 +75,36 @@ class SentryMessageTests: XCTestCase { let expected = "\(beginning){\n formatted = \"\(message.formatted)\";\n}>" XCTAssertEqual(expected, actual) } + + func testDecode_WithAllProperties() throws { + // Arrange + let message = fixture.message + let actual = message.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryMessage?) + + // Assert + XCTAssertEqual(message.formatted, decoded.formatted) + XCTAssertEqual(message.message, decoded.message) + XCTAssertEqual(message.params, decoded.params) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let message = fixture.message + message.message = nil + message.params = nil + let actual = message.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryMessage?) + + // Assert + XCTAssertEqual(message.formatted, decoded.formatted) + XCTAssertNil(decoded.message) + XCTAssertNil(decoded.params) + } } 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/SentryRequestTests.swift b/Tests/SentryTests/Protocol/SentryRequestTests.swift index afc7b0f38fd..8b7b93d6883 100644 --- a/Tests/SentryTests/Protocol/SentryRequestTests.swift +++ b/Tests/SentryTests/Protocol/SentryRequestTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryRequestTests: XCTestCase { @@ -33,4 +34,62 @@ class SentryRequestTests: XCTestCase { XCTAssertNil(actual["body_size"]) } + + func testDecode_WithAllProperties() throws { + // Arrange + let request = TestData.request + let actual = request.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryRequest?) + + // Assert + XCTAssertEqual(request.bodySize, decoded.bodySize) + XCTAssertEqual(request.cookies, decoded.cookies) + XCTAssertEqual(request.headers, decoded.headers) + XCTAssertEqual(request.fragment, decoded.fragment) + XCTAssertEqual(request.method, decoded.method) + XCTAssertEqual(request.queryString, decoded.queryString) + XCTAssertEqual(request.url, decoded.url) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let request = SentryRequest() + let actual = request.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryRequest?) + + // Assert + XCTAssertNil(decoded.bodySize) + XCTAssertNil(decoded.cookies) + XCTAssertNil(decoded.headers) + XCTAssertNil(decoded.fragment) + XCTAssertNil(decoded.method) + XCTAssertNil(decoded.queryString) + XCTAssertNil(decoded.url) + } + + func testDecode_OnlyWithBodySize() throws { + // Arrange + let request = SentryRequest() + request.bodySize = 100 + let actual = request.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryRequest?) + + // Assert + XCTAssertEqual(request.bodySize, decoded.bodySize) + XCTAssertNil(decoded.cookies) + XCTAssertNil(decoded.headers) + XCTAssertNil(decoded.fragment) + XCTAssertNil(decoded.method) + XCTAssertNil(decoded.queryString) + XCTAssertNil(decoded.url) + } } 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/SentryThreadTests.swift b/Tests/SentryTests/Protocol/SentryThreadTests.swift index bf38903c350..db7f6bddf85 100644 --- a/Tests/SentryTests/Protocol/SentryThreadTests.swift +++ b/Tests/SentryTests/Protocol/SentryThreadTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import XCTest class SentryThreadTests: XCTestCase { @@ -34,4 +35,57 @@ class SentryThreadTests: XCTestCase { SentryBooleanSerialization.test(thread, property: "current") SentryBooleanSerialization.test(thread, property: "isMain", serializedProperty: "main") } + + func testDecode_WithAllProperties() throws { + // Arrange + let thread = TestData.thread + let actual = thread.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryThread?) + + // Assert + XCTAssertEqual(thread.threadId, decoded.threadId) + XCTAssertEqual(thread.name, decoded.name) + XCTAssertEqual(thread.crashed, decoded.crashed) + XCTAssertEqual(thread.current, decoded.current) + XCTAssertEqual(thread.isMain, decoded.isMain) + + let decodedStacktrace = try XCTUnwrap(decoded.stacktrace) + let threadStacktrace = try XCTUnwrap(thread.stacktrace) + XCTAssertEqual(threadStacktrace.frames.count, decodedStacktrace.frames.count) + XCTAssertEqual(threadStacktrace.registers, decodedStacktrace.registers) + XCTAssertEqual(threadStacktrace.snapshot, decodedStacktrace.snapshot) + } + + func testDecode_WithAllPropertiesNil() throws { + // Arrange + let thread = SentryThread(threadId: 0) + let actual = thread.serialize() + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act + let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as SentryThread?) + + // Assert + XCTAssertEqual(thread.threadId, decoded.threadId) + XCTAssertNil(decoded.name) + XCTAssertNil(decoded.stacktrace) + XCTAssertNil(decoded.crashed) + XCTAssertNil(decoded.current) + XCTAssertNil(decoded.isMain) + } + + func testDecode_WithWrongThreadId_ReturnsNil () throws { + // Arrange + let thread = SentryThread(threadId: 10) + var actual = thread.serialize() + actual["id"] = "nil" + let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: actual)) + + // Act & Assert + XCTAssertNil(decodeFromJSONData(jsonData: data) as SentryThread?) + } + } diff --git a/Tests/SentryTests/Protocol/SentryUserTests.swift b/Tests/SentryTests/Protocol/SentryUserTests.swift index 3d5f6051257..604e9bf2581 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/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index f1d5e6c27b0..1e5f1c5740d 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -42,7 +42,7 @@ class TestData { event.logger = "logger" event.message = SentryMessage(formatted: "message") event.modules = ["module": "1"] - event.platform = "Apple" + event.platform = SentryPlatformName event.releaseName = SentryMeta.versionString event.sdk = sdk event.serverName = "serverName" 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.