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 }