From eae7f93ad603399067ffb6306a1fb6048899539d Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 14 Feb 2024 13:28:05 +0100 Subject: [PATCH 01/88] feat(Session Replay): ReplayEvent, ReplayRecording and Envelope handling --- Sentry.xcodeproj/project.pbxproj | 44 +++++++++++++ Sources/Sentry/SentryClient.m | 50 ++++++++++++++- Sources/Sentry/SentryDateUtil.m | 5 ++ Sources/Sentry/SentryEnvelope.m | 5 ++ .../Sentry/SentryReplayEnvelopeItemHeader.m | 35 ++++++++++ Sources/Sentry/SentryReplayEvent.m | 28 ++++++++ Sources/Sentry/SentryReplayRecording.m | 64 +++++++++++++++++++ Sources/Sentry/SentrySerialization.m | 6 +- .../include/HybridPublic/SentryEnvelope.h | 2 + .../HybridPublic/SentryEnvelopeItemType.h | 2 + Sources/Sentry/include/SentryClient+Private.h | 6 +- Sources/Sentry/include/SentryDateUtil.h | 2 + .../include/SentryReplayEnvelopeItemHeader.h | 20 ++++++ Sources/Sentry/include/SentryReplayEvent.h | 39 +++++++++++ .../Sentry/include/SentryReplayRecording.h | 42 ++++++++++++ Sources/Sentry/include/SentrySerialization.h | 2 +- .../Helper/SentryDateUtilTests.swift | 8 +++ .../SentryReplayEnvelopeItemHeaderTests.swift | 44 +++++++++++++ .../SentryReplayEventTests.swift | 30 +++++++++ .../SentryReplayRecordingTests.swift | 41 ++++++++++++ .../Protocol/SentryEnvelopeTests.swift | 7 ++ Tests/SentryTests/SentryClientTests.swift | 26 ++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 3 + 23 files changed, 503 insertions(+), 8 deletions(-) create mode 100644 Sources/Sentry/SentryReplayEnvelopeItemHeader.m create mode 100644 Sources/Sentry/SentryReplayEvent.m create mode 100644 Sources/Sentry/SentryReplayRecording.m create mode 100644 Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h create mode 100644 Sources/Sentry/include/SentryReplayEvent.h create mode 100644 Sources/Sentry/include/SentryReplayRecording.h create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c6fd97719aa..e972a73648d 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -743,6 +743,9 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; + D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */; }; + D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */; }; + D80694CA2B7CD65800B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */; }; D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */; }; D808FB8B281BCE96009A2A33 /* TestSentrySwizzleWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */; }; D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */; }; @@ -791,6 +794,8 @@ D867063E27C3BC2400048851 /* SentryCoreDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063B27C3BC2400048851 /* SentryCoreDataSwizzling.h */; }; D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; }; D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; }; + D86B7B5C2B7A529C0017E8D9 /* SentryReplayEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */; }; + D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */; }; D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; @@ -801,6 +806,10 @@ D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Private, ); }; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */ = {isa = PBXBuildFile; fileRef = D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */; }; + D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */ = {isa = PBXBuildFile; fileRef = D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */; }; + D88D6C212B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */; }; + D88D6C222B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ABB0BC29264275005D1E24 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A349B291D0C0B005A27A9 /* Sentry.swift */; }; @@ -1724,6 +1733,9 @@ A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; + D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEventTests.swift; sourceTree = ""; }; + D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayRecordingTests.swift; sourceTree = ""; }; + D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEnvelopeItemHeaderTests.swift; sourceTree = ""; }; D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackerTests.swift; sourceTree = ""; }; D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentrySwizzleWrapper.swift; sourceTree = ""; }; D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackingIntegrationTests.swift; sourceTree = ""; }; @@ -1779,6 +1791,8 @@ D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTracker.h; path = include/SentryCoreDataTracker.h; sourceTree = ""; }; D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = ""; }; D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = ""; }; + D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayEvent.h; path = include/SentryReplayEvent.h; sourceTree = ""; }; + D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayEvent.m; sourceTree = ""; }; D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; @@ -1790,6 +1804,10 @@ D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = include/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayRecording.h; path = include/SentryReplayRecording.h; sourceTree = ""; }; + D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayRecording.m; sourceTree = ""; }; + D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayEnvelopeItemHeader.h; path = include/SentryReplayEnvelopeItemHeader.h; sourceTree = ""; }; + D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayEnvelopeItemHeader.m; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; @@ -2758,6 +2776,7 @@ 7BE0DC40272AEA0A004FA8B7 /* Performance */, 7BE0DC3F272AE9F0004FA8B7 /* Session */, 7BE0DC3E272AE9DC004FA8B7 /* SentryCrash */, + D80694C12B7CC85800B820E6 /* SessionReplay */, 7B59398324AB481B0003AAD2 /* NotificationCenterTestCase.swift */, 0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */, ); @@ -3358,6 +3377,16 @@ path = Swift; sourceTree = ""; }; + D80694C12B7CC85800B820E6 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, + D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, + D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; D808FB85281AB2EF009A2A33 /* UIEvents */ = { isa = PBXGroup; children = ( @@ -3370,6 +3399,12 @@ D80CD8D52B752FD9002F710B /* SessionReplay */ = { isa = PBXGroup; children = ( + D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */, + D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */, + D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, + D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, + D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, + D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, ); name = SessionReplay; sourceTree = ""; @@ -3658,6 +3693,7 @@ 7B7D873224864BB900D2ECFF /* SentryCrashMachineContextWrapper.h in Headers */, 861265F92404EC1500C4AFDE /* NSArray+SentrySanitize.h in Headers */, 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, + D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, @@ -3721,6 +3757,7 @@ 63FE70F920DA4C1000CDBAE8 /* SentryCrashMonitor.h in Headers */, D8BD2E6829361A0F00D96C6A /* PrivatesHeader.h in Headers */, 7B98D7CB25FB64EC00C5A389 /* SentryWatchdogTerminationTrackingIntegration.h in Headers */, + D88D6C212B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h in Headers */, 63FE710920DA4C1000CDBAE8 /* SentryCrashFileUtils.h in Headers */, 03F84D1F27DD414C008FE43F /* SentryAsyncSafeLogging.h in Headers */, 7BE3C76B2445C27A00A38442 /* SentryCurrentDateProvider.h in Headers */, @@ -3769,6 +3806,7 @@ D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */, 7B3B83722833832B0001FDEB /* SentrySpanOperations.h in Headers */, 7BF9EF722722A84800B5BBEF /* SentryClassRegistrator.h in Headers */, + D86B7B5C2B7A529C0017E8D9 /* SentryReplayEvent.h in Headers */, 63FE715520DA4C1100CDBAE8 /* SentryCrashStackCursor_MachineContext.h in Headers */, 62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */, 15360CF02433A16D00112302 /* SentryInstallation.h in Headers */, @@ -4184,6 +4222,7 @@ 639FCFA91EBC80CC00778193 /* SentryFrame.m in Sources */, D858FA672A29EAB3002A3503 /* SentryBinaryImageCache.m in Sources */, 8E564AEA267AF22600FE117D /* SentryNetworkTracker.m in Sources */, + D88D6C222B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m in Sources */, 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, @@ -4227,6 +4266,7 @@ 7B56D73324616D9500B842DA /* SentryConcurrentRateLimitsDictionary.m in Sources */, 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, 0A80E433291017C300095219 /* SentryWatchdogTerminationScopeObserver.m in Sources */, + D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */, 7BECF42826145CD900D9826E /* SentryMechanismMeta.m in Sources */, 8E7C982F2693D56000E6336C /* SentryTraceHeader.m in Sources */, 63FE715F20DA4C1100CDBAE8 /* SentryCrashID.c in Sources */, @@ -4330,6 +4370,7 @@ 861265FA2404EC1500C4AFDE /* NSArray+SentrySanitize.m in Sources */, D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */, 63FE711520DA4C1000CDBAE8 /* SentryCrashJSONCodec.c in Sources */, + D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */, 03F84D3327DD4191008FE43F /* SentryMachLogging.cpp in Sources */, 84F993C42A62A74000EC0190 /* SentryCurrentDateProvider.m in Sources */, D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */, @@ -4425,6 +4466,7 @@ D8137D54272B53070082656C /* TestSentrySpan.m in Sources */, 7BECF432261463E600D9826E /* SentryMechanismMetaTests.swift in Sources */, 7BE8E8462593313500C4DA1F /* SentryAttachment+Equality.m in Sources */, + D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */, 63FE721F20DA66EC00CDBAE8 /* SentryCrashSignalInfo_Tests.m in Sources */, 0ADC33F128D9BE940078D980 /* TestSentryUIDeviceWrapper.swift in Sources */, 63FE721420DA66EC00CDBAE8 /* SentryCrashMemory_Tests.m in Sources */, @@ -4458,6 +4500,7 @@ 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */, 7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */, 7BBD18BB24530D2600427C76 /* SentryFileManagerTests.swift in Sources */, + D80694CA2B7CD65800B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift in Sources */, 63FE722020DA66EC00CDBAE8 /* SentryCrashObjC_Tests.m in Sources */, 7B58816727FC5D790098B121 /* SentryDiscardReasonMapperTests.swift in Sources */, 63FE720320DA66EC00CDBAE8 /* SentryCrashCPU_Tests.m in Sources */, @@ -4516,6 +4559,7 @@ 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */, 8431F00529B2849A00D8DC56 /* (null) in Sources */, 7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */, + D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */, 7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */, 8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */, 7BE0DC2F272ABAF6004FA8B7 /* SentryAutoBreadcrumbTrackingIntegrationTests.swift in Sources */, diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 82e79df0475..03cfa1d1a70 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -34,8 +34,12 @@ #import "SentryOptions+Private.h" #import "SentryPropagationContext.h" #import "SentryRandom.h" +#import "SentryReplayEnvelopeItemHeader.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" +#import "SentrySerialization.h" #import "SentrySession.h" #import "SentryStacktraceBuilder.h" #import "SentrySwift.h" @@ -472,13 +476,53 @@ - (void)captureSession:(SentrySession *)session } SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithSession:session]; - SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:nil - traceContext:nil]; - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] singleItem:item]; [self captureEnvelope:envelope]; } +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL +{ + replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent + withScope:[[SentryScope alloc] init] + alwaysAttachStacktrace:NO]; + + if (replayEvent == nil) { + return; + } else if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { + SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The " + @"replay was discarded."); + return; + } + + // breadcrumbs for replay will be send with ReplayRecording + replayEvent.breadcrumbs = nil; + + SentryEnvelopeItem *eventEnvelopeItem = [[SentryEnvelopeItem alloc] initWithEvent:replayEvent]; + + NSData *recording = [SentrySerialization dataWithJSONObject:[replayRecording serialize]]; + SentryEnvelopeItem *recordingEnvelopeItem = [[SentryEnvelopeItem alloc] + initWithHeader:[SentryReplayEnvelopeItemHeader + replayRecordingHeaderWithSegmentId:replayRecording.segmentId + length:recording.length] + data:recording]; + + NSData *video = [NSData dataWithContentsOfURL:videoURL]; + SentryEnvelopeItem *videoEnvelopeItem = [[SentryEnvelopeItem alloc] + initWithHeader:[SentryReplayEnvelopeItemHeader + replayVideoHeaderWithSegmentId:replayRecording.segmentId + length:video.length] + data:video]; + + SentryEnvelope *envelope = [[SentryEnvelope alloc] + initWithHeader:[SentryEnvelopeHeader empty] + items:@[ eventEnvelopeItem, recordingEnvelopeItem, videoEnvelopeItem ]]; + + [self captureEnvelope:envelope]; +} + - (void)captureEnvelope:(SentryEnvelope *)envelope { if ([self isDisabled]) { diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index f362b345fc1..30e19e10d10 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -38,6 +38,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } ++ (long)javascriptDate:(NSDate *)date +{ + return (NSInteger)([date timeIntervalSince1970] * 1000); +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index fbe35f5c2d7..068d1c8b94a 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -48,6 +48,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId return self; } ++ (instancetype)empty +{ + return [[SentryEnvelopeHeader alloc] initWithId:nil traceContext:nil]; +} + @end @implementation SentryEnvelopeItem diff --git a/Sources/Sentry/SentryReplayEnvelopeItemHeader.m b/Sources/Sentry/SentryReplayEnvelopeItemHeader.m new file mode 100644 index 00000000000..59ae939c3e5 --- /dev/null +++ b/Sources/Sentry/SentryReplayEnvelopeItemHeader.m @@ -0,0 +1,35 @@ +#import "SentryReplayEnvelopeItemHeader.h" +#import "SentryEnvelopeItemType.h" + +@implementation SentryReplayEnvelopeItemHeader + +- (instancetype)initWithType:(NSString *)type + segmentId:(NSInteger)segmentId + length:(NSUInteger)length +{ + if (self = [super initWithType:type length:length]) { + self.segmentId = segmentId; + } + return self; +} + ++ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length +{ + return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayRecording + segmentId:segmentId + length:length]; +} + ++ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length +{ + return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayVideo + segmentId:segmentId + length:length]; +} + +- (NSDictionary *)serialize +{ + return @{ @"type" : self.type, @"length" : @(self.length), @"segment_id" : @(self.segmentId) }; +} + +@end diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m new file mode 100644 index 00000000000..a28de9a8cff --- /dev/null +++ b/Sources/Sentry/SentryReplayEvent.m @@ -0,0 +1,28 @@ +#import "SentryReplayEvent.h" +#import "SentryDateUtil.h" +#import "SentryId.h" + +@implementation SentryReplayEvent + +- (NSDictionary *)serialize +{ + NSMutableDictionary *result = [[super serialize] mutableCopy]; + + NSMutableArray *trace_ids = [NSMutableArray array]; + + for (SentryId *traceId in self.traceIds) { + [trace_ids addObject:traceId.sentryIdString]; + } + + result[@"urls"] = self.urls; + result[@"replay_start_timestamp"] = + @([SentryDateUtil javascriptDate:self.replayStartTimestamp]); + result[@"trace_ids"] = trace_ids; + result[@"replay_id"] = self.replayId.sentryIdString; + result[@"segment_id"] = @(self.segmentId); + result[@"replay_type"] = @"buffer"; + + return result; +} + +@end diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m new file mode 100644 index 00000000000..55a639967bd --- /dev/null +++ b/Sources/Sentry/SentryReplayRecording.m @@ -0,0 +1,64 @@ +#import "SentryReplayRecording.h" +#import "SentryDateUtil.h" + +@implementation SentryReplayRecording + +- (instancetype)initWithSegmentId:(NSInteger)segmentId + size:(NSInteger)size + start:(NSDate *)start + duration:(NSTimeInterval)duration + frameCount:(NSInteger)frameCount + frameRate:(NSInteger)frameRate + height:(NSInteger)height + width:(NSInteger)width +{ + if (self = [super init]) { + self.segmentId = segmentId; + self.size = size; + self.start = start; + self.duration = duration; + self.frameCount = frameCount; + self.frameRate = frameRate; + self.height = height; + self.width = width; + } + return self; +} + +- (nonnull NSArray *> *)serialize +{ + + long timestamp = [SentryDateUtil javascriptDate:self.start]; + + NSDictionary *metaInfo = @{ + @"type" : @4, + @"timestamp" : @(timestamp), + @"data" : @ { @"href" : @"", @"height" : @(self.height), @"width" : @(self.width) } + }; + + NSDictionary *recordingInfo = @{ + @"type" : @5, + @"timestamp" : @(timestamp), + @"data" : @ { + @"tag" : @"video", + @"payload" : @ { + @"segmentId" : @(self.segmentId), + @"size" : @(self.size), + @"duration" : @(self.duration), + @"encoding" : @"h264", + @"container" : @"mp4", + @"height" : @(self.height), + @"width" : @(self.width), + @"frameCount" : @(self.frameCount), + @"frameRateType" : @"constant", + @"frameRate" : @(self.frameRate), + @"left" : @0, + @"top" : @0, + } + } + }; + + return @[ metaInfo, recordingInfo ]; +} + +@end diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m index 27fe09500f0..6e16cb05b97 100644 --- a/Sources/Sentry/SentrySerialization.m +++ b/Sources/Sentry/SentrySerialization.m @@ -16,15 +16,15 @@ @implementation SentrySerialization -+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary ++ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject { - if (![NSJSONSerialization isValidJSONObject:dictionary]) { + if (![NSJSONSerialization isValidJSONObject:jsonObject]) { SENTRY_LOG_ERROR(@"Dictionary is not a valid JSON object."); return nil; } NSError *error = nil; - NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error]; + NSData *data = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:&error]; if (error) { SENTRY_LOG_ERROR(@"Internal error while serializing JSON: %@", error); } diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h index 8006c6d07ac..4d7efdafe85 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h @@ -68,6 +68,8 @@ SENTRY_NO_INIT */ @property (nullable, nonatomic, copy) NSDate *sentAt; ++ (instancetype)empty; + @end @interface SentryEnvelopeItem : NSObject diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h index d999cfdd47e..8619dca5ab5 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h @@ -5,3 +5,5 @@ static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction"; static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment"; static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report"; static NSString *const SentryEnvelopeItemTypeProfile = @"profile"; +static NSString *const SentryEnvelopeItemTypeReplayVideo = @"replay_video"; +static NSString *const SentryEnvelopeItemTypeReplayRecording = @"replay_recording"; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index a9bcd469818..bd1fbd85949 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -3,7 +3,7 @@ #import "SentryDiscardReason.h" @class SentrySession, SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector, - SentryEnvelope; + SentryReplayEvent, SentryReplayRecording, SentryEnvelope; NS_ASSUME_NONNULL_BEGIN @@ -42,6 +42,10 @@ SentryClient () additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:)); +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + - (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:)); /** diff --git a/Sources/Sentry/include/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index 60c8fdb6562..98fa11de6bb 100644 --- a/Sources/Sentry/include/SentryDateUtil.h +++ b/Sources/Sentry/include/SentryDateUtil.h @@ -9,6 +9,8 @@ NS_SWIFT_NAME(DateUtil) + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second; ++ (long)javascriptDate:(NSDate *)date; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h b/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h new file mode 100644 index 00000000000..77d4d782bc1 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h @@ -0,0 +1,20 @@ +#import "SentryEnvelopeItemHeader.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryReplayEnvelopeItemHeader : SentryEnvelopeItemHeader + +@property (nonatomic) NSInteger segmentId; + +- (instancetype)initWithType:(NSString *)type + segmentId:(NSInteger)segmentId + length:(NSUInteger)length; + ++ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length; + ++ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayEvent.h b/Sources/Sentry/include/SentryReplayEvent.h new file mode 100644 index 00000000000..74f2beb22ff --- /dev/null +++ b/Sources/Sentry/include/SentryReplayEvent.h @@ -0,0 +1,39 @@ +#import "SentryEvent.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SentryId; + +@interface SentryReplayEvent : SentryEvent + +/** + * Start time of the replay segment + */ +@property (nonatomic, strong) NSDate *replayStartTimestamp; + +/** + * Number of the segment in the replay. + * This is an incremental number + */ +@property (nonatomic) NSInteger segmentId; + +/** + * This will be used to store the name of the screens + * that appear during the duration of the replay segment. + */ +@property (nonatomic, strong) NSArray *urls; + +/** + * Trace ids happening during the duration of the replay segment. + */ +@property (nonatomic, strong) NSArray *traceIds; + +/** + * The replay id to which this segment belongs to. + */ +@property (nonatomic, strong) SentryId *replayId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h new file mode 100644 index 00000000000..66dfe526034 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -0,0 +1,42 @@ +#import "SentrySerializable.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SentryId; + +@interface SentryReplayRecording : NSObject + +@property (nonatomic) NSInteger segmentId; + +/** + * Video file size + */ +@property (nonatomic) NSInteger size; + +@property (nonatomic, strong) NSDate *start; + +@property (nonatomic) NSTimeInterval duration; + +@property (nonatomic) NSInteger frameCount; + +@property (nonatomic) NSInteger frameRate; + +@property (nonatomic) NSInteger height; + +@property (nonatomic) NSInteger width; + +- (instancetype)initWithSegmentId:(NSInteger)segmentId + size:(NSInteger)size + start:(NSDate *)start + duration:(NSTimeInterval)duration + frameCount:(NSInteger)frameCount + frameRate:(NSInteger)frameRate + height:(NSInteger)height + width:(NSInteger)width; + +- (nonnull NSArray *> *)serialize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySerialization.h b/Sources/Sentry/include/SentrySerialization.h index fbfcec32e4d..704e9b5cfd7 100644 --- a/Sources/Sentry/include/SentrySerialization.h +++ b/Sources/Sentry/include/SentrySerialization.h @@ -8,7 +8,7 @@ static int const SENTRY_BAGGAGE_MAX_SIZE = 8192; @interface SentrySerialization : NSObject -+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary; ++ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject; + (NSData *_Nullable)dataWithSession:(SentrySession *)session; diff --git a/Tests/SentryTests/Helper/SentryDateUtilTests.swift b/Tests/SentryTests/Helper/SentryDateUtilTests.swift index 507b1a3b3ad..bb3e5e30b44 100644 --- a/Tests/SentryTests/Helper/SentryDateUtilTests.swift +++ b/Tests/SentryTests/Helper/SentryDateUtilTests.swift @@ -1,3 +1,4 @@ +import Nimble import SentryTestUtils import XCTest @@ -54,4 +55,11 @@ class SentryDateUtilTests: XCTestCase { XCTAssertNil(DateUtil.getMaximumDate(nil, andOther: nil)) } + func testJavascriptDate() { + let testDate = Date(timeIntervalSince1970: 60) + let timestamp = DateUtil.javascriptDate(testDate) + + expect(timestamp) == 60_000 + } + } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift new file mode 100644 index 00000000000..1d809b72243 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift @@ -0,0 +1,44 @@ +import Foundation +import Nimble +import XCTest + +class SentryReplayEnvelopeItemHeaderTests: XCTestCase { + + func testInitWithTypeSegmentIdLength() { + let header = SentryReplayEnvelopeItemHeader(type: "testType", segmentId: 1, length: 100) + + expect(header.type) == "testType" + expect(header.segmentId) == 1 + expect(header.length) == 100 + } + + func testReplayRecordingHeader() { + let header = SentryReplayEnvelopeItemHeader.replayRecordingHeader(withSegmentId: 2, length: 200) + + expect(header.type) == SentryEnvelopeItemTypeReplayRecording + expect(header.segmentId) == 2 + expect(header.length) == 200 + } + + func testReplayVideoHeader() { + let header = SentryReplayEnvelopeItemHeader.replayVideoHeader(withSegmentId: 3, length: 300) + + expect(header.type) == SentryEnvelopeItemTypeReplayVideo + expect(header.segmentId) == 3 + expect(header.length) == 300 + } + + func testSerialize() { + let header = SentryReplayEnvelopeItemHeader(type: "testType", segmentId: 4, length: 400) + let serialized = header.serialize() + + expect(serialized["type"] as? String) == "testType" + expect(serialized["length"] as? Int) == 400 + expect(serialized["segment_id"] as? Int) == 4 + } + + func testEnvelopeItemHeaderType() { + expect(SentryEnvelopeItemTypeReplayVideo) == "replay_video" + expect(SentryEnvelopeItemTypeReplayRecording) == "replay_recording" + } +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift new file mode 100644 index 00000000000..96391581a46 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift @@ -0,0 +1,30 @@ +import Foundation +import Nimble +import XCTest + +class SentryReplayEventTests: XCTestCase { + + func test_Serialize() { + let sut = SentryReplayEvent() + sut.urls = ["Screen 1", "Screen 2"] + sut.replayStartTimestamp = Date(timeIntervalSince1970: 1) + + let traceIds = [SentryId(), SentryId()] + sut.traceIds = traceIds + + let replayId = SentryId() + sut.replayId = replayId + + sut.segmentId = 3 + + let result = sut.serialize() + + expect(result["urls"] as? [String]) == ["Screen 1", "Screen 2"] + expect(result["replay_start_timestamp"] as? Int) == 1_000 + expect(result["trace_ids"] as? [String]) == [ traceIds[0].sentryIdString, traceIds[1].sentryIdString] + expect(result["replay_id"] as? String) == replayId.sentryIdString + expect(result["segment_id"] as? Int) == 3 + expect(result["replay_type"] as? String) == "buffer" + } + +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift new file mode 100644 index 00000000000..3d8f01c3da3 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift @@ -0,0 +1,41 @@ +import Foundation +import Nimble +import XCTest + +class SentryReplayRecordingTests: XCTestCase { + + func test_serialize() { + let sut = SentryReplayRecording(segmentId: 3, size: 200, start: Date(timeIntervalSince1970: 2), duration: 5_000, frameCount: 5, frameRate: 1, height: 930, width: 390) + + let data = sut.serialize() + + let metaInfo = data[0] + let metaInfoData = metaInfo["data"] as? [String: Any] + + let recordingInfo = data[1] + let recordingData = recordingInfo["data"] as? [String: Any] + let recordingPayload = recordingData?["payload"] as? [String: Any] + + expect(metaInfo["type"] as? Int) == 4 + expect(metaInfo["timestamp"] as? Int) == 2_000 + expect(metaInfoData?["href"] as? String) == "" + expect(metaInfoData?["height"] as? Int) == 930 + expect(metaInfoData?["width"] as? Int) == 390 + + expect(recordingInfo["type"] as? Int) == 5 + expect(recordingInfo["timestamp"] as? Int) == 2_000 + expect(recordingData?["tag"] as? String) == "video" + expect(recordingPayload?["segmentId"] as? Int) == 3 + expect(recordingPayload?["size"] as? Int) == 200 + expect(recordingPayload?["duration"] as? Int) == 5_000 + expect(recordingPayload?["encoding"] as? String) == "h264" + expect(recordingPayload?["container"] as? String) == "mp4" + expect(recordingPayload?["height"] as? Int) == 930 + expect(recordingPayload?["width"] as? Int) == 390 + expect(recordingPayload?["frameCount"] as? Int) == 5 + expect(recordingPayload?["frameRateType"] as? String) == "constant" + expect(recordingPayload?["frameRate"] as? Int) == 1 + expect(recordingPayload?["left"] as? Int) == 0 + expect(recordingPayload?["top"] as? Int) == 0 + } +} diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index a2903c61179..d365a05283b 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -1,3 +1,4 @@ +import Nimble import SentryTestUtils import XCTest @@ -235,6 +236,12 @@ class SentryEnvelopeTests: XCTestCase { XCTAssertEqual(attachment.contentType, envelopeItem.header.contentType) } + func testEmptyHeader() { + let sut = SentryEnvelopeHeader.empty() + expect(sut.eventId) == nil + expect(sut.traceContext) == nil + } + func testInitWithFileAttachment() { writeDataToFile(data: fixture.data ?? Data()) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index b650280d35f..3908b95e494 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1576,6 +1576,32 @@ class SentryClientTest: XCTestCase { } } + func testCaptureReplayEvent() { + let sut = fixture.getSut() + let replayEvent = SentryReplayEvent() + replayEvent.segmentId = 2 + let replayRecording = SentryReplayRecording() + replayRecording.segmentId = 2 + + //Not a video url, but its ok for test the envelope + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!) + + let envelope = fixture.transport.sentEnvelopes.first + + let recordingHeader = envelope?.items[1].header as? SentryReplayEnvelopeItemHeader + let videoHeader = envelope?.items[2].header as? SentryReplayEnvelopeItemHeader + + expect(envelope?.items.count) == 3 + expect(envelope?.items[2].data.count) == 120_617 + expect(recordingHeader?.segmentId) == 2 + expect(videoHeader?.segmentId) == 2 + + expect(recordingHeader?.type) == SentryEnvelopeItemTypeReplayRecording + expect(videoHeader?.type) == SentryEnvelopeItemTypeReplayVideo + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index acbe3676eaa..61e518cdd08 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -216,6 +216,9 @@ #import "SentryNSProcessInfoWrapper.h" #import "SentryPerformanceTracker+Testing.h" #import "SentryPropagationContext.h" +#import "SentryReplayEnvelopeItemHeader.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" #import "SentrySampleDecision+Private.h" #import "SentrySpanOperations.h" #import "SentryTimeToDisplayTracker.h" From 9e21c1eccfaf763fd44e02168501dea50fbeb3cf Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 15 Feb 2024 10:31:29 +0100 Subject: [PATCH 02/88] refactoring --- Sentry.xcodeproj/project.pbxproj | 8 ++++++++ Sources/Sentry/SentryDateUtil.m | 2 +- Sources/Sentry/SentryReplayEvent.m | 4 ++-- Sources/Sentry/SentryReplayRecording.m | 8 ++++---- Sources/Sentry/SentryReplayType.m | 14 ++++++++++++++ Sources/Sentry/include/SentryDateUtil.h | 2 +- Sources/Sentry/include/SentryReplayEvent.h | 6 ++++++ Sources/Sentry/include/SentryReplayRecording.h | 4 ++++ Sources/Sentry/include/SentryReplayType.h | 16 ++++++++++++++++ .../SentryTests/Helper/SentryDateUtilTests.swift | 2 +- 10 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 Sources/Sentry/SentryReplayType.m create mode 100644 Sources/Sentry/include/SentryReplayType.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index e972a73648d..977046834a6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -746,6 +746,8 @@ D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */; }; D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */; }; D80694CA2B7CD65800B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */; }; + D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */ = {isa = PBXBuildFile; fileRef = D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */; }; + D80694CE2B7E0A3E00B820E6 /* SentryReplayType.m in Sources */ = {isa = PBXBuildFile; fileRef = D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */; }; D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */; }; D808FB8B281BCE96009A2A33 /* TestSentrySwizzleWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */; }; D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */; }; @@ -1736,6 +1738,8 @@ D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEventTests.swift; sourceTree = ""; }; D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayRecordingTests.swift; sourceTree = ""; }; D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEnvelopeItemHeaderTests.swift; sourceTree = ""; }; + D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayType.h; path = include/SentryReplayType.h; sourceTree = ""; }; + D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayType.m; sourceTree = ""; }; D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackerTests.swift; sourceTree = ""; }; D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentrySwizzleWrapper.swift; sourceTree = ""; }; D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackingIntegrationTests.swift; sourceTree = ""; }; @@ -3401,6 +3405,8 @@ children = ( D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */, D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */, + D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */, + D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, @@ -3811,6 +3817,7 @@ 62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */, 15360CF02433A16D00112302 /* SentryInstallation.h in Headers */, 63FE714720DA4C1100CDBAE8 /* SentryCrashMachineContext.h in Headers */, + D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */, 7BA61CAB247BA98100C130A8 /* SentryDebugImageProvider.h in Headers */, 7BC63F0828081242009D9E37 /* SentrySwizzleWrapper.h in Headers */, 638DC9A01EBC6B6400A66E41 /* SentryRequestOperation.h in Headers */, @@ -4297,6 +4304,7 @@ 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, 62C316832B1F2EA1000D7031 /* SentryDelayedFramesTracker.m in Sources */, D8BFE37329A3782F002E73F3 /* SentryTimeToDisplayTracker.m in Sources */, + D80694CE2B7E0A3E00B820E6 /* SentryReplayType.m in Sources */, 15360CCF2432777500112302 /* SentrySessionTracker.m in Sources */, 6334314320AD9AE40077E581 /* SentryMechanism.m in Sources */, 63FE70D320DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.c in Sources */, diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index 30e19e10d10..299805e48e4 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -38,7 +38,7 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } -+ (long)javascriptDate:(NSDate *)date ++ (long)millisecondsSince1970:(NSDate *)date { return (NSInteger)([date timeIntervalSince1970] * 1000); } diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index a28de9a8cff..2d67ea6c494 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -16,11 +16,11 @@ - (NSDictionary *)serialize result[@"urls"] = self.urls; result[@"replay_start_timestamp"] = - @([SentryDateUtil javascriptDate:self.replayStartTimestamp]); + @([SentryDateUtil millisecondsSince1970:self.replayStartTimestamp]); result[@"trace_ids"] = trace_ids; result[@"replay_id"] = self.replayId.sentryIdString; result[@"segment_id"] = @(self.segmentId); - result[@"replay_type"] = @"buffer"; + result[@"replay_type"] = nameForSentryReplayType(self.replayType); return result; } diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index 55a639967bd..e91d3e81e05 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -28,7 +28,7 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId - (nonnull NSArray *> *)serialize { - long timestamp = [SentryDateUtil javascriptDate:self.start]; + long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; NSDictionary *metaInfo = @{ @"type" : @4, @@ -45,12 +45,12 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId @"segmentId" : @(self.segmentId), @"size" : @(self.size), @"duration" : @(self.duration), - @"encoding" : @"h264", - @"container" : @"mp4", + @"encoding" : SentryReplayEncoding, + @"container" : SentryReplayContainer, @"height" : @(self.height), @"width" : @(self.width), @"frameCount" : @(self.frameCount), - @"frameRateType" : @"constant", + @"frameRateType" : SentryReplayFrameRateType, @"frameRate" : @(self.frameRate), @"left" : @0, @"top" : @0, diff --git a/Sources/Sentry/SentryReplayType.m b/Sources/Sentry/SentryReplayType.m new file mode 100644 index 00000000000..c4d200310f7 --- /dev/null +++ b/Sources/Sentry/SentryReplayType.m @@ -0,0 +1,14 @@ +#import "SentryReplayType.h" + +NSString *const kSentryReplayTypeNameBuffer = @"buffer"; +NSString *const kSentryReplayTypeNameSession = @"session"; + +NSString *_Nonnull nameForSentryReplayType(SentryReplayType replayType) +{ + switch (replayType) { + case kSentryReplayTypeBuffer: + return kSentryReplayTypeNameBuffer; + case kSentryReplayTypeSession: + return kSentryReplayTypeNameSession; + } +} diff --git a/Sources/Sentry/include/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index 98fa11de6bb..8cb845c984a 100644 --- a/Sources/Sentry/include/SentryDateUtil.h +++ b/Sources/Sentry/include/SentryDateUtil.h @@ -9,7 +9,7 @@ NS_SWIFT_NAME(DateUtil) + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second; -+ (long)javascriptDate:(NSDate *)date; ++ (long)millisecondsSince1970:(NSDate *)date; @end diff --git a/Sources/Sentry/include/SentryReplayEvent.h b/Sources/Sentry/include/SentryReplayEvent.h index 74f2beb22ff..ef20250097d 100644 --- a/Sources/Sentry/include/SentryReplayEvent.h +++ b/Sources/Sentry/include/SentryReplayEvent.h @@ -1,4 +1,5 @@ #import "SentryEvent.h" +#import "SentryReplayType.h" #import NS_ASSUME_NONNULL_BEGIN @@ -34,6 +35,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, strong) SentryId *replayId; +/** + * The type of the replay + */ +@property (nonatomic) SentryReplayType replayType; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h index 66dfe526034..3c46237c9cf 100644 --- a/Sources/Sentry/include/SentryReplayRecording.h +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -5,6 +5,10 @@ NS_ASSUME_NONNULL_BEGIN @class SentryId; +static NSString *const SentryReplayEncoding = @"h264"; +static NSString *const SentryReplayContainer = @"mp4"; +static NSString *const SentryReplayFrameRateType = @"constant"; + @interface SentryReplayRecording : NSObject @property (nonatomic) NSInteger segmentId; diff --git a/Sources/Sentry/include/SentryReplayType.h b/Sources/Sentry/include/SentryReplayType.h new file mode 100644 index 00000000000..93c018806b5 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayType.h @@ -0,0 +1,16 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, SentryReplayType) { + kSentryReplayTypeBuffer = 0, // Replay triggered by an action + kSentryReplayTypeSession // Full session replay +}; + +FOUNDATION_EXPORT NSString *const kSentryReplayTypeNameBuffer; +FOUNDATION_EXPORT NSString *const kSentryReplayTypeNameSession; + +NSString *nameForSentryReplayType(SentryReplayType replayType); + +NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Helper/SentryDateUtilTests.swift b/Tests/SentryTests/Helper/SentryDateUtilTests.swift index bb3e5e30b44..50096006244 100644 --- a/Tests/SentryTests/Helper/SentryDateUtilTests.swift +++ b/Tests/SentryTests/Helper/SentryDateUtilTests.swift @@ -57,7 +57,7 @@ class SentryDateUtilTests: XCTestCase { func testJavascriptDate() { let testDate = Date(timeIntervalSince1970: 60) - let timestamp = DateUtil.javascriptDate(testDate) + let timestamp = DateUtil.millisecondsSince1970(testDate) expect(timestamp) == 60_000 } From ad347c3c00e941b0029519d9f243d06eadf47d54 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 16 Feb 2024 10:57:26 +0100 Subject: [PATCH 03/88] SessionReplayIntegration --- Sentry.xcodeproj/project.pbxproj | 42 ++++ Sources/Sentry/Public/SentryOptions.h | 12 +- Sources/Sentry/Public/SentryReplaySettings.h | 35 +++ Sources/Sentry/SentryOnDemandReplay.m | 206 +++++++++++++++++ Sources/Sentry/SentryOptions.m | 14 +- Sources/Sentry/SentryReplaySettings.m | 43 ++++ Sources/Sentry/SentrySessionReplay.m | 212 ++++++++++++++++++ .../Sentry/SentrySessionReplayIntegration.m | 56 +++++ Sources/Sentry/SentryViewPhotographer.m | 144 ++++++++++++ .../SentryReplaySettings+Private.h | 19 ++ Sources/Sentry/include/SentryOnDemandReplay.h | 32 +++ Sources/Sentry/include/SentrySessionReplay.h | 26 +++ .../include/SentrySessionReplayIntegration.h | 11 + .../Sentry/include/SentryViewPhotographer.h | 19 ++ 14 files changed, 866 insertions(+), 5 deletions(-) create mode 100644 Sources/Sentry/Public/SentryReplaySettings.h create mode 100644 Sources/Sentry/SentryOnDemandReplay.m create mode 100644 Sources/Sentry/SentryReplaySettings.m create mode 100644 Sources/Sentry/SentrySessionReplay.m create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m create mode 100644 Sources/Sentry/SentryViewPhotographer.m create mode 100644 Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h create mode 100644 Sources/Sentry/include/SentryOnDemandReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.h create mode 100644 Sources/Sentry/include/SentryViewPhotographer.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 977046834a6..3b13f68ec68 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -765,6 +765,16 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; + D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */; }; + D83D07832B7E5F2100CC9674 /* SentryReplaySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */; }; + D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; + D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; + D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; + D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */; }; + D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */; }; + D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */; }; + D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */; }; + D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */; }; D8479328278873A100BE8E99 /* SentryByteCountFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = D8479327278873A100BE8E99 /* SentryByteCountFormatter.h */; }; @@ -1764,6 +1774,17 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; + D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplaySettings.h; path = Public/SentryReplaySettings.h; sourceTree = ""; }; + D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplaySettings.m; sourceTree = ""; }; + D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplaySettings+Private.h"; path = "include/HybridPublic/SentryReplaySettings+Private.h"; sourceTree = ""; }; + D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; + D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; + D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; + D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; + D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; + D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; + D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryBinaryImageCache+Private.h"; sourceTree = ""; }; D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryByteCountFormatter.m; sourceTree = ""; }; @@ -3411,6 +3432,17 @@ D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, + D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */, + D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */, + D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */, + D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */, + D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */, + D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */, + D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */, + D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */, + D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */, + D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */, + D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */, ); name = SessionReplay; sourceTree = ""; @@ -3631,6 +3663,7 @@ 03F84D2A27DD416B008FE43F /* SentryProfilingLogging.hpp in Headers */, 63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */, 7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */, + D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */, 63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */, 7BE912AB272162AF00E49E62 /* SentryNoOpSpan.h in Headers */, 63FE70D120DA4C1000CDBAE8 /* SentryCrashMonitorContext.h in Headers */, @@ -3646,6 +3679,7 @@ 0356A570288B4612008BF593 /* SentryProfilesSampler.h in Headers */, 63295AF51EF3C7DB002D4490 /* NSDictionary+SentrySanitize.h in Headers */, 8E4A037825F6F52100000D77 /* SentrySampleDecision.h in Headers */, + D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */, 63FE717920DA4C1100CDBAE8 /* SentryCrashReportStore.h in Headers */, 0AAE202128ED9BCC00D0CD80 /* SentryReachability.h in Headers */, D858FA662A29EAB3002A3503 /* SentryBinaryImageCache.h in Headers */, @@ -3701,6 +3735,7 @@ 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, + D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, 7BC9A20228F41350001E7C4C /* SentryMeasurementUnit.h in Headers */, @@ -3825,6 +3860,7 @@ 6344DDB01EC308E400D9160D /* SentryCrashInstallationReporter.h in Headers */, 8E25C95725F836EE00DC215B /* SentryRandom.h in Headers */, 7B5CAF7527F5A67C00ED0DB6 /* SentryNSURLRequestBuilder.h in Headers */, + D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */, 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, 7BA61CB4247BC3EB00C130A8 /* SentryCrashBinaryImageProvider.h in Headers */, 63FE713D20DA4C1100CDBAE8 /* SentryCrashLogger.h in Headers */, @@ -3852,6 +3888,7 @@ 6334314120AD9AE40077E581 /* SentryMechanism.h in Headers */, 03F84D2827DD414C008FE43F /* SentryCPU.h in Headers */, 0A80E435291017D500095219 /* SentryWatchdogTerminationScopeObserver.h in Headers */, + D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */, 7B610D642512399600B0B5D9 /* SentryHub+Private.h in Headers */, D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */, 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, @@ -4173,6 +4210,7 @@ 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, 7B6C5EDE264E8DF00010D138 /* SentryFramesTracker.m in Sources */, + D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */, D84F833E2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m in Sources */, 7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */, 63AA76A31EB9CBAA00D153DE /* SentryDsn.m in Sources */, @@ -4233,6 +4271,7 @@ 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, + D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */, 7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */, 63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */, 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, @@ -4259,6 +4298,7 @@ 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, 63FE712120DA4C1000CDBAE8 /* SentryCrashSymbolicator.c in Sources */, + D83D07832B7E5F2100CC9674 /* SentryReplaySettings.m in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, @@ -4299,6 +4339,7 @@ 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, + D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, @@ -4394,6 +4435,7 @@ 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, + D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index c7714a631ae..7e95d580ea5 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -3,7 +3,7 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope; +@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, SentryReplaySettings; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -268,6 +268,15 @@ NS_SWIFT_NAME(Options) * @note Default value is @c NO . */ @property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing; + + +/** + * @warning This is an experimental feature and may still have bugs. + * Settings to configure the session replay. + * @node Default value is @c nil . + */ +@property (nonatomic, strong) SentryReplaySettings * sessionReplaySettings API_AVAILABLE(ios(16.0), tvos(16.0)); + #endif // SENTRY_UIKIT_AVAILABLE /** @@ -523,6 +532,7 @@ NS_SWIFT_NAME(Options) YES)`. */ @property (nonatomic, copy) NSString *cacheDirectoryPath; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h new file mode 100644 index 00000000000..1c66a0f2712 --- /dev/null +++ b/Sources/Sentry/Public/SentryReplaySettings.h @@ -0,0 +1,35 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryReplaySettings : NSObject + +/** + * Indicates the percentage in which the replay for the session will be created. + * @discussion Specifying @c 0 means never, @c 1.0 means always. + * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * @note The default is @c 0. + */ +@property (nonatomic) CGFloat replaysSessionSampleRate; + +/** + * Indicates the percentage in which a 30 seconds replay will be send with error events. + * @discussion Specifying @c 0 means never, @c 1.0 means always. + * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * @note The default is @c 0. + */ +@property (nonatomic) CGFloat replaysOnErrorSampleRate; + +/** + * Inittialize the settings of session replay + * + * @param sessionSampleRate Indicates the percentage in which the replay for the session will be created. + * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with error events. + */ +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m new file mode 100644 index 00000000000..36c55b93154 --- /dev/null +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -0,0 +1,206 @@ +#import "SentryOnDemandReplay.h" + +#if SENTRY_HAS_UIKIT +#import +#import +#import "SentryLog.h" +@interface SentryReplayFrame : NSObject + +@property (nonatomic, strong) NSString *imagePath; +@property (nonatomic, strong) NSDate *time; + +-(instancetype) initWithPath:(NSString *)path time:(NSDate*)time; + +@end + +@implementation SentryReplayFrame +-(instancetype) initWithPath:(NSString *)path time:(NSDate*)time { + if (self = [super init]) { + self.imagePath = path; + self.time = time; + } + return self; +} + +@end + +@implementation SentryOnDemandReplay +{ + NSString * _outputPath; + NSDate * _startTime; + NSMutableArray * _frames; + CGSize _videoSize; + dispatch_queue_t _onDemandDispatchQueue; +} + +- (instancetype)initWithOutputPath:(NSString *)outputPath { + if (self = [super init]) { + _outputPath = outputPath; + _startTime = [[NSDate alloc] init]; + _frames = [NSMutableArray array]; + //_videoSize = CGSizeMake(300, 651); + _videoSize = CGSizeMake(200, 434); + _bitRate = 20000; + _cacheMaxSize = NSUIntegerMax; + _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); + + } + return self; +} + +- (void)addFrame:(UIImage *)image { + dispatch_async(_onDemandDispatchQueue, ^{ + NSData * data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); + NSDate* date = [[NSDate alloc] init]; + NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; + NSString *imagePath = [self->_outputPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; + + [data writeToFile:imagePath atomically:YES]; + + SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; + [self->_frames addObject:frame]; + + while (self->_frames.count > self->_cacheMaxSize) { + [self removeOldestFrame]; + } + }); +} + +- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth { + CGSize originalSize = originalImage.size; + CGFloat aspectRatio = originalSize.width / originalSize.height; + + CGFloat newWidth = MIN(originalSize.width, maxWidth); + CGFloat newHeight = newWidth / aspectRatio; + + CGSize newSize = CGSizeMake(newWidth, newHeight); + + UIGraphicsBeginImageContextWithOptions(newSize, NO, 1); + [originalImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return resizedImage; +} + +- (void)releaseFramesUntil:(NSDate *)date { + dispatch_async(_onDemandDispatchQueue, ^{ + while (self->_frames.count > 0 && [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { + [self removeOldestFrame]; + } + }); +} + +- (void)removeOldestFrame { + NSError * error; + if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath error:&error]){ + SENTRY_LOG_DEBUG(@"Could not delete replay frame at: %@. %@",_frames.firstObject.imagePath, error); + } + [_frames removeObjectAtIndex:0]; +} + +- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + completion:(void (^)(BOOL success, NSError *error))completion { + // Set up AVAssetWriter with appropriate settings + AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL + fileType:AVFileTypeQuickTimeMovie + error:nil]; + + NSDictionary *videoSettings = @{ + AVVideoCodecKey: AVVideoCodecTypeH264, + AVVideoWidthKey: @(_videoSize.width), + AVVideoHeightKey: @(_videoSize.height), + AVVideoCompressionPropertiesKey: @{ + AVVideoAverageBitRateKey: @(_bitRate), + AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel, + }, + }; + + AVAssetWriterInput *videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; + NSDictionary *bufferAttributes = @{ + (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB), + }; + + AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:bufferAttributes]; + + [videoWriter addInput:videoWriterInput]; + + // Start writing video + [videoWriter startWriting]; + [videoWriter startSessionAtSourceTime:kCMTimeZero]; + + NSDate* end = [beginning dateByAddingTimeInterval:duration]; + __block NSInteger frameCount = 0; + NSMutableArray * frames = [NSMutableArray array]; + for (SentryReplayFrame *frame in self->_frames) { + if ([frame.time compare:beginning] == NSOrderedAscending) { + continue;; + } else if ([frame.time compare:end] == NSOrderedDescending) { + break; + } + [frames addObject:frame.imagePath]; + } + + [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue usingBlock:^{ + UIImage *image = [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image pixelBufferAdaptor:pixelBufferAdaptor presentationTime:presentTime]) { + if (completion) { + completion(NO, videoWriter.error); + } + } + } + + if (frameCount >= frames.count){ + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + completion(videoWriter.status == AVAssetWriterStatusCompleted, videoWriter.error); + } + }]; + } + }]; +} + +- (BOOL)appendPixelBufferForImage:(UIImage *)image pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor presentationTime:(CMTime)presentationTime { + CVReturn status = kCVReturnSuccess; + + CVPixelBufferRef pixelBuffer = NULL; + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); + + if (status != kCVReturnSuccess) { + return NO; + } + + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); + + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); + + CGContextTranslateCTM(context, 0, image.size.height); + CGContextScaleCTM(context, 1.0, -1.0); + + UIGraphicsPushContext(context); + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + UIGraphicsPopContext(); + + CGColorSpaceRelease(rgbColorSpace); + CGContextRelease(context); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + + // Append the pixel buffer with the current image to the video + BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]; + + CVPixelBufferRelease(pixelBuffer); + + return success; +} + + +@end +#endif //SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 82a1e4a506e..ef4fa424a4a 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,12 +23,11 @@ # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" -# if SENTRY_HAS_UIKIT -# import "SentryScreenshotIntegration.h" -# endif // SENTRY_HAS_UIKIT +# import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" +# import "SentryReplaySettings+Private.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @@ -264,6 +263,7 @@ - (void)setDsn:(NSString *)dsn } } + /** * Populates all @c SentryOptions values from @c options dict using fallbacks/defaults if needed. */ @@ -391,7 +391,6 @@ - (BOOL)validateOptions:(NSDictionary *)options if ([self isBlock:options[@"initialScope"]]) { self.initialScope = options[@"initialScope"]; } - #if SENTRY_HAS_UIKIT [self setBool:options[@"enableUIViewControllerTracing"] block:^(BOOL value) { self->_enableUIViewControllerTracing = value; }]; @@ -411,6 +410,13 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; + + if (@available(iOS 16.0, *)) { + if ([options[@"sessionReplaySettings"] isKindOfClass:NSDictionary.class]) { + self.sessionReplaySettings = [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; + } + } + #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m new file mode 100644 index 00000000000..b2a67f474fd --- /dev/null +++ b/Sources/Sentry/SentryReplaySettings.m @@ -0,0 +1,43 @@ +#import "SentryReplaySettings.h" + + +@interface SentryReplaySettings () + +@property (nonatomic) NSInteger replayBitRate; + +@end + +@implementation SentryReplaySettings + +-(instancetype)init { + if (self = [super init]) { + self.replaysSessionSampleRate = 0; + self.replaysOnErrorSampleRate = 0; + self.replayBitRate = 20000; + } + return self; +} + +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate { + if (self = [self init]) { + self.replaysSessionSampleRate = sessionSampleRate; + self.replaysOnErrorSampleRate = errorSampleRate; + } + + return self; +} + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary { + if (self = [self init]) { + if ([dictionary[@"replaysSessionSampleRate"] isKindOfClass:NSNumber.class]) { + self.replaysSessionSampleRate = [dictionary[@"replaysSessionSampleRate"] floatValue]; + } + + if ([dictionary[@"replaysOnErrorSampleRate"] isKindOfClass:NSNumber.class]) { + self.replaysOnErrorSampleRate = [dictionary[@"replaysOnErrorSampleRate"] floatValue]; + } + } + return self; +} + +@end diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m new file mode 100644 index 00000000000..8d9bd4678a8 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.m @@ -0,0 +1,212 @@ +#import "SentrySessionReplay.h" +#import "SentryVideoReplay.h" +#import "SentryImagesReplay.h" +#import "SentryViewPhotographer.h" +#import "SentryOndemandReplay.h" +#import "SentryAttachment+Private.h" +#import "SentryLog.h" +#import "SentryTouchesTracker.h" +//#define use_video 1 +#define use_ondemand 1 + +@implementation SentrySessionReplay { + UIView * _rootView; + BOOL _processingScreenshot; + CADisplayLink * _displayLink; + NSDate * _lastScreenShot; + NSDate * _videoSegmentStart; + NSURL * _urlToCache; + NSDate * _sessionStart; + SentryReplaySettings * _settings; +#if use_video + SentryVideoReplay * replayMaker; +#elif use_ondemand + SentryOnDemandReplay * _replayMaker; +#else + SentryImagesReplay * replayMaker; +#endif + + NSMutableArray* imageCollection; + SentryTouchesTracker * _touchesTracker; +} + +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings { + if (self = [super init]) { + _settings = replaySettings; + } + return self; +} + +- (void)start:(UIView *)rootView fullSession:(BOOL)full { + if (rootView == nil) { + SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); + return; + } + + @synchronized (self) { + if (_displayLink == nil) { + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } else { + //Session display is already running. + return; + } + + _rootView = rootView; + _lastScreenShot = [[NSDate alloc] init]; + _videoSegmentStart = nil; + _sessionStart = _lastScreenShot; + _touchesTracker = [[SentryTouchesTracker alloc] init]; + [_touchesTracker start]; + + NSURL * docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject URLByAppendingPathComponent:@"io.sentry"]; + + NSString * currentSession = [NSUUID UUID].UUIDString; + _urlToCache = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache withIntermediateDirectories:YES attributes:nil error:nil]; + } + + _replayMaker = +#if use_video + [[SentryVideoReplay alloc] initWithOutputPath:[urlToCache URLByAppendingPathComponent:@"sr.mp4"].path frameSize:rootView.frame.size framesPerSec:1]; +#elif use_ondemand + [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; + _replayMaker.bitRate = _settings.replayBitRate; + _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; +#else + [[SentryImagesReplay alloc] initWithOutputPath:urlToCache.path]; +#endif + imageCollection = [NSMutableArray array]; + + NSLog(@"Recording session to %@",_urlToCache); + } +} + +- (void)stop { + [_displayLink invalidate]; + _displayLink = nil; +#ifdef use_video + [videoReplay finalizeVideoWithCompletion:^(BOOL success, NSError * _Nonnull error) { + if (!success) { + NSLog(@"%@", error); + } + }]; +#endif +} + +- (NSArray *)processAttachments:(NSArray *)attachments + forEvent:(nonnull SentryEvent *)event +{ +#if use_ondemand + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { + return attachments; + } + + NSLog(@"Recording session event id %@", event.eventId); + NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; + + NSURL * finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + + dispatch_group_t _wait_for_render = dispatch_group_create(); + + dispatch_group_enter(_wait_for_render); + [_replayMaker createVideoOf:30 + from:[NSDate dateWithTimeIntervalSinceNow:-30] + outputFileURL:finalPath + completion:^(BOOL success, NSError * _Nonnull error) { + dispatch_group_leave(_wait_for_render); + }]; + dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); + + SentryAttachment *attachment = + [[SentryAttachment alloc] initWithPath:finalPath.path + filename:@"replay.mp4" + contentType:@"video/mp4"]; + + [result addObject:attachment]; + + return result; +#else + return attachments; +#endif +} + +- (void)sendReplayForEvent:(SentryEvent *)event { +#if use_ondemand + +#endif +} + +- (void)newFrame:(CADisplayLink *)sender { + NSDate * now = [[NSDate alloc] init]; + + if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { + [self takeScreenshot]; + _lastScreenShot = now; + + if (_videoSegmentStart == nil) { + _videoSegmentStart = now; + } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { + [self prepareSegmentUntil: now]; + } + } +} + +- (void)prepareSegmentUntil:(NSDate *)date { + NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; + NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; + NSURL * pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { + NSError * error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path withIntermediateDirectories:YES attributes:nil error:&error]){ + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", error.localizedDescription); + return; + } + } + + pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4",from, to]]; + + dispatch_group_t _wait_for_render = dispatch_group_create(); + + dispatch_group_enter(_wait_for_render); + [_replayMaker createVideoOf:5 + from:[date dateByAddingTimeInterval:-5] + outputFileURL:pathToSegment + completion:^(BOOL success, NSError * _Nonnull error) { + dispatch_group_leave(_wait_for_render); + + //Need to send the segment here + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; + +} + +- (void)takeScreenshot { + if (_processingScreenshot) { return; } + @synchronized (self) { + if (_processingScreenshot) { return; } + _processingScreenshot = YES; + } + + UIImage* screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; + + _processingScreenshot = NO; + + dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ +#if use_video + [self->replayMaker addFrame:screenshot withCompletion:^(BOOL success, NSError * _Nonnull error) { + + }]; +#else + [self->_replayMaker addFrame:screenshot]; +#endif + }); +} + +@end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m new file mode 100644 index 00000000000..2964a8dbaf0 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -0,0 +1,56 @@ +#import "SentrySessionReplayIntegration.h" +#import "SentrySessionReplay.h" +#import "SentryDependencyContainer.h" +#import "SentryUIApplication.h" +#import "SentrySDK+Private.h" +#import "SentryClient+Private.h" +#import "SentryHub+Private.h" +#import "SentrySDK+Private.h" +#import "SentryReplaySettings.h" +#import "SentryRandom.h" + +@implementation SentrySessionReplayIntegration { + SentrySessionReplay * sessionReplay; +} + +- (BOOL)installWithOptions:(nonnull SentryOptions *)options +{ + if ([super installWithOptions:options] == NO) { + return NO; + } + + if (options.replaySettings.replaysSessionSampleRate == 0 && options.replaySettings.replaysOnErrorSampleRate == 0) { + return NO; + } + + sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.replaySettings]; + + [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.replaySettings.replaysSessionSampleRate]]; + + SentryClient *client = [SentrySDK.currentHub getClient]; + [client addAttachmentProcessor:sessionReplay]; + + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; + return YES; +} + +-(void)stop { + [sessionReplay stop]; +} + +- (SentryIntegrationOption)integrationOptions +{ + return kIntegrationOptionEnableReplay; +} + +- (void)uninstall +{ + +} + +- (BOOL)shouldReplayFullSession:(CGFloat)rate { + return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; +} + +@end diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m new file mode 100644 index 00000000000..aaeca74ec0c --- /dev/null +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -0,0 +1,144 @@ +#import "SentryViewPhotographer.h" + +#if SENTRY_HAS_UIKIT + +@implementation SentryViewPhotographer { + NSMutableArray * _ignoreClasses; + NSMutableArray * _redactClasses; +} + ++(SentryViewPhotographer *)shared { + static SentryViewPhotographer* _shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _shared = [[SentryViewPhotographer alloc] init]; + }); + + return _shared; +} + +-(instancetype)init { + if (self = [super init]) { + _ignoreClasses = @[ + UISlider.class, + UISwitch.class + ].mutableCopy; + + _redactClasses = @[ + UILabel.class, + UITextView.class, + UITextField.class + ].mutableCopy; + + + NSArray * extraClasses = @[ + @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + @"SwiftUI._UIGraphicsView", + @"SwiftUI.ImageLayer" + ]; + + for (NSString * className in extraClasses) { + Class viewClass = NSClassFromString(className); + if (viewClass != nil) {[_redactClasses addObject:viewClass];} + } + } + return self; +} + +-(UIImage*)imageFromUIView:(UIView *)view { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + + [view.layer renderInContext:currentContext]; + + [self maskText:view context:currentContext]; + + UIImage* screenshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return screenshot; +} + +- (void)maskText:(UIView *)view context:(CGContextRef)context { + [UIColor.blackColor setFill]; + CGPathRef maskPath = [self buildPathForView:view inPath:CGPathCreateMutable() visibleArea:view.frame]; + CGContextAddPath(context, maskPath); + CGContextFillPath(context); +} + +- (BOOL)shouldIgnoreView:(UIView *)view { + return [view isKindOfClass:UISwitch.class]; +} + +- (BOOL)shouldIgnore:(UIView *)view { + for (Class class in _ignoreClasses) { + if ([view isKindOfClass:class]) { + return true; + } + } + return false; +} + +- (BOOL)shouldRedact:(UIView *)view { + for (Class class in _redactClasses) { + if ([view isKindOfClass:class]) { + return true; + } + } + + return ([view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); +} + +- (BOOL)shouldRedactImageView:(UIImageView *)imageView { + return imageView.image != nil + && [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil + && (imageView.image.size.width > 10 && imageView.image.size.height > 10); //This is to avoid redact gradient backgroud that are usually small lines repeating +} + +- (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)path visibleArea:(CGRect)area { + CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; + + if (!CGRectIntersectsRect(area, rectInWindow)) { + return path; + } + + if (view.hidden || view.alpha == 0) { + return path; + } + + BOOL ignore = [self shouldIgnore:view]; + if (!ignore && [self shouldRedact:view]) { + CGPathAddRect(path, NULL, rectInWindow); + return path; + } else if ([self isOpaqueOrHasBackground:view]) { + CGMutablePathRef newPath = [self excludeRect:rectInWindow fromPath:path]; + CGPathRelease(path); + path = newPath; + } + + if (!ignore) { + for (UIView *subview in view.subviews) { + path = [self buildPathForView:subview inPath:path visibleArea:area]; + } + } + + return path; +} + +- (CGMutablePathRef) excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { + if (@available(iOS 16.0, *)) { + CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); + CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); + return CGPathCreateMutableCopy(newPath); + } + return path; +} + +- (BOOL)isOpaqueOrHasBackground:(UIView *)view { + return view.isOpaque || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); +} + +@end + +#endif //SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h new file mode 100644 index 00000000000..8c799c15004 --- /dev/null +++ b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h @@ -0,0 +1,19 @@ +#import "SentryReplaySettings.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryReplaySettings (Private) + +/** + * Defines the quality of the session replay. + * Higher bit rates better quality, but also bigger files to transfer. + * @note The default value is @c 20000; + */ +@property (nonatomic) NSInteger replayBitRate; + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h new file mode 100644 index 00000000000..dc4d78ada3f --- /dev/null +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -0,0 +1,32 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryOnDemandReplay : NSObject + +@property (nonatomic) NSInteger bitRate; + +@property (nonatomic) NSUInteger cacheMaxSize; + +- (instancetype)initWithOutputPath:(NSString *)outputPath; + +- (void)addFrame:(UIImage *)image; + +- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + completion:(void (^)(BOOL success, NSError *error))completion; + +/** + * Remove cached frames until given date. + */ +- (void)releaseFramesUntil:(NSDate *)date; + +@end + +NS_ASSUME_NONNULL_END +#endif //SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h new file mode 100644 index 00000000000..43d89925ba5 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -0,0 +1,26 @@ +#import +#import +#import "SentryEvent.h" +#import "SentryClient+Private.h" +#import "SentryReplaySettings.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplay : NSObject + +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings; + +/** + * Start recording the session using rootView as image source. + * If full is @c YES, we transmit the entire session to sentry. + */ +- (void)start:(UIView *)rootView fullSession:(BOOL)full; + +/** + * Stop recording the session replay + */ +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h new file mode 100644 index 00000000000..6d0dbb0389b --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -0,0 +1,11 @@ +#import +#import "SentryBaseIntegration.h" +#import "SentryIntegrationProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplayIntegration : SentryBaseIntegration + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h new file mode 100644 index 00000000000..e03a8749fbf --- /dev/null +++ b/Sources/Sentry/include/SentryViewPhotographer.h @@ -0,0 +1,19 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryViewPhotographer : NSObject + +@property (nonatomic, readonly, class) SentryViewPhotographer* shared; + +-(UIImage*)imageFromUIView:(UIView *)view; + +@end + +NS_ASSUME_NONNULL_END + +#endif //SENTRY_HAS_UIKIT From 723b074deb6d3e8cfe783a9032c49473019a6854 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 08:02:50 +0100 Subject: [PATCH 04/88] MsgPack --- Sentry.xcodeproj/project.pbxproj | 8 ++ Sources/Sentry/SentryMsgPackSerializer.m | 77 +++++++++++++++++++ Sources/Sentry/SentryReplayRecording.m | 8 +- .../Sentry/include/SentryMsgPackSerializer.h | 32 ++++++++ .../Sentry/include/SentryReplayRecording.h | 4 +- 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 Sources/Sentry/SentryMsgPackSerializer.m create mode 100644 Sources/Sentry/include/SentryMsgPackSerializer.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 977046834a6..becb2fffe74 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -765,6 +765,8 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; + D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; + D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */; }; D8479328278873A100BE8E99 /* SentryByteCountFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = D8479327278873A100BE8E99 /* SentryByteCountFormatter.h */; }; @@ -1764,6 +1766,8 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; + D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; + D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryBinaryImageCache+Private.h"; sourceTree = ""; }; D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryByteCountFormatter.m; sourceTree = ""; }; @@ -3318,6 +3322,8 @@ D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */, 0A2D8DA6289BC905008720F6 /* SentryViewHierarchy.h */, 0A2D8DA7289BC905008720F6 /* SentryViewHierarchy.m */, + D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */, + D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */, ); name = Tools; sourceTree = ""; @@ -3665,6 +3671,7 @@ 7B98D7E425FB7A7200C5A389 /* SentryAppState.h in Headers */, 7BDEAA022632A4580001EA25 /* SentryOptions+Private.h in Headers */, A8AFFCCD29069C3E00967CD7 /* SentryHttpStatusCodeRange.h in Headers */, + D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */, D84F833D2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h in Headers */, 15E0A8EA240F2C9000F044E3 /* SentrySerialization.h in Headers */, 63FE70EF20DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.h in Headers */, @@ -4296,6 +4303,7 @@ 7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */, 844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */, + D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */, 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m new file mode 100644 index 00000000000..9ce13ced6a6 --- /dev/null +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -0,0 +1,77 @@ +#import "SentryMsgPackSerializer.h" + +@implementation SentryMsgPackSerializer + ++ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path { + NSOutputStream * outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; + + uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); + [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; + + // Iterate over the map and serialize each key-value pair + [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + // Pack the key as a string + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t keyLength = (uint8_t)keyData.length; + uint8_t str8Header = (uint8_t)0xD9; + [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; + [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; + + [outputStream write:keyData.bytes maxLength:keyData.length]; + + // Pack the value as a binary string + uint32_t valueLength = (uint32_t)[value streamSize]; + //We will always use the 4 bytes data length for simplicity. + //Worst case we're losing 3 bytes. + uint8_t bin32Header = (uint8_t)0xC6; + [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; + valueLength = NSSwapHostIntToBig(valueLength); + [outputStream write:(uint8_t*)&valueLength maxLength:sizeof(uint32_t)]; + + NSInputStream * inputStream = [value asInputStream]; + [inputStream open]; + + uint8_t buffer[1024]; + NSInteger bytesRead; + + while ([inputStream hasBytesAvailable]) { + bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (bytesRead > 0) { + [outputStream write:buffer maxLength:bytesRead]; + } else if (bytesRead < 0) { + break; + } + } + + [inputStream close]; + }]; +} + +@end + +@implementation NSURL (SentryStreameble) + +- (NSInputStream *)asInputStream { + return [[NSInputStream alloc] initWithURL:self]; +} + +- (NSInteger)streamSize { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:nil]; + NSNumber *fileSize = attributes[NSFileSize]; + return [fileSize unsignedIntegerValue]; +} + +@end + +@implementation NSData (SentryStreameble) + +- (NSInputStream *)asInputStream { + return [[NSInputStream alloc] initWithData:self]; +} + +- (NSInteger)streamSize { + return self.length; +} + +@end diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index e91d3e81e05..21249f9d2b0 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -1,6 +1,8 @@ #import "SentryReplayRecording.h" #import "SentryDateUtil.h" +NS_ASSUME_NONNULL_BEGIN + @implementation SentryReplayRecording - (instancetype)initWithSegmentId:(NSInteger)segmentId @@ -25,11 +27,13 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId return self; } -- (nonnull NSArray *> *)serialize +- (NSArray *> *)serialize { long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; + //This format is defined by RRWeb + //empty values are required by the format NSDictionary *metaInfo = @{ @"type" : @4, @"timestamp" : @(timestamp), @@ -62,3 +66,5 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId } @end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h new file mode 100644 index 00000000000..b40044846a9 --- /dev/null +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -0,0 +1,32 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + + +@protocol SentryStreameble + +- (NSInputStream *) asInputStream; + +- (NSInteger) streamSize; + +@end + +/** + * This is a partial implementation of the MessagePack format. + * We only need to concatenate a list of NSData into an envelope item. + */ +@interface SentryMsgPackSerializer : NSObject + ++ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path; + +@end + + +@interface NSInputStream (inputStreameble) +@end + +@interface NSURL (inputStreameble) +@end + + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h index 3c46237c9cf..45e379c980d 100644 --- a/Sources/Sentry/include/SentryReplayRecording.h +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -3,8 +3,6 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryId; - static NSString *const SentryReplayEncoding = @"h264"; static NSString *const SentryReplayContainer = @"mp4"; static NSString *const SentryReplayFrameRateType = @"constant"; @@ -39,7 +37,7 @@ static NSString *const SentryReplayFrameRateType = @"constant"; height:(NSInteger)height width:(NSInteger)width; -- (nonnull NSArray *> *)serialize; +- (NSArray *> *)serialize; @end From d27ec151b5ccaed625ba87c97d1dd66fe6f256c1 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 09:02:51 +0100 Subject: [PATCH 05/88] Capture with scope --- Sources/Sentry/SentryClient.m | 3 ++- Sources/Sentry/SentryHub.m | 6 ++++++ Sources/Sentry/SentryReplayEvent.m | 6 +++++- Sources/Sentry/include/SentryClient+Private.h | 3 ++- Sources/Sentry/include/SentryHub+Private.h | 6 ++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 03cfa1d1a70..fd02fff78dc 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -484,9 +484,10 @@ - (void)captureSession:(SentrySession *)session - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording video:(NSURL *)videoURL + withScope:(SentryScope *)scope { replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent - withScope:[[SentryScope alloc] init] + withScope:scope alwaysAttachStacktrace:NO]; if (replayEvent == nil) { diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 7172a17a6c9..19e1eda942c 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -310,6 +310,12 @@ - (SentryId *)captureEvent:(SentryEvent *)event return SentryId.empty; } +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL { + [_client captureReplayEvent:replayEvent replayRecording:replayRecording video:videoURL withScope:self.scope]; +} + - (id)startTransactionWithName:(NSString *)name operation:(NSString *)operation { return [self startTransactionWithContext:[[SentryTransactionContext alloc] diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index 2d67ea6c494..5c707dddfea 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -2,13 +2,15 @@ #import "SentryDateUtil.h" #import "SentryId.h" +NS_ASSUME_NONNULL_BEGIN + @implementation SentryReplayEvent - (NSDictionary *)serialize { NSMutableDictionary *result = [[super serialize] mutableCopy]; - NSMutableArray *trace_ids = [NSMutableArray array]; + NSMutableArray *trace_ids = [[NSMutableArray alloc] initWithCapacity:self.traceIds.count]; for (SentryId *traceId in self.traceIds) { [trace_ids addObject:traceId.sentryIdString]; @@ -26,3 +28,5 @@ - (NSDictionary *)serialize } @end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index bd1fbd85949..5bd2d6f3387 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -44,7 +44,8 @@ SentryClient () - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording - video:(NSURL *)videoURL; + video:(NSURL *)videoURL + withScope:(SentryScope *)scope; - (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:)); diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index f37cd70115b..4ec899c44b5 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -10,6 +10,8 @@ @class SentrySession; @class SentryTracer; @class SentryTracerConfiguration; +@class SentryReplayEvent; +@class SentryReplayRecording; NS_ASSUME_NONNULL_BEGIN @@ -33,6 +35,10 @@ SentryHub () - (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope; +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + - (void)closeCachedSessionWithTimestamp:(NSDate *_Nullable)timestamp; - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transactionContext From 665bec442e8fb63f30b49954b55430c02b36bb01 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 15:32:19 +0100 Subject: [PATCH 06/88] MsgPack Tests --- Sentry.xcodeproj/project.pbxproj | 4 + Sources/Sentry/SentryMsgPackSerializer.m | 14 +-- .../Sentry/include/SentryMsgPackSerializer.h | 2 +- Tests/SentryTests/SentryClientTests.swift | 2 +- .../SentryMsgPackSerializerTests.m | 88 +++++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 6 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 Tests/SentryTests/SentryMsgPackSerializerTests.m diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index becb2fffe74..7e40a2ff221 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -848,6 +848,7 @@ D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; + D8F8F5572B835BC600AC5465 /* SentryMsgPackSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */; }; D8FFE50C2703DBB400607131 /* SwizzlingCallTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FFE50B2703DAAE00607131 /* SwizzlingCallTests.swift */; }; /* End PBXBuildFile section */ @@ -1854,6 +1855,7 @@ D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryPredicateDescriptor.h; path = include/SentryPredicateDescriptor.h; sourceTree = ""; }; D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPredicateDescriptorTests.swift; sourceTree = ""; }; + D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializerTests.m; sourceTree = ""; }; D8FFE50B2703DAAE00607131 /* SwizzlingCallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwizzlingCallTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -3459,6 +3461,7 @@ D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */, D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */, D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, + D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, ); name = Tools; sourceTree = ""; @@ -4450,6 +4453,7 @@ 7B3B473E25D6CEA500D01640 /* SentryNSErrorTests.swift in Sources */, 632331F62404FFA8008D91D6 /* SentryScopeTests.m in Sources */, D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */, + D8F8F5572B835BC600AC5465 /* SentryMsgPackSerializerTests.m in Sources */, 0A283E79291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift in Sources */, 63FE720D20DA66EC00CDBAE8 /* NSError+SimpleConstructor_Tests.m in Sources */, 69BEE6F72620729E006DF9DF /* UrlSessionDelegateSpy.swift in Sources */, diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index 9ce13ced6a6..47e25ab88bb 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -4,25 +4,23 @@ @implementation SentryMsgPackSerializer + (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path { NSOutputStream * outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; + [outputStream open]; - uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); + uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; - // Iterate over the map and serialize each key-value pair [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - // Pack the key as a string NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres uint8_t keyLength = (uint8_t)keyData.length; - uint8_t str8Header = (uint8_t)0xD9; [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; [outputStream write:keyData.bytes maxLength:keyData.length]; - // Pack the value as a binary string uint32_t valueLength = (uint32_t)[value streamSize]; - //We will always use the 4 bytes data length for simplicity. - //Worst case we're losing 3 bytes. + // We will always use the 4 bytes data length for simplicity. + // Worst case we're losing 3 bytes. uint8_t bin32Header = (uint8_t)0xC6; [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; valueLength = NSSwapHostIntToBig(valueLength); @@ -45,6 +43,8 @@ + (void)serializeDictionaryToMessagePack:(NSDictionary +@interface NSData (inputStreameble) @end @interface NSURL (inputStreameble) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 3908b95e494..4d6b6fa8e1c 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1586,7 +1586,7 @@ class SentryClientTest: XCTestCase { //Not a video url, but its ok for test the envelope let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") - sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!) + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) let envelope = fixture.transport.sentEnvelopes.first diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m new file mode 100644 index 00000000000..9ed1e457ce8 --- /dev/null +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -0,0 +1,88 @@ +#import +#import "SentryMsgPackSerializer.h" +#import + +@interface SentryMsgPackSerializerTests : XCTestCase + +@end + +@implementation SentryMsgPackSerializerTests + +- (void) testSerializeNSData { + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + + NSDictionary> *dictionary = @{ + @"key1": [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], + @"key2": [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] + }; + + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + + NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + [self assertMsgPack:tempFile]; + + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; +} + +- (void) testSerializeURL { + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"file1.dat"]; + NSURL *file2URL = [tempDirectoryURL URLByAppendingPathComponent:@"file2.dat"]; + + [@"File 1" writeToURL:file1URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; + [@"File 2" writeToURL:file2URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; + + NSDictionary> *dictionary = @{ + @"key1": file1URL, + @"key2": file2URL + }; + + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + + NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + + [self assertMsgPack:tempFile]; + + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; + [[NSFileManager defaultManager] removeItemAtURL:file1URL error:nil]; + [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; +} + + +- (void)assertMsgPack:(NSData *)data { + NSInputStream* stream = [NSInputStream inputStreamWithData:data]; + [stream open]; + + uint8_t buffer[1024]; + [stream read:buffer maxLength:1]; + + XCTAssertEqual(buffer[0] & 0x80, 0x80); //Assert data is a dictionary + + uint8_t dicSize = buffer[0] & 0x0F; //Gets dictionary length + + for (int i = 0; i < dicSize; i++) { //for each item in the dictionary + [stream read:buffer maxLength:1]; + XCTAssertEqual(buffer[0], (uint8_t)0xD9); //Asserts key is a string of up to 255 characteres + [stream read:buffer maxLength:1]; + uint8_t stringLen = buffer[0]; //Gets string length + NSInteger read = [stream read:buffer maxLength:stringLen]; //read the key from the buffer + buffer[read] = 0; //append a null terminator to the string + NSString * key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; + XCTAssertEqual(key.length, stringLen); + + [stream read:buffer maxLength:1]; + XCTAssertEqual(buffer[0], (uint8_t)0xC6); + [stream read:buffer maxLength:sizeof(uint32_t)]; + uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t*)buffer); + [stream read:buffer maxLength:dataLen]; + } + + //We should be at the end of the data by now and nothing left to read + NSInteger IsEndOfFile = [stream read:buffer maxLength:1]; + XCTAssertEqual(IsEndOfFile, 0); +} + + +@end diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 61e518cdd08..bca21b965ba 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -224,3 +224,4 @@ #import "SentryTimeToDisplayTracker.h" #import "SentryTracerConfiguration.h" #import "TestSentryViewHierarchy.h" +#import "SentryMsgPackSerializer.h" From f42c9e784f33f476d026f73848d44af1a6ad4832 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 19 Feb 2024 14:35:41 +0000 Subject: [PATCH 07/88] Format code --- Sources/Sentry/SentryHub.m | 8 +- Sources/Sentry/SentryMsgPackSerializer.m | 96 ++++++++++--------- Sources/Sentry/SentryReplayRecording.m | 4 +- .../Sentry/include/SentryMsgPackSerializer.h | 17 ++-- .../SentryMsgPackSerializerTests.m | 82 ++++++++-------- .../SentryTests/SentryTests-Bridging-Header.h | 2 +- 6 files changed, 112 insertions(+), 97 deletions(-) diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 19e1eda942c..d1762f1f5be 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -312,8 +312,12 @@ - (SentryId *)captureEvent:(SentryEvent *)event - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording - video:(NSURL *)videoURL { - [_client captureReplayEvent:replayEvent replayRecording:replayRecording video:videoURL withScope:self.scope]; + video:(NSURL *)videoURL +{ + [_client captureReplayEvent:replayEvent + replayRecording:replayRecording + video:videoURL + withScope:self.scope]; } - (id)startTransactionWithName:(NSString *)name operation:(NSString *)operation diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index 47e25ab88bb..a5d31e3dcad 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -2,60 +2,67 @@ @implementation SentryMsgPackSerializer -+ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path { - NSOutputStream * outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; ++ (void)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path +{ + NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; [outputStream open]; - + uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; - - [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; - uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres - uint8_t keyLength = (uint8_t)keyData.length; - [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; - [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; - - [outputStream write:keyData.bytes maxLength:keyData.length]; - - uint32_t valueLength = (uint32_t)[value streamSize]; - // We will always use the 4 bytes data length for simplicity. - // Worst case we're losing 3 bytes. - uint8_t bin32Header = (uint8_t)0xC6; - [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; - valueLength = NSSwapHostIntToBig(valueLength); - [outputStream write:(uint8_t*)&valueLength maxLength:sizeof(uint32_t)]; - - NSInputStream * inputStream = [value asInputStream]; - [inputStream open]; - - uint8_t buffer[1024]; - NSInteger bytesRead; - - while ([inputStream hasBytesAvailable]) { - bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; - if (bytesRead > 0) { - [outputStream write:buffer maxLength:bytesRead]; - } else if (bytesRead < 0) { - break; + + [dictionary + enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres + uint8_t keyLength = (uint8_t)keyData.length; + [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; + [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; + + [outputStream write:keyData.bytes maxLength:keyData.length]; + + uint32_t valueLength = (uint32_t)[value streamSize]; + // We will always use the 4 bytes data length for simplicity. + // Worst case we're losing 3 bytes. + uint8_t bin32Header = (uint8_t)0xC6; + [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; + valueLength = NSSwapHostIntToBig(valueLength); + [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; + + NSInputStream *inputStream = [value asInputStream]; + [inputStream open]; + + uint8_t buffer[1024]; + NSInteger bytesRead; + + while ([inputStream hasBytesAvailable]) { + bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (bytesRead > 0) { + [outputStream write:buffer maxLength:bytesRead]; + } else if (bytesRead < 0) { + break; + } } - } - [inputStream close]; - }]; - + [inputStream close]; + }]; + [outputStream close]; } @end -@implementation NSURL (SentryStreameble) +@implementation +NSURL (SentryStreameble) -- (NSInputStream *)asInputStream { +- (NSInputStream *)asInputStream +{ return [[NSInputStream alloc] initWithURL:self]; } -- (NSInteger)streamSize { +- (NSInteger)streamSize +{ NSFileManager *fileManager = [NSFileManager defaultManager]; NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:nil]; NSNumber *fileSize = attributes[NSFileSize]; @@ -64,13 +71,16 @@ - (NSInteger)streamSize { @end -@implementation NSData (SentryStreameble) +@implementation +NSData (SentryStreameble) -- (NSInputStream *)asInputStream { +- (NSInputStream *)asInputStream +{ return [[NSInputStream alloc] initWithData:self]; } -- (NSInteger)streamSize { +- (NSInteger)streamSize +{ return self.length; } diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index 21249f9d2b0..c086e9dda6c 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -32,8 +32,8 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; - //This format is defined by RRWeb - //empty values are required by the format + // This format is defined by RRWeb + // empty values are required by the format NSDictionary *metaInfo = @{ @"type" : @4, @"timestamp" : @(timestamp), diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h index 2ede4da9a11..34a20f7e1ee 100644 --- a/Sources/Sentry/include/SentryMsgPackSerializer.h +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -2,12 +2,11 @@ NS_ASSUME_NONNULL_BEGIN - @protocol SentryStreameble -- (NSInputStream *) asInputStream; +- (NSInputStream *)asInputStream; -- (NSInteger) streamSize; +- (NSInteger)streamSize; @end @@ -17,16 +16,18 @@ NS_ASSUME_NONNULL_BEGIN */ @interface SentryMsgPackSerializer : NSObject -+ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path; ++ (void)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path; @end - -@interface NSData (inputStreameble) +@interface +NSData (inputStreameble) @end -@interface NSURL (inputStreameble) +@interface +NSURL (inputStreameble) @end - NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m index 9ed1e457ce8..e7ab2fe7bab 100644 --- a/Tests/SentryTests/SentryMsgPackSerializerTests.m +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -1,5 +1,5 @@ -#import #import "SentryMsgPackSerializer.h" +#import #import @interface SentryMsgPackSerializerTests : XCTestCase @@ -8,81 +8,81 @@ @interface SentryMsgPackSerializerTests : XCTestCase @implementation SentryMsgPackSerializerTests -- (void) testSerializeNSData { +- (void)testSerializeNSData +{ NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; - + NSDictionary> *dictionary = @{ - @"key1": [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], - @"key2": [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] + @"key1" : [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], + @"key2" : [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] }; - + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - - NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; [self assertMsgPack:tempFile]; - + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; } -- (void) testSerializeURL { +- (void)testSerializeURL +{ NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"file1.dat"]; NSURL *file2URL = [tempDirectoryURL URLByAppendingPathComponent:@"file2.dat"]; - + [@"File 1" writeToURL:file1URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; [@"File 2" writeToURL:file2URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; - - NSDictionary> *dictionary = @{ - @"key1": file1URL, - @"key2": file2URL - }; - + + NSDictionary> *dictionary = + @{ @"key1" : file1URL, @"key2" : file2URL }; + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - - NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; - + + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + [self assertMsgPack:tempFile]; - + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; [[NSFileManager defaultManager] removeItemAtURL:file1URL error:nil]; [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; } - -- (void)assertMsgPack:(NSData *)data { - NSInputStream* stream = [NSInputStream inputStreamWithData:data]; +- (void)assertMsgPack:(NSData *)data +{ + NSInputStream *stream = [NSInputStream inputStreamWithData:data]; [stream open]; - + uint8_t buffer[1024]; [stream read:buffer maxLength:1]; - - XCTAssertEqual(buffer[0] & 0x80, 0x80); //Assert data is a dictionary - - uint8_t dicSize = buffer[0] & 0x0F; //Gets dictionary length - - for (int i = 0; i < dicSize; i++) { //for each item in the dictionary + + XCTAssertEqual(buffer[0] & 0x80, 0x80); // Assert data is a dictionary + + uint8_t dicSize = buffer[0] & 0x0F; // Gets dictionary length + + for (int i = 0; i < dicSize; i++) { // for each item in the dictionary [stream read:buffer maxLength:1]; - XCTAssertEqual(buffer[0], (uint8_t)0xD9); //Asserts key is a string of up to 255 characteres + XCTAssertEqual(buffer[0], (uint8_t)0xD9); // Asserts key is a string of up to 255 + // characteres [stream read:buffer maxLength:1]; - uint8_t stringLen = buffer[0]; //Gets string length - NSInteger read = [stream read:buffer maxLength:stringLen]; //read the key from the buffer - buffer[read] = 0; //append a null terminator to the string - NSString * key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; + uint8_t stringLen = buffer[0]; // Gets string length + NSInteger read = [stream read:buffer maxLength:stringLen]; // read the key from the buffer + buffer[read] = 0; // append a null terminator to the string + NSString *key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; XCTAssertEqual(key.length, stringLen); - + [stream read:buffer maxLength:1]; XCTAssertEqual(buffer[0], (uint8_t)0xC6); [stream read:buffer maxLength:sizeof(uint32_t)]; - uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t*)buffer); + uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t *)buffer); [stream read:buffer maxLength:dataLen]; } - - //We should be at the end of the data by now and nothing left to read + + // We should be at the end of the data by now and nothing left to read NSInteger IsEndOfFile = [stream read:buffer maxLength:1]; XCTAssertEqual(IsEndOfFile, 0); } - @end diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index bca21b965ba..183cc625794 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -213,6 +213,7 @@ #import "SentryEnvelopeAttachmentHeader.h" #import "SentryExtraContextProvider.h" #import "SentryMeasurementValue.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryPerformanceTracker+Testing.h" #import "SentryPropagationContext.h" @@ -224,4 +225,3 @@ #import "SentryTimeToDisplayTracker.h" #import "SentryTracerConfiguration.h" #import "TestSentryViewHierarchy.h" -#import "SentryMsgPackSerializer.h" From 398e3d043185bcc960b24158eeb7162a48b6bba5 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 17:11:59 +0100 Subject: [PATCH 08/88] Replacing envelope item --- Sentry.xcodeproj/project.pbxproj | 12 --- Sources/Sentry/SentryClient.m | 32 ++----- Sources/Sentry/SentryDataCategoryMapper.m | 9 ++ Sources/Sentry/SentryEnvelope.m | 32 +++++++ Sources/Sentry/SentryHub.m | 8 +- Sources/Sentry/SentryMsgPackSerializer.m | 96 ++++++++++--------- .../Sentry/SentryReplayEnvelopeItemHeader.m | 35 ------- Sources/Sentry/SentryReplayRecording.m | 9 +- .../HybridPublic/SentryEnvelopeItemType.h | 1 - Sources/Sentry/include/SentryDataCategory.h | 4 +- .../Sentry/include/SentryDataCategoryMapper.h | 1 + .../Sentry/include/SentryEnvelope+Private.h | 6 ++ .../Sentry/include/SentryMsgPackSerializer.h | 17 ++-- .../include/SentryReplayEnvelopeItemHeader.h | 20 ---- .../Sentry/include/SentryReplayRecording.h | 2 + .../SentryReplayEnvelopeItemHeaderTests.swift | 44 --------- Tests/SentryTests/SentryClientTests.swift | 28 ++++-- .../SentryMsgPackSerializerTests.m | 82 ++++++++-------- .../SentryTests/SentryTests-Bridging-Header.h | 3 +- 19 files changed, 198 insertions(+), 243 deletions(-) delete mode 100644 Sources/Sentry/SentryReplayEnvelopeItemHeader.m delete mode 100644 Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h delete mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 7e40a2ff221..faf3461ff3b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -745,7 +745,6 @@ D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */; }; D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */; }; - D80694CA2B7CD65800B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */; }; D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */ = {isa = PBXBuildFile; fileRef = D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */; }; D80694CE2B7E0A3E00B820E6 /* SentryReplayType.m in Sources */ = {isa = PBXBuildFile; fileRef = D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */; }; D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */; }; @@ -812,8 +811,6 @@ D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */ = {isa = PBXBuildFile; fileRef = D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */; }; D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */ = {isa = PBXBuildFile; fileRef = D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */; }; - D88D6C212B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */; }; - D88D6C222B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ABB0BC29264275005D1E24 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A349B291D0C0B005A27A9 /* Sentry.swift */; }; @@ -1740,7 +1737,6 @@ D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEventTests.swift; sourceTree = ""; }; D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayRecordingTests.swift; sourceTree = ""; }; - D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEnvelopeItemHeaderTests.swift; sourceTree = ""; }; D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayType.h; path = include/SentryReplayType.h; sourceTree = ""; }; D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayType.m; sourceTree = ""; }; D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackerTests.swift; sourceTree = ""; }; @@ -1815,8 +1811,6 @@ D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayRecording.h; path = include/SentryReplayRecording.h; sourceTree = ""; }; D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayRecording.m; sourceTree = ""; }; - D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayEnvelopeItemHeader.h; path = include/SentryReplayEnvelopeItemHeader.h; sourceTree = ""; }; - D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayEnvelopeItemHeader.m; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; @@ -3394,7 +3388,6 @@ children = ( D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, - D80694C82B7CD65500B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3417,8 +3410,6 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, - D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, ); name = SessionReplay; sourceTree = ""; @@ -3773,7 +3764,6 @@ 63FE70F920DA4C1000CDBAE8 /* SentryCrashMonitor.h in Headers */, D8BD2E6829361A0F00D96C6A /* PrivatesHeader.h in Headers */, 7B98D7CB25FB64EC00C5A389 /* SentryWatchdogTerminationTrackingIntegration.h in Headers */, - D88D6C212B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h in Headers */, 63FE710920DA4C1000CDBAE8 /* SentryCrashFileUtils.h in Headers */, 03F84D1F27DD414C008FE43F /* SentryAsyncSafeLogging.h in Headers */, 7BE3C76B2445C27A00A38442 /* SentryCurrentDateProvider.h in Headers */, @@ -4239,7 +4229,6 @@ 639FCFA91EBC80CC00778193 /* SentryFrame.m in Sources */, D858FA672A29EAB3002A3503 /* SentryBinaryImageCache.m in Sources */, 8E564AEA267AF22600FE117D /* SentryNetworkTracker.m in Sources */, - D88D6C222B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m in Sources */, 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, @@ -4520,7 +4509,6 @@ 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */, 7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */, 7BBD18BB24530D2600427C76 /* SentryFileManagerTests.swift in Sources */, - D80694CA2B7CD65800B820E6 /* SentryReplayEnvelopeItemHeaderTests.swift in Sources */, 63FE722020DA66EC00CDBAE8 /* SentryCrashObjC_Tests.m in Sources */, 7B58816727FC5D790098B121 /* SentryDiscardReasonMapperTests.swift in Sources */, 63FE720320DA66EC00CDBAE8 /* SentryCrashCPU_Tests.m in Sources */, diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index fd02fff78dc..7a4d326678d 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -13,7 +13,7 @@ #import "SentryDependencyContainer.h" #import "SentryDispatchQueueWrapper.h" #import "SentryDsn.h" -#import "SentryEnvelope.h" +#import "SentryEnvelope+Private.h" #import "SentryEnvelopeItemType.h" #import "SentryEvent.h" #import "SentryException.h" @@ -30,13 +30,12 @@ #import "SentryMechanismMeta.h" #import "SentryMessage.h" #import "SentryMeta.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSError.h" #import "SentryOptions+Private.h" #import "SentryPropagationContext.h" #import "SentryRandom.h" -#import "SentryReplayEnvelopeItemHeader.h" #import "SentryReplayEvent.h" -#import "SentryReplayRecording.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySerialization.h" @@ -489,7 +488,6 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent withScope:scope alwaysAttachStacktrace:NO]; - if (replayEvent == nil) { return; } else if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { @@ -501,25 +499,13 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent // breadcrumbs for replay will be send with ReplayRecording replayEvent.breadcrumbs = nil; - SentryEnvelopeItem *eventEnvelopeItem = [[SentryEnvelopeItem alloc] initWithEvent:replayEvent]; - - NSData *recording = [SentrySerialization dataWithJSONObject:[replayRecording serialize]]; - SentryEnvelopeItem *recordingEnvelopeItem = [[SentryEnvelopeItem alloc] - initWithHeader:[SentryReplayEnvelopeItemHeader - replayRecordingHeaderWithSegmentId:replayRecording.segmentId - length:recording.length] - data:recording]; - - NSData *video = [NSData dataWithContentsOfURL:videoURL]; - SentryEnvelopeItem *videoEnvelopeItem = [[SentryEnvelopeItem alloc] - initWithHeader:[SentryReplayEnvelopeItemHeader - replayVideoHeaderWithSegmentId:replayRecording.segmentId - length:video.length] - data:video]; - - SentryEnvelope *envelope = [[SentryEnvelope alloc] - initWithHeader:[SentryEnvelopeHeader empty] - items:@[ eventEnvelopeItem, recordingEnvelopeItem, videoEnvelopeItem ]]; + SentryEnvelopeItem *videoEnvelopeItem = + [[SentryEnvelopeItem alloc] initWithReplayEvent:replayEvent + replayRecording:replayRecording + video:videoURL]; + + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] + items:@[ videoEnvelopeItem ]]; [self captureEnvelope:envelope]; } diff --git a/Sources/Sentry/SentryDataCategoryMapper.m b/Sources/Sentry/SentryDataCategoryMapper.m index ed7df3829f5..820afa56502 100644 --- a/Sources/Sentry/SentryDataCategoryMapper.m +++ b/Sources/Sentry/SentryDataCategoryMapper.m @@ -11,6 +11,7 @@ NSString *const kSentryDataCategoryNameAttachment = @"attachment"; NSString *const kSentryDataCategoryNameUserFeedback = @"user_report"; NSString *const kSentryDataCategoryNameProfile = @"profile"; +NSString *const kSentryDataCategoryNameReplay = @"replay"; NSString *const kSentryDataCategoryNameUnknown = @"unknown"; NS_ASSUME_NONNULL_BEGIN @@ -33,6 +34,9 @@ if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) { return kSentryDataCategoryProfile; } + if ([itemType isEqualToString:SentryEnvelopeItemTypeReplayVideo]) { + return kSentryDataCategoryReplay; + } return kSentryDataCategoryDefault; } @@ -73,6 +77,9 @@ if ([value isEqualToString:kSentryDataCategoryNameProfile]) { return kSentryDataCategoryProfile; } + if ([value isEqualToString:kSentryDataCategoryNameReplay]) { + return kSentryDataCategoryReplay; + } return kSentryDataCategoryUnknown; } @@ -103,6 +110,8 @@ return kSentryDataCategoryNameProfile; case kSentryDataCategoryUnknown: return kSentryDataCategoryNameUnknown; + case kSentryDataCategoryReplay: + return kSentryDataCategoryNameReplay; } } diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 068d1c8b94a..c9fd9c3358c 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -9,6 +9,9 @@ #import "SentryLog.h" #import "SentryMessage.h" #import "SentryMeta.h" +#import "SentryMsgPackSerializer.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" #import "SentrySdkInfo.h" #import "SentrySerialization.h" #import "SentrySession.h" @@ -203,6 +206,35 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return [self initWithHeader:itemHeader data:data]; } +- (instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL +{ + NSData *replayEventData = [SentrySerialization dataWithJSONObject:[replayEvent serialize]]; + NSMutableData *recording = [NSMutableData data]; + [recording appendData:[SentrySerialization + dataWithJSONObject:[replayRecording headerForReplayRecording]]]; + [recording appendData:[SentrySerialization dataWithJSONObject:[replayRecording serialize]]]; + + NSURL *envelopeContentUrl = + [[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"]; + + [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{ + @"replay_event" : replayEventData, + @"replay_recording" : recording, + @"replay_video" : videoURL + } + intoFile:envelopeContentUrl]; + + // TODO: Create and envelope item that accepts and URL as content so we dont need to load the + // content in memory just to save it back to disk. + NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl]; + return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] + initWithType:SentryEnvelopeItemTypeReplayVideo + length:envelopeItemContent.length] + data:envelopeItemContent]; +} + @end @implementation SentryEnvelope diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 19e1eda942c..d1762f1f5be 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -312,8 +312,12 @@ - (SentryId *)captureEvent:(SentryEvent *)event - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording - video:(NSURL *)videoURL { - [_client captureReplayEvent:replayEvent replayRecording:replayRecording video:videoURL withScope:self.scope]; + video:(NSURL *)videoURL +{ + [_client captureReplayEvent:replayEvent + replayRecording:replayRecording + video:videoURL + withScope:self.scope]; } - (id)startTransactionWithName:(NSString *)name operation:(NSString *)operation diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index 47e25ab88bb..a5d31e3dcad 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -2,60 +2,67 @@ @implementation SentryMsgPackSerializer -+ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path { - NSOutputStream * outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; ++ (void)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path +{ + NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; [outputStream open]; - + uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; - - [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; - uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres - uint8_t keyLength = (uint8_t)keyData.length; - [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; - [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; - - [outputStream write:keyData.bytes maxLength:keyData.length]; - - uint32_t valueLength = (uint32_t)[value streamSize]; - // We will always use the 4 bytes data length for simplicity. - // Worst case we're losing 3 bytes. - uint8_t bin32Header = (uint8_t)0xC6; - [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; - valueLength = NSSwapHostIntToBig(valueLength); - [outputStream write:(uint8_t*)&valueLength maxLength:sizeof(uint32_t)]; - - NSInputStream * inputStream = [value asInputStream]; - [inputStream open]; - - uint8_t buffer[1024]; - NSInteger bytesRead; - - while ([inputStream hasBytesAvailable]) { - bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; - if (bytesRead > 0) { - [outputStream write:buffer maxLength:bytesRead]; - } else if (bytesRead < 0) { - break; + + [dictionary + enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres + uint8_t keyLength = (uint8_t)keyData.length; + [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; + [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; + + [outputStream write:keyData.bytes maxLength:keyData.length]; + + uint32_t valueLength = (uint32_t)[value streamSize]; + // We will always use the 4 bytes data length for simplicity. + // Worst case we're losing 3 bytes. + uint8_t bin32Header = (uint8_t)0xC6; + [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; + valueLength = NSSwapHostIntToBig(valueLength); + [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; + + NSInputStream *inputStream = [value asInputStream]; + [inputStream open]; + + uint8_t buffer[1024]; + NSInteger bytesRead; + + while ([inputStream hasBytesAvailable]) { + bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (bytesRead > 0) { + [outputStream write:buffer maxLength:bytesRead]; + } else if (bytesRead < 0) { + break; + } } - } - [inputStream close]; - }]; - + [inputStream close]; + }]; + [outputStream close]; } @end -@implementation NSURL (SentryStreameble) +@implementation +NSURL (SentryStreameble) -- (NSInputStream *)asInputStream { +- (NSInputStream *)asInputStream +{ return [[NSInputStream alloc] initWithURL:self]; } -- (NSInteger)streamSize { +- (NSInteger)streamSize +{ NSFileManager *fileManager = [NSFileManager defaultManager]; NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:nil]; NSNumber *fileSize = attributes[NSFileSize]; @@ -64,13 +71,16 @@ - (NSInteger)streamSize { @end -@implementation NSData (SentryStreameble) +@implementation +NSData (SentryStreameble) -- (NSInputStream *)asInputStream { +- (NSInputStream *)asInputStream +{ return [[NSInputStream alloc] initWithData:self]; } -- (NSInteger)streamSize { +- (NSInteger)streamSize +{ return self.length; } diff --git a/Sources/Sentry/SentryReplayEnvelopeItemHeader.m b/Sources/Sentry/SentryReplayEnvelopeItemHeader.m deleted file mode 100644 index 59ae939c3e5..00000000000 --- a/Sources/Sentry/SentryReplayEnvelopeItemHeader.m +++ /dev/null @@ -1,35 +0,0 @@ -#import "SentryReplayEnvelopeItemHeader.h" -#import "SentryEnvelopeItemType.h" - -@implementation SentryReplayEnvelopeItemHeader - -- (instancetype)initWithType:(NSString *)type - segmentId:(NSInteger)segmentId - length:(NSUInteger)length -{ - if (self = [super initWithType:type length:length]) { - self.segmentId = segmentId; - } - return self; -} - -+ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length -{ - return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayRecording - segmentId:segmentId - length:length]; -} - -+ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length -{ - return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayVideo - segmentId:segmentId - length:length]; -} - -- (NSDictionary *)serialize -{ - return @{ @"type" : self.type, @"length" : @(self.length), @"segment_id" : @(self.segmentId) }; -} - -@end diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index 21249f9d2b0..059ac1bfff7 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -27,13 +27,18 @@ - (instancetype)initWithSegmentId:(NSInteger)segmentId return self; } +- (NSDictionary *)headerForReplayRecording +{ + return @{ @"segment_id" : @(self.segmentId) }; +} + - (NSArray *> *)serialize { long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; - //This format is defined by RRWeb - //empty values are required by the format + // This format is defined by RRWeb + // empty values are required by the format NSDictionary *metaInfo = @{ @"type" : @4, @"timestamp" : @(timestamp), diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h index 8619dca5ab5..b0ad1fc0b3f 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h @@ -6,4 +6,3 @@ static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment"; static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report"; static NSString *const SentryEnvelopeItemTypeProfile = @"profile"; static NSString *const SentryEnvelopeItemTypeReplayVideo = @"replay_video"; -static NSString *const SentryEnvelopeItemTypeReplayRecording = @"replay_recording"; diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index ff62ecbc21f..860aa8cc7b4 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -14,7 +14,8 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryAttachment = 5, kSentryDataCategoryUserFeedback = 6, kSentryDataCategoryProfile = 7, - kSentryDataCategoryUnknown = 8 + kSentryDataCategoryUnknown = 8, + kSentryDataCategoryReplay = 9 }; static DEPRECATED_MSG_ATTRIBUTE( @@ -30,4 +31,5 @@ static DEPRECATED_MSG_ATTRIBUTE( @"user_report", @"profile", @"unkown", + @"replay", }; diff --git a/Sources/Sentry/include/SentryDataCategoryMapper.h b/Sources/Sentry/include/SentryDataCategoryMapper.h index 41e4ece8d49..47f2121d40d 100644 --- a/Sources/Sentry/include/SentryDataCategoryMapper.h +++ b/Sources/Sentry/include/SentryDataCategoryMapper.h @@ -11,6 +11,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameTransaction; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameAttachment; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUserFeedback; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile; +FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameReplay; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown; SentryDataCategory sentryDataCategoryForNSUInteger(NSUInteger value); diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index 98682fa1a2f..5fa11247704 100644 --- a/Sources/Sentry/include/SentryEnvelope+Private.h +++ b/Sources/Sentry/include/SentryEnvelope+Private.h @@ -2,11 +2,17 @@ NS_ASSUME_NONNULL_BEGIN +@class SentryReplayEvent, SentryReplayRecording; + @interface SentryEnvelopeItem () - (instancetype)initWithClientReport:(SentryClientReport *)clientReport; +- (instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h index 2ede4da9a11..34a20f7e1ee 100644 --- a/Sources/Sentry/include/SentryMsgPackSerializer.h +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -2,12 +2,11 @@ NS_ASSUME_NONNULL_BEGIN - @protocol SentryStreameble -- (NSInputStream *) asInputStream; +- (NSInputStream *)asInputStream; -- (NSInteger) streamSize; +- (NSInteger)streamSize; @end @@ -17,16 +16,18 @@ NS_ASSUME_NONNULL_BEGIN */ @interface SentryMsgPackSerializer : NSObject -+ (void)serializeDictionaryToMessagePack:(NSDictionary> *)dictionary intoFile:(NSURL *)path; ++ (void)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path; @end - -@interface NSData (inputStreameble) +@interface +NSData (inputStreameble) @end -@interface NSURL (inputStreameble) +@interface +NSURL (inputStreameble) @end - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h b/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h deleted file mode 100644 index 77d4d782bc1..00000000000 --- a/Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h +++ /dev/null @@ -1,20 +0,0 @@ -#import "SentryEnvelopeItemHeader.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryReplayEnvelopeItemHeader : SentryEnvelopeItemHeader - -@property (nonatomic) NSInteger segmentId; - -- (instancetype)initWithType:(NSString *)type - segmentId:(NSInteger)segmentId - length:(NSUInteger)length; - -+ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length; - -+ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h index 45e379c980d..c4b402a6db8 100644 --- a/Sources/Sentry/include/SentryReplayRecording.h +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -39,6 +39,8 @@ static NSString *const SentryReplayFrameRateType = @"constant"; - (NSArray *> *)serialize; +- (NSDictionary *)headerForReplayRecording; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift deleted file mode 100644 index 1d809b72243..00000000000 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEnvelopeItemHeaderTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import Nimble -import XCTest - -class SentryReplayEnvelopeItemHeaderTests: XCTestCase { - - func testInitWithTypeSegmentIdLength() { - let header = SentryReplayEnvelopeItemHeader(type: "testType", segmentId: 1, length: 100) - - expect(header.type) == "testType" - expect(header.segmentId) == 1 - expect(header.length) == 100 - } - - func testReplayRecordingHeader() { - let header = SentryReplayEnvelopeItemHeader.replayRecordingHeader(withSegmentId: 2, length: 200) - - expect(header.type) == SentryEnvelopeItemTypeReplayRecording - expect(header.segmentId) == 2 - expect(header.length) == 200 - } - - func testReplayVideoHeader() { - let header = SentryReplayEnvelopeItemHeader.replayVideoHeader(withSegmentId: 3, length: 300) - - expect(header.type) == SentryEnvelopeItemTypeReplayVideo - expect(header.segmentId) == 3 - expect(header.length) == 300 - } - - func testSerialize() { - let header = SentryReplayEnvelopeItemHeader(type: "testType", segmentId: 4, length: 400) - let serialized = header.serialize() - - expect(serialized["type"] as? String) == "testType" - expect(serialized["length"] as? Int) == 400 - expect(serialized["segment_id"] as? Int) == 4 - } - - func testEnvelopeItemHeaderType() { - expect(SentryEnvelopeItemTypeReplayVideo) == "replay_video" - expect(SentryEnvelopeItemTypeReplayRecording) == "replay_recording" - } -} diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 4d6b6fa8e1c..48e970f1dec 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1589,17 +1589,27 @@ class SentryClientTest: XCTestCase { sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) let envelope = fixture.transport.sentEnvelopes.first - - let recordingHeader = envelope?.items[1].header as? SentryReplayEnvelopeItemHeader - let videoHeader = envelope?.items[2].header as? SentryReplayEnvelopeItemHeader - expect(envelope?.items.count) == 3 - expect(envelope?.items[2].data.count) == 120_617 - expect(recordingHeader?.segmentId) == 2 - expect(videoHeader?.segmentId) == 2 + expect(envelope?.items.count) == 1 + expect(envelope?.items[0].header.type) == SentryEnvelopeItemTypeReplayVideo + } + + func testCaptureReplayEvent_WrongEventFromEventProcessor() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return Event() + } + + let replayEvent = SentryReplayEvent() + replayEvent.segmentId = 2 + let replayRecording = SentryReplayRecording() + replayRecording.segmentId = 2 + + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) - expect(recordingHeader?.type) == SentryEnvelopeItemTypeReplayRecording - expect(videoHeader?.type) == SentryEnvelopeItemTypeReplayVideo + //Nothing should be captured because beforeSend returned a non ReplayEvent + expect(self.fixture.transport.sentEnvelopes.count) == 0 } private func givenEventWithDebugMeta() -> Event { diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m index 9ed1e457ce8..e7ab2fe7bab 100644 --- a/Tests/SentryTests/SentryMsgPackSerializerTests.m +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -1,5 +1,5 @@ -#import #import "SentryMsgPackSerializer.h" +#import #import @interface SentryMsgPackSerializerTests : XCTestCase @@ -8,81 +8,81 @@ @interface SentryMsgPackSerializerTests : XCTestCase @implementation SentryMsgPackSerializerTests -- (void) testSerializeNSData { +- (void)testSerializeNSData +{ NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; - + NSDictionary> *dictionary = @{ - @"key1": [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], - @"key2": [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] + @"key1" : [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], + @"key2" : [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] }; - + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - - NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; [self assertMsgPack:tempFile]; - + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; } -- (void) testSerializeURL { +- (void)testSerializeURL +{ NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"file1.dat"]; NSURL *file2URL = [tempDirectoryURL URLByAppendingPathComponent:@"file2.dat"]; - + [@"File 1" writeToURL:file1URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; [@"File 2" writeToURL:file2URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; - - NSDictionary> *dictionary = @{ - @"key1": file1URL, - @"key2": file2URL - }; - + + NSDictionary> *dictionary = + @{ @"key1" : file1URL, @"key2" : file2URL }; + [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - - NSData * tempFile = [NSData dataWithContentsOfURL:tempFileURL]; - + + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + [self assertMsgPack:tempFile]; - + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; [[NSFileManager defaultManager] removeItemAtURL:file1URL error:nil]; [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; } - -- (void)assertMsgPack:(NSData *)data { - NSInputStream* stream = [NSInputStream inputStreamWithData:data]; +- (void)assertMsgPack:(NSData *)data +{ + NSInputStream *stream = [NSInputStream inputStreamWithData:data]; [stream open]; - + uint8_t buffer[1024]; [stream read:buffer maxLength:1]; - - XCTAssertEqual(buffer[0] & 0x80, 0x80); //Assert data is a dictionary - - uint8_t dicSize = buffer[0] & 0x0F; //Gets dictionary length - - for (int i = 0; i < dicSize; i++) { //for each item in the dictionary + + XCTAssertEqual(buffer[0] & 0x80, 0x80); // Assert data is a dictionary + + uint8_t dicSize = buffer[0] & 0x0F; // Gets dictionary length + + for (int i = 0; i < dicSize; i++) { // for each item in the dictionary [stream read:buffer maxLength:1]; - XCTAssertEqual(buffer[0], (uint8_t)0xD9); //Asserts key is a string of up to 255 characteres + XCTAssertEqual(buffer[0], (uint8_t)0xD9); // Asserts key is a string of up to 255 + // characteres [stream read:buffer maxLength:1]; - uint8_t stringLen = buffer[0]; //Gets string length - NSInteger read = [stream read:buffer maxLength:stringLen]; //read the key from the buffer - buffer[read] = 0; //append a null terminator to the string - NSString * key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; + uint8_t stringLen = buffer[0]; // Gets string length + NSInteger read = [stream read:buffer maxLength:stringLen]; // read the key from the buffer + buffer[read] = 0; // append a null terminator to the string + NSString *key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; XCTAssertEqual(key.length, stringLen); - + [stream read:buffer maxLength:1]; XCTAssertEqual(buffer[0], (uint8_t)0xC6); [stream read:buffer maxLength:sizeof(uint32_t)]; - uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t*)buffer); + uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t *)buffer); [stream read:buffer maxLength:dataLen]; } - - //We should be at the end of the data by now and nothing left to read + + // We should be at the end of the data by now and nothing left to read NSInteger IsEndOfFile = [stream read:buffer maxLength:1]; XCTAssertEqual(IsEndOfFile, 0); } - @end diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index bca21b965ba..b7a440904d3 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -213,10 +213,10 @@ #import "SentryEnvelopeAttachmentHeader.h" #import "SentryExtraContextProvider.h" #import "SentryMeasurementValue.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryPerformanceTracker+Testing.h" #import "SentryPropagationContext.h" -#import "SentryReplayEnvelopeItemHeader.h" #import "SentryReplayEvent.h" #import "SentryReplayRecording.h" #import "SentrySampleDecision+Private.h" @@ -224,4 +224,3 @@ #import "SentryTimeToDisplayTracker.h" #import "SentryTracerConfiguration.h" #import "TestSentryViewHierarchy.h" -#import "SentryMsgPackSerializer.h" From 33ead44a280d883b71fea76b2161a5fc94a13245 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 17:26:56 +0100 Subject: [PATCH 09/88] more test --- Sources/Sentry/SentryClient.m | 5 ++--- Tests/SentryTests/SentryClientTests.swift | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 7a4d326678d..dc1f99b9038 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -488,9 +488,8 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent withScope:scope alwaysAttachStacktrace:NO]; - if (replayEvent == nil) { - return; - } else if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { + + if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The " @"replay was discarded."); return; diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 48e970f1dec..98e934749b1 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1601,9 +1601,7 @@ class SentryClientTest: XCTestCase { } let replayEvent = SentryReplayEvent() - replayEvent.segmentId = 2 let replayRecording = SentryReplayRecording() - replayRecording.segmentId = 2 let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) @@ -1612,6 +1610,22 @@ class SentryClientTest: XCTestCase { expect(self.fixture.transport.sentEnvelopes.count) == 0 } + func testCaptureReplayEvent_DontCaptureNilEvent() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return nil + } + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) + + //Nothing should be captured because beforeSend returned nil + expect(self.fixture.transport.sentEnvelopes.count) == 0 + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() From 6cd821e6216a9beb83bc88e0f7df0d52b8d2dfa2 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 19 Feb 2024 16:28:00 +0000 Subject: [PATCH 10/88] Format code --- Sources/Sentry/SentryClient.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index dc1f99b9038..c5e9b688d11 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -488,7 +488,7 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent withScope:scope alwaysAttachStacktrace:NO]; - + if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The " @"replay was discarded."); From 216c42e741f5fb8fa35e736267dff9c01ced9e50 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 17:40:26 +0100 Subject: [PATCH 11/88] Hub test --- Tests/SentryTests/SentryClientTests.swift | 3 --- Tests/SentryTests/SentryHubTests.swift | 28 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 98e934749b1..8fdaef10a18 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1587,10 +1587,7 @@ class SentryClientTest: XCTestCase { let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) - let envelope = fixture.transport.sentEnvelopes.first - - expect(envelope?.items.count) == 1 expect(envelope?.items[0].header.type) == SentryEnvelopeItemTypeReplayVideo } diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index f5bfcf2821e..ecb32c1e51c 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -737,6 +737,34 @@ class SentryHubTests: XCTestCase { assertNoEnvelopesCaptured() } + func testCaptureReplay(){ + class SentryClientMockReplay : SentryClient { + var replayEvent : SentryReplayEvent? + var replayRecording : SentryReplayRecording? + var videoUrl : URL? + var scope : Scope? + override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL, with scope: Scope) { + self.replayEvent = replayEvent + self.replayRecording = replayRecording + self.videoUrl = videoURL + self.scope = scope + } + } + let mockClient = SentryClientMockReplay(options: fixture.options) + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + let videoUrl = URL(string: "https://sentry.io")! + + sut.bindClient(mockClient) + sut.capture(replayEvent, replayRecording: replayRecording, video: videoUrl) + + expect(mockClient?.replayEvent) == replayEvent + expect(mockClient?.replayRecording) == replayRecording + expect(mockClient?.videoUrl) == videoUrl + expect(mockClient?.scope) == sut.scope + } + func testCaptureEnvelope_WithSession() { let envelope = SentryEnvelope(session: SentrySession(releaseName: "", distinctId: "")) sut.capture(envelope) From 21efb9a1f4e300b66f1492f8707f1cffbb6d2b30 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 19 Feb 2024 17:40:41 +0100 Subject: [PATCH 12/88] Update SentryHubTests.swift --- Tests/SentryTests/SentryHubTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index ecb32c1e51c..fea5d003a40 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -737,12 +737,12 @@ class SentryHubTests: XCTestCase { assertNoEnvelopesCaptured() } - func testCaptureReplay(){ - class SentryClientMockReplay : SentryClient { - var replayEvent : SentryReplayEvent? - var replayRecording : SentryReplayRecording? - var videoUrl : URL? - var scope : Scope? + func testCaptureReplay() { + class SentryClientMockReplay: SentryClient { + var replayEvent: SentryReplayEvent? + var replayRecording: SentryReplayRecording? + var videoUrl: URL? + var scope: Scope? override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL, with scope: Scope) { self.replayEvent = replayEvent self.replayRecording = replayRecording From d6d8ce0e2252c00fdd1f41b05a0bead62dae36df Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 14:17:46 +0100 Subject: [PATCH 13/88] integration --- Sources/Sentry/SentryBaseIntegration.m | 50 ++++++++++++------- Sources/Sentry/SentryOptions.m | 2 + Sources/Sentry/SentrySessionReplay.m | 42 +--------------- .../Sentry/SentrySessionReplayIntegration.m | 30 ++++++----- .../Sentry/include/SentryBaseIntegration.h | 1 + 5 files changed, 55 insertions(+), 70 deletions(-) diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 5706d68e925..8bb3107908b 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -4,6 +4,7 @@ #import #import #import +#import "SentryReplaySettings.h" NS_ASSUME_NONNULL_BEGIN @@ -32,114 +33,127 @@ - (void)logWithReason:(NSString *)reason - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options { SentryIntegrationOption integrationOptions = [self integrationOptions]; - + if (integrationOptions & kIntegrationOptionNone) { return YES; } - + if ((integrationOptions & kIntegrationOptionEnableAutoSessionTracking) && !options.enableAutoSessionTracking) { [self logWithOptionName:@"enableAutoSessionTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableWatchdogTerminationTracking) && !options.enableWatchdogTerminationTracking) { [self logWithOptionName:@"enableWatchdogTerminationTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoPerformanceTracing) && !options.enableAutoPerformanceTracing) { [self logWithOptionName:@"enableAutoPerformanceTracing"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionEnableUIViewControllerTracing) && !options.enableUIViewControllerTracing) { [self logWithOptionName:@"enableUIViewControllerTracing"]; return NO; } - + # if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachScreenshot) && !options.attachScreenshot) { [self logWithOptionName:@"attachScreenshot"]; return NO; } # endif // SENTRY_HAS_UIKIT - + if ((integrationOptions & kIntegrationOptionEnableUserInteractionTracing) && !options.enableUserInteractionTracing) { [self logWithOptionName:@"enableUserInteractionTracing"]; return NO; } #endif - + if (integrationOptions & kIntegrationOptionEnableAppHangTracking) { if (!options.enableAppHangTracking) { [self logWithOptionName:@"enableAppHangTracking"]; return NO; } - + if (options.appHangTimeoutInterval == 0) { [self logWithReason:@"because appHangTimeoutInterval is 0"]; return NO; } } - + if ((integrationOptions & kIntegrationOptionEnableNetworkTracking) && !options.enableNetworkTracking) { [self logWithOptionName:@"enableNetworkTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableFileIOTracing) && !options.enableFileIOTracing) { [self logWithOptionName:@"enableFileIOTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableNetworkBreadcrumbs) && !options.enableNetworkBreadcrumbs) { [self logWithOptionName:@"enableNetworkBreadcrumbs"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableCoreDataTracing) && !options.enableCoreDataTracing) { [self logWithOptionName:@"enableCoreDataTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableSwizzling) && !options.enableSwizzling) { [self logWithOptionName:@"enableSwizzling"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoBreadcrumbTracking) && !options.enableAutoBreadcrumbTracking) { [self logWithOptionName:@"enableAutoBreadcrumbTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionIsTracingEnabled) && !options.isTracingEnabled) { [self logWithOptionName:@"isTracingEnabled"]; return NO; } - + if ((integrationOptions & kIntegrationOptionDebuggerNotAttached) && [SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced]) { [self logWithReason:@"because the debugger is attached"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachViewHierarchy) && !options.attachViewHierarchy) { [self logWithOptionName:@"attachViewHierarchy"]; return NO; } + + if (integrationOptions & kIntegrationOptionEnableReplay) { + if (@available(iOS 16.0, *)) { + if (options.sessionReplaySettings.replaysOnErrorSampleRate == 0 + && options.sessionReplaySettings.replaysSessionSampleRate == 0 ){ + [self logWithOptionName:@"sessionReplaySettings"]; + return NO; + } + } else { + [self logWithReason:@"Session replay requires iOS 16 or above"]; + return NO; + } + } #endif if ((integrationOptions & kIntegrationOptionEnableCrashHandler) diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index ef4fa424a4a..65fc53a7a16 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -28,6 +28,7 @@ # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" # import "SentryReplaySettings+Private.h" +# import "SentrySessionReplayIntegration.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @@ -66,6 +67,7 @@ - (void)setMeasurement:(SentryMeasurementValue *)measurement NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), + NSStringFromClass([SentrySessionReplayIntegration class]), #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 8d9bd4678a8..1936890cc2d 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,13 +1,9 @@ #import "SentrySessionReplay.h" -#import "SentryVideoReplay.h" -#import "SentryImagesReplay.h" #import "SentryViewPhotographer.h" #import "SentryOndemandReplay.h" #import "SentryAttachment+Private.h" #import "SentryLog.h" -#import "SentryTouchesTracker.h" -//#define use_video 1 -#define use_ondemand 1 +#import "SentryReplaySettings+Private.h" @implementation SentrySessionReplay { UIView * _rootView; @@ -18,16 +14,9 @@ @implementation SentrySessionReplay { NSURL * _urlToCache; NSDate * _sessionStart; SentryReplaySettings * _settings; -#if use_video - SentryVideoReplay * replayMaker; -#elif use_ondemand SentryOnDemandReplay * _replayMaker; -#else - SentryImagesReplay * replayMaker; -#endif NSMutableArray* imageCollection; - SentryTouchesTracker * _touchesTracker; } - (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings { @@ -56,8 +45,6 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full { _lastScreenShot = [[NSDate alloc] init]; _videoSegmentStart = nil; _sessionStart = _lastScreenShot; - _touchesTracker = [[SentryTouchesTracker alloc] init]; - [_touchesTracker start]; NSURL * docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject URLByAppendingPathComponent:@"io.sentry"]; @@ -69,15 +56,9 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full { } _replayMaker = -#if use_video - [[SentryVideoReplay alloc] initWithOutputPath:[urlToCache URLByAppendingPathComponent:@"sr.mp4"].path frameSize:rootView.frame.size framesPerSec:1]; -#elif use_ondemand [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; _replayMaker.bitRate = _settings.replayBitRate; _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; -#else - [[SentryImagesReplay alloc] initWithOutputPath:urlToCache.path]; -#endif imageCollection = [NSMutableArray array]; NSLog(@"Recording session to %@",_urlToCache); @@ -87,19 +68,11 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full { - (void)stop { [_displayLink invalidate]; _displayLink = nil; -#ifdef use_video - [videoReplay finalizeVideoWithCompletion:^(BOOL success, NSError * _Nonnull error) { - if (!success) { - NSLog(@"%@", error); - } - }]; -#endif } - (NSArray *)processAttachments:(NSArray *)attachments forEvent:(nonnull SentryEvent *)event { -#if use_ondemand if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { return attachments; } @@ -128,15 +101,10 @@ - (void)stop { [result addObject:attachment]; return result; -#else - return attachments; -#endif } - (void)sendReplayForEvent:(SentryEvent *)event { -#if use_ondemand -#endif } - (void)newFrame:(CADisplayLink *)sender { @@ -199,13 +167,7 @@ - (void)takeScreenshot { dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(backgroundQueue, ^{ -#if use_video - [self->replayMaker addFrame:screenshot withCompletion:^(BOOL success, NSError * _Nonnull error) { - - }]; -#else - [self->_replayMaker addFrame:screenshot]; -#endif + [self->_replayMaker addFrame:screenshot]; }); } diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 2964a8dbaf0..eb4fe00cfe3 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -8,6 +8,8 @@ #import "SentrySDK+Private.h" #import "SentryReplaySettings.h" #import "SentryRandom.h" +#import "SentryOptions.h" + @implementation SentrySessionReplayIntegration { SentrySessionReplay * sessionReplay; @@ -19,20 +21,24 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - if (options.replaySettings.replaysSessionSampleRate == 0 && options.replaySettings.replaysOnErrorSampleRate == 0) { + if (@available(iOS 16.0, *)) { + if (options.sessionReplaySettings.replaysSessionSampleRate == 0 && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { + return NO; + } + + sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; + + [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.sessionReplaySettings.replaysSessionSampleRate]]; + + SentryClient *client = [SentrySDK.currentHub getClient]; + [client addAttachmentProcessor:sessionReplay]; + + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; + return YES; + } else { return NO; } - - sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.replaySettings]; - - [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.replaySettings.replaysSessionSampleRate]]; - - SentryClient *client = [SentrySDK.currentHub getClient]; - [client addAttachmentProcessor:sessionReplay]; - - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; - return YES; } -(void)stop { diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h index c620ab20205..82952f16d4b 100644 --- a/Sources/Sentry/include/SentryBaseIntegration.h +++ b/Sources/Sentry/include/SentryBaseIntegration.h @@ -23,6 +23,7 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) { kIntegrationOptionAttachViewHierarchy = 1 << 15, kIntegrationOptionEnableCrashHandler = 1 << 16, kIntegrationOptionEnableMetricKit = 1 << 17, + kIntegrationOptionEnableReplay = 1 << 18, }; @interface SentryBaseIntegration : NSObject From 522e97287b337ad4353a5874a42283f244193122 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 16:08:07 +0100 Subject: [PATCH 14/88] More tests and log messages --- Sources/Sentry/SentryClient.m | 5 + Sources/Sentry/SentryEnvelope.m | 10 +- Sources/Sentry/SentryMsgPackSerializer.m | 100 +++++++++++------- .../Sentry/include/SentryEnvelope+Private.h | 2 +- .../Sentry/include/SentryMsgPackSerializer.h | 10 +- Tests/SentryTests/SentryClientTests.swift | 16 +++ .../SentryMsgPackSerializerTests.m | 24 +++-- 7 files changed, 112 insertions(+), 55 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index c5e9b688d11..c2a91b6c48b 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -503,6 +503,11 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:replayRecording video:videoURL]; + if (videoEnvelopeItem == nil) { + SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an Envelope Item could not be created."); + return; + } + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] items:@[ videoEnvelopeItem ]]; diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index c9fd9c3358c..538ab0898c5 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -206,7 +206,7 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return [self initWithHeader:itemHeader data:data]; } -- (instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent +- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording video:(NSURL *)videoURL { @@ -219,15 +219,17 @@ - (instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent NSURL *envelopeContentUrl = [[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"]; - [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{ + BOOL success = [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{ @"replay_event" : replayEventData, @"replay_recording" : recording, @"replay_video" : videoURL } intoFile:envelopeContentUrl]; + if (success == NO) { + SENTRY_LOG_DEBUG(@"Could not create MessagePack for session replay envelope item."); + return nil; + } - // TODO: Create and envelope item that accepts and URL as content so we dont need to load the - // content in memory just to save it back to disk. NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl]; return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeReplayVideo diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index a5d31e3dcad..da31dd23739 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -1,9 +1,11 @@ #import "SentryMsgPackSerializer.h" +#import "SentryLog.h" + @implementation SentryMsgPackSerializer -+ (void)serializeDictionaryToMessagePack: - (NSDictionary> *)dictionary ++ (BOOL)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary intoFile:(NSURL *)path { NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; @@ -11,44 +13,60 @@ + (void)serializeDictionaryToMessagePack: uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; - - [dictionary - enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; - uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characteres - uint8_t keyLength = (uint8_t)keyData.length; - [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; - [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; - - [outputStream write:keyData.bytes maxLength:keyData.length]; - - uint32_t valueLength = (uint32_t)[value streamSize]; - // We will always use the 4 bytes data length for simplicity. - // Worst case we're losing 3 bytes. - uint8_t bin32Header = (uint8_t)0xC6; - [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; - valueLength = NSSwapHostIntToBig(valueLength); - [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; - - NSInputStream *inputStream = [value asInputStream]; - [inputStream open]; - - uint8_t buffer[1024]; - NSInteger bytesRead; - - while ([inputStream hasBytesAvailable]) { - bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; - if (bytesRead > 0) { - [outputStream write:buffer maxLength:bytesRead]; - } else if (bytesRead < 0) { - break; - } + + for (NSString * key in dictionary) { + id value = dictionary[key]; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characters + uint8_t keyLength = (uint8_t)keyData.length; + [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; + [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; + + [outputStream write:keyData.bytes maxLength:keyData.length]; + + NSInteger dataLength = [value streamSize]; + if (dataLength <= 0) { + // MsgPack is being used strictly for session replay. + // An item with a length of 0 will not be useful. + // If we plan to use MsgPack for something else, + // this needs to be re-evaluated. + SENTRY_LOG_DEBUG(@"Data for MessagePack dictionary has no content - Input: %@",value); + return NO; + } + + uint32_t valueLength = (uint32_t)dataLength; + // We will always use the 4 bytes data length for simplicity. + // Worst case we're losing 3 bytes. + uint8_t bin32Header = (uint8_t)0xC6; + [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; + valueLength = NSSwapHostIntToBig(valueLength); + [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; + + NSInputStream *inputStream = [value asInputStream]; + [inputStream open]; + + uint8_t buffer[1024]; + NSInteger bytesRead; + + while ([inputStream hasBytesAvailable]) { + bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (bytesRead > 0) { + [outputStream write:buffer maxLength:bytesRead]; + } else if (bytesRead < 0) { + SENTRY_LOG_DEBUG(@"Error reading bytes from input stream - Input: %@ - %li",value, bytesRead); + + [inputStream close]; + [outputStream close]; + return NO; } - - [inputStream close]; - }]; - + } + + [inputStream close]; + } [outputStream close]; + + return YES; } @end @@ -64,7 +82,11 @@ - (NSInputStream *)asInputStream - (NSInteger)streamSize { NSFileManager *fileManager = [NSFileManager defaultManager]; - NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:nil]; + NSError * error; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:&error]; + if (attributes == nil) { + SENTRY_LOG_DEBUG(@"Could not read file attributes - File: %@ - %@",self, error); + } NSNumber *fileSize = attributes[NSFileSize]; return [fileSize unsignedIntegerValue]; } diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index 5fa11247704..40120d80e43 100644 --- a/Sources/Sentry/include/SentryEnvelope+Private.h +++ b/Sources/Sentry/include/SentryEnvelope+Private.h @@ -9,7 +9,7 @@ SentryEnvelopeItem () - (instancetype)initWithClientReport:(SentryClientReport *)clientReport; -- (instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent +- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording video:(NSURL *)videoURL; diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h index 34a20f7e1ee..d6a1485e372 100644 --- a/Sources/Sentry/include/SentryMsgPackSerializer.h +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -2,7 +2,7 @@ NS_ASSUME_NONNULL_BEGIN -@protocol SentryStreameble +@protocol SentryStreamable - (NSInputStream *)asInputStream; @@ -16,18 +16,18 @@ NS_ASSUME_NONNULL_BEGIN */ @interface SentryMsgPackSerializer : NSObject -+ (void)serializeDictionaryToMessagePack: - (NSDictionary> *)dictionary ++ (BOOL)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary intoFile:(NSURL *)path; @end @interface -NSData (inputStreameble) +NSData (inputStreameble) @end @interface -NSURL (inputStreameble) +NSURL (inputStreameble) @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 8fdaef10a18..f2ce58488b9 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1623,6 +1623,22 @@ class SentryClientTest: XCTestCase { expect(self.fixture.transport.sentEnvelopes.count) == 0 } + func testCaptureReplayEvent_InvalidFile() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return nil + } + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + + let movieUrl = URL(string: "NoFile")! + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl, with: Scope()) + + //Nothing should be captured because beforeSend returned nil + expect(self.fixture.transport.sentEnvelopes.count) == 0 + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m index e7ab2fe7bab..84144b58f7e 100644 --- a/Tests/SentryTests/SentryMsgPackSerializerTests.m +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -13,13 +13,13 @@ - (void)testSerializeNSData NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; - NSDictionary> *dictionary = @{ + NSDictionary> *dictionary = @{ @"key1" : [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], @"key2" : [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] }; - [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + XCTAssertTrue(result); NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; [self assertMsgPack:tempFile]; @@ -36,11 +36,11 @@ - (void)testSerializeURL [@"File 1" writeToURL:file1URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; [@"File 2" writeToURL:file2URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; - NSDictionary> *dictionary = + NSDictionary> *dictionary = @{ @"key1" : file1URL, @"key2" : file2URL }; - [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; - + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + XCTAssertTrue(result); NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; [self assertMsgPack:tempFile]; @@ -50,6 +50,18 @@ - (void)testSerializeURL [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; } +- (void)testSerializeInvalidFile { + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"notAFile.dat"]; + + NSDictionary> *dictionary = + @{ @"key1" : file1URL }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + XCTAssertFalse(result); +} + - (void)assertMsgPack:(NSData *)data { NSInputStream *stream = [NSInputStream inputStreamWithData:data]; From 522a5da5781b8cce2753a5aa565e39deca150451 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 20 Feb 2024 15:10:18 +0000 Subject: [PATCH 15/88] Format code --- Sources/Sentry/SentryClient.m | 5 +-- Sources/Sentry/SentryEnvelope.m | 6 ++-- Sources/Sentry/SentryMsgPackSerializer.m | 34 +++++++++---------- .../Sentry/include/SentryEnvelope+Private.h | 4 +-- .../SentryMsgPackSerializerTests.m | 17 ++++++---- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index c2a91b6c48b..df5b74311a2 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -504,10 +504,11 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent video:videoURL]; if (videoEnvelopeItem == nil) { - SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an Envelope Item could not be created."); + SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an " + @"Envelope Item could not be created."); return; } - + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] items:@[ videoEnvelopeItem ]]; diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 538ab0898c5..e6752312dd6 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -207,8 +207,8 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment } - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent - replayRecording:(SentryReplayRecording *)replayRecording - video:(NSURL *)videoURL + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL { NSData *replayEventData = [SentrySerialization dataWithJSONObject:[replayEvent serialize]]; NSMutableData *recording = [NSMutableData data]; @@ -224,7 +224,7 @@ - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent @"replay_recording" : recording, @"replay_video" : videoURL } - intoFile:envelopeContentUrl]; + intoFile:envelopeContentUrl]; if (success == NO) { SENTRY_LOG_DEBUG(@"Could not create MessagePack for session replay envelope item."); return nil; diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index da31dd23739..ae83a003237 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -1,7 +1,6 @@ #import "SentryMsgPackSerializer.h" #import "SentryLog.h" - @implementation SentryMsgPackSerializer + (BOOL)serializeDictionaryToMessagePack: @@ -13,28 +12,28 @@ + (BOOL)serializeDictionaryToMessagePack: uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; - - for (NSString * key in dictionary) { + + for (NSString *key in dictionary) { id value = dictionary[key]; - + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characters uint8_t keyLength = (uint8_t)keyData.length; [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; - + [outputStream write:keyData.bytes maxLength:keyData.length]; - + NSInteger dataLength = [value streamSize]; if (dataLength <= 0) { // MsgPack is being used strictly for session replay. // An item with a length of 0 will not be useful. // If we plan to use MsgPack for something else, // this needs to be re-evaluated. - SENTRY_LOG_DEBUG(@"Data for MessagePack dictionary has no content - Input: %@",value); + SENTRY_LOG_DEBUG(@"Data for MessagePack dictionary has no content - Input: %@", value); return NO; } - + uint32_t valueLength = (uint32_t)dataLength; // We will always use the 4 bytes data length for simplicity. // Worst case we're losing 3 bytes. @@ -42,30 +41,31 @@ + (BOOL)serializeDictionaryToMessagePack: [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; valueLength = NSSwapHostIntToBig(valueLength); [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; - + NSInputStream *inputStream = [value asInputStream]; [inputStream open]; - + uint8_t buffer[1024]; NSInteger bytesRead; - + while ([inputStream hasBytesAvailable]) { bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; if (bytesRead > 0) { [outputStream write:buffer maxLength:bytesRead]; } else if (bytesRead < 0) { - SENTRY_LOG_DEBUG(@"Error reading bytes from input stream - Input: %@ - %li",value, bytesRead); - + SENTRY_LOG_DEBUG( + @"Error reading bytes from input stream - Input: %@ - %li", value, bytesRead); + [inputStream close]; [outputStream close]; return NO; } } - + [inputStream close]; } [outputStream close]; - + return YES; } @@ -82,10 +82,10 @@ - (NSInputStream *)asInputStream - (NSInteger)streamSize { NSFileManager *fileManager = [NSFileManager defaultManager]; - NSError * error; + NSError *error; NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:&error]; if (attributes == nil) { - SENTRY_LOG_DEBUG(@"Could not read file attributes - File: %@ - %@",self, error); + SENTRY_LOG_DEBUG(@"Could not read file attributes - File: %@ - %@", self, error); } NSNumber *fileSize = attributes[NSFileSize]; return [fileSize unsignedIntegerValue]; diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index 40120d80e43..a31018b5e29 100644 --- a/Sources/Sentry/include/SentryEnvelope+Private.h +++ b/Sources/Sentry/include/SentryEnvelope+Private.h @@ -10,8 +10,8 @@ SentryEnvelopeItem () - (instancetype)initWithClientReport:(SentryClientReport *)clientReport; - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent - replayRecording:(SentryReplayRecording *)replayRecording - video:(NSURL *)videoURL; + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; @end diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m index 84144b58f7e..6606a1e121f 100644 --- a/Tests/SentryTests/SentryMsgPackSerializerTests.m +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -18,7 +18,8 @@ - (void)testSerializeNSData @"key2" : [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] }; - BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; XCTAssertTrue(result); NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; [self assertMsgPack:tempFile]; @@ -39,7 +40,8 @@ - (void)testSerializeURL NSDictionary> *dictionary = @{ @"key1" : file1URL, @"key2" : file2URL }; - BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; XCTAssertTrue(result); NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; @@ -50,15 +52,16 @@ - (void)testSerializeURL [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; } -- (void)testSerializeInvalidFile { +- (void)testSerializeInvalidFile +{ NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"notAFile.dat"]; - - NSDictionary> *dictionary = - @{ @"key1" : file1URL }; - BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary intoFile:tempFileURL]; + NSDictionary> *dictionary = @{ @"key1" : file1URL }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; XCTAssertFalse(result); } From 4463feda0eb1de886ec07e99d6848fac7944fc48 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 16:25:54 +0100 Subject: [PATCH 16/88] CategoryMapper tests --- Sources/Sentry/include/SentryDataCategory.h | 6 +++--- .../Networking/SentryDataCategoryMapperTests.swift | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index 860aa8cc7b4..9b3a5c1a8cc 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -14,8 +14,8 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryAttachment = 5, kSentryDataCategoryUserFeedback = 6, kSentryDataCategoryProfile = 7, - kSentryDataCategoryUnknown = 8, - kSentryDataCategoryReplay = 9 + kSentryDataCategoryReplay = 8, + kSentryDataCategoryUnknown = 9 }; static DEPRECATED_MSG_ATTRIBUTE( @@ -30,6 +30,6 @@ static DEPRECATED_MSG_ATTRIBUTE( @"attachment", @"user_report", @"profile", - @"unkown", @"replay", + @"unkown", }; diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index 40df5a07408..8f31a4595c9 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -9,6 +9,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(.attachment, sentryDataCategoryForEnvelopItemType("attachment")) XCTAssertEqual(.profile, sentryDataCategoryForEnvelopItemType("profile")) XCTAssertEqual(.default, sentryDataCategoryForEnvelopItemType("unknown item type")) + XCTAssertEqual(.replay, sentryDataCategoryForEnvelopItemType("replay_video")) } func testMapIntegerToCategory() { @@ -20,9 +21,9 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(.attachment, sentryDataCategoryForNSUInteger(5)) XCTAssertEqual(.userFeedback, sentryDataCategoryForNSUInteger(6)) XCTAssertEqual(.profile, sentryDataCategoryForNSUInteger(7)) - XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(8)) - - XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(9), "Failed to map unknown category number to case .unknown") + XCTAssertEqual(.replay, sentryDataCategoryForNSUInteger(8)) + XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(9)) + XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(10), "Failed to map unknown category number to case .unknown") } func testMapStringToCategory() { From b14f253bf5d0e75f80139777e72a7684e8502047 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 16:33:00 +0100 Subject: [PATCH 17/88] Update SentryMsgPackSerializer.m --- Sources/Sentry/SentryMsgPackSerializer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index ae83a003237..fce90c08555 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -54,7 +54,7 @@ + (BOOL)serializeDictionaryToMessagePack: [outputStream write:buffer maxLength:bytesRead]; } else if (bytesRead < 0) { SENTRY_LOG_DEBUG( - @"Error reading bytes from input stream - Input: %@ - %li", value, bytesRead); + @"Error reading bytes from input stream - Input: %@ - %li", value, (long)bytesRead); [inputStream close]; [outputStream close]; From 16b192d2a71a112cffe07f5bf994ae72c39b4108 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 20 Feb 2024 15:33:58 +0000 Subject: [PATCH 18/88] Format code --- Sources/Sentry/SentryMsgPackSerializer.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m index fce90c08555..657aacac599 100644 --- a/Sources/Sentry/SentryMsgPackSerializer.m +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -53,8 +53,8 @@ + (BOOL)serializeDictionaryToMessagePack: if (bytesRead > 0) { [outputStream write:buffer maxLength:bytesRead]; } else if (bytesRead < 0) { - SENTRY_LOG_DEBUG( - @"Error reading bytes from input stream - Input: %@ - %li", value, (long)bytesRead); + SENTRY_LOG_DEBUG(@"Error reading bytes from input stream - Input: %@ - %li", value, + (long)bytesRead); [inputStream close]; [outputStream close]; From 3d371609e2b0eef690c15d6642fd065effbca706 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 20 Feb 2024 16:14:26 +0000 Subject: [PATCH 19/88] Format code --- Sources/Sentry/Public/SentryOptions.h | 7 +- Sources/Sentry/Public/SentryReplaySettings.h | 11 +- Sources/Sentry/SentryBaseIntegration.m | 42 ++-- Sources/Sentry/SentryOnDemandReplay.m | 200 ++++++++++-------- Sources/Sentry/SentryOptions.m | 12 +- Sources/Sentry/SentryReplaySettings.m | 18 +- Sources/Sentry/SentrySessionReplay.m | 188 ++++++++-------- .../Sentry/SentrySessionReplayIntegration.m | 50 +++-- Sources/Sentry/SentryViewPhotographer.m | 116 +++++----- .../SentryReplaySettings+Private.h | 5 +- Sources/Sentry/include/SentryOnDemandReplay.h | 8 +- Sources/Sentry/include/SentrySessionReplay.h | 6 +- .../include/SentrySessionReplayIntegration.h | 2 +- .../Sentry/include/SentryViewPhotographer.h | 8 +- 14 files changed, 371 insertions(+), 302 deletions(-) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 7e95d580ea5..70f59c1b107 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -3,7 +3,8 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, SentryReplaySettings; +@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, + SentryReplaySettings; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -269,13 +270,13 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing; - /** * @warning This is an experimental feature and may still have bugs. * Settings to configure the session replay. * @node Default value is @c nil . */ -@property (nonatomic, strong) SentryReplaySettings * sessionReplaySettings API_AVAILABLE(ios(16.0), tvos(16.0)); +@property (nonatomic, strong) + SentryReplaySettings *sessionReplaySettings API_AVAILABLE(ios(16.0), tvos(16.0)); #endif // SENTRY_UIKIT_AVAILABLE diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h index 1c66a0f2712..16455041d7e 100644 --- a/Sources/Sentry/Public/SentryReplaySettings.h +++ b/Sources/Sentry/Public/SentryReplaySettings.h @@ -24,11 +24,14 @@ NS_ASSUME_NONNULL_BEGIN /** * Inittialize the settings of session replay - * - * @param sessionSampleRate Indicates the percentage in which the replay for the session will be created. - * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with error events. + * + * @param sessionSampleRate Indicates the percentage in which the replay for the session will be + * created. + * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with + * error events. */ -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate; +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate + replaysOnErrorSampleRate:(CGFloat)errorSampleRate; @end diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 8bb3107908b..cdd47fb0e0f 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,10 +1,10 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" +#import "SentryReplaySettings.h" #import #import #import -#import "SentryReplaySettings.h" NS_ASSUME_NONNULL_BEGIN @@ -33,119 +33,119 @@ - (void)logWithReason:(NSString *)reason - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options { SentryIntegrationOption integrationOptions = [self integrationOptions]; - + if (integrationOptions & kIntegrationOptionNone) { return YES; } - + if ((integrationOptions & kIntegrationOptionEnableAutoSessionTracking) && !options.enableAutoSessionTracking) { [self logWithOptionName:@"enableAutoSessionTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableWatchdogTerminationTracking) && !options.enableWatchdogTerminationTracking) { [self logWithOptionName:@"enableWatchdogTerminationTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoPerformanceTracing) && !options.enableAutoPerformanceTracing) { [self logWithOptionName:@"enableAutoPerformanceTracing"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionEnableUIViewControllerTracing) && !options.enableUIViewControllerTracing) { [self logWithOptionName:@"enableUIViewControllerTracing"]; return NO; } - + # if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachScreenshot) && !options.attachScreenshot) { [self logWithOptionName:@"attachScreenshot"]; return NO; } # endif // SENTRY_HAS_UIKIT - + if ((integrationOptions & kIntegrationOptionEnableUserInteractionTracing) && !options.enableUserInteractionTracing) { [self logWithOptionName:@"enableUserInteractionTracing"]; return NO; } #endif - + if (integrationOptions & kIntegrationOptionEnableAppHangTracking) { if (!options.enableAppHangTracking) { [self logWithOptionName:@"enableAppHangTracking"]; return NO; } - + if (options.appHangTimeoutInterval == 0) { [self logWithReason:@"because appHangTimeoutInterval is 0"]; return NO; } } - + if ((integrationOptions & kIntegrationOptionEnableNetworkTracking) && !options.enableNetworkTracking) { [self logWithOptionName:@"enableNetworkTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableFileIOTracing) && !options.enableFileIOTracing) { [self logWithOptionName:@"enableFileIOTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableNetworkBreadcrumbs) && !options.enableNetworkBreadcrumbs) { [self logWithOptionName:@"enableNetworkBreadcrumbs"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableCoreDataTracing) && !options.enableCoreDataTracing) { [self logWithOptionName:@"enableCoreDataTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableSwizzling) && !options.enableSwizzling) { [self logWithOptionName:@"enableSwizzling"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoBreadcrumbTracking) && !options.enableAutoBreadcrumbTracking) { [self logWithOptionName:@"enableAutoBreadcrumbTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionIsTracingEnabled) && !options.isTracingEnabled) { [self logWithOptionName:@"isTracingEnabled"]; return NO; } - + if ((integrationOptions & kIntegrationOptionDebuggerNotAttached) && [SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced]) { [self logWithReason:@"because the debugger is attached"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachViewHierarchy) && !options.attachViewHierarchy) { [self logWithOptionName:@"attachViewHierarchy"]; return NO; } - + if (integrationOptions & kIntegrationOptionEnableReplay) { if (@available(iOS 16.0, *)) { if (options.sessionReplaySettings.replaysOnErrorSampleRate == 0 - && options.sessionReplaySettings.replaysSessionSampleRate == 0 ){ + && options.sessionReplaySettings.replaysSessionSampleRate == 0) { [self logWithOptionName:@"sessionReplaySettings"]; return NO; } diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index 36c55b93154..e6302a5b4dc 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -1,20 +1,21 @@ #import "SentryOnDemandReplay.h" #if SENTRY_HAS_UIKIT -#import -#import -#import "SentryLog.h" +# import "SentryLog.h" +# import +# import @interface SentryReplayFrame : NSObject @property (nonatomic, strong) NSString *imagePath; @property (nonatomic, strong) NSDate *time; --(instancetype) initWithPath:(NSString *)path time:(NSDate*)time; +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time; @end @implementation SentryReplayFrame --(instancetype) initWithPath:(NSString *)path time:(NSDate*)time { +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time +{ if (self = [super init]) { self.imagePath = path; self.time = time; @@ -24,16 +25,16 @@ -(instancetype) initWithPath:(NSString *)path time:(NSDate*)time { @end -@implementation SentryOnDemandReplay -{ - NSString * _outputPath; - NSDate * _startTime; - NSMutableArray * _frames; +@implementation SentryOnDemandReplay { + NSString *_outputPath; + NSDate *_startTime; + NSMutableArray *_frames; CGSize _videoSize; dispatch_queue_t _onDemandDispatchQueue; } -- (instancetype)initWithOutputPath:(NSString *)outputPath { +- (instancetype)initWithOutputPath:(NSString *)outputPath +{ if (self = [super init]) { _outputPath = outputPath; _startTime = [[NSDate alloc] init]; @@ -43,30 +44,32 @@ - (instancetype)initWithOutputPath:(NSString *)outputPath { _bitRate = 20000; _cacheMaxSize = NSUIntegerMax; _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); - } return self; } -- (void)addFrame:(UIImage *)image { +- (void)addFrame:(UIImage *)image +{ dispatch_async(_onDemandDispatchQueue, ^{ - NSData * data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); - NSDate* date = [[NSDate alloc] init]; + NSData *data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); + NSDate *date = [[NSDate alloc] init]; NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; - NSString *imagePath = [self->_outputPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; - + NSString *imagePath = [self->_outputPath + stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; + [data writeToFile:imagePath atomically:YES]; - + SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; [self->_frames addObject:frame]; - + while (self->_frames.count > self->_cacheMaxSize) { [self removeOldestFrame]; } }); } -- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth { +- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth +{ CGSize originalSize = originalImage.size; CGFloat aspectRatio = originalSize.width / originalSize.height; @@ -83,124 +86,149 @@ - (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth return resizedImage; } -- (void)releaseFramesUntil:(NSDate *)date { +- (void)releaseFramesUntil:(NSDate *)date +{ dispatch_async(_onDemandDispatchQueue, ^{ - while (self->_frames.count > 0 && [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { + while (self->_frames.count > 0 && + [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { [self removeOldestFrame]; } }); } -- (void)removeOldestFrame { - NSError * error; - if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath error:&error]){ - SENTRY_LOG_DEBUG(@"Could not delete replay frame at: %@. %@",_frames.firstObject.imagePath, error); +- (void)removeOldestFrame +{ + NSError *error; + if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath + error:&error]) { + SENTRY_LOG_DEBUG( + @"Could not delete replay frame at: %@. %@", _frames.firstObject.imagePath, error); } [_frames removeObjectAtIndex:0]; } -- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion { + completion:(void (^)(BOOL success, NSError *error))completion +{ // Set up AVAssetWriter with appropriate settings AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL fileType:AVFileTypeQuickTimeMovie error:nil]; - + NSDictionary *videoSettings = @{ - AVVideoCodecKey: AVVideoCodecTypeH264, - AVVideoWidthKey: @(_videoSize.width), - AVVideoHeightKey: @(_videoSize.height), - AVVideoCompressionPropertiesKey: @{ - AVVideoAverageBitRateKey: @(_bitRate), - AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel, + AVVideoCodecKey : AVVideoCodecTypeH264, + AVVideoWidthKey : @(_videoSize.width), + AVVideoHeightKey : @(_videoSize.height), + AVVideoCompressionPropertiesKey : @ { + AVVideoAverageBitRateKey : @(_bitRate), + AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel, }, }; - - AVAssetWriterInput *videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; + + AVAssetWriterInput *videoWriterInput = + [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo + outputSettings:videoSettings]; NSDictionary *bufferAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB), + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB), }; - - AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:bufferAttributes]; - + + AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput + sourcePixelBufferAttributes:bufferAttributes]; + [videoWriter addInput:videoWriterInput]; - + // Start writing video [videoWriter startWriting]; [videoWriter startSessionAtSourceTime:kCMTimeZero]; - - NSDate* end = [beginning dateByAddingTimeInterval:duration]; + + NSDate *end = [beginning dateByAddingTimeInterval:duration]; __block NSInteger frameCount = 0; - NSMutableArray * frames = [NSMutableArray array]; + NSMutableArray *frames = [NSMutableArray array]; for (SentryReplayFrame *frame in self->_frames) { - if ([frame.time compare:beginning] == NSOrderedAscending) { - continue;; + if ([frame.time compare:beginning] == NSOrderedAscending) { + continue; + ; } else if ([frame.time compare:end] == NSOrderedDescending) { break; } [frames addObject:frame.imagePath]; } - - [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue usingBlock:^{ - UIImage *image = [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); - - if (![self appendPixelBufferForImage:image pixelBufferAdaptor:pixelBufferAdaptor presentationTime:presentTime]) { - if (completion) { - completion(NO, videoWriter.error); - } - } - } - - if (frameCount >= frames.count){ - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - completion(videoWriter.status == AVAssetWriterStatusCompleted, videoWriter.error); - } - }]; - } - }]; + + [videoWriterInput + requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue + usingBlock:^{ + UIImage *image = + [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { + if (completion) { + completion(NO, videoWriter.error); + } + } + } + + if (frameCount >= frames.count) { + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + completion(videoWriter.status + == AVAssetWriterStatusCompleted, + videoWriter.error); + } + }]; + } + }]; } -- (BOOL)appendPixelBufferForImage:(UIImage *)image pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor presentationTime:(CMTime)presentationTime { +- (BOOL)appendPixelBufferForImage:(UIImage *)image + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime +{ CVReturn status = kCVReturnSuccess; - + CVPixelBufferRef pixelBuffer = NULL; - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); - + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, + (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); + if (status != kCVReturnSuccess) { return NO; } - + CVPixelBufferLockBaseAddress(pixelBuffer, 0); void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - + CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, + (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, + (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); + CGContextTranslateCTM(context, 0, image.size.height); CGContextScaleCTM(context, 1.0, -1.0); - + UIGraphicsPushContext(context); [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; UIGraphicsPopContext(); - + CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); - + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - + // Append the pixel buffer with the current image to the video - BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]; - + BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer + withPresentationTime:presentationTime]; + CVPixelBufferRelease(pixelBuffer); - + return success; } - @end -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 65fc53a7a16..556593d3330 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,12 +23,12 @@ # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" +# import "SentryReplaySettings+Private.h" # import "SentryScreenshotIntegration.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" -# import "SentryReplaySettings+Private.h" -# import "SentrySessionReplayIntegration.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @@ -265,7 +265,6 @@ - (void)setDsn:(NSString *)dsn } } - /** * Populates all @c SentryOptions values from @c options dict using fallbacks/defaults if needed. */ @@ -412,13 +411,14 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; - + if (@available(iOS 16.0, *)) { if ([options[@"sessionReplaySettings"] isKindOfClass:NSDictionary.class]) { - self.sessionReplaySettings = [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; + self.sessionReplaySettings = + [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; } } - + #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m index b2a67f474fd..82a2113fad4 100644 --- a/Sources/Sentry/SentryReplaySettings.m +++ b/Sources/Sentry/SentryReplaySettings.m @@ -1,7 +1,7 @@ #import "SentryReplaySettings.h" - -@interface SentryReplaySettings () +@interface +SentryReplaySettings () @property (nonatomic) NSInteger replayBitRate; @@ -9,7 +9,8 @@ @interface SentryReplaySettings () @implementation SentryReplaySettings --(instancetype)init { +- (instancetype)init +{ if (self = [super init]) { self.replaysSessionSampleRate = 0; self.replaysOnErrorSampleRate = 0; @@ -18,21 +19,24 @@ -(instancetype)init { return self; } -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate { +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate + replaysOnErrorSampleRate:(CGFloat)errorSampleRate +{ if (self = [self init]) { self.replaysSessionSampleRate = sessionSampleRate; self.replaysOnErrorSampleRate = errorSampleRate; } - + return self; } -- (instancetype)initWithDictionary:(NSDictionary*)dictionary { +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ if (self = [self init]) { if ([dictionary[@"replaysSessionSampleRate"] isKindOfClass:NSNumber.class]) { self.replaysSessionSampleRate = [dictionary[@"replaysSessionSampleRate"] floatValue]; } - + if ([dictionary[@"replaysOnErrorSampleRate"] isKindOfClass:NSNumber.class]) { self.replaysOnErrorSampleRate = [dictionary[@"replaysOnErrorSampleRate"] floatValue]; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 1936890cc2d..4b7aa7da009 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,71 +1,78 @@ #import "SentrySessionReplay.h" -#import "SentryViewPhotographer.h" -#import "SentryOndemandReplay.h" #import "SentryAttachment+Private.h" #import "SentryLog.h" +#import "SentryOndemandReplay.h" #import "SentryReplaySettings+Private.h" +#import "SentryViewPhotographer.h" @implementation SentrySessionReplay { - UIView * _rootView; + UIView *_rootView; BOOL _processingScreenshot; - CADisplayLink * _displayLink; - NSDate * _lastScreenShot; - NSDate * _videoSegmentStart; - NSURL * _urlToCache; - NSDate * _sessionStart; - SentryReplaySettings * _settings; - SentryOnDemandReplay * _replayMaker; - - NSMutableArray* imageCollection; + CADisplayLink *_displayLink; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSURL *_urlToCache; + NSDate *_sessionStart; + SentryReplaySettings *_settings; + SentryOnDemandReplay *_replayMaker; + + NSMutableArray *imageCollection; } -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings { +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings +{ if (self = [super init]) { _settings = replaySettings; } return self; } -- (void)start:(UIView *)rootView fullSession:(BOOL)full { +- (void)start:(UIView *)rootView fullSession:(BOOL)full +{ if (rootView == nil) { SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); return; } - - @synchronized (self) { + + @synchronized(self) { if (_displayLink == nil) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } else { - //Session display is already running. + // Session display is already running. return; } - + _rootView = rootView; _lastScreenShot = [[NSDate alloc] init]; _videoSegmentStart = nil; _sessionStart = _lastScreenShot; - - NSURL * docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject URLByAppendingPathComponent:@"io.sentry"]; - - NSString * currentSession = [NSUUID UUID].UUIDString; + + NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory + inDomains:NSUserDomainMask] + .firstObject URLByAppendingPathComponent:@"io.sentry"]; + + NSString *currentSession = [NSUUID UUID].UUIDString; _urlToCache = [docs URLByAppendingPathComponent:currentSession]; - + if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache withIntermediateDirectories:YES attributes:nil error:nil]; + [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache + withIntermediateDirectories:YES + attributes:nil + error:nil]; } - - _replayMaker = - [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; + + _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; _replayMaker.bitRate = _settings.replayBitRate; _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; imageCollection = [NSMutableArray array]; - - NSLog(@"Recording session to %@",_urlToCache); + + NSLog(@"Recording session to %@", _urlToCache); } } -- (void)stop { +- (void)stop +{ [_displayLink invalidate]; _displayLink = nil; } @@ -76,99 +83,108 @@ - (void)stop { if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { return attachments; } - + NSLog(@"Recording session event id %@", event.eventId); NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; - - NSURL * finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + dispatch_group_t _wait_for_render = dispatch_group_create(); - + dispatch_group_enter(_wait_for_render); [_replayMaker createVideoOf:30 - from:[NSDate dateWithTimeIntervalSinceNow:-30] - outputFileURL:finalPath - completion:^(BOOL success, NSError * _Nonnull error) { - dispatch_group_leave(_wait_for_render); - }]; + from:[NSDate dateWithTimeIntervalSinceNow:-30] + outputFileURL:finalPath + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + }]; dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); - - SentryAttachment *attachment = - [[SentryAttachment alloc] initWithPath:finalPath.path - filename:@"replay.mp4" - contentType:@"video/mp4"]; + + SentryAttachment *attachment = [[SentryAttachment alloc] initWithPath:finalPath.path + filename:@"replay.mp4" + contentType:@"video/mp4"]; [result addObject:attachment]; - + return result; } -- (void)sendReplayForEvent:(SentryEvent *)event { - +- (void)sendReplayForEvent:(SentryEvent *)event +{ } -- (void)newFrame:(CADisplayLink *)sender { - NSDate * now = [[NSDate alloc] init]; - +- (void)newFrame:(CADisplayLink *)sender +{ + NSDate *now = [[NSDate alloc] init]; + if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { [self takeScreenshot]; _lastScreenShot = now; - + if (_videoSegmentStart == nil) { _videoSegmentStart = now; } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { - [self prepareSegmentUntil: now]; + [self prepareSegmentUntil:now]; } } } -- (void)prepareSegmentUntil:(NSDate *)date { +- (void)prepareSegmentUntil:(NSDate *)date +{ NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; - NSURL * pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; - + NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { - NSError * error; - if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path withIntermediateDirectories:YES attributes:nil error:&error]){ - SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", error.localizedDescription); + NSError *error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", + error.localizedDescription); return; } } - - pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4",from, to]]; - + + pathToSegment = [pathToSegment + URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + dispatch_group_t _wait_for_render = dispatch_group_create(); - + dispatch_group_enter(_wait_for_render); [_replayMaker createVideoOf:5 - from:[date dateByAddingTimeInterval:-5] - outputFileURL:pathToSegment - completion:^(BOOL success, NSError * _Nonnull error) { - dispatch_group_leave(_wait_for_render); - - //Need to send the segment here - - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; - + from:[date dateByAddingTimeInterval:-5] + outputFileURL:pathToSegment + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + + // Need to send the segment here + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; } -- (void)takeScreenshot { - if (_processingScreenshot) { return; } - @synchronized (self) { - if (_processingScreenshot) { return; } +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } _processingScreenshot = YES; } - - UIImage* screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; - + + UIImage *screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; + _processingScreenshot = NO; - - dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(backgroundQueue, ^{ - [self->_replayMaker addFrame:screenshot]; - }); + + dispatch_queue_t backgroundQueue + = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrame:screenshot]; }); } @end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index eb4fe00cfe3..f6953ae919c 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,18 +1,16 @@ #import "SentrySessionReplayIntegration.h" -#import "SentrySessionReplay.h" -#import "SentryDependencyContainer.h" -#import "SentryUIApplication.h" -#import "SentrySDK+Private.h" #import "SentryClient+Private.h" +#import "SentryDependencyContainer.h" #import "SentryHub+Private.h" -#import "SentrySDK+Private.h" -#import "SentryReplaySettings.h" -#import "SentryRandom.h" #import "SentryOptions.h" - +#import "SentryRandom.h" +#import "SentryReplaySettings.h" +#import "SentrySDK+Private.h" +#import "SentrySessionReplay.h" +#import "SentryUIApplication.h" @implementation SentrySessionReplayIntegration { - SentrySessionReplay * sessionReplay; + SentrySessionReplay *sessionReplay; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -20,28 +18,36 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options if ([super installWithOptions:options] == NO) { return NO; } - + if (@available(iOS 16.0, *)) { - if (options.sessionReplaySettings.replaysSessionSampleRate == 0 && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { + if (options.sessionReplaySettings.replaysSessionSampleRate == 0 + && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { return NO; } - - sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; - - [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplaySettings.replaysSessionSampleRate]]; - + + sessionReplay = + [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; + + [sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.sessionReplaySettings + .replaysSessionSampleRate]]; + SentryClient *client = [SentrySDK.currentHub getClient]; [client addAttachmentProcessor:sessionReplay]; - - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; return YES; } else { return NO; } } --(void)stop { +- (void)stop +{ [sessionReplay stop]; } @@ -52,10 +58,10 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { - } -- (BOOL)shouldReplayFullSession:(CGFloat)rate { +- (BOOL)shouldReplayFullSession:(CGFloat)rate +{ return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; } diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index aaeca74ec0c..698ba9c53f7 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -3,75 +3,74 @@ #if SENTRY_HAS_UIKIT @implementation SentryViewPhotographer { - NSMutableArray * _ignoreClasses; - NSMutableArray * _redactClasses; + NSMutableArray *_ignoreClasses; + NSMutableArray *_redactClasses; } -+(SentryViewPhotographer *)shared { - static SentryViewPhotographer* _shared = nil; ++ (SentryViewPhotographer *)shared +{ + static SentryViewPhotographer *_shared = nil; static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _shared = [[SentryViewPhotographer alloc] init]; - }); - + dispatch_once(&onceToken, ^{ _shared = [[SentryViewPhotographer alloc] init]; }); + return _shared; } --(instancetype)init { +- (instancetype)init +{ if (self = [super init]) { - _ignoreClasses = @[ - UISlider.class, - UISwitch.class - ].mutableCopy; - - _redactClasses = @[ - UILabel.class, - UITextView.class, - UITextField.class - ].mutableCopy; - - - NSArray * extraClasses = @[ + _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; + + _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; + + NSArray *extraClasses = @[ @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - @"SwiftUI._UIGraphicsView", - @"SwiftUI.ImageLayer" + @"SwiftUI._UIGraphicsView", @"SwiftUI.ImageLayer" ]; - - for (NSString * className in extraClasses) { + + for (NSString *className in extraClasses) { Class viewClass = NSClassFromString(className); - if (viewClass != nil) {[_redactClasses addObject:viewClass];} + if (viewClass != nil) { + [_redactClasses addObject:viewClass]; + } } } return self; } --(UIImage*)imageFromUIView:(UIView *)view { +- (UIImage *)imageFromUIView:(UIView *)view +{ UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); CGContextRef currentContext = UIGraphicsGetCurrentContext(); - + [view.layer renderInContext:currentContext]; - + [self maskText:view context:currentContext]; - - UIImage* screenshot = UIGraphicsGetImageFromCurrentImageContext(); + + UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - + return screenshot; } -- (void)maskText:(UIView *)view context:(CGContextRef)context { +- (void)maskText:(UIView *)view context:(CGContextRef)context +{ [UIColor.blackColor setFill]; - CGPathRef maskPath = [self buildPathForView:view inPath:CGPathCreateMutable() visibleArea:view.frame]; + CGPathRef maskPath = [self buildPathForView:view + inPath:CGPathCreateMutable() + visibleArea:view.frame]; CGContextAddPath(context, maskPath); CGContextFillPath(context); } -- (BOOL)shouldIgnoreView:(UIView *)view { +- (BOOL)shouldIgnoreView:(UIView *)view +{ return [view isKindOfClass:UISwitch.class]; } -- (BOOL)shouldIgnore:(UIView *)view { +- (BOOL)shouldIgnore:(UIView *)view +{ for (Class class in _ignoreClasses) { if ([view isKindOfClass:class]) { return true; @@ -80,33 +79,41 @@ - (BOOL)shouldIgnore:(UIView *)view { return false; } -- (BOOL)shouldRedact:(UIView *)view { +- (BOOL)shouldRedact:(UIView *)view +{ for (Class class in _redactClasses) { if ([view isKindOfClass:class]) { return true; } } - - return ([view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); + + return ( + [view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); } -- (BOOL)shouldRedactImageView:(UIImageView *)imageView { - return imageView.image != nil - && [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil - && (imageView.image.size.width > 10 && imageView.image.size.height > 10); //This is to avoid redact gradient backgroud that are usually small lines repeating +- (BOOL)shouldRedactImageView:(UIImageView *)imageView +{ + return imageView.image != nil && + [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil + && (imageView.image.size.width > 10 + && imageView.image.size.height > 10); // This is to avoid redact gradient backgroud that + // are usually small lines repeating } -- (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)path visibleArea:(CGRect)area { +- (CGMutablePathRef)buildPathForView:(UIView *)view + inPath:(CGMutablePathRef)path + visibleArea:(CGRect)area +{ CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; if (!CGRectIntersectsRect(area, rectInWindow)) { return path; } - + if (view.hidden || view.alpha == 0) { return path; } - + BOOL ignore = [self shouldIgnore:view]; if (!ignore && [self shouldRedact:view]) { CGPathAddRect(path, NULL, rectInWindow); @@ -116,7 +123,7 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)pat CGPathRelease(path); path = newPath; } - + if (!ignore) { for (UIView *subview in view.subviews) { path = [self buildPathForView:subview inPath:path visibleArea:area]; @@ -126,19 +133,22 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)pat return path; } -- (CGMutablePathRef) excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { +- (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ if (@available(iOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); - } + } return path; } -- (BOOL)isOpaqueOrHasBackground:(UIView *)view { - return view.isOpaque || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); +- (BOOL)isOpaqueOrHasBackground:(UIView *)view +{ + return view.isOpaque + || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); } @end -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h index 8c799c15004..8aae0a4b713 100644 --- a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h +++ b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h @@ -3,7 +3,8 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentryReplaySettings (Private) +@interface +SentryReplaySettings (Private) /** * Defines the quality of the session replay. @@ -12,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) NSInteger replayBitRate; -- (instancetype)initWithDictionary:(NSDictionary*)dictionary; +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; @end diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index dc4d78ada3f..e1e9127cf77 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -2,8 +2,7 @@ #import #if SENTRY_HAS_UIKIT -#import - +# import NS_ASSUME_NONNULL_BEGIN @@ -17,7 +16,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)addFrame:(UIImage *)image; -- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL completion:(void (^)(BOOL success, NSError *error))completion; @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 43d89925ba5..d8437c32c71 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -1,8 +1,8 @@ -#import -#import -#import "SentryEvent.h" #import "SentryClient+Private.h" +#import "SentryEvent.h" #import "SentryReplaySettings.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index 6d0dbb0389b..b789b91009a 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -1,6 +1,6 @@ -#import #import "SentryBaseIntegration.h" #import "SentryIntegrationProtocol.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h index e03a8749fbf..2425af8794c 100644 --- a/Sources/Sentry/include/SentryViewPhotographer.h +++ b/Sources/Sentry/include/SentryViewPhotographer.h @@ -2,18 +2,18 @@ #import #if SENTRY_HAS_UIKIT -#import +# import NS_ASSUME_NONNULL_BEGIN @interface SentryViewPhotographer : NSObject -@property (nonatomic, readonly, class) SentryViewPhotographer* shared; +@property (nonatomic, readonly, class) SentryViewPhotographer *shared; --(UIImage*)imageFromUIView:(UIView *)view; +- (UIImage *)imageFromUIView:(UIView *)view; @end NS_ASSUME_NONNULL_END -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT From b725be79189fd1a16da7697336327883f3e8bff4 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 17:23:42 +0100 Subject: [PATCH 20/88] Clean Up PR --- Sentry.xcodeproj/project.pbxproj | 34 --- Sources/Sentry/SentryOnDemandReplay.m | 234 ------------------ Sources/Sentry/SentrySessionReplay.m | 190 -------------- .../Sentry/SentrySessionReplayIntegration.m | 68 ----- Sources/Sentry/SentryViewPhotographer.m | 154 ------------ Sources/Sentry/include/SentryOnDemandReplay.h | 32 --- Sources/Sentry/include/SentrySessionReplay.h | 26 -- .../include/SentrySessionReplayIntegration.h | 11 - .../Sentry/include/SentryViewPhotographer.h | 19 -- 9 files changed, 768 deletions(-) delete mode 100644 Sources/Sentry/SentryOnDemandReplay.m delete mode 100644 Sources/Sentry/SentrySessionReplay.m delete mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m delete mode 100644 Sources/Sentry/SentryViewPhotographer.m delete mode 100644 Sources/Sentry/include/SentryOnDemandReplay.h delete mode 100644 Sources/Sentry/include/SentrySessionReplay.h delete mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.h delete mode 100644 Sources/Sentry/include/SentryViewPhotographer.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 326a000e76d..bf8dffc99bf 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -766,14 +766,6 @@ D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */; }; D83D07832B7E5F2100CC9674 /* SentryReplaySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */; }; - D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; - D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; - D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; - D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */; }; - D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */; }; - D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */; }; - D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */; }; - D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */; }; D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; @@ -1776,14 +1768,6 @@ D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplaySettings.h; path = Public/SentryReplaySettings.h; sourceTree = ""; }; D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplaySettings.m; sourceTree = ""; }; D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplaySettings+Private.h"; path = "include/HybridPublic/SentryReplaySettings+Private.h"; sourceTree = ""; }; - D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; - D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; - D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; - D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; - D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; - D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; - D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; - D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; @@ -3431,19 +3415,9 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, - D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */, D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */, D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */, - D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */, - D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */, - D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */, - D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */, - D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */, - D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */, - D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */, - D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */, ); name = SessionReplay; sourceTree = ""; @@ -3665,7 +3639,6 @@ 03F84D2A27DD416B008FE43F /* SentryProfilingLogging.hpp in Headers */, 63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */, 7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */, - D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */, 63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */, 7BE912AB272162AF00E49E62 /* SentryNoOpSpan.h in Headers */, 63FE70D120DA4C1000CDBAE8 /* SentryCrashMonitorContext.h in Headers */, @@ -3738,7 +3711,6 @@ 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, - D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, 7BC9A20228F41350001E7C4C /* SentryMeasurementUnit.h in Headers */, @@ -3862,7 +3834,6 @@ 6344DDB01EC308E400D9160D /* SentryCrashInstallationReporter.h in Headers */, 8E25C95725F836EE00DC215B /* SentryRandom.h in Headers */, 7B5CAF7527F5A67C00ED0DB6 /* SentryNSURLRequestBuilder.h in Headers */, - D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */, 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, 7BA61CB4247BC3EB00C130A8 /* SentryCrashBinaryImageProvider.h in Headers */, 63FE713D20DA4C1100CDBAE8 /* SentryCrashLogger.h in Headers */, @@ -3890,7 +3861,6 @@ 6334314120AD9AE40077E581 /* SentryMechanism.h in Headers */, 03F84D2827DD414C008FE43F /* SentryCPU.h in Headers */, 0A80E435291017D500095219 /* SentryWatchdogTerminationScopeObserver.h in Headers */, - D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */, 7B610D642512399600B0B5D9 /* SentryHub+Private.h in Headers */, D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */, 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, @@ -4212,7 +4182,6 @@ 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, 7B6C5EDE264E8DF00010D138 /* SentryFramesTracker.m in Sources */, - D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */, D84F833E2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m in Sources */, 7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */, 63AA76A31EB9CBAA00D153DE /* SentryDsn.m in Sources */, @@ -4272,7 +4241,6 @@ 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, - D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */, 7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */, 63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */, 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, @@ -4341,7 +4309,6 @@ 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, - D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, @@ -4437,7 +4404,6 @@ 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, - D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m deleted file mode 100644 index e6302a5b4dc..00000000000 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ /dev/null @@ -1,234 +0,0 @@ -#import "SentryOnDemandReplay.h" - -#if SENTRY_HAS_UIKIT -# import "SentryLog.h" -# import -# import -@interface SentryReplayFrame : NSObject - -@property (nonatomic, strong) NSString *imagePath; -@property (nonatomic, strong) NSDate *time; - -- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time; - -@end - -@implementation SentryReplayFrame -- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time -{ - if (self = [super init]) { - self.imagePath = path; - self.time = time; - } - return self; -} - -@end - -@implementation SentryOnDemandReplay { - NSString *_outputPath; - NSDate *_startTime; - NSMutableArray *_frames; - CGSize _videoSize; - dispatch_queue_t _onDemandDispatchQueue; -} - -- (instancetype)initWithOutputPath:(NSString *)outputPath -{ - if (self = [super init]) { - _outputPath = outputPath; - _startTime = [[NSDate alloc] init]; - _frames = [NSMutableArray array]; - //_videoSize = CGSizeMake(300, 651); - _videoSize = CGSizeMake(200, 434); - _bitRate = 20000; - _cacheMaxSize = NSUIntegerMax; - _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); - } - return self; -} - -- (void)addFrame:(UIImage *)image -{ - dispatch_async(_onDemandDispatchQueue, ^{ - NSData *data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); - NSDate *date = [[NSDate alloc] init]; - NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; - NSString *imagePath = [self->_outputPath - stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; - - [data writeToFile:imagePath atomically:YES]; - - SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; - [self->_frames addObject:frame]; - - while (self->_frames.count > self->_cacheMaxSize) { - [self removeOldestFrame]; - } - }); -} - -- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth -{ - CGSize originalSize = originalImage.size; - CGFloat aspectRatio = originalSize.width / originalSize.height; - - CGFloat newWidth = MIN(originalSize.width, maxWidth); - CGFloat newHeight = newWidth / aspectRatio; - - CGSize newSize = CGSizeMake(newWidth, newHeight); - - UIGraphicsBeginImageContextWithOptions(newSize, NO, 1); - [originalImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return resizedImage; -} - -- (void)releaseFramesUntil:(NSDate *)date -{ - dispatch_async(_onDemandDispatchQueue, ^{ - while (self->_frames.count > 0 && - [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { - [self removeOldestFrame]; - } - }); -} - -- (void)removeOldestFrame -{ - NSError *error; - if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath - error:&error]) { - SENTRY_LOG_DEBUG( - @"Could not delete replay frame at: %@. %@", _frames.firstObject.imagePath, error); - } - [_frames removeObjectAtIndex:0]; -} - -- (void)createVideoOf:(NSTimeInterval)duration - from:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion -{ - // Set up AVAssetWriter with appropriate settings - AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL - fileType:AVFileTypeQuickTimeMovie - error:nil]; - - NSDictionary *videoSettings = @{ - AVVideoCodecKey : AVVideoCodecTypeH264, - AVVideoWidthKey : @(_videoSize.width), - AVVideoHeightKey : @(_videoSize.height), - AVVideoCompressionPropertiesKey : @ { - AVVideoAverageBitRateKey : @(_bitRate), - AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel, - }, - }; - - AVAssetWriterInput *videoWriterInput = - [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo - outputSettings:videoSettings]; - NSDictionary *bufferAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB), - }; - - AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor - assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput - sourcePixelBufferAttributes:bufferAttributes]; - - [videoWriter addInput:videoWriterInput]; - - // Start writing video - [videoWriter startWriting]; - [videoWriter startSessionAtSourceTime:kCMTimeZero]; - - NSDate *end = [beginning dateByAddingTimeInterval:duration]; - __block NSInteger frameCount = 0; - NSMutableArray *frames = [NSMutableArray array]; - for (SentryReplayFrame *frame in self->_frames) { - if ([frame.time compare:beginning] == NSOrderedAscending) { - continue; - ; - } else if ([frame.time compare:end] == NSOrderedDescending) { - break; - } - [frames addObject:frame.imagePath]; - } - - [videoWriterInput - requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue - usingBlock:^{ - UIImage *image = - [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); - - if (![self appendPixelBufferForImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { - if (completion) { - completion(NO, videoWriter.error); - } - } - } - - if (frameCount >= frames.count) { - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - completion(videoWriter.status - == AVAssetWriterStatusCompleted, - videoWriter.error); - } - }]; - } - }]; -} - -- (BOOL)appendPixelBufferForImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime -{ - CVReturn status = kCVReturnSuccess; - - CVPixelBufferRef pixelBuffer = NULL; - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, - (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); - - if (status != kCVReturnSuccess) { - return NO; - } - - CVPixelBufferLockBaseAddress(pixelBuffer, 0); - void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - - CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, - (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, - (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - - CGContextTranslateCTM(context, 0, image.size.height); - CGContextScaleCTM(context, 1.0, -1.0); - - UIGraphicsPushContext(context); - [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - UIGraphicsPopContext(); - - CGColorSpaceRelease(rgbColorSpace); - CGContextRelease(context); - - CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - - // Append the pixel buffer with the current image to the video - BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer - withPresentationTime:presentationTime]; - - CVPixelBufferRelease(pixelBuffer); - - return success; -} - -@end -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m deleted file mode 100644 index 4b7aa7da009..00000000000 --- a/Sources/Sentry/SentrySessionReplay.m +++ /dev/null @@ -1,190 +0,0 @@ -#import "SentrySessionReplay.h" -#import "SentryAttachment+Private.h" -#import "SentryLog.h" -#import "SentryOndemandReplay.h" -#import "SentryReplaySettings+Private.h" -#import "SentryViewPhotographer.h" - -@implementation SentrySessionReplay { - UIView *_rootView; - BOOL _processingScreenshot; - CADisplayLink *_displayLink; - NSDate *_lastScreenShot; - NSDate *_videoSegmentStart; - NSURL *_urlToCache; - NSDate *_sessionStart; - SentryReplaySettings *_settings; - SentryOnDemandReplay *_replayMaker; - - NSMutableArray *imageCollection; -} - -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings -{ - if (self = [super init]) { - _settings = replaySettings; - } - return self; -} - -- (void)start:(UIView *)rootView fullSession:(BOOL)full -{ - if (rootView == nil) { - SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); - return; - } - - @synchronized(self) { - if (_displayLink == nil) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } else { - // Session display is already running. - return; - } - - _rootView = rootView; - _lastScreenShot = [[NSDate alloc] init]; - _videoSegmentStart = nil; - _sessionStart = _lastScreenShot; - - NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory - inDomains:NSUserDomainMask] - .firstObject URLByAppendingPathComponent:@"io.sentry"]; - - NSString *currentSession = [NSUUID UUID].UUIDString; - _urlToCache = [docs URLByAppendingPathComponent:currentSession]; - - if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache - withIntermediateDirectories:YES - attributes:nil - error:nil]; - } - - _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; - _replayMaker.bitRate = _settings.replayBitRate; - _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; - imageCollection = [NSMutableArray array]; - - NSLog(@"Recording session to %@", _urlToCache); - } -} - -- (void)stop -{ - [_displayLink invalidate]; - _displayLink = nil; -} - -- (NSArray *)processAttachments:(NSArray *)attachments - forEvent:(nonnull SentryEvent *)event -{ - if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { - return attachments; - } - - NSLog(@"Recording session event id %@", event.eventId); - NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; - - NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - - dispatch_group_t _wait_for_render = dispatch_group_create(); - - dispatch_group_enter(_wait_for_render); - [_replayMaker createVideoOf:30 - from:[NSDate dateWithTimeIntervalSinceNow:-30] - outputFileURL:finalPath - completion:^(BOOL success, NSError *_Nonnull error) { - dispatch_group_leave(_wait_for_render); - }]; - dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); - - SentryAttachment *attachment = [[SentryAttachment alloc] initWithPath:finalPath.path - filename:@"replay.mp4" - contentType:@"video/mp4"]; - - [result addObject:attachment]; - - return result; -} - -- (void)sendReplayForEvent:(SentryEvent *)event -{ -} - -- (void)newFrame:(CADisplayLink *)sender -{ - NSDate *now = [[NSDate alloc] init]; - - if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { - [self takeScreenshot]; - _lastScreenShot = now; - - if (_videoSegmentStart == nil) { - _videoSegmentStart = now; - } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { - [self prepareSegmentUntil:now]; - } - } -} - -- (void)prepareSegmentUntil:(NSDate *)date -{ - NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; - NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; - NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; - - if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { - NSError *error; - if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path - withIntermediateDirectories:YES - attributes:nil - error:&error]) { - SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", - error.localizedDescription); - return; - } - } - - pathToSegment = [pathToSegment - URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - - dispatch_group_t _wait_for_render = dispatch_group_create(); - - dispatch_group_enter(_wait_for_render); - [_replayMaker createVideoOf:5 - from:[date dateByAddingTimeInterval:-5] - outputFileURL:pathToSegment - completion:^(BOOL success, NSError *_Nonnull error) { - dispatch_group_leave(_wait_for_render); - - // Need to send the segment here - - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; -} - -- (void)takeScreenshot -{ - if (_processingScreenshot) { - return; - } - @synchronized(self) { - if (_processingScreenshot) { - return; - } - _processingScreenshot = YES; - } - - UIImage *screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; - - _processingScreenshot = NO; - - dispatch_queue_t backgroundQueue - = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrame:screenshot]; }); -} - -@end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m deleted file mode 100644 index f6953ae919c..00000000000 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ /dev/null @@ -1,68 +0,0 @@ -#import "SentrySessionReplayIntegration.h" -#import "SentryClient+Private.h" -#import "SentryDependencyContainer.h" -#import "SentryHub+Private.h" -#import "SentryOptions.h" -#import "SentryRandom.h" -#import "SentryReplaySettings.h" -#import "SentrySDK+Private.h" -#import "SentrySessionReplay.h" -#import "SentryUIApplication.h" - -@implementation SentrySessionReplayIntegration { - SentrySessionReplay *sessionReplay; -} - -- (BOOL)installWithOptions:(nonnull SentryOptions *)options -{ - if ([super installWithOptions:options] == NO) { - return NO; - } - - if (@available(iOS 16.0, *)) { - if (options.sessionReplaySettings.replaysSessionSampleRate == 0 - && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { - return NO; - } - - sessionReplay = - [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; - - [sessionReplay - start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplaySettings - .replaysSessionSampleRate]]; - - SentryClient *client = [SentrySDK.currentHub getClient]; - [client addAttachmentProcessor:sessionReplay]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(stop) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - return YES; - } else { - return NO; - } -} - -- (void)stop -{ - [sessionReplay stop]; -} - -- (SentryIntegrationOption)integrationOptions -{ - return kIntegrationOptionEnableReplay; -} - -- (void)uninstall -{ -} - -- (BOOL)shouldReplayFullSession:(CGFloat)rate -{ - return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; -} - -@end diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m deleted file mode 100644 index 698ba9c53f7..00000000000 --- a/Sources/Sentry/SentryViewPhotographer.m +++ /dev/null @@ -1,154 +0,0 @@ -#import "SentryViewPhotographer.h" - -#if SENTRY_HAS_UIKIT - -@implementation SentryViewPhotographer { - NSMutableArray *_ignoreClasses; - NSMutableArray *_redactClasses; -} - -+ (SentryViewPhotographer *)shared -{ - static SentryViewPhotographer *_shared = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ _shared = [[SentryViewPhotographer alloc] init]; }); - - return _shared; -} - -- (instancetype)init -{ - if (self = [super init]) { - _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; - - _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; - - NSArray *extraClasses = @[ - @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", - @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - @"SwiftUI._UIGraphicsView", @"SwiftUI.ImageLayer" - ]; - - for (NSString *className in extraClasses) { - Class viewClass = NSClassFromString(className); - if (viewClass != nil) { - [_redactClasses addObject:viewClass]; - } - } - } - return self; -} - -- (UIImage *)imageFromUIView:(UIView *)view -{ - UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); - CGContextRef currentContext = UIGraphicsGetCurrentContext(); - - [view.layer renderInContext:currentContext]; - - [self maskText:view context:currentContext]; - - UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return screenshot; -} - -- (void)maskText:(UIView *)view context:(CGContextRef)context -{ - [UIColor.blackColor setFill]; - CGPathRef maskPath = [self buildPathForView:view - inPath:CGPathCreateMutable() - visibleArea:view.frame]; - CGContextAddPath(context, maskPath); - CGContextFillPath(context); -} - -- (BOOL)shouldIgnoreView:(UIView *)view -{ - return [view isKindOfClass:UISwitch.class]; -} - -- (BOOL)shouldIgnore:(UIView *)view -{ - for (Class class in _ignoreClasses) { - if ([view isKindOfClass:class]) { - return true; - } - } - return false; -} - -- (BOOL)shouldRedact:(UIView *)view -{ - for (Class class in _redactClasses) { - if ([view isKindOfClass:class]) { - return true; - } - } - - return ( - [view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); -} - -- (BOOL)shouldRedactImageView:(UIImageView *)imageView -{ - return imageView.image != nil && - [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil - && (imageView.image.size.width > 10 - && imageView.image.size.height > 10); // This is to avoid redact gradient backgroud that - // are usually small lines repeating -} - -- (CGMutablePathRef)buildPathForView:(UIView *)view - inPath:(CGMutablePathRef)path - visibleArea:(CGRect)area -{ - CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; - - if (!CGRectIntersectsRect(area, rectInWindow)) { - return path; - } - - if (view.hidden || view.alpha == 0) { - return path; - } - - BOOL ignore = [self shouldIgnore:view]; - if (!ignore && [self shouldRedact:view]) { - CGPathAddRect(path, NULL, rectInWindow); - return path; - } else if ([self isOpaqueOrHasBackground:view]) { - CGMutablePathRef newPath = [self excludeRect:rectInWindow fromPath:path]; - CGPathRelease(path); - path = newPath; - } - - if (!ignore) { - for (UIView *subview in view.subviews) { - path = [self buildPathForView:subview inPath:path visibleArea:area]; - } - } - - return path; -} - -- (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path -{ - if (@available(iOS 16.0, *)) { - CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); - CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); - return CGPathCreateMutableCopy(newPath); - } - return path; -} - -- (BOOL)isOpaqueOrHasBackground:(UIView *)view -{ - return view.isOpaque - || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); -} - -@end - -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h deleted file mode 100644 index e1e9127cf77..00000000000 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ /dev/null @@ -1,32 +0,0 @@ -#import "SentryDefines.h" -#import - -#if SENTRY_HAS_UIKIT -# import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryOnDemandReplay : NSObject - -@property (nonatomic) NSInteger bitRate; - -@property (nonatomic) NSUInteger cacheMaxSize; - -- (instancetype)initWithOutputPath:(NSString *)outputPath; - -- (void)addFrame:(UIImage *)image; - -- (void)createVideoOf:(NSTimeInterval)duration - from:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion; - -/** - * Remove cached frames until given date. - */ -- (void)releaseFramesUntil:(NSDate *)date; - -@end - -NS_ASSUME_NONNULL_END -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h deleted file mode 100644 index d8437c32c71..00000000000 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ /dev/null @@ -1,26 +0,0 @@ -#import "SentryClient+Private.h" -#import "SentryEvent.h" -#import "SentryReplaySettings.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentrySessionReplay : NSObject - -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings; - -/** - * Start recording the session using rootView as image source. - * If full is @c YES, we transmit the entire session to sentry. - */ -- (void)start:(UIView *)rootView fullSession:(BOOL)full; - -/** - * Stop recording the session replay - */ -- (void)stop; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h deleted file mode 100644 index b789b91009a..00000000000 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ /dev/null @@ -1,11 +0,0 @@ -#import "SentryBaseIntegration.h" -#import "SentryIntegrationProtocol.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentrySessionReplayIntegration : SentryBaseIntegration - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h deleted file mode 100644 index 2425af8794c..00000000000 --- a/Sources/Sentry/include/SentryViewPhotographer.h +++ /dev/null @@ -1,19 +0,0 @@ -#import "SentryDefines.h" -#import - -#if SENTRY_HAS_UIKIT -# import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryViewPhotographer : NSObject - -@property (nonatomic, readonly, class) SentryViewPhotographer *shared; - -- (UIImage *)imageFromUIView:(UIView *)view; - -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_HAS_UIKIT From 5443cdce26c21ccd3e8adaea784bd8e4b99687d2 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Feb 2024 17:26:50 +0100 Subject: [PATCH 21/88] Update SentryOptions.m --- Sources/Sentry/SentryOptions.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 556593d3330..8c1b27395c7 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -25,7 +25,6 @@ # import "SentryPerformanceTrackingIntegration.h" # import "SentryReplaySettings+Private.h" # import "SentryScreenshotIntegration.h" -# import "SentrySessionReplayIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" @@ -67,7 +66,6 @@ - (void)setMeasurement:(SentryMeasurementValue *)measurement NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), - NSStringFromClass([SentrySessionReplayIntegration class]), #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), From 32227b1ff5c120f3291194afd62c3cabe87d3deb Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 09:42:36 +0100 Subject: [PATCH 22/88] Test --- Tests/SentryTests/SentryOptionsTest.m | 25 +++++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 2 files changed, 26 insertions(+) diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index e9025655764..e309f1435cb 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -543,6 +543,7 @@ - (void)testNSNull_SetsDefaultValue #if SENTRY_HAS_UIKIT @"enableUIViewControllerTracing" : [NSNull null], @"attachScreenshot" : [NSNull null], + @"sessionReplaySettings" : [NSNull null], #endif @"enableAppHangTracking" : [NSNull null], @"appHangTimeoutInterval" : [NSNull null], @@ -601,6 +602,9 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(options.enableUserInteractionTracing, YES); XCTAssertEqual(options.enablePreWarmedAppStartTracing, NO); XCTAssertEqual(options.attachViewHierarchy, NO); + if (@available(iOS 16.0, *)) { + XCTAssertNil(options.sessionReplaySettings); + } #endif XCTAssertFalse(options.enableTracing); XCTAssertTrue(options.enableAppHangTracking); @@ -774,6 +778,27 @@ - (void)testEnablePreWarmedAppStartTracking [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:NO]; } +- (void)testSessionReplaySettingsInit +{ + if (@available(iOS 16.0, *)) { + SentryOptions *options = [self getValidOptions:@{ + @"sessionReplaySettings" : + @ { @"replaysSessionSampleRate" : @2, @"replaysOnErrorSampleRate" : @4 } + }]; + XCTAssertEqual(options.sessionReplaySettings.replaysSessionSampleRate, 2); + XCTAssertEqual(options.sessionReplaySettings.replaysOnErrorSampleRate, 4); + } +} + +- (void)testSessionReplaySettingsDefaults +{ + if (@available(iOS 16.0, *)) { + SentryOptions *options = [self getValidOptions:@{ @"sessionReplaySettings" : @ {} }]; + XCTAssertEqual(options.sessionReplaySettings.replaysSessionSampleRate, 0); + XCTAssertEqual(options.sessionReplaySettings.replaysOnErrorSampleRate, 0); + } +} + #endif #if SENTRY_HAS_METRIC_KIT diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index b7a440904d3..3edc64f28ea 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -219,6 +219,7 @@ #import "SentryPropagationContext.h" #import "SentryReplayEvent.h" #import "SentryReplayRecording.h" +#import "SentryReplaySettings.h" #import "SentrySampleDecision+Private.h" #import "SentrySpanOperations.h" #import "SentryTimeToDisplayTracker.h" From a99524da21332a64bc0ee2d04462f3a6c33efc0a Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 09:48:23 +0100 Subject: [PATCH 23/88] Revert "Clean Up PR" This reverts commit b725be79189fd1a16da7697336327883f3e8bff4. --- Sentry.xcodeproj/project.pbxproj | 34 +++ Sources/Sentry/SentryOnDemandReplay.m | 234 ++++++++++++++++++ Sources/Sentry/SentrySessionReplay.m | 190 ++++++++++++++ .../Sentry/SentrySessionReplayIntegration.m | 68 +++++ Sources/Sentry/SentryViewPhotographer.m | 154 ++++++++++++ Sources/Sentry/include/SentryOnDemandReplay.h | 32 +++ Sources/Sentry/include/SentrySessionReplay.h | 26 ++ .../include/SentrySessionReplayIntegration.h | 11 + .../Sentry/include/SentryViewPhotographer.h | 19 ++ 9 files changed, 768 insertions(+) create mode 100644 Sources/Sentry/SentryOnDemandReplay.m create mode 100644 Sources/Sentry/SentrySessionReplay.m create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m create mode 100644 Sources/Sentry/SentryViewPhotographer.m create mode 100644 Sources/Sentry/include/SentryOnDemandReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.h create mode 100644 Sources/Sentry/include/SentryViewPhotographer.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index bf8dffc99bf..326a000e76d 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -766,6 +766,14 @@ D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */; }; D83D07832B7E5F2100CC9674 /* SentryReplaySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */; }; + D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; + D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; + D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; + D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */; }; + D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */; }; + D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */; }; + D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */; }; + D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */; }; D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; @@ -1768,6 +1776,14 @@ D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplaySettings.h; path = Public/SentryReplaySettings.h; sourceTree = ""; }; D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplaySettings.m; sourceTree = ""; }; D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplaySettings+Private.h"; path = "include/HybridPublic/SentryReplaySettings+Private.h"; sourceTree = ""; }; + D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; + D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; + D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; + D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; + D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; + D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; + D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; @@ -3415,9 +3431,19 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, + D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, + D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */, D83D07842B7E634F00CC9674 /* SentryReplaySettings+Private.h */, D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */, + D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */, + D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */, + D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */, + D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */, + D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */, + D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */, + D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */, + D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */, ); name = SessionReplay; sourceTree = ""; @@ -3639,6 +3665,7 @@ 03F84D2A27DD416B008FE43F /* SentryProfilingLogging.hpp in Headers */, 63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */, 7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */, + D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */, 63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */, 7BE912AB272162AF00E49E62 /* SentryNoOpSpan.h in Headers */, 63FE70D120DA4C1000CDBAE8 /* SentryCrashMonitorContext.h in Headers */, @@ -3711,6 +3738,7 @@ 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, + D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, 7BC9A20228F41350001E7C4C /* SentryMeasurementUnit.h in Headers */, @@ -3834,6 +3862,7 @@ 6344DDB01EC308E400D9160D /* SentryCrashInstallationReporter.h in Headers */, 8E25C95725F836EE00DC215B /* SentryRandom.h in Headers */, 7B5CAF7527F5A67C00ED0DB6 /* SentryNSURLRequestBuilder.h in Headers */, + D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */, 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, 7BA61CB4247BC3EB00C130A8 /* SentryCrashBinaryImageProvider.h in Headers */, 63FE713D20DA4C1100CDBAE8 /* SentryCrashLogger.h in Headers */, @@ -3861,6 +3890,7 @@ 6334314120AD9AE40077E581 /* SentryMechanism.h in Headers */, 03F84D2827DD414C008FE43F /* SentryCPU.h in Headers */, 0A80E435291017D500095219 /* SentryWatchdogTerminationScopeObserver.h in Headers */, + D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */, 7B610D642512399600B0B5D9 /* SentryHub+Private.h in Headers */, D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */, 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, @@ -4182,6 +4212,7 @@ 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, 7B6C5EDE264E8DF00010D138 /* SentryFramesTracker.m in Sources */, + D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */, D84F833E2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m in Sources */, 7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */, 63AA76A31EB9CBAA00D153DE /* SentryDsn.m in Sources */, @@ -4241,6 +4272,7 @@ 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, + D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */, 7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */, 63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */, 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, @@ -4309,6 +4341,7 @@ 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, + D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, @@ -4404,6 +4437,7 @@ 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, + D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m new file mode 100644 index 00000000000..e6302a5b4dc --- /dev/null +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -0,0 +1,234 @@ +#import "SentryOnDemandReplay.h" + +#if SENTRY_HAS_UIKIT +# import "SentryLog.h" +# import +# import +@interface SentryReplayFrame : NSObject + +@property (nonatomic, strong) NSString *imagePath; +@property (nonatomic, strong) NSDate *time; + +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time; + +@end + +@implementation SentryReplayFrame +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time +{ + if (self = [super init]) { + self.imagePath = path; + self.time = time; + } + return self; +} + +@end + +@implementation SentryOnDemandReplay { + NSString *_outputPath; + NSDate *_startTime; + NSMutableArray *_frames; + CGSize _videoSize; + dispatch_queue_t _onDemandDispatchQueue; +} + +- (instancetype)initWithOutputPath:(NSString *)outputPath +{ + if (self = [super init]) { + _outputPath = outputPath; + _startTime = [[NSDate alloc] init]; + _frames = [NSMutableArray array]; + //_videoSize = CGSizeMake(300, 651); + _videoSize = CGSizeMake(200, 434); + _bitRate = 20000; + _cacheMaxSize = NSUIntegerMax; + _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); + } + return self; +} + +- (void)addFrame:(UIImage *)image +{ + dispatch_async(_onDemandDispatchQueue, ^{ + NSData *data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); + NSDate *date = [[NSDate alloc] init]; + NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; + NSString *imagePath = [self->_outputPath + stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; + + [data writeToFile:imagePath atomically:YES]; + + SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; + [self->_frames addObject:frame]; + + while (self->_frames.count > self->_cacheMaxSize) { + [self removeOldestFrame]; + } + }); +} + +- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth +{ + CGSize originalSize = originalImage.size; + CGFloat aspectRatio = originalSize.width / originalSize.height; + + CGFloat newWidth = MIN(originalSize.width, maxWidth); + CGFloat newHeight = newWidth / aspectRatio; + + CGSize newSize = CGSizeMake(newWidth, newHeight); + + UIGraphicsBeginImageContextWithOptions(newSize, NO, 1); + [originalImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return resizedImage; +} + +- (void)releaseFramesUntil:(NSDate *)date +{ + dispatch_async(_onDemandDispatchQueue, ^{ + while (self->_frames.count > 0 && + [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { + [self removeOldestFrame]; + } + }); +} + +- (void)removeOldestFrame +{ + NSError *error; + if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath + error:&error]) { + SENTRY_LOG_DEBUG( + @"Could not delete replay frame at: %@. %@", _frames.firstObject.imagePath, error); + } + [_frames removeObjectAtIndex:0]; +} + +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + completion:(void (^)(BOOL success, NSError *error))completion +{ + // Set up AVAssetWriter with appropriate settings + AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL + fileType:AVFileTypeQuickTimeMovie + error:nil]; + + NSDictionary *videoSettings = @{ + AVVideoCodecKey : AVVideoCodecTypeH264, + AVVideoWidthKey : @(_videoSize.width), + AVVideoHeightKey : @(_videoSize.height), + AVVideoCompressionPropertiesKey : @ { + AVVideoAverageBitRateKey : @(_bitRate), + AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel, + }, + }; + + AVAssetWriterInput *videoWriterInput = + [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo + outputSettings:videoSettings]; + NSDictionary *bufferAttributes = @{ + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB), + }; + + AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput + sourcePixelBufferAttributes:bufferAttributes]; + + [videoWriter addInput:videoWriterInput]; + + // Start writing video + [videoWriter startWriting]; + [videoWriter startSessionAtSourceTime:kCMTimeZero]; + + NSDate *end = [beginning dateByAddingTimeInterval:duration]; + __block NSInteger frameCount = 0; + NSMutableArray *frames = [NSMutableArray array]; + for (SentryReplayFrame *frame in self->_frames) { + if ([frame.time compare:beginning] == NSOrderedAscending) { + continue; + ; + } else if ([frame.time compare:end] == NSOrderedDescending) { + break; + } + [frames addObject:frame.imagePath]; + } + + [videoWriterInput + requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue + usingBlock:^{ + UIImage *image = + [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { + if (completion) { + completion(NO, videoWriter.error); + } + } + } + + if (frameCount >= frames.count) { + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + completion(videoWriter.status + == AVAssetWriterStatusCompleted, + videoWriter.error); + } + }]; + } + }]; +} + +- (BOOL)appendPixelBufferForImage:(UIImage *)image + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime +{ + CVReturn status = kCVReturnSuccess; + + CVPixelBufferRef pixelBuffer = NULL; + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, + (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); + + if (status != kCVReturnSuccess) { + return NO; + } + + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); + + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, + (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, + (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); + + CGContextTranslateCTM(context, 0, image.size.height); + CGContextScaleCTM(context, 1.0, -1.0); + + UIGraphicsPushContext(context); + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + UIGraphicsPopContext(); + + CGColorSpaceRelease(rgbColorSpace); + CGContextRelease(context); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + + // Append the pixel buffer with the current image to the video + BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer + withPresentationTime:presentationTime]; + + CVPixelBufferRelease(pixelBuffer); + + return success; +} + +@end +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m new file mode 100644 index 00000000000..4b7aa7da009 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.m @@ -0,0 +1,190 @@ +#import "SentrySessionReplay.h" +#import "SentryAttachment+Private.h" +#import "SentryLog.h" +#import "SentryOndemandReplay.h" +#import "SentryReplaySettings+Private.h" +#import "SentryViewPhotographer.h" + +@implementation SentrySessionReplay { + UIView *_rootView; + BOOL _processingScreenshot; + CADisplayLink *_displayLink; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSURL *_urlToCache; + NSDate *_sessionStart; + SentryReplaySettings *_settings; + SentryOnDemandReplay *_replayMaker; + + NSMutableArray *imageCollection; +} + +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings +{ + if (self = [super init]) { + _settings = replaySettings; + } + return self; +} + +- (void)start:(UIView *)rootView fullSession:(BOOL)full +{ + if (rootView == nil) { + SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); + return; + } + + @synchronized(self) { + if (_displayLink == nil) { + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } else { + // Session display is already running. + return; + } + + _rootView = rootView; + _lastScreenShot = [[NSDate alloc] init]; + _videoSegmentStart = nil; + _sessionStart = _lastScreenShot; + + NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory + inDomains:NSUserDomainMask] + .firstObject URLByAppendingPathComponent:@"io.sentry"]; + + NSString *currentSession = [NSUUID UUID].UUIDString; + _urlToCache = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + + _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; + _replayMaker.bitRate = _settings.replayBitRate; + _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; + imageCollection = [NSMutableArray array]; + + NSLog(@"Recording session to %@", _urlToCache); + } +} + +- (void)stop +{ + [_displayLink invalidate]; + _displayLink = nil; +} + +- (NSArray *)processAttachments:(NSArray *)attachments + forEvent:(nonnull SentryEvent *)event +{ + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { + return attachments; + } + + NSLog(@"Recording session event id %@", event.eventId); + NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + + dispatch_group_t _wait_for_render = dispatch_group_create(); + + dispatch_group_enter(_wait_for_render); + [_replayMaker createVideoOf:30 + from:[NSDate dateWithTimeIntervalSinceNow:-30] + outputFileURL:finalPath + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + }]; + dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); + + SentryAttachment *attachment = [[SentryAttachment alloc] initWithPath:finalPath.path + filename:@"replay.mp4" + contentType:@"video/mp4"]; + + [result addObject:attachment]; + + return result; +} + +- (void)sendReplayForEvent:(SentryEvent *)event +{ +} + +- (void)newFrame:(CADisplayLink *)sender +{ + NSDate *now = [[NSDate alloc] init]; + + if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { + [self takeScreenshot]; + _lastScreenShot = now; + + if (_videoSegmentStart == nil) { + _videoSegmentStart = now; + } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { + [self prepareSegmentUntil:now]; + } + } +} + +- (void)prepareSegmentUntil:(NSDate *)date +{ + NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; + NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; + NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { + NSError *error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", + error.localizedDescription); + return; + } + } + + pathToSegment = [pathToSegment + URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + + dispatch_group_t _wait_for_render = dispatch_group_create(); + + dispatch_group_enter(_wait_for_render); + [_replayMaker createVideoOf:5 + from:[date dateByAddingTimeInterval:-5] + outputFileURL:pathToSegment + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + + // Need to send the segment here + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; +} + +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } + _processingScreenshot = YES; + } + + UIImage *screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; + + _processingScreenshot = NO; + + dispatch_queue_t backgroundQueue + = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrame:screenshot]; }); +} + +@end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m new file mode 100644 index 00000000000..f6953ae919c --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -0,0 +1,68 @@ +#import "SentrySessionReplayIntegration.h" +#import "SentryClient+Private.h" +#import "SentryDependencyContainer.h" +#import "SentryHub+Private.h" +#import "SentryOptions.h" +#import "SentryRandom.h" +#import "SentryReplaySettings.h" +#import "SentrySDK+Private.h" +#import "SentrySessionReplay.h" +#import "SentryUIApplication.h" + +@implementation SentrySessionReplayIntegration { + SentrySessionReplay *sessionReplay; +} + +- (BOOL)installWithOptions:(nonnull SentryOptions *)options +{ + if ([super installWithOptions:options] == NO) { + return NO; + } + + if (@available(iOS 16.0, *)) { + if (options.sessionReplaySettings.replaysSessionSampleRate == 0 + && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { + return NO; + } + + sessionReplay = + [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; + + [sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.sessionReplaySettings + .replaysSessionSampleRate]]; + + SentryClient *client = [SentrySDK.currentHub getClient]; + [client addAttachmentProcessor:sessionReplay]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + return YES; + } else { + return NO; + } +} + +- (void)stop +{ + [sessionReplay stop]; +} + +- (SentryIntegrationOption)integrationOptions +{ + return kIntegrationOptionEnableReplay; +} + +- (void)uninstall +{ +} + +- (BOOL)shouldReplayFullSession:(CGFloat)rate +{ + return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; +} + +@end diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m new file mode 100644 index 00000000000..698ba9c53f7 --- /dev/null +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -0,0 +1,154 @@ +#import "SentryViewPhotographer.h" + +#if SENTRY_HAS_UIKIT + +@implementation SentryViewPhotographer { + NSMutableArray *_ignoreClasses; + NSMutableArray *_redactClasses; +} + ++ (SentryViewPhotographer *)shared +{ + static SentryViewPhotographer *_shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ _shared = [[SentryViewPhotographer alloc] init]; }); + + return _shared; +} + +- (instancetype)init +{ + if (self = [super init]) { + _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; + + _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; + + NSArray *extraClasses = @[ + @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + @"SwiftUI._UIGraphicsView", @"SwiftUI.ImageLayer" + ]; + + for (NSString *className in extraClasses) { + Class viewClass = NSClassFromString(className); + if (viewClass != nil) { + [_redactClasses addObject:viewClass]; + } + } + } + return self; +} + +- (UIImage *)imageFromUIView:(UIView *)view +{ + UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + + [view.layer renderInContext:currentContext]; + + [self maskText:view context:currentContext]; + + UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return screenshot; +} + +- (void)maskText:(UIView *)view context:(CGContextRef)context +{ + [UIColor.blackColor setFill]; + CGPathRef maskPath = [self buildPathForView:view + inPath:CGPathCreateMutable() + visibleArea:view.frame]; + CGContextAddPath(context, maskPath); + CGContextFillPath(context); +} + +- (BOOL)shouldIgnoreView:(UIView *)view +{ + return [view isKindOfClass:UISwitch.class]; +} + +- (BOOL)shouldIgnore:(UIView *)view +{ + for (Class class in _ignoreClasses) { + if ([view isKindOfClass:class]) { + return true; + } + } + return false; +} + +- (BOOL)shouldRedact:(UIView *)view +{ + for (Class class in _redactClasses) { + if ([view isKindOfClass:class]) { + return true; + } + } + + return ( + [view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); +} + +- (BOOL)shouldRedactImageView:(UIImageView *)imageView +{ + return imageView.image != nil && + [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil + && (imageView.image.size.width > 10 + && imageView.image.size.height > 10); // This is to avoid redact gradient backgroud that + // are usually small lines repeating +} + +- (CGMutablePathRef)buildPathForView:(UIView *)view + inPath:(CGMutablePathRef)path + visibleArea:(CGRect)area +{ + CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; + + if (!CGRectIntersectsRect(area, rectInWindow)) { + return path; + } + + if (view.hidden || view.alpha == 0) { + return path; + } + + BOOL ignore = [self shouldIgnore:view]; + if (!ignore && [self shouldRedact:view]) { + CGPathAddRect(path, NULL, rectInWindow); + return path; + } else if ([self isOpaqueOrHasBackground:view]) { + CGMutablePathRef newPath = [self excludeRect:rectInWindow fromPath:path]; + CGPathRelease(path); + path = newPath; + } + + if (!ignore) { + for (UIView *subview in view.subviews) { + path = [self buildPathForView:subview inPath:path visibleArea:area]; + } + } + + return path; +} + +- (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ + if (@available(iOS 16.0, *)) { + CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); + CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); + return CGPathCreateMutableCopy(newPath); + } + return path; +} + +- (BOOL)isOpaqueOrHasBackground:(UIView *)view +{ + return view.isOpaque + || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); +} + +@end + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h new file mode 100644 index 00000000000..e1e9127cf77 --- /dev/null +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -0,0 +1,32 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT +# import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryOnDemandReplay : NSObject + +@property (nonatomic) NSInteger bitRate; + +@property (nonatomic) NSUInteger cacheMaxSize; + +- (instancetype)initWithOutputPath:(NSString *)outputPath; + +- (void)addFrame:(UIImage *)image; + +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + completion:(void (^)(BOOL success, NSError *error))completion; + +/** + * Remove cached frames until given date. + */ +- (void)releaseFramesUntil:(NSDate *)date; + +@end + +NS_ASSUME_NONNULL_END +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h new file mode 100644 index 00000000000..d8437c32c71 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -0,0 +1,26 @@ +#import "SentryClient+Private.h" +#import "SentryEvent.h" +#import "SentryReplaySettings.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplay : NSObject + +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings; + +/** + * Start recording the session using rootView as image source. + * If full is @c YES, we transmit the entire session to sentry. + */ +- (void)start:(UIView *)rootView fullSession:(BOOL)full; + +/** + * Stop recording the session replay + */ +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h new file mode 100644 index 00000000000..b789b91009a --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -0,0 +1,11 @@ +#import "SentryBaseIntegration.h" +#import "SentryIntegrationProtocol.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplayIntegration : SentryBaseIntegration + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h new file mode 100644 index 00000000000..2425af8794c --- /dev/null +++ b/Sources/Sentry/include/SentryViewPhotographer.h @@ -0,0 +1,19 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT +# import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryViewPhotographer : NSObject + +@property (nonatomic, readonly, class) SentryViewPhotographer *shared; + +- (UIImage *)imageFromUIView:(UIView *)view; + +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT From cc5eadf7120b82f6f3bf7011b4c114f78538a2e5 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 10:00:07 +0100 Subject: [PATCH 24/88] Fixing UIKIT references --- Sources/Sentry/SentrySessionReplay.m | 13 +++++++++++-- Sources/Sentry/SentrySessionReplayIntegration.m | 9 ++++++++- Sources/Sentry/SentryViewPhotographer.m | 2 ++ Sources/Sentry/include/SentrySessionReplay.h | 6 +++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 4b7aa7da009..f8b5af231e1 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -5,6 +5,10 @@ #import "SentryReplaySettings+Private.h" #import "SentryViewPhotographer.h" +#if SENTRY_HAS_UIKIT + +NS_ASSUME_NONNULL_BEGIN + @implementation SentrySessionReplay { UIView *_rootView; BOOL _processingScreenshot; @@ -77,8 +81,9 @@ - (void)stop _displayLink = nil; } -- (NSArray *)processAttachments:(NSArray *)attachments - forEvent:(nonnull SentryEvent *)event +- (nullable NSArray *)processAttachments: + (nullable NSArray *)attachments + forEvent:(nonnull SentryEvent *)event { if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { return attachments; @@ -188,3 +193,7 @@ - (void)takeScreenshot } @end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index f6953ae919c..af6b766de76 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -7,7 +7,11 @@ #import "SentryReplaySettings.h" #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" -#import "SentryUIApplication.h" + +#if SENTRY_HAS_UIKIT +# import "SentryUIApplication.h" + +NS_ASSUME_NONNULL_BEGIN @implementation SentrySessionReplayIntegration { SentrySessionReplay *sessionReplay; @@ -66,3 +70,6 @@ - (BOOL)shouldReplayFullSession:(CGFloat)rate } @end +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index 698ba9c53f7..5c784390d8c 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -1,6 +1,7 @@ #import "SentryViewPhotographer.h" #if SENTRY_HAS_UIKIT +NS_ASSUME_NONNULL_BEGIN @implementation SentryViewPhotographer { NSMutableArray *_ignoreClasses; @@ -151,4 +152,5 @@ - (BOOL)isOpaqueOrHasBackground:(UIView *)view @end +NS_ASSUME_NONNULL_END #endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index d8437c32c71..41350a3b51d 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -2,7 +2,10 @@ #import "SentryEvent.h" #import "SentryReplaySettings.h" #import -#import + +#if SENTRY_HAS_UIKIT + +# import NS_ASSUME_NONNULL_BEGIN @@ -24,3 +27,4 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END +#endif // SENTRY_HAS_UIKIT From 77066c741c4bd3b8dd360df300330e03f30d51bc Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 13:20:45 +0100 Subject: [PATCH 25/88] fixing for tvos --- Sources/Sentry/Public/SentryReplaySettings.h | 4 ++-- Sources/Sentry/SentryBaseIntegration.m | 2 +- Sources/Sentry/SentryOptions.m | 2 +- Sources/Sentry/SentryReplaySettings.m | 4 ++++ Tests/SentryTests/SentryOptionsTest.m | 6 +++--- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h index 16455041d7e..88f88d54693 100644 --- a/Sources/Sentry/Public/SentryReplaySettings.h +++ b/Sources/Sentry/Public/SentryReplaySettings.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN * to the default. * @note The default is @c 0. */ -@property (nonatomic) CGFloat replaysSessionSampleRate; +@property (nonatomic) float replaysSessionSampleRate; /** * Indicates the percentage in which a 30 seconds replay will be send with error events. @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN * to the default. * @note The default is @c 0. */ -@property (nonatomic) CGFloat replaysOnErrorSampleRate; +@property (nonatomic) float replaysOnErrorSampleRate; /** * Inittialize the settings of session replay diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index cdd47fb0e0f..4db26c0997d 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -143,7 +143,7 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options } if (integrationOptions & kIntegrationOptionEnableReplay) { - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { if (options.sessionReplaySettings.replaysOnErrorSampleRate == 0 && options.sessionReplaySettings.replaysSessionSampleRate == 0) { [self logWithOptionName:@"sessionReplaySettings"]; diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 8c1b27395c7..65c8a83312b 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -410,7 +410,7 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { if ([options[@"sessionReplaySettings"] isKindOfClass:NSDictionary.class]) { self.sessionReplaySettings = [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m index 82a2113fad4..41c7273d34f 100644 --- a/Sources/Sentry/SentryReplaySettings.m +++ b/Sources/Sentry/SentryReplaySettings.m @@ -1,5 +1,7 @@ #import "SentryReplaySettings.h" +NS_ASSUME_NONNULL_BEGIN + @interface SentryReplaySettings () @@ -45,3 +47,5 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index e309f1435cb..142c1f4e981 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -602,7 +602,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(options.enableUserInteractionTracing, YES); XCTAssertEqual(options.enablePreWarmedAppStartTracing, NO); XCTAssertEqual(options.attachViewHierarchy, NO); - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { XCTAssertNil(options.sessionReplaySettings); } #endif @@ -780,7 +780,7 @@ - (void)testEnablePreWarmedAppStartTracking - (void)testSessionReplaySettingsInit { - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { SentryOptions *options = [self getValidOptions:@{ @"sessionReplaySettings" : @ { @"replaysSessionSampleRate" : @2, @"replaysOnErrorSampleRate" : @4 } @@ -792,7 +792,7 @@ - (void)testSessionReplaySettingsInit - (void)testSessionReplaySettingsDefaults { - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { SentryOptions *options = [self getValidOptions:@{ @"sessionReplaySettings" : @ {} }]; XCTAssertEqual(options.sessionReplaySettings.replaysSessionSampleRate, 0); XCTAssertEqual(options.sessionReplaySettings.replaysOnErrorSampleRate, 0); From e5e5b125b47ee15e223e017403ef9ed588364bab Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 13:24:52 +0100 Subject: [PATCH 26/88] more fixes --- Sources/Sentry/Public/SentryReplaySettings.h | 4 ++-- Sources/Sentry/SentryReplaySettings.m | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h index 88f88d54693..ff524b79f06 100644 --- a/Sources/Sentry/Public/SentryReplaySettings.h +++ b/Sources/Sentry/Public/SentryReplaySettings.h @@ -30,8 +30,8 @@ NS_ASSUME_NONNULL_BEGIN * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with * error events. */ -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate - replaysOnErrorSampleRate:(CGFloat)errorSampleRate; +- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate + replaysOnErrorSampleRate:(float)errorSampleRate; @end diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m index 41c7273d34f..6bb9994b8b0 100644 --- a/Sources/Sentry/SentryReplaySettings.m +++ b/Sources/Sentry/SentryReplaySettings.m @@ -21,8 +21,8 @@ - (instancetype)init return self; } -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate - replaysOnErrorSampleRate:(CGFloat)errorSampleRate +- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate + replaysOnErrorSampleRate:(float)errorSampleRate { if (self = [self init]) { self.replaysSessionSampleRate = sessionSampleRate; From 939a6c107d6ae55aceefa43ef3dd2ed09c56f5ec Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 13:30:48 +0100 Subject: [PATCH 27/88] Update Sentry.h --- Sources/Sentry/Public/Sentry.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index afaa72b0986..580e822c4ea 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -30,6 +30,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; #import "SentryMessage.h" #import "SentryNSError.h" #import "SentryOptions.h" +#import "SentryReplaySettings.h" #import "SentryRequest.h" #import "SentrySDK.h" #import "SentrySampleDecision.h" From 837c211832ea40a21c6b6d5fef3a98ae8b11e7d0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 21 Feb 2024 13:37:45 +0100 Subject: [PATCH 28/88] Update project.pbxproj --- Sentry.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index bf8dffc99bf..340483e8ce6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -764,7 +764,7 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; - D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */; }; + D83D07812B7E5EFA00CC9674 /* SentryReplaySettings.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplaySettings.h */; settings = {ATTRIBUTES = (Public, ); }; }; D83D07832B7E5F2100CC9674 /* SentryReplaySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplaySettings.m */; }; D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; From 77f6e42715b1c0003ec629af2b17756baf1271e6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 22 Feb 2024 13:29:29 +0100 Subject: [PATCH 29/88] fix for tvOS --- Sources/Sentry/SentrySessionReplayIntegration.m | 2 +- Sources/Sentry/SentryViewPhotographer.m | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index af6b766de76..086b4fb3642 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -23,7 +23,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { if (options.sessionReplaySettings.replaysSessionSampleRate == 0 && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { return NO; diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index 5c784390d8c..3539dd37e88 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -20,8 +20,9 @@ + (SentryViewPhotographer *)shared - (instancetype)init { if (self = [super init]) { - _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; - +#if TARGET_OS_IOS + _ignoreClasses = @[ UISlider.class, UISwitch.class].mutableCopy; +#endif _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; NSArray *extraClasses = @[ @@ -67,7 +68,7 @@ - (void)maskText:(UIView *)view context:(CGContextRef)context - (BOOL)shouldIgnoreView:(UIView *)view { - return [view isKindOfClass:UISwitch.class]; + return [_ignoreClasses containsObject:view.class]; } - (BOOL)shouldIgnore:(UIView *)view @@ -136,7 +137,7 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view - (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { - if (@available(iOS 16.0, *)) { + if (@available(iOS 16.0,tvOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); From 6f07d9e74e373f5763c6b2e54474cb113ca4b74e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 22 Feb 2024 12:30:26 +0000 Subject: [PATCH 30/88] Format code --- Sources/Sentry/SentryViewPhotographer.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index 3539dd37e88..63fe7768d00 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -20,9 +20,9 @@ + (SentryViewPhotographer *)shared - (instancetype)init { if (self = [super init]) { -#if TARGET_OS_IOS - _ignoreClasses = @[ UISlider.class, UISwitch.class].mutableCopy; -#endif +# if TARGET_OS_IOS + _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; +# endif _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; NSArray *extraClasses = @[ @@ -137,7 +137,7 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view - (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { - if (@available(iOS 16.0,tvOS 16.0, *)) { + if (@available(iOS 16.0, tvOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); From 39f8f3d79be707f58a0fdf56ac3109123db5f237 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 10:13:50 +0100 Subject: [PATCH 31/88] adjust options --- Sources/Sentry/SentrySessionReplay.m | 10 +++++----- Sources/Sentry/SentrySessionReplayIntegration.m | 10 +++++----- Sources/Sentry/include/SentrySessionReplay.h | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index f8b5af231e1..c2c76075e8e 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -2,7 +2,7 @@ #import "SentryAttachment+Private.h" #import "SentryLog.h" #import "SentryOndemandReplay.h" -#import "SentryReplaySettings+Private.h" +#import "SentryReplayOptions+Private.h" #import "SentryViewPhotographer.h" #if SENTRY_HAS_UIKIT @@ -17,16 +17,16 @@ @implementation SentrySessionReplay { NSDate *_videoSegmentStart; NSURL *_urlToCache; NSDate *_sessionStart; - SentryReplaySettings *_settings; + SentryReplayOptions *_replayOptions; SentryOnDemandReplay *_replayMaker; NSMutableArray *imageCollection; } -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions { if (self = [super init]) { - _settings = replaySettings; + _replayOptions = replayOptions; } return self; } @@ -67,7 +67,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full } _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; - _replayMaker.bitRate = _settings.replayBitRate; + _replayMaker.bitRate = _replayOptions.replayBitRate; _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; imageCollection = [NSMutableArray array]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 086b4fb3642..e7ec926951e 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -4,7 +4,7 @@ #import "SentryHub+Private.h" #import "SentryOptions.h" #import "SentryRandom.h" -#import "SentryReplaySettings.h" +#import "SentryReplayOptions.h" #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" @@ -24,17 +24,17 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options } if (@available(iOS 16.0, tvOS 16.0, *)) { - if (options.sessionReplaySettings.replaysSessionSampleRate == 0 - && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { + if (options.sessionReplayOptions.replaysSessionSampleRate == 0 + && options.sessionReplayOptions.replaysOnErrorSampleRate == 0) { return NO; } sessionReplay = - [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; + [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplaySettings + fullSession:[self shouldReplayFullSession:options.sessionReplayOptions .replaysSessionSampleRate]]; SentryClient *client = [SentrySDK.currentHub getClient]; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 41350a3b51d..0260a5791a9 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -1,6 +1,6 @@ #import "SentryClient+Private.h" #import "SentryEvent.h" -#import "SentryReplaySettings.h" +#import "SentryReplayOptions.h" #import #if SENTRY_HAS_UIKIT @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SentrySessionReplay : NSObject -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings; +- (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; /** * Start recording the session using rootView as image source. From 19b1cec907f5f1521b70ee26f233ecb01aff07dd Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 5 Mar 2024 09:15:17 +0000 Subject: [PATCH 32/88] Format code --- Sources/Sentry/SentrySessionReplayIntegration.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index e7ec926951e..7df58484019 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -29,8 +29,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - sessionReplay = - [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; + sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject From 78eb6bb1dfd868e037d2be94bb06d548638b8114 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 10:28:39 +0100 Subject: [PATCH 33/88] Update SentryViewPhotographer.m --- Sources/Sentry/SentryViewPhotographer.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index 63fe7768d00..7cfe34c3ae0 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -1,6 +1,8 @@ #import "SentryViewPhotographer.h" #if SENTRY_HAS_UIKIT +#import + NS_ASSUME_NONNULL_BEGIN @implementation SentryViewPhotographer { From 98ba2509321740fe631de49a7c3c4fb65aa67f1e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 5 Mar 2024 09:29:52 +0000 Subject: [PATCH 34/88] Format code --- Sources/Sentry/SentryViewPhotographer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index 7cfe34c3ae0..188ab718ab9 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -1,7 +1,7 @@ #import "SentryViewPhotographer.h" #if SENTRY_HAS_UIKIT -#import +# import NS_ASSUME_NONNULL_BEGIN From 369c117fbfb516dde533ac061d7deb9a5ad0dcbd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 10:43:42 +0100 Subject: [PATCH 35/88] Some fixes --- Sources/Sentry/Public/SentryReplaySettings.h | 38 --------------- Sources/Sentry/SentryReplaySettings.m | 51 -------------------- Sources/Sentry/SentrySessionReplay.m | 2 +- 3 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 Sources/Sentry/Public/SentryReplaySettings.h delete mode 100644 Sources/Sentry/SentryReplaySettings.m diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h deleted file mode 100644 index ff524b79f06..00000000000 --- a/Sources/Sentry/Public/SentryReplaySettings.h +++ /dev/null @@ -1,38 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryReplaySettings : NSObject - -/** - * Indicates the percentage in which the replay for the session will be created. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it - * to the default. - * @note The default is @c 0. - */ -@property (nonatomic) float replaysSessionSampleRate; - -/** - * Indicates the percentage in which a 30 seconds replay will be send with error events. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it - * to the default. - * @note The default is @c 0. - */ -@property (nonatomic) float replaysOnErrorSampleRate; - -/** - * Inittialize the settings of session replay - * - * @param sessionSampleRate Indicates the percentage in which the replay for the session will be - * created. - * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with - * error events. - */ -- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate - replaysOnErrorSampleRate:(float)errorSampleRate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m deleted file mode 100644 index 6bb9994b8b0..00000000000 --- a/Sources/Sentry/SentryReplaySettings.m +++ /dev/null @@ -1,51 +0,0 @@ -#import "SentryReplaySettings.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface -SentryReplaySettings () - -@property (nonatomic) NSInteger replayBitRate; - -@end - -@implementation SentryReplaySettings - -- (instancetype)init -{ - if (self = [super init]) { - self.replaysSessionSampleRate = 0; - self.replaysOnErrorSampleRate = 0; - self.replayBitRate = 20000; - } - return self; -} - -- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate - replaysOnErrorSampleRate:(float)errorSampleRate -{ - if (self = [self init]) { - self.replaysSessionSampleRate = sessionSampleRate; - self.replaysOnErrorSampleRate = errorSampleRate; - } - - return self; -} - -- (instancetype)initWithDictionary:(NSDictionary *)dictionary -{ - if (self = [self init]) { - if ([dictionary[@"replaysSessionSampleRate"] isKindOfClass:NSNumber.class]) { - self.replaysSessionSampleRate = [dictionary[@"replaysSessionSampleRate"] floatValue]; - } - - if ([dictionary[@"replaysOnErrorSampleRate"] isKindOfClass:NSNumber.class]) { - self.replaysOnErrorSampleRate = [dictionary[@"replaysOnErrorSampleRate"] floatValue]; - } - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index c2c76075e8e..5c5566b3dbe 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,7 +1,7 @@ #import "SentrySessionReplay.h" #import "SentryAttachment+Private.h" #import "SentryLog.h" -#import "SentryOndemandReplay.h" +#import "SentryOnDemandReplay.h" #import "SentryReplayOptions+Private.h" #import "SentryViewPhotographer.h" From 46f04d840ad301158a41f3df1315cb66edb79daa Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 13:07:47 +0100 Subject: [PATCH 36/88] tying everything --- Sentry.xcodeproj/project.pbxproj | 32 +++++++-- Sources/Sentry/SentryOnDemandReplay.m | 71 +++++++++++-------- Sources/Sentry/SentrySessionReplay.m | 70 ++++++++++-------- .../SentryReplaySettings+Private.h | 20 ------ Sources/Sentry/include/SentryOnDemandReplay.h | 13 +++- .../SessionReplay/SentryVideoInfo.swift | 4 ++ 6 files changed, 120 insertions(+), 90 deletions(-) delete mode 100644 Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 5d580cfc7f1..3518e1d1d59 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -768,6 +768,8 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; + D83D07812B7E5EFA00CC9674 /* SentryReplayOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D83D07832B7E5F2100CC9674 /* SentryReplayOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */; }; D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; @@ -776,8 +778,6 @@ D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */; }; D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */; }; D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */; }; - D83D07812B7E5EFA00CC9674 /* SentryReplayOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D83D07832B7E5F2100CC9674 /* SentryReplayOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */; }; D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; @@ -856,6 +856,7 @@ D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB742D294B294B00A5F964 /* MockUIScene.m */; }; D8CCFC632A1520C900DE232E /* SentryBinaryImageCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */; }; D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; + D8F016CD2B9741E0007B9AFB /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */; }; D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; @@ -1784,6 +1785,9 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; + D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplayOptions.h; path = Public/SentryReplayOptions.h; sourceTree = ""; }; + D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplayOptions.m; sourceTree = ""; }; + D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplayOptions+Private.h"; path = "include/HybridPublic/SentryReplayOptions+Private.h"; sourceTree = ""; }; D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; @@ -1792,9 +1796,6 @@ D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; - D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplayOptions.h; path = Public/SentryReplayOptions.h; sourceTree = ""; }; - D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplayOptions.m; sourceTree = ""; }; - D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplayOptions+Private.h"; path = "include/HybridPublic/SentryReplayOptions+Private.h"; sourceTree = ""; }; D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; @@ -1876,6 +1877,7 @@ D8CB742D294B294B00A5F964 /* MockUIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockUIScene.m; sourceTree = ""; }; D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBinaryImageCacheTests.m; sourceTree = ""; }; D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegrationObjCTests.m; sourceTree = ""; }; + D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; D8F01DE42A126B62008F4996 /* HybridPod.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = HybridPod.podspec; sourceTree = ""; }; D8F01DE52A126BF5008F4996 /* HybridTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridTest.swift; sourceTree = ""; }; D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; @@ -3414,6 +3416,7 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + D8F016CA2B97419A007B9AFB /* Integrations */, D856272A2A374A6800FB8062 /* Tools */, 7BF65060292B8EFE00BBA5A8 /* MetricKit */, D81A349B291D0C0B005A27A9 /* Sentry.swift */, @@ -3449,8 +3452,6 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D88D6C1F2B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.h */, - D88D6C202B7BA25400C8C633 /* SentryReplayEnvelopeItemHeader.m */, D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */, D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */, D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */, @@ -3603,6 +3604,22 @@ path = Resources; sourceTree = ""; }; + D8F016CA2B97419A007B9AFB /* Integrations */ = { + isa = PBXGroup; + children = ( + D8F016CB2B9741AA007B9AFB /* SessionReplay */, + ); + path = Integrations; + sourceTree = ""; + }; + D8F016CB2B9741AA007B9AFB /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; D8F01DE32A125D7B008F4996 /* HybridSDKTest */ = { isa = PBXGroup; children = ( @@ -4444,6 +4461,7 @@ D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */, 63FE711520DA4C1000CDBAE8 /* SentryCrashJSONCodec.c in Sources */, D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */, + D8F016CD2B9741E0007B9AFB /* SentryVideoInfo.swift in Sources */, 03F84D3327DD4191008FE43F /* SentryMachLogging.cpp in Sources */, 84F993C42A62A74000EC0190 /* SentryCurrentDateProvider.m in Sources */, D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */, diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index e6302a5b4dc..aba0f496a5f 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -25,6 +25,10 @@ - (instancetype)initWithPath:(NSString *)path time:(NSDate *)time @end +@implementation SentryVideoInfo + +@end + @implementation SentryOnDemandReplay { NSString *_outputPath; NSDate *_startTime; @@ -39,7 +43,6 @@ - (instancetype)initWithOutputPath:(NSString *)outputPath _outputPath = outputPath; _startTime = [[NSDate alloc] init]; _frames = [NSMutableArray array]; - //_videoSize = CGSizeMake(300, 651); _videoSize = CGSizeMake(200, 434); _bitRate = 20000; _cacheMaxSize = NSUIntegerMax; @@ -110,7 +113,7 @@ - (void)removeOldestFrame - (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion + completion:(void (^)(SentryVideoInfo *, NSError *error))completion { // Set up AVAssetWriter with appropriate settings AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL @@ -150,41 +153,47 @@ - (void)createVideoOf:(NSTimeInterval)duration for (SentryReplayFrame *frame in self->_frames) { if ([frame.time compare:beginning] == NSOrderedAscending) { continue; - ; } else if ([frame.time compare:end] == NSOrderedDescending) { break; } [frames addObject:frame.imagePath]; } - + [videoWriterInput - requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue - usingBlock:^{ - UIImage *image = - [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); - - if (![self appendPixelBufferForImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { - if (completion) { - completion(NO, videoWriter.error); - } - } - } - - if (frameCount >= frames.count) { - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - completion(videoWriter.status - == AVAssetWriterStatusCompleted, - videoWriter.error); - } - }]; - } - }]; + requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue + usingBlock:^{ + UIImage *image = + [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { + if (completion) { + completion(nil, videoWriter.error); + } + } + } + + if (frameCount >= frames.count) { + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + SentryVideoInfo * videoInfo = nil; + if (videoWriter.status == AVAssetWriterStatusCompleted) { + videoInfo = [[SentryVideoInfo alloc] init]; + videoInfo.height = (int)self->_videoSize.height; + videoInfo.width = (int)self->_videoSize.width; + videoInfo.frameCount = (int)frames.count; + videoInfo.frameRate = 1; + videoInfo.duration = frames.count; + } + completion(videoInfo, videoWriter.error); + } + }]; + } + }]; } - (BOOL)appendPixelBufferForImage:(UIImage *)image diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 5c5566b3dbe..8e03088983d 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -4,6 +4,11 @@ #import "SentryOnDemandReplay.h" #import "SentryReplayOptions+Private.h" #import "SentryViewPhotographer.h" +#import "SentrySDK+Private.h" +#import "SentryHub+Private.h" +#import "SentryReplayEvent.h" +#import "SentryId.h" +#import "SentryReplayRecording.h" #if SENTRY_HAS_UIKIT @@ -19,7 +24,7 @@ @implementation SentrySessionReplay { NSDate *_sessionStart; SentryReplayOptions *_replayOptions; SentryOnDemandReplay *_replayMaker; - + SentryId * sessionReplayId; NSMutableArray *imageCollection; } @@ -72,6 +77,10 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full imageCollection = [NSMutableArray array]; NSLog(@"Recording session to %@", _urlToCache); + + if (full) { + sessionReplayId = [[SentryId alloc] init]; + } } } @@ -81,6 +90,7 @@ - (void)stop _displayLink = nil; } +// TODO: dont use processAttachments to capture replay - (nullable NSArray *)processAttachments: (nullable NSArray *)attachments forEvent:(nonnull SentryEvent *)event @@ -89,29 +99,26 @@ - (void)stop return attachments; } - NSLog(@"Recording session event id %@", event.eventId); - NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; - NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - - dispatch_group_t _wait_for_render = dispatch_group_create(); - - dispatch_group_enter(_wait_for_render); + NSDate * replayStart = [NSDate dateWithTimeIntervalSinceNow:-30]; + [_replayMaker createVideoOf:30 - from:[NSDate dateWithTimeIntervalSinceNow:-30] + from:replayStart outputFileURL:finalPath - completion:^(BOOL success, NSError *_Nonnull error) { - dispatch_group_leave(_wait_for_render); - }]; - dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); - - SentryAttachment *attachment = [[SentryAttachment alloc] initWithPath:finalPath.path - filename:@"replay.mp4" - contentType:@"video/mp4"]; - - [result addObject:attachment]; - - return result; + completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { + SentryReplayEvent * replayEvent = [[SentryReplayEvent alloc] init]; + replayEvent.replayType = kSentryReplayTypeBuffer; + replayEvent.replayId = [[SentryId alloc] init]; + replayEvent.replayStartTimestamp = replayStart; + replayEvent.segmentId = 1; + + + SentryReplayRecording * recording = [[SentryReplayRecording alloc] initWithSegmentId:1 size:1 start:replayStart duration:videoInfo.duration frameCount:videoInfo.frameCount frameRate:videoInfo.frameRate height:videoInfo.height width:videoInfo.width]; + + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:finalPath]; + }]; + + return attachments; } - (void)sendReplayForEvent:(SentryEvent *)event @@ -155,20 +162,21 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - dispatch_group_t _wait_for_render = dispatch_group_create(); - - dispatch_group_enter(_wait_for_render); [_replayMaker createVideoOf:5 from:[date dateByAddingTimeInterval:-5] outputFileURL:pathToSegment - completion:^(BOOL success, NSError *_Nonnull error) { - dispatch_group_leave(_wait_for_render); - - // Need to send the segment here + completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { + + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; + + +} - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; +- (void) captureSegment:(NSURL *)filePath { + } - (void)takeScreenshot diff --git a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h deleted file mode 100644 index 8aae0a4b713..00000000000 --- a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h +++ /dev/null @@ -1,20 +0,0 @@ -#import "SentryReplaySettings.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface -SentryReplaySettings (Private) - -/** - * Defines the quality of the session replay. - * Higher bit rates better quality, but also bigger files to transfer. - * @note The default value is @c 20000; - */ -@property (nonatomic) NSInteger replayBitRate; - -- (instancetype)initWithDictionary:(NSDictionary *)dictionary; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index e1e9127cf77..02de6dc5b88 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -6,6 +6,17 @@ NS_ASSUME_NONNULL_BEGIN +@interface SentryVideoInfo : NSObject + +@property (nonatomic) int height; +@property (nonatomic) int width; +@property (nonatomic) NSTimeInterval duration; +@property (nonatomic) int frameCount; +@property (nonatomic) int frameRate; + +@end + + @interface SentryOnDemandReplay : NSObject @property (nonatomic) NSInteger bitRate; @@ -19,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion; + completion:(void (^)(SentryVideoInfo *, NSError *error))completion; /** * Remove cached frames until given date. diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift new file mode 100644 index 00000000000..1cbb335c8f8 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -0,0 +1,4 @@ +import Foundation + + + From 68dc4cd3273ac80ef0a394c7220b0f10d7c1e50b Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 6 Mar 2024 11:25:21 +0100 Subject: [PATCH 37/88] WIP --- Package.swift | 14 +++-- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 10 +++- Sentry.xcodeproj/project.pbxproj | 16 ++--- Sources/Sentry/Public/Sentry.h | 1 - Sources/Sentry/Public/SentryReplayOptions.h | 38 ------------ Sources/Sentry/SentryBaseIntegration.m | 2 +- Sources/Sentry/SentryClient.m | 5 ++ Sources/Sentry/SentryDateUtil.m | 4 +- Sources/Sentry/SentryOnDemandReplay.m | 11 +--- Sources/Sentry/SentryOptions.m | 4 +- Sources/Sentry/SentryReplayEvent.m | 2 +- Sources/Sentry/SentryReplayOptions.m | 51 ---------------- Sources/Sentry/SentryReplayRecording.m | 2 +- Sources/Sentry/SentrySessionReplay.m | 33 ++++++----- .../Sentry/SentrySessionReplayIntegration.m | 2 +- .../SentryReplayOptions+Private.h | 20 ------- Sources/Sentry/include/SentryDateUtil.h | 2 +- Sources/Sentry/include/SentryOnDemandReplay.h | 13 +--- Sources/Sentry/include/SentrySessionReplay.h | 2 +- .../SessionReplay/SentryReplayOptions.swift | 59 +++++++++++++++++++ .../SessionReplay/SentryVideoInfo.swift | 20 ++++++- .../Helper/SentryDateUtilTests.swift | 2 +- 22 files changed, 133 insertions(+), 180 deletions(-) delete mode 100644 Sources/Sentry/Public/SentryReplayOptions.h delete mode 100644 Sources/Sentry/SentryReplayOptions.m delete mode 100644 Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift diff --git a/Package.swift b/Package.swift index dc9cb29114b..1f9b2083be1 100644 --- a/Package.swift +++ b/Package.swift @@ -10,11 +10,15 @@ let package = Package( .library(name: "SentrySwiftUI", targets: ["Sentry", "SentrySwiftUI"]) ], targets: [ - .binaryTarget( - name: "Sentry", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry.xcframework.zip", - checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Static - ), + +.binaryTarget( + name: "", + url: "", + checksum: "" +) + + , + .binaryTarget( name: "Sentry-Dynamic", url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry-Dynamic.xcframework.zip", diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 26cd244d1bd..5cd63ca279d 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -14,13 +14,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let dsn = DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN DSNStorage.shared.saveDSN(dsn: dsn) - SentrySDK.start { options in + SentrySDK.start(configureOptions: { options in options.dsn = dsn options.beforeSend = { event in return event } options.debug = true + if #available(iOS 16.0, *) { + options.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1) + } + if #available(iOS 15.0, *) { options.enableMetricKit = true } @@ -57,7 +61,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.sessionTrackingIntervalMillis = 5_000 options.attachScreenshot = true options.attachViewHierarchy = true - + #if targetEnvironment(simulator) options.enableSpotlight = true options.environment = "test-app" @@ -126,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } return scope } - } + }) } //swiftlint:enable function_body_length diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 376a7f8d8eb..7bbfac9b4d6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -767,8 +767,6 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; - D83D07812B7E5EFA00CC9674 /* SentryReplayOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D83D07832B7E5F2100CC9674 /* SentryReplayOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */; }; D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; @@ -855,6 +853,7 @@ D8CCFC632A1520C900DE232E /* SentryBinaryImageCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */; }; D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; D8F016CD2B9741E0007B9AFB /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */; }; + D8F016DE2B975345007B9AFB /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */; }; D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; @@ -1752,9 +1751,6 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; - D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryReplayOptions.h; path = Public/SentryReplayOptions.h; sourceTree = ""; }; - D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryReplayOptions.m; sourceTree = ""; }; - D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryReplayOptions+Private.h"; path = "include/HybridPublic/SentryReplayOptions+Private.h"; sourceTree = ""; }; D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; @@ -1809,9 +1805,9 @@ D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = include/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayRecording.h; path = include/SentryReplayRecording.h; sourceTree = ""; }; D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayRecording.m; sourceTree = ""; }; - D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; @@ -1848,6 +1844,7 @@ D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBinaryImageCacheTests.m; sourceTree = ""; }; D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegrationObjCTests.m; sourceTree = ""; }; D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; + D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8F01DE42A126B62008F4996 /* HybridPod.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = HybridPod.podspec; sourceTree = ""; }; D8F01DE52A126BF5008F4996 /* HybridTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridTest.swift; sourceTree = ""; }; D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; @@ -3420,9 +3417,6 @@ D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */, D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */, D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */, - D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */, - D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */, - D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */, ); name = SessionReplay; sourceTree = ""; @@ -3577,6 +3571,7 @@ isa = PBXGroup; children = ( D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */, + D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3677,7 +3672,6 @@ 03F84D2727DD414C008FE43F /* SentryMachLogging.hpp in Headers */, 63295AF51EF3C7DB002D4490 /* NSDictionary+SentrySanitize.h in Headers */, 8E4A037825F6F52100000D77 /* SentrySampleDecision.h in Headers */, - D83D07812B7E5EFA00CC9674 /* SentryReplayOptions.h in Headers */, 63FE717920DA4C1100CDBAE8 /* SentryCrashReportStore.h in Headers */, 0AAE202128ED9BCC00D0CD80 /* SentryReachability.h in Headers */, D858FA662A29EAB3002A3503 /* SentryBinaryImageCache.h in Headers */, @@ -4237,6 +4231,7 @@ 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, 630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */, 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */, + D8F016DE2B975345007B9AFB /* SentryReplayOptions.swift in Sources */, 7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */, 639FCFA11EBC804600778193 /* SentryException.m in Sources */, D80CD8D42B75144B002F710B /* SwiftDescriptor.swift in Sources */, @@ -4260,7 +4255,6 @@ 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, 63FE712120DA4C1000CDBAE8 /* SentryCrashSymbolicator.c in Sources */, - D83D07832B7E5F2100CC9674 /* SentryReplayOptions.m in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index ac23d1d73d9..afaa72b0986 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -30,7 +30,6 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; #import "SentryMessage.h" #import "SentryNSError.h" #import "SentryOptions.h" -#import "SentryReplayOptions.h" #import "SentryRequest.h" #import "SentrySDK.h" #import "SentrySampleDecision.h" diff --git a/Sources/Sentry/Public/SentryReplayOptions.h b/Sources/Sentry/Public/SentryReplayOptions.h deleted file mode 100644 index 3220a5f8c91..00000000000 --- a/Sources/Sentry/Public/SentryReplayOptions.h +++ /dev/null @@ -1,38 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryReplayOptions : NSObject - -/** - * Indicates the percentage in which the replay for the session will be created. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it - * to the default. - * @note The default is @c 0. - */ -@property (nonatomic) float replaysSessionSampleRate; - -/** - * Indicates the percentage in which a 30 seconds replay will be send with error events. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it - * to the default. - * @note The default is @c 0. - */ -@property (nonatomic) float replaysOnErrorSampleRate; - -/** - * Inittialize the settings of session replay - * - * @param sessionSampleRate Indicates the percentage in which the replay for the session will be - * created. - * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with - * error events. - */ -- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate - replaysOnErrorSampleRate:(float)errorSampleRate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 40b4cf97520..f5e04831fd1 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,10 +1,10 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" -#import "SentryReplayOptions.h" #import #import #import +#import "SentrySwift.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 0cd79aa658a..63a7e2ffd0d 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -509,6 +509,11 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] items:@[ videoEnvelopeItem ]]; + NSData * data = [SentrySerialization dataWithEnvelope:envelope error:nil]; + + [data writeToURL:[videoURL URLByAppendingPathExtension:@"json"] atomically:YES]; + NSLog(@"### %@", videoURL); + [self captureEnvelope:envelope]; } diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index 299805e48e4..2589b0d19ea 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -38,9 +38,9 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } -+ (long)millisecondsSince1970:(NSDate *)date ++ (double)secondsSince1970:(NSDate *)date { - return (NSInteger)([date timeIntervalSince1970] * 1000); + return [date timeIntervalSince1970]; } @end diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index aba0f496a5f..ada25e46f7f 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -25,10 +25,6 @@ - (instancetype)initWithPath:(NSString *)path time:(NSDate *)time @end -@implementation SentryVideoInfo - -@end - @implementation SentryOnDemandReplay { NSString *_outputPath; NSDate *_startTime; @@ -182,12 +178,7 @@ - (void)createVideoOf:(NSTimeInterval)duration if (completion) { SentryVideoInfo * videoInfo = nil; if (videoWriter.status == AVAssetWriterStatusCompleted) { - videoInfo = [[SentryVideoInfo alloc] init]; - videoInfo.height = (int)self->_videoSize.height; - videoInfo.width = (int)self->_videoSize.width; - videoInfo.frameCount = (int)frames.count; - videoInfo.frameRate = 1; - videoInfo.duration = frames.count; + videoInfo = [[SentryVideoInfo alloc] initWithHeight:(NSInteger)self->_videoSize.height width:(NSInteger)self->_videoSize.width duration:frames.count frameCount:frames.count frameRate:1]; } completion(videoInfo, videoWriter.error); } diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 151d523ebad..95dc2d2fd94 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -16,18 +16,17 @@ #import "SentrySDK.h" #import "SentryScope.h" #import "SentrySwiftAsyncIntegration.h" - #import #if SENTRY_HAS_UIKIT # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" -# import "SentryReplayOptions+Private.h" # import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" +# import "SentrySessionReplayIntegration.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @@ -58,6 +57,7 @@ - (void)setMeasurement:(SentryMeasurementValue *)measurement NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), + NSStringFromClass([SentrySessionReplayIntegration class]), #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index c7298e03c49..39e8c0a3b5a 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -27,7 +27,7 @@ - (NSDictionary *)serialize result[@"urls"] = self.urls; result[@"replay_start_timestamp"] = - @([SentryDateUtil millisecondsSince1970:self.replayStartTimestamp]); + @([SentryDateUtil secondsSince1970:self.replayStartTimestamp]); result[@"trace_ids"] = trace_ids; result[@"replay_id"] = self.replayId.sentryIdString; result[@"segment_id"] = @(self.segmentId); diff --git a/Sources/Sentry/SentryReplayOptions.m b/Sources/Sentry/SentryReplayOptions.m deleted file mode 100644 index 9e9922ff7ed..00000000000 --- a/Sources/Sentry/SentryReplayOptions.m +++ /dev/null @@ -1,51 +0,0 @@ -#import "SentryReplayOptions.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface -SentryReplayOptions () - -@property (nonatomic) NSInteger replayBitRate; - -@end - -@implementation SentryReplayOptions - -- (instancetype)init -{ - if (self = [super init]) { - self.replaysSessionSampleRate = 0; - self.replaysOnErrorSampleRate = 0; - self.replayBitRate = 20000; - } - return self; -} - -- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate - replaysOnErrorSampleRate:(float)errorSampleRate -{ - if (self = [self init]) { - self.replaysSessionSampleRate = sessionSampleRate; - self.replaysOnErrorSampleRate = errorSampleRate; - } - - return self; -} - -- (instancetype)initWithDictionary:(NSDictionary *)dictionary -{ - if (self = [self init]) { - if ([dictionary[@"replaysSessionSampleRate"] isKindOfClass:NSNumber.class]) { - self.replaysSessionSampleRate = [dictionary[@"replaysSessionSampleRate"] floatValue]; - } - - if ([dictionary[@"replaysOnErrorSampleRate"] isKindOfClass:NSNumber.class]) { - self.replaysOnErrorSampleRate = [dictionary[@"replaysOnErrorSampleRate"] floatValue]; - } - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index 059ac1bfff7..18fbe2f3787 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -35,7 +35,7 @@ - (NSDictionary *)headerForReplayRecording - (NSArray *> *)serialize { - long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; + double timestamp = [SentryDateUtil secondsSince1970:self.start]; // This format is defined by RRWeb // empty values are required by the format diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 8e03088983d..08a3956f168 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -2,7 +2,7 @@ #import "SentryAttachment+Private.h" #import "SentryLog.h" #import "SentryOnDemandReplay.h" -#import "SentryReplayOptions+Private.h" +#import "SentrySwift.h" #import "SentryViewPhotographer.h" #import "SentrySDK+Private.h" #import "SentryHub+Private.h" @@ -106,18 +106,8 @@ - (void)stop from:replayStart outputFileURL:finalPath completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { - SentryReplayEvent * replayEvent = [[SentryReplayEvent alloc] init]; - replayEvent.replayType = kSentryReplayTypeBuffer; - replayEvent.replayId = [[SentryId alloc] init]; - replayEvent.replayStartTimestamp = replayStart; - replayEvent.segmentId = 1; - - - SentryReplayRecording * recording = [[SentryReplayRecording alloc] initWithSegmentId:1 size:1 start:replayStart duration:videoInfo.duration frameCount:videoInfo.frameCount frameRate:videoInfo.frameRate height:videoInfo.height width:videoInfo.width]; - - [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:finalPath]; + [self captureSegment:videoInfo videoUrl:finalPath startedAt:replayStart replayId:[[SentryId alloc] init]]; }]; - return attachments; } @@ -162,21 +152,32 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + NSDate * segmentStart = [date dateByAddingTimeInterval:-5]; + [_replayMaker createVideoOf:5 - from:[date dateByAddingTimeInterval:-5] + from:segmentStart outputFileURL:pathToSegment completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { + //[self captureSegment:videoInfo videoUrl:pathToSegment startedAt:segmentStart replayId:self->sessionReplayId]; [self->_replayMaker releaseFramesUntil:date]; self->_videoSegmentStart = nil; }]; - - } -- (void) captureSegment:(NSURL *)filePath { +- (void) captureSegment:(SentryVideoInfo *)videoInfo videoUrl:(NSURL *)filePath startedAt:(NSDate*)replayStart replayId:(SentryId *)replayid { + SentryReplayEvent * replayEvent = [[SentryReplayEvent alloc] init]; + replayEvent.replayType = kSentryReplayTypeBuffer; + replayEvent.replayId = replayid; + replayEvent.replayStartTimestamp = replayStart; + replayEvent.segmentId = 0; + + SentryReplayRecording * recording = [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId size:1 start:replayStart duration:videoInfo.duration frameCount:videoInfo.frameCount frameRate:videoInfo.frameRate height:videoInfo.height width:videoInfo.width]; + + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:filePath]; + SENTRY_LOG_DEBUG(@"Session replay: ReplayId: %@, EventId: %@", replayEvent.replayId, replayEvent.eventId); } - (void)takeScreenshot diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 7df58484019..da62eaadee3 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -4,7 +4,7 @@ #import "SentryHub+Private.h" #import "SentryOptions.h" #import "SentryRandom.h" -#import "SentryReplayOptions.h" +#import "SentrySwift.h" #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" diff --git a/Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h deleted file mode 100644 index 2ef6e8094bb..00000000000 --- a/Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h +++ /dev/null @@ -1,20 +0,0 @@ -#import "SentryReplayOptions.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface -SentryReplayOptions (Private) - -/** - * Defines the quality of the session replay. - * Higher bit rates better quality, but also bigger files to transfer. - * @note The default value is @c 20000; - */ -@property (nonatomic) NSInteger replayBitRate; - -- (instancetype)initWithDictionary:(NSDictionary *)dictionary; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index 8cb845c984a..ad246545773 100644 --- a/Sources/Sentry/include/SentryDateUtil.h +++ b/Sources/Sentry/include/SentryDateUtil.h @@ -9,7 +9,7 @@ NS_SWIFT_NAME(DateUtil) + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second; -+ (long)millisecondsSince1970:(NSDate *)date; ++ (double)secondsSince1970:(NSDate *)date; @end diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index 02de6dc5b88..6c7945b51e6 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -1,22 +1,11 @@ #import "SentryDefines.h" #import - +#import "SentrySwift.h" #if SENTRY_HAS_UIKIT # import NS_ASSUME_NONNULL_BEGIN -@interface SentryVideoInfo : NSObject - -@property (nonatomic) int height; -@property (nonatomic) int width; -@property (nonatomic) NSTimeInterval duration; -@property (nonatomic) int frameCount; -@property (nonatomic) int frameRate; - -@end - - @interface SentryOnDemandReplay : NSObject @property (nonatomic) NSInteger bitRate; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 0260a5791a9..ae3b4cf6876 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -1,7 +1,7 @@ #import "SentryClient+Private.h" #import "SentryEvent.h" -#import "SentryReplayOptions.h" #import +#import "SentrySwift.h" #if SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift new file mode 100644 index 00000000000..65157b9c8f8 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -0,0 +1,59 @@ +import Foundation + + + + +@objcMembers +public class SentryReplayOptions : NSObject { + /** + * Indicates the percentage in which the replay for the session will be created. + * @discussion Specifying @c 0 means never, @c 1.0 means always. + * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * @note The default is @c 0. + */ + public let replaysSessionSampleRate: Float + + /** + * Indicates the percentage in which a 30 seconds replay will be send with error events. + * @discussion Specifying @c 0 means never, @c 1.0 means always. + * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * @note The default is @c 0. + */ + public let replaysOnErrorSampleRate: Float + + /** + * Defines the quality of the session replay. + * Higher bit rates better quality, but also bigger files to transfer. + * @note The default value is @c 20000; + */ + let replayBitRate = 20000; + + /** + * Inittialize session replay options disabled + */ + public override init() { + self.replaysSessionSampleRate = 0 + self.replaysOnErrorSampleRate = 0 + } + + /** + * Inittialize session replay options + * + * sessionSampleRate Indicates the percentage in which the replay for the session will be + * created. + * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with + * error events. + */ + public init(sessionSampleRate: Float, errorSampleRate: Float) { + self.replaysSessionSampleRate = sessionSampleRate + self.replaysOnErrorSampleRate = errorSampleRate + } + + convenience init(dictionary: NSDictionary) { + let sessionSampleRate = (dictionary["replaysSessionSampleRate"] as? NSNumber)?.floatValue ?? 0 + let onErrorSampleRate = (dictionary["replaysOnErrorSampleRate"] as? NSNumber)?.floatValue ?? 0 + self.init(sessionSampleRate: sessionSampleRate, errorSampleRate: onErrorSampleRate) + } +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift index 1cbb335c8f8..ea60991449c 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -1,4 +1,20 @@ import Foundation - - +@objcMembers +class SentryVideoInfo : NSObject { + + let height : Int + let width : Int + let duration : TimeInterval + let frameCount : Int + let frameRate : Int + + init(height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int) { + self.height = height + self.width = width + self.duration = duration + self.frameCount = frameCount + self.frameRate = frameRate + } + +} diff --git a/Tests/SentryTests/Helper/SentryDateUtilTests.swift b/Tests/SentryTests/Helper/SentryDateUtilTests.swift index 50096006244..9c2cc0ad07c 100644 --- a/Tests/SentryTests/Helper/SentryDateUtilTests.swift +++ b/Tests/SentryTests/Helper/SentryDateUtilTests.swift @@ -57,7 +57,7 @@ class SentryDateUtilTests: XCTestCase { func testJavascriptDate() { let testDate = Date(timeIntervalSince1970: 60) - let timestamp = DateUtil.millisecondsSince1970(testDate) + let timestamp = DateUtil.secondsSince1970(testDate) expect(timestamp) == 60_000 } From 58d938d672127bbe4169694e17e95d4137df3413 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 7 Mar 2024 13:43:10 +0100 Subject: [PATCH 38/88] Replay working --- Sources/Sentry/SentryClient.m | 2 +- Sources/Sentry/SentryDateUtil.m | 4 ++-- Sources/Sentry/SentryReplayEvent.m | 8 ++++---- Sources/Sentry/SentryReplayRecording.m | 2 +- Sources/Sentry/SentrySessionReplay.m | 4 ++-- Sources/Sentry/include/SentryDateUtil.h | 2 +- Sources/Sentry/include/SentryReplayEvent.h | 5 ----- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 63a7e2ffd0d..94ad64471f6 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -506,7 +506,7 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent return; } - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] items:@[ videoEnvelopeItem ]]; NSData * data = [SentrySerialization dataWithEnvelope:envelope error:nil]; diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index 2589b0d19ea..96e785a6b9d 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -38,9 +38,9 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } -+ (double)secondsSince1970:(NSDate *)date ++ (long)millisecondsSince1970:(NSDate *)date { - return [date timeIntervalSince1970]; + return (long)([date timeIntervalSince1970] * 1000); } @end diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index 39e8c0a3b5a..bef2f49ed5d 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -26,13 +26,13 @@ - (NSDictionary *)serialize } result[@"urls"] = self.urls; - result[@"replay_start_timestamp"] = - @([SentryDateUtil secondsSince1970:self.replayStartTimestamp]); + result[@"replay_start_timestamp"] = @(self.replayStartTimestamp.timeIntervalSince1970); result[@"trace_ids"] = trace_ids; - result[@"replay_id"] = self.replayId.sentryIdString; + result[@"replay_id"] = self.eventId.sentryIdString; result[@"segment_id"] = @(self.segmentId); result[@"replay_type"] = nameForSentryReplayType(self.replayType); - + result[@"error_ids"] = @[]; + return result; } diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m index 18fbe2f3787..059ac1bfff7 100644 --- a/Sources/Sentry/SentryReplayRecording.m +++ b/Sources/Sentry/SentryReplayRecording.m @@ -35,7 +35,7 @@ - (NSDictionary *)headerForReplayRecording - (NSArray *> *)serialize { - double timestamp = [SentryDateUtil secondsSince1970:self.start]; + long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; // This format is defined by RRWeb // empty values are required by the format diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 08a3956f168..ca7b4a3e8cb 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -169,7 +169,7 @@ - (void)prepareSegmentUntil:(NSDate *)date - (void) captureSegment:(SentryVideoInfo *)videoInfo videoUrl:(NSURL *)filePath startedAt:(NSDate*)replayStart replayId:(SentryId *)replayid { SentryReplayEvent * replayEvent = [[SentryReplayEvent alloc] init]; replayEvent.replayType = kSentryReplayTypeBuffer; - replayEvent.replayId = replayid; + replayEvent.eventId = replayid; replayEvent.replayStartTimestamp = replayStart; replayEvent.segmentId = 0; @@ -177,7 +177,7 @@ - (void) captureSegment:(SentryVideoInfo *)videoInfo videoUrl:(NSURL *)filePath [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:filePath]; - SENTRY_LOG_DEBUG(@"Session replay: ReplayId: %@, EventId: %@", replayEvent.replayId, replayEvent.eventId); + SENTRY_LOG_DEBUG(@"Session replay: ReplayId: %@ \nAT: %@", replayEvent.eventId, filePath); } - (void)takeScreenshot diff --git a/Sources/Sentry/include/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index ad246545773..8cb845c984a 100644 --- a/Sources/Sentry/include/SentryDateUtil.h +++ b/Sources/Sentry/include/SentryDateUtil.h @@ -9,7 +9,7 @@ NS_SWIFT_NAME(DateUtil) + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second; -+ (double)secondsSince1970:(NSDate *)date; ++ (long)millisecondsSince1970:(NSDate *)date; @end diff --git a/Sources/Sentry/include/SentryReplayEvent.h b/Sources/Sentry/include/SentryReplayEvent.h index ef20250097d..14a9fd382d5 100644 --- a/Sources/Sentry/include/SentryReplayEvent.h +++ b/Sources/Sentry/include/SentryReplayEvent.h @@ -30,11 +30,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, strong) NSArray *traceIds; -/** - * The replay id to which this segment belongs to. - */ -@property (nonatomic, strong) SentryId *replayId; - /** * The type of the replay */ From 9dbacfd8199098d047d02a2c466c32688aaa4db3 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 7 Mar 2024 12:44:23 +0000 Subject: [PATCH 39/88] Format code --- Sources/Sentry/SentryBaseIntegration.m | 2 +- Sources/Sentry/SentryClient.m | 13 ++-- Sources/Sentry/SentryOnDemandReplay.m | 68 ++++++++++-------- Sources/Sentry/SentryOptions.m | 2 +- Sources/Sentry/SentryReplayEvent.m | 4 +- Sources/Sentry/SentrySessionReplay.m | 69 +++++++++++-------- .../Sentry/SentrySessionReplayIntegration.m | 2 +- Sources/Sentry/include/SentryOnDemandReplay.h | 2 +- Sources/Sentry/include/SentrySessionReplay.h | 2 +- .../SentryCrashMonitor_CPPException.cpp | 3 +- .../SessionReplay/SentryReplayOptions.swift | 7 +- .../SessionReplay/SentryVideoInfo.swift | 12 ++-- Tests/SentryTests/SentryInterfacesTests.m | 4 +- 13 files changed, 105 insertions(+), 85 deletions(-) diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index f5e04831fd1..757c7fd2e0a 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,10 +1,10 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" +#import "SentrySwift.h" #import #import #import -#import "SentrySwift.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 94ad64471f6..4e8b77e3632 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -506,14 +506,15 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent return; } - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] - items:@[ videoEnvelopeItem ]]; + SentryEnvelope *envelope = [[SentryEnvelope alloc] + initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] + items:@[ videoEnvelopeItem ]]; - NSData * data = [SentrySerialization dataWithEnvelope:envelope error:nil]; - - [data writeToURL:[videoURL URLByAppendingPathExtension:@"json"] atomically:YES]; + NSData *data = [SentrySerialization dataWithEnvelope:envelope error:nil]; + + [data writeToURL:[videoURL URLByAppendingPathExtension:@"json"] atomically:YES]; NSLog(@"### %@", videoURL); - + [self captureEnvelope:envelope]; } diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index ada25e46f7f..b9ce84b1764 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -154,37 +154,45 @@ - (void)createVideoOf:(NSTimeInterval)duration } [frames addObject:frame.imagePath]; } - + [videoWriterInput - requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue - usingBlock:^{ - UIImage *image = - [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); - - if (![self appendPixelBufferForImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { - if (completion) { - completion(nil, videoWriter.error); - } - } - } - - if (frameCount >= frames.count) { - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - SentryVideoInfo * videoInfo = nil; - if (videoWriter.status == AVAssetWriterStatusCompleted) { - videoInfo = [[SentryVideoInfo alloc] initWithHeight:(NSInteger)self->_videoSize.height width:(NSInteger)self->_videoSize.width duration:frames.count frameCount:frames.count frameRate:1]; - } - completion(videoInfo, videoWriter.error); - } - }]; - } - }]; + requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue + usingBlock:^{ + UIImage *image = + [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { + if (completion) { + completion(nil, videoWriter.error); + } + } + } + + if (frameCount >= frames.count) { + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + SentryVideoInfo *videoInfo = nil; + if (videoWriter.status + == AVAssetWriterStatusCompleted) { + videoInfo = [[SentryVideoInfo alloc] + initWithHeight:(NSInteger) + self->_videoSize.height + width:(NSInteger) + self->_videoSize.width + duration:frames.count + frameCount:frames.count + frameRate:1]; + } + completion(videoInfo, videoWriter.error); + } + }]; + } + }]; } - (BOOL)appendPixelBufferForImage:(UIImage *)image diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 95dc2d2fd94..b309c386adf 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,10 +23,10 @@ # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" # import "SentryScreenshotIntegration.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" -# import "SentrySessionReplayIntegration.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index bef2f49ed5d..601a4c1ed04 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -26,13 +26,13 @@ - (NSDictionary *)serialize } result[@"urls"] = self.urls; - result[@"replay_start_timestamp"] = @(self.replayStartTimestamp.timeIntervalSince1970); + result[@"replay_start_timestamp"] = @(self.replayStartTimestamp.timeIntervalSince1970); result[@"trace_ids"] = trace_ids; result[@"replay_id"] = self.eventId.sentryIdString; result[@"segment_id"] = @(self.segmentId); result[@"replay_type"] = nameForSentryReplayType(self.replayType); result[@"error_ids"] = @[]; - + return result; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index ca7b4a3e8cb..ccbf6858564 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,14 +1,14 @@ #import "SentrySessionReplay.h" #import "SentryAttachment+Private.h" +#import "SentryHub+Private.h" +#import "SentryId.h" #import "SentryLog.h" #import "SentryOnDemandReplay.h" -#import "SentrySwift.h" -#import "SentryViewPhotographer.h" -#import "SentrySDK+Private.h" -#import "SentryHub+Private.h" #import "SentryReplayEvent.h" -#import "SentryId.h" #import "SentryReplayRecording.h" +#import "SentrySDK+Private.h" +#import "SentrySwift.h" +#import "SentryViewPhotographer.h" #if SENTRY_HAS_UIKIT @@ -24,7 +24,7 @@ @implementation SentrySessionReplay { NSDate *_sessionStart; SentryReplayOptions *_replayOptions; SentryOnDemandReplay *_replayMaker; - SentryId * sessionReplayId; + SentryId *sessionReplayId; NSMutableArray *imageCollection; } @@ -77,7 +77,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full imageCollection = [NSMutableArray array]; NSLog(@"Recording session to %@", _urlToCache); - + if (full) { sessionReplayId = [[SentryId alloc] init]; } @@ -100,14 +100,17 @@ - (void)stop } NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - NSDate * replayStart = [NSDate dateWithTimeIntervalSinceNow:-30]; - + NSDate *replayStart = [NSDate dateWithTimeIntervalSinceNow:-30]; + [_replayMaker createVideoOf:30 from:replayStart outputFileURL:finalPath - completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { - [self captureSegment:videoInfo videoUrl:finalPath startedAt:replayStart replayId:[[SentryId alloc] init]]; - }]; + completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { + [self captureSegment:videoInfo + videoUrl:finalPath + startedAt:replayStart + replayId:[[SentryId alloc] init]]; + }]; return attachments; } @@ -152,31 +155,43 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - NSDate * segmentStart = [date dateByAddingTimeInterval:-5]; - + NSDate *segmentStart = [date dateByAddingTimeInterval:-5]; + [_replayMaker createVideoOf:5 from:segmentStart outputFileURL:pathToSegment - completion:^(SentryVideoInfo * videoInfo, NSError *_Nonnull error) { - - //[self captureSegment:videoInfo videoUrl:pathToSegment startedAt:segmentStart replayId:self->sessionReplayId]; - - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; + completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { + //[self captureSegment:videoInfo videoUrl:pathToSegment + //startedAt:segmentStart replayId:self->sessionReplayId]; + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; } -- (void) captureSegment:(SentryVideoInfo *)videoInfo videoUrl:(NSURL *)filePath startedAt:(NSDate*)replayStart replayId:(SentryId *)replayid { - SentryReplayEvent * replayEvent = [[SentryReplayEvent alloc] init]; +- (void)captureSegment:(SentryVideoInfo *)videoInfo + videoUrl:(NSURL *)filePath + startedAt:(NSDate *)replayStart + replayId:(SentryId *)replayid +{ + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] init]; replayEvent.replayType = kSentryReplayTypeBuffer; replayEvent.eventId = replayid; replayEvent.replayStartTimestamp = replayStart; replayEvent.segmentId = 0; - - SentryReplayRecording * recording = [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId size:1 start:replayStart duration:videoInfo.duration frameCount:videoInfo.frameCount frameRate:videoInfo.frameRate height:videoInfo.height width:videoInfo.width]; - + + SentryReplayRecording *recording = + [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId + size:1 + start:replayStart + duration:videoInfo.duration + frameCount:videoInfo.frameCount + frameRate:videoInfo.frameRate + height:videoInfo.height + width:videoInfo.width]; + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:filePath]; - + SENTRY_LOG_DEBUG(@"Session replay: ReplayId: %@ \nAT: %@", replayEvent.eventId, filePath); } diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index da62eaadee3..fca5fe7ad89 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -4,9 +4,9 @@ #import "SentryHub+Private.h" #import "SentryOptions.h" #import "SentryRandom.h" -#import "SentrySwift.h" #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" +#import "SentrySwift.h" #if SENTRY_HAS_UIKIT # import "SentryUIApplication.h" diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index 6c7945b51e6..e7a7a03c0e5 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -1,6 +1,6 @@ #import "SentryDefines.h" -#import #import "SentrySwift.h" +#import #if SENTRY_HAS_UIKIT # import diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index ae3b4cf6876..05404409aa5 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -1,7 +1,7 @@ #import "SentryClient+Private.h" #import "SentryEvent.h" -#import #import "SentrySwift.h" +#import #if SENTRY_HAS_UIKIT diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp index 784289a8622..ec93b882a73 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp @@ -135,8 +135,7 @@ CPPExceptionTerminate(void) strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff)); } #define CATCH_VALUE(TYPE, PRINTFTYPE) \ - catch (TYPE value) \ - { \ + catch (TYPE value) { \ snprintf(descriptionBuff, sizeof(descriptionBuff), "%" #PRINTFTYPE, value); \ } CATCH_VALUE(char, d) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 65157b9c8f8..47f973f52a6 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -1,10 +1,7 @@ import Foundation - - - @objcMembers -public class SentryReplayOptions : NSObject { +public class SentryReplayOptions: NSObject { /** * Indicates the percentage in which the replay for the session will be created. * @discussion Specifying @c 0 means never, @c 1.0 means always. @@ -28,7 +25,7 @@ public class SentryReplayOptions : NSObject { * Higher bit rates better quality, but also bigger files to transfer. * @note The default value is @c 20000; */ - let replayBitRate = 20000; + let replayBitRate = 20_000 /** * Inittialize session replay options disabled diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift index ea60991449c..887768368d3 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -1,13 +1,13 @@ import Foundation @objcMembers -class SentryVideoInfo : NSObject { +class SentryVideoInfo: NSObject { - let height : Int - let width : Int - let duration : TimeInterval - let frameCount : Int - let frameRate : Int + let height: Int + let width: Int + let duration: TimeInterval + let frameCount: Int + let frameRate: Int init(height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int) { self.height = height diff --git a/Tests/SentryTests/SentryInterfacesTests.m b/Tests/SentryTests/SentryInterfacesTests.m index 5d01ae94745..eaa4902954b 100644 --- a/Tests/SentryTests/SentryInterfacesTests.m +++ b/Tests/SentryTests/SentryInterfacesTests.m @@ -177,7 +177,7 @@ - (void)testTransactionEvent @1 : @"1", @2 : @2, @3 : @ { @"a" : @0 }, - @4 : @[ @"1", @2, @{ @"a" : @0 }, @[ @"a" ], testDate, testURL ], + @4 : @[ @"1", @2, @ { @"a" : @0 }, @[ @"a" ], testDate, testURL ], @5 : testDate, @6 : testURL } @@ -191,7 +191,7 @@ - (void)testTransactionEvent @"2" : @2, @"3" : @ { @"a" : @0 }, @"4" : @[ - @"1", @2, @{ @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", + @"1", @2, @ { @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", @"https://sentry.io" ], @"5" : @"2020-02-27T11:35:26.000Z", From f3d8336c995f25106a8d74129bb1eb46172cde2c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 7 Mar 2024 16:07:33 +0100 Subject: [PATCH 40/88] using global processor --- Sources/Sentry/SentrySessionReplay.m | 12 ++---------- Sources/Sentry/SentrySessionReplayIntegration.m | 13 +++++++++---- Sources/Sentry/include/SentrySessionReplay.h | 4 +++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index ccbf6858564..2f256751964 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -90,13 +90,10 @@ - (void)stop _displayLink = nil; } -// TODO: dont use processAttachments to capture replay -- (nullable NSArray *)processAttachments: - (nullable NSArray *)attachments - forEvent:(nonnull SentryEvent *)event +- (void)replayForEvent:(SentryEvent *)event; { if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { - return attachments; + return; } NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; @@ -111,11 +108,6 @@ - (void)stop startedAt:replayStart replayId:[[SentryId alloc] init]]; }]; - return attachments; -} - -- (void)sendReplayForEvent:(SentryEvent *)event -{ } - (void)newFrame:(CADisplayLink *)sender diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index fca5fe7ad89..89c86e42bed 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,4 +1,5 @@ #import "SentrySessionReplayIntegration.h" +#import "SentryGlobalEventProcessor.h" #import "SentryClient+Private.h" #import "SentryDependencyContainer.h" #import "SentryHub+Private.h" @@ -7,6 +8,7 @@ #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" #import "SentrySwift.h" +#import "SentryGlobalEventProcessor.h" #if SENTRY_HAS_UIKIT # import "SentryUIApplication.h" @@ -35,14 +37,17 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options start:SentryDependencyContainer.sharedInstance.application.windows.firstObject fullSession:[self shouldReplayFullSession:options.sessionReplayOptions .replaysSessionSampleRate]]; - - SentryClient *client = [SentrySDK.currentHub getClient]; - [client addAttachmentProcessor:sessionReplay]; - + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent * _Nullable(SentryEvent * _Nonnull event) { + [self->sessionReplay replayForEvent:event]; + return event; + }]; + return YES; } else { return NO; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 05404409aa5..56fe62ed819 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentrySessionReplay : NSObject +@interface SentrySessionReplay : NSObject - (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; @@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)stop; +- (void)replayForEvent:(SentryEvent *)event; + @end NS_ASSUME_NONNULL_END From 1a0783777e738876be29698f2a966954d74e01d2 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 11 Mar 2024 07:58:33 +0100 Subject: [PATCH 41/88] refactoring --- Sources/Sentry/SentryFileManager.m | 6 + Sources/Sentry/SentryOnDemandReplay.m | 109 ++++++++++++------ Sources/Sentry/SentrySessionReplay.m | 51 +++++--- .../Sentry/SentrySessionReplayIntegration.m | 18 +-- Sources/Sentry/include/SentryFileManager.h | 2 + Sources/Sentry/include/SentryOnDemandReplay.h | 4 + .../SentryCrashMonitor_CPPException.cpp | 3 +- .../SessionReplay/SentryReplayOptions.swift | 12 ++ .../SessionReplay/SentryVideoInfo.swift | 6 +- Tests/SentryTests/SentryInterfacesTests.m | 4 +- .../SentryTests/SentryTests-Bridging-Header.h | 48 +++----- 11 files changed, 164 insertions(+), 99 deletions(-) diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index 1f01fd86c6e..74edbbdf8ec 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -752,6 +752,12 @@ - (void)createPathsWithOptions:(SentryOptions *)options return sentryApplicationSupportPath; } +- (NSInteger)fileSize:(NSURL *)path { + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path error:nil]; + NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize] ?: @(-1); + return [fileSize integerValue]; +} + NSURL *_Nullable sentryLaunchConfigFileURL = nil; NSURL *_Nullable launchProfileConfigFileURL(void) diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index b9ce84b1764..a94825ebd98 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -4,6 +4,7 @@ # import "SentryLog.h" # import # import + @interface SentryReplayFrame : NSObject @property (nonatomic, strong) NSString *imagePath; @@ -22,15 +23,22 @@ - (instancetype)initWithPath:(NSString *)path time:(NSDate *)time } return self; } +@end + +@interface SentryPixelBuffer : NSObject +- (nullable instancetype)initWithSize:(CGSize) size; +- (BOOL)appendImage:(UIImage *)image + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime; @end @implementation SentryOnDemandReplay { NSString *_outputPath; NSDate *_startTime; NSMutableArray *_frames; - CGSize _videoSize; dispatch_queue_t _onDemandDispatchQueue; + SentryPixelBuffer * _currentPixelBuffer; } - (instancetype)initWithOutputPath:(NSString *)outputPath @@ -42,6 +50,7 @@ - (instancetype)initWithOutputPath:(NSString *)outputPath _videoSize = CGSizeMake(200, 434); _bitRate = 20000; _cacheMaxSize = NSUIntegerMax; + _frameRate = 1; _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); } return self; @@ -77,7 +86,7 @@ - (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth CGSize newSize = CGSizeMake(newWidth, newHeight); - UIGraphicsBeginImageContextWithOptions(newSize, NO, 1); + UIGraphicsBeginImageContextWithOptions(newSize, NO, self.frameRate); [originalImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -146,26 +155,35 @@ - (void)createVideoOf:(NSTimeInterval)duration NSDate *end = [beginning dateByAddingTimeInterval:duration]; __block NSInteger frameCount = 0; NSMutableArray *frames = [NSMutableArray array]; + + NSDate *start = [NSDate date]; + NSDate *actualEnd = nil; for (SentryReplayFrame *frame in self->_frames) { if ([frame.time compare:beginning] == NSOrderedAscending) { continue; } else if ([frame.time compare:end] == NSOrderedDescending) { break; } + if ([frame.time compare:start] == NSOrderedAscending) { + start = frame.time; + } + actualEnd = frame.time; [frames addObject:frame.imagePath]; } - + + _currentPixelBuffer = [[SentryPixelBuffer alloc] initWithSize:CGSizeMake(_videoSize.width, _videoSize.height)]; + [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue usingBlock:^{ UIImage *image = [UIImage imageWithContentsOfFile:frames[frameCount]]; if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); + CMTime presentTime = CMTimeMake(frameCount++, (int32_t)self->_frameRate); - if (![self appendPixelBufferForImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { + if (![self->_currentPixelBuffer appendImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { if (completion) { completion(nil, videoWriter.error); } @@ -186,7 +204,10 @@ - (void)createVideoOf:(NSTimeInterval)duration self->_videoSize.width duration:frames.count frameCount:frames.count - frameRate:1]; + frameRate:1 + start:start + end:actualEnd + ]; } completion(videoInfo, videoWriter.error); } @@ -195,46 +216,58 @@ - (void)createVideoOf:(NSTimeInterval)duration }]; } -- (BOOL)appendPixelBufferForImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime -{ - CVReturn status = kCVReturnSuccess; - - CVPixelBufferRef pixelBuffer = NULL; - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, - (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); - - if (status != kCVReturnSuccess) { - return NO; - } +@end - CVPixelBufferLockBaseAddress(pixelBuffer, 0); - void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, - (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, - (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - CGContextTranslateCTM(context, 0, image.size.height); - CGContextScaleCTM(context, 1.0, -1.0); +@implementation SentryPixelBuffer { + CVPixelBufferRef _pixelBuffer; + CGContextRef _context; + CGColorSpaceRef _rgbColorSpace; +} - UIGraphicsPushContext(context); - [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - UIGraphicsPopContext(); +- (nullable instancetype)initWithSize:(CGSize) size { + if (self = [super init]) { + CVReturn status = kCVReturnSuccess; + + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, + (size_t)size.height, kCVPixelFormatType_32ARGB, NULL, &_pixelBuffer); + + if (status != kCVReturnSuccess) { + return nil; + } + void *pixelData = CVPixelBufferGetBaseAddress(_pixelBuffer); + _rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + _context = CGBitmapContextCreate(pixelData, (size_t)size.width, + (size_t)size.height, 8, CVPixelBufferGetBytesPerRow(_pixelBuffer), _rgbColorSpace, + (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); + + CGContextTranslateCTM(_context, 0, size.height); + CGContextScaleCTM(_context, 1.0, -1.0); + } + return self; +} - CGColorSpaceRelease(rgbColorSpace); - CGContextRelease(context); +- (void)dealloc { + CVPixelBufferRelease(_pixelBuffer); + CGContextRelease(_context); + CGColorSpaceRelease(_rgbColorSpace); +} - CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); +- (BOOL)appendImage:(UIImage *)image + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime +{ + CVPixelBufferLockBaseAddress(_pixelBuffer, 0); + + CGContextDrawImage(_context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); + + CVPixelBufferUnlockBaseAddress(_pixelBuffer, 0); // Append the pixel buffer with the current image to the video - BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer + BOOL success = [pixelBufferAdaptor appendPixelBuffer:_pixelBuffer withPresentationTime:presentationTime]; - CVPixelBufferRelease(pixelBuffer); - return success; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 2f256751964..2338eb72a66 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -9,6 +9,9 @@ #import "SentrySDK+Private.h" #import "SentrySwift.h" #import "SentryViewPhotographer.h" +#import "SentryDependencyContainer.h" +#import "SentryFileManager.h" +#import "SentryCurrentDateProvider.h" #if SENTRY_HAS_UIKIT @@ -26,6 +29,8 @@ @implementation SentrySessionReplay { SentryOnDemandReplay *_replayMaker; SentryId *sessionReplayId; NSMutableArray *imageCollection; + int _currentSegmentId; + BOOL _isFullSession; } - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions @@ -36,6 +41,10 @@ - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions return self; } +- (SentryCurrentDateProvider *)dateProvider { + return SentryDependencyContainer.sharedInstance.dateProvider; +} + - (void)start:(UIView *)rootView fullSession:(BOOL)full { if (rootView == nil) { @@ -56,6 +65,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full _lastScreenShot = [[NSDate alloc] init]; _videoSegmentStart = nil; _sessionStart = _lastScreenShot; + _currentSegmentId = 0; NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] @@ -78,6 +88,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full NSLog(@"Recording session to %@", _urlToCache); + _isFullSession = full; if (full) { sessionReplayId = [[SentryId alloc] init]; } @@ -97,22 +108,23 @@ - (void)replayForEvent:(SentryEvent *)event; } NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - NSDate *replayStart = [NSDate dateWithTimeIntervalSinceNow:-30]; + NSDate *replayStart = [[self dateProvider].date dateByAddingTimeInterval:-30]; [_replayMaker createVideoOf:30 from:replayStart outputFileURL:finalPath completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - [self captureSegment:videoInfo + [self captureSegment:0 + video:videoInfo videoUrl:finalPath - startedAt:replayStart - replayId:[[SentryId alloc] init]]; + replayId:[[SentryId alloc] init] + replayType:kSentryReplayTypeBuffer]; }]; } - (void)newFrame:(CADisplayLink *)sender { - NSDate *now = [[NSDate alloc] init]; + NSDate *now = [self dateProvider].date; if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { [self takeScreenshot]; @@ -120,7 +132,7 @@ - (void)newFrame:(CADisplayLink *)sender if (_videoSegmentStart == nil) { _videoSegmentStart = now; - } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { + } else if (_isFullSession && [now timeIntervalSinceDate:_videoSegmentStart] >= 5) { [self prepareSegmentUntil:now]; } } @@ -147,35 +159,42 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - NSDate *segmentStart = [date dateByAddingTimeInterval:-5]; + NSDate *segmentStart = [[self dateProvider].date dateByAddingTimeInterval:-5]; [_replayMaker createVideoOf:5 from:segmentStart outputFileURL:pathToSegment completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - //[self captureSegment:videoInfo videoUrl:pathToSegment - //startedAt:segmentStart replayId:self->sessionReplayId]; + [self captureSegment:self->_currentSegmentId + video:videoInfo + videoUrl:pathToSegment + replayId:self->sessionReplayId + replayType:kSentryReplayTypeSession]; [self->_replayMaker releaseFramesUntil:date]; self->_videoSegmentStart = nil; }]; } -- (void)captureSegment:(SentryVideoInfo *)videoInfo +- (void)captureSegment:(NSInteger)segment + video:(SentryVideoInfo *)videoInfo videoUrl:(NSURL *)filePath - startedAt:(NSDate *)replayStart replayId:(SentryId *)replayid + replayType:(SentryReplayType)replayType { SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] init]; - replayEvent.replayType = kSentryReplayTypeBuffer; + replayEvent.replayType = replayType; replayEvent.eventId = replayid; - replayEvent.replayStartTimestamp = replayStart; - replayEvent.segmentId = 0; + replayEvent.replayStartTimestamp = videoInfo.start; + replayEvent.segmentId = segment; + replayEvent.timestamp = videoInfo.end; + NSInteger fileSize = [SentryDependencyContainer.sharedInstance.fileManager fileSize:filePath]; + SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId - size:1 - start:replayStart + size:fileSize + start:videoInfo.start duration:videoInfo.duration frameCount:videoInfo.frameCount frameRate:videoInfo.frameRate diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 89c86e42bed..b801f4c1a52 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,14 +1,13 @@ #import "SentrySessionReplayIntegration.h" -#import "SentryGlobalEventProcessor.h" #import "SentryClient+Private.h" #import "SentryDependencyContainer.h" +#import "SentryGlobalEventProcessor.h" #import "SentryHub+Private.h" #import "SentryOptions.h" #import "SentryRandom.h" #import "SentrySDK+Private.h" #import "SentrySessionReplay.h" #import "SentrySwift.h" -#import "SentryGlobalEventProcessor.h" #if SENTRY_HAS_UIKIT # import "SentryUIApplication.h" @@ -37,17 +36,18 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options start:SentryDependencyContainer.sharedInstance.application.windows.firstObject fullSession:[self shouldReplayFullSession:options.sessionReplayOptions .replaysSessionSampleRate]]; - + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; - - [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent * _Nullable(SentryEvent * _Nonnull event) { - [self->sessionReplay replayForEvent:event]; - return event; - }]; - + + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [self->sessionReplay replayForEvent:event]; + return event; + }]; + return YES; } else { return NO; diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index cb774591c79..3cc54aceddb 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -61,6 +61,8 @@ SENTRY_NO_INIT - (void)deleteOldEnvelopeItems; +- (NSInteger)fileSize:(NSURL *)path; + /** * Only used for testing. */ diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index e7a7a03c0e5..9ce8ea88ae6 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -10,8 +10,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSInteger bitRate; +@property (nonatomic) NSInteger frameRate; + @property (nonatomic) NSUInteger cacheMaxSize; +@property (nonatomic) CGSize videoSize; + - (instancetype)initWithOutputPath:(NSString *)outputPath; - (void)addFrame:(UIImage *)image; diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp index ec93b882a73..784289a8622 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp @@ -135,7 +135,8 @@ CPPExceptionTerminate(void) strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff)); } #define CATCH_VALUE(TYPE, PRINTFTYPE) \ - catch (TYPE value) { \ + catch (TYPE value) \ + { \ snprintf(descriptionBuff, sizeof(descriptionBuff), "%" #PRINTFTYPE, value); \ } CATCH_VALUE(char, d) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 47f973f52a6..94ca1931bb0 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -27,6 +27,18 @@ public class SentryReplayOptions: NSObject { */ let replayBitRate = 20_000 + let frameRate = 1 + + /** + * The scale related to the window size at which the replay will be created + */ + let sizeScale = 0.8 + + /** + * The maximum duration of replays for error events. + */ + let errorReplayDuration = TimeInterval(30) + /** * Inittialize session replay options disabled */ diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift index 887768368d3..b8d3fef269f 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -8,13 +8,17 @@ class SentryVideoInfo: NSObject { let duration: TimeInterval let frameCount: Int let frameRate: Int + let start: Date + let end: Date - init(height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int) { + init(height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int, start: Date, end: Date) { self.height = height self.width = width self.duration = duration self.frameCount = frameCount self.frameRate = frameRate + self.start = start + self.end = end } } diff --git a/Tests/SentryTests/SentryInterfacesTests.m b/Tests/SentryTests/SentryInterfacesTests.m index eaa4902954b..5d01ae94745 100644 --- a/Tests/SentryTests/SentryInterfacesTests.m +++ b/Tests/SentryTests/SentryInterfacesTests.m @@ -177,7 +177,7 @@ - (void)testTransactionEvent @1 : @"1", @2 : @2, @3 : @ { @"a" : @0 }, - @4 : @[ @"1", @2, @ { @"a" : @0 }, @[ @"a" ], testDate, testURL ], + @4 : @[ @"1", @2, @{ @"a" : @0 }, @[ @"a" ], testDate, testURL ], @5 : testDate, @6 : testURL } @@ -191,7 +191,7 @@ - (void)testTransactionEvent @"2" : @2, @"3" : @ { @"a" : @0 }, @"4" : @[ - @"1", @2, @ { @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", + @"1", @2, @{ @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", @"https://sentry.io" ], @"5" : @"2020-02-27T11:35:26.000Z", diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index ad42b1df4b1..77162c7b70b 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -39,6 +39,7 @@ #import "NSMutableDictionary+Sentry.h" #import "NSURLProtocolSwizzle.h" #import "PrivateSentrySDKOnly.h" +#import "Sentry/Sentry-Swift.h" #import "SentryANRTracker.h" #import "SentryANRTrackingIntegration.h" #import "SentryAppStartMeasurement.h" @@ -51,6 +52,7 @@ #import "SentryAutoBreadcrumbTrackingIntegration.h" #import "SentryAutoSessionTrackingIntegration.h" #import "SentryBaggage.h" +#import "SentryBinaryImageCache+Private.h" #import "SentryBooleanSerialization.h" #import "SentryBreadcrumbDelegate.h" #import "SentryBreadcrumbTracker.h" @@ -63,6 +65,7 @@ #import "SentryCoreDataSwizzling.h" #import "SentryCoreDataTracker+Test.h" #import "SentryCoreDataTrackingIntegration.h" +#import "SentryCrashBinaryImageCache.h" #import "SentryCrashBinaryImageProvider.h" #import "SentryCrashC.h" #import "SentryCrashDebug.h" @@ -98,13 +101,17 @@ #import "SentryDiscardReason.h" #import "SentryDiscardReasonMapper.h" #import "SentryDiscardedEvent.h" +#import "SentryDispatchFactory.h" #import "SentryDispatchQueueWrapper.h" +#import "SentryDispatchSourceWrapper.h" #import "SentryDisplayLinkWrapper.h" #import "SentryDsn.h" #import "SentryEnvelope+Private.h" +#import "SentryEnvelopeAttachmentHeader.h" #import "SentryEnvelopeItemType.h" #import "SentryEnvelopeRateLimit.h" #import "SentryEvent+Private.h" +#import "SentryExtraContextProvider.h" #import "SentryFileContents.h" #import "SentryFileIOTrackingIntegration.h" #import "SentryFileManager+Test.h" @@ -130,10 +137,12 @@ #import "SentryLog+TestInit.h" #import "SentryLog.h" #import "SentryLogOutput.h" +#import "SentryMeasurementValue.h" #import "SentryMechanism.h" #import "SentryMechanismMeta.h" #import "SentryMeta.h" #import "SentryMigrateSessionInit.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSDataTracker.h" #import "SentryNSError.h" #import "SentryNSNotificationCenterWrapper.h" @@ -148,17 +157,23 @@ #import "SentryObjCRuntimeWrapper.h" #import "SentryOptions+HybridSDKs.h" #import "SentryOptions+Private.h" +#import "SentryPerformanceTracker+Testing.h" #import "SentryPerformanceTracker.h" #import "SentryPerformanceTrackingIntegration.h" #import "SentryPredicateDescriptor.h" +#import "SentryPropagationContext.h" #import "SentryQueueableRequestManager.h" #import "SentryRandom.h" #import "SentryRateLimitParser.h" #import "SentryRateLimits.h" #import "SentryReachability.h" +#import "SentryReplayEvent.h" +#import "SentryReplayOptions.h" +#import "SentryReplayRecording.h" #import "SentryRetryAfterHeaderParser.h" #import "SentrySDK+Private.h" #import "SentrySDK+Tests.h" +#import "SentrySampleDecision+Private.h" #import "SentryScope+Private.h" #import "SentryScopeObserver.h" #import "SentryScopeSyncC.h" @@ -166,19 +181,6 @@ #import "SentryScreenshot.h" #import "SentryScreenshotIntegration.h" #import "SentrySdkInfo.h" -#import "SentrySwiftAsyncIntegration.h" -#import "Sentry/Sentry-Swift.h" -#import "SentryBinaryImageCache+Private.h" -#import "SentryCrashBinaryImageCache.h" -#import "SentryDispatchFactory.h" -#import "SentryDispatchSourceWrapper.h" -#import "SentryEnvelopeAttachmentHeader.h" -#import "SentryExtraContextProvider.h" -#import "SentryMeasurementValue.h" -#import "SentryNSProcessInfoWrapper.h" -#import "SentryPerformanceTracker+Testing.h" -#import "SentryPropagationContext.h" -#import "SentrySampleDecision+Private.h" #import "SentrySerialization.h" #import "SentrySession+Private.h" #import "SentrySessionTracker.h" @@ -189,6 +191,7 @@ #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" #import "SentrySubClassFinder.h" +#import "SentrySwiftAsyncIntegration.h" #import "SentrySwizzleWrapper.h" #import "SentrySysctl.h" #import "SentrySystemEventBreadcrumbs.h" @@ -219,24 +222,5 @@ #import "TestNSURLRequestBuilder.h" #import "TestSentryCrashWrapper.h" #import "TestSentrySpan.h" -#import "URLSessionTaskMock.h" -#import "SentryBinaryImageCache+Private.h" -#import "SentryCrashBinaryImageCache.h" -#import "SentryDispatchFactory.h" -#import "SentryDispatchSourceWrapper.h" -#import "SentryEnvelopeAttachmentHeader.h" -#import "SentryExtraContextProvider.h" -#import "SentryMeasurementValue.h" -#import "SentryMsgPackSerializer.h" -#import "SentryNSProcessInfoWrapper.h" -#import "SentryPerformanceTracker+Testing.h" -#import "SentryPropagationContext.h" -#import "SentryReplayEvent.h" -#import "SentryReplayOptions.h" -#import "SentryReplayRecording.h" -#import "SentrySampleDecision+Private.h" -#import "SentrySpanOperations.h" -#import "SentryTimeToDisplayTracker.h" -#import "SentryTracerConfiguration.h" #import "TestSentryViewHierarchy.h" #import "URLSessionTaskMock.h" From 6add56e23052f08a6ac5361a0743e90460487ab3 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 11 Mar 2024 10:03:42 +0100 Subject: [PATCH 42/88] more info in videoInfo --- Sources/Sentry/SentryFileManager.m | 6 +- Sources/Sentry/SentryOnDemandReplay.m | 80 +++++++++++-------- Sources/Sentry/SentrySessionReplay.m | 22 +++-- .../SessionReplay/SentryVideoInfo.swift | 6 +- 4 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index 74edbbdf8ec..41426d3b815 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -752,8 +752,10 @@ - (void)createPathsWithOptions:(SentryOptions *)options return sentryApplicationSupportPath; } -- (NSInteger)fileSize:(NSURL *)path { - NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path error:nil]; +- (NSInteger)fileSize:(NSURL *)path +{ + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path + error:nil]; NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize] ?: @(-1); return [fileSize integerValue]; } diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index a94825ebd98..102a8eea145 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -1,6 +1,8 @@ #import "SentryOnDemandReplay.h" #if SENTRY_HAS_UIKIT +# import "SentryDependencyContainer.h" +# import "SentryFileManager.h" # import "SentryLog.h" # import # import @@ -26,11 +28,11 @@ - (instancetype)initWithPath:(NSString *)path time:(NSDate *)time @end @interface SentryPixelBuffer : NSObject -- (nullable instancetype)initWithSize:(CGSize) size; +- (nullable instancetype)initWithSize:(CGSize)size; - (BOOL)appendImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime; + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime; @end @implementation SentryOnDemandReplay { @@ -38,7 +40,7 @@ @implementation SentryOnDemandReplay { NSDate *_startTime; NSMutableArray *_frames; dispatch_queue_t _onDemandDispatchQueue; - SentryPixelBuffer * _currentPixelBuffer; + SentryPixelBuffer *_currentPixelBuffer; } - (instancetype)initWithOutputPath:(NSString *)outputPath @@ -170,20 +172,22 @@ - (void)createVideoOf:(NSTimeInterval)duration actualEnd = frame.time; [frames addObject:frame.imagePath]; } - - _currentPixelBuffer = [[SentryPixelBuffer alloc] initWithSize:CGSizeMake(_videoSize.width, _videoSize.height)]; - + + _currentPixelBuffer = + [[SentryPixelBuffer alloc] initWithSize:CGSizeMake(_videoSize.width, _videoSize.height)]; + [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue usingBlock:^{ UIImage *image = [UIImage imageWithContentsOfFile:frames[frameCount]]; if (image) { - CMTime presentTime = CMTimeMake(frameCount++, (int32_t)self->_frameRate); + CMTime presentTime + = CMTimeMake(frameCount++, (int32_t)self->_frameRate); if (![self->_currentPixelBuffer appendImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { if (completion) { completion(nil, videoWriter.error); } @@ -197,17 +201,22 @@ - (void)createVideoOf:(NSTimeInterval)duration SentryVideoInfo *videoInfo = nil; if (videoWriter.status == AVAssetWriterStatusCompleted) { + + NSInteger fileSize = + [SentryDependencyContainer.sharedInstance + .fileManager fileSize:outputFileURL]; + videoInfo = [[SentryVideoInfo alloc] - initWithHeight:(NSInteger) - self->_videoSize.height - width:(NSInteger) - self->_videoSize.width - duration:frames.count - frameCount:frames.count - frameRate:1 - start:start - end:actualEnd - ]; + initWithPath:outputFileURL + height:(NSInteger) + self->_videoSize.height + width:(NSInteger)self->_videoSize.width + duration:frames.count + frameCount:frames.count + frameRate:1 + start:start + end:actualEnd + fileSize:fileSize]; } completion(videoInfo, videoWriter.error); } @@ -218,28 +227,27 @@ - (void)createVideoOf:(NSTimeInterval)duration @end - - @implementation SentryPixelBuffer { CVPixelBufferRef _pixelBuffer; CGContextRef _context; CGColorSpaceRef _rgbColorSpace; } -- (nullable instancetype)initWithSize:(CGSize) size { +- (nullable instancetype)initWithSize:(CGSize)size +{ if (self = [super init]) { CVReturn status = kCVReturnSuccess; - - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, - (size_t)size.height, kCVPixelFormatType_32ARGB, NULL, &_pixelBuffer); - + + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, (size_t)size.height, + kCVPixelFormatType_32ARGB, NULL, &_pixelBuffer); + if (status != kCVReturnSuccess) { return nil; } void *pixelData = CVPixelBufferGetBaseAddress(_pixelBuffer); _rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - _context = CGBitmapContextCreate(pixelData, (size_t)size.width, - (size_t)size.height, 8, CVPixelBufferGetBytesPerRow(_pixelBuffer), _rgbColorSpace, + _context = CGBitmapContextCreate(pixelData, (size_t)size.width, (size_t)size.height, 8, + CVPixelBufferGetBytesPerRow(_pixelBuffer), _rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); CGContextTranslateCTM(_context, 0, size.height); @@ -248,20 +256,22 @@ - (nullable instancetype)initWithSize:(CGSize) size { return self; } -- (void)dealloc { +- (void)dealloc +{ CVPixelBufferRelease(_pixelBuffer); CGContextRelease(_context); CGColorSpaceRelease(_rgbColorSpace); } - (BOOL)appendImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime { CVPixelBufferLockBaseAddress(_pixelBuffer, 0); - - CGContextDrawImage(_context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); - + + CGContextDrawImage( + _context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); + CVPixelBufferUnlockBaseAddress(_pixelBuffer, 0); // Append the pixel buffer with the current image to the video diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 2338eb72a66..3f3d9a90488 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,5 +1,8 @@ #import "SentrySessionReplay.h" #import "SentryAttachment+Private.h" +#import "SentryCurrentDateProvider.h" +#import "SentryDependencyContainer.h" +#import "SentryFileManager.h" #import "SentryHub+Private.h" #import "SentryId.h" #import "SentryLog.h" @@ -9,9 +12,6 @@ #import "SentrySDK+Private.h" #import "SentrySwift.h" #import "SentryViewPhotographer.h" -#import "SentryDependencyContainer.h" -#import "SentryFileManager.h" -#import "SentryCurrentDateProvider.h" #if SENTRY_HAS_UIKIT @@ -41,7 +41,8 @@ - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions return self; } -- (SentryCurrentDateProvider *)dateProvider { +- (SentryCurrentDateProvider *)dateProvider +{ return SentryDependencyContainer.sharedInstance.dateProvider; } @@ -116,7 +117,6 @@ - (void)replayForEvent:(SentryEvent *)event; completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { [self captureSegment:0 video:videoInfo - videoUrl:finalPath replayId:[[SentryId alloc] init] replayType:kSentryReplayTypeBuffer]; }]; @@ -167,7 +167,6 @@ - (void)prepareSegmentUntil:(NSDate *)date completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { [self captureSegment:self->_currentSegmentId video:videoInfo - videoUrl:pathToSegment replayId:self->sessionReplayId replayType:kSentryReplayTypeSession]; @@ -178,7 +177,6 @@ - (void)prepareSegmentUntil:(NSDate *)date - (void)captureSegment:(NSInteger)segment video:(SentryVideoInfo *)videoInfo - videoUrl:(NSURL *)filePath replayId:(SentryId *)replayid replayType:(SentryReplayType)replayType { @@ -189,11 +187,9 @@ - (void)captureSegment:(NSInteger)segment replayEvent.segmentId = segment; replayEvent.timestamp = videoInfo.end; - NSInteger fileSize = [SentryDependencyContainer.sharedInstance.fileManager fileSize:filePath]; - SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId - size:fileSize + size:videoInfo.fileSize start:videoInfo.start duration:videoInfo.duration frameCount:videoInfo.frameCount @@ -201,9 +197,9 @@ - (void)captureSegment:(NSInteger)segment height:videoInfo.height width:videoInfo.width]; - [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:filePath]; - - SENTRY_LOG_DEBUG(@"Session replay: ReplayId: %@ \nAT: %@", replayEvent.eventId, filePath); + [SentrySDK.currentHub captureReplayEvent:replayEvent + replayRecording:recording + video:videoInfo.path]; } - (void)takeScreenshot diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift index b8d3fef269f..2d7518f9e7b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -3,6 +3,7 @@ import Foundation @objcMembers class SentryVideoInfo: NSObject { + let path: URL let height: Int let width: Int let duration: TimeInterval @@ -10,8 +11,9 @@ class SentryVideoInfo: NSObject { let frameRate: Int let start: Date let end: Date + let fileSize: Int - init(height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int, start: Date, end: Date) { + init(path: URL, height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int, start: Date, end: Date, fileSize: Int) { self.height = height self.width = width self.duration = duration @@ -19,6 +21,8 @@ class SentryVideoInfo: NSObject { self.frameRate = frameRate self.start = start self.end = end + self.path = path + self.fileSize = fileSize } } From f648e9307c267e4e1aed961696b9a9df217eb881 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 11 Mar 2024 17:21:21 +0100 Subject: [PATCH 43/88] More refactoring --- Sources/Sentry/SentryClient.m | 5 -- Sources/Sentry/SentrySessionReplay.m | 74 ++++++++++++------- .../SessionReplay/SentryReplayOptions.swift | 9 +++ 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 4e8b77e3632..7f20258948d 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -510,11 +510,6 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] items:@[ videoEnvelopeItem ]]; - NSData *data = [SentrySerialization dataWithEnvelope:envelope error:nil]; - - [data writeToURL:[videoURL URLByAppendingPathExtension:@"json"] atomically:YES]; - NSLog(@"### %@", videoURL); - [self captureEnvelope:envelope]; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 3f3d9a90488..3dad144433b 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -15,6 +15,8 @@ #if SENTRY_HAS_UIKIT +static NSString *SENTRY_REPLAY_FOLDER = @"replay"; + NS_ASSUME_NONNULL_BEGIN @implementation SentrySessionReplay { @@ -68,9 +70,9 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full _sessionStart = _lastScreenShot; _currentSegmentId = 0; - NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory - inDomains:NSUserDomainMask] - .firstObject URLByAppendingPathComponent:@"io.sentry"]; + NSURL *docs = [NSURL + fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; NSString *currentSession = [NSUUID UUID].UUIDString; _urlToCache = [docs URLByAppendingPathComponent:currentSession]; @@ -84,7 +86,8 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; _replayMaker.bitRate = _replayOptions.replayBitRate; - _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; + _replayMaker.cacheMaxSize = (NSInteger)(full ? _replayOptions.sessionSegmentDuration + : _replayOptions.errorReplayDuration); imageCollection = [NSMutableArray array]; NSLog(@"Recording session to %@", _urlToCache); @@ -104,22 +107,31 @@ - (void)stop - (void)replayForEvent:(SentryEvent *)event; { + if (_isFullSession) { + return; + } + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { return; } + if (_isFullSession) { + [self updateEvent:event withReplayId:sessionReplayId]; + return; + } + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - NSDate *replayStart = [[self dateProvider].date dateByAddingTimeInterval:-30]; + NSDate *replayStart = [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; - [_replayMaker createVideoOf:30 - from:replayStart - outputFileURL:finalPath - completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - [self captureSegment:0 - video:videoInfo - replayId:[[SentryId alloc] init] - replayType:kSentryReplayTypeBuffer]; - }]; + [self createAndCapture:finalPath duration:_replayOptions.errorReplayDuration startedAt:replayStart]; + + self->_isFullSession = YES; +} + +- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId { + NSMutableDictionary * context = [NSMutableDictionary dictionaryWithDictionary:event.context]; + context[@"session_replay"] = sentryId; + event.context = context; } - (void)newFrame:(CADisplayLink *)sender @@ -132,7 +144,7 @@ - (void)newFrame:(CADisplayLink *)sender if (_videoSegmentStart == nil) { _videoSegmentStart = now; - } else if (_isFullSession && [now timeIntervalSinceDate:_videoSegmentStart] >= 5) { + } else if (_isFullSession && [now timeIntervalSinceDate:_videoSegmentStart] >= _replayOptions.sessionSegmentDuration) { [self prepareSegmentUntil:now]; } } @@ -159,20 +171,28 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - NSDate *segmentStart = [[self dateProvider].date dateByAddingTimeInterval:-5]; + NSDate *segmentStart = [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + + [self createAndCapture:pathToSegment duration:_replayOptions.sessionSegmentDuration startedAt:segmentStart]; +} - [_replayMaker createVideoOf:5 - from:segmentStart - outputFileURL:pathToSegment +- (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { + [_replayMaker createVideoOf:duration + from:start + outputFileURL:videoUrl completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - [self captureSegment:self->_currentSegmentId - video:videoInfo - replayId:self->sessionReplayId - replayType:kSentryReplayTypeSession]; - - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); + } else { + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->sessionReplayId + replayType:kSentryReplayTypeSession]; + + [self->_replayMaker releaseFramesUntil:videoInfo.end]; + self->_videoSegmentStart = nil; + } + }]; } - (void)captureSegment:(NSInteger)segment diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 94ca1931bb0..218b7cd0c01 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -27,6 +27,10 @@ public class SentryReplayOptions: NSObject { */ let replayBitRate = 20_000 + /** + * Number of frames per second of the replay. + * The more the havier the process is. + */ let frameRate = 1 /** @@ -39,6 +43,11 @@ public class SentryReplayOptions: NSObject { */ let errorReplayDuration = TimeInterval(30) + /** + * The maximum duration of the segment of a session replay. + */ + let sessionSegmentDuration = TimeInterval(5) + /** * Inittialize session replay options disabled */ From e394969da66fed4a68701c3636eaf2436c0d3469 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 11 Mar 2024 16:22:20 +0000 Subject: [PATCH 44/88] Format code --- Sources/Sentry/SentrySessionReplay.m | 56 +++++++++++-------- .../SentryCrashMonitor_CPPException.cpp | 3 +- Tests/SentryTests/SentryInterfacesTests.m | 4 +- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 3dad144433b..36891b56fac 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -119,17 +119,21 @@ - (void)replayForEvent:(SentryEvent *)event; [self updateEvent:event withReplayId:sessionReplayId]; return; } - + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - NSDate *replayStart = [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + NSDate *replayStart = + [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + + [self createAndCapture:finalPath + duration:_replayOptions.errorReplayDuration + startedAt:replayStart]; - [self createAndCapture:finalPath duration:_replayOptions.errorReplayDuration startedAt:replayStart]; - self->_isFullSession = YES; } -- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId { - NSMutableDictionary * context = [NSMutableDictionary dictionaryWithDictionary:event.context]; +- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId +{ + NSMutableDictionary *context = [NSMutableDictionary dictionaryWithDictionary:event.context]; context[@"session_replay"] = sentryId; event.context = context; } @@ -144,7 +148,9 @@ - (void)newFrame:(CADisplayLink *)sender if (_videoSegmentStart == nil) { _videoSegmentStart = now; - } else if (_isFullSession && [now timeIntervalSinceDate:_videoSegmentStart] >= _replayOptions.sessionSegmentDuration) { + } else if (_isFullSession && + [now timeIntervalSinceDate:_videoSegmentStart] + >= _replayOptions.sessionSegmentDuration) { [self prepareSegmentUntil:now]; } } @@ -171,28 +177,34 @@ - (void)prepareSegmentUntil:(NSDate *)date pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - NSDate *segmentStart = [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + NSDate *segmentStart = + [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; - [self createAndCapture:pathToSegment duration:_replayOptions.sessionSegmentDuration startedAt:segmentStart]; + [self createAndCapture:pathToSegment + duration:_replayOptions.sessionSegmentDuration + startedAt:segmentStart]; } -- (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { +- (void)createAndCapture:(NSURL *)videoUrl + duration:(NSTimeInterval)duration + startedAt:(NSDate *)start +{ [_replayMaker createVideoOf:duration from:start outputFileURL:videoUrl completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); - } else { - [self captureSegment:self->_currentSegmentId++ - video:videoInfo - replayId:self->sessionReplayId - replayType:kSentryReplayTypeSession]; - - [self->_replayMaker releaseFramesUntil:videoInfo.end]; - self->_videoSegmentStart = nil; - } - }]; + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); + } else { + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->sessionReplayId + replayType:kSentryReplayTypeSession]; + + [self->_replayMaker releaseFramesUntil:videoInfo.end]; + self->_videoSegmentStart = nil; + } + }]; } - (void)captureSegment:(NSInteger)segment diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp index 784289a8622..ec93b882a73 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp @@ -135,8 +135,7 @@ CPPExceptionTerminate(void) strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff)); } #define CATCH_VALUE(TYPE, PRINTFTYPE) \ - catch (TYPE value) \ - { \ + catch (TYPE value) { \ snprintf(descriptionBuff, sizeof(descriptionBuff), "%" #PRINTFTYPE, value); \ } CATCH_VALUE(char, d) diff --git a/Tests/SentryTests/SentryInterfacesTests.m b/Tests/SentryTests/SentryInterfacesTests.m index 5d01ae94745..eaa4902954b 100644 --- a/Tests/SentryTests/SentryInterfacesTests.m +++ b/Tests/SentryTests/SentryInterfacesTests.m @@ -177,7 +177,7 @@ - (void)testTransactionEvent @1 : @"1", @2 : @2, @3 : @ { @"a" : @0 }, - @4 : @[ @"1", @2, @{ @"a" : @0 }, @[ @"a" ], testDate, testURL ], + @4 : @[ @"1", @2, @ { @"a" : @0 }, @[ @"a" ], testDate, testURL ], @5 : testDate, @6 : testURL } @@ -191,7 +191,7 @@ - (void)testTransactionEvent @"2" : @2, @"3" : @ { @"a" : @0 }, @"4" : @[ - @"1", @2, @{ @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", + @"1", @2, @ { @"a" : @0 }, @[ @"a" ], @"2020-02-27T11:35:26.000Z", @"https://sentry.io" ], @"5" : @"2020-02-27T11:35:26.000Z", From acddddc72710d938547f588abc4cdff41419ed65 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Mar 2024 10:09:13 +0100 Subject: [PATCH 45/88] context --- Sources/Sentry/SentrySessionReplay.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 3dad144433b..df0b6a02e49 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -130,7 +130,7 @@ - (void)replayForEvent:(SentryEvent *)event; - (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId { NSMutableDictionary * context = [NSMutableDictionary dictionaryWithDictionary:event.context]; - context[@"session_replay"] = sentryId; + context[@"replay_id"] = sentryId; event.context = context; } From 0dc0d923c61b90cc8b20b5fd40d970df71e08ef1 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 12 Mar 2024 09:15:29 +0000 Subject: [PATCH 46/88] Format code --- Sources/Sentry/SentrySessionReplay.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index b2da14a05f5..e675f7c68e2 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -131,8 +131,9 @@ - (void)replayForEvent:(SentryEvent *)event; self->_isFullSession = YES; } -- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId { - NSMutableDictionary * context = [NSMutableDictionary dictionaryWithDictionary:event.context]; +- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId +{ + NSMutableDictionary *context = [NSMutableDictionary dictionaryWithDictionary:event.context]; context[@"replay_id"] = sentryId; event.context = context; } From 049b933676b553c0f0627c716ce910573246d3d6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Mar 2024 15:02:24 +0100 Subject: [PATCH 47/88] fixed merging --- Sentry.xcodeproj/project.pbxproj | 103 +++++++++++++-------------- Sources/Sentry/SentrySessionReplay.m | 2 - 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 2fc58a9c642..fa53a456ed4 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -764,14 +764,6 @@ D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; - D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */; }; - D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */; }; - D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */; }; - D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */; }; - D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */; }; - D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */; }; - D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */; }; - D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */; }; D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; @@ -842,6 +834,16 @@ D8C66A372A77B1F70015696A /* SentryPropagationContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D8C66A352A77B1F70015696A /* SentryPropagationContext.m */; }; D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9928000E23007E326E /* SentryUIApplication.h */; }; D8C67E9C28000E24007E326E /* SentryScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9A28000E23007E326E /* SentryScreenshot.h */; }; + D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; + D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; + D8CAC0322BA0668C00E38F34 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */; }; + D8CAC0332BA0668C00E38F34 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */; }; + D8CAC0352BA066D000E38F34 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */; }; + D8CAC0372BA0676C00E38F34 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */; }; + D8CAC03A2BA0677F00E38F34 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */; }; + D8CAC03B2BA0677F00E38F34 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */; }; + D8CAC03D2BA067C000E38F34 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */; }; + D8CAC03F2BA067CF00E38F34 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */; }; D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -850,8 +852,6 @@ D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB742D294B294B00A5F964 /* MockUIScene.m */; }; D8CCFC632A1520C900DE232E /* SentryBinaryImageCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */; }; D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; - D8F016CD2B9741E0007B9AFB /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */; }; - D8F016DE2B975345007B9AFB /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */; }; D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B22B9622D6007B9AFB /* SentryId.swift */; }; D8F016B62B962548007B9AFB /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B52B962548007B9AFB /* StringExtensions.swift */; }; D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; @@ -1749,14 +1749,6 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; - D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; - D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; - D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; - D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; - D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; - D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; - D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; - D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; @@ -1833,6 +1825,16 @@ D8C66A352A77B1F70015696A /* SentryPropagationContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPropagationContext.m; sourceTree = ""; }; D8C67E9928000E23007E326E /* SentryUIApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryUIApplication.h; path = include/SentryUIApplication.h; sourceTree = ""; }; D8C67E9A28000E23007E326E /* SentryScreenshot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshot.h; path = include/SentryScreenshot.h; sourceTree = ""; }; + D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; + D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; + D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; + D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; + D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; + D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; + D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; + D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; + D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; @@ -1842,8 +1844,6 @@ D8CB742D294B294B00A5F964 /* MockUIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockUIScene.m; sourceTree = ""; }; D8CCFC622A1520C900DE232E /* SentryBinaryImageCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBinaryImageCacheTests.m; sourceTree = ""; }; D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegrationObjCTests.m; sourceTree = ""; }; - D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; - D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8F016B22B9622D6007B9AFB /* SentryId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryId.swift; sourceTree = ""; }; D8F016B52B962548007B9AFB /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; D8F01DE42A126B62008F4996 /* HybridPod.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = HybridPod.podspec; sourceTree = ""; }; @@ -2112,7 +2112,6 @@ 84C47B2B2A09239100DAEB8A /* .codecov.yml */, 844DA80628246D5000E6B62E /* .craft.yml */, 844DA80A28246D5000E6B62E /* .swiftlint.yml */, - 84281C552A579C2B00EE88F2 /* SentryTestUtilsObjc */, 6304360C1EC05CEF00C4D3FA /* Frameworks */, 6327C5D41EB8A783004E799B /* Products */, 63AA756E1EB8AEDB00D153DE /* Sources */, @@ -3241,13 +3240,6 @@ path = Profiling; sourceTree = ""; }; - 84281C552A579C2B00EE88F2 /* SentryTestUtilsObjc */ = { - isa = PBXGroup; - children = ( - ); - path = SentryTestUtilsObjc; - sourceTree = ""; - }; 8431EFDB29B27B3D00D8DC56 /* SentryProfilerTests */ = { isa = PBXGroup; children = ( @@ -3378,8 +3370,7 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( - D8F016CA2B97419A007B9AFB /* Integrations */, - D856272A2A374A6800FB8062 /* Tools */, + D8CAC02D2BA0663E00E38F34 /* Integrations */, 621D9F2D2B9B030E003D94DE /* Helper */, D8F016B42B962533007B9AFB /* Extensions */, 7BF65060292B8EFE00BBA5A8 /* MetricKit */, @@ -3418,14 +3409,14 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D83D07872B7E65F000CC9674 /* SentryViewPhotographer.h */, - D83D07852B7E65DC00CC9674 /* SentryViewPhotographer.m */, - D83D078B2B7E692300CC9674 /* SentryOnDemandReplay.h */, - D83D07892B7E691400CC9674 /* SentryOnDemandReplay.m */, - D83D07912B7E714B00CC9674 /* SentrySessionReplay.h */, - D83D078D2B7E712F00CC9674 /* SentrySessionReplay.m */, - D83D07932B7E715700CC9674 /* SentrySessionReplayIntegration.h */, - D83D078F2B7E713A00CC9674 /* SentrySessionReplayIntegration.m */, + D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */, + D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */, + D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */, + D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */, + D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */, + D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */, + D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */, + D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */, ); name = SessionReplay; sourceTree = ""; @@ -3570,21 +3561,23 @@ path = Resources; sourceTree = ""; }; - D8F016CA2B97419A007B9AFB /* Integrations */ = { + D8CAC02C2BA0663E00E38F34 /* SessionReplay */ = { isa = PBXGroup; children = ( - D8F016CB2B9741AA007B9AFB /* SessionReplay */, + D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, + D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, ); - path = Integrations; + path = SessionReplay; sourceTree = ""; }; - D8F016CB2B9741AA007B9AFB /* SessionReplay */ = { + D8CAC02D2BA0663E00E38F34 /* Integrations */ = { isa = PBXGroup; children = ( - D8F016CC2B9741E0007B9AFB /* SentryVideoInfo.swift */, - D8F016DD2B975345007B9AFB /* SentryReplayOptions.swift */, + D8CAC02C2BA0663E00E38F34 /* SessionReplay */, ); - path = SessionReplay; + path = Integrations; + sourceTree = ""; + }; D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( @@ -3661,6 +3654,7 @@ D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, 7D7F0A5F23DF3D2C00A4629C /* SentryGlobalEventProcessor.h in Headers */, D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */, + D8CAC0372BA0676C00E38F34 /* SentryOnDemandReplay.h in Headers */, 0A2D8D5D289815EB008720F6 /* SentryBaseIntegration.h in Headers */, 84AC61D629F75A98009EEF61 /* SentryDispatchFactory.h in Headers */, 63FE716520DA4C1100CDBAE8 /* SentryCrashMemory.h in Headers */, @@ -3673,15 +3667,16 @@ 8E4E7C6D25DAAAFE006AB9E2 /* SentryTransaction.h in Headers */, 63FE715D20DA4C1100CDBAE8 /* SentryCrashSymbolicator.h in Headers */, D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */, + D8CAC03A2BA0677F00E38F34 /* SentrySessionReplay.h in Headers */, 622C08D829E546F4002571D4 /* SentryTraceOrigins.h in Headers */, 7BECF42226145C5D00D9826E /* SentryMechanismMeta.h in Headers */, 63FE718920DA4C1100CDBAE8 /* SentryCrash.h in Headers */, 63AA769A1EB9C1C200D153DE /* SentryLog.h in Headers */, 7B56D73124616CCD00B842DA /* SentryConcurrentRateLimitsDictionary.h in Headers */, 03F84D2A27DD416B008FE43F /* SentryProfilingLogging.hpp in Headers */, + D8CAC03B2BA0677F00E38F34 /* SentrySessionReplayIntegration.h in Headers */, 63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */, 7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */, - D83D078C2B7E692300CC9674 /* SentryOnDemandReplay.h in Headers */, 63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */, 7BE912AB272162AF00E49E62 /* SentryNoOpSpan.h in Headers */, 63FE70D120DA4C1000CDBAE8 /* SentryCrashMonitorContext.h in Headers */, @@ -3753,7 +3748,6 @@ 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, - D83D07942B7E715700CC9674 /* SentrySessionReplayIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, 7BC9A20228F41350001E7C4C /* SentryMeasurementUnit.h in Headers */, @@ -3776,6 +3770,7 @@ 7B6C5EE0264E8E050010D138 /* SentryFramesTracker.h in Headers */, 63FE715720DA4C1100CDBAE8 /* SentryCrashThread.h in Headers */, 7BF9EF862722D10600B5BBEF /* SentryTestObjCRuntimeWrapper.h in Headers */, + D8CAC03F2BA067CF00E38F34 /* SentryViewPhotographer.h in Headers */, 15360CD2243277A000112302 /* SentrySessionTracker.h in Headers */, 63FE718B20DA4C1100CDBAE8 /* SentryCrashReport.h in Headers */, 7D0FCFB22379B915004DD83A /* SentryHub.h in Headers */, @@ -3876,7 +3871,6 @@ 6344DDB01EC308E400D9160D /* SentryCrashInstallationReporter.h in Headers */, 8E25C95725F836EE00DC215B /* SentryRandom.h in Headers */, 7B5CAF7527F5A67C00ED0DB6 /* SentryNSURLRequestBuilder.h in Headers */, - D83D07882B7E65F000CC9674 /* SentryViewPhotographer.h in Headers */, 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, 7BA61CB4247BC3EB00C130A8 /* SentryCrashBinaryImageProvider.h in Headers */, 63FE713D20DA4C1100CDBAE8 /* SentryCrashLogger.h in Headers */, @@ -3905,7 +3899,6 @@ 6334314120AD9AE40077E581 /* SentryMechanism.h in Headers */, 03F84D2827DD414C008FE43F /* SentryCPU.h in Headers */, 0A80E435291017D500095219 /* SentryWatchdogTerminationScopeObserver.h in Headers */, - D83D07922B7E714B00CC9674 /* SentrySessionReplay.h in Headers */, 7B610D642512399600B0B5D9 /* SentryHub+Private.h in Headers */, D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */, 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, @@ -4182,11 +4175,11 @@ 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, + D8CAC03D2BA067C000E38F34 /* SentryViewPhotographer.m in Sources */, 631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */, 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, 7B6C5EDE264E8DF00010D138 /* SentryFramesTracker.m in Sources */, - D83D07902B7E713A00CC9674 /* SentrySessionReplayIntegration.m in Sources */, D84F833E2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m in Sources */, 7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */, 84302A812B5767A50027A629 /* SentryLaunchProfiling.m in Sources */, @@ -4248,13 +4241,11 @@ 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, - D83D078E2B7E712F00CC9674 /* SentrySessionReplay.m in Sources */, 7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */, 63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */, 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, 630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */, 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */, - D8F016DE2B975345007B9AFB /* SentryReplayOptions.swift in Sources */, 7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */, 639FCFA11EBC804600778193 /* SentryException.m in Sources */, D80CD8D42B75144B002F710B /* SwiftDescriptor.swift in Sources */, @@ -4272,6 +4263,8 @@ D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */, A839D89A24864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m in Sources */, 7D082B8323C628790029866B /* SentryMeta.m in Sources */, + D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */, + D8CAC0332BA0668C00E38F34 /* SentrySessionReplayIntegration.m in Sources */, 63FE710720DA4C1000CDBAE8 /* SentryCrashStackCursor_SelfThread.m in Sources */, 63FE711120DA4C1000CDBAE8 /* SentryCrashDebug.c in Sources */, 7B883F49253D714C00879E62 /* SentryCrashUUIDConversion.c in Sources */, @@ -4294,6 +4287,7 @@ 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, 0A80E433291017C300095219 /* SentryWatchdogTerminationScopeObserver.m in Sources */, D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */, + D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */, 7BECF42826145CD900D9826E /* SentryMechanismMeta.m in Sources */, 8E7C982F2693D56000E6336C /* SentryTraceHeader.m in Sources */, 63FE715F20DA4C1100CDBAE8 /* SentryCrashID.c in Sources */, @@ -4320,7 +4314,6 @@ 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, - D83D07862B7E65DC00CC9674 /* SentryViewPhotographer.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, @@ -4390,6 +4383,7 @@ D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */, 8EBF870926140D37001A6853 /* SentryPerformanceTracker.m in Sources */, D80CD8D02B75143F002F710B /* UrlSanitized.swift in Sources */, + D8CAC0352BA066D000E38F34 /* SentryOnDemandReplay.m in Sources */, D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, 7BC9A20428F4166D001E7C4C /* SentryMeasurementValue.m in Sources */, @@ -4404,7 +4398,6 @@ D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */, 63FE711520DA4C1000CDBAE8 /* SentryCrashJSONCodec.c in Sources */, D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */, - D8F016CD2B9741E0007B9AFB /* SentryVideoInfo.swift in Sources */, 03F84D3327DD4191008FE43F /* SentryMachLogging.cpp in Sources */, D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */, 7B4E375F258231FC00059C93 /* SentryAttachment.m in Sources */, @@ -4414,12 +4407,12 @@ 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */, 7BC9A20628F41781001E7C4C /* SentryMeasurementUnit.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, + D8CAC0322BA0668C00E38F34 /* SentrySessionReplay.m in Sources */, 63FE713520DA4C1100CDBAE8 /* SentryCrashMemory.c in Sources */, 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, - D83D078A2B7E691400CC9674 /* SentryOnDemandReplay.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index e675f7c68e2..4551adc23cd 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,10 +1,8 @@ #import "SentrySessionReplay.h" #import "SentryAttachment+Private.h" -#import "SentryCurrentDateProvider.h" #import "SentryDependencyContainer.h" #import "SentryFileManager.h" #import "SentryHub+Private.h" -#import "SentryId.h" #import "SentryLog.h" #import "SentryOnDemandReplay.h" #import "SentryReplayEvent.h" From 0521b2273ebbe9c905fcc80571b9f9071aa03421 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 14 Mar 2024 10:31:05 +0100 Subject: [PATCH 48/88] wip --- Sources/Sentry/SentryOnDemandReplay.m | 56 ++++++++++++--------------- Sources/Sentry/SentrySessionReplay.m | 4 ++ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index 102a8eea145..7160905ed8c 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -28,7 +28,8 @@ - (instancetype)initWithPath:(NSString *)path time:(NSDate *)time @end @interface SentryPixelBuffer : NSObject -- (nullable instancetype)initWithSize:(CGSize)size; + +- (instancetype)initWithSize:(CGSize)size; - (BOOL)appendImage:(UIImage *)image pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor @@ -173,8 +174,7 @@ - (void)createVideoOf:(NSTimeInterval)duration [frames addObject:frame.imagePath]; } - _currentPixelBuffer = - [[SentryPixelBuffer alloc] initWithSize:CGSizeMake(_videoSize.width, _videoSize.height)]; + _currentPixelBuffer = [[SentryPixelBuffer alloc] initWithSize:_videoSize]; [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue @@ -228,57 +228,49 @@ - (void)createVideoOf:(NSTimeInterval)duration @end @implementation SentryPixelBuffer { - CVPixelBufferRef _pixelBuffer; - CGContextRef _context; - CGColorSpaceRef _rgbColorSpace; + CVPixelBufferRef pixelBuffer; + CGColorSpaceRef rgbColorSpace; + CGSize _size; } -- (nullable instancetype)initWithSize:(CGSize)size +- (instancetype)initWithSize:(CGSize)size { - if (self = [super init]) { - CVReturn status = kCVReturnSuccess; - - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, (size_t)size.height, - kCVPixelFormatType_32ARGB, NULL, &_pixelBuffer); + self = [super init]; + if (self) { + CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, + (size_t)size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); if (status != kCVReturnSuccess) { return nil; } - void *pixelData = CVPixelBufferGetBaseAddress(_pixelBuffer); - _rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - _context = CGBitmapContextCreate(pixelData, (size_t)size.width, (size_t)size.height, 8, - CVPixelBufferGetBytesPerRow(_pixelBuffer), _rgbColorSpace, - (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - - CGContextTranslateCTM(_context, 0, size.height); - CGContextScaleCTM(_context, 1.0, -1.0); + rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + _size = size; } return self; } - (void)dealloc { - CVPixelBufferRelease(_pixelBuffer); - CGContextRelease(_context); - CGColorSpaceRelease(_rgbColorSpace); + CVPixelBufferRelease(pixelBuffer); + CGColorSpaceRelease(rgbColorSpace); } - (BOOL)appendImage:(UIImage *)image pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor presentationTime:(CMTime)presentationTime { - CVPixelBufferLockBaseAddress(_pixelBuffer, 0); - - CGContextDrawImage( - _context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); - CVPixelBufferUnlockBaseAddress(_pixelBuffer, 0); + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - // Append the pixel buffer with the current image to the video - BOOL success = [pixelBufferAdaptor appendPixelBuffer:_pixelBuffer - withPresentationTime:presentationTime]; + CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)_size.width, + (size_t)_size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, + (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - return success; + CGContextDrawImage(context, CGRectMake(0, 0, _size.width, _size.height), image.CGImage); + CGContextRelease(context); + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return [pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]; } @end diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 4551adc23cd..6516b5be82e 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -187,6 +187,10 @@ - (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { + + if (sessionReplayId == nil) { + sessionReplayId = [[SentryId alloc] init]; + } [_replayMaker createVideoOf:duration from:start outputFileURL:videoUrl From b55c88cf7319665ee1355c98a52cb3f3c0712335 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 14 Mar 2024 16:32:18 +0100 Subject: [PATCH 49/88] SessionReplayIntegration in swift --- Sentry.xcodeproj/project.pbxproj | 12 +-- .../Sentry/SentrySessionReplayIntegration.m | 79 ------------------- Sources/Sentry/include/SentryPrivate.h | 5 +- .../include/SentrySessionReplayIntegration.h | 11 --- .../SentrySessionReplayIntegration.swift | 55 +++++++++++++ 5 files changed, 63 insertions(+), 99 deletions(-) delete mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m delete mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 220c289343f..1965b4e3543 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -839,14 +839,13 @@ D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; D8CAC0322BA0668C00E38F34 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */; }; - D8CAC0332BA0668C00E38F34 /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */; }; D8CAC0352BA066D000E38F34 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */; }; D8CAC0372BA0676C00E38F34 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */; }; D8CAC03A2BA0677F00E38F34 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */; }; - D8CAC03B2BA0677F00E38F34 /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */; }; D8CAC03D2BA067C000E38F34 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */; }; D8CAC03F2BA067CF00E38F34 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; + D8CAC06F2BA337F800E38F34 /* SentrySessionReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */; }; D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1833,14 +1832,13 @@ D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; - D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; - D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; + D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegration.swift; sourceTree = ""; }; D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; @@ -3434,8 +3432,6 @@ D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */, D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */, - D8CAC0392BA0677E00E38F34 /* SentrySessionReplayIntegration.h */, - D8CAC0312BA0668C00E38F34 /* SentrySessionReplayIntegration.m */, D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */, D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */, D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */, @@ -3589,6 +3585,7 @@ children = ( D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, + D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3699,7 +3696,6 @@ 63AA769A1EB9C1C200D153DE /* SentryLog.h in Headers */, 7B56D73124616CCD00B842DA /* SentryConcurrentRateLimitsDictionary.h in Headers */, 03F84D2A27DD416B008FE43F /* SentryProfilingLogging.hpp in Headers */, - D8CAC03B2BA0677F00E38F34 /* SentrySessionReplayIntegration.h in Headers */, 63FE714D20DA4C1100CDBAE8 /* SentryCrashJSONCodec.h in Headers */, 7BAF3DD4243DD40F008A5414 /* SentryTransportFactory.h in Headers */, 63FE717F20DA4C1100CDBAE8 /* SentryCrashReportFields.h in Headers */, @@ -4289,7 +4285,6 @@ A839D89A24864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m in Sources */, 7D082B8323C628790029866B /* SentryMeta.m in Sources */, D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */, - D8CAC0332BA0668C00E38F34 /* SentrySessionReplayIntegration.m in Sources */, 63FE710720DA4C1000CDBAE8 /* SentryCrashStackCursor_SelfThread.m in Sources */, 63FE711120DA4C1000CDBAE8 /* SentryCrashDebug.c in Sources */, 7B883F49253D714C00879E62 /* SentryCrashUUIDConversion.c in Sources */, @@ -4414,6 +4409,7 @@ D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, 7BC9A20428F4166D001E7C4C /* SentryMeasurementValue.m in Sources */, + D8CAC06F2BA337F800E38F34 /* SentrySessionReplayIntegration.swift in Sources */, D859696B27BECD8F0036A46E /* SentryCoreDataTrackingIntegration.m in Sources */, 7BD86EC7264A641D005439DB /* SentrySysctl.m in Sources */, 84281C432A578E5600EE88F2 /* SentryProfilerState.mm in Sources */, diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m deleted file mode 100644 index b801f4c1a52..00000000000 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ /dev/null @@ -1,79 +0,0 @@ -#import "SentrySessionReplayIntegration.h" -#import "SentryClient+Private.h" -#import "SentryDependencyContainer.h" -#import "SentryGlobalEventProcessor.h" -#import "SentryHub+Private.h" -#import "SentryOptions.h" -#import "SentryRandom.h" -#import "SentrySDK+Private.h" -#import "SentrySessionReplay.h" -#import "SentrySwift.h" - -#if SENTRY_HAS_UIKIT -# import "SentryUIApplication.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation SentrySessionReplayIntegration { - SentrySessionReplay *sessionReplay; -} - -- (BOOL)installWithOptions:(nonnull SentryOptions *)options -{ - if ([super installWithOptions:options] == NO) { - return NO; - } - - if (@available(iOS 16.0, tvOS 16.0, *)) { - if (options.sessionReplayOptions.replaysSessionSampleRate == 0 - && options.sessionReplayOptions.replaysOnErrorSampleRate == 0) { - return NO; - } - - sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; - - [sessionReplay - start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplayOptions - .replaysSessionSampleRate]]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(stop) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - - [SentryGlobalEventProcessor.shared - addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - [self->sessionReplay replayForEvent:event]; - return event; - }]; - - return YES; - } else { - return NO; - } -} - -- (void)stop -{ - [sessionReplay stop]; -} - -- (SentryIntegrationOption)integrationOptions -{ - return kIntegrationOptionEnableReplay; -} - -- (void)uninstall -{ -} - -- (BOOL)shouldReplayFullSession:(CGFloat)rate -{ - return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; -} - -@end -NS_ASSUME_NONNULL_END - -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 9a17ee9c8d8..b885b9ed2f5 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,7 +1,10 @@ // Sentry internal headers that are needed for swift code #import "SentryBaggage.h" -#import "SentryBaseIntegration.h" +#import "SentryDependencyContainer.h" +#import "SentryGlobalEventProcessor.h" #import "SentryRandom.h" #import "SentrySdkInfo.h" +#import "SentrySessionReplay.h" #import "SentryTime.h" +#import "SentryUIApplication.h" diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h deleted file mode 100644 index b789b91009a..00000000000 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ /dev/null @@ -1,11 +0,0 @@ -#import "SentryBaseIntegration.h" -#import "SentryIntegrationProtocol.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentrySessionReplayIntegration : SentryBaseIntegration - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift new file mode 100644 index 00000000000..31e81035de4 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift @@ -0,0 +1,55 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +#if canImport(UIKit) +import UIKit + +@objcMembers +class SentrySessionReplayIntegration: NSObject, SentryIntegrationProtocol { + + private var sessionReplay: SentrySessionReplay? + + func install(with options: Options) -> Bool { + if #available(iOS 16.0, tvOS 16, *) { + if options.sessionReplayOptions.replaysSessionSampleRate == 0 && + options.sessionReplayOptions.replaysOnErrorSampleRate == 0 { + print("[SessionReplayIntegration] No enabled because sessionReplayOptions is disabled.") + return false + } + + guard let window = SentryDependencyContainer.sharedInstance().application.windows?.first else { + print("[SessionReplayIntegration] No window to record") + return false + } + + sessionReplay = SentrySessionReplay(settings: options.sessionReplayOptions) + sessionReplay?.start(window, fullSession: shouldReplayFullSession(sampleRate: options.sessionReplayOptions.replaysSessionSampleRate)) + + NotificationCenter.default.addObserver(self, selector: #selector(stop), name: UIApplication.didEnterBackgroundNotification, object: nil) + + SentryGlobalEventProcessor.shared().add { event in + self.sessionReplay?.replay(for: event) + return event + } + + } else { + print("[SessionReplayIntegration] OS version not supported for session replay. Requires iOS 16 or tvOS 16") + return false + } + return true + } + + func stop() { + sessionReplay?.stop() + } + + func uninstall() { + stop() + } + + func shouldReplayFullSession(sampleRate: Float) -> Bool { + return SentryDependencyContainer.sharedInstance().random.nextNumber() < Double(sampleRate) + } +} + +#endif // canImport(UIKit) From d261a3ad69bc85df8ed9437d0be68d004725bacd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 14 Mar 2024 16:33:43 +0100 Subject: [PATCH 50/88] Update Package.swift --- Package.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 1f9b2083be1..049aa3949d0 100644 --- a/Package.swift +++ b/Package.swift @@ -10,15 +10,11 @@ let package = Package( .library(name: "SentrySwiftUI", targets: ["Sentry", "SentrySwiftUI"]) ], targets: [ - -.binaryTarget( - name: "", - url: "", - checksum: "" -) - - , - + .binaryTarget( + name: "Sentry", + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry.xcframework.zip", + checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Static + ), .binaryTarget( name: "Sentry-Dynamic", url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry-Dynamic.xcframework.zip", From c46aae860f5e380f46fa063a15f63637b1322d40 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 14 Mar 2024 16:34:26 +0100 Subject: [PATCH 51/88] Update Package.swift --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 049aa3949d0..dc9cb29114b 100644 --- a/Package.swift +++ b/Package.swift @@ -11,10 +11,10 @@ let package = Package( ], targets: [ .binaryTarget( - name: "Sentry", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry.xcframework.zip", - checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Static - ), + name: "Sentry", + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry.xcframework.zip", + checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Static + ), .binaryTarget( name: "Sentry-Dynamic", url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry-Dynamic.xcframework.zip", From 649ff4b9a884bbcf31844b350da58956dc7cdafd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 18 Mar 2024 16:41:33 +0100 Subject: [PATCH 52/88] Make it swift --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 5 + Sentry.xcodeproj/project.pbxproj | 40 +-- Sources/Sentry/SentryOnDemandReplay.m | 277 ------------------ Sources/Sentry/SentryOptions.m | 1 - Sources/Sentry/SentrySessionReplay.m | 264 ----------------- Sources/Sentry/SentryViewPhotographer.m | 159 ---------- Sources/Sentry/include/SentryOnDemandReplay.h | 36 --- Sources/Sentry/include/SentryPrivate.h | 7 +- Sources/Sentry/include/SentrySessionReplay.h | 32 -- .../Sentry/include/SentryViewPhotographer.h | 19 -- .../SessionReplay/SentryOnDemandReplay.swift | 158 ++++++++++ .../SessionReplay/SentryPixelBuffer.swift | 44 +++ .../SessionReplay/SentrySessionReplay.swift | 167 +++++++++++ .../SentrySessionReplayIntegration.swift | 6 +- .../SentryViewPhotographer.swift | 108 +++++++ 15 files changed, 507 insertions(+), 816 deletions(-) delete mode 100644 Sources/Sentry/SentryOnDemandReplay.m delete mode 100644 Sources/Sentry/SentrySessionReplay.m delete mode 100644 Sources/Sentry/SentryViewPhotographer.m delete mode 100644 Sources/Sentry/include/SentryOnDemandReplay.h delete mode 100644 Sources/Sentry/include/SentrySessionReplay.h delete mode 100644 Sources/Sentry/include/SentryViewPhotographer.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index d889305b442..0a4644e39ba 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -1,6 +1,11 @@ import Sentry import UIKit +func log(_ message: String) { + SentrySDK.addBreadcrumb(Breadcrumb(level: .debug, category: message)) + NSLog("%@", message) +} + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 1965b4e3543..72d58adbee5 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -743,6 +743,8 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; + D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; + D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */; }; D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */; }; D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */; }; D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */ = {isa = PBXBuildFile; fileRef = D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */; }; @@ -838,14 +840,10 @@ D8C67E9C28000E24007E326E /* SentryScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9A28000E23007E326E /* SentryScreenshot.h */; }; D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; - D8CAC0322BA0668C00E38F34 /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */; }; - D8CAC0352BA066D000E38F34 /* SentryOnDemandReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */; }; - D8CAC0372BA0676C00E38F34 /* SentryOnDemandReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */; }; - D8CAC03A2BA0677F00E38F34 /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */; }; - D8CAC03D2BA067C000E38F34 /* SentryViewPhotographer.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */; }; - D8CAC03F2BA067CF00E38F34 /* SentryViewPhotographer.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; D8CAC06F2BA337F800E38F34 /* SentrySessionReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */; }; + D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */; }; + D8CAC0752BA4843A00E38F34 /* SentrySessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */; }; D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1728,6 +1726,8 @@ A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; + D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplay.swift; sourceTree = ""; }; + D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPixelBuffer.swift; sourceTree = ""; }; D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEventTests.swift; sourceTree = ""; }; D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayRecordingTests.swift; sourceTree = ""; }; D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayType.h; path = include/SentryReplayType.h; sourceTree = ""; }; @@ -1831,14 +1831,10 @@ D8C67E9A28000E23007E326E /* SentryScreenshot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshot.h; path = include/SentryScreenshot.h; sourceTree = ""; }; D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; - D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; - D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryOnDemandReplay.m; sourceTree = ""; }; - D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryOnDemandReplay.h; path = include/SentryOnDemandReplay.h; sourceTree = ""; }; - D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; - D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryViewPhotographer.m; sourceTree = ""; }; - D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryViewPhotographer.h; path = include/SentryViewPhotographer.h; sourceTree = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegration.swift; sourceTree = ""; }; + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographer.swift; sourceTree = ""; }; + D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplay.swift; sourceTree = ""; }; D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; @@ -3430,12 +3426,6 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D8CAC0382BA0677E00E38F34 /* SentrySessionReplay.h */, - D8CAC0302BA0668C00E38F34 /* SentrySessionReplay.m */, - D8CAC0362BA0676C00E38F34 /* SentryOnDemandReplay.h */, - D8CAC0342BA066D000E38F34 /* SentryOnDemandReplay.m */, - D8CAC03E2BA067CF00E38F34 /* SentryViewPhotographer.h */, - D8CAC03C2BA067C000E38F34 /* SentryViewPhotographer.m */, ); name = SessionReplay; sourceTree = ""; @@ -3586,6 +3576,10 @@ D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */, + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, + D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */, + D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, + D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3676,7 +3670,6 @@ D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, 7D7F0A5F23DF3D2C00A4629C /* SentryGlobalEventProcessor.h in Headers */, D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */, - D8CAC0372BA0676C00E38F34 /* SentryOnDemandReplay.h in Headers */, 0A2D8D5D289815EB008720F6 /* SentryBaseIntegration.h in Headers */, 84AC61D629F75A98009EEF61 /* SentryDispatchFactory.h in Headers */, 63FE716520DA4C1100CDBAE8 /* SentryCrashMemory.h in Headers */, @@ -3689,7 +3682,6 @@ 8E4E7C6D25DAAAFE006AB9E2 /* SentryTransaction.h in Headers */, 63FE715D20DA4C1100CDBAE8 /* SentryCrashSymbolicator.h in Headers */, D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */, - D8CAC03A2BA0677F00E38F34 /* SentrySessionReplay.h in Headers */, 622C08D829E546F4002571D4 /* SentryTraceOrigins.h in Headers */, 7BECF42226145C5D00D9826E /* SentryMechanismMeta.h in Headers */, 63FE718920DA4C1100CDBAE8 /* SentryCrash.h in Headers */, @@ -3791,7 +3783,6 @@ 7B6C5EE0264E8E050010D138 /* SentryFramesTracker.h in Headers */, 63FE715720DA4C1100CDBAE8 /* SentryCrashThread.h in Headers */, 7BF9EF862722D10600B5BBEF /* SentryTestObjCRuntimeWrapper.h in Headers */, - D8CAC03F2BA067CF00E38F34 /* SentryViewPhotographer.h in Headers */, 15360CD2243277A000112302 /* SentrySessionTracker.h in Headers */, 63FE718B20DA4C1100CDBAE8 /* SentryCrashReport.h in Headers */, 7D0FCFB22379B915004DD83A /* SentryHub.h in Headers */, @@ -4196,7 +4187,6 @@ 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, - D8CAC03D2BA067C000E38F34 /* SentryViewPhotographer.m in Sources */, 631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */, 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, @@ -4226,6 +4216,7 @@ 15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */, 7BC85235245880AE005A70F0 /* SentryDataCategoryMapper.m in Sources */, 7B7A30C824B48389005A4C6E /* SentryCrashWrapper.m in Sources */, + D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */, D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */, 63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */, 63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */, @@ -4328,6 +4319,7 @@ 63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */, A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */, 7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */, + D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */, 844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */, D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */, @@ -4369,6 +4361,7 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, + D8CAC0752BA4843A00E38F34 /* SentrySessionReplay.swift in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */, @@ -4405,7 +4398,6 @@ D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */, 8EBF870926140D37001A6853 /* SentryPerformanceTracker.m in Sources */, D80CD8D02B75143F002F710B /* UrlSanitized.swift in Sources */, - D8CAC0352BA066D000E38F34 /* SentryOnDemandReplay.m in Sources */, D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, 7BC9A20428F4166D001E7C4C /* SentryMeasurementValue.m in Sources */, @@ -4418,6 +4410,7 @@ 8453421228BE855D00C22EEC /* SentrySampleDecision.m in Sources */, 7B7D872E2486482600D2ECFF /* SentryStacktraceBuilder.m in Sources */, 861265FA2404EC1500C4AFDE /* NSArray+SentrySanitize.m in Sources */, + D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */, D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */, 63FE711520DA4C1000CDBAE8 /* SentryCrashJSONCodec.c in Sources */, D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */, @@ -4430,7 +4423,6 @@ 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */, 7BC9A20628F41781001E7C4C /* SentryMeasurementUnit.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, - D8CAC0322BA0668C00E38F34 /* SentrySessionReplay.m in Sources */, 63FE713520DA4C1100CDBAE8 /* SentryCrashMemory.c in Sources */, 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m deleted file mode 100644 index 7160905ed8c..00000000000 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ /dev/null @@ -1,277 +0,0 @@ -#import "SentryOnDemandReplay.h" - -#if SENTRY_HAS_UIKIT -# import "SentryDependencyContainer.h" -# import "SentryFileManager.h" -# import "SentryLog.h" -# import -# import - -@interface SentryReplayFrame : NSObject - -@property (nonatomic, strong) NSString *imagePath; -@property (nonatomic, strong) NSDate *time; - -- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time; - -@end - -@implementation SentryReplayFrame -- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time -{ - if (self = [super init]) { - self.imagePath = path; - self.time = time; - } - return self; -} -@end - -@interface SentryPixelBuffer : NSObject - -- (instancetype)initWithSize:(CGSize)size; - -- (BOOL)appendImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime; -@end - -@implementation SentryOnDemandReplay { - NSString *_outputPath; - NSDate *_startTime; - NSMutableArray *_frames; - dispatch_queue_t _onDemandDispatchQueue; - SentryPixelBuffer *_currentPixelBuffer; -} - -- (instancetype)initWithOutputPath:(NSString *)outputPath -{ - if (self = [super init]) { - _outputPath = outputPath; - _startTime = [[NSDate alloc] init]; - _frames = [NSMutableArray array]; - _videoSize = CGSizeMake(200, 434); - _bitRate = 20000; - _cacheMaxSize = NSUIntegerMax; - _frameRate = 1; - _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); - } - return self; -} - -- (void)addFrame:(UIImage *)image -{ - dispatch_async(_onDemandDispatchQueue, ^{ - NSData *data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); - NSDate *date = [[NSDate alloc] init]; - NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; - NSString *imagePath = [self->_outputPath - stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; - - [data writeToFile:imagePath atomically:YES]; - - SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; - [self->_frames addObject:frame]; - - while (self->_frames.count > self->_cacheMaxSize) { - [self removeOldestFrame]; - } - }); -} - -- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth -{ - CGSize originalSize = originalImage.size; - CGFloat aspectRatio = originalSize.width / originalSize.height; - - CGFloat newWidth = MIN(originalSize.width, maxWidth); - CGFloat newHeight = newWidth / aspectRatio; - - CGSize newSize = CGSizeMake(newWidth, newHeight); - - UIGraphicsBeginImageContextWithOptions(newSize, NO, self.frameRate); - [originalImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return resizedImage; -} - -- (void)releaseFramesUntil:(NSDate *)date -{ - dispatch_async(_onDemandDispatchQueue, ^{ - while (self->_frames.count > 0 && - [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { - [self removeOldestFrame]; - } - }); -} - -- (void)removeOldestFrame -{ - NSError *error; - if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath - error:&error]) { - SENTRY_LOG_DEBUG( - @"Could not delete replay frame at: %@. %@", _frames.firstObject.imagePath, error); - } - [_frames removeObjectAtIndex:0]; -} - -- (void)createVideoOf:(NSTimeInterval)duration - from:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(SentryVideoInfo *, NSError *error))completion -{ - // Set up AVAssetWriter with appropriate settings - AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL - fileType:AVFileTypeQuickTimeMovie - error:nil]; - - NSDictionary *videoSettings = @{ - AVVideoCodecKey : AVVideoCodecTypeH264, - AVVideoWidthKey : @(_videoSize.width), - AVVideoHeightKey : @(_videoSize.height), - AVVideoCompressionPropertiesKey : @ { - AVVideoAverageBitRateKey : @(_bitRate), - AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel, - }, - }; - - AVAssetWriterInput *videoWriterInput = - [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo - outputSettings:videoSettings]; - NSDictionary *bufferAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB), - }; - - AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor - assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput - sourcePixelBufferAttributes:bufferAttributes]; - - [videoWriter addInput:videoWriterInput]; - - // Start writing video - [videoWriter startWriting]; - [videoWriter startSessionAtSourceTime:kCMTimeZero]; - - NSDate *end = [beginning dateByAddingTimeInterval:duration]; - __block NSInteger frameCount = 0; - NSMutableArray *frames = [NSMutableArray array]; - - NSDate *start = [NSDate date]; - NSDate *actualEnd = nil; - for (SentryReplayFrame *frame in self->_frames) { - if ([frame.time compare:beginning] == NSOrderedAscending) { - continue; - } else if ([frame.time compare:end] == NSOrderedDescending) { - break; - } - if ([frame.time compare:start] == NSOrderedAscending) { - start = frame.time; - } - actualEnd = frame.time; - [frames addObject:frame.imagePath]; - } - - _currentPixelBuffer = [[SentryPixelBuffer alloc] initWithSize:_videoSize]; - - [videoWriterInput - requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue - usingBlock:^{ - UIImage *image = - [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime - = CMTimeMake(frameCount++, (int32_t)self->_frameRate); - - if (![self->_currentPixelBuffer appendImage:image - pixelBufferAdaptor:pixelBufferAdaptor - presentationTime:presentTime]) { - if (completion) { - completion(nil, videoWriter.error); - } - } - } - - if (frameCount >= frames.count) { - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - SentryVideoInfo *videoInfo = nil; - if (videoWriter.status - == AVAssetWriterStatusCompleted) { - - NSInteger fileSize = - [SentryDependencyContainer.sharedInstance - .fileManager fileSize:outputFileURL]; - - videoInfo = [[SentryVideoInfo alloc] - initWithPath:outputFileURL - height:(NSInteger) - self->_videoSize.height - width:(NSInteger)self->_videoSize.width - duration:frames.count - frameCount:frames.count - frameRate:1 - start:start - end:actualEnd - fileSize:fileSize]; - } - completion(videoInfo, videoWriter.error); - } - }]; - } - }]; -} - -@end - -@implementation SentryPixelBuffer { - CVPixelBufferRef pixelBuffer; - CGColorSpaceRef rgbColorSpace; - CGSize _size; -} - -- (instancetype)initWithSize:(CGSize)size -{ - self = [super init]; - if (self) { - CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, - (size_t)size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); - - if (status != kCVReturnSuccess) { - return nil; - } - rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - _size = size; - } - return self; -} - -- (void)dealloc -{ - CVPixelBufferRelease(pixelBuffer); - CGColorSpaceRelease(rgbColorSpace); -} - -- (BOOL)appendImage:(UIImage *)image - pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor - presentationTime:(CMTime)presentationTime -{ - - CVPixelBufferLockBaseAddress(pixelBuffer, 0); - void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - - CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)_size.width, - (size_t)_size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, - (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - - CGContextDrawImage(context, CGRectMake(0, 0, _size.width, _size.height), image.CGImage); - CGContextRelease(context); - CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - return [pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]; -} - -@end -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index b309c386adf..31162e8c93b 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,7 +23,6 @@ # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" # import "SentryScreenshotIntegration.h" -# import "SentrySessionReplayIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m deleted file mode 100644 index 6516b5be82e..00000000000 --- a/Sources/Sentry/SentrySessionReplay.m +++ /dev/null @@ -1,264 +0,0 @@ -#import "SentrySessionReplay.h" -#import "SentryAttachment+Private.h" -#import "SentryDependencyContainer.h" -#import "SentryFileManager.h" -#import "SentryHub+Private.h" -#import "SentryLog.h" -#import "SentryOnDemandReplay.h" -#import "SentryReplayEvent.h" -#import "SentryReplayRecording.h" -#import "SentrySDK+Private.h" -#import "SentrySwift.h" -#import "SentryViewPhotographer.h" - -#if SENTRY_HAS_UIKIT - -static NSString *SENTRY_REPLAY_FOLDER = @"replay"; - -NS_ASSUME_NONNULL_BEGIN - -@implementation SentrySessionReplay { - UIView *_rootView; - BOOL _processingScreenshot; - CADisplayLink *_displayLink; - NSDate *_lastScreenShot; - NSDate *_videoSegmentStart; - NSURL *_urlToCache; - NSDate *_sessionStart; - SentryReplayOptions *_replayOptions; - SentryOnDemandReplay *_replayMaker; - SentryId *sessionReplayId; - NSMutableArray *imageCollection; - int _currentSegmentId; - BOOL _isFullSession; -} - -- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions -{ - if (self = [super init]) { - _replayOptions = replayOptions; - } - return self; -} - -- (SentryCurrentDateProvider *)dateProvider -{ - return SentryDependencyContainer.sharedInstance.dateProvider; -} - -- (void)start:(UIView *)rootView fullSession:(BOOL)full -{ - if (rootView == nil) { - SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); - return; - } - - @synchronized(self) { - if (_displayLink == nil) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } else { - // Session display is already running. - return; - } - - _rootView = rootView; - _lastScreenShot = [[NSDate alloc] init]; - _videoSegmentStart = nil; - _sessionStart = _lastScreenShot; - _currentSegmentId = 0; - - NSURL *docs = [NSURL - fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; - docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; - - NSString *currentSession = [NSUUID UUID].UUIDString; - _urlToCache = [docs URLByAppendingPathComponent:currentSession]; - - if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache - withIntermediateDirectories:YES - attributes:nil - error:nil]; - } - - _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; - _replayMaker.bitRate = _replayOptions.replayBitRate; - _replayMaker.cacheMaxSize = (NSInteger)(full ? _replayOptions.sessionSegmentDuration - : _replayOptions.errorReplayDuration); - imageCollection = [NSMutableArray array]; - - NSLog(@"Recording session to %@", _urlToCache); - - _isFullSession = full; - if (full) { - sessionReplayId = [[SentryId alloc] init]; - } - } -} - -- (void)stop -{ - [_displayLink invalidate]; - _displayLink = nil; -} - -- (void)replayForEvent:(SentryEvent *)event; -{ - if (_isFullSession) { - return; - } - - if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { - return; - } - - if (_isFullSession) { - [self updateEvent:event withReplayId:sessionReplayId]; - return; - } - - NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - NSDate *replayStart = - [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; - - [self createAndCapture:finalPath - duration:_replayOptions.errorReplayDuration - startedAt:replayStart]; - - self->_isFullSession = YES; -} - -- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId -{ - NSMutableDictionary *context = [NSMutableDictionary dictionaryWithDictionary:event.context]; - context[@"replay_id"] = sentryId; - event.context = context; -} - -- (void)newFrame:(CADisplayLink *)sender -{ - NSDate *now = [self dateProvider].date; - - if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { - [self takeScreenshot]; - _lastScreenShot = now; - - if (_videoSegmentStart == nil) { - _videoSegmentStart = now; - } else if (_isFullSession && - [now timeIntervalSinceDate:_videoSegmentStart] - >= _replayOptions.sessionSegmentDuration) { - [self prepareSegmentUntil:now]; - } - } -} - -- (void)prepareSegmentUntil:(NSDate *)date -{ - NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; - NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; - NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; - - if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { - NSError *error; - if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path - withIntermediateDirectories:YES - attributes:nil - error:&error]) { - SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", - error.localizedDescription); - return; - } - } - - pathToSegment = [pathToSegment - URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; - - NSDate *segmentStart = - [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; - - [self createAndCapture:pathToSegment - duration:_replayOptions.sessionSegmentDuration - startedAt:segmentStart]; -} - -- (void)createAndCapture:(NSURL *)videoUrl - duration:(NSTimeInterval)duration - startedAt:(NSDate *)start -{ - - if (sessionReplayId == nil) { - sessionReplayId = [[SentryId alloc] init]; - } - [_replayMaker createVideoOf:duration - from:start - outputFileURL:videoUrl - completion:^(SentryVideoInfo *videoInfo, NSError *_Nonnull error) { - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); - } else { - [self captureSegment:self->_currentSegmentId++ - video:videoInfo - replayId:self->sessionReplayId - replayType:kSentryReplayTypeSession]; - - [self->_replayMaker releaseFramesUntil:videoInfo.end]; - self->_videoSegmentStart = nil; - } - }]; -} - -- (void)captureSegment:(NSInteger)segment - video:(SentryVideoInfo *)videoInfo - replayId:(SentryId *)replayid - replayType:(SentryReplayType)replayType -{ - SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] init]; - replayEvent.replayType = replayType; - replayEvent.eventId = replayid; - replayEvent.replayStartTimestamp = videoInfo.start; - replayEvent.segmentId = segment; - replayEvent.timestamp = videoInfo.end; - - SentryReplayRecording *recording = - [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId - size:videoInfo.fileSize - start:videoInfo.start - duration:videoInfo.duration - frameCount:videoInfo.frameCount - frameRate:videoInfo.frameRate - height:videoInfo.height - width:videoInfo.width]; - - [SentrySDK.currentHub captureReplayEvent:replayEvent - replayRecording:recording - video:videoInfo.path]; -} - -- (void)takeScreenshot -{ - if (_processingScreenshot) { - return; - } - @synchronized(self) { - if (_processingScreenshot) { - return; - } - _processingScreenshot = YES; - } - - UIImage *screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; - - _processingScreenshot = NO; - - dispatch_queue_t backgroundQueue - = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrame:screenshot]; }); -} - -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m deleted file mode 100644 index 188ab718ab9..00000000000 --- a/Sources/Sentry/SentryViewPhotographer.m +++ /dev/null @@ -1,159 +0,0 @@ -#import "SentryViewPhotographer.h" - -#if SENTRY_HAS_UIKIT -# import - -NS_ASSUME_NONNULL_BEGIN - -@implementation SentryViewPhotographer { - NSMutableArray *_ignoreClasses; - NSMutableArray *_redactClasses; -} - -+ (SentryViewPhotographer *)shared -{ - static SentryViewPhotographer *_shared = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ _shared = [[SentryViewPhotographer alloc] init]; }); - - return _shared; -} - -- (instancetype)init -{ - if (self = [super init]) { -# if TARGET_OS_IOS - _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; -# endif - _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; - - NSArray *extraClasses = @[ - @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", - @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - @"SwiftUI._UIGraphicsView", @"SwiftUI.ImageLayer" - ]; - - for (NSString *className in extraClasses) { - Class viewClass = NSClassFromString(className); - if (viewClass != nil) { - [_redactClasses addObject:viewClass]; - } - } - } - return self; -} - -- (UIImage *)imageFromUIView:(UIView *)view -{ - UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); - CGContextRef currentContext = UIGraphicsGetCurrentContext(); - - [view.layer renderInContext:currentContext]; - - [self maskText:view context:currentContext]; - - UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return screenshot; -} - -- (void)maskText:(UIView *)view context:(CGContextRef)context -{ - [UIColor.blackColor setFill]; - CGPathRef maskPath = [self buildPathForView:view - inPath:CGPathCreateMutable() - visibleArea:view.frame]; - CGContextAddPath(context, maskPath); - CGContextFillPath(context); -} - -- (BOOL)shouldIgnoreView:(UIView *)view -{ - return [_ignoreClasses containsObject:view.class]; -} - -- (BOOL)shouldIgnore:(UIView *)view -{ - for (Class class in _ignoreClasses) { - if ([view isKindOfClass:class]) { - return true; - } - } - return false; -} - -- (BOOL)shouldRedact:(UIView *)view -{ - for (Class class in _redactClasses) { - if ([view isKindOfClass:class]) { - return true; - } - } - - return ( - [view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); -} - -- (BOOL)shouldRedactImageView:(UIImageView *)imageView -{ - return imageView.image != nil && - [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil - && (imageView.image.size.width > 10 - && imageView.image.size.height > 10); // This is to avoid redact gradient backgroud that - // are usually small lines repeating -} - -- (CGMutablePathRef)buildPathForView:(UIView *)view - inPath:(CGMutablePathRef)path - visibleArea:(CGRect)area -{ - CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; - - if (!CGRectIntersectsRect(area, rectInWindow)) { - return path; - } - - if (view.hidden || view.alpha == 0) { - return path; - } - - BOOL ignore = [self shouldIgnore:view]; - if (!ignore && [self shouldRedact:view]) { - CGPathAddRect(path, NULL, rectInWindow); - return path; - } else if ([self isOpaqueOrHasBackground:view]) { - CGMutablePathRef newPath = [self excludeRect:rectInWindow fromPath:path]; - CGPathRelease(path); - path = newPath; - } - - if (!ignore) { - for (UIView *subview in view.subviews) { - path = [self buildPathForView:subview inPath:path visibleArea:area]; - } - } - - return path; -} - -- (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path -{ - if (@available(iOS 16.0, tvOS 16.0, *)) { - CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); - CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); - return CGPathCreateMutableCopy(newPath); - } - return path; -} - -- (BOOL)isOpaqueOrHasBackground:(UIView *)view -{ - return view.isOpaque - || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); -} - -@end - -NS_ASSUME_NONNULL_END -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h deleted file mode 100644 index 9ce8ea88ae6..00000000000 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ /dev/null @@ -1,36 +0,0 @@ -#import "SentryDefines.h" -#import "SentrySwift.h" -#import -#if SENTRY_HAS_UIKIT -# import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryOnDemandReplay : NSObject - -@property (nonatomic) NSInteger bitRate; - -@property (nonatomic) NSInteger frameRate; - -@property (nonatomic) NSUInteger cacheMaxSize; - -@property (nonatomic) CGSize videoSize; - -- (instancetype)initWithOutputPath:(NSString *)outputPath; - -- (void)addFrame:(UIImage *)image; - -- (void)createVideoOf:(NSTimeInterval)duration - from:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(SentryVideoInfo *, NSError *error))completion; - -/** - * Remove cached frames until given date. - */ -- (void)releaseFramesUntil:(NSDate *)date; - -@end - -NS_ASSUME_NONNULL_END -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index b885b9ed2f5..b1a828ccf0a 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -2,9 +2,14 @@ #import "SentryBaggage.h" #import "SentryDependencyContainer.h" +#import "SentryFileManager.h" #import "SentryGlobalEventProcessor.h" +#import "SentryHub+Private.h" #import "SentryRandom.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" +#import "SentryReplayType.h" +#import "SentrySDK+Private.h" #import "SentrySdkInfo.h" -#import "SentrySessionReplay.h" #import "SentryTime.h" #import "SentryUIApplication.h" diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h deleted file mode 100644 index 56fe62ed819..00000000000 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ /dev/null @@ -1,32 +0,0 @@ -#import "SentryClient+Private.h" -#import "SentryEvent.h" -#import "SentrySwift.h" -#import - -#if SENTRY_HAS_UIKIT - -# import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentrySessionReplay : NSObject - -- (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; - -/** - * Start recording the session using rootView as image source. - * If full is @c YES, we transmit the entire session to sentry. - */ -- (void)start:(UIView *)rootView fullSession:(BOOL)full; - -/** - * Stop recording the session replay - */ -- (void)stop; - -- (void)replayForEvent:(SentryEvent *)event; - -@end - -NS_ASSUME_NONNULL_END -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h deleted file mode 100644 index 2425af8794c..00000000000 --- a/Sources/Sentry/include/SentryViewPhotographer.h +++ /dev/null @@ -1,19 +0,0 @@ -#import "SentryDefines.h" -#import - -#if SENTRY_HAS_UIKIT -# import - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryViewPhotographer : NSObject - -@property (nonatomic, readonly, class) SentryViewPhotographer *shared; - -- (UIImage *)imageFromUIView:(UIView *)view; - -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift new file mode 100644 index 00000000000..92c34e854f8 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -0,0 +1,158 @@ +#if canImport(UIKit) +@_implementationOnly import _SentryPrivate +import AVFoundation +import CoreGraphics +import Foundation +import UIKit + +struct SentryReplayFrame { + let imagePath: String + let time: Date + + init(imagePath: String, time: Date) { + self.imagePath = imagePath + self.time = time + } +} + +class SentryOnDemandReplay { + private let _outputPath: String + private let _onDemandDispatchQueue: DispatchQueue + + private var _starttime = Date() + private var _frames = [SentryReplayFrame]() + private var _currentPixelBuffer: SentryPixelBuffer? + + var videoSize = CGSize(width: 200, height: 434) + var bitRate = 20_000 + var frameRate = 1 + var cacheMaxSize = UInt.max + + init(outputPath: String) { + self._outputPath = outputPath + _onDemandDispatchQueue = DispatchQueue(label: "io.sentry.sessionreplay.ondemand") + } + + func addFrame(image: UIImage) { + _onDemandDispatchQueue.async { + self.asyncAddFrame(image: image) + } + } + + private func asyncAddFrame(image: UIImage) { + guard let data = resizeImage(image, maxWidth: 300)?.pngData() else { return } + + let date = Date() + let interval = date.timeIntervalSince(_starttime) + let imagePath = (_outputPath as NSString).appendingPathComponent("\(interval).png") + try? data.write(to: URL(fileURLWithPath: imagePath)) + _frames.append(SentryReplayFrame(imagePath: imagePath, time: date)) + + while _frames.count > cacheMaxSize { + let first = _frames.removeFirst() + try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath)) + } + } + + private func resizeImage(_ originalImage: UIImage, maxWidth: CGFloat) -> UIImage? { + let originalSize = originalImage.size + let aspectRatio = originalSize.width / originalSize.height + + let newWidth = min(originalSize.width, maxWidth) + let newHeight = newWidth / aspectRatio + + let newSize = CGSize(width: newWidth, height: newHeight) + + UIGraphicsBeginImageContextWithOptions(newSize, false, 1) + originalImage.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resizedImage + } + + func releaseFramesUntil(_ date: Date) { + _onDemandDispatchQueue.async { + while let first = self._frames.first, first.time < date { + self._frames.removeFirst() + try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath)) + } + } + } + + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { + let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mov) + + let videoSettings = createVideoSettings() + + let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + let bufferAttributes: [String: Any] = [ + String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_32ARGB + ] + + let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: bufferAttributes) + + videoWriter.add(videoWriterInput) + videoWriter.startWriting() + videoWriter.startSession(atSourceTime: .zero) + + let end = beginning.addingTimeInterval(duration) + var frameCount = 0 + var frames = [String]() + + var start = Date() + var actualEnd = Date() + + for frame in _frames { + if frame.time < beginning { continue } else if frame.time > end { break } + if frame.time < start { start = frame.time } + + actualEnd = frame.time + frames.append(frame.imagePath) + } + + if frames.isEmpty { return } + + _currentPixelBuffer = SentryPixelBuffer(size: videoSize) + + videoWriterInput.requestMediaDataWhenReady(on: _onDemandDispatchQueue) { [weak self] in + guard let self = self else { return } + + let imagePath = frames[frameCount] + if let image = UIImage(contentsOfFile: imagePath) { + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) + frameCount += 1 + + if self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) != true { + completion(nil, videoWriter.error) + } + } + + if frameCount >= frames.count { + videoWriterInput.markAsFinished() + videoWriter.finishWriting { + var videoInfo: SentryVideoInfo? + if videoWriter.status == .completed { + let fileSize = SentryDependencyContainer.sharedInstance().fileManager.fileSize(outputFileURL) + videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(self.videoSize.height), width: Int(self.videoSize.width), duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: actualEnd, fileSize: fileSize) + } + completion(videoInfo, videoWriter.error) + } + } + } + } + + private func createVideoSettings() -> [String: Any] { + return [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: videoSize.width, + AVVideoHeightKey: videoSize.height, + AVVideoCompressionPropertiesKey: [ + AVVideoAverageBitRateKey: bitRate, + AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel + ] + ] + } +} + +#endif // canImport(UIKit) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift new file mode 100644 index 00000000000..ac8e7c42a2d --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -0,0 +1,44 @@ +import AVFoundation +import CoreGraphics +import Foundation +import UIKit + +class SentryPixelBuffer { + private var pixelBuffer: CVPixelBuffer? + private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + private let size: CGSize + + init?(size: CGSize) { + self.size = size + let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32ARGB, nil, &pixelBuffer) + if status != kCVReturnSuccess { + return nil + } + } + + func append(image: UIImage, pixelBufferAdapter: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { + guard let pixelBuffer else { return false } + + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) + + guard + let cgimage = image.cgImage, + let context = CGContext( + data: pixelData, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), + space: rgbColorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue + ) else { + return false + } + + context.draw(cgimage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + + return pixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime) + } +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift new file mode 100644 index 00000000000..03a4696640c --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -0,0 +1,167 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +#if canImport(UIKit) +import UIKit + +let SENTRY_REPLAY_FOLDER = "replay" + +@objcMembers +class SentrySessionReplay: NSObject { + private var _rootView: UIView? + private var _processingScreenshot = false + private var _displayLink: CADisplayLink? + private var _lastScreenshot = Date() + private var _segmentStart: Date? + private var _sessionStart = Date() + private var _urlToCache: URL + private let _replayOptions: SentryReplayOptions + private var _replayMaker: SentryOnDemandReplay? + private var _sessionReplayId = SentryId() + private var _imageCollection = [UIImage]() + private var _currentSegment = 0 + private var _isFullSession = false + + init(replayOptions: SentryReplayOptions) { + _replayOptions = replayOptions + _urlToCache = URL(fileURLWithPath: SentryDependencyContainer.sharedInstance().fileManager.sentryPath) + .appendingPathComponent(SENTRY_REPLAY_FOLDER) + .appendingPathComponent(UUID().uuidString) + } + + func start(rootView: UIView, isFullSession: Bool) { + if _displayLink != nil { + return + } + _displayLink = CADisplayLink(target: self, selector: #selector(newFrame(_:) )) + _displayLink?.add(to: RunLoop.main, forMode: .common) + _rootView = rootView + _lastScreenshot = Date() + _sessionStart = _lastScreenshot + _segmentStart = nil + _currentSegment = 0 + _sessionReplayId = SentryId() + + if !FileManager.default.fileExists(atPath: _urlToCache.path) { + try? FileManager.default.createDirectory(at: _urlToCache, withIntermediateDirectories: true) + } + + _replayMaker = SentryOnDemandReplay(outputPath: _urlToCache.path) + _replayMaker?.bitRate = _replayOptions.replayBitRate + _replayMaker?.cacheMaxSize = UInt(isFullSession ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration) + _imageCollection.removeAll() + } + + func stop() { + _displayLink?.invalidate() + _displayLink = nil + } + + func replayFor(event: Event) { + if _isFullSession || (event.error == nil && event.exceptions?.count ?? 0 == 0) { + return + } + + let finalPath = _urlToCache.appendingPathComponent("replay.mp4") + let replayStart = Date().addingTimeInterval(-_replayOptions.errorReplayDuration) + + createAndCapture(finalPath, duration: _replayOptions.errorReplayDuration, startedAt: replayStart) + promoteToFull() + } + + private func promoteToFull() { + _isFullSession = true + _replayMaker?.cacheMaxSize = UInt(_replayOptions.sessionSegmentDuration) + } + + @objc + private func newFrame(_ sender: CADisplayLink) { + let now = Date() + if now.timeIntervalSince(_lastScreenshot) > (1 / Double(_replayOptions.frameRate)) { + self.takeScreenshot() + _lastScreenshot = now + + if _segmentStart == nil { + _segmentStart = now + } else if let _segmentStart, + _isFullSession && + now.timeIntervalSince(_segmentStart) >= _replayOptions.sessionSegmentDuration { + prepareSegmentUntil(now) + } + } + } + + private func prepareSegmentUntil(_ date: Date) { + guard let _segmentStart else { return } + let from = _segmentStart.timeIntervalSince(_sessionStart) + let to = date.timeIntervalSince(_sessionStart) + var pathToSegment = _urlToCache.appendingPathComponent("segments") + if !FileManager.default.fileExists(atPath: pathToSegment.path) { + try? FileManager.default.createDirectory(at: pathToSegment, withIntermediateDirectories: true) + } + pathToSegment = pathToSegment.appendingPathComponent("\(from)-\(to).mp4") + let segmentStart = Date().addingTimeInterval(-_replayOptions.sessionSegmentDuration) + + createAndCapture(pathToSegment, duration: _replayOptions.sessionSegmentDuration, startedAt: segmentStart) + + } + + private func createAndCapture( _ path: URL, duration: TimeInterval, startedAt start: Date ) { + do { + try _replayMaker?.createVideoWith(duration: duration, beginning: start, outputFileURL: path, completion: { [weak self] videoInfo, error in + guard let self = self else { return } + if let error { + print("[SentrySessionReplay] Could not create replay video - \(error)") + } else if let videoInfo { + self.captureSegment(id: self._currentSegment, video: videoInfo, replayType: .session) + self._replayMaker?.releaseFramesUntil(videoInfo.end) + self._segmentStart = nil + self._currentSegment += 1 + } + }) + } catch { + print("[SentrySessionReplay] Could not generate session replay segment - \(error)") + } + } + + private func captureSegment(id: Int, video: SentryVideoInfo, replayType: SentryReplayType) { + let replayEvent = SentryReplayEvent() + replayEvent.replayType = replayType + replayEvent.eventId = self._sessionReplayId + replayEvent.replayStartTimestamp = video.start + replayEvent.segmentId = id + replayEvent.timestamp = video.end + + let recording = SentryReplayRecording(segmentId: id, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width) + + SentrySDK.currentHub().capture(replayEvent, replayRecording: recording, video: video.path) + } + + func takeScreenshot() { + if _processingScreenshot { + return + } + + objc_sync_enter(self) + defer { + objc_sync_exit(self) + } + + guard !_processingScreenshot else { + return + } + + _processingScreenshot = true + defer { _processingScreenshot = false } + + guard let _rootView, let screenshot = SentryViewPhotographer.shared.image(view: _rootView) else { return } + + let backgroundQueue = DispatchQueue.global(qos: .default) + backgroundQueue.async { [weak self] in + guard let self = self else { return } + self._replayMaker?.addFrame(image: screenshot) + } + } +} + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift index 31e81035de4..cc22859b8d0 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift @@ -22,13 +22,13 @@ class SentrySessionReplayIntegration: NSObject, SentryIntegrationProtocol { return false } - sessionReplay = SentrySessionReplay(settings: options.sessionReplayOptions) - sessionReplay?.start(window, fullSession: shouldReplayFullSession(sampleRate: options.sessionReplayOptions.replaysSessionSampleRate)) + sessionReplay = SentrySessionReplay(replayOptions: options.sessionReplayOptions) + sessionReplay?.start(rootView: window, isFullSession: shouldReplayFullSession(sampleRate: options.sessionReplayOptions.replaysSessionSampleRate)) NotificationCenter.default.addObserver(self, selector: #selector(stop), name: UIApplication.didEnterBackgroundNotification, object: nil) SentryGlobalEventProcessor.shared().add { event in - self.sessionReplay?.replay(for: event) + self.sessionReplay?.replayFor(event: event) return event } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift new file mode 100644 index 00000000000..cff98dc978b --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -0,0 +1,108 @@ +import Foundation + +#if canImport(UIKit) +import CoreGraphics +import UIKit + +@objcMembers +class SentryViewPhotographer: NSObject { + + private var ignoreClasses: [AnyClass] = [] + private var redactClasses: [AnyClass] = [] + + static let shared = SentryViewPhotographer() + + override init() { +#if os(iOS) + ignoreClasses = [ UISlider.self, UISwitch.self ] +#endif + redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ + "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" + ].compactMap { NSClassFromString($0) } + } + + func image(view: UIView) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) + if let currentContext = UIGraphicsGetCurrentContext() { + view.layer.render(in: currentContext) + self.mask(view: view, context: currentContext) + if let screenshot = UIGraphicsGetImageFromCurrentImageContext() { + UIGraphicsEndImageContext() + return screenshot + } + } + UIGraphicsEndImageContext() + return nil + } + + private func mask(view: UIView, context: CGContext) { + UIColor.black.setFill() + let maskPath = self.buildPath(view: view, path: CGMutablePath(), area: view.frame) + context.addPath(maskPath) + context.fillPath() + } + + private func shouldIgnore(view: UIView) -> Bool { + ignoreClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(view: UIView) -> Bool { + if let imageView = view as? UIImageView { + return shouldRedact(imageView: imageView) + } + + return redactClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(imageView: UIImageView) -> Bool { + // Checking the size is to avoid redact gradient backgroud that + // are usually small lines repeating + guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } + return image.imageAsset?.value(forKey: "_containingBundle") != nil + } + + private func buildPath(view: UIView, path: CGMutablePath, area: CGRect) -> CGMutablePath { + let rectInWindow = view.convert(view.bounds, to: nil) + + if !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { + return path + } + + var result = path + + let ignore = shouldIgnore(view: view) + + if !ignore && shouldRedact(view: view) { + result.addRect(rectInWindow) + return result + } else if isOpaqueOrHasBackground(view) { + let newPath = excludeRect(rectInWindow, fromPath: result) + result = newPath + } + + if !ignore { + for subview in view.subviews { + result = buildPath(view: subview, path: path, area: area) + } + } + + return result + } + + private func excludeRect(_ rectangle: CGRect, fromPath path: CGMutablePath) -> CGMutablePath { + if #available(iOS 16.0, tvOS 16.0, *) { + let exclude = CGPath(rect: rectangle, transform: nil) + let newPath = path.subtracting(exclude, using: .evenOdd) + return newPath.mutableCopy() ?? path + } + return path + } + + private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { + return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) + } +} + +#endif // canImport(UIKit) From 554e5ba723d144d7ab6d75c5fa87f184ea2a00b1 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 18 Mar 2024 16:51:05 +0100 Subject: [PATCH 53/88] Update SentrySessionReplay.swift --- .../SessionReplay/SentrySessionReplay.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 03a4696640c..be107cadb81 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -21,6 +21,7 @@ class SentrySessionReplay: NSObject { private var _imageCollection = [UIImage]() private var _currentSegment = 0 private var _isFullSession = false + private var _processingLock = NSLock() init(replayOptions: SentryReplayOptions) { _replayOptions = replayOptions @@ -142,16 +143,15 @@ class SentrySessionReplay: NSObject { return } - objc_sync_enter(self) - defer { - objc_sync_exit(self) - } - - guard !_processingScreenshot else { + _processingLock.lock() + if _processingScreenshot { + _processingLock.unlock() return } - + _processingScreenshot = true + _processingLock.unlock() + defer { _processingScreenshot = false } guard let _rootView, let screenshot = SentryViewPhotographer.shared.image(view: _rootView) else { return } From 6c2db344e29770bd17bab95db5ea38417468dda8 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 18 Mar 2024 16:53:36 +0100 Subject: [PATCH 54/88] Update AppDelegate.swift --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 0a4644e39ba..d889305b442 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -1,11 +1,6 @@ import Sentry import UIKit -func log(_ message: String) { - SentrySDK.addBreadcrumb(Breadcrumb(level: .debug, category: message)) - NSLog("%@", message) -} - @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { From 97ea26558c518bcd06e724f6dd11417b1c7bd8fb Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 18 Mar 2024 17:22:14 +0100 Subject: [PATCH 55/88] fixes --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../Swift/Integrations/SessionReplay/SentryPixelBuffer.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 92c34e854f8..a8c624b883b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -150,7 +150,7 @@ class SentryOnDemandReplay { AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: bitRate, AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel - ] + ] as [String: Any] ] } } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index ac8e7c42a2d..4f5018797a5 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -1,3 +1,5 @@ +#if canImport(UIKit) + import AVFoundation import CoreGraphics import Foundation @@ -42,3 +44,5 @@ class SentryPixelBuffer { return pixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime) } } + +#endif // canImport(UIKit) From ba6c5a32e4820b19a7364b9e8531b27eff324994 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 19 Mar 2024 17:00:32 +0100 Subject: [PATCH 56/88] tests --- SentryTestUtils/TestTransport.swift | 1 + Sources/Sentry/include/SentryPrivate.h | 2 +- Tests/SentryTests/Helper/SentryDateUtilTests.swift | 2 +- .../Integrations/SessionReplay/SentryReplayEventTests.swift | 2 +- Tests/SentryTests/SentryTests-Bridging-Header.h | 3 +-- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SentryTestUtils/TestTransport.swift b/SentryTestUtils/TestTransport.swift index 5e3a31bbbf6..eab268c9299 100644 --- a/SentryTestUtils/TestTransport.swift +++ b/SentryTestUtils/TestTransport.swift @@ -1,3 +1,4 @@ +import _SentryPrivate import Foundation @objc diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index b1a828ccf0a..13671258f1e 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -12,4 +12,4 @@ #import "SentrySDK+Private.h" #import "SentrySdkInfo.h" #import "SentryTime.h" -#import "SentryUIApplication.h" +#import "SentryUIApplication+Private.h" diff --git a/Tests/SentryTests/Helper/SentryDateUtilTests.swift b/Tests/SentryTests/Helper/SentryDateUtilTests.swift index 9c2cc0ad07c..50096006244 100644 --- a/Tests/SentryTests/Helper/SentryDateUtilTests.swift +++ b/Tests/SentryTests/Helper/SentryDateUtilTests.swift @@ -57,7 +57,7 @@ class SentryDateUtilTests: XCTestCase { func testJavascriptDate() { let testDate = Date(timeIntervalSince1970: 60) - let timestamp = DateUtil.secondsSince1970(testDate) + let timestamp = DateUtil.millisecondsSince1970(testDate) expect(timestamp) == 60_000 } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift index 96391581a46..024a4a2aa2e 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift @@ -13,7 +13,7 @@ class SentryReplayEventTests: XCTestCase { sut.traceIds = traceIds let replayId = SentryId() - sut.replayId = replayId + sut.eventId = replayId sut.segmentId = 3 diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 3a0a234efd5..206b194e6f9 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -163,8 +163,6 @@ #import "SentryRateLimits.h" #import "SentryReachability.h" #import "SentryReplayEvent.h" -#import "SentryReplayOptions.h" -#import "SentryReplayRecording.h" #import "SentryRetryAfterHeaderParser.h" #import "SentrySDK+Private.h" #import "SentrySDK+Tests.h" @@ -234,3 +232,4 @@ #import "TestSentrySpan.h" #import "TestSentryViewHierarchy.h" #import "URLSessionTaskMock.h" +@import _SentryPrivate; From 0da9473dbf5b2ab1478a9eabe0a326e26e93a773 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 20 Mar 2024 12:46:46 +0100 Subject: [PATCH 57/88] Update SentryFileManager.m --- Sources/Sentry/SentryFileManager.m | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index 411b4d87f61..c78e7394959 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -728,6 +728,14 @@ - (void)createPathsWithOptions:(SentryOptions *)options self.envelopesPath = [self.sentryPath stringByAppendingPathComponent:EnvelopesPathComponent]; } +- (NSInteger)fileSize:(NSURL *)path +{ + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path + error:nil]; + NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize] ?: @(-1); + return [fileSize integerValue]; +} + #if SENTRY_TARGET_PROFILING_SUPPORTED /** * @note This method must be statically accessible because it will be called during app launch, @@ -752,14 +760,6 @@ - (void)createPathsWithOptions:(SentryOptions *)options return sentryApplicationSupportPath; } -- (NSInteger)fileSize:(NSURL *)path -{ - NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path - error:nil]; - NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize] ?: @(-1); - return [fileSize integerValue]; -} - NSURL *_Nullable sentryLaunchConfigFileURL = nil; NSURL *_Nullable launchProfileConfigFileURL(void) From 3252a83a03a5303d0e56417c4b6bd22e9cf44d38 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 20 Mar 2024 13:39:39 +0100 Subject: [PATCH 58/88] Update Sentry.podspec --- Sentry.podspec | 91 ++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/Sentry.podspec b/Sentry.podspec index ab0d7a8b9f5..8d98a9143c4 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,50 +1,53 @@ Pod::Spec.new do |s| - s.name = "Sentry" - s.version = "8.21.0" - s.summary = "Sentry client for cocoa" - s.homepage = "https://github.com/getsentry/sentry-cocoa" - s.license = "mit" - s.authors = "Sentry" - s.source = { :git => "https://github.com/getsentry/sentry-cocoa.git", - :tag => s.version.to_s } - - s.ios.deployment_target = "11.0" - s.osx.deployment_target = "10.13" - s.tvos.deployment_target = "11.0" - s.watchos.deployment_target = "4.0" - s.visionos.deployment_target = "1.0" - s.module_name = "Sentry" - s.requires_arc = true - s.frameworks = 'Foundation' - s.swift_versions = "5.5" - s.pod_target_xcconfig = { - 'GCC_ENABLE_CPP_EXCEPTIONS' => 'YES', - 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++14', - 'CLANG_CXX_LIBRARY' => 'libc++', - 'APPLICATION_EXTENSION_API_ONLY' => 'YES', - 'SWIFT_INCLUDE_PATHS' => '${PODS_TARGET_SRCROOT}/Sources/Sentry/include' - } - s.watchos.pod_target_xcconfig = { - 'OTHER_LDFLAGS' => '$(inherited) -framework WatchKit' - } - - s.default_subspecs = ['Core'] - - s.subspec 'Core' do |sp| - sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", - "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}", "Sources/Sentry/include/module.modulemap" - sp.public_header_files = + s.name = "Sentry" + s.version = "8.21.0" + s.summary = "Sentry client for cocoa" + s.homepage = "https://github.com/getsentry/sentry-cocoa" + s.license = "mit" + s.authors = "Sentry" + s.source = { :git => "https://github.com/getsentry/sentry-cocoa.git", + :tag => s.version.to_s } + + s.ios.deployment_target = "11.0" + s.osx.deployment_target = "10.13" + s.tvos.deployment_target = "11.0" + s.watchos.deployment_target = "4.0" + s.visionos.deployment_target = "1.0" + s.module_name = "Sentry" + s.requires_arc = true + s.frameworks = 'Foundation' + s.swift_versions = "5.5" + s.pod_target_xcconfig = { + 'GCC_ENABLE_CPP_EXCEPTIONS' => 'YES', + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++14', + 'CLANG_CXX_LIBRARY' => 'libc++', + 'APPLICATION_EXTENSION_API_ONLY' => 'YES', + 'SWIFT_INCLUDE_PATHS' => '${PODS_TARGET_SRCROOT}/Sources/Sentry/include' + } + s.watchos.pod_target_xcconfig = { + 'OTHER_LDFLAGS' => '$(inherited) -framework WatchKit' + } + + s.default_subspecs = ['Core'] + + s.subspec 'Core' do |sp| + sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", + "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" + + sp.preserve_path = "Sources/Sentry/include/module.modulemap" + sp.public_header_files = "Sources/Sentry/Public/*.h" - sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } - end - - s.subspec 'HybridSDK' do |sp| - sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", + sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } + end + + s.subspec 'HybridSDK' do |sp| + sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" - sp.public_header_files = + sp.preserve_path = "Sources/Sentry/include/module.modulemap" + sp.public_header_files = "Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h" - - sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } - end + + sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } + end end From d8ce8d69948f49123b71a11bd08ea505a016af6f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 25 Mar 2024 11:22:54 +0100 Subject: [PATCH 59/88] revert Session Replay --- Sentry.xcodeproj/project.pbxproj | 2 + Sources/Sentry/include/SentryPrivate.h | 5 - .../Sentry/include/SentryReplayRecording.h | 1 - .../SessionReplay/SentrySessionReplay.swift | 334 +++++++++--------- 4 files changed, 169 insertions(+), 173 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 72d58adbee5..c7597440fd9 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1763,6 +1763,7 @@ D84DAD4F2B17428D003CF120 /* SentryTestUtilsDynamic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryTestUtilsDynamic.h; sourceTree = ""; }; D84F833B2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySwiftAsyncIntegration.h; path = include/SentrySwiftAsyncIntegration.h; sourceTree = ""; }; D84F833C2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySwiftAsyncIntegration.m; sourceTree = ""; }; + D8511F722BAC8F750015E6FD /* Sentry.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Sentry.modulemap; sourceTree = ""; }; D85596F1280580F10041FF8B /* SentryScreenshotIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshotIntegration.m; sourceTree = ""; }; D855AD61286ED6A4002573E1 /* SentryCrashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCrashTests.m; sourceTree = ""; }; D855B3E727D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackingIntegrationTest.swift; sourceTree = ""; }; @@ -3566,6 +3567,7 @@ isa = PBXGroup; children = ( D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */, + D8511F722BAC8F750015E6FD /* Sentry.modulemap */, ); path = Resources; sourceTree = ""; diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 13671258f1e..db30e35c92f 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,15 +1,10 @@ // Sentry internal headers that are needed for swift code #import "SentryBaggage.h" -#import "SentryDependencyContainer.h" #import "SentryFileManager.h" #import "SentryGlobalEventProcessor.h" -#import "SentryHub+Private.h" #import "SentryRandom.h" -#import "SentryReplayEvent.h" #import "SentryReplayRecording.h" #import "SentryReplayType.h" -#import "SentrySDK+Private.h" #import "SentrySdkInfo.h" #import "SentryTime.h" -#import "SentryUIApplication+Private.h" diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h index c4b402a6db8..40cedc079dd 100644 --- a/Sources/Sentry/include/SentryReplayRecording.h +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -1,4 +1,3 @@ -#import "SentrySerializable.h" #import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index be107cadb81..eb19b393dc1 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -1,167 +1,167 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -#if canImport(UIKit) -import UIKit - -let SENTRY_REPLAY_FOLDER = "replay" - -@objcMembers -class SentrySessionReplay: NSObject { - private var _rootView: UIView? - private var _processingScreenshot = false - private var _displayLink: CADisplayLink? - private var _lastScreenshot = Date() - private var _segmentStart: Date? - private var _sessionStart = Date() - private var _urlToCache: URL - private let _replayOptions: SentryReplayOptions - private var _replayMaker: SentryOnDemandReplay? - private var _sessionReplayId = SentryId() - private var _imageCollection = [UIImage]() - private var _currentSegment = 0 - private var _isFullSession = false - private var _processingLock = NSLock() - - init(replayOptions: SentryReplayOptions) { - _replayOptions = replayOptions - _urlToCache = URL(fileURLWithPath: SentryDependencyContainer.sharedInstance().fileManager.sentryPath) - .appendingPathComponent(SENTRY_REPLAY_FOLDER) - .appendingPathComponent(UUID().uuidString) - } - - func start(rootView: UIView, isFullSession: Bool) { - if _displayLink != nil { - return - } - _displayLink = CADisplayLink(target: self, selector: #selector(newFrame(_:) )) - _displayLink?.add(to: RunLoop.main, forMode: .common) - _rootView = rootView - _lastScreenshot = Date() - _sessionStart = _lastScreenshot - _segmentStart = nil - _currentSegment = 0 - _sessionReplayId = SentryId() - - if !FileManager.default.fileExists(atPath: _urlToCache.path) { - try? FileManager.default.createDirectory(at: _urlToCache, withIntermediateDirectories: true) - } - - _replayMaker = SentryOnDemandReplay(outputPath: _urlToCache.path) - _replayMaker?.bitRate = _replayOptions.replayBitRate - _replayMaker?.cacheMaxSize = UInt(isFullSession ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration) - _imageCollection.removeAll() - } - - func stop() { - _displayLink?.invalidate() - _displayLink = nil - } - - func replayFor(event: Event) { - if _isFullSession || (event.error == nil && event.exceptions?.count ?? 0 == 0) { - return - } - - let finalPath = _urlToCache.appendingPathComponent("replay.mp4") - let replayStart = Date().addingTimeInterval(-_replayOptions.errorReplayDuration) - - createAndCapture(finalPath, duration: _replayOptions.errorReplayDuration, startedAt: replayStart) - promoteToFull() - } - - private func promoteToFull() { - _isFullSession = true - _replayMaker?.cacheMaxSize = UInt(_replayOptions.sessionSegmentDuration) - } - - @objc - private func newFrame(_ sender: CADisplayLink) { - let now = Date() - if now.timeIntervalSince(_lastScreenshot) > (1 / Double(_replayOptions.frameRate)) { - self.takeScreenshot() - _lastScreenshot = now - - if _segmentStart == nil { - _segmentStart = now - } else if let _segmentStart, - _isFullSession && - now.timeIntervalSince(_segmentStart) >= _replayOptions.sessionSegmentDuration { - prepareSegmentUntil(now) - } - } - } - - private func prepareSegmentUntil(_ date: Date) { - guard let _segmentStart else { return } - let from = _segmentStart.timeIntervalSince(_sessionStart) - let to = date.timeIntervalSince(_sessionStart) - var pathToSegment = _urlToCache.appendingPathComponent("segments") - if !FileManager.default.fileExists(atPath: pathToSegment.path) { - try? FileManager.default.createDirectory(at: pathToSegment, withIntermediateDirectories: true) - } - pathToSegment = pathToSegment.appendingPathComponent("\(from)-\(to).mp4") - let segmentStart = Date().addingTimeInterval(-_replayOptions.sessionSegmentDuration) - - createAndCapture(pathToSegment, duration: _replayOptions.sessionSegmentDuration, startedAt: segmentStart) - - } - - private func createAndCapture( _ path: URL, duration: TimeInterval, startedAt start: Date ) { - do { - try _replayMaker?.createVideoWith(duration: duration, beginning: start, outputFileURL: path, completion: { [weak self] videoInfo, error in - guard let self = self else { return } - if let error { - print("[SentrySessionReplay] Could not create replay video - \(error)") - } else if let videoInfo { - self.captureSegment(id: self._currentSegment, video: videoInfo, replayType: .session) - self._replayMaker?.releaseFramesUntil(videoInfo.end) - self._segmentStart = nil - self._currentSegment += 1 - } - }) - } catch { - print("[SentrySessionReplay] Could not generate session replay segment - \(error)") - } - } - - private func captureSegment(id: Int, video: SentryVideoInfo, replayType: SentryReplayType) { - let replayEvent = SentryReplayEvent() - replayEvent.replayType = replayType - replayEvent.eventId = self._sessionReplayId - replayEvent.replayStartTimestamp = video.start - replayEvent.segmentId = id - replayEvent.timestamp = video.end - - let recording = SentryReplayRecording(segmentId: id, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width) - - SentrySDK.currentHub().capture(replayEvent, replayRecording: recording, video: video.path) - } - - func takeScreenshot() { - if _processingScreenshot { - return - } - - _processingLock.lock() - if _processingScreenshot { - _processingLock.unlock() - return - } - - _processingScreenshot = true - _processingLock.unlock() - - defer { _processingScreenshot = false } - - guard let _rootView, let screenshot = SentryViewPhotographer.shared.image(view: _rootView) else { return } - - let backgroundQueue = DispatchQueue.global(qos: .default) - backgroundQueue.async { [weak self] in - guard let self = self else { return } - self._replayMaker?.addFrame(image: screenshot) - } - } -} - -#endif // SENTRY_HAS_UIKIT +//@_implementationOnly import _SentryPrivate +//import Foundation +// +//#if canImport(UIKit) +//import UIKit +// +//let SENTRY_REPLAY_FOLDER = "replay" +// +//@objcMembers +//class SentrySessionReplay: NSObject { +// private var _rootView: UIView? +// private var _processingScreenshot = false +// private var _displayLink: CADisplayLink? +// private var _lastScreenshot = Date() +// private var _segmentStart: Date? +// private var _sessionStart = Date() +// private var _urlToCache: URL +// private let _replayOptions: SentryReplayOptions +// private var _replayMaker: SentryOnDemandReplay? +// private var _sessionReplayId = SentryId() +// private var _imageCollection = [UIImage]() +// private var _currentSegment = 0 +// private var _isFullSession = false +// private var _processingLock = NSLock() +// +// init(replayOptions: SentryReplayOptions) { +// _replayOptions = replayOptions +// _urlToCache = URL(fileURLWithPath: SentryDependencyContainer.sharedInstance().fileManager.sentryPath) +// .appendingPathComponent(SENTRY_REPLAY_FOLDER) +// .appendingPathComponent(UUID().uuidString) +// } +// +// func start(rootView: UIView, isFullSession: Bool) { +// if _displayLink != nil { +// return +// } +// _displayLink = CADisplayLink(target: self, selector: #selector(newFrame(_:) )) +// _displayLink?.add(to: RunLoop.main, forMode: .common) +// _rootView = rootView +// _lastScreenshot = Date() +// _sessionStart = _lastScreenshot +// _segmentStart = nil +// _currentSegment = 0 +// _sessionReplayId = SentryId() +// +// if !FileManager.default.fileExists(atPath: _urlToCache.path) { +// try? FileManager.default.createDirectory(at: _urlToCache, withIntermediateDirectories: true) +// } +// +// _replayMaker = SentryOnDemandReplay(outputPath: _urlToCache.path) +// _replayMaker?.bitRate = _replayOptions.replayBitRate +// _replayMaker?.cacheMaxSize = UInt(isFullSession ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration) +// _imageCollection.removeAll() +// } +// +// func stop() { +// _displayLink?.invalidate() +// _displayLink = nil +// } +// +// func replayFor(event: Event) { +// if _isFullSession || (event.error == nil && event.exceptions?.count ?? 0 == 0) { +// return +// } +// +// let finalPath = _urlToCache.appendingPathComponent("replay.mp4") +// let replayStart = Date().addingTimeInterval(-_replayOptions.errorReplayDuration) +// +// createAndCapture(finalPath, duration: _replayOptions.errorReplayDuration, startedAt: replayStart) +// promoteToFull() +// } +// +// private func promoteToFull() { +// _isFullSession = true +// _replayMaker?.cacheMaxSize = UInt(_replayOptions.sessionSegmentDuration) +// } +// +// @objc +// private func newFrame(_ sender: CADisplayLink) { +// let now = Date() +// if now.timeIntervalSince(_lastScreenshot) > (1 / Double(_replayOptions.frameRate)) { +// self.takeScreenshot() +// _lastScreenshot = now +// +// if _segmentStart == nil { +// _segmentStart = now +// } else if let _segmentStart, +// _isFullSession && +// now.timeIntervalSince(_segmentStart) >= _replayOptions.sessionSegmentDuration { +// prepareSegmentUntil(now) +// } +// } +// } +// +// private func prepareSegmentUntil(_ date: Date) { +// guard let _segmentStart else { return } +// let from = _segmentStart.timeIntervalSince(_sessionStart) +// let to = date.timeIntervalSince(_sessionStart) +// var pathToSegment = _urlToCache.appendingPathComponent("segments") +// if !FileManager.default.fileExists(atPath: pathToSegment.path) { +// try? FileManager.default.createDirectory(at: pathToSegment, withIntermediateDirectories: true) +// } +// pathToSegment = pathToSegment.appendingPathComponent("\(from)-\(to).mp4") +// let segmentStart = Date().addingTimeInterval(-_replayOptions.sessionSegmentDuration) +// +// createAndCapture(pathToSegment, duration: _replayOptions.sessionSegmentDuration, startedAt: segmentStart) +// +// } +// +// private func createAndCapture( _ path: URL, duration: TimeInterval, startedAt start: Date ) { +// do { +// try _replayMaker?.createVideoWith(duration: duration, beginning: start, outputFileURL: path, completion: { [weak self] videoInfo, error in +// guard let self = self else { return } +// if let error { +// print("[SentrySessionReplay] Could not create replay video - \(error)") +// } else if let videoInfo { +// self.captureSegment(id: self._currentSegment, video: videoInfo, replayType: .session) +// self._replayMaker?.releaseFramesUntil(videoInfo.end) +// self._segmentStart = nil +// self._currentSegment += 1 +// } +// }) +// } catch { +// print("[SentrySessionReplay] Could not generate session replay segment - \(error)") +// } +// } +// +// private func captureSegment(id: Int, video: SentryVideoInfo, replayType: SentryReplayType) { +// let replayEvent = SentryReplayEvent() +// replayEvent.replayType = replayType +// replayEvent.eventId = self._sessionReplayId +// replayEvent.replayStartTimestamp = video.start +// replayEvent.segmentId = id +// replayEvent.timestamp = video.end +// +// let recording = SentryReplayRecording(segmentId: id, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width) +// +// SentrySDK.currentHub().capture(replayEvent, replayRecording: recording, video: video.path) +// } +// +// func takeScreenshot() { +// if _processingScreenshot { +// return +// } +// +// _processingLock.lock() +// if _processingScreenshot { +// _processingLock.unlock() +// return +// } +// +// _processingScreenshot = true +// _processingLock.unlock() +// +// defer { _processingScreenshot = false } +// +// guard let _rootView, let screenshot = SentryViewPhotographer.shared.image(view: _rootView) else { return } +// +// let backgroundQueue = DispatchQueue.global(qos: .default) +// backgroundQueue.async { [weak self] in +// guard let self = self else { return } +// self._replayMaker?.addFrame(image: screenshot) +// } +// } +//} +// +//#endif // SENTRY_HAS_UIKIT From 1afd0d9d45a5c6130a34cbd900e80568abfea02a Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 25 Mar 2024 11:52:53 +0100 Subject: [PATCH 60/88] Reverting some things :( --- Sentry.xcodeproj/project.pbxproj | 24 +- Sources/Sentry/SentryOptions.m | 1 + Sources/Sentry/SentrySessionReplay.h | 32 +++ Sources/Sentry/SentrySessionReplay.m | 264 ++++++++++++++++++ .../Sentry/SentrySessionReplayIntegration.h | 11 + .../Sentry/SentrySessionReplayIntegration.m | 79 ++++++ .../SessionReplay/SentryOnDemandReplay.swift | 6 +- .../SessionReplay/SentrySessionReplay.swift | 167 ----------- .../SentrySessionReplayIntegration.swift | 55 ---- 9 files changed, 407 insertions(+), 232 deletions(-) create mode 100644 Sources/Sentry/SentrySessionReplay.h create mode 100644 Sources/Sentry/SentrySessionReplay.m create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.h create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m delete mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift delete mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c7597440fd9..474fc35bb0d 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -765,6 +765,10 @@ D8199DC229376FC10074249E /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63AA759B1EB8AEF500D153DE /* Sentry.framework */; }; D81A346C291AECC7005A27A9 /* PrivateSentrySDKOnly.h in Headers */ = {isa = PBXBuildFile; fileRef = D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */; settings = {ATTRIBUTES = (Private, ); }; }; D81FDF12280EA1060045E0E4 /* SentryScreenShotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */; }; + D820CDB32BB1886100BA339D /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB22BB1886100BA339D /* SentrySessionReplay.m */; }; + D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB12BB1886100BA339D /* SentrySessionReplay.h */; }; + D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; + D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; @@ -841,9 +845,7 @@ D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; - D8CAC06F2BA337F800E38F34 /* SentrySessionReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */; }; D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */; }; - D8CAC0752BA4843A00E38F34 /* SentrySessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */; }; D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1749,6 +1751,10 @@ D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PrivateSentrySDKOnly.h; path = include/HybridPublic/PrivateSentrySDKOnly.h; sourceTree = ""; }; D81A349F291D5568005A27A9 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenShotTests.swift; sourceTree = ""; }; + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentrySessionReplay.h; sourceTree = ""; }; + D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentrySessionReplayIntegration.h; sourceTree = ""; }; + D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; @@ -1833,9 +1839,7 @@ D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; - D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegration.swift; sourceTree = ""; }; D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographer.swift; sourceTree = ""; }; - D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplay.swift; sourceTree = ""; }; D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; @@ -3427,6 +3431,10 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */, + D820CDB22BB1886100BA339D /* SentrySessionReplay.m */, + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, + D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, ); name = SessionReplay; sourceTree = ""; @@ -3577,9 +3585,7 @@ children = ( D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, - D8CAC06E2BA337F800E38F34 /* SentrySessionReplayIntegration.swift */, D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, - D8CAC0742BA4843A00E38F34 /* SentrySessionReplay.swift */, D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, ); @@ -3722,6 +3728,7 @@ 8EAE980B261E9F530073B6B3 /* SentryPerformanceTracker.h in Headers */, 63FE718520DA4C1100CDBAE8 /* SentryCrashC.h in Headers */, 8EA1ED0D2669028C00E62B98 /* SentryUIViewControllerSwizzling.h in Headers */, + D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */, 7B98D7E425FB7A7200C5A389 /* SentryAppState.h in Headers */, 7BDEAA022632A4580001EA25 /* SentryOptions+Private.h in Headers */, A8AFFCCD29069C3E00967CD7 /* SentryHttpStatusCodeRange.h in Headers */, @@ -3760,6 +3767,7 @@ 7B8713AE26415ADF006D6004 /* SentryAppStartTrackingIntegration.h in Headers */, 7B7D873224864BB900D2ECFF /* SentryCrashMachineContextWrapper.h in Headers */, 861265F92404EC1500C4AFDE /* NSArray+SentrySanitize.h in Headers */, + D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */, 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, @@ -4338,6 +4346,7 @@ 6334314320AD9AE40077E581 /* SentryMechanism.m in Sources */, 63FE70D320DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.c in Sources */, 639FCFA51EBC809A00778193 /* SentryStacktrace.m in Sources */, + D820CDB32BB1886100BA339D /* SentrySessionReplay.m in Sources */, 63FE70DF20DA4C1000CDBAE8 /* SentryCrashMonitorType.c in Sources */, 7BF9EF7E2722B91F00B5BBEF /* SentryDefaultObjCRuntimeWrapper.m in Sources */, 7BC3936E25B1AB72004F03D3 /* SentryLevelMapper.m in Sources */, @@ -4363,7 +4372,6 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, - D8CAC0752BA4843A00E38F34 /* SentrySessionReplay.swift in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */, @@ -4403,7 +4411,6 @@ D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */, D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */, 7BC9A20428F4166D001E7C4C /* SentryMeasurementValue.m in Sources */, - D8CAC06F2BA337F800E38F34 /* SentrySessionReplayIntegration.swift in Sources */, D859696B27BECD8F0036A46E /* SentryCoreDataTrackingIntegration.m in Sources */, 7BD86EC7264A641D005439DB /* SentrySysctl.m in Sources */, 84281C432A578E5600EE88F2 /* SentryProfilerState.mm in Sources */, @@ -4430,6 +4437,7 @@ 63FE710520DA4C1000CDBAE8 /* SentryCrashLogger.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, + D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 31162e8c93b..1f8c2834cc8 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -15,6 +15,7 @@ #import "SentryOptions+Private.h" #import "SentrySDK.h" #import "SentryScope.h" +#import "SentrySessionReplayIntegration.h" #import "SentrySwiftAsyncIntegration.h" #import diff --git a/Sources/Sentry/SentrySessionReplay.h b/Sources/Sentry/SentrySessionReplay.h new file mode 100644 index 00000000000..72d5764c2d9 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.h @@ -0,0 +1,32 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT +# import + +@class SentryReplayOptions; +@class SentryEvent; + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplay : NSObject + +- (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; + +/** + * Start recording the session using rootView as image source. + * If full is @c YES, we transmit the entire session to sentry. + */ +- (void)start:(UIView *)rootView fullSession:(BOOL)full; + +/** + * Stop recording the session replay + */ +- (void)stop; + +- (void)replayForEvent:(SentryEvent *)event; + +@end + +NS_ASSUME_NONNULL_END +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m new file mode 100644 index 00000000000..d145d8ab9df --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.m @@ -0,0 +1,264 @@ +#import "SentrySessionReplay.h" +#import "SentryAttachment+Private.h" +#import "SentryDependencyContainer.h" +#import "SentryFileManager.h" +#import "SentryHub+Private.h" +#import "SentryLog.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" +#import "SentrySDK+Private.h" +#import "SentrySwift.h" + +#if SENTRY_HAS_UIKIT + +static NSString *SENTRY_REPLAY_FOLDER = @"replay"; + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentrySessionReplay { + UIView *_rootView; + BOOL _processingScreenshot; + CADisplayLink *_displayLink; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSURL *_urlToCache; + NSDate *_sessionStart; + SentryReplayOptions *_replayOptions; + SentryOnDemandReplay *_replayMaker; + SentryId *sessionReplayId; + NSMutableArray *imageCollection; + int _currentSegmentId; + BOOL _isFullSession; +} + +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions +{ + if (self = [super init]) { + _replayOptions = replayOptions; + } + return self; +} + +- (SentryCurrentDateProvider *)dateProvider +{ + return SentryDependencyContainer.sharedInstance.dateProvider; +} + +- (void)start:(UIView *)rootView fullSession:(BOOL)full +{ + if (rootView == nil) { + SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); + return; + } + + @synchronized(self) { + if (_displayLink == nil) { + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } else { + // Session display is already running. + return; + } + + _rootView = rootView; + _lastScreenShot = [[NSDate alloc] init]; + _videoSegmentStart = nil; + _sessionStart = _lastScreenShot; + _currentSegmentId = 0; + + NSURL *docs = [NSURL + fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; + + NSString *currentSession = [NSUUID UUID].UUIDString; + _urlToCache = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + + _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; + _replayMaker.bitRate = _replayOptions.replayBitRate; + _replayMaker.cacheMaxSize = (NSInteger)(full ? _replayOptions.sessionSegmentDuration + : _replayOptions.errorReplayDuration); + imageCollection = [NSMutableArray array]; + + NSLog(@"Recording session to %@", _urlToCache); + + _isFullSession = full; + if (full) { + sessionReplayId = [[SentryId alloc] init]; + } + } +} + +- (void)stop +{ + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)replayForEvent:(SentryEvent *)event; +{ + if (_isFullSession) { + return; + } + + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { + return; + } + + if (_isFullSession) { + [self updateEvent:event withReplayId:sessionReplayId]; + return; + } + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + NSDate *replayStart = + [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + + [self createAndCapture:finalPath + duration:_replayOptions.errorReplayDuration + startedAt:replayStart]; + + self->_isFullSession = YES; +} + +- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId +{ + NSMutableDictionary *context = [NSMutableDictionary dictionaryWithDictionary:event.context]; + context[@"replay_id"] = sentryId; + event.context = context; +} + +- (void)newFrame:(CADisplayLink *)sender +{ + NSDate *now = [self dateProvider].date; + + if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { + [self takeScreenshot]; + _lastScreenShot = now; + + if (_videoSegmentStart == nil) { + _videoSegmentStart = now; + } else if (_isFullSession && + [now timeIntervalSinceDate:_videoSegmentStart] + >= _replayOptions.sessionSegmentDuration) { + [self prepareSegmentUntil:now]; + } + } +} + +- (void)prepareSegmentUntil:(NSDate *)date +{ + NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; + NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; + NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { + NSError *error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", + error.localizedDescription); + return; + } + } + + pathToSegment = [pathToSegment + URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + + NSDate *segmentStart = + [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + + [self createAndCapture:pathToSegment + duration:_replayOptions.sessionSegmentDuration + startedAt:segmentStart]; +} + +- (void)createAndCapture:(NSURL *)videoUrl + duration:(NSTimeInterval)duration + startedAt:(NSDate *)start +{ + + if (sessionReplayId == nil) { + sessionReplayId = [[SentryId alloc] init]; + } + [_replayMaker + createVideoWithDuration:duration + beginning:start + outputFileURL:videoUrl + error:nil + completion:^(SentryVideoInfo *videoInfo, NSError *error) { + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); + } else { + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->sessionReplayId + replayType:kSentryReplayTypeSession]; + + [self->_replayMaker releaseFramesUntil:videoInfo.end]; + self->_videoSegmentStart = nil; + } + }]; +} + +- (void)captureSegment:(NSInteger)segment + video:(SentryVideoInfo *)videoInfo + replayId:(SentryId *)replayid + replayType:(SentryReplayType)replayType +{ + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] init]; + replayEvent.replayType = replayType; + replayEvent.eventId = replayid; + replayEvent.replayStartTimestamp = videoInfo.start; + replayEvent.segmentId = segment; + replayEvent.timestamp = videoInfo.end; + + SentryReplayRecording *recording = + [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId + size:videoInfo.fileSize + start:videoInfo.start + duration:videoInfo.duration + frameCount:videoInfo.frameCount + frameRate:videoInfo.frameRate + height:videoInfo.height + width:videoInfo.width]; + + [SentrySDK.currentHub captureReplayEvent:replayEvent + replayRecording:recording + video:videoInfo.path]; +} + +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } + _processingScreenshot = YES; + } + + UIImage *screenshot = [SentryViewPhotographer.shared imageWithView:_rootView]; + + _processingScreenshot = NO; + + dispatch_queue_t backgroundQueue + = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrameWithImage:screenshot]; }); +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplayIntegration.h b/Sources/Sentry/SentrySessionReplayIntegration.h new file mode 100644 index 00000000000..84dc79a2828 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.h @@ -0,0 +1,11 @@ +#import "SentryBaseIntegration.h" +#import "SentrySwift.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentrySessionReplayIntegration : SentryBaseIntegration + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m new file mode 100644 index 00000000000..b801f4c1a52 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -0,0 +1,79 @@ +#import "SentrySessionReplayIntegration.h" +#import "SentryClient+Private.h" +#import "SentryDependencyContainer.h" +#import "SentryGlobalEventProcessor.h" +#import "SentryHub+Private.h" +#import "SentryOptions.h" +#import "SentryRandom.h" +#import "SentrySDK+Private.h" +#import "SentrySessionReplay.h" +#import "SentrySwift.h" + +#if SENTRY_HAS_UIKIT +# import "SentryUIApplication.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentrySessionReplayIntegration { + SentrySessionReplay *sessionReplay; +} + +- (BOOL)installWithOptions:(nonnull SentryOptions *)options +{ + if ([super installWithOptions:options] == NO) { + return NO; + } + + if (@available(iOS 16.0, tvOS 16.0, *)) { + if (options.sessionReplayOptions.replaysSessionSampleRate == 0 + && options.sessionReplayOptions.replaysOnErrorSampleRate == 0) { + return NO; + } + + sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; + + [sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.sessionReplayOptions + .replaysSessionSampleRate]]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [self->sessionReplay replayForEvent:event]; + return event; + }]; + + return YES; + } else { + return NO; + } +} + +- (void)stop +{ + [sessionReplay stop]; +} + +- (SentryIntegrationOption)integrationOptions +{ + return kIntegrationOptionEnableReplay; +} + +- (void)uninstall +{ +} + +- (BOOL)shouldReplayFullSession:(CGFloat)rate +{ + return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; +} + +@end +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index a8c624b883b..4e49390b45a 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -15,7 +15,8 @@ struct SentryReplayFrame { } } -class SentryOnDemandReplay { +@objcMembers +class SentryOnDemandReplay: NSObject { private let _outputPath: String private let _onDemandDispatchQueue: DispatchQueue @@ -133,7 +134,8 @@ class SentryOnDemandReplay { videoWriter.finishWriting { var videoInfo: SentryVideoInfo? if videoWriter.status == .completed { - let fileSize = SentryDependencyContainer.sharedInstance().fileManager.fileSize(outputFileURL) + let fileAttributes = try? FileManager.default.attributesOfItem(atPath: outputFileURL.path) + let fileSize = fileAttributes?[FileAttributeKey.size] as? Int ?? -1 videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(self.videoSize.height), width: Int(self.videoSize.width), duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: actualEnd, fileSize: fileSize) } completion(videoInfo, videoWriter.error) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift deleted file mode 100644 index eb19b393dc1..00000000000 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ /dev/null @@ -1,167 +0,0 @@ -//@_implementationOnly import _SentryPrivate -//import Foundation -// -//#if canImport(UIKit) -//import UIKit -// -//let SENTRY_REPLAY_FOLDER = "replay" -// -//@objcMembers -//class SentrySessionReplay: NSObject { -// private var _rootView: UIView? -// private var _processingScreenshot = false -// private var _displayLink: CADisplayLink? -// private var _lastScreenshot = Date() -// private var _segmentStart: Date? -// private var _sessionStart = Date() -// private var _urlToCache: URL -// private let _replayOptions: SentryReplayOptions -// private var _replayMaker: SentryOnDemandReplay? -// private var _sessionReplayId = SentryId() -// private var _imageCollection = [UIImage]() -// private var _currentSegment = 0 -// private var _isFullSession = false -// private var _processingLock = NSLock() -// -// init(replayOptions: SentryReplayOptions) { -// _replayOptions = replayOptions -// _urlToCache = URL(fileURLWithPath: SentryDependencyContainer.sharedInstance().fileManager.sentryPath) -// .appendingPathComponent(SENTRY_REPLAY_FOLDER) -// .appendingPathComponent(UUID().uuidString) -// } -// -// func start(rootView: UIView, isFullSession: Bool) { -// if _displayLink != nil { -// return -// } -// _displayLink = CADisplayLink(target: self, selector: #selector(newFrame(_:) )) -// _displayLink?.add(to: RunLoop.main, forMode: .common) -// _rootView = rootView -// _lastScreenshot = Date() -// _sessionStart = _lastScreenshot -// _segmentStart = nil -// _currentSegment = 0 -// _sessionReplayId = SentryId() -// -// if !FileManager.default.fileExists(atPath: _urlToCache.path) { -// try? FileManager.default.createDirectory(at: _urlToCache, withIntermediateDirectories: true) -// } -// -// _replayMaker = SentryOnDemandReplay(outputPath: _urlToCache.path) -// _replayMaker?.bitRate = _replayOptions.replayBitRate -// _replayMaker?.cacheMaxSize = UInt(isFullSession ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration) -// _imageCollection.removeAll() -// } -// -// func stop() { -// _displayLink?.invalidate() -// _displayLink = nil -// } -// -// func replayFor(event: Event) { -// if _isFullSession || (event.error == nil && event.exceptions?.count ?? 0 == 0) { -// return -// } -// -// let finalPath = _urlToCache.appendingPathComponent("replay.mp4") -// let replayStart = Date().addingTimeInterval(-_replayOptions.errorReplayDuration) -// -// createAndCapture(finalPath, duration: _replayOptions.errorReplayDuration, startedAt: replayStart) -// promoteToFull() -// } -// -// private func promoteToFull() { -// _isFullSession = true -// _replayMaker?.cacheMaxSize = UInt(_replayOptions.sessionSegmentDuration) -// } -// -// @objc -// private func newFrame(_ sender: CADisplayLink) { -// let now = Date() -// if now.timeIntervalSince(_lastScreenshot) > (1 / Double(_replayOptions.frameRate)) { -// self.takeScreenshot() -// _lastScreenshot = now -// -// if _segmentStart == nil { -// _segmentStart = now -// } else if let _segmentStart, -// _isFullSession && -// now.timeIntervalSince(_segmentStart) >= _replayOptions.sessionSegmentDuration { -// prepareSegmentUntil(now) -// } -// } -// } -// -// private func prepareSegmentUntil(_ date: Date) { -// guard let _segmentStart else { return } -// let from = _segmentStart.timeIntervalSince(_sessionStart) -// let to = date.timeIntervalSince(_sessionStart) -// var pathToSegment = _urlToCache.appendingPathComponent("segments") -// if !FileManager.default.fileExists(atPath: pathToSegment.path) { -// try? FileManager.default.createDirectory(at: pathToSegment, withIntermediateDirectories: true) -// } -// pathToSegment = pathToSegment.appendingPathComponent("\(from)-\(to).mp4") -// let segmentStart = Date().addingTimeInterval(-_replayOptions.sessionSegmentDuration) -// -// createAndCapture(pathToSegment, duration: _replayOptions.sessionSegmentDuration, startedAt: segmentStart) -// -// } -// -// private func createAndCapture( _ path: URL, duration: TimeInterval, startedAt start: Date ) { -// do { -// try _replayMaker?.createVideoWith(duration: duration, beginning: start, outputFileURL: path, completion: { [weak self] videoInfo, error in -// guard let self = self else { return } -// if let error { -// print("[SentrySessionReplay] Could not create replay video - \(error)") -// } else if let videoInfo { -// self.captureSegment(id: self._currentSegment, video: videoInfo, replayType: .session) -// self._replayMaker?.releaseFramesUntil(videoInfo.end) -// self._segmentStart = nil -// self._currentSegment += 1 -// } -// }) -// } catch { -// print("[SentrySessionReplay] Could not generate session replay segment - \(error)") -// } -// } -// -// private func captureSegment(id: Int, video: SentryVideoInfo, replayType: SentryReplayType) { -// let replayEvent = SentryReplayEvent() -// replayEvent.replayType = replayType -// replayEvent.eventId = self._sessionReplayId -// replayEvent.replayStartTimestamp = video.start -// replayEvent.segmentId = id -// replayEvent.timestamp = video.end -// -// let recording = SentryReplayRecording(segmentId: id, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width) -// -// SentrySDK.currentHub().capture(replayEvent, replayRecording: recording, video: video.path) -// } -// -// func takeScreenshot() { -// if _processingScreenshot { -// return -// } -// -// _processingLock.lock() -// if _processingScreenshot { -// _processingLock.unlock() -// return -// } -// -// _processingScreenshot = true -// _processingLock.unlock() -// -// defer { _processingScreenshot = false } -// -// guard let _rootView, let screenshot = SentryViewPhotographer.shared.image(view: _rootView) else { return } -// -// let backgroundQueue = DispatchQueue.global(qos: .default) -// backgroundQueue.async { [weak self] in -// guard let self = self else { return } -// self._replayMaker?.addFrame(image: screenshot) -// } -// } -//} -// -//#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift deleted file mode 100644 index cc22859b8d0..00000000000 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift +++ /dev/null @@ -1,55 +0,0 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -#if canImport(UIKit) -import UIKit - -@objcMembers -class SentrySessionReplayIntegration: NSObject, SentryIntegrationProtocol { - - private var sessionReplay: SentrySessionReplay? - - func install(with options: Options) -> Bool { - if #available(iOS 16.0, tvOS 16, *) { - if options.sessionReplayOptions.replaysSessionSampleRate == 0 && - options.sessionReplayOptions.replaysOnErrorSampleRate == 0 { - print("[SessionReplayIntegration] No enabled because sessionReplayOptions is disabled.") - return false - } - - guard let window = SentryDependencyContainer.sharedInstance().application.windows?.first else { - print("[SessionReplayIntegration] No window to record") - return false - } - - sessionReplay = SentrySessionReplay(replayOptions: options.sessionReplayOptions) - sessionReplay?.start(rootView: window, isFullSession: shouldReplayFullSession(sampleRate: options.sessionReplayOptions.replaysSessionSampleRate)) - - NotificationCenter.default.addObserver(self, selector: #selector(stop), name: UIApplication.didEnterBackgroundNotification, object: nil) - - SentryGlobalEventProcessor.shared().add { event in - self.sessionReplay?.replayFor(event: event) - return event - } - - } else { - print("[SessionReplayIntegration] OS version not supported for session replay. Requires iOS 16 or tvOS 16") - return false - } - return true - } - - func stop() { - sessionReplay?.stop() - } - - func uninstall() { - stop() - } - - func shouldReplayFullSession(sampleRate: Float) -> Bool { - return SentryDependencyContainer.sharedInstance().random.nextNumber() < Double(sampleRate) - } -} - -#endif // canImport(UIKit) From 1612a2140c281f477a09f4937e249d79b24aac1d Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 11:22:08 +0100 Subject: [PATCH 61/88] Update Package.swift --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index dc9cb29114b..93455c3b6a3 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,13 @@ let package = Package( targets: [ .binaryTarget( name: "Sentry", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry.xcframework.zip", - checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Static + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.4/Sentry.xcframework.zip", + checksum: "0fb20e85ff8fe2fdfcf6add48bd510bccf113f7db3795931e1d8dc0dbbc6d46d" //Sentry-Static ), .binaryTarget( name: "Sentry-Dynamic", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.0-alpha.0/Sentry-Dynamic.xcframework.zip", - checksum: "86156301aee5c8774a8cd5c240286f914f6e7721aaac5a7c9d049ea613a4b730" //Sentry-Dynamic + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.22.4/Sentry-Dynamic.xcframework.zip", + checksum: "391cb3b9fe2e967383e9232c53daa547ca60a02b1515ff99da6515dbced165a5" //Sentry-Dynamic ), .target ( name: "SentrySwiftUI", dependencies: ["Sentry", "SentryInternal"], From 8fd5e306556470ea9b3b6f6ff041c0ca4bb10b09 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 13:27:17 +0100 Subject: [PATCH 62/88] Fixing CI --- Sentry.xcodeproj/project.pbxproj | 8 ++++++++ Sources/Sentry/SentryCoreGraphicsHelper.m | 13 +++++++++++++ Sources/Sentry/include/SentryCoreGraphicsHelper.h | 10 ++++++++++ Sources/Sentry/include/SentryPrivate.h | 1 + .../SessionReplay/SentryPixelBuffer.swift | 2 +- .../SessionReplay/SentryViewPhotographer.swift | 8 ++------ 6 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 Sources/Sentry/SentryCoreGraphicsHelper.m create mode 100644 Sources/Sentry/include/SentryCoreGraphicsHelper.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 474fc35bb0d..f825838265c 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -769,6 +769,8 @@ D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB12BB1886100BA339D /* SentrySessionReplay.h */; }; D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; + D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */; }; + D820CE142BB2F13C00BA339D /* SentryCoreGraphicsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; @@ -1755,6 +1757,8 @@ D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentrySessionReplayIntegration.h; sourceTree = ""; }; D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCoreGraphicsHelper.h; path = include/SentryCoreGraphicsHelper.h; sourceTree = ""; }; + D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCoreGraphicsHelper.m; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; @@ -3435,6 +3439,8 @@ D820CDB22BB1886100BA339D /* SentrySessionReplay.m */, D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, + D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */, + D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */, ); name = SessionReplay; sourceTree = ""; @@ -3673,6 +3679,7 @@ 7B0A54222521C21E00A71716 /* SentryFrameRemover.h in Headers */, 63FE70CD20DA4C1000CDBAE8 /* SentryCrashDoctor.h in Headers */, D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */, + D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */, 7B6438AA26A70F24000D0F65 /* UIViewController+Sentry.h in Headers */, 639FCFAC1EBC811400778193 /* SentryUser.h in Headers */, D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, @@ -4191,6 +4198,7 @@ 7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */, 7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */, 7DC27EC723997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.m in Sources */, + D820CE142BB2F13C00BA339D /* SentryCoreGraphicsHelper.m in Sources */, 63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */, 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */, 7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */, diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m new file mode 100644 index 00000000000..691edb64370 --- /dev/null +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -0,0 +1,13 @@ +#import "SentryCoreGraphicsHelper.h" + +@implementation SentryCoreGraphicsHelper ++ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ + if (@available(iOS 16.0, tvOS 16.0, *)) { + CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); + CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); + return CGPathCreateMutableCopy(newPath); + } + return path; +} +@end diff --git a/Sources/Sentry/include/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h new file mode 100644 index 00000000000..10e25535224 --- /dev/null +++ b/Sources/Sentry/include/SentryCoreGraphicsHelper.h @@ -0,0 +1,10 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryCoreGraphicsHelper : NSObject ++ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path; +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index db30e35c92f..48a58c45403 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,6 +1,7 @@ // Sentry internal headers that are needed for swift code #import "SentryBaggage.h" +#import "SentryCoreGraphicsHelper.h" #import "SentryFileManager.h" #import "SentryGlobalEventProcessor.h" #import "SentryRandom.h" diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index 4f5018797a5..ec1eb9b9be6 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -19,7 +19,7 @@ class SentryPixelBuffer { } func append(image: UIImage, pixelBufferAdapter: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { - guard let pixelBuffer else { return false } + guard let pixelBuffer = pixelBuffer else { return false } CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index cff98dc978b..d6ad147f381 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -1,3 +1,4 @@ +@_implementationOnly import _SentryPrivate import Foundation #if canImport(UIKit) @@ -92,12 +93,7 @@ class SentryViewPhotographer: NSObject { } private func excludeRect(_ rectangle: CGRect, fromPath path: CGMutablePath) -> CGMutablePath { - if #available(iOS 16.0, tvOS 16.0, *) { - let exclude = CGPath(rect: rectangle, transform: nil) - let newPath = path.subtracting(exclude, using: .evenOdd) - return newPath.mutableCopy() ?? path - } - return path + return SentryCoreGraphicsHelper.excludeRect(rectangle, from: path).takeRetainedValue() } private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { From 0d26201e4b70803161a70af07383eeeaf771f081 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 13:40:37 +0100 Subject: [PATCH 63/88] wip --- Sources/Sentry/SentryCoreGraphicsHelper.m | 3 ++- Sources/Sentry/include/SentryCoreGraphicsHelper.h | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m index 691edb64370..822bd91b936 100644 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -1,5 +1,5 @@ #import "SentryCoreGraphicsHelper.h" - +#if SENTRY_HAS_UIKIT @implementation SentryCoreGraphicsHelper + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { @@ -11,3 +11,4 @@ + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)pat return path; } @end +#endif diff --git a/Sources/Sentry/include/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h index 10e25535224..e561984de1b 100644 --- a/Sources/Sentry/include/SentryCoreGraphicsHelper.h +++ b/Sources/Sentry/include/SentryCoreGraphicsHelper.h @@ -1,10 +1,13 @@ +#import "SentryDefines.h" #import #import NS_ASSUME_NONNULL_BEGIN +#if SENTRY_HAS_UIKIT @interface SentryCoreGraphicsHelper : NSObject + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path; @end +#endif NS_ASSUME_NONNULL_END From 846a53a78660c0286d923dbc782d84755ab7ffff Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 13:57:33 +0100 Subject: [PATCH 64/88] restrict version --- Sources/Sentry/SentrySessionReplay.h | 1 + Sources/Sentry/SentrySessionReplay.m | 8 +++++++- .../Sentry/SentrySessionReplayIntegration.m | 19 ++++++++++++------- .../SessionReplay/SentryOnDemandReplay.swift | 1 + .../SessionReplay/SentryPixelBuffer.swift | 2 ++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.h b/Sources/Sentry/SentrySessionReplay.h index 72d5764c2d9..56f8710681a 100644 --- a/Sources/Sentry/SentrySessionReplay.h +++ b/Sources/Sentry/SentrySessionReplay.h @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN +API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject - (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index d145d8ab9df..bf0c107f1a6 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -10,7 +10,9 @@ #import "SentrySwift.h" #if SENTRY_HAS_UIKIT - +# if TARGET_OS_IOS || TARGET_OS_TVOS +# if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_16_0 \ + || __TV_OS_VERSION_MIN_REQUIRED >= __TV_16_0 static NSString *SENTRY_REPLAY_FOLDER = @"replay"; NS_ASSUME_NONNULL_BEGIN @@ -259,6 +261,10 @@ - (void)takeScreenshot @end +# endif // __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_16_0 || __TV_OS_VERSION_MIN_REQUIRED + // >= __TV_16_0 +# endif // TARGET_OS_IOS || TARGET_OS_TVOS + NS_ASSUME_NONNULL_END #endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index b801f4c1a52..7e97290a8be 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -14,9 +14,13 @@ NS_ASSUME_NONNULL_BEGIN -@implementation SentrySessionReplayIntegration { - SentrySessionReplay *sessionReplay; -} +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentrySessionReplayIntegration () +@property (nonatomic, strong) SentrySessionReplay *sessionReplay; +@end + +@implementation SentrySessionReplayIntegration - (BOOL)installWithOptions:(nonnull SentryOptions *)options { @@ -30,9 +34,10 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; + self.sessionReplay = + [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; - [sessionReplay + [self.sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject fullSession:[self shouldReplayFullSession:options.sessionReplayOptions .replaysSessionSampleRate]]; @@ -44,7 +49,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - [self->sessionReplay replayForEvent:event]; + [self.sessionReplay replayForEvent:event]; return event; }]; @@ -56,7 +61,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options - (void)stop { - [sessionReplay stop]; + [self.sessionReplay stop]; } - (SentryIntegrationOption)integrationOptions diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 4e49390b45a..b85096d8b98 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -15,6 +15,7 @@ struct SentryReplayFrame { } } +@available(iOS 16.0, tvOS 16.0, *) @objcMembers class SentryOnDemandReplay: NSObject { private let _outputPath: String diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index ec1eb9b9be6..7d025b4cbf0 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -5,6 +5,8 @@ import CoreGraphics import Foundation import UIKit +@available(iOS, introduced: 16.0) +@available(tvOS, introduced: 16.0) class SentryPixelBuffer { private var pixelBuffer: CVPixelBuffer? private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() From 355f62764b54d35b9bc6e3042f6feb8492af1d28 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 14:13:19 +0100 Subject: [PATCH 65/88] replay for ios and tvos --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 3 +++ .../Swift/Integrations/SessionReplay/SentryPixelBuffer.swift | 5 ++--- .../Integrations/SessionReplay/SentryViewPhotographer.swift | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index b85096d8b98..fc24af02ebe 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -1,4 +1,6 @@ #if canImport(UIKit) +#if os(iOS) || os(tvOS) + @_implementationOnly import _SentryPrivate import AVFoundation import CoreGraphics @@ -158,4 +160,5 @@ class SentryOnDemandReplay: NSObject { } } +#endif // os(iOS) || os(tvOS) #endif // canImport(UIKit) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index 7d025b4cbf0..ada36733399 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -5,8 +5,7 @@ import CoreGraphics import Foundation import UIKit -@available(iOS, introduced: 16.0) -@available(tvOS, introduced: 16.0) +#if os(iOS) || os(tvOS) class SentryPixelBuffer { private var pixelBuffer: CVPixelBuffer? private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() @@ -46,5 +45,5 @@ class SentryPixelBuffer { return pixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime) } } - +#endif #endif // canImport(UIKit) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index d6ad147f381..76a7cbe35ef 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -2,9 +2,13 @@ import Foundation #if canImport(UIKit) +#if os(iOS) || os(tvOS) + import CoreGraphics import UIKit +@available(iOS, introduced: 16.0) +@available(tvOS, introduced: 16.0) @objcMembers class SentryViewPhotographer: NSObject { @@ -101,4 +105,5 @@ class SentryViewPhotographer: NSObject { } } +#endif #endif // canImport(UIKit) From 4376764528b40b8cdaf614154b44ebaec00963b7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 14:26:48 +0100 Subject: [PATCH 66/88] Update SentrySessionReplay.m --- Sources/Sentry/SentrySessionReplay.m | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index bf0c107f1a6..d00f50ea075 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -10,9 +10,6 @@ #import "SentrySwift.h" #if SENTRY_HAS_UIKIT -# if TARGET_OS_IOS || TARGET_OS_TVOS -# if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_16_0 \ - || __TV_OS_VERSION_MIN_REQUIRED >= __TV_16_0 static NSString *SENTRY_REPLAY_FOLDER = @"replay"; NS_ASSUME_NONNULL_BEGIN @@ -113,11 +110,6 @@ - (void)replayForEvent:(SentryEvent *)event; return; } - if (_isFullSession) { - [self updateEvent:event withReplayId:sessionReplayId]; - return; - } - NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; NSDate *replayStart = [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; @@ -129,13 +121,6 @@ - (void)replayForEvent:(SentryEvent *)event; self->_isFullSession = YES; } -- (void)updateEvent:(SentryEvent *)event withReplayId:(SentryId *)sentryId -{ - NSMutableDictionary *context = [NSMutableDictionary dictionaryWithDictionary:event.context]; - context[@"replay_id"] = sentryId; - event.context = context; -} - - (void)newFrame:(CADisplayLink *)sender { NSDate *now = [self dateProvider].date; @@ -261,10 +246,6 @@ - (void)takeScreenshot @end -# endif // __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_16_0 || __TV_OS_VERSION_MIN_REQUIRED - // >= __TV_16_0 -# endif // TARGET_OS_IOS || TARGET_OS_TVOS - NS_ASSUME_NONNULL_END #endif // SENTRY_HAS_UIKIT From f2fd17953e826c2bf8111329f8e925a2ac198112 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 26 Mar 2024 14:47:21 +0100 Subject: [PATCH 67/88] Check for UIKit --- Sentry.xcodeproj/project.pbxproj | 6 ++++++ .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../Integrations/SessionReplay/SentryPixelBuffer.swift | 2 +- .../SessionReplay/SentryViewPhotographer.swift | 7 +++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index f825838265c..ce16ef131e8 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -4943,6 +4943,7 @@ LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4985,6 +4986,7 @@ ); ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5163,6 +5165,7 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5304,6 +5307,7 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = "-DSENTRY_NO_UIKIT"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5793,6 +5797,7 @@ ); ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = "-DSENTRY_NO_UIKIT"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6033,6 +6038,7 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index fc24af02ebe..c9ee00aa9ec 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -1,4 +1,4 @@ -#if canImport(UIKit) +#if canImport(UIKit) && !SENTRY_NO_UIKIT #if os(iOS) || os(tvOS) @_implementationOnly import _SentryPrivate diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index ada36733399..3c9de36f0d7 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -1,4 +1,4 @@ -#if canImport(UIKit) +#if canImport(UIKit) && !SENTRY_NO_UIKIT import AVFoundation import CoreGraphics diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index 76a7cbe35ef..7692afc0920 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -1,10 +1,9 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -#if canImport(UIKit) +#if canImport(UIKit) && !SENTRY_NO_UIKIT #if os(iOS) || os(tvOS) +@_implementationOnly import _SentryPrivate import CoreGraphics +import Foundation import UIKit @available(iOS, introduced: 16.0) From 2eec7ba20b3fe34b6d26ca9dcbfeb7751d52fdb0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 11:54:18 +0100 Subject: [PATCH 68/88] Update SentryReplayEventTests.swift --- .../Integrations/SessionReplay/SentryReplayEventTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift index 024a4a2aa2e..00dc0162ab8 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift @@ -20,7 +20,7 @@ class SentryReplayEventTests: XCTestCase { let result = sut.serialize() expect(result["urls"] as? [String]) == ["Screen 1", "Screen 2"] - expect(result["replay_start_timestamp"] as? Int) == 1_000 + expect(result["replay_start_timestamp"] as? Int) == 1 expect(result["trace_ids"] as? [String]) == [ traceIds[0].sentryIdString, traceIds[1].sentryIdString] expect(result["replay_id"] as? String) == replayId.sentryIdString expect(result["segment_id"] as? Int) == 3 From 693decfeb15982d66bcd24a6cb1ab0027ca1e97c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 13:25:43 +0100 Subject: [PATCH 69/88] fix tests --- Sentry.xcodeproj/project.pbxproj | 6 ------ Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm | 1 - Sources/Sentry/SentryCoreGraphicsHelper.m | 3 +++ Sources/Sentry/SentrySessionReplay.m | 1 + Sources/Sentry/include/SentrySwift.h | 1 + 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ce16ef131e8..2a42053d855 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -6324,7 +6324,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -6381,7 +6380,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug_without_UIKit; @@ -6435,7 +6433,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Test; @@ -6489,7 +6486,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = TestCI; @@ -6543,7 +6539,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -6597,7 +6592,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release_without_UIKit; diff --git a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm index fe2913dfa2f..8ab1a1eca43 100644 --- a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm +++ b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm @@ -1,7 +1,6 @@ #import "SentryProfiledTracerConcurrency.h" #if SENTRY_TARGET_PROFILING_SUPPORTED - # import "SentryInternalDefines.h" # import "SentryLog.h" # import "SentryProfiler+Private.h" diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m index 822bd91b936..b6c1454bb90 100644 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -3,11 +3,14 @@ @implementation SentryCoreGraphicsHelper + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { +# if (TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0) \ + || (TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_16_0) if (@available(iOS 16.0, tvOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); } +# endif return path; } @end diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index d00f50ea075..8e3c19df71e 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -83,6 +83,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full _replayMaker.bitRate = _replayOptions.replayBitRate; _replayMaker.cacheMaxSize = (NSInteger)(full ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration); + imageCollection = [NSMutableArray array]; NSLog(@"Recording session to %@", _urlToCache); diff --git a/Sources/Sentry/include/SentrySwift.h b/Sources/Sentry/include/SentrySwift.h index 861a37090c8..db24a8d62f5 100644 --- a/Sources/Sentry/include/SentrySwift.h +++ b/Sources/Sentry/include/SentrySwift.h @@ -3,6 +3,7 @@ #ifdef __cplusplus # if __has_include() +# import # import # endif #endif From 9224a00289590b8895dec4676f3de1f6e68d6251 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 15:36:58 +0100 Subject: [PATCH 70/88] fixing references --- Sources/Sentry/SentryCoreGraphicsHelper.m | 5 ++- Sources/Sentry/include/SentrySwift.h | 2 +- .../SessionReplay/SentryOnDemandReplay.swift | 16 ++++--- .../SessionReplay/SentryPixelBuffer.swift | 2 +- .../SentryViewPhotographer.swift | 2 +- scripts/build-xcframework.sh | 43 +++++++++++++++++-- 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m index b6c1454bb90..5da4722ba9b 100644 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -3,13 +3,14 @@ @implementation SentryCoreGraphicsHelper + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { -# if (TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0) \ - || (TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_16_0) +# if (TARGET_OS_IOS || TARGET_OS_TV) +# ifdef __IPHONE_16_0 if (@available(iOS 16.0, tvOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); } +# endif # endif return path; } diff --git a/Sources/Sentry/include/SentrySwift.h b/Sources/Sentry/include/SentrySwift.h index db24a8d62f5..01120ac77a8 100644 --- a/Sources/Sentry/include/SentrySwift.h +++ b/Sources/Sentry/include/SentrySwift.h @@ -3,9 +3,9 @@ #ifdef __cplusplus # if __has_include() -# import # import # endif +# import #endif #if __has_include("Sentry-Swift.h") diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index c9ee00aa9ec..d57b355fc3e 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -1,5 +1,5 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || os(visionOS) @_implementationOnly import _SentryPrivate import AVFoundation @@ -17,7 +17,7 @@ struct SentryReplayFrame { } } -@available(iOS 16.0, tvOS 16.0, *) +@available(iOS 16.0, tvOS 16.0, visionOS 1.0, *) @objcMembers class SentryOnDemandReplay: NSObject { private let _outputPath: String @@ -27,7 +27,9 @@ class SentryOnDemandReplay: NSObject { private var _frames = [SentryReplayFrame]() private var _currentPixelBuffer: SentryPixelBuffer? - var videoSize = CGSize(width: 200, height: 434) + var videoWidth = 200 + var videoHeight = 434 + var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max @@ -117,7 +119,7 @@ class SentryOnDemandReplay: NSObject { if frames.isEmpty { return } - _currentPixelBuffer = SentryPixelBuffer(size: videoSize) + _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight)) videoWriterInput.requestMediaDataWhenReady(on: _onDemandDispatchQueue) { [weak self] in guard let self = self else { return } @@ -139,7 +141,7 @@ class SentryOnDemandReplay: NSObject { if videoWriter.status == .completed { let fileAttributes = try? FileManager.default.attributesOfItem(atPath: outputFileURL.path) let fileSize = fileAttributes?[FileAttributeKey.size] as? Int ?? -1 - videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(self.videoSize.height), width: Int(self.videoSize.width), duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: actualEnd, fileSize: fileSize) + videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: actualEnd, fileSize: fileSize) } completion(videoInfo, videoWriter.error) } @@ -150,8 +152,8 @@ class SentryOnDemandReplay: NSObject { private func createVideoSettings() -> [String: Any] { return [ AVVideoCodecKey: AVVideoCodecType.h264, - AVVideoWidthKey: videoSize.width, - AVVideoHeightKey: videoSize.height, + AVVideoWidthKey: videoWidth, + AVVideoHeightKey: videoHeight, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: bitRate, AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index 3c9de36f0d7..be6069af4f1 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -5,7 +5,7 @@ import CoreGraphics import Foundation import UIKit -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || os(visionOS) class SentryPixelBuffer { private var pixelBuffer: CVPixelBuffer? private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index 7692afc0920..cc13b180510 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -1,5 +1,5 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT -#if os(iOS) || os(tvOS) +#if os(iOS) || os(tvOS) || os(visionOS) @_implementationOnly import _SentryPrivate import CoreGraphics diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh index 034adecbd4f..9662a22d0be 100755 --- a/scripts/build-xcframework.sh +++ b/scripts/build-xcframework.sh @@ -1,6 +1,8 @@ #!/bin/bash -sdks=(iphoneos iphonesimulator macosx appletvos appletvsimulator watchos watchsimulator xros xrsimulator) +set -eou pipefail + +sdks=( iphoneos iphonesimulator macosx appletvos appletvsimulator watchos watchsimulator xros xrsimulator ) rm -rf Carthage/ mkdir Carthage @@ -11,14 +13,34 @@ generate_xcframework() { local scheme="$1" local sufix="${2:-}" local MACH_O_TYPE="${3-mh_dylib}" - + local configuration="${4-Release}" local createxcframework="xcodebuild -create-xcframework " + local GCC_GENERATE_DEBUGGING_SYMBOLS="YES" + + if [ "$MACH_O_TYPE" = "staticlib" ]; then + #For static framework we disabled symbols because they are not distributed in the framework causing warnings. + GCC_GENERATE_DEBUGGING_SYMBOLS="NO" + fi + + rm -rf Carthage/DerivedData for sdk in "${sdks[@]}"; do if [[ -n "$(grep "${sdk}" <<< "$ALL_SDKS")" ]]; then - xcodebuild archive -project Sentry.xcodeproj/ -scheme "$scheme" -configuration Release -sdk "$sdk" -archivePath ./Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE=$MACH_O_TYPE - + + xcodebuild archive -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$configuration" -sdk "$sdk" -archivePath ./Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE=$MACH_O_TYPE ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS" + createxcframework+="-framework Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive/Products/Library/Frameworks/${scheme}.framework " + + if [ "$MACH_O_TYPE" = "staticlib" ]; then + local infoPlist="Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive/Products/Library/Frameworks/${scheme}.framework/Info.plist" + + if [ ! -e "$infoPlist" ]; then + infoPlist="Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive/Products/Library/Frameworks/${scheme}.framework/Resources/Info.plist" + fi + # This workaround is necessary to make Sentry Static framework to work + #More information in here: https://github.com/getsentry/sentry-cocoa/issues/3769 + plutil -replace "MinimumOSVersion" -string "9999" "$infoPlist" + fi if [ -d "Carthage/archive/${scheme}${sufix}/${sdk}.xcarchive/dSYMs/${scheme}.framework.dSYM" ]; then # Has debug symbols @@ -29,6 +51,19 @@ generate_xcframework() { fi done + #Create framework for mac catalyst + xcodebuild -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$configuration" -sdk iphoneos -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath ./Carthage/DerivedData CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE=$MACH_O_TYPE SUPPORTS_MACCATALYST=YES ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS" + + if [ "$MACH_O_TYPE" = "staticlib" ]; then + local infoPlist="Carthage/DerivedData/Build/Products/"$configuration"-maccatalyst/${scheme}.framework/Resources/Info.plist" + plutil -replace "MinimumOSVersion" -string "9999" "$infoPlist" + fi + + createxcframework+="-framework Carthage/DerivedData/Build/Products/"$configuration"-maccatalyst/${scheme}.framework " + if [ -d "Carthage/DerivedData/Build/Products/"$configuration"-maccatalyst/${scheme}.framework.dSYM" ]; then + createxcframework+="-debug-symbols $(pwd -P)/Carthage/DerivedData/Build/Products/"$configuration"-maccatalyst/${scheme}.framework.dSYM " + fi + createxcframework+="-output Carthage/${scheme}${sufix}.xcframework" $createxcframework } From 456e7256b2c9c74b0adda0309669fc01bef1299b Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 15:42:50 +0100 Subject: [PATCH 71/88] Update SentryOnDemandReplay.swift --- .../Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index d57b355fc3e..970aa8d6f66 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -17,7 +17,7 @@ struct SentryReplayFrame { } } -@available(iOS 16.0, tvOS 16.0, visionOS 1.0, *) +@available(iOS 16.0, tvOS 16.0, *) @objcMembers class SentryOnDemandReplay: NSObject { private let _outputPath: String From 2b254d167ddfde91dd888c033e60399369ea67dd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 15:55:18 +0100 Subject: [PATCH 72/88] remove vision --- Sources/Sentry/SentryOptions.m | 2 ++ Sources/Sentry/SentrySessionReplay.h | 2 +- Sources/Sentry/SentrySessionReplay.m | 2 +- Sources/Sentry/SentrySessionReplayIntegration.h | 4 ++-- Sources/Sentry/SentrySessionReplayIntegration.m | 2 +- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../Swift/Integrations/SessionReplay/SentryPixelBuffer.swift | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 1f8c2834cc8..59e7ba8fe53 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -57,7 +57,9 @@ - (void)setMeasurement:(SentryMeasurementValue *)measurement NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), +# if !TARGET_OS_VISION NSStringFromClass([SentrySessionReplayIntegration class]), +# endif #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), diff --git a/Sources/Sentry/SentrySessionReplay.h b/Sources/Sentry/SentrySessionReplay.h index 56f8710681a..c0ace81dca2 100644 --- a/Sources/Sentry/SentrySessionReplay.h +++ b/Sources/Sentry/SentrySessionReplay.h @@ -1,7 +1,7 @@ #import "SentryDefines.h" #import -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION # import @class SentryReplayOptions; diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 8e3c19df71e..0859f30ce05 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -9,7 +9,7 @@ #import "SentrySDK+Private.h" #import "SentrySwift.h" -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION static NSString *SENTRY_REPLAY_FOLDER = @"replay"; NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentrySessionReplayIntegration.h b/Sources/Sentry/SentrySessionReplayIntegration.h index 84dc79a2828..7bc085cc181 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/SentrySessionReplayIntegration.h @@ -3,9 +3,9 @@ #import NS_ASSUME_NONNULL_BEGIN - +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION @interface SentrySessionReplayIntegration : SentryBaseIntegration @end - +#endif NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 7e97290a8be..5f486702a4b 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -9,7 +9,7 @@ #import "SentrySessionReplay.h" #import "SentrySwift.h" -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION # import "SentryUIApplication.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 970aa8d6f66..6f23eaab0ef 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -1,5 +1,5 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT -#if os(iOS) || os(tvOS) || os(visionOS) +#if os(iOS) || os(tvOS) @_implementationOnly import _SentryPrivate import AVFoundation diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index be6069af4f1..3c9de36f0d7 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -5,7 +5,7 @@ import CoreGraphics import Foundation import UIKit -#if os(iOS) || os(tvOS) || os(visionOS) +#if os(iOS) || os(tvOS) class SentryPixelBuffer { private var pixelBuffer: CVPixelBuffer? private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() From a73feb18556b66fbc48ea39aa150dc2a164f2606 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 15:59:50 +0100 Subject: [PATCH 73/88] Update SentryViewPhotographer.swift --- .../Integrations/SessionReplay/SentryViewPhotographer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index cc13b180510..7692afc0920 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -1,5 +1,5 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT -#if os(iOS) || os(tvOS) || os(visionOS) +#if os(iOS) || os(tvOS) @_implementationOnly import _SentryPrivate import CoreGraphics From 4b4e7b394c79cff7f07370bb720edee1156f7517 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 27 Mar 2024 16:23:08 +0100 Subject: [PATCH 74/88] Update SentrySessionReplayIntegration.h --- Sources/Sentry/SentrySessionReplayIntegration.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.h b/Sources/Sentry/SentrySessionReplayIntegration.h index 7bc085cc181..63458ab6df2 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/SentrySessionReplayIntegration.h @@ -1,4 +1,5 @@ #import "SentryBaseIntegration.h" +#import "SentryDefines.h" #import "SentrySwift.h" #import From 5bdf0dd7a53012cbf48328045925d256a584030d Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 28 Mar 2024 10:52:01 +0100 Subject: [PATCH 75/88] tests --- Sentry.xcodeproj/project.pbxproj | 8 +- .../SentryProfiledTracerConcurrency.mm | 1 + Sources/Sentry/SentrySessionReplay.m | 16 ++-- .../Sentry/SentrySessionReplayIntegration.m | 10 ++- .../{ => include}/SentrySessionReplay.h | 4 +- .../SentrySessionReplayIntegration.h | 0 Sources/Sentry/include/SentrySwift.h | 1 - .../Helper/SentryFileManagerTests.swift | 12 +++ .../SentrySessionReplayIntegrationTests.swift | 74 +++++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 10 files changed, 111 insertions(+), 16 deletions(-) rename Sources/Sentry/{ => include}/SentrySessionReplay.h (78%) rename Sources/Sentry/{ => include}/SentrySessionReplayIntegration.h (100%) create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 2a42053d855..846c5d8503c 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -800,6 +800,7 @@ D85D3BEA278DF63D001B2889 /* SentryByteCountFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85D3BE9278DF63D001B2889 /* SentryByteCountFormatterTests.swift */; }; D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */ = {isa = PBXBuildFile; fileRef = D8603DD4284F8497000E1227 /* SentryBaggage.m */; }; D8603DD8284F894C000E1227 /* SentryBaggage.h in Headers */ = {isa = PBXBuildFile; fileRef = D8603DD7284F894C000E1227 /* SentryBaggage.h */; }; + D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */; }; D865892F29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */; }; D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */ = {isa = PBXBuildFile; fileRef = D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */; }; D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */; }; @@ -1753,9 +1754,9 @@ D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PrivateSentrySDKOnly.h; path = include/HybridPublic/PrivateSentrySDKOnly.h; sourceTree = ""; }; D81A349F291D5568005A27A9 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenShotTests.swift; sourceTree = ""; }; - D820CDB12BB1886100BA339D /* SentrySessionReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentrySessionReplay.h; sourceTree = ""; }; + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; - D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentrySessionReplayIntegration.h; sourceTree = ""; }; + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCoreGraphicsHelper.h; path = include/SentryCoreGraphicsHelper.h; sourceTree = ""; }; D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCoreGraphicsHelper.m; sourceTree = ""; }; @@ -1790,6 +1791,7 @@ D85D3BE9278DF63D001B2889 /* SentryByteCountFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryByteCountFormatterTests.swift; sourceTree = ""; }; D8603DD4284F8497000E1227 /* SentryBaggage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBaggage.m; sourceTree = ""; }; D8603DD7284F894C000E1227 /* SentryBaggage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryBaggage.h; path = include/SentryBaggage.h; sourceTree = ""; }; + D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegrationTests.swift; sourceTree = ""; }; D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryCrashBinaryImageCache.h; sourceTree = ""; }; D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentryCrashBinaryImageCache.c; sourceTree = ""; }; D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTrackingIntegration.h; path = include/SentryCoreDataTrackingIntegration.h; sourceTree = ""; }; @@ -3413,6 +3415,7 @@ children = ( D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, + D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */, ); path = SessionReplay; sourceTree = ""; @@ -4591,6 +4594,7 @@ 7BA0C04C28056556003E0326 /* SentryTransportAdapterTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, + D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, 7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */, 7B4260342630315C00B36EDD /* SampleError.swift in Sources */, D855B3E827D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift in Sources */, diff --git a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm index 8ab1a1eca43..fe2913dfa2f 100644 --- a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm +++ b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm @@ -1,6 +1,7 @@ #import "SentryProfiledTracerConcurrency.h" #if SENTRY_TARGET_PROFILING_SUPPORTED + # import "SentryInternalDefines.h" # import "SentryLog.h" # import "SentryProfiler+Private.h" diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 0859f30ce05..943f832fdcc 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -28,21 +28,19 @@ @implementation SentrySessionReplay { NSMutableArray *imageCollection; int _currentSegmentId; BOOL _isFullSession; + SentryCurrentDateProvider *_dateProvider; } - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + dateProvider:(SentryCurrentDateProvider *)dateProvider { if (self = [super init]) { _replayOptions = replayOptions; + _dateProvider = dateProvider; } return self; } -- (SentryCurrentDateProvider *)dateProvider -{ - return SentryDependencyContainer.sharedInstance.dateProvider; -} - - (void)start:(UIView *)rootView fullSession:(BOOL)full { if (rootView == nil) { @@ -103,7 +101,7 @@ - (void)stop - (void)replayForEvent:(SentryEvent *)event; { - if (_isFullSession) { + if (_isFullSession || _displayLink == nil) { return; } @@ -113,7 +111,7 @@ - (void)replayForEvent:(SentryEvent *)event; NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; NSDate *replayStart = - [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; [self createAndCapture:finalPath duration:_replayOptions.errorReplayDuration @@ -124,7 +122,7 @@ - (void)replayForEvent:(SentryEvent *)event; - (void)newFrame:(CADisplayLink *)sender { - NSDate *now = [self dateProvider].date; + NSDate *now = _dateProvider.date; if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { [self takeScreenshot]; @@ -162,7 +160,7 @@ - (void)prepareSegmentUntil:(NSDate *)date URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; NSDate *segmentStart = - [[self dateProvider].date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; [self createAndCapture:pathToSegment duration:_replayOptions.sessionSegmentDuration diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 5f486702a4b..55debc30510 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -29,13 +29,17 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options } if (@available(iOS 16.0, tvOS 16.0, *)) { - if (options.sessionReplayOptions.replaysSessionSampleRate == 0 + BOOL shouldReplayFullSession = + [self shouldReplayFullSession:options.sessionReplayOptions.replaysSessionSampleRate]; + + if (!shouldReplayFullSession && options.sessionReplayOptions.replaysOnErrorSampleRate == 0) { return NO; } - self.sessionReplay = - [[SentrySessionReplay alloc] initWithSettings:options.sessionReplayOptions]; + self.sessionReplay = [[SentrySessionReplay alloc] + initWithSettings:options.sessionReplayOptions + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider]; [self.sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject diff --git a/Sources/Sentry/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h similarity index 78% rename from Sources/Sentry/SentrySessionReplay.h rename to Sources/Sentry/include/SentrySessionReplay.h index c0ace81dca2..a07eacbe411 100644 --- a/Sources/Sentry/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -6,13 +6,15 @@ @class SentryReplayOptions; @class SentryEvent; +@class SentryCurrentDateProvider; NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject -- (instancetype)initWithSettings:(SentryReplayOptions *)replaySettings; +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + dateProvider:(SentryCurrentDateProvider *)dateProvider; /** * Start recording the session using rootView as image source. diff --git a/Sources/Sentry/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h similarity index 100% rename from Sources/Sentry/SentrySessionReplayIntegration.h rename to Sources/Sentry/include/SentrySessionReplayIntegration.h diff --git a/Sources/Sentry/include/SentrySwift.h b/Sources/Sentry/include/SentrySwift.h index 01120ac77a8..861a37090c8 100644 --- a/Sources/Sentry/include/SentrySwift.h +++ b/Sources/Sentry/include/SentrySwift.h @@ -5,7 +5,6 @@ # if __has_include() # import # endif -# import #endif #if __has_include("Sentry-Swift.h") diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index 0489c8fe7b9..1ce95cec12b 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -562,6 +562,18 @@ class SentryFileManagerTests: XCTestCase { XCTAssertNil(sut.readAppState()) } + func testFileSize() { + let testFile = FileManager.default.temporaryDirectory.appendingPathComponent("TestFile") + + defer { + try? FileManager.default.removeItem(at: testFile) + } + + try? "123".write(to: testFile, atomically: true, encoding: .utf8) + + expect(self.sut.fileSize(testFile)) == 3 + } + func testDeletePreviousAppState() { sut.store(TestData.appState) sut.moveAppStateToPreviousAppState() diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift new file mode 100644 index 00000000000..007fe423609 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -0,0 +1,74 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) + +@available(iOS 16.0, tvOS 16.0, *) +class SentrySessionReplayIntegrationTests: XCTestCase { + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func testNoInstall() { + SentrySDK.start { + $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 + expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + } + + func testInstallFullSessionReplay() { + SentrySDK.start { + $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 0) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testNoInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.3) + + SentrySDK.start { + $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0.2, errorSampleRate: 0) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 + expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + } + + func testInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.1) + + SentrySDK.start { + $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0.2, errorSampleRate: 0) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testInstallFullErrorReplay() { + SentrySDK.start { + $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0.1) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } +} + +#endif diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 206b194e6f9..6abc11455dc 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -12,6 +12,7 @@ #if SENTRY_HAS_UIKIT # import "MockUIScene.h" # import "SentryFramesTracker+TestInit.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIApplication+Private.h" # import "SentryUIApplication.h" # import "SentryUIDeviceWrapper.h" From 55d376a4130eca3deaae1f86cc4457773715432c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 28 Mar 2024 12:06:17 +0100 Subject: [PATCH 76/88] Update SentrySessionReplayIntegrationTests.swift --- .../SentrySessionReplayIntegrationTests.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 007fe423609..cd225c29602 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -9,6 +9,12 @@ import XCTest @available(iOS 16.0, tvOS 16.0, *) class SentrySessionReplayIntegrationTests: XCTestCase { + override func setUpWithError() throws { + if #unavailable(iOS 16.0, tvOS 16.0) { + throw XCTSkip("iOS version not supported") + } + } + override func tearDown() { super.tearDown() clearTestState() @@ -60,7 +66,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { expect(SentryGlobalEventProcessor.shared().processors.count) == 1 } - func testInstallFullErrorReplay() { + func testInstallErrorReplay() { SentrySDK.start { $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0.1) $0.setIntegrations([SentrySessionReplayIntegration.self]) From a1553fbf3bd6fd39e6847fa3860e65f0540356b3 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 11:27:40 +0200 Subject: [PATCH 77/88] tests --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 4 + SentryTestUtils/TestCurrentDateProvider.swift | 4 + Sources/Sentry/SentrySessionReplay.m | 111 ++++++----- .../Sentry/SentrySessionReplayIntegration.m | 44 ++++- Sources/Sentry/include/SentrySessionReplay.h | 28 ++- .../SentrySessionReplayTests.swift | 180 ++++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 8 files changed, 314 insertions(+), 60 deletions(-) create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index d889305b442..65ad7be235a 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *) { - options.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1) + options.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1) } if #available(iOS 15.0, *) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 846c5d8503c..b6fc09e7023 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -801,6 +801,7 @@ D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */ = {isa = PBXBuildFile; fileRef = D8603DD4284F8497000E1227 /* SentryBaggage.m */; }; D8603DD8284F894C000E1227 /* SentryBaggage.h in Headers */ = {isa = PBXBuildFile; fileRef = D8603DD7284F894C000E1227 /* SentryBaggage.h */; }; D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */; }; + D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */; }; D865892F29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */; }; D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */ = {isa = PBXBuildFile; fileRef = D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */; }; D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */; }; @@ -1792,6 +1793,7 @@ D8603DD4284F8497000E1227 /* SentryBaggage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBaggage.m; sourceTree = ""; }; D8603DD7284F894C000E1227 /* SentryBaggage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryBaggage.h; path = include/SentryBaggage.h; sourceTree = ""; }; D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegrationTests.swift; sourceTree = ""; }; + D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayTests.swift; sourceTree = ""; }; D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryCrashBinaryImageCache.h; sourceTree = ""; }; D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentryCrashBinaryImageCache.c; sourceTree = ""; }; D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTrackingIntegration.h; path = include/SentryCoreDataTrackingIntegration.h; sourceTree = ""; }; @@ -3416,6 +3418,7 @@ D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */, + D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */, ); path = SessionReplay; sourceTree = ""; @@ -4678,6 +4681,7 @@ 7BB7E7C729267A28004BF96B /* EmptyIntegration.swift in Sources */, 7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */, 7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */, + D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */, 7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */, 7B6ADFCF26A02CAE0076C206 /* SentryCrashReportTests.swift in Sources */, D8B76B062808066D000A58C4 /* SentryScreenshotIntegrationTests.swift in Sources */, diff --git a/SentryTestUtils/TestCurrentDateProvider.swift b/SentryTestUtils/TestCurrentDateProvider.swift index d80436ff57d..3a62a39f63d 100644 --- a/SentryTestUtils/TestCurrentDateProvider.swift +++ b/SentryTestUtils/TestCurrentDateProvider.swift @@ -43,6 +43,10 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider { setDate(date: date().addingTimeInterval(TimeInterval(nanoseconds) / 1e9)) internalSystemTime += nanoseconds } + + public func advanceBy(interval: TimeInterval) { + setDate(date: date().addingTimeInterval(interval)) + } public var timezoneOffsetValue = 0 public override func timezoneOffset() -> Int { diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 943f832fdcc..105c7c3f578 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,42 +1,63 @@ #import "SentrySessionReplay.h" #import "SentryAttachment+Private.h" #import "SentryDependencyContainer.h" +#import "SentryDisplayLinkWrapper.h" #import "SentryFileManager.h" #import "SentryHub+Private.h" #import "SentryLog.h" +#import "SentryRandom.h" #import "SentryReplayEvent.h" #import "SentryReplayRecording.h" #import "SentrySDK+Private.h" #import "SentrySwift.h" #if SENTRY_HAS_UIKIT && !TARGET_OS_VISION -static NSString *SENTRY_REPLAY_FOLDER = @"replay"; NS_ASSUME_NONNULL_BEGIN +@interface +SentrySessionReplay () + +@property (nonatomic) BOOL isFullSession; + +@end + @implementation SentrySessionReplay { + NSURL *_urlToCache; UIView *_rootView; - BOOL _processingScreenshot; - CADisplayLink *_displayLink; NSDate *_lastScreenShot; NSDate *_videoSegmentStart; - NSURL *_urlToCache; NSDate *_sessionStart; + NSMutableArray *imageCollection; + SentryId *sessionReplayId; SentryReplayOptions *_replayOptions; SentryOnDemandReplay *_replayMaker; - SentryId *sessionReplayId; - NSMutableArray *imageCollection; - int _currentSegmentId; - BOOL _isFullSession; + SentryDisplayLinkWrapper *_displayLink; SentryCurrentDateProvider *_dateProvider; + id _sentryRandom; + id _screenshotProvider; + int _currentSegmentId; + BOOL _isRunning; + BOOL _processingScreenshot; } - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + replayFolderPath:(NSURL *)folderPath + screenshotProvider:(id)screenshotProvider + replayMaker:(id)replayMaker dateProvider:(SentryCurrentDateProvider *)dateProvider + random:(id)random + displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; { if (self = [super init]) { _replayOptions = replayOptions; _dateProvider = dateProvider; + _sentryRandom = random; + _screenshotProvider = screenshotProvider; + _displayLink = displayLinkWrapper; + _isRunning = false; + _urlToCache = folderPath; + _replayMaker = replayMaker; } return self; } @@ -48,60 +69,38 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full return; } + if (_isRunning) { + return; + } @synchronized(self) { - if (_displayLink == nil) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } else { - // Session display is already running. + if (_isRunning) { return; } - - _rootView = rootView; - _lastScreenShot = [[NSDate alloc] init]; - _videoSegmentStart = nil; - _sessionStart = _lastScreenShot; - _currentSegmentId = 0; - - NSURL *docs = [NSURL - fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; - docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; - - NSString *currentSession = [NSUUID UUID].UUIDString; - _urlToCache = [docs URLByAppendingPathComponent:currentSession]; - - if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache - withIntermediateDirectories:YES - attributes:nil - error:nil]; - } - - _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; - _replayMaker.bitRate = _replayOptions.replayBitRate; - _replayMaker.cacheMaxSize = (NSInteger)(full ? _replayOptions.sessionSegmentDuration - : _replayOptions.errorReplayDuration); - - imageCollection = [NSMutableArray array]; - - NSLog(@"Recording session to %@", _urlToCache); - - _isFullSession = full; - if (full) { - sessionReplayId = [[SentryId alloc] init]; - } + [_displayLink linkWithTarget:self selector:@selector(newFrame:)]; + _isRunning = true; } + _rootView = rootView; + _lastScreenShot = _dateProvider.date; + _videoSegmentStart = nil; + _sessionStart = _lastScreenShot; + _currentSegmentId = 0; + sessionReplayId = [[SentryId alloc] init]; + + imageCollection = [NSMutableArray array]; + _isFullSession = full; } - (void)stop { - [_displayLink invalidate]; - _displayLink = nil; + @synchronized(self) { + [_displayLink invalidate]; + _isRunning = NO; + } } - (void)replayForEvent:(SentryEvent *)event; { - if (_isFullSession || _displayLink == nil) { + if (_isFullSession || !_isRunning) { return; } @@ -109,6 +108,10 @@ - (void)replayForEvent:(SentryEvent *)event; return; } + if ([_sentryRandom nextNumber] > _replayOptions.replaysOnErrorSampleRate) { + return; + } + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; NSDate *replayStart = [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; @@ -124,7 +127,7 @@ - (void)newFrame:(CADisplayLink *)sender { NSDate *now = _dateProvider.date; - if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { + if ([now timeIntervalSinceDate:_lastScreenShot] >= 1) { [self takeScreenshot]; _lastScreenShot = now; @@ -171,10 +174,6 @@ - (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { - - if (sessionReplayId == nil) { - sessionReplayId = [[SentryId alloc] init]; - } [_replayMaker createVideoWithDuration:duration beginning:start @@ -234,7 +233,7 @@ - (void)takeScreenshot _processingScreenshot = YES; } - UIImage *screenshot = [SentryViewPhotographer.shared imageWithView:_rootView]; + UIImage *screenshot = [_screenshotProvider imageWithView:_rootView]; _processingScreenshot = NO; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 55debc30510..3c5e084348e 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,6 +1,8 @@ #import "SentrySessionReplayIntegration.h" #import "SentryClient+Private.h" #import "SentryDependencyContainer.h" +#import "SentryDisplayLinkWrapper.h" +#import "SentryFileManager.h" #import "SentryGlobalEventProcessor.h" #import "SentryHub+Private.h" #import "SentryOptions.h" @@ -14,12 +16,24 @@ NS_ASSUME_NONNULL_BEGIN +static NSString *SENTRY_REPLAY_FOLDER = @"replay"; + API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplayIntegration () @property (nonatomic, strong) SentrySessionReplay *sessionReplay; @end +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentryViewPhotographer (SentryViewScreenshotProvider) +@end + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentryOnDemandReplay (SentryReplayMaker) +@end + @implementation SentrySessionReplayIntegration - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -37,9 +51,35 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } + NSURL *docs = [NSURL + fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; + NSString *currentSession = [NSUUID UUID].UUIDString; + docs = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:docs + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + + SentryOnDemandReplay *replayMaker = + [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; + replayMaker.bitRate = options.sessionReplayOptions.replayBitRate; + replayMaker.cacheMaxSize = (NSInteger)(shouldReplayFullSession + ? options.sessionReplayOptions.sessionSegmentDuration + : options.sessionReplayOptions.errorReplayDuration); + self.sessionReplay = [[SentrySessionReplay alloc] - initWithSettings:options.sessionReplayOptions - dateProvider:SentryDependencyContainer.sharedInstance.dateProvider]; + initWithSettings:options.sessionReplayOptions + replayFolderPath:docs + screenshotProvider:SentryViewPhotographer.shared + replayMaker:replayMaker + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider + random:SentryDependencyContainer.sharedInstance.random + + displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; [self.sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index a07eacbe411..9af7c7c80cd 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -7,14 +7,40 @@ @class SentryReplayOptions; @class SentryEvent; @class SentryCurrentDateProvider; +@class SentryDisplayLinkWrapper; +@class SentryVideoInfo; + +@protocol SentryRandom; NS_ASSUME_NONNULL_BEGIN +@protocol SentryReplayMaker + +- (void)addFrameWithImage:(UIImage *)image; +- (void)releaseFramesUntil:(NSDate *)date; +- (BOOL)createVideoWithDuration:(NSTimeInterval)duration + beginning:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + error:(NSError *_Nullable *_Nullable)error + completion: + (void (^)(SentryVideoInfo *_Nullable, NSError *_Nullable))completion; + +@end + +@protocol SentryViewScreenshotProvider +- (UIImage *)imageWithView:(UIView *)view; +@end + API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions - dateProvider:(SentryCurrentDateProvider *)dateProvider; + replayFolderPath:(NSURL *)folderPath + screenshotProvider:(id)photographer + replayMaker:(id)replayMaker + dateProvider:(SentryCurrentDateProvider *)dateProvider + random:(id)random + displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; /** * Start recording the session using rootView as image source. diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift new file mode 100644 index 00000000000..daa8cfe97f8 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -0,0 +1,180 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) +@available(iOS 16.0, tvOS 16.0, *) +class SentrySessionReplayTests: XCTestCase { + + private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { + func image(with view: UIView) -> UIImage { UIImage.add } + } + + private class TestReplayMaker: NSObject, SentryReplayMaker { + struct CreateVideoCall { + var duration: TimeInterval + var beginning: Date + var outputFileURL: URL + var completion: ((Sentry.SentryVideoInfo?, (any Error)?) -> Void) + } + + var lastCallToCreateVideo: CreateVideoCall? + func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (Sentry.SentryVideoInfo?, (any Error)?) -> Void) throws { + lastCallToCreateVideo = CreateVideoCall(duration: duration, + beginning: beginning, + outputFileURL: outputFileURL, + completion: completion) + + try? "Video Data".write(to: outputFileURL, atomically: true, encoding: .utf8) + + let videoInfo = SentryVideoInfo(path: outputFileURL, height: 1_024, width: 480, duration: duration, frameCount: 5, frameRate: 1, start: beginning, end: beginning.addingTimeInterval(duration), fileSize: 10) + + completion(videoInfo, nil) + } + + var lastFrame: UIImage? + func addFrame(with image: UIImage) { + lastFrame = image + } + + var lastReleaseUntil: Date? + func releaseFrames(until date: Date) { + lastReleaseUntil = date + } + + } + + private class ReplayHub: SentryHub { + var lastEvent: SentryReplayEvent? + var lastRecording: SentryReplayRecording? + var lastVideo: URL? + + override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) { + lastEvent = replayEvent + lastRecording = replayRecording + lastVideo = videoURL + } + } + + private class Fixture { + let dateProvider = TestCurrentDateProvider() + let random = TestRandom(value: 0) + let screenshotProvider = ScreenshotProvider() + let displayLink = TestDisplayLinkWrapper() + let rootView = UIView() + let hub = ReplayHub(client: nil, andScope: nil) + let replayMaker = TestReplayMaker() + let cacheFolder = FileManager.default.temporaryDirectory + + func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, errorSampleRate: 0) ) -> SentrySessionReplay { + + return SentrySessionReplay(settings: options, + replayFolderPath: cacheFolder, + screenshotProvider: screenshotProvider, + replayMaker: replayMaker, + dateProvider: dateProvider, + random: random, + displayLinkWrapper: displayLink) + } + } + + override func setUpWithError() throws { + if #unavailable(iOS 16.0, tvOS 16.0) { + throw XCTSkip("iOS version not supported") + } + } + + override func setUp() { + super.setUp() + SentrySDK.setCurrentHub(fixture.hub) + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + private let fixture = Fixture() + + func testDontSentReplay_NoFullSession() { + let sut = fixture.getSut() + sut.start(fixture.rootView, fullSession: false) + + fixture.dateProvider.advance(by: 1) + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + expect(self.fixture.hub.lastEvent) == nil + } + + func testSentReplay_FullSession() { + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: true) + + fixture.dateProvider.advance(by: 1) + + let start = fixture.dateProvider.date() + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + guard let videoArguments = fixture.replayMaker.lastCallToCreateVideo else { + fail("Replay maker create video was not called") + return + } + + expect(videoArguments.duration) == 5 + expect(videoArguments.beginning) == start + expect(videoArguments.outputFileURL) == fixture.cacheFolder.appendingPathComponent("segments/1.000000-6.000000.mp4") + + expect(self.fixture.hub.lastRecording) != nil + expect(self.fixture.hub.lastVideo) == videoArguments.outputFileURL + assertFullSession(sut, expected: true) + } + + func testDontSentReplay_NotFullSession() { + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + fixture.dateProvider.advance(by: 1) + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + let videoArguments = fixture.replayMaker.lastCallToCreateVideo + + expect(videoArguments) == nil + assertFullSession(sut, expected: false) + } + + func testChangeReplayMode_forErrorEvent() { + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + let event = Event(error: NSError(domain: "Some error", code: 1)) + + sut.replay(for: event) + assertFullSession(sut, expected: true) + } + + func testDontChangeReplayMode_forNonErrorEvent() { + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + let event = Event(level: .info) + + sut.replay(for: event) + assertFullSession(sut, expected: false) + } + + func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { + expect(Dynamic(sessionReplay).isFullSession) == expected + } +} + +#endif diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 6abc11455dc..7a8808f2105 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -12,6 +12,7 @@ #if SENTRY_HAS_UIKIT # import "MockUIScene.h" # import "SentryFramesTracker+TestInit.h" +# import "SentrySessionReplay.h" # import "SentrySessionReplayIntegration.h" # import "SentryUIApplication+Private.h" # import "SentryUIApplication.h" From d527f76d4f7b7d72744010e094df0249de38cb98 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 13:31:03 +0200 Subject: [PATCH 78/88] Apply suggestions from code review Co-authored-by: Andrew McKnight --- Sources/Sentry/include/SentrySessionReplayIntegration.h | 2 +- .../Integrations/SessionReplay/SentryViewPhotographer.swift | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index 63458ab6df2..4500aeaa3d9 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -8,5 +8,5 @@ NS_ASSUME_NONNULL_BEGIN @interface SentrySessionReplayIntegration : SentryBaseIntegration @end -#endif +#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index 7692afc0920..da121c994fc 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -19,7 +19,7 @@ class SentryViewPhotographer: NSObject { override init() { #if os(iOS) ignoreClasses = [ UISlider.self, UISwitch.self ] -#endif +#endif // os(iOS) redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", @@ -82,8 +82,7 @@ class SentryViewPhotographer: NSObject { result.addRect(rectInWindow) return result } else if isOpaqueOrHasBackground(view) { - let newPath = excludeRect(rectInWindow, fromPath: result) - result = newPath + result = excludeRect(rectInWindow, fromPath: result) } if !ignore { From f4045c91f82463211bd4a40912547129f95ce27c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 13:36:42 +0200 Subject: [PATCH 79/88] wip --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 5 ++++- .../Integrations/SessionReplay/SentryPixelBuffer.swift | 6 +++--- .../SessionReplay/SentrySessionReplayTests.swift | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 6f23eaab0ef..6c2057d73ab 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -125,12 +125,13 @@ class SentryOnDemandReplay: NSObject { guard let self = self else { return } let imagePath = frames[frameCount] + if let image = UIImage(contentsOfFile: imagePath) { let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) - frameCount += 1 if self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) != true { completion(nil, videoWriter.error) + videoWriterInput.markAsFinished() } } @@ -146,6 +147,8 @@ class SentryOnDemandReplay: NSObject { completion(videoInfo, videoWriter.error) } } + + frameCount += 1 } } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift index 3c9de36f0d7..264e2b5c056 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -1,11 +1,11 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) import AVFoundation import CoreGraphics import Foundation import UIKit -#if os(iOS) || os(tvOS) class SentryPixelBuffer { private var pixelBuffer: CVPixelBuffer? private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() @@ -45,5 +45,5 @@ class SentryPixelBuffer { return pixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime) } } -#endif -#endif // canImport(UIKit) +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index daa8cfe97f8..92d73064e5a 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -69,7 +69,6 @@ class SentrySessionReplayTests: XCTestCase { let cacheFolder = FileManager.default.temporaryDirectory func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, errorSampleRate: 0) ) -> SentrySessionReplay { - return SentrySessionReplay(settings: options, replayFolderPath: cacheFolder, screenshotProvider: screenshotProvider, From d1e50bef777694b80f92f35c27ff1ee7d641a168 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 14:38:30 +0200 Subject: [PATCH 80/88] ref --- Sources/Sentry/SentrySessionReplay.m | 2 +- .../Sentry/SentrySessionReplayIntegration.m | 25 ++++----- Sources/Sentry/include/SentrySessionReplay.h | 5 +- .../SessionReplay/SentryOnDemandReplay.swift | 54 +++++++++++++------ .../SentryViewPhotographer.swift | 32 +++++------ .../Helper/SentryFileManagerTests.swift | 4 +- 6 files changed, 73 insertions(+), 49 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 105c7c3f578..e49c3ebed6c 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -98,7 +98,7 @@ - (void)stop } } -- (void)replayForEvent:(SentryEvent *)event; +- (void)captureReplayForEvent:(SentryEvent *)event; { if (_isFullSession || !_isRunning) { return; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 3c5e084348e..147c11c3175 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,17 +1,18 @@ #import "SentrySessionReplayIntegration.h" -#import "SentryClient+Private.h" -#import "SentryDependencyContainer.h" -#import "SentryDisplayLinkWrapper.h" -#import "SentryFileManager.h" -#import "SentryGlobalEventProcessor.h" -#import "SentryHub+Private.h" -#import "SentryOptions.h" -#import "SentryRandom.h" -#import "SentrySDK+Private.h" -#import "SentrySessionReplay.h" -#import "SentrySwift.h" #if SENTRY_HAS_UIKIT && !TARGET_OS_VISION + +# import "SentryClient+Private.h" +# import "SentryDependencyContainer.h" +# import "SentryDisplayLinkWrapper.h" +# import "SentryFileManager.h" +# import "SentryGlobalEventProcessor.h" +# import "SentryHub+Private.h" +# import "SentryOptions.h" +# import "SentryRandom.h" +# import "SentrySDK+Private.h" +# import "SentrySessionReplay.h" +# import "SentrySwift.h" # import "SentryUIApplication.h" NS_ASSUME_NONNULL_BEGIN @@ -93,7 +94,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - [self.sessionReplay replayForEvent:event]; + [self.sessionReplay captureReplayForEvent:event]; return event; }]; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 9af7c7c80cd..705902b2779 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -53,7 +53,10 @@ API_AVAILABLE(ios(16.0), tvos(16.0)) */ - (void)stop; -- (void)replayForEvent:(SentryEvent *)event; +/** + * Captures a replay for given event. + */ +- (void)captureReplayForEvent:(SentryEvent *)event; @end diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 6c2057d73ab..9b22eea3189 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -17,6 +17,10 @@ struct SentryReplayFrame { } } +enum SentryOnDemandReplayError: Error { + case cantReadVideoSize +} + @available(iOS 16.0, tvOS 16.0, *) @objcMembers class SentryOnDemandReplay: NSObject { @@ -51,7 +55,12 @@ class SentryOnDemandReplay: NSObject { let date = Date() let interval = date.timeIntervalSince(_starttime) let imagePath = (_outputPath as NSString).appendingPathComponent("\(interval).png") - try? data.write(to: URL(fileURLWithPath: imagePath)) + do { + try data.write(to: URL(fileURLWithPath: imagePath)) + } catch { + print("[SentryOnDemandReplay] Could not save replay frame. Error: \(error)") + return + } _frames.append(SentryReplayFrame(imagePath: imagePath, time: date)) while _frames.count > cacheMaxSize { @@ -102,20 +111,8 @@ class SentryOnDemandReplay: NSObject { videoWriter.startWriting() videoWriter.startSession(atSourceTime: .zero) - let end = beginning.addingTimeInterval(duration) var frameCount = 0 - var frames = [String]() - - var start = Date() - var actualEnd = Date() - - for frame in _frames { - if frame.time < beginning { continue } else if frame.time > end { break } - if frame.time < start { start = frame.time } - - actualEnd = frame.time - frames.append(frame.imagePath) - } + let (frames, start, end) = filterFrames(beginning: beginning, end: beginning.addingTimeInterval(duration)) if frames.isEmpty { return } @@ -140,9 +137,16 @@ class SentryOnDemandReplay: NSObject { videoWriter.finishWriting { var videoInfo: SentryVideoInfo? if videoWriter.status == .completed { - let fileAttributes = try? FileManager.default.attributesOfItem(atPath: outputFileURL.path) - let fileSize = fileAttributes?[FileAttributeKey.size] as? Int ?? -1 - videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: actualEnd, fileSize: fileSize) + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path) + guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else { + completion(nil, SentryOnDemandReplayError.cantReadVideoSize) + return + } + videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: end, fileSize: fileSize) + } catch { + completion(nil, error) + } } completion(videoInfo, videoWriter.error) } @@ -152,6 +156,22 @@ class SentryOnDemandReplay: NSObject { } } + private func filterFrames(beginning: Date, end: Date) -> ([String], firstFrame: Date, lastFrame: Date) { + var frames = [String]() + + var start = Date() + var actualEnd = Date() + + for frame in _frames { + if frame.time < beginning { continue } else if frame.time > end { break } + if frame.time < start { start = frame.time } + + actualEnd = frame.time + frames.append(frame.imagePath) + } + return (frames, start, actualEnd) + } + private func createVideoSettings() -> [String: Any] { return [ AVVideoCodecKey: AVVideoCodecType.h264, diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift index da121c994fc..09d27f9ad40 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift @@ -11,7 +11,9 @@ import UIKit @objcMembers class SentryViewPhotographer: NSObject { + //This is a list of UIView subclasses that will be ignored during redact process private var ignoreClasses: [AnyClass] = [] + //This is a list of UIView subclasses that need to be redacted from screenshot private var redactClasses: [AnyClass] = [] static let shared = SentryViewPhotographer() @@ -29,16 +31,18 @@ class SentryViewPhotographer: NSObject { func image(view: UIView) -> UIImage? { UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) - if let currentContext = UIGraphicsGetCurrentContext() { - view.layer.render(in: currentContext) - self.mask(view: view, context: currentContext) - if let screenshot = UIGraphicsGetImageFromCurrentImageContext() { - UIGraphicsEndImageContext() - return screenshot - } + + defer { + UIGraphicsEndImageContext() } - UIGraphicsEndImageContext() - return nil + + guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } + + view.layer.render(in: currentContext) + self.mask(view: view, context: currentContext) + + guard let screenshot = UIGraphicsGetImageFromCurrentImageContext() else { return nil } + return screenshot } private func mask(view: UIView, context: CGContext) { @@ -82,7 +86,7 @@ class SentryViewPhotographer: NSObject { result.addRect(rectInWindow) return result } else if isOpaqueOrHasBackground(view) { - result = excludeRect(rectInWindow, fromPath: result) + result = SentryCoreGraphicsHelper.excludeRect(rectInWindow, from: result).takeRetainedValue() } if !ignore { @@ -94,14 +98,10 @@ class SentryViewPhotographer: NSObject { return result } - private func excludeRect(_ rectangle: CGRect, fromPath path: CGMutablePath) -> CGMutablePath { - return SentryCoreGraphicsHelper.excludeRect(rectangle, from: path).takeRetainedValue() - } - private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) } } -#endif -#endif // canImport(UIKit) +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index 1ce95cec12b..f13482c8e3f 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -562,14 +562,14 @@ class SentryFileManagerTests: XCTestCase { XCTAssertNil(sut.readAppState()) } - func testFileSize() { + func testFileSize() throws { let testFile = FileManager.default.temporaryDirectory.appendingPathComponent("TestFile") defer { try? FileManager.default.removeItem(at: testFile) } - try? "123".write(to: testFile, atomically: true, encoding: .utf8) + try "123".write(to: testFile, atomically: true, encoding: .utf8) expect(self.sut.fileSize(testFile)) == 3 } From 8c638c7360c4554b6a149d6b556aa9c378bcfd59 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 14:43:11 +0200 Subject: [PATCH 81/88] Revert "Update Sentry.podspec" This reverts commit 3252a83a03a5303d0e56417c4b6bd22e9cf44d38. --- Sentry.podspec | 91 ++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/Sentry.podspec b/Sentry.podspec index 8d98a9143c4..ab0d7a8b9f5 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,53 +1,50 @@ Pod::Spec.new do |s| - s.name = "Sentry" - s.version = "8.21.0" - s.summary = "Sentry client for cocoa" - s.homepage = "https://github.com/getsentry/sentry-cocoa" - s.license = "mit" - s.authors = "Sentry" - s.source = { :git => "https://github.com/getsentry/sentry-cocoa.git", - :tag => s.version.to_s } - - s.ios.deployment_target = "11.0" - s.osx.deployment_target = "10.13" - s.tvos.deployment_target = "11.0" - s.watchos.deployment_target = "4.0" - s.visionos.deployment_target = "1.0" - s.module_name = "Sentry" - s.requires_arc = true - s.frameworks = 'Foundation' - s.swift_versions = "5.5" - s.pod_target_xcconfig = { - 'GCC_ENABLE_CPP_EXCEPTIONS' => 'YES', - 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++14', - 'CLANG_CXX_LIBRARY' => 'libc++', - 'APPLICATION_EXTENSION_API_ONLY' => 'YES', - 'SWIFT_INCLUDE_PATHS' => '${PODS_TARGET_SRCROOT}/Sources/Sentry/include' - } - s.watchos.pod_target_xcconfig = { - 'OTHER_LDFLAGS' => '$(inherited) -framework WatchKit' - } - - s.default_subspecs = ['Core'] - - s.subspec 'Core' do |sp| - sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", - "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" - - sp.preserve_path = "Sources/Sentry/include/module.modulemap" - sp.public_header_files = + s.name = "Sentry" + s.version = "8.21.0" + s.summary = "Sentry client for cocoa" + s.homepage = "https://github.com/getsentry/sentry-cocoa" + s.license = "mit" + s.authors = "Sentry" + s.source = { :git => "https://github.com/getsentry/sentry-cocoa.git", + :tag => s.version.to_s } + + s.ios.deployment_target = "11.0" + s.osx.deployment_target = "10.13" + s.tvos.deployment_target = "11.0" + s.watchos.deployment_target = "4.0" + s.visionos.deployment_target = "1.0" + s.module_name = "Sentry" + s.requires_arc = true + s.frameworks = 'Foundation' + s.swift_versions = "5.5" + s.pod_target_xcconfig = { + 'GCC_ENABLE_CPP_EXCEPTIONS' => 'YES', + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++14', + 'CLANG_CXX_LIBRARY' => 'libc++', + 'APPLICATION_EXTENSION_API_ONLY' => 'YES', + 'SWIFT_INCLUDE_PATHS' => '${PODS_TARGET_SRCROOT}/Sources/Sentry/include' + } + s.watchos.pod_target_xcconfig = { + 'OTHER_LDFLAGS' => '$(inherited) -framework WatchKit' + } + + s.default_subspecs = ['Core'] + + s.subspec 'Core' do |sp| + sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", + "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}", "Sources/Sentry/include/module.modulemap" + sp.public_header_files = "Sources/Sentry/Public/*.h" - sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } - end - - s.subspec 'HybridSDK' do |sp| - sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", + sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } + end + + s.subspec 'HybridSDK' do |sp| + sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" - sp.preserve_path = "Sources/Sentry/include/module.modulemap" - sp.public_header_files = + sp.public_header_files = "Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h" - - sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } - end + + sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } + end end From da03a6928cc16f0d5951e22d3439847d267e643d Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 14:46:26 +0200 Subject: [PATCH 82/88] Update Sentry.podspec --- Sentry.podspec | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sentry.podspec b/Sentry.podspec index ab0d7a8b9f5..afea218dc1e 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -32,7 +32,8 @@ Pod::Spec.new do |s| s.subspec 'Core' do |sp| sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", - "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}", "Sources/Sentry/include/module.modulemap" + "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" + sp.preserve_path = "Sources/Sentry/include/module.modulemap" sp.public_header_files = "Sources/Sentry/Public/*.h" sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" } @@ -41,7 +42,8 @@ Pod::Spec.new do |s| s.subspec 'HybridSDK' do |sp| sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" - + + sp.preserve_path = "Sources/Sentry/include/module.modulemap" sp.public_header_files = "Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h" From f36b11d154dd50da32c5b973668286c416474b60 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 15:01:44 +0200 Subject: [PATCH 83/88] remove file size --- Sources/Sentry/SentryFileManager.m | 8 -------- Sources/Sentry/include/SentryFileManager.h | 2 -- .../SentryTests/Helper/SentryFileManagerTests.swift | 12 ------------ .../SessionReplay/SentrySessionReplayTests.swift | 4 ++-- 4 files changed, 2 insertions(+), 24 deletions(-) diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index c78e7394959..363b4b6b6d7 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -728,14 +728,6 @@ - (void)createPathsWithOptions:(SentryOptions *)options self.envelopesPath = [self.sentryPath stringByAppendingPathComponent:EnvelopesPathComponent]; } -- (NSInteger)fileSize:(NSURL *)path -{ - NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path.path - error:nil]; - NSNumber *fileSize = [fileAttributes objectForKey:NSFileSize] ?: @(-1); - return [fileSize integerValue]; -} - #if SENTRY_TARGET_PROFILING_SUPPORTED /** * @note This method must be statically accessible because it will be called during app launch, diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index 3cc54aceddb..cb774591c79 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -61,8 +61,6 @@ SENTRY_NO_INIT - (void)deleteOldEnvelopeItems; -- (NSInteger)fileSize:(NSURL *)path; - /** * Only used for testing. */ diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index f13482c8e3f..0489c8fe7b9 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -562,18 +562,6 @@ class SentryFileManagerTests: XCTestCase { XCTAssertNil(sut.readAppState()) } - func testFileSize() throws { - let testFile = FileManager.default.temporaryDirectory.appendingPathComponent("TestFile") - - defer { - try? FileManager.default.removeItem(at: testFile) - } - - try "123".write(to: testFile, atomically: true, encoding: .utf8) - - expect(self.sut.fileSize(testFile)) == 3 - } - func testDeletePreviousAppState() { sut.store(TestData.appState) sut.moveAppStateToPreviousAppState() diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 92d73064e5a..055a7a74240 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -157,7 +157,7 @@ class SentrySessionReplayTests: XCTestCase { let event = Event(error: NSError(domain: "Some error", code: 1)) - sut.replay(for: event) + sut.capture(for: event) assertFullSession(sut, expected: true) } @@ -167,7 +167,7 @@ class SentrySessionReplayTests: XCTestCase { let event = Event(level: .info) - sut.replay(for: event) + sut.capture(for: event) assertFullSession(sut, expected: false) } From c2d264f93b91bbfd4bf1ed4c8bcd8e52861961c0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 15:06:22 +0200 Subject: [PATCH 84/88] Apply suggestions from code review Co-authored-by: Andrew McKnight --- Sources/Sentry/SentryCoreGraphicsHelper.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m index 5da4722ba9b..56bb3816299 100644 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -10,9 +10,9 @@ + (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)pat CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); } -# endif -# endif +# endif // defined(__IPHONE_16_0) +# endif // (TARGET_OS_IOS || TARGET_OS_TV) return path; } @end -#endif +#endif // SENTRY_HAS_UIKIT From 98fec3b68fcd77d53fbb611287f809f5e116e0a2 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 15:16:20 +0200 Subject: [PATCH 85/88] NoUI config --- Sentry.xcodeproj/project.pbxproj | 12 ++++-------- Sources/Configuration/SentryNoUI.xcconfig | 3 +++ 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 Sources/Configuration/SentryNoUI.xcconfig diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index b6fc09e7023..285782c1291 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1781,6 +1781,7 @@ D855B3E727D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackingIntegrationTest.swift; sourceTree = ""; }; D855B3E927D652C700BCED76 /* TestCoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCoreDataStack.swift; sourceTree = ""; }; D856272B2A374A8600FB8062 /* UrlSanitized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitized.swift; sourceTree = ""; }; + D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SentryNoUI.xcconfig; sourceTree = ""; }; D85790282976A69F00C6AC1F /* TestDebugImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDebugImageProvider.swift; sourceTree = ""; }; D85852B427ECEEDA00C6D8AE /* SentryScreenshot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshot.m; sourceTree = ""; }; D85852B827EDDC5900C6D8AE /* SentryUIApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUIApplication.m; sourceTree = ""; }; @@ -2390,6 +2391,7 @@ 84B7FA4729B2995A00AD93B1 /* DeploymentTargets.xcconfig */, 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */, D8199DCF29376FF40074249E /* SentrySwiftUI.xcconfig */, + D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */, ); path = Configuration; sourceTree = ""; @@ -4951,7 +4953,6 @@ LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4994,7 +4995,6 @@ ); ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5173,7 +5173,6 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5281,7 +5280,7 @@ }; 841C60C42A69DE6B00E1C00F /* Debug_without_UIKit */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */; + baseConfigurationReference = D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -5315,7 +5314,6 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = "-DSENTRY_NO_UIKIT"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5775,7 +5773,7 @@ }; 8483D06B2AC7627800143615 /* Release_without_UIKit */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */; + baseConfigurationReference = D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -5805,7 +5803,6 @@ ); ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = "-DSENTRY_NO_UIKIT"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6046,7 +6043,6 @@ ); ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-DCARTHAGE_$(CARTHAGE)"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry; PRODUCT_NAME = Sentry; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Sources/Configuration/SentryNoUI.xcconfig b/Sources/Configuration/SentryNoUI.xcconfig new file mode 100644 index 00000000000..b6261431f16 --- /dev/null +++ b/Sources/Configuration/SentryNoUI.xcconfig @@ -0,0 +1,3 @@ +#include "Sentry.xcconfig" + +OTHER_SWIFT_FLAGS = -DSENTRY_NO_UIKIT From 55430548a05648dc7f67c8a5335a42c14b5dce73 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 2 Apr 2024 15:22:02 +0200 Subject: [PATCH 86/88] Update SentryReplayOptions.swift --- .../Integrations/SessionReplay/SentryReplayOptions.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 218b7cd0c01..2cf51c8d158 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -57,11 +57,10 @@ public class SentryReplayOptions: NSObject { } /** - * Inittialize session replay options - * - * sessionSampleRate Indicates the percentage in which the replay for the session will be - * created. - * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with + * Initialize session replay options + * - parameters: + * - sessionSampleRate Indicates the percentage in which the replay for the session will be created. + * - errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with * error events. */ public init(sessionSampleRate: Float, errorSampleRate: Float) { From 1a1405f0ae7536bba1182e9409adb7303b94ced0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 4 Apr 2024 15:19:10 +0200 Subject: [PATCH 87/88] Update SentryOnDemandReplay.swift --- .../SessionReplay/SentryOnDemandReplay.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 9b22eea3189..2c77b97b43b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -121,18 +121,18 @@ class SentryOnDemandReplay: NSObject { videoWriterInput.requestMediaDataWhenReady(on: _onDemandDispatchQueue) { [weak self] in guard let self = self else { return } - let imagePath = frames[frameCount] - - if let image = UIImage(contentsOfFile: imagePath) { - let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) - - if self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) != true { - completion(nil, videoWriter.error) - videoWriterInput.markAsFinished() + if frameCount < frames.count { + let imagePath = frames[frameCount] + if let image = UIImage(contentsOfFile: imagePath) { + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) + + if self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) != true { + completion(nil, videoWriter.error) + videoWriterInput.markAsFinished() + } } - } - - if frameCount >= frames.count { + frameCount += 1 + } else { videoWriterInput.markAsFinished() videoWriter.finishWriting { var videoInfo: SentryVideoInfo? @@ -151,8 +151,6 @@ class SentryOnDemandReplay: NSObject { completion(videoInfo, videoWriter.error) } } - - frameCount += 1 } } From 1f8f703ff6c7e3c92a696b61d42bfd76b357afa6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 10 Apr 2024 14:50:05 +0200 Subject: [PATCH 88/88] feat(Session Replay): Experimental Options (#3816) Relates to: [Epic] Mobile Replay: Private Alpha Release sentry#63255 Added and experimental sub option to SentryOption to expose experimental features --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 10 ++- Sources/Sentry/Public/SentryOptions.h | 15 +++-- Sources/Sentry/SentryBaseIntegration.m | 4 +- Sources/Sentry/SentryEnvelope.m | 5 ++ Sources/Sentry/SentryOptions.m | 16 +++-- Sources/Sentry/SentrySessionReplay.m | 49 +++++++++++---- .../Sentry/SentrySessionReplayIntegration.m | 20 +++--- Sources/Sentry/include/SentrySessionReplay.h | 3 +- .../SessionReplay/SentryOnDemandReplay.swift | 7 ++- .../SessionReplay/SentryReplayOptions.swift | 63 +++++++++++++------ .../Swift/Protocol/SentryRedactOptions.swift | 7 +++ Sources/Swift/SentryExperimentalOptions.swift | 18 ++++++ .../SentryViewPhotographer.swift | 36 ++++++----- .../SentrySessionReplayIntegrationTests.swift | 37 +++++------ .../SentrySessionReplayTests.swift | 55 ++++++++++++---- Tests/SentryTests/SentryOptionsTest.m | 17 +++-- 17 files changed, 242 insertions(+), 122 deletions(-) create mode 100644 Sources/Swift/Protocol/SentryRedactOptions.swift create mode 100644 Sources/Swift/SentryExperimentalOptions.swift rename Sources/Swift/{Integrations/SessionReplay => Tools}/SentryViewPhotographer.swift (72%) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 65ad7be235a..0f706a8d055 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *) { - options.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true) } if #available(iOS 15.0, *) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 285782c1291..771304d6804 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -813,6 +813,8 @@ D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; + D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; + D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */; }; D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */; }; @@ -1808,6 +1810,8 @@ D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; + D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactOptions.swift; sourceTree = ""; }; + D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExperimentalOptions.swift; sourceTree = ""; }; D880E3A628573E87008A90DB /* SentryBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageTests.swift; sourceTree = ""; }; D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = ""; }; D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerTest.swift; sourceTree = ""; }; @@ -3409,6 +3413,7 @@ D8F016B12B9622B7007B9AFB /* Protocol */, D856272A2A374A6800FB8062 /* Tools */, D800942628F82F3A005D3943 /* SwiftDescriptor.swift */, + D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, ); path = Swift; @@ -3522,6 +3527,7 @@ children = ( D856272B2A374A8600FB8062 /* UrlSanitized.swift */, D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */, + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, ); path = Tools; sourceTree = ""; @@ -3599,7 +3605,6 @@ children = ( D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, - D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, ); @@ -3619,6 +3624,7 @@ children = ( D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, + D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */, ); path = Protocol; sourceTree = ""; @@ -4213,6 +4219,7 @@ 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, + D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */, 631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */, 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, @@ -4284,6 +4291,7 @@ 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, 630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */, 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */, + D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */, 7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */, 639FCFA11EBC804600778193 /* SentryException.m in Sources */, D80CD8D42B75144B002F710B /* SwiftDescriptor.swift in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 7d2e6c8a328..c410475cfb3 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, SentryReplayOptions; +@class SentryExperimentalOptions; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -271,14 +272,6 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing; -/** - * @warning This is an experimental feature and may still have bugs. - * Settings to configure the session replay. - * @node Default value is @c nil . - */ -@property (nonatomic, strong) - SentryReplayOptions *sessionReplayOptions API_AVAILABLE(ios(16.0), tvos(16.0)); - #endif // SENTRY_UIKIT_AVAILABLE /** @@ -567,6 +560,12 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, copy) NSString *spotlightUrl; +/** + * This will agreggate options for all experimental features. + * Be aware that the options available for experimental can change at any time. + */ +@property (nonatomic, readonly) SentryExperimentalOptions *experimental; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index dee148cf0bd..e1fc0b7ad81 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -144,8 +144,8 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options if (integrationOptions & kIntegrationOptionEnableReplay) { if (@available(iOS 16.0, tvOS 16.0, *)) { - if (options.sessionReplayOptions.replaysOnErrorSampleRate == 0 - && options.sessionReplayOptions.replaysSessionSampleRate == 0) { + if (options.experimental.sessionReplay.errorSampleRate == 0 + && options.experimental.sessionReplay.sessionSampleRate == 0) { [self logWithOptionName:@"sessionReplaySettings"]; return NO; } diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 52e13dc81a5..e98ba01ee44 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -227,6 +227,11 @@ - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent } NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl]; + + NSError *error; + if (![NSFileManager.defaultManager removeItemAtURL:envelopeContentUrl error:&error]) { + SENTRY_LOG_ERROR(@"Cound not delete temporary replay content from disk: %@", error); + } return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeReplayVideo length:envelopeItemContent.length] diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 59e7ba8fe53..151e6a981dc 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -16,6 +16,7 @@ #import "SentrySDK.h" #import "SentryScope.h" #import "SentrySessionReplayIntegration.h" +#import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" #import @@ -105,7 +106,7 @@ - (instancetype)init self.enableTimeToFullDisplayTracing = NO; self.initialScope = ^SentryScope *(SentryScope *scope) { return scope; }; - + _experimental = [[SentryExperimentalOptions alloc] init]; _enableTracing = NO; _enableTracingManual = NO; #if SENTRY_HAS_UIKIT @@ -403,13 +404,6 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; - if (@available(iOS 16.0, tvOS 16.0, *)) { - if ([options[@"sessionReplayOptions"] isKindOfClass:NSDictionary.class]) { - self.sessionReplayOptions = - [[SentryReplayOptions alloc] initWithDictionary:options[@"sessionReplayOptions"]]; - } - } - #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] @@ -505,6 +499,10 @@ - (BOOL)validateOptions:(NSDictionary *)options self.spotlightUrl = options[@"spotlightUrl"]; } + if ([options[@"experimental"] isKindOfClass:NSDictionary.class]) { + [self.experimental validateOptions:options[@"experimental"]]; + } + return YES; } @@ -745,4 +743,4 @@ - (NSString *)debugDescription } #endif // defined(DEBUG) || defined(TEST) || defined(TESTCI) -@end +@end \ No newline at end of file diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index e49c3ebed6c..d6830ed4f19 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -18,6 +18,8 @@ @interface SentrySessionReplay () +@property (nonatomic) BOOL isRunning; + @property (nonatomic) BOOL isFullSession; @end @@ -37,8 +39,8 @@ @implementation SentrySessionReplay { id _sentryRandom; id _screenshotProvider; int _currentSegmentId; - BOOL _isRunning; BOOL _processingScreenshot; + BOOL _reachedMaximumDuration; } - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions @@ -55,9 +57,10 @@ - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions _sentryRandom = random; _screenshotProvider = screenshotProvider; _displayLink = displayLinkWrapper; - _isRunning = false; + _isRunning = NO; _urlToCache = folderPath; _replayMaker = replayMaker; + _reachedMaximumDuration = NO; } return self; } @@ -72,22 +75,31 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full if (_isRunning) { return; } + @synchronized(self) { if (_isRunning) { return; } [_displayLink linkWithTarget:self selector:@selector(newFrame:)]; - _isRunning = true; + _isRunning = YES; } + _rootView = rootView; _lastScreenShot = _dateProvider.date; _videoSegmentStart = nil; - _sessionStart = _lastScreenShot; _currentSegmentId = 0; sessionReplayId = [[SentryId alloc] init]; imageCollection = [NSMutableArray array]; - _isFullSession = full; + if (full) { + [self startFullReplay]; + } +} + +- (void)startFullReplay +{ + _sessionStart = _lastScreenShot; + _isFullSession = YES; } - (void)stop @@ -108,7 +120,7 @@ - (void)captureReplayForEvent:(SentryEvent *)event; return; } - if ([_sentryRandom nextNumber] > _replayOptions.replaysOnErrorSampleRate) { + if ([_sentryRandom nextNumber] > _replayOptions.errorSampleRate) { return; } @@ -120,13 +132,25 @@ - (void)captureReplayForEvent:(SentryEvent *)event; duration:_replayOptions.errorReplayDuration startedAt:replayStart]; - self->_isFullSession = YES; + [self startFullReplay]; } - (void)newFrame:(CADisplayLink *)sender { + if (!_isRunning) { + return; + } + NSDate *now = _dateProvider.date; + if (_isFullSession && + [now timeIntervalSinceDate:_sessionStart] > _replayOptions.maximumDuration) { + _reachedMaximumDuration = YES; + [self prepareSegmentUntil:now]; + [self stop]; + return; + } + if ([now timeIntervalSinceDate:_lastScreenShot] >= 1) { [self takeScreenshot]; _lastScreenShot = now; @@ -143,8 +167,6 @@ - (void)newFrame:(CADisplayLink *)sender - (void)prepareSegmentUntil:(NSDate *)date { - NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; - NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { @@ -160,7 +182,7 @@ - (void)prepareSegmentUntil:(NSDate *)date } pathToSegment = [pathToSegment - URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + URLByAppendingPathComponent:[NSString stringWithFormat:@"%i.mp4", _currentSegmentId]]; NSDate *segmentStart = [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; @@ -219,6 +241,11 @@ - (void)captureSegment:(NSInteger)segment [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:videoInfo.path]; + + NSError *error; + if (![NSFileManager.defaultManager removeItemAtURL:videoInfo.path error:&error]) { + SENTRY_LOG_ERROR(@"Cound not delete replay segment from disk: %@", error); + } } - (void)takeScreenshot @@ -233,7 +260,7 @@ - (void)takeScreenshot _processingScreenshot = YES; } - UIImage *screenshot = [_screenshotProvider imageWithView:_rootView]; + UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions]; _processingScreenshot = NO; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 147c11c3175..3664c37e058 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -44,11 +44,12 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options } if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryReplayOptions *replayOptions = options.experimental.sessionReplay; + BOOL shouldReplayFullSession = - [self shouldReplayFullSession:options.sessionReplayOptions.replaysSessionSampleRate]; + [self shouldReplayFullSession:replayOptions.sessionSampleRate]; - if (!shouldReplayFullSession - && options.sessionReplayOptions.replaysOnErrorSampleRate == 0) { + if (!shouldReplayFullSession && replayOptions.errorSampleRate == 0) { return NO; } @@ -67,13 +68,13 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; - replayMaker.bitRate = options.sessionReplayOptions.replayBitRate; - replayMaker.cacheMaxSize = (NSInteger)(shouldReplayFullSession - ? options.sessionReplayOptions.sessionSegmentDuration - : options.sessionReplayOptions.errorReplayDuration); + replayMaker.bitRate = replayOptions.replayBitRate; + replayMaker.cacheMaxSize + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + : replayOptions.errorReplayDuration); self.sessionReplay = [[SentrySessionReplay alloc] - initWithSettings:options.sessionReplayOptions + initWithSettings:replayOptions replayFolderPath:docs screenshotProvider:SentryViewPhotographer.shared replayMaker:replayMaker @@ -84,8 +85,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [self.sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplayOptions - .replaysSessionSampleRate]]; + fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 705902b2779..953f11fdf81 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -11,6 +11,7 @@ @class SentryVideoInfo; @protocol SentryRandom; +@protocol SentryRedactOptions; NS_ASSUME_NONNULL_BEGIN @@ -28,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN @end @protocol SentryViewScreenshotProvider -- (UIImage *)imageWithView:(UIView *)view; +- (UIImage *)imageWithView:(UIView *)view options:(id)options; @end API_AVAILABLE(ios(16.0), tvos(16.0)) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 2c77b97b43b..8c069b3c540 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -123,13 +123,14 @@ class SentryOnDemandReplay: NSObject { if frameCount < frames.count { let imagePath = frames[frameCount] + if let image = UIImage(contentsOfFile: imagePath) { let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) - - if self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) != true { + guard self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) == true else { completion(nil, videoWriter.error) videoWriterInput.markAsFinished() - } + return + } } frameCount += 1 } else { diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 2cf51c8d158..ce0f3fcfb32 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -1,25 +1,41 @@ import Foundation @objcMembers -public class SentryReplayOptions: NSObject { +public class SentryReplayOptions: NSObject, SentryRedactOptions { /** * Indicates the percentage in which the replay for the session will be created. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * - Specifying @c 0 means never, @c 1.0 means always. + * - note: The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it * to the default. - * @note The default is @c 0. + * - note: The default is 0. */ - public let replaysSessionSampleRate: Float + public var sessionSampleRate: Float /** * Indicates the percentage in which a 30 seconds replay will be send with error events. - * @discussion Specifying @c 0 means never, @c 1.0 means always. - * @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * - Specifying 0 means never, 1.0 means always. + * - note: The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it * to the default. - * @note The default is @c 0. + * - note: The default is 0. */ - public let replaysOnErrorSampleRate: Float - + public var errorSampleRate: Float + + /** + * Indicates whether session replay should redact all text in the app + * by drawing a black rectangle over it. + * + * - note: The default is true + */ + public var redactAllText = true + + /** + * Indicates whether session replay should redact all non-bundled image + * in the app by drawing a black rectangle over it. + * + * - note: The default is true + */ + public var redactAllImages = true + /** * Defines the quality of the session replay. * Higher bit rates better quality, but also bigger files to transfer. @@ -48,12 +64,17 @@ public class SentryReplayOptions: NSObject { */ let sessionSegmentDuration = TimeInterval(5) + /** + * The maximum duration of a replay session. + */ + let maximumDuration = TimeInterval(3_600) + /** * Inittialize session replay options disabled */ public override init() { - self.replaysSessionSampleRate = 0 - self.replaysOnErrorSampleRate = 0 + self.sessionSampleRate = 0 + self.errorSampleRate = 0 } /** @@ -63,14 +84,18 @@ public class SentryReplayOptions: NSObject { * - errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with * error events. */ - public init(sessionSampleRate: Float, errorSampleRate: Float) { - self.replaysSessionSampleRate = sessionSampleRate - self.replaysOnErrorSampleRate = errorSampleRate + public init(sessionSampleRate: Float = 0, errorSampleRate: Float = 0, redactAllText: Bool = true, redactAllImages: Bool = true) { + self.sessionSampleRate = sessionSampleRate + self.errorSampleRate = errorSampleRate + self.redactAllText = redactAllText + self.redactAllImages = redactAllImages } - convenience init(dictionary: NSDictionary) { - let sessionSampleRate = (dictionary["replaysSessionSampleRate"] as? NSNumber)?.floatValue ?? 0 - let onErrorSampleRate = (dictionary["replaysOnErrorSampleRate"] as? NSNumber)?.floatValue ?? 0 - self.init(sessionSampleRate: sessionSampleRate, errorSampleRate: onErrorSampleRate) + convenience init(dictionary: [String: Any]) { + let sessionSampleRate = (dictionary["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0 + let onErrorSampleRate = (dictionary["errorSampleRate"] as? NSNumber)?.floatValue ?? 0 + let redactAllText = (dictionary["redactAllText"] as? NSNumber)?.boolValue ?? true + let redactAllImages = (dictionary["redactAllImages"] as? NSNumber)?.boolValue ?? true + self.init(sessionSampleRate: sessionSampleRate, errorSampleRate: onErrorSampleRate, redactAllText: redactAllText, redactAllImages: redactAllImages) } } diff --git a/Sources/Swift/Protocol/SentryRedactOptions.swift b/Sources/Swift/Protocol/SentryRedactOptions.swift new file mode 100644 index 00000000000..cdd38e819a1 --- /dev/null +++ b/Sources/Swift/Protocol/SentryRedactOptions.swift @@ -0,0 +1,7 @@ +import Foundation + +@objc +protocol SentryRedactOptions { + var redactAllText: Bool { get } + var redactAllImages: Bool { get } +} diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift new file mode 100644 index 00000000000..9cf1a1947fd --- /dev/null +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -0,0 +1,18 @@ +@objcMembers +public class SentryExperimentalOptions: NSObject { + #if canImport(UIKit) + /** + * Settings to configure the session replay. + */ + public var sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0) + #endif + + func validateOptions(_ options: [String: Any]?) { + #if canImport(UIKit) + if let sessionReplayOptions = options?["sessionReplay"] as? [String: Any] { + sessionReplay = SentryReplayOptions(dictionary: sessionReplayOptions) + } + #endif + } + +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift similarity index 72% rename from Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift rename to Sources/Swift/Tools/SentryViewPhotographer.swift index 09d27f9ad40..36667e4dec2 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -29,7 +29,8 @@ class SentryViewPhotographer: NSObject { ].compactMap { NSClassFromString($0) } } - func image(view: UIView) -> UIImage? { + @objc(imageWithView:options:) + func image(view: UIView, options: SentryRedactOptions) -> UIImage? { UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) defer { @@ -39,15 +40,19 @@ class SentryViewPhotographer: NSObject { guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } view.layer.render(in: currentContext) - self.mask(view: view, context: currentContext) + self.mask(view: view, context: currentContext, options: options) guard let screenshot = UIGraphicsGetImageFromCurrentImageContext() else { return nil } return screenshot } - private func mask(view: UIView, context: CGContext) { + private func mask(view: UIView, context: CGContext, options: SentryRedactOptions?) { UIColor.black.setFill() - let maskPath = self.buildPath(view: view, path: CGMutablePath(), area: view.frame) + let maskPath = self.buildPath(view: view, + path: CGMutablePath(), + area: view.frame, + redactText: options?.redactAllText ?? true, + redactImage: options?.redactAllImages ?? true) context.addPath(maskPath) context.fillPath() } @@ -57,24 +62,20 @@ class SentryViewPhotographer: NSObject { } private func shouldRedact(view: UIView) -> Bool { - if let imageView = view as? UIImageView { - return shouldRedact(imageView: imageView) - } - - return redactClasses.contains { view.isKind(of: $0) } + return redactClasses.contains { view.isKind(of: $0) } } private func shouldRedact(imageView: UIImageView) -> Bool { // Checking the size is to avoid redact gradient backgroud that // are usually small lines repeating guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } - return image.imageAsset?.value(forKey: "_containingBundle") != nil + return image.imageAsset?.value(forKey: "_containingBundle") == nil } - private func buildPath(view: UIView, path: CGMutablePath, area: CGRect) -> CGMutablePath { + private func buildPath(view: UIView, path: CGMutablePath, area: CGRect, redactText: Bool, redactImage: Bool) -> CGMutablePath { let rectInWindow = view.convert(view.bounds, to: nil) - if !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { + if (!redactImage && !redactText) || !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { return path } @@ -82,7 +83,14 @@ class SentryViewPhotographer: NSObject { let ignore = shouldIgnore(view: view) - if !ignore && shouldRedact(view: view) { + let redact: Bool = { + if redactImage, let imageView = view as? UIImageView { + return shouldRedact(imageView: imageView) + } + return redactText && shouldRedact(view: view) + }() + + if !ignore && redact { result.addRect(rectInWindow) return result } else if isOpaqueOrHasBackground(view) { @@ -91,7 +99,7 @@ class SentryViewPhotographer: NSObject { if !ignore { for subview in view.subviews { - result = buildPath(view: subview, path: path, area: area) + result = buildPath(view: subview, path: path, area: area, redactText: redactText, redactImage: redactImage) } } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index cd225c29602..57af570ad3f 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -6,11 +6,10 @@ import XCTest #if os(iOS) || os(tvOS) -@available(iOS 16.0, tvOS 16.0, *) class SentrySessionReplayIntegrationTests: XCTestCase { override func setUpWithError() throws { - if #unavailable(iOS 16.0, tvOS 16.0) { + guard #available(iOS 16.0, tvOS 16.0, *) else { throw XCTSkip("iOS version not supported") } } @@ -20,21 +19,24 @@ class SentrySessionReplayIntegrationTests: XCTestCase { clearTestState() } - func testNoInstall() { - SentrySDK.start { - $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0) - $0.setIntegrations([SentrySessionReplayIntegration.self]) + func startSDK(sessionSampleRate: Float, errorSampleRate: Float) { + if #available(iOS 16.0, tvOS 16.0, *) { + SentrySDK.start { + $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } } + } + + func testNoInstall() { + startSDK(sessionSampleRate: 0, errorSampleRate: 0) expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 expect(SentryGlobalEventProcessor.shared().processors.count) == 0 } func testInstallFullSessionReplay() { - SentrySDK.start { - $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 0) - $0.setIntegrations([SentrySessionReplayIntegration.self]) - } + startSDK(sessionSampleRate: 1, errorSampleRate: 0) expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 expect(SentryGlobalEventProcessor.shared().processors.count) == 1 @@ -44,10 +46,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.3) - SentrySDK.start { - $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0.2, errorSampleRate: 0) - $0.setIntegrations([SentrySessionReplayIntegration.self]) - } + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 expect(SentryGlobalEventProcessor.shared().processors.count) == 0 @@ -57,20 +56,14 @@ class SentrySessionReplayIntegrationTests: XCTestCase { SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.1) - SentrySDK.start { - $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0.2, errorSampleRate: 0) - $0.setIntegrations([SentrySessionReplayIntegration.self]) - } + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 expect(SentryGlobalEventProcessor.shared().processors.count) == 1 } func testInstallErrorReplay() { - SentrySDK.start { - $0.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0.1) - $0.setIntegrations([SentrySessionReplayIntegration.self]) - } + startSDK(sessionSampleRate: 0, errorSampleRate: 0.1) expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 expect(SentryGlobalEventProcessor.shared().processors.count) == 1 diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 055a7a74240..1925b4eb2e9 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -5,11 +5,10 @@ import SentryTestUtils import XCTest #if os(iOS) || os(tvOS) -@available(iOS 16.0, tvOS 16.0, *) class SentrySessionReplayTests: XCTestCase { private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { - func image(with view: UIView) -> UIImage { UIImage.add } + func image(with view: UIView, options: SentryRedactOptions) -> UIImage { UIImage.add } } private class TestReplayMaker: NSObject, SentryReplayMaker { @@ -17,11 +16,11 @@ class SentrySessionReplayTests: XCTestCase { var duration: TimeInterval var beginning: Date var outputFileURL: URL - var completion: ((Sentry.SentryVideoInfo?, (any Error)?) -> Void) + var completion: ((Sentry.SentryVideoInfo?, Error?) -> Void) } var lastCallToCreateVideo: CreateVideoCall? - func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (Sentry.SentryVideoInfo?, (any Error)?) -> Void) throws { + func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { lastCallToCreateVideo = CreateVideoCall(duration: duration, beginning: beginning, outputFileURL: outputFileURL, @@ -43,7 +42,6 @@ class SentrySessionReplayTests: XCTestCase { func releaseFrames(until date: Date) { lastReleaseUntil = date } - } private class ReplayHub: SentryHub { @@ -58,6 +56,7 @@ class SentrySessionReplayTests: XCTestCase { } } + @available(iOS 16.0, tvOS 16.0, *) private class Fixture { let dateProvider = TestCurrentDateProvider() let random = TestRandom(value: 0) @@ -80,14 +79,13 @@ class SentrySessionReplayTests: XCTestCase { } override func setUpWithError() throws { - if #unavailable(iOS 16.0, tvOS 16.0) { + guard #available(iOS 16.0, tvOS 16.0, *) else { throw XCTSkip("iOS version not supported") } } override func setUp() { super.setUp() - SentrySDK.setCurrentHub(fixture.hub) } override func tearDown() { @@ -95,9 +93,16 @@ class SentrySessionReplayTests: XCTestCase { clearTestState() } - private let fixture = Fixture() + @available(iOS 16.0, tvOS 16, *) + private func startFixture() -> Fixture { + let fixture = Fixture() + SentrySDK.setCurrentHub(fixture.hub) + return fixture + } + @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NoFullSession() { + let fixture = startFixture() let sut = fixture.getSut() sut.start(fixture.rootView, fullSession: false) @@ -106,10 +111,12 @@ class SentrySessionReplayTests: XCTestCase { fixture.dateProvider.advance(by: 5) Dynamic(sut).newFrame(nil) - expect(self.fixture.hub.lastEvent) == nil + expect(fixture.hub.lastEvent) == nil } + @available(iOS 16.0, tvOS 16, *) func testSentReplay_FullSession() { + let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: true) @@ -128,14 +135,16 @@ class SentrySessionReplayTests: XCTestCase { expect(videoArguments.duration) == 5 expect(videoArguments.beginning) == start - expect(videoArguments.outputFileURL) == fixture.cacheFolder.appendingPathComponent("segments/1.000000-6.000000.mp4") + expect(videoArguments.outputFileURL) == fixture.cacheFolder.appendingPathComponent("segments/0.mp4") - expect(self.fixture.hub.lastRecording) != nil - expect(self.fixture.hub.lastVideo) == videoArguments.outputFileURL + expect(fixture.hub.lastRecording) != nil + expect(fixture.hub.lastVideo) == videoArguments.outputFileURL assertFullSession(sut, expected: true) } + @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NotFullSession() { + let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: false) @@ -151,7 +160,9 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: false) } + @available(iOS 16.0, tvOS 16, *) func testChangeReplayMode_forErrorEvent() { + let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: false) @@ -161,16 +172,36 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: true) } + @available(iOS 16.0, tvOS 16, *) func testDontChangeReplayMode_forNonErrorEvent() { + let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: false) let event = Event(level: .info) sut.capture(for: event) + assertFullSession(sut, expected: false) } + @available(iOS 16.0, tvOS 16, *) + func testSessionReplayMaximumDuration() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: true) + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + expect(Dynamic(sut).isRunning) == true + fixture.dateProvider.advance(by: 3_600) + Dynamic(sut).newFrame(nil) + + expect(Dynamic(sut).isRunning) == false + } + + @available(iOS 16.0, tvOS 16, *) func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { expect(Dynamic(sessionReplay).isFullSession) == expected } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index d8de03496a5..1ceb3dbc5d2 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -604,9 +604,8 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(options.enableUserInteractionTracing, YES); XCTAssertEqual(options.enablePreWarmedAppStartTracing, NO); XCTAssertEqual(options.attachViewHierarchy, NO); - if (@available(iOS 16.0, tvOS 16.0, *)) { - XCTAssertNil(options.sessionReplayOptions); - } + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 0); + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 0); #endif XCTAssertFalse(options.enableTracing); XCTAssertTrue(options.enableAppHangTracking); @@ -786,11 +785,11 @@ - (void)testSessionReplaySettingsInit { if (@available(iOS 16.0, tvOS 16.0, *)) { SentryOptions *options = [self getValidOptions:@{ - @"sessionReplayOptions" : - @ { @"replaysSessionSampleRate" : @2, @"replaysOnErrorSampleRate" : @4 } + @"experimental" : + @ { @"sessionReplay" : @ { @"sessionSampleRate" : @2, @"errorSampleRate" : @4 } } }]; - XCTAssertEqual(options.sessionReplayOptions.replaysSessionSampleRate, 2); - XCTAssertEqual(options.sessionReplayOptions.replaysOnErrorSampleRate, 4); + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 2); + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 4); } } @@ -798,8 +797,8 @@ - (void)testSessionReplaySettingsDefaults { if (@available(iOS 16.0, tvOS 16.0, *)) { SentryOptions *options = [self getValidOptions:@{ @"sessionReplayOptions" : @ {} }]; - XCTAssertEqual(options.sessionReplayOptions.replaysSessionSampleRate, 0); - XCTAssertEqual(options.sessionReplayOptions.replaysOnErrorSampleRate, 0); + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 0); + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 0); } }