From 0f3280e5394d44e1af0465d1399428c263bc316f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 9 Feb 2024 09:53:04 +0100 Subject: [PATCH 01/14] Update project.pbxproj --- Sentry.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 4fa5d95953f..3898d01c380 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -2174,6 +2174,7 @@ 6383953423ABA269000C1594 /* Integrations */ = { isa = PBXGroup; children = ( + D80CD8D52B752FD9002F710B /* SessionReplay */, 7B08A3432924CF4E0059603A /* MetricKit */, 7B127B0B27CF6EF600A71ED2 /* ANR */, 7BE0DC33272AE74A004FA8B7 /* Breadcrumbs */, @@ -3366,6 +3367,13 @@ path = UIEvents; sourceTree = ""; }; + D80CD8D52B752FD9002F710B /* SessionReplay */ = { + isa = PBXGroup; + children = ( + ); + name = SessionReplay; + sourceTree = ""; + }; D8105B37297A86B800299F03 /* Recovered References */ = { isa = PBXGroup; children = ( From 0e524b593e42eda15f7016fe3045e4e845b69591 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 12 Feb 2024 10:07:40 +0100 Subject: [PATCH 02/14] feat: Add Session Replay --- CHANGELOG.md | 4 ++++ Sentry.xcodeproj/project.pbxproj | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e164c9634..0a6ec90de02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add Session Replay (#) + ### Improvements - Cache installationID async to avoid file IO on the main thread when starting the SDK (#3601) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 3898d01c380..c6fd97719aa 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -2174,7 +2174,6 @@ 6383953423ABA269000C1594 /* Integrations */ = { isa = PBXGroup; children = ( - D80CD8D52B752FD9002F710B /* SessionReplay */, 7B08A3432924CF4E0059603A /* MetricKit */, 7B127B0B27CF6EF600A71ED2 /* ANR */, 7BE0DC33272AE74A004FA8B7 /* Breadcrumbs */, @@ -2184,6 +2183,7 @@ 7BE0DC35272AE7BF004FA8B7 /* SentryCrash */, D85596EF280580BE0041FF8B /* Screenshot */, 0A9BF4E028A114690068D266 /* ViewHierarchy */, + D80CD8D52B752FD9002F710B /* SessionReplay */, 7D7F0A5E23DF3D2C00A4629C /* SentryGlobalEventProcessor.h */, 7DAC588E23D8B2E0001CF26B /* SentryGlobalEventProcessor.m */, 7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */, From cd816d54f76d46a6bfc452d86120d221f4a1a67a Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 12 Feb 2024 10:09:00 +0100 Subject: [PATCH 03/14] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a6ec90de02..abde07f0309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add Session Replay (#) +- Add Session Replay (#3625) ### Improvements From 88c754764b59241b394135d3462210c8f8df381c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 29 Feb 2024 10:26:25 +0100 Subject: [PATCH 04/14] feat(Session Replay): ReplayEvent, ReplayRecording and Envelope handling (#3638) Added SentryReplayEven, SentryReplayRecording and Envelope handling for this new types. Co-authored-by: Philipp Hofmann --- Sentry.xcodeproj/project.pbxproj | 52 +++++++++ Sources/Sentry/SentryClient.m | 44 ++++++- Sources/Sentry/SentryDataCategoryMapper.m | 9 ++ Sources/Sentry/SentryDateUtil.m | 5 + Sources/Sentry/SentryEnvelope.m | 39 +++++++ Sources/Sentry/SentryHub.m | 10 ++ Sources/Sentry/SentryMsgPackSerializer.m | 110 ++++++++++++++++++ Sources/Sentry/SentryReplayEvent.m | 41 +++++++ Sources/Sentry/SentryReplayRecording.m | 75 ++++++++++++ Sources/Sentry/SentryReplayType.m | 14 +++ Sources/Sentry/SentrySerialization.m | 6 +- .../include/HybridPublic/SentryEnvelope.h | 2 + .../HybridPublic/SentryEnvelopeItemType.h | 1 + Sources/Sentry/include/SentryClient+Private.h | 7 +- Sources/Sentry/include/SentryDataCategory.h | 4 +- .../Sentry/include/SentryDataCategoryMapper.h | 1 + Sources/Sentry/include/SentryDateUtil.h | 2 + .../Sentry/include/SentryEnvelope+Private.h | 6 + Sources/Sentry/include/SentryHub+Private.h | 6 + .../Sentry/include/SentryMsgPackSerializer.h | 33 ++++++ Sources/Sentry/include/SentryReplayEvent.h | 45 +++++++ .../Sentry/include/SentryReplayRecording.h | 46 ++++++++ Sources/Sentry/include/SentryReplayType.h | 16 +++ Sources/Sentry/include/SentrySerialization.h | 2 +- .../Helper/SentryDateUtilTests.swift | 8 ++ .../SentryReplayEventTests.swift | 30 +++++ .../SentryReplayRecordingTests.swift | 41 +++++++ .../SentryDataCategoryMapperTests.swift | 7 +- .../Protocol/SentryEnvelopeTests.swift | 7 ++ Tests/SentryTests/SentryClientTests.swift | 63 ++++++++++ Tests/SentryTests/SentryHubTests.swift | 28 +++++ .../SentryMsgPackSerializerTests.m | 103 ++++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 3 + 33 files changed, 853 insertions(+), 13 deletions(-) create mode 100644 Sources/Sentry/SentryMsgPackSerializer.m create mode 100644 Sources/Sentry/SentryReplayEvent.m create mode 100644 Sources/Sentry/SentryReplayRecording.m create mode 100644 Sources/Sentry/SentryReplayType.m create mode 100644 Sources/Sentry/include/SentryMsgPackSerializer.h create mode 100644 Sources/Sentry/include/SentryReplayEvent.h create mode 100644 Sources/Sentry/include/SentryReplayRecording.h create mode 100644 Sources/Sentry/include/SentryReplayType.h create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift create mode 100644 Tests/SentryTests/SentryMsgPackSerializerTests.m diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index a60c07f24a1..704647195a2 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -747,6 +747,10 @@ 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 */; }; + 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 */; }; @@ -764,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 */; }; + 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 */; }; @@ -795,6 +801,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 */; }; @@ -805,6 +813,8 @@ 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 */; }; 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 */; }; @@ -839,6 +849,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 */ @@ -1735,6 +1746,10 @@ 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 = ""; }; + 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 = ""; }; @@ -1759,6 +1774,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 = ""; }; @@ -1790,6 +1807,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 = ""; }; @@ -1801,6 +1820,8 @@ 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 = ""; }; 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 = ""; }; @@ -1839,6 +1860,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 */ @@ -2772,6 +2794,7 @@ 7BE0DC40272AEA0A004FA8B7 /* Performance */, 7BE0DC3F272AE9F0004FA8B7 /* Session */, 7BE0DC3E272AE9DC004FA8B7 /* SentryCrash */, + D80694C12B7CC85800B820E6 /* SessionReplay */, 7B59398324AB481B0003AAD2 /* NotificationCenterTestCase.swift */, 0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */, ); @@ -3311,6 +3334,8 @@ D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */, 0A2D8DA6289BC905008720F6 /* SentryViewHierarchy.h */, 0A2D8DA7289BC905008720F6 /* SentryViewHierarchy.m */, + D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */, + D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */, ); name = Tools; sourceTree = ""; @@ -3376,6 +3401,15 @@ path = Swift; sourceTree = ""; }; + D80694C12B7CC85800B820E6 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, + D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; D808FB85281AB2EF009A2A33 /* UIEvents */ = { isa = PBXGroup; children = ( @@ -3388,6 +3422,12 @@ D80CD8D52B752FD9002F710B /* SessionReplay */ = { isa = PBXGroup; children = ( + D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */, + D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */, + D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */, + D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, + D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, + D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, ); name = SessionReplay; sourceTree = ""; @@ -3430,6 +3470,7 @@ D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */, D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */, D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, + D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, ); name = Tools; sourceTree = ""; @@ -3642,6 +3683,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 */, @@ -3677,6 +3719,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 */, @@ -3788,10 +3831,12 @@ 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 */, 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 */, @@ -4249,6 +4294,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 */, @@ -4271,6 +4317,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 */, @@ -4279,6 +4326,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 */, @@ -4352,6 +4400,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 */, @@ -4414,6 +4463,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 */, @@ -4447,6 +4497,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 */, @@ -4537,6 +4588,7 @@ 7BD4BD4B27EB2DC20071F4FF /* SentryDiscardedEventTests.swift in Sources */, 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m 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 d7435185011..a5ec52eb94a 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,12 +30,15 @@ #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 "SentryReplayEvent.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" +#import "SentrySerialization.h" #import "SentrySession.h" #import "SentryStacktraceBuilder.h" #import "SentrySwift.h" @@ -472,13 +475,46 @@ - (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 + withScope:(SentryScope *)scope +{ + 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."); + return; + } + + // breadcrumbs for replay will be send with ReplayRecording + replayEvent.breadcrumbs = nil; + + SentryEnvelopeItem *videoEnvelopeItem = + [[SentryEnvelopeItem alloc] initWithReplayEvent: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 ]]; + + [self captureEnvelope:envelope]; +} + - (void)captureEnvelope:(SentryEnvelope *)envelope { if ([self isDisabled]) { 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/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index f362b345fc1..299805e48e4 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -38,6 +38,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } ++ (long)millisecondsSince1970:(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..e6752312dd6 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" @@ -48,6 +51,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId return self; } ++ (instancetype)empty +{ + return [[SentryEnvelopeHeader alloc] initWithId:nil traceContext:nil]; +} + @end @implementation SentryEnvelopeItem @@ -198,6 +206,37 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return [self initWithHeader:itemHeader data:data]; } +- (nullable 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"]; + + 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; + } + + 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 ef1727fb722..17e0cba9d45 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -297,6 +297,16 @@ - (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/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m new file mode 100644 index 00000000000..1bbe76e027b --- /dev/null +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -0,0 +1,110 @@ +#import "SentryMsgPackSerializer.h" +#import "SentryLog.h" + +@implementation SentryMsgPackSerializer + ++ (BOOL)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)]; + + 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, + (long)bytesRead); + + [inputStream close]; + [outputStream close]; + return NO; + } + } + + [inputStream close]; + } + [outputStream close]; + + return YES; +} + +@end + +@implementation +NSURL (SentryStreameble) + +- (NSInputStream *)asInputStream +{ + return [[NSInputStream alloc] initWithURL:self]; +} + +- (NSInteger)streamSize +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:&error]; + if (attributes == nil) { + SENTRY_LOG_DEBUG(@"Could not read file attributes - File: %@ - %@", self, error); + return -1; + } + 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/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m new file mode 100644 index 00000000000..c7298e03c49 --- /dev/null +++ b/Sources/Sentry/SentryReplayEvent.m @@ -0,0 +1,41 @@ +#import "SentryReplayEvent.h" +#import "SentryDateUtil.h" +#import "SentryEnvelopeItemType.h" +#import "SentryId.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryReplayEvent + +- (instancetype)init +{ + if (self = [super init]) { + self.type = SentryEnvelopeItemTypeReplayVideo; + } + return self; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *result = [[super serialize] mutableCopy]; + + NSMutableArray *trace_ids = [[NSMutableArray alloc] initWithCapacity:self.traceIds.count]; + + for (SentryId *traceId in self.traceIds) { + [trace_ids addObject:traceId.sentryIdString]; + } + + result[@"urls"] = self.urls; + result[@"replay_start_timestamp"] = + @([SentryDateUtil millisecondsSince1970:self.replayStartTimestamp]); + result[@"trace_ids"] = trace_ids; + result[@"replay_id"] = self.replayId.sentryIdString; + result[@"segment_id"] = @(self.segmentId); + result[@"replay_type"] = nameForSentryReplayType(self.replayType); + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m new file mode 100644 index 00000000000..059ac1bfff7 --- /dev/null +++ b/Sources/Sentry/SentryReplayRecording.m @@ -0,0 +1,75 @@ +#import "SentryReplayRecording.h" +#import "SentryDateUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +@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; +} + +- (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 + 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" : SentryReplayEncoding, + @"container" : SentryReplayContainer, + @"height" : @(self.height), + @"width" : @(self.width), + @"frameCount" : @(self.frameCount), + @"frameRateType" : SentryReplayFrameRateType, + @"frameRate" : @(self.frameRate), + @"left" : @0, + @"top" : @0, + } + } + }; + + return @[ metaInfo, recordingInfo ]; +} + +@end + +NS_ASSUME_NONNULL_END 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/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..b0ad1fc0b3f 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h @@ -5,3 +5,4 @@ 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"; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index a9bcd469818..5bd2d6f3387 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,11 @@ SentryClient () additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:)); +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL + withScope:(SentryScope *)scope; + - (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:)); /** diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index ff62ecbc21f..9b3a5c1a8cc 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 + kSentryDataCategoryReplay = 8, + kSentryDataCategoryUnknown = 9 }; static DEPRECATED_MSG_ATTRIBUTE( @@ -29,5 +30,6 @@ static DEPRECATED_MSG_ATTRIBUTE( @"attachment", @"user_report", @"profile", + @"replay", @"unkown", }; 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/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index 60c8fdb6562..8cb845c984a 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)millisecondsSince1970:(NSDate *)date; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index 98682fa1a2f..a31018b5e29 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; +- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + @end NS_ASSUME_NONNULL_END 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 diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h new file mode 100644 index 00000000000..d6a1485e372 --- /dev/null +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -0,0 +1,33 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol SentryStreamable + +- (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 + ++ (BOOL)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path; + +@end + +@interface +NSData (inputStreameble) +@end + +@interface +NSURL (inputStreameble) +@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..ef20250097d --- /dev/null +++ b/Sources/Sentry/include/SentryReplayEvent.h @@ -0,0 +1,45 @@ +#import "SentryEvent.h" +#import "SentryReplayType.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; + +/** + * 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 new file mode 100644 index 00000000000..c4b402a6db8 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -0,0 +1,46 @@ +#import "SentrySerializable.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const SentryReplayEncoding = @"h264"; +static NSString *const SentryReplayContainer = @"mp4"; +static NSString *const SentryReplayFrameRateType = @"constant"; + +@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; + +- (NSArray *> *)serialize; + +- (NSDictionary *)headerForReplayRecording; + +@end + +NS_ASSUME_NONNULL_END 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/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..50096006244 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.millisecondsSince1970(testDate) + + expect(timestamp) == 60_000 + } + } 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/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() { 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 8cccc5efde7..af2baea84d9 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1571,6 +1571,69 @@ 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!, with: Scope()) + let envelope = fixture.transport.sentEnvelopes.first + expect(envelope?.items[0].header.type) == SentryEnvelopeItemTypeReplayVideo + } + + func testCaptureReplayEvent_WrongEventFromEventProcessor() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return Event() + } + + 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 a non ReplayEvent + 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 + } + + 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/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 69cfd78389f..8e21e612779 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -736,6 +736,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) diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m new file mode 100644 index 00000000000..6606a1e121f --- /dev/null +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -0,0 +1,103 @@ +#import "SentryMsgPackSerializer.h" +#import +#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] + }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; + XCTAssertTrue(result); + 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 }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; + XCTAssertTrue(result); + 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)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]; + [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 6b7ae1e43ff..6adb9b2b09f 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -215,9 +215,12 @@ #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 "SentryReplayRecording.h" #import "SentrySampleDecision+Private.h" #import "SentrySpanOperations.h" #import "SentryTimeToDisplayTracker.h" From 2c6fff0e01989d7e5922b070a171c8823086d459 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 29 Feb 2024 11:49:32 +0100 Subject: [PATCH 05/14] Feat(Session Replay): Replay Options (#3674) Settings for session replay in the Options --- Sentry.xcodeproj/project.pbxproj | 10 ++++ Sources/Sentry/Public/Sentry.h | 1 + Sources/Sentry/Public/SentryOptions.h | 12 ++++- Sources/Sentry/Public/SentryReplayOptions.h | 38 ++++++++++++++ Sources/Sentry/SentryBaseIntegration.m | 14 +++++ Sources/Sentry/SentryOptions.m | 14 +++-- Sources/Sentry/SentryReplayOptions.m | 51 +++++++++++++++++++ .../SentryReplayOptions+Private.h | 20 ++++++++ .../Sentry/include/SentryBaseIntegration.h | 1 + Tests/SentryTests/SentryOptionsTest.m | 25 +++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 11 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 Sources/Sentry/Public/SentryReplayOptions.h create mode 100644 Sources/Sentry/SentryReplayOptions.m create mode 100644 Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 704647195a2..dd90698a65a 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 */; }; 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 */; }; @@ -1774,6 +1776,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 = ""; }; 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 = ""; }; @@ -3428,6 +3433,9 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, + D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */, + D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */, + D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */, ); name = SessionReplay; sourceTree = ""; @@ -3664,6 +3672,7 @@ 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 */, @@ -4279,6 +4288,7 @@ 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 afaa72b0986..ac23d1d73d9 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 "SentryReplayOptions.h" #import "SentryRequest.h" #import "SentrySDK.h" #import "SentrySampleDecision.h" diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 4bb0ce9680d..7d2e6c8a328 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; +@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, + SentryReplayOptions; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -269,6 +270,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) + SentryReplayOptions *sessionReplayOptions API_AVAILABLE(ios(16.0), tvos(16.0)); + #endif // SENTRY_UIKIT_AVAILABLE /** diff --git a/Sources/Sentry/Public/SentryReplayOptions.h b/Sources/Sentry/Public/SentryReplayOptions.h new file mode 100644 index 00000000000..3220a5f8c91 --- /dev/null +++ b/Sources/Sentry/Public/SentryReplayOptions.h @@ -0,0 +1,38 @@ +#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 5706d68e925..40b4cf97520 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,6 +1,7 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" +#import "SentryReplayOptions.h" #import #import #import @@ -140,6 +141,19 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options [self logWithOptionName:@"attachViewHierarchy"]; return NO; } + + if (integrationOptions & kIntegrationOptionEnableReplay) { + if (@available(iOS 16.0, tvOS 16.0, *)) { + if (options.sessionReplayOptions.replaysOnErrorSampleRate == 0 + && options.sessionReplayOptions.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 a1b6f0958a5..151d523ebad 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,9 +23,8 @@ # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" -# if SENTRY_HAS_UIKIT -# import "SentryScreenshotIntegration.h" -# endif // SENTRY_HAS_UIKIT +# import "SentryReplayOptions+Private.h" +# import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" @@ -382,7 +381,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; }]; @@ -402,6 +400,14 @@ - (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"] diff --git a/Sources/Sentry/SentryReplayOptions.m b/Sources/Sentry/SentryReplayOptions.m new file mode 100644 index 00000000000..9e9922ff7ed --- /dev/null +++ b/Sources/Sentry/SentryReplayOptions.m @@ -0,0 +1,51 @@ +#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/include/HybridPublic/SentryReplayOptions+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h new file mode 100644 index 00000000000..2ef6e8094bb --- /dev/null +++ b/Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h @@ -0,0 +1,20 @@ +#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/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 diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 088b09f21f3..d8de03496a5 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -544,6 +544,7 @@ - (void)testNSNull_SetsDefaultValue #if SENTRY_HAS_UIKIT @"enableUIViewControllerTracing" : [NSNull null], @"attachScreenshot" : [NSNull null], + @"sessionReplayOptions" : [NSNull null], #endif @"enableAppHangTracking" : [NSNull null], @"appHangTimeoutInterval" : [NSNull null], @@ -603,6 +604,9 @@ - (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); + } #endif XCTAssertFalse(options.enableTracing); XCTAssertTrue(options.enableAppHangTracking); @@ -778,6 +782,27 @@ - (void)testEnablePreWarmedAppStartTracking [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:NO]; } +- (void)testSessionReplaySettingsInit +{ + if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryOptions *options = [self getValidOptions:@{ + @"sessionReplayOptions" : + @ { @"replaysSessionSampleRate" : @2, @"replaysOnErrorSampleRate" : @4 } + }]; + XCTAssertEqual(options.sessionReplayOptions.replaysSessionSampleRate, 2); + XCTAssertEqual(options.sessionReplayOptions.replaysOnErrorSampleRate, 4); + } +} + +- (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); + } +} + #endif #if SENTRY_HAS_METRIC_KIT diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 6adb9b2b09f..475682b3819 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -220,6 +220,7 @@ #import "SentryPerformanceTracker+Testing.h" #import "SentryPropagationContext.h" #import "SentryReplayEvent.h" +#import "SentryReplayOptions.h" #import "SentryReplayRecording.h" #import "SentrySampleDecision+Private.h" #import "SentrySpanOperations.h" From 90e65829a09ea20f7707eaccd16f5aceba941f3f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 1 Mar 2024 11:28:00 +0100 Subject: [PATCH 06/14] ref(Session Replay):Replay recording serialization (#3677) --- Sources/Sentry/SentryEnvelope.m | 6 +----- Sources/Sentry/SentrySerialization.m | 11 +++++++++++ Sources/Sentry/include/SentrySerialization.h | 4 +++- .../Helper/SentrySerializationTests.swift | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index e6752312dd6..52e13dc81a5 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -211,11 +211,7 @@ - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent 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]]]; - + NSData *recording = [SentrySerialization dataWithReplayRecording:replayRecording]; NSURL *envelopeContentUrl = [[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"]; diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m index 6e16cb05b97..3d5c06e5248 100644 --- a/Sources/Sentry/SentrySerialization.m +++ b/Sources/Sentry/SentrySerialization.m @@ -8,6 +8,7 @@ #import "SentryId.h" #import "SentryLevelMapper.h" #import "SentryLog.h" +#import "SentryReplayRecording.h" #import "SentrySdkInfo.h" #import "SentrySession.h" #import "SentryTraceContext.h" @@ -322,6 +323,16 @@ + (SentrySession *_Nullable)sessionWithData:(NSData *)sessionData return session; } ++ (NSData *)dataWithReplayRecording:(SentryReplayRecording *)replayRecording +{ + NSMutableData *recording = [NSMutableData data]; + [recording appendData:[SentrySerialization + dataWithJSONObject:[replayRecording headerForReplayRecording]]]; + [recording appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [recording appendData:[SentrySerialization dataWithJSONObject:[replayRecording serialize]]]; + return recording; +} + + (SentryAppState *_Nullable)appStateWithData:(NSData *)data { NSError *error = nil; diff --git a/Sources/Sentry/include/SentrySerialization.h b/Sources/Sentry/include/SentrySerialization.h index 704e9b5cfd7..0ff1c23951b 100644 --- a/Sources/Sentry/include/SentrySerialization.h +++ b/Sources/Sentry/include/SentrySerialization.h @@ -1,6 +1,6 @@ #import "SentryDefines.h" -@class SentrySession, SentryEnvelope, SentryAppState; +@class SentrySession, SentryEnvelope, SentryAppState, SentryReplayRecording; NS_ASSUME_NONNULL_BEGIN @@ -20,6 +20,8 @@ static int const SENTRY_BAGGAGE_MAX_SIZE = 8192; + (NSData *_Nullable)dataWithEnvelope:(SentryEnvelope *)envelope error:(NSError *_Nullable *_Nullable)error; ++ (NSData *)dataWithReplayRecording:(SentryReplayRecording *)replayRecording; + + (SentryEnvelope *_Nullable)envelopeWithData:(NSData *)data; + (SentryAppState *_Nullable)appStateWithData:(NSData *)sessionData; diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index 26f277c6639..a848b898773 100644 --- a/Tests/SentryTests/Helper/SentrySerializationTests.swift +++ b/Tests/SentryTests/Helper/SentrySerializationTests.swift @@ -1,3 +1,4 @@ +import Nimble import XCTest class SentrySerializationTests: XCTestCase { @@ -230,6 +231,22 @@ class SentrySerializationTests: XCTestCase { XCTAssertNil(SentrySerialization.session(with: data)) } + func testSerializeReplayRecording() { + class MockReplayRecording: SentryReplayRecording { + override func serialize() -> [[String: Any]] { + return [["KEY": "VALUE"]] + } + } + + let date = Date(timeIntervalSince1970: 2) + let recording = MockReplayRecording(segmentId: 5, size: 5_000, start: date, duration: 5_000, frameCount: 5, frameRate: 1, height: 320, width: 950) + let data = SentrySerialization.data(with: recording) + + let serialized = String(data: data, encoding: .utf8) + + expect(serialized) == "{\"segment_id\":5}\n[{\"KEY\":\"VALUE\"}]" + } + func testLevelFromEventData() { let envelopeItem = SentryEnvelopeItem(event: TestData.event) From 9bcf1e7b99127aba7dbf7cca1f31c3764a9dabbe Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 09:52:27 +0100 Subject: [PATCH 07/14] feat(Session Replay): Prepare Session event (#3693) Handling session event during prepareEvent. --- Sources/Sentry/SentryClient.m | 9 ++++----- Sources/Sentry/SentryScope.m | 3 ++- Tests/SentryTests/SentryClientTests.swift | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index a5ec52eb94a..0cd79aa658a 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -495,9 +495,6 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent return; } - // breadcrumbs for replay will be send with ReplayRecording - replayEvent.breadcrumbs = nil; - SentryEnvelopeItem *videoEnvelopeItem = [[SentryEnvelopeItem alloc] initWithReplayEvent:replayEvent replayRecording:replayRecording @@ -589,9 +586,11 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event BOOL eventIsNotATransaction = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction]; + BOOL eventIsNotReplay + = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]; // Transactions have their own sampleRate - if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) { + if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) { SENTRY_LOG_DEBUG(@"Event got sampled, will not send the event"); [self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate]; return nil; @@ -619,7 +618,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event [self setSdk:event]; // We don't want to attach debug meta and stacktraces for transactions; - if (eventIsNotATransaction) { + if (eventIsNotATransaction && eventIsNotReplay) { BOOL shouldAttachStacktrace = alwaysAttachStacktrace || self.options.attachStacktrace || (nil != event.exceptions && [event.exceptions count] > 0); diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 2eba7d70bde..351e4ae971e 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -496,7 +496,8 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event event.fingerprint = fingerprints; } - if (event.breadcrumbs == nil) { + if (event.breadcrumbs == nil + && ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]) { NSArray *breadcrumbs = [self breadcrumbs]; event.breadcrumbs = [breadcrumbs subarrayWithRange:NSMakeRange(0, MIN(maxBreadcrumbs, [breadcrumbs count]))]; diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index af2baea84d9..e8c229995f4 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1634,6 +1634,27 @@ class SentryClientTest: XCTestCase { expect(self.fixture.transport.sentEnvelopes.count) == 0 } + func testCaptureReplayEvent_noBradcrumbsThreadsDebugMeta() { + 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") + + let scope = Scope() + scope.addBreadcrumb(Breadcrumb(level: .debug, category: "Test Breadcrumb")) + + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) + + expect(replayEvent.breadcrumbs) == nil + expect(replayEvent.threads) == nil + expect(replayEvent.debugMeta) == nil + + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() From 0a08f99d4d2cc294892a65e4c1b1ca76e9144979 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 5 Mar 2024 13:30:04 +0100 Subject: [PATCH 08/14] Fixing Merge --- .../Networking/SentryDataCategoryMapperTests.swift | 7 +++++-- Tests/SentryTests/SentryTests-Bridging-Header.h | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index 143194912ae..8ccf0359b7c 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -10,7 +10,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForEnvelopItemType("attachment")) == .attachment expect(sentryDataCategoryForEnvelopItemType("profile")) == .profile expect(sentryDataCategoryForEnvelopItemType("statsd")) == .statsd - expect(sentryDataCategoryForEnvelopItemType("statsd")) == .replay + expect(sentryDataCategoryForEnvelopItemType("replay_video")) == .replay expect(sentryDataCategoryForEnvelopItemType("unknown item type")) == .default } @@ -24,7 +24,8 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForNSUInteger(6)) == .userFeedback expect(sentryDataCategoryForNSUInteger(7)) == .profile expect(sentryDataCategoryForNSUInteger(8)) == .statsd - expect(sentryDataCategoryForNSUInteger(9)) == .unknown + expect(sentryDataCategoryForNSUInteger(9)) == .replay + expect(sentryDataCategoryForNSUInteger(10)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(10), "Failed to map unknown category number to case .unknown") } @@ -39,6 +40,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForString(kSentryDataCategoryNameUserFeedback)) == .userFeedback expect(sentryDataCategoryForString(kSentryDataCategoryNameProfile)) == .profile expect(sentryDataCategoryForString(kSentryDataCategoryNameStatsd)) == .statsd + expect(sentryDataCategoryForString(kSentryDataCategoryNameReplay)) == .replay expect(sentryDataCategoryForString(kSentryDataCategoryNameUnknown)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForString("gdfagdfsa"), "Failed to map unknown category name to case .unknown") @@ -54,6 +56,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(nameForSentryDataCategory(.userFeedback)) == kSentryDataCategoryNameUserFeedback expect(nameForSentryDataCategory(.profile)) == kSentryDataCategoryNameProfile expect(nameForSentryDataCategory(.statsd)) == kSentryDataCategoryNameStatsd + expect(nameForSentryDataCategory(.replay)) == kSentryDataCategoryNameReplay expect(nameForSentryDataCategory(.unknown)) == kSentryDataCategoryNameUnknown } } diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 0c643ce2c71..ad42b1df4b1 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -220,7 +220,6 @@ #import "TestSentryCrashWrapper.h" #import "TestSentrySpan.h" #import "URLSessionTaskMock.h" -@import SentryPrivate; #import "SentryBinaryImageCache+Private.h" #import "SentryCrashBinaryImageCache.h" #import "SentryDispatchFactory.h" From 639e9fcca01eba85d402d7a51fb62c8fa01e0f61 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 5 Mar 2024 12:31:11 +0000 Subject: [PATCH 09/14] Format code --- .../SentryTests/SentryTests-Bridging-Header.h | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) 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 73cbdd97fe52ba00eb797d951ceff45cc3b8dec0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Mar 2024 10:19:25 +0100 Subject: [PATCH 10/14] format --- .../Recording/Monitors/SentryCrashMonitor_CPPException.cpp | 3 ++- Tests/SentryTests/SentryInterfacesTests.m | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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/Tests/SentryTests/SentryInterfacesTests.m b/Tests/SentryTests/SentryInterfacesTests.m index d344bd1d4f4..096db0915ea 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 8a4584be8d53a942310c455923d11a9f635dc1d0 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 12 Mar 2024 09:20:28 +0000 Subject: [PATCH 11/14] Format code --- .../Recording/Monitors/SentryCrashMonitor_CPPException.cpp | 3 +-- Tests/SentryTests/SentryInterfacesTests.m | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) 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 096db0915ea..d344bd1d4f4 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 d23eefa446cfbc11530ef18daf87789de9d7fe72 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Mar 2024 10:28:00 +0100 Subject: [PATCH 12/14] Update SentryReplayEvent.m --- Sources/Sentry/SentryReplayEvent.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index c7298e03c49..88899b97c69 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -1,7 +1,7 @@ #import "SentryReplayEvent.h" #import "SentryDateUtil.h" #import "SentryEnvelopeItemType.h" -#import "SentryId.h" +#import "SentrySwift.h" NS_ASSUME_NONNULL_BEGIN From b5ee0f3f21cd72804ff80c4dc45cc39d77eda8f4 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 10 Apr 2024 14:54:25 +0200 Subject: [PATCH 13/14] feat(Session Replay): Session Replay Integration (#3671) Adding Session replay integration --- Package.swift | 8 +- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 10 +- Sentry.podspec | 6 +- Sentry.xcodeproj/project.pbxproj | 110 +++++-- SentryTestUtils/TestCurrentDateProvider.swift | 4 + SentryTestUtils/TestTransport.swift | 1 + Sources/Configuration/SentryNoUI.xcconfig | 3 + Sources/Sentry/Public/Sentry.h | 1 - Sources/Sentry/Public/SentryOptions.h | 15 +- Sources/Sentry/Public/SentryReplayOptions.h | 38 --- Sources/Sentry/SentryBaseIntegration.m | 6 +- Sources/Sentry/SentryClient.m | 5 +- Sources/Sentry/SentryCoreGraphicsHelper.m | 18 ++ Sources/Sentry/SentryDateUtil.m | 2 +- Sources/Sentry/SentryEnvelope.m | 5 + Sources/Sentry/SentryOptions.m | 22 +- Sources/Sentry/SentryReplayEvent.m | 6 +- Sources/Sentry/SentryReplayOptions.m | 51 ---- Sources/Sentry/SentrySessionReplay.m | 276 ++++++++++++++++++ .../Sentry/SentrySessionReplayIntegration.m | 129 ++++++++ .../SentryReplayOptions+Private.h | 20 -- .../Sentry/include/SentryCoreGraphicsHelper.h | 13 + Sources/Sentry/include/SentryPrivate.h | 6 +- Sources/Sentry/include/SentryReplayEvent.h | 5 - .../Sentry/include/SentryReplayRecording.h | 1 - Sources/Sentry/include/SentrySessionReplay.h | 65 +++++ .../include/SentrySessionReplayIntegration.h | 12 + .../SessionReplay/SentryOnDemandReplay.swift | 188 ++++++++++++ .../SessionReplay/SentryPixelBuffer.swift | 49 ++++ .../SessionReplay/SentryReplayOptions.swift | 101 +++++++ .../SessionReplay/SentryVideoInfo.swift | 28 ++ .../Swift/Protocol/SentryRedactOptions.swift | 7 + Sources/Swift/SentryExperimentalOptions.swift | 18 ++ .../Swift/Tools/SentryViewPhotographer.swift | 115 ++++++++ .../SentryReplayEventTests.swift | 4 +- .../SentrySessionReplayIntegrationTests.swift | 73 +++++ .../SentrySessionReplayTests.swift | 210 +++++++++++++ Tests/SentryTests/SentryOptionsTest.m | 17 +- .../SentryTests/SentryTests-Bridging-Header.h | 5 +- scripts/build-xcframework.sh | 43 ++- 40 files changed, 1498 insertions(+), 198 deletions(-) create mode 100644 Sources/Configuration/SentryNoUI.xcconfig delete mode 100644 Sources/Sentry/Public/SentryReplayOptions.h create mode 100644 Sources/Sentry/SentryCoreGraphicsHelper.m delete mode 100644 Sources/Sentry/SentryReplayOptions.m create mode 100644 Sources/Sentry/SentrySessionReplay.m create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m delete mode 100644 Sources/Sentry/include/HybridPublic/SentryReplayOptions+Private.h create mode 100644 Sources/Sentry/include/SentryCoreGraphicsHelper.h create mode 100644 Sources/Sentry/include/SentrySessionReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.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/SentryReplayOptions.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift create mode 100644 Sources/Swift/Protocol/SentryRedactOptions.swift create mode 100644 Sources/Swift/SentryExperimentalOptions.swift create mode 100644 Sources/Swift/Tools/SentryViewPhotographer.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift 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"], diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 7fec10b0155..0f706a8d055 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -15,13 +15,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.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true) + } + if #available(iOS 15.0, *) { options.enableMetricKit = true } @@ -58,7 +62,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" @@ -127,7 +131,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } return scope } - } + }) } //swiftlint:enable function_body_length 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" diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 2e201c08e73..771304d6804 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 */; }; @@ -763,11 +765,15 @@ 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 */; }; + 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 */; }; - 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 */; }; @@ -794,6 +800,8 @@ 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 */; }; + 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 */; }; @@ -805,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 */; }; @@ -838,7 +848,10 @@ 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 */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; + D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.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, ); }; }; @@ -1721,6 +1734,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 = ""; }; @@ -1742,13 +1757,16 @@ 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; 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; 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 = ""; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; @@ -1759,11 +1777,13 @@ 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 = ""; }; 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 = ""; }; @@ -1775,6 +1795,8 @@ 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 = ""; }; + 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 = ""; }; @@ -1788,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 = ""; }; @@ -1795,9 +1819,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 = ""; }; @@ -1825,7 +1849,10 @@ 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 = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographer.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 = ""; }; @@ -2119,7 +2146,6 @@ 84C47B2B2A09239100DAEB8A /* .codecov.yml */, 844DA80628246D5000E6B62E /* .craft.yml */, 844DA80A28246D5000E6B62E /* .swiftlint.yml */, - 84281C552A579C2B00EE88F2 /* SentryTestUtilsObjc */, 6304360C1EC05CEF00C4D3FA /* Frameworks */, 6327C5D41EB8A783004E799B /* Products */, 63AA756E1EB8AEDB00D153DE /* Sources */, @@ -2369,6 +2395,7 @@ 84B7FA4729B2995A00AD93B1 /* DeploymentTargets.xcconfig */, 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */, D8199DCF29376FF40074249E /* SentrySwiftUI.xcconfig */, + D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */, ); path = Configuration; sourceTree = ""; @@ -3249,13 +3276,6 @@ path = Profiling; sourceTree = ""; }; - 84281C552A579C2B00EE88F2 /* SentryTestUtilsObjc */ = { - isa = PBXGroup; - children = ( - ); - path = SentryTestUtilsObjc; - sourceTree = ""; - }; 8431EFDB29B27B3D00D8DC56 /* SentryProfilerTests */ = { isa = PBXGroup; children = ( @@ -3386,12 +3406,14 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + D8CAC02D2BA0663E00E38F34 /* Integrations */, 621D9F2D2B9B030E003D94DE /* Helper */, D8F016B42B962533007B9AFB /* Extensions */, 7BF65060292B8EFE00BBA5A8 /* MetricKit */, D8F016B12B9622B7007B9AFB /* Protocol */, D856272A2A374A6800FB8062 /* Tools */, D800942628F82F3A005D3943 /* SwiftDescriptor.swift */, + D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, ); path = Swift; @@ -3402,6 +3424,8 @@ children = ( D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, + D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */, + D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3424,9 +3448,12 @@ D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, - D83D07802B7E5EFA00CC9674 /* SentryReplayOptions.h */, - D83D07842B7E634F00CC9674 /* SentryReplayOptions+Private.h */, - D83D07822B7E5F2100CC9674 /* SentryReplayOptions.m */, + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */, + D820CDB22BB1886100BA339D /* SentrySessionReplay.m */, + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, + D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, + D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */, + D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */, ); name = SessionReplay; sourceTree = ""; @@ -3500,6 +3527,7 @@ children = ( D856272B2A374A8600FB8062 /* UrlSanitized.swift */, D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */, + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, ); path = Tools; sourceTree = ""; @@ -3567,15 +3595,36 @@ isa = PBXGroup; children = ( D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */, + D8511F722BAC8F750015E6FD /* Sentry.modulemap */, ); path = Resources; sourceTree = ""; }; + D8CAC02C2BA0663E00E38F34 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, + D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, + D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, + D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; + D8CAC02D2BA0663E00E38F34 /* Integrations */ = { + isa = PBXGroup; + children = ( + D8CAC02C2BA0663E00E38F34 /* SessionReplay */, + ); + path = Integrations; + sourceTree = ""; + }; D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, + D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */, ); path = Protocol; sourceTree = ""; @@ -3644,6 +3693,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 */, @@ -3683,7 +3733,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 */, @@ -3700,6 +3749,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 */, @@ -3738,6 +3788,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 */, @@ -4161,12 +4212,14 @@ 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 */, 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 */, @@ -4196,6 +4249,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 */, @@ -4237,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 */, @@ -4254,13 +4309,13 @@ D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */, A839D89A24864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m in Sources */, 7D082B8323C628790029866B /* SentryMeta.m in Sources */, + D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */, 63FE710720DA4C1000CDBAE8 /* SentryCrashStackCursor_SelfThread.m in Sources */, 63FE711120DA4C1000CDBAE8 /* SentryCrashDebug.c in Sources */, 7B883F49253D714C00879E62 /* SentryCrashUUIDConversion.c in Sources */, 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 */, @@ -4277,6 +4332,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 */, @@ -4297,6 +4353,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 */, @@ -4313,6 +4370,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 */, @@ -4385,6 +4443,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 */, @@ -4402,6 +4461,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 */, @@ -4547,6 +4607,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 */, @@ -4630,6 +4691,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 */, @@ -5226,7 +5288,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; @@ -5719,7 +5781,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; @@ -6274,7 +6336,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; @@ -6331,7 +6392,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; @@ -6385,7 +6445,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Test; @@ -6439,7 +6498,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = TestCI; @@ -6493,7 +6551,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -6547,7 +6604,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/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/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/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 diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index 0011cd04527..39f57279dfc 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -28,7 +28,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/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/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 6c2592d48fc..e1fc0b7ad81 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,7 +1,7 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" -#import "SentryReplayOptions.h" +#import "SentrySwift.h" #import #import #import @@ -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/SentryClient.m b/Sources/Sentry/SentryClient.m index 8d684abec59..f777f42fbb7 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -504,8 +504,9 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent return; } - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] - items:@[ videoEnvelopeItem ]]; + SentryEnvelope *envelope = [[SentryEnvelope alloc] + initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] + items:@[ videoEnvelopeItem ]]; [self captureEnvelope:envelope]; } diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m new file mode 100644 index 00000000000..56bb3816299 --- /dev/null +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -0,0 +1,18 @@ +#import "SentryCoreGraphicsHelper.h" +#if SENTRY_HAS_UIKIT +@implementation SentryCoreGraphicsHelper ++ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ +# 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 // defined(__IPHONE_16_0) +# endif // (TARGET_OS_IOS || TARGET_OS_TV) + return path; +} +@end +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index f62964fbd5c..669e94ae721 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -40,7 +40,7 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ + (long)millisecondsSince1970:(NSDate *)date { - return (NSInteger)([date timeIntervalSince1970] * 1000); + return (long)([date timeIntervalSince1970] * 1000); } @end 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 151d523ebad..151e6a981dc 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -15,15 +15,15 @@ #import "SentryOptions+Private.h" #import "SentrySDK.h" #import "SentryScope.h" +#import "SentrySessionReplayIntegration.h" +#import "SentrySwift.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" @@ -58,6 +58,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]), @@ -103,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 @@ -401,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"] @@ -503,6 +499,10 @@ - (BOOL)validateOptions:(NSDictionary *)options self.spotlightUrl = options[@"spotlightUrl"]; } + if ([options[@"experimental"] isKindOfClass:NSDictionary.class]) { + [self.experimental validateOptions:options[@"experimental"]]; + } + return YES; } @@ -743,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/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m index 88899b97c69..c5b28c8485c 100644 --- a/Sources/Sentry/SentryReplayEvent.m +++ b/Sources/Sentry/SentryReplayEvent.m @@ -26,12 +26,12 @@ - (NSDictionary *)serialize } result[@"urls"] = self.urls; - result[@"replay_start_timestamp"] = - @([SentryDateUtil millisecondsSince1970: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/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/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m new file mode 100644 index 00000000000..d6830ed4f19 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.m @@ -0,0 +1,276 @@ +#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 + +NS_ASSUME_NONNULL_BEGIN + +@interface +SentrySessionReplay () + +@property (nonatomic) BOOL isRunning; + +@property (nonatomic) BOOL isFullSession; + +@end + +@implementation SentrySessionReplay { + NSURL *_urlToCache; + UIView *_rootView; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSDate *_sessionStart; + NSMutableArray *imageCollection; + SentryId *sessionReplayId; + SentryReplayOptions *_replayOptions; + SentryOnDemandReplay *_replayMaker; + SentryDisplayLinkWrapper *_displayLink; + SentryCurrentDateProvider *_dateProvider; + id _sentryRandom; + id _screenshotProvider; + int _currentSegmentId; + BOOL _processingScreenshot; + BOOL _reachedMaximumDuration; +} + +- (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 = NO; + _urlToCache = folderPath; + _replayMaker = replayMaker; + _reachedMaximumDuration = NO; + } + 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; + } + + if (_isRunning) { + return; + } + + @synchronized(self) { + if (_isRunning) { + return; + } + [_displayLink linkWithTarget:self selector:@selector(newFrame:)]; + _isRunning = YES; + } + + _rootView = rootView; + _lastScreenShot = _dateProvider.date; + _videoSegmentStart = nil; + _currentSegmentId = 0; + sessionReplayId = [[SentryId alloc] init]; + + imageCollection = [NSMutableArray array]; + if (full) { + [self startFullReplay]; + } +} + +- (void)startFullReplay +{ + _sessionStart = _lastScreenShot; + _isFullSession = YES; +} + +- (void)stop +{ + @synchronized(self) { + [_displayLink invalidate]; + _isRunning = NO; + } +} + +- (void)captureReplayForEvent:(SentryEvent *)event; +{ + if (_isFullSession || !_isRunning) { + return; + } + + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { + return; + } + + if ([_sentryRandom nextNumber] > _replayOptions.errorSampleRate) { + return; + } + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + NSDate *replayStart = + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + + [self createAndCapture:finalPath + duration:_replayOptions.errorReplayDuration + startedAt:replayStart]; + + [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; + + if (_videoSegmentStart == nil) { + _videoSegmentStart = now; + } else if (_isFullSession && + [now timeIntervalSinceDate:_videoSegmentStart] + >= _replayOptions.sessionSegmentDuration) { + [self prepareSegmentUntil:now]; + } + } +} + +- (void)prepareSegmentUntil:(NSDate *)date +{ + 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:@"%i.mp4", _currentSegmentId]]; + + NSDate *segmentStart = + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + + [self createAndCapture:pathToSegment + duration:_replayOptions.sessionSegmentDuration + startedAt:segmentStart]; +} + +- (void)createAndCapture:(NSURL *)videoUrl + duration:(NSTimeInterval)duration + startedAt:(NSDate *)start +{ + [_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]; + + NSError *error; + if (![NSFileManager.defaultManager removeItemAtURL:videoInfo.path error:&error]) { + SENTRY_LOG_ERROR(@"Cound not delete replay segment from disk: %@", error); + } +} + +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } + _processingScreenshot = YES; + } + + UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions]; + + _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.m b/Sources/Sentry/SentrySessionReplayIntegration.m new file mode 100644 index 00000000000..3664c37e058 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -0,0 +1,129 @@ +#import "SentrySessionReplayIntegration.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 + +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 +{ + if ([super installWithOptions:options] == NO) { + return NO; + } + + if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryReplayOptions *replayOptions = options.experimental.sessionReplay; + + BOOL shouldReplayFullSession = + [self shouldReplayFullSession:replayOptions.sessionSampleRate]; + + if (!shouldReplayFullSession && replayOptions.errorSampleRate == 0) { + 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 = replayOptions.replayBitRate; + replayMaker.cacheMaxSize + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + : replayOptions.errorReplayDuration); + + self.sessionReplay = [[SentrySessionReplay alloc] + initWithSettings:replayOptions + 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 + fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [self.sessionReplay captureReplayForEvent:event]; + return event; + }]; + + return YES; + } else { + return NO; + } +} + +- (void)stop +{ + [self.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/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/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h new file mode 100644 index 00000000000..e561984de1b --- /dev/null +++ b/Sources/Sentry/include/SentryCoreGraphicsHelper.h @@ -0,0 +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 diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 9a17ee9c8d8..48a58c45403 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,7 +1,11 @@ // Sentry internal headers that are needed for swift code #import "SentryBaggage.h" -#import "SentryBaseIntegration.h" +#import "SentryCoreGraphicsHelper.h" +#import "SentryFileManager.h" +#import "SentryGlobalEventProcessor.h" #import "SentryRandom.h" +#import "SentryReplayRecording.h" +#import "SentryReplayType.h" #import "SentrySdkInfo.h" #import "SentryTime.h" 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 */ 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/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h new file mode 100644 index 00000000000..953f11fdf81 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -0,0 +1,65 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +# import + +@class SentryReplayOptions; +@class SentryEvent; +@class SentryCurrentDateProvider; +@class SentryDisplayLinkWrapper; +@class SentryVideoInfo; + +@protocol SentryRandom; +@protocol SentryRedactOptions; + +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 options:(id)options; +@end + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface SentrySessionReplay : NSObject + +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + 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. + * 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; + +/** + * Captures a replay for given event. + */ +- (void)captureReplayForEvent:(SentryEvent *)event; + +@end + +NS_ASSUME_NONNULL_END +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h new file mode 100644 index 00000000000..4500aeaa3d9 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -0,0 +1,12 @@ +#import "SentryBaseIntegration.h" +#import "SentryDefines.h" +#import "SentrySwift.h" +#import + +NS_ASSUME_NONNULL_BEGIN +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +@interface SentrySessionReplayIntegration : SentryBaseIntegration + +@end +#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION +NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift new file mode 100644 index 00000000000..8c069b3c540 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -0,0 +1,188 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +@_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 + } +} + +enum SentryOnDemandReplayError: Error { + case cantReadVideoSize +} + +@available(iOS 16.0, tvOS 16.0, *) +@objcMembers +class SentryOnDemandReplay: NSObject { + private let _outputPath: String + private let _onDemandDispatchQueue: DispatchQueue + + private var _starttime = Date() + private var _frames = [SentryReplayFrame]() + private var _currentPixelBuffer: SentryPixelBuffer? + + var videoWidth = 200 + var videoHeight = 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") + 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 { + 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) + + var frameCount = 0 + let (frames, start, end) = filterFrames(beginning: beginning, end: beginning.addingTimeInterval(duration)) + + if frames.isEmpty { return } + + _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight)) + + videoWriterInput.requestMediaDataWhenReady(on: _onDemandDispatchQueue) { [weak self] in + guard let self = self else { return } + + if frameCount < frames.count { + let imagePath = frames[frameCount] + + if let image = UIImage(contentsOfFile: imagePath) { + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) + guard self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) == true else { + completion(nil, videoWriter.error) + videoWriterInput.markAsFinished() + return + } + } + frameCount += 1 + } else { + videoWriterInput.markAsFinished() + videoWriter.finishWriting { + var videoInfo: SentryVideoInfo? + if videoWriter.status == .completed { + 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) + } + } + } + } + + 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, + AVVideoWidthKey: videoWidth, + AVVideoHeightKey: videoHeight, + AVVideoCompressionPropertiesKey: [ + AVVideoAverageBitRateKey: bitRate, + AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel + ] as [String: Any] + ] + } +} + +#endif // os(iOS) || os(tvOS) +#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..264e2b5c056 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -0,0 +1,49 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +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 = 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) + } +} +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift new file mode 100644 index 00000000000..ce0f3fcfb32 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -0,0 +1,101 @@ +import Foundation + +@objcMembers +public class SentryReplayOptions: NSObject, SentryRedactOptions { + /** + * Indicates the percentage in which the replay for the session will be created. + * - 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 0. + */ + public var sessionSampleRate: Float + + /** + * Indicates the percentage in which a 30 seconds replay will be send with error events. + * - 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 0. + */ + 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. + * @note The default value is @c 20000; + */ + let replayBitRate = 20_000 + + /** + * Number of frames per second of the replay. + * The more the havier the process is. + */ + 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) + + /** + * The maximum duration of the segment of a session replay. + */ + 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.sessionSampleRate = 0 + self.errorSampleRate = 0 + } + + /** + * 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 = 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: [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/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift new file mode 100644 index 00000000000..2d7518f9e7b --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -0,0 +1,28 @@ +import Foundation + +@objcMembers +class SentryVideoInfo: NSObject { + + let path: URL + let height: Int + let width: Int + let duration: TimeInterval + let frameCount: Int + let frameRate: Int + let start: Date + let end: Date + let fileSize: Int + + 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 + self.frameCount = frameCount + self.frameRate = frameRate + self.start = start + self.end = end + self.path = path + self.fileSize = fileSize + } + +} 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/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift new file mode 100644 index 00000000000..36667e4dec2 --- /dev/null +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -0,0 +1,115 @@ +#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) +@available(tvOS, introduced: 16.0) +@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() + + override init() { +#if os(iOS) + ignoreClasses = [ UISlider.self, UISwitch.self ] +#endif // os(iOS) + redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ + "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" + ].compactMap { NSClassFromString($0) } + } + + @objc(imageWithView:options:) + func image(view: UIView, options: SentryRedactOptions) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) + + defer { + UIGraphicsEndImageContext() + } + + guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } + + view.layer.render(in: 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, options: SentryRedactOptions?) { + UIColor.black.setFill() + let maskPath = self.buildPath(view: view, + path: CGMutablePath(), + area: view.frame, + redactText: options?.redactAllText ?? true, + redactImage: options?.redactAllImages ?? true) + context.addPath(maskPath) + context.fillPath() + } + + private func shouldIgnore(view: UIView) -> Bool { + ignoreClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(view: UIView) -> Bool { + 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, redactText: Bool, redactImage: Bool) -> CGMutablePath { + let rectInWindow = view.convert(view.bounds, to: nil) + + if (!redactImage && !redactText) || !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { + return path + } + + var result = path + + let ignore = shouldIgnore(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) { + result = SentryCoreGraphicsHelper.excludeRect(rectInWindow, from: result).takeRetainedValue() + } + + if !ignore { + for subview in view.subviews { + result = buildPath(view: subview, path: path, area: area, redactText: redactText, redactImage: redactImage) + } + } + + return result + } + + private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { + return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) + } +} + +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift index 96391581a46..00dc0162ab8 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift @@ -13,14 +13,14 @@ class SentryReplayEventTests: XCTestCase { sut.traceIds = traceIds let replayId = SentryId() - sut.replayId = replayId + sut.eventId = 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["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 diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift new file mode 100644 index 00000000000..57af570ad3f --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -0,0 +1,73 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) + +class SentrySessionReplayIntegrationTests: XCTestCase { + + override func setUpWithError() throws { + guard #available(iOS 16.0, tvOS 16.0, *) else { + throw XCTSkip("iOS version not supported") + } + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + 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() { + startSDK(sessionSampleRate: 1, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testNoInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.3) + + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 + expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + } + + func testInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.1) + + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testInstallErrorReplay() { + startSDK(sessionSampleRate: 0, errorSampleRate: 0.1) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } +} + +#endif diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift new file mode 100644 index 00000000000..1925b4eb2e9 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -0,0 +1,210 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) +class SentrySessionReplayTests: XCTestCase { + + private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { + func image(with view: UIView, options: SentryRedactOptions) -> UIImage { UIImage.add } + } + + private class TestReplayMaker: NSObject, SentryReplayMaker { + struct CreateVideoCall { + var duration: TimeInterval + var beginning: Date + var outputFileURL: URL + var completion: ((Sentry.SentryVideoInfo?, Error?) -> Void) + } + + var lastCallToCreateVideo: CreateVideoCall? + func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, 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 + } + } + + @available(iOS 16.0, tvOS 16.0, *) + 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 { + guard #available(iOS 16.0, tvOS 16.0, *) else { + throw XCTSkip("iOS version not supported") + } + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + @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) + + fixture.dateProvider.advance(by: 1) + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(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) + + 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/0.mp4") + + 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) + + 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) + } + + @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) + + let event = Event(error: NSError(domain: "Some error", code: 1)) + + sut.capture(for: event) + 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 + } +} + +#endif 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); } } diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 3a0a234efd5..7a8808f2105 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -12,6 +12,8 @@ #if SENTRY_HAS_UIKIT # import "MockUIScene.h" # import "SentryFramesTracker+TestInit.h" +# import "SentrySessionReplay.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIApplication+Private.h" # import "SentryUIApplication.h" # import "SentryUIDeviceWrapper.h" @@ -163,8 +165,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 +234,4 @@ #import "TestSentrySpan.h" #import "TestSentryViewHierarchy.h" #import "URLSessionTaskMock.h" +@import _SentryPrivate; 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 0e077873316fe8cbe492c0e3981e8e1194f419f6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 10 Apr 2024 15:21:34 +0200 Subject: [PATCH 14/14] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75598e4d8fe..14d85e5246c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add timing API for Metrics (#3812): - Add [rate limiting](https://develop.sentry.dev/sdk/rate-limiting/) for Metrics (#3838) - Data normalization for Metrics (#3843) +- Add Session Replay (#3625) ## 8.23.0 @@ -78,7 +79,6 @@ CocoaPods, which you shouldn't have included directly. ### Features -- Add Session Replay (#3625) - Add support for Sentry [Spotlight](https://spotlightjs.com/) (#3642), which is basically Sentry for development. Read our [blog post](https://blog.sentry.io/sentry-for-development/) to find out more. - Add field `SentrySDK.detectedStartUpCrash` (#3644)