diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f44651bfc3..9095eb4b2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,41 @@ > This release promotes Metrics out of experimental and **removes** `options.experimental.enableMetrics` and `options.experimental.beforeSendMetric`. If you set either of these, your app will fail to compile after upgrading. > Migrate to `options.enableMetrics` and `options.beforeSendMetric` (top-level on `Options`) — the defaults and behavior are unchanged. +> [!WARNING] +> Session Replay now runs on iOS 26 with Liquid Glass. Previously, the SDK auto-disabled Session Replay on iOS 26 builds that adopted Liquid Glass (built with Xcode 26+ and `UIDesignRequiresCompatibility` not set). Now that the underlying redaction issues have been addressed, replays will be captured in those environments — verify your masking still meets your privacy requirements. +> If you want to keep Session Replay disabled on Liquid Glass builds, you will need to gate the sample rates yourself. For reference, see how the SDK used to perform the check: https://github.com/getsentry/sentry-cocoa/blob/adef457c1344e5efda4b74a1f33913d4d49ef7e0/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift +> +> Example — disable Session Replay when Liquid Glass is active: +> +> ```swift +> import Sentry +> +> SentrySDK.start { options in +> options.dsn = "___PUBLIC_DSN___" +> +> var sessionRate: Float = 0.2 +> var errorRate: Float = 1.0 +> +> if #available(iOS 26.0, *) { +> let compatibilityMode = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool ?? false +> let xcodeVersion = Int(Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String ?? "") ?? 0 +> let builtWithXcode26OrLater = xcodeVersion >= 2600 +> let liquidGlassActive = builtWithXcode26OrLater && !compatibilityMode +> if liquidGlassActive { +> sessionRate = 0 +> errorRate = 0 +> } +> } +> +> options.sessionReplay.sessionSampleRate = sessionRate +> options.sessionReplay.onErrorSampleRate = errorRate +> } +> ``` + +### Improvements + +- Remove `enableSessionReplayInUnreliableEnvironment` experimental option and the environment checker that temporarily disabled Session Replay on iOS 26 (#7831) + ### Features - Make feature Metrics generally available, moving experimental options to top-level options (#7843) diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift index d1e73d5b3a5..c124dcb458a 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift @@ -110,8 +110,6 @@ public enum SentrySDKOverrides: String, CaseIterable { case disableMaskAllImages = "--io.sentry.session-replay.disable-mask-all-images" case disableMaskAllText = "--io.sentry.session-replay.disable-mask-all-text" - - case enableInUnreliableEnvironment = "--io.sentry.session-replay.enable-in-unreliable-environment" } case sessionReplay = "Session Replay" @@ -323,7 +321,7 @@ extension SentrySDKOverrides.Performance { extension SentrySDKOverrides.SessionReplay { public var overrideType: OverrideType { switch self { - case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .enableInUnreliableEnvironment: return .boolean + case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages: return .boolean case .onErrorSampleRate, .sessionSampleRate: return .float case .quality: return .string } @@ -420,7 +418,7 @@ extension SentrySDKOverrides.SessionReplay { public var ignoresDisableEverything: Bool { switch self { case .disable: return false - case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality, .enableInUnreliableEnvironment: return true + case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality: return true } } } diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index d8aa538f1b7..367119d2012 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -87,10 +87,6 @@ public struct SentrySDKWrapper { ] let defaultReplayQuality = options.sessionReplay.quality options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality(rawValue: (SentrySDKOverrides.SessionReplay.quality.stringValue as? NSString)?.integerValue ?? defaultReplayQuality.rawValue) ?? defaultReplayQuality - - // Allow configuring unreliable environment protection via SDK override. - // Default to false for the sample app to allow testing on iOS 26+ with Liquid Glass. - options.experimental.enableSessionReplayInUnreliableEnvironment = SentrySDKOverrides.SessionReplay.enableInUnreliableEnvironment.boolValue } #endif // !os(macOS) && !os(watchOS) && !os(visionOS) diff --git a/Samples/Shared/feature-flags.yml b/Samples/Shared/feature-flags.yml index aee6be986e6..937e81f33a8 100644 --- a/Samples/Shared/feature-flags.yml +++ b/Samples/Shared/feature-flags.yml @@ -22,7 +22,6 @@ schemeTemplates: "--io.sentry.session-replay.enable-fast-view-rendering": false "--io.sentry.session-replay.disable-mask-all-images": false "--io.sentry.session-replay.disable-mask-all-text": false - "--io.sentry.session-replay.enable-in-unreliable-environment": false # user feedback "--io.sentry.feedback.use-custom-feedback-button": false diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 834c69f6bed..75824e79874 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -401,7 +401,6 @@ D4CBA2472DE06D0200581618 /* libSentryTestUtils.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8431F00A29B284F200D8DC56 /* libSentryTestUtils.a */; }; D4CBA2532DE06D1600581618 /* TestConstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CBA2512DE06D1600581618 /* TestConstantTests.swift */; }; D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; - D4E9420C2E9D1D8000DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */; }; D4ECA4012E3CBEDE00C757EA /* SentryDummyPublicEmptyClass.m in Sources */ = {isa = PBXBuildFile; fileRef = D4ECA4002E3CBEDE00C757EA /* SentryDummyPublicEmptyClass.m */; }; D4ECA4022E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m in Sources */ = {isa = PBXBuildFile; fileRef = D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */; }; D4EE12D22DE9AC3800385BAF /* TestNSNotificationCenterWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */; }; @@ -1054,7 +1053,6 @@ D4CBA2432DE06D0200581618 /* SentryTestUtilsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SentryTestUtilsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D4CBA2512DE06D1600581618 /* TestConstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstantTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; - D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDummyPrivateEmptyClass.m; sourceTree = ""; }; D4ECA4002E3CBEDE00C757EA /* SentryDummyPublicEmptyClass.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDummyPublicEmptyClass.m; sourceTree = ""; }; D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSNotificationCenterWrapperTests.swift; sourceTree = ""; }; @@ -2106,7 +2104,6 @@ D43B0E5F2DE7416600EE3759 /* TestFileManagerTests.swift */, D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */, D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */, - D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */, ); path = Sources; sourceTree = ""; @@ -3120,7 +3117,6 @@ D44B16722DE464AD006DBDB3 /* TestDispatchFactoryTests.swift in Sources */, D4EE12D22DE9AC3800385BAF /* TestNSNotificationCenterWrapperTests.swift in Sources */, 62E75EB92E152953002EC91B /* InvocationsTests.swift in Sources */, - D4E9420C2E9D1D8000DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */, D4599F8F2E99113E0045BB95 /* TestInfoPlistWrapperTests.swift in Sources */, D4CBA2532DE06D1600581618 /* TestConstantTests.swift in Sources */, ); diff --git a/SentryTestUtils/Sources/TestSessionReplayEnvironmentChecker.swift b/SentryTestUtils/Sources/TestSessionReplayEnvironmentChecker.swift deleted file mode 100644 index 95bf23abb32..00000000000 --- a/SentryTestUtils/Sources/TestSessionReplayEnvironmentChecker.swift +++ /dev/null @@ -1,22 +0,0 @@ -@_spi(Private) @testable import Sentry - -@_spi(Private) public class TestSessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider { - - public var isReliableInvocations = Invocations() - private var mockedIsReliableReturnValue: Bool - - public init( - mockedIsReliableReturnValue: Bool - ) { - self.mockedIsReliableReturnValue = mockedIsReliableReturnValue - } - - public func isReliable() -> Bool { - isReliableInvocations.record(()) - return mockedIsReliableReturnValue - } - - public func mockIsReliableReturnValue(_ returnValue: Bool) { - mockedIsReliableReturnValue = returnValue - } -} diff --git a/SentryTestUtilsTests/Sources/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtilsTests/Sources/TestSessionReplayEnvironmentCheckerTests.swift deleted file mode 100644 index 96949c7ad5f..00000000000 --- a/SentryTestUtilsTests/Sources/TestSessionReplayEnvironmentCheckerTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -@_spi(Private) @testable import Sentry -@_spi(Private) @testable import SentryTestUtils -import XCTest - -class TestSessionReplayEnvironmentCheckerTests: XCTestCase { - - // MARK: - isReliable() - - func testIsReliable_withoutMockedValue_shouldReturnDefaultValue() throws { - // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker( - mockedIsReliableReturnValue: true - ) - - // -- Act -- - let result = sut.isReliable() - - // -- Assert -- - XCTAssertTrue(result, "isReliable() should return the same value as the one mocked") - } - - func testIsReliable_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { - // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker( - mockedIsReliableReturnValue: false - ) - sut.mockIsReliableReturnValue(true) - - // -- Act -- - let result = sut.isReliable() - - // -- Assert -- - XCTAssertTrue(result, "isReliable() should return the same value as the one mocked") - } - - func testIsReliable_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws { - // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker( - mockedIsReliableReturnValue: false - ) - sut.mockIsReliableReturnValue(true) - - // -- Act -- - let result1 = sut.isReliable() - let result2 = sut.isReliable() - - // -- Assert -- - XCTAssertTrue(result1) - XCTAssertTrue(result2) - } - - func testIsReliable_shouldRecordInvocations() throws { - // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker( - mockedIsReliableReturnValue: false - ) - sut.mockIsReliableReturnValue(true) - - // -- Act -- - _ = sut.isReliable() - _ = sut.isReliable() - _ = sut.isReliable() - - // -- Assert -- - XCTAssertEqual(sut.isReliableInvocations.count, 3) - } -} diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index 5682e77ee84..0b6eca7a356 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -60,15 +60,6 @@ - (void)start SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false SentryOptions, SentrySDKInternal.currentHub.client.options); SentryDependencyContainer *sharedContainer = [SentryDependencyContainer sharedInstance]; - if (![SentrySessionReplay - shouldEnableSessionReplayWithEnvironmentChecker: - [sharedContainer sessionReplayEnvironmentChecker] - experimentalOptions:currentOptions - .experimental]) { - SENTRY_LOG_ERROR(@"[Session Replay] Session replay is disabled due to " - @"environment potentially causing PII leaks."); - return; - } SENTRY_LOG_DEBUG(@"[Session Replay] Initializing replay integration"); replayIntegration = diff --git a/Sources/Swift/Helper/Dependencies.swift b/Sources/Swift/Helper/Dependencies.swift index 388598fcd6d..1d76c3a7b67 100644 --- a/Sources/Swift/Helper/Dependencies.swift +++ b/Sources/Swift/Helper/Dependencies.swift @@ -6,9 +6,6 @@ @objc public static let threadWrapper = SentryThreadWrapper() @objc public static let processInfoWrapper: SentryProcessInfoSource = ProcessInfo.processInfo static let infoPlistWrapper: SentryInfoPlistWrapperProvider = SentryInfoPlistWrapper() - @objc public static let sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentChecker = { - SentrySessionReplayEnvironmentChecker(infoPlistWrapper: Dependencies.infoPlistWrapper) - }() @objc public static let dispatchQueueWrapper = SentryDispatchQueueWrapper() @objc public static let notificationCenterWrapper: SentryNSNotificationCenterWrapper = NotificationCenter.default @objc public static let binaryImageCache = SentryBinaryImageCache() diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift index f13ca9606ab..de6a954ba6e 100644 --- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift +++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift @@ -1,18 +1,4 @@ enum SentryInfoPlistKey: String { - /// Key used to set the Xcode version used to build app - case xcodeVersion = "DTXcode" - - /// A Boolean value that indicates whether the system runs the app using a compatibility mode for UI. - /// - /// If `YES`, the system runs the app using a compatibility mode for UI elements. The compatibility mode displays the app as it looks when built against previous versions of the SDKs. - /// - /// If `NO`, the system uses the UI design of the running OS, with no compatibility mode. Absence of the key, or NO, is the default value for apps linking against the latest SDKs. - /// - /// - Warning: This key is used temporarily while reviewing and refining an app's UI for the design in the latest SDKs (i.e. Liquid Glass). - /// - /// - SeeAlso: [Apple Documentation](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIDesignRequiresCompatibility) - case designRequiresCompatibility = "UIDesignRequiresCompatibility" - /// The extension configuration dictionary for app extensions /// /// - SeeAlso: [Apple Documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsextension) diff --git a/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift b/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift deleted file mode 100644 index f745575eacc..00000000000 --- a/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift +++ /dev/null @@ -1,4 +0,0 @@ -enum SentryXcodeVersion: Int { - case xcode16_4 = 1_640 - case xcode26 = 2_600 -} diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 68e81626383..cfab32c157c 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -69,23 +69,6 @@ import UIKit deinit { displayLink.invalidate() } - @objc - static public func shouldEnableSessionReplay(environmentChecker: SentrySessionReplayEnvironmentCheckerProvider, experimentalOptions: SentryExperimentalOptions) -> Bool { - // Detect if we are running on iOS 26.0 with Liquid Glass and disable session replay. - // This needs to be done until masking for session replay is properly supported, as it can lead - // to PII leaks otherwise. - if environmentChecker.isReliable() { - return true - } - guard experimentalOptions.enableSessionReplayInUnreliableEnvironment else { - SentrySDKLog.fatal("[Session Replay] Detected environment potentially causing PII leaks, disabling Session Replay. To override this mechanism, set `options.experimental.enableSessionReplayInUnreliableEnvironment` to `true`") - return false - } - SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.experimental.enableSessionReplayInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") - - return true - } - public func start(rootView: UIView?, fullSession: Bool) { SentrySDKLog.debug("[Session Replay] Starting session replay with full session: \(fullSession)") guard !isRunning else { diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift deleted file mode 100644 index 0c8b8ef2569..00000000000 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift +++ /dev/null @@ -1,115 +0,0 @@ -// swiftlint:disable missing_docs -@objc @_spi(Private) public class SentrySessionReplayEnvironmentChecker: NSObject, SentrySessionReplayEnvironmentCheckerProvider { - /// Represents the reliability assessment of the environment for Session Replay. - private enum Reliability { - /// The environment is confirmed to be reliable (no Liquid Glass issues). - case reliable - /// The environment is confirmed to be unreliable (Liquid Glass will cause issues). - case unreliable - /// Unable to determine reliability (missing data, errors, etc.). - /// Treated as unreliable defensively. - case unclear - } - - private let infoPlistWrapper: SentryInfoPlistWrapperProvider - - init(infoPlistWrapper: SentryInfoPlistWrapperProvider) { - self.infoPlistWrapper = infoPlistWrapper - super.init() - } - - // swiftlint:disable:next cyclomatic_complexity function_body_length - public func isReliable() -> Bool { - // Defensive programming: Assume unreliable environment by default on iOS 26.0+ - // and only mark as safe if we have explicit proof it's not using Liquid Glass. - // - // Liquid Glass introduces changes to text rendering that breaks masking in Session Replay. - // It's used on iOS 26.0+ UNLESS one of these conditions is met: - // 1. UIDesignRequiresCompatibility is explicitly set to YES in Info.plist - // 2. The app was built with Xcode < 26.0 (DTXcode < 2600) - - // Run all checks and return true (reliable) if ANY check confirms reliability - if checkIOSVersion() == .reliable { - return true - } - if checkCompatibilityMode() == .reliable { - return true - } - if checkXcodeVersion() == .reliable { - return true - } - - // No proof of reliability found - treat as unreliable (defensively) - SentrySDKLog.warning("[Session Replay] Detected environment as unreliable - no proof of reliability found") - return false - } - - private func checkIOSVersion() -> Reliability { - guard #available(iOS 26.0, *) else { - SentrySDKLog.debug("[Session Replay] Running on iOS version prior to 26.0+ - reliable") - return .reliable - } - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+") - return .unclear - } - - private func checkCompatibilityMode() -> Reliability { - do { - var error: NSError? - let isRequired = infoPlistWrapper.getAppValueBoolean( - for: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - errorPtr: &error - ) - if let error = error as Error? { - throw error - } - if isRequired { - SentrySDKLog.debug("[Session Replay] UIDesignRequiresCompatibility = YES - reliable") - return .reliable - } - - SentrySDKLog.debug("[Session Replay] UIDesignRequiresCompatibility = NO - unreliable") - return .unreliable - } catch SentryInfoPlistError.mainInfoPlistNotFound { - SentrySDKLog.warning("[Session Replay] Info.plist not found - unclear") - return .unclear - } catch SentryInfoPlistError.keyNotFound { - // Key not found means the default behavior applies (no compatibility mode) - SentrySDKLog.debug("[Session Replay] UIDesignRequiresCompatibility not set - unclear") - return .unclear - } catch { - SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error) - unclear") - return .unclear - } - } - - private func checkXcodeVersion() -> Reliability { - do { - // DTXcode format: Xcode 16.4 = "1640", Xcode 26.0 = "2600" - let xcodeVersionString = try infoPlistWrapper.getAppValueString( - for: SentryInfoPlistKey.xcodeVersion.rawValue - ) - guard let xcodeVersion = Int(xcodeVersionString) else { - SentrySDKLog.warning("[Session Replay] DTXcode value '\(xcodeVersionString)' is not a valid integer - unclear") - return .unclear - } - if xcodeVersion >= SentryXcodeVersion.xcode26.rawValue { - SentrySDKLog.debug("[Session Replay] Built with Xcode \(xcodeVersionString) (>= 26.0) - unreliable") - return .unreliable - } - - SentrySDKLog.debug("[Session Replay] Built with Xcode \(xcodeVersionString) (< 26.0) - reliable") - return .reliable - } catch SentryInfoPlistError.mainInfoPlistNotFound { - SentrySDKLog.warning("[Session Replay] Info.plist not found - unclear") - return .unclear - } catch SentryInfoPlistError.keyNotFound { - SentrySDKLog.debug("[Session Replay] DTXcode not found in Info.plist - unclear") - return .unclear - } catch { - SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error) - unclear") - return .unclear - } - } -} -// swiftlint:enable missing_docs diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift deleted file mode 100644 index 2138e5797d8..00000000000 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift +++ /dev/null @@ -1,8 +0,0 @@ -// swiftlint:disable missing_docs -@_spi(Private) @objc public protocol SentrySessionReplayEnvironmentCheckerProvider { - /// Checks if the runtime environment is considered unreliable with regards to Session Replay masking. - /// - /// - Returns: `true` if reliable, otherwise `false` - func isReliable() -> Bool -} -// swiftlint:enable missing_docs diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift index 24f5aaacac5..ff63922040d 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift @@ -4,7 +4,7 @@ #if (os(iOS) || os(tvOS)) && !SENTRY_NO_UI_FRAMEWORK import UIKit -typealias SessionReplayIntegrationScope = SessionReplayEnvironmentCheckerProvider & NotificationCenterProvider & RateLimitsProvider & CurrentDateProvider & RandomProvider & FileManagerProvider & CrashWrapperProvider & ReachabilityProvider & GlobalEventProcessorProvider & DispatchQueueWrapperProvider & ApplicationProvider & DispatchFactoryProvider +typealias SessionReplayIntegrationScope = NotificationCenterProvider & RateLimitsProvider & CurrentDateProvider & RandomProvider & FileManagerProvider & CrashWrapperProvider & ReachabilityProvider & GlobalEventProcessorProvider & DispatchQueueWrapperProvider & ApplicationProvider & DispatchFactoryProvider // This is static because it will be used for swizzling and would cause retain cycles private var touchTracker: SentryTouchTracker? @@ -44,14 +44,6 @@ public class SentrySessionReplayIntegration: NSObject, SwiftIntegration, SentryS // MARK: - Initialization required convenience init?(with options: Options, dependencies: SentryDependencyContainer) { - guard SentrySessionReplay.shouldEnableSessionReplay( - environmentChecker: dependencies.sessionReplayEnvironmentChecker, - experimentalOptions: options.experimental - ) else { - SentrySDKLog.debug("Not going to enable SentrySessionReplayIntegration because environment check failed.") - return nil - } - guard options.sessionReplay.sessionSampleRate > 0 || options.sessionReplay.onErrorSampleRate > 0 else { SentrySDKLog.debug("Not going to enable SentrySessionReplayIntegration because sample rates are 0.") return nil diff --git a/Sources/Swift/SentryDependencyContainer.swift b/Sources/Swift/SentryDependencyContainer.swift index fc468d1e179..d4a21fdd2b6 100644 --- a/Sources/Swift/SentryDependencyContainer.swift +++ b/Sources/Swift/SentryDependencyContainer.swift @@ -133,7 +133,6 @@ extension SentryFileManager: SentryFileManagerProtocol { } currentDateProvider: Dependencies.dateProvider) @objc public var reachability = SentryReachability() @objc public var sysctlWrapper = Dependencies.sysctlWrapper - @objc public var sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider = Dependencies.sessionReplayEnvironmentChecker @objc public var debugImageProvider = Dependencies.debugImageProvider @objc public var objcRuntimeWrapper: SentryObjCRuntimeWrapper = SentryDefaultObjCRuntimeWrapper() var extensionDetector: SentryExtensionDetector = { @@ -460,11 +459,6 @@ protocol ViewHierarchyProviderProvider { extension SentryDependencyContainer: ViewHierarchyProviderProvider { } #endif -protocol SessionReplayEnvironmentCheckerProvider { - var sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider { get } -} -extension SentryDependencyContainer: SessionReplayEnvironmentCheckerProvider {} - protocol NotificationCenterProvider { var notificationCenterWrapper: SentryNSNotificationCenterWrapper { get } } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 611c36dbd4b..0bcbf70b88d 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -13,23 +13,6 @@ public final class SentryExperimentalOptions: NSObject { */ public var enableUnhandledCPPExceptionsV2 = false - /** - * Forces enabling of session replay in unreliable environments. - * - * Due to internal changes with the release of Liquid Glass on iOS 26.0, the masking of text and images can not be reliably guaranteed. - * Therefore the SDK uses a defensive programming approach to disable the session replay integration by default, unless the environment is detected as reliable. - * - * Indicators for reliable environments include: - * - Running on an older version of iOS that doesn't have Liquid Glass (iOS 18 or earlier) - * - UIDesignRequiresCompatibility is explicitly set to YES in Info.plist - * - The app was built with Xcode < 26.0 (DTXcode < 2600) - * - * - Important: This flag allows to re-enable the session replay integration on iOS 26.0 and later, but please be aware that text and images may not be masked as expected. - * - * - Note: See [GitHub issues #6389](https://github.com/getsentry/sentry-cocoa/issues/6389) for more information. - */ - public var enableSessionReplayInUnreliableEnvironment = false - /// When enabled, the SDK uses a more efficient mechanism for detecting watchdog terminations. public var enableWatchdogTerminationsV2 = false diff --git a/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift deleted file mode 100644 index 35c8a3f4b2e..00000000000 --- a/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -@_spi(Private) @testable import Sentry -import XCTest - -class SentryInfoPlistKeyTests: XCTestCase { - func testXcodeVersion_shouldReturnExpectedConstant() { - XCTAssertEqual(SentryInfoPlistKey.xcodeVersion.rawValue, "DTXcode") - } - - func testDesignRequiresCompatibility_shouldReturnExpectedConstant() { - XCTAssertEqual(SentryInfoPlistKey.designRequiresCompatibility.rawValue, "UIDesignRequiresCompatibility") - } -} diff --git a/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift index 8838af7bb38..217da8057ff 100644 --- a/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift +++ b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift @@ -158,28 +158,23 @@ class SentryInfoPlistWrapperTests: XCTestCase { } func testGetAppValueString_withSentryInfoPlistKey_shouldWork() throws { - // Arrange - // Test with the actual enum keys used in production - let xcodeKey = SentryInfoPlistKey.xcodeVersion.rawValue - // Act - let value = try sut.getAppValueString(for: xcodeKey) + let value = try sut.getAppValueString(for: "TestStringKey") // Assert - XCTAssertEqual(value, "1610", "Should return the DTXcode value from test bundle") + XCTAssertEqual(value, "TestStringValue", "Should return the TestStringKey value from test bundle") } func testGetAppValueBoolean_withSentryInfoPlistKey_shouldWork() { // Arrange - let compatibilityKey = SentryInfoPlistKey.designRequiresCompatibility.rawValue var error: NSError? // Act - let value = sut.getAppValueBoolean(for: compatibilityKey, errorPtr: &error) + let value = sut.getAppValueBoolean(for: "TestBooleanFalse", errorPtr: &error) // Assert XCTAssertNil(error, "Should not have an error when reading a valid boolean") - XCTAssertFalse(value, "Should return false for UIDesignRequiresCompatibility key") + XCTAssertFalse(value, "Should return false for TestBooleanFalse key") } // MARK: - Multiple Consecutive Calls diff --git a/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift deleted file mode 100644 index 3a27f5801cc..00000000000 --- a/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -@_spi(Private) @testable import Sentry -import XCTest - -class SentryXcodeVersionTests: XCTestCase { - func testXcode16_4_shouldReturnExpectedVersion() { - XCTAssertEqual(SentryXcodeVersion.xcode16_4.rawValue, 1_640) - } - - func testXcode26_shouldReturnExpectedVersion() { - XCTAssertEqual(SentryXcodeVersion.xcode26.rawValue, 2_600) - } -} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift index 015a60c264e..62d0df198d1 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift @@ -17,8 +17,6 @@ class SentryReplayApiTests: XCTestCase { // Arrange let options = Options() options.sessionReplay.sessionSampleRate = 1.0 - // Ensure the integration will always be enabled - options.experimental.enableSessionReplayInUnreliableEnvironment = true let mockClient = TestClient(options: options) let mockReplayIntegration = try XCTUnwrap(MockSessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance())) let mockHub = TestHub(client: mockClient, andScope: Scope()) @@ -35,62 +33,6 @@ class SentryReplayApiTests: XCTestCase { XCTAssertTrue(mockReplayIntegration.startCalled) XCTAssertEqual(mockHub.installedIntegrations().count, 1) // No new integration added } - - func testStart_whenReplayIntegrationNilAndUnreliableToEnable_shouldNotCreateIntegration() { - // Arrange - let options = Options() - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = false - let mockClient = TestClient(options: options) - let mockHub = TestHub(client: mockClient, andScope: Scope()) - mockHub.removeAllIntegrations() - SentrySDKInternal.setCurrentHub(mockHub) - - let sut = SentryReplayApi() - - SentryDependencyContainer.sharedInstance().sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - - // Act - sut.start() - - // Assert - XCTAssertTrue(mockHub.installedIntegrations().isEmpty) - } - - func testStart_whenReplayIntegrationNilWithUnreliableEnvironmentAndOverrideOptionEnabled_shouldCreateAndInstallIntegration() throws { - // Arrange - let options = Options() - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = true - options.dsn = "https://user@test.com/test" - let mockClient = TestClient(options: options) - let mockHub = TestHub(client: mockClient, andScope: Scope()) - mockHub.removeAllIntegrations() - SentrySDKInternal.setCurrentHub(mockHub) - - let sut = SentryReplayApi() - - SentryDependencyContainer.sharedInstance().sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - - let dispatchQueue = TestSentryDispatchQueueWrapper() - SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueue - SentryDependencyContainer.sharedInstance().fileManager = try SentryFileManager( - options: options, - dateProvider: SentryDependencyContainer.sharedInstance().dateProvider, - dispatchQueueWrapper: dispatchQueue - ) - - // Act - sut.start() - - // Assert - XCTAssertEqual(mockHub.installedIntegrations().count, 1) - let integration = try XCTUnwrap(mockHub.installedIntegrations().first as? SentrySessionReplayIntegration) - XCTAssertNotNil(integration.sessionReplay) - XCTAssertTrue(integration.sessionReplay?.isRunning ?? false) - SentrySDKInternal.currentHub().endSession() - XCTAssertTrue(integration.sessionReplay?.isFullSession ?? false) - } } private class MockSessionReplayIntegration: SentrySessionReplayIntegration { diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift deleted file mode 100644 index dc422a1f82b..00000000000 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift +++ /dev/null @@ -1,407 +0,0 @@ -@_spi(Private) @testable import Sentry -@_spi(Private) import SentryTestUtils -import XCTest - -final class SentrySessionReplayEnvironmentCheckerTests: XCTestCase { - - private var infoPlistWrapper: TestInfoPlistWrapper! - private var sut: SentrySessionReplayEnvironmentChecker! - - override func setUp() { - super.setUp() - infoPlistWrapper = TestInfoPlistWrapper() - - // Set up default mocks to prevent precondition failures - // Individual tests can override these as needed - - // Default: compatibility mode not set (key not found = unclear) - infoPlistWrapper.mockGetAppValueBooleanThrowError( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.designRequiresCompatibility.rawValue) as NSError - ) - - // Default: Xcode version not set (key not found = unclear) - infoPlistWrapper.mockGetAppValueStringThrowError( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.xcodeVersion.rawValue) - ) - - sut = SentrySessionReplayEnvironmentChecker(infoPlistWrapper: infoPlistWrapper) - } - - override func tearDown() { - sut = nil - infoPlistWrapper = nil - super.tearDown() - } - - // MARK: - iOS Version Check Tests - - func testIsReliable_onIOSOlderThan26_returnsTrue() throws { - // iOS < 26.0 is always reliable (no Liquid Glass) - guard #unavailable(iOS 26.0) else { - throw XCTSkip("Test requires iOS < 26.0") - } - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "iOS < 26.0 should always be reliable") - } - - // MARK: - Compatibility Mode Tests (iOS 26+) - - func testIsReliable_onIOS26_withCompatibilityModeYES_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanReturnValue( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - value: true - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "UIDesignRequiresCompatibility = YES should make environment reliable") - } - - func testIsReliable_onIOS26_withCompatibilityModeNO_withOldXcode_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanReturnValue( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - value: false - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "\(SentryXcodeVersion.xcode16_4.rawValue)" // Xcode 16.4 < 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Xcode < 26.0 should make environment reliable even with compatibility mode NO") - } - - func testIsReliable_onIOS26_withCompatibilityModeNO_withNewXcode_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanReturnValue( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - value: false - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "\(SentryXcodeVersion.xcode26.rawValue)" // Xcode 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "iOS 26+ with compatibility mode NO and Xcode >= 26.0 should be unreliable") - } - - // MARK: - Xcode Version Tests (iOS 26+) - - func testIsReliable_onIOS26_withXcodeOlderThan26_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "\(SentryXcodeVersion.xcode16_4.rawValue)" // Xcode 16.4 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Xcode < 26.0 should make environment reliable") - } - - func testIsReliable_onIOS26_withXcode26OrNewer_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "\(SentryXcodeVersion.xcode26.rawValue)" // Xcode 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Xcode >= 26.0 on iOS 26+ should be unreliable") - } - - func testIsReliable_onIOS26_withInvalidXcodeVersion_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "invalid_version" - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Invalid Xcode version should be treated as unreliable (defensive)") - } - - func testIsReliable_onIOS26_withMissingXcodeVersion_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringThrowError( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.xcodeVersion.rawValue) - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Missing Xcode version should be treated as unreliable (defensive)") - } - - // MARK: - Compatibility Mode Error Handling Tests (iOS 26+) - - func testIsReliable_onIOS26_withMissingCompatibilityKey_withOldXcode_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanThrowError( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.designRequiresCompatibility.rawValue) as NSError - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "1640" // Xcode 16.4 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Old Xcode version should make it reliable even without compatibility key") - } - - func testIsReliable_onIOS26_withMissingCompatibilityKey_withNewXcode_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanThrowError( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.designRequiresCompatibility.rawValue) as NSError - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "2600" // Xcode 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "New Xcode with missing compatibility key should be unreliable") - } - - func testIsReliable_onIOS26_withInfoPlistNotFound_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanThrowError( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - error: SentryInfoPlistError.mainInfoPlistNotFound as NSError - ) - infoPlistWrapper.mockGetAppValueStringThrowError( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - error: SentryInfoPlistError.mainInfoPlistNotFound as Error - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Missing Info.plist should be treated as unreliable (defensive)") - } - - // MARK: - Edge Cases and Error Handling (iOS 26+) - - func testIsReliable_onIOS26_withAllChecksUnclear_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - all checks return unclear - infoPlistWrapper.mockGetAppValueBooleanThrowError( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.designRequiresCompatibility.rawValue) as NSError - ) - infoPlistWrapper.mockGetAppValueStringThrowError( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.xcodeVersion.rawValue) - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "When all checks are unclear, should be treated as unreliable (defensive)") - } - - func testIsReliable_onIOS26_withXcodeExactly2600_returnsFalse() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "2600" // Exactly Xcode 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Xcode 26.0 (exactly) should be unreliable") - } - - func testIsReliable_onIOS26_withXcodeExactly2599_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "2599" // Just before Xcode 26.0 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Xcode 25.9.9 (< 26.0) should be reliable") - } - - // MARK: - Multiple Reliable Conditions Tests (iOS 26+) - - func testIsReliable_onIOS26_withMultipleReliableConditions_returnsTrue() throws { - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - both compatibility mode AND old Xcode - infoPlistWrapper.mockGetAppValueBooleanReturnValue( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - value: true - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "1640" // Xcode 16.4 - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Multiple reliable conditions should make environment reliable") - } - - // MARK: - Real-World Scenario Tests (iOS 26+) - - func testIsReliable_typicalNewApp_onIOS26_withXcode26_returnsFalse() throws { - // Typical scenario: New app built with Xcode 26 running on iOS 26 - // Should be detected as unreliable (Liquid Glass will be used) - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "2600" - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertFalse(result, "Typical new app on iOS 26 with Xcode 26 should be unreliable") - } - - func testIsReliable_legacyApp_onIOS26_withXcode16_returnsTrue() throws { - // Legacy scenario: Old app built with Xcode 16 running on iOS 26 - // Should be detected as reliable (Liquid Glass won't be used) - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "1600" - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "Legacy app built with Xcode 16 on iOS 26 should be reliable") - } - - func testIsReliable_appOptingIntoCompatibility_onIOS26_withXcode26_returnsTrue() throws { - // Scenario: New app with Xcode 26 but explicitly opts into compatibility mode - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // Arrange - infoPlistWrapper.mockGetAppValueBooleanReturnValue( - forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - value: true - ) - infoPlistWrapper.mockGetAppValueStringReturnValue( - forKey: SentryInfoPlistKey.xcodeVersion.rawValue, - value: "2600" - ) - - // Act - let result = sut.isReliable() - - // Assert - XCTAssertTrue(result, "App with compatibility mode enabled should be reliable") - } -} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 942afa9eb13..9a89e84512b 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -746,36 +746,6 @@ class SentrySessionReplayIntegrationTests: XCTestCase { // -- Assert -- XCTAssertNil(weakSut, "SentrySessionReplayIntegration should be deallocated") } - - func testInstallWithOptions_WithUnsafe_withoutOverrideOptionEnabled_shouldReturnFalse() { - // -- Arrange -- - let options = Options() - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = false - - SentryDependencyContainer.sharedInstance().sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - - // -- Act -- - let instance = SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance()) - - // -- Assert -- - XCTAssertNil(instance) - } - - func testInstallWithOptions_WithUnsafe_withOverrideOptionEnabled_shouldReturnTrue() { - // -- Arrange -- - let options = Options() - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = true - - SentryDependencyContainer.sharedInstance().sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - - // -- Act -- - let instance = SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance()) - - // -- Assert -- - XCTAssertNotNil(instance) - } func testReplayIdAndSessionReplayCleared_whenMaxDurationReached() throws { // -- Arrange -- @@ -828,21 +798,6 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNil(sut.sessionReplay) } - func testInstallWithOptions_WithoutUnsafe_shouldReturnTrue() { - // -- Arrange -- - let options = Options() - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = false - - SentryDependencyContainer.sharedInstance().sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: true) - - // -- Act -- - let instance = SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance()) - - // -- Assert -- - XCTAssertNotNil(instance) - } - private func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws { let replayFolder = replayFolder() let jsonPath = replayFolder + "/replay.current" diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index b5f6f25363a..ab512cbc7a8 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -621,30 +621,6 @@ class SentrySessionReplayTests: XCTestCase { XCTAssertEqual(fixture.displayLink.invalidateInvocations.count, 1) } - func testShouldEnableSessionReplay_withUnreliableEnvironment_withoutOverrideOptionEnabled_shouldNotStart() { - // -- Arrange -- - let environmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - let experimentalOptions = SentryExperimentalOptions() - experimentalOptions.enableSessionReplayInUnreliableEnvironment = false - - // -- Assert -- - // Verify that session replay will not actually start - // (it should have been blocked by isInUnreliableEnvironment) - XCTAssertFalse(SentrySessionReplay.shouldEnableSessionReplay(environmentChecker: environmentChecker, experimentalOptions: experimentalOptions)) - } - - func testShouldEnableSessionReplay_withUnreliableEnvironment_withOverrideOptionEnabled_shouldStart() { - // -- Arrange -- - let environmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) - let experimentalOptions = SentryExperimentalOptions() - experimentalOptions.enableSessionReplayInUnreliableEnvironment = true - - // -- Assert -- - // Verify that session replay will start despite unreliable environment - // (override option is enabled) - XCTAssertTrue(SentrySessionReplay.shouldEnableSessionReplay(environmentChecker: environmentChecker, experimentalOptions: experimentalOptions)) - } - // MARK: - Frame Rate Tests func testFrameRate_1FPS_takesScreenshotsAtCorrectInterval() { diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index e6d31584ded..243ac36637b 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -355,7 +355,6 @@ class PrivateSentrySDKOnlyTests: XCTestCase { let options = Options.noIntegrations() options.sessionReplay = .init(sessionSampleRate: 1.0) - options.experimental.enableSessionReplayInUnreliableEnvironment = true SentrySDKInternal.start(options: options) var didCallCaptureReplay = false @@ -416,7 +415,6 @@ class PrivateSentrySDKOnlyTests: XCTestCase { SentrySDK.start { $0.removeAllIntegrations() $0.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - $0.experimental.enableSessionReplayInUnreliableEnvironment = true } PrivateSentrySDKOnly.setIgnoreContainerClass(IgnoreContainer.self) @@ -433,7 +431,6 @@ class PrivateSentrySDKOnlyTests: XCTestCase { SentrySDK.start { $0.removeAllIntegrations() $0.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - $0.experimental.enableSessionReplayInUnreliableEnvironment = true } PrivateSentrySDKOnly.setRedactContainerClass(RedactContainer.self) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 9a93bd69f6e..3f46c1f55b9 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -701,7 +701,6 @@ class SentryHubTests: XCTestCase { // Setup replay integration let replayOptions = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 0.0) fixture.options.sessionReplay = replayOptions - fixture.options.experimental.enableSessionReplayInUnreliableEnvironment = true let replayIntegration = try XCTUnwrap(SentrySessionReplayIntegration(with: fixture.options, dependencies: SentryDependencyContainer.sharedInstance())) replayIntegration.addItselfToSentryHub(hub: sut) @@ -1675,7 +1674,6 @@ class SentryHubTests: XCTestCase { func testGetSessionReplayId_ReturnsNilWhenSessionReplayIsNil() throws { let options = Options() options.sessionReplay.sessionSampleRate = 1.0 - options.experimental.enableSessionReplayInUnreliableEnvironment = true let integration = try XCTUnwrap(SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance())) integration.addItselfToSentryHub(hub: sut) @@ -1687,7 +1685,6 @@ class SentryHubTests: XCTestCase { func testGetSessionReplayId_ReturnsNilWhenSessionReplayIdIsNil() throws { let options = Options() options.sessionReplay.sessionSampleRate = 1.0 - options.experimental.enableSessionReplayInUnreliableEnvironment = true let integration = try XCTUnwrap(SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance())) let mockSessionReplay = createMockSessionReplay() Dynamic(integration).sessionReplay = mockSessionReplay @@ -1701,7 +1698,6 @@ class SentryHubTests: XCTestCase { func testGetSessionReplayId_ReturnsIdStringWhenSessionReplayIdExists() throws { let options = Options() options.sessionReplay.sessionSampleRate = 1.0 - options.experimental.enableSessionReplayInUnreliableEnvironment = true let integration = try XCTUnwrap(SentrySessionReplayIntegration(with: options, dependencies: SentryDependencyContainer.sharedInstance())) let mockSessionReplay = createMockSessionReplay() let rootView = UIView() diff --git a/sdk_api.json b/sdk_api.json index 08b344619e4..1d1bdb5c02e 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -787,6 +787,13 @@ "name": "UIKit", "printedName": "UIKit" }, + { + "declKind": "Import", + "kind": "Import", + "moduleName": "Sentry", + "name": "UniformTypeIdentifiers", + "printedName": "UniformTypeIdentifiers" + }, { "declKind": "Import", "kind": "Import", @@ -36724,82 +36731,6 @@ "printedName": "init()", "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)init" }, - { - "accessors": [ - { - "accessorKind": "get", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declAttributes": [ - "Final", - "ObjC" - ], - "declKind": "Accessor", - "implicit": true, - "kind": "Accessor", - "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvg", - "moduleName": "Sentry", - "name": "Get", - "printedName": "Get()", - "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)enableSessionReplayInUnreliableEnvironment" - }, - { - "accessorKind": "set", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - }, - { - "kind": "TypeNominal", - "name": "Void", - "printedName": "()" - } - ], - "declAttributes": [ - "Final", - "ObjC" - ], - "declKind": "Accessor", - "implicit": true, - "kind": "Accessor", - "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvs", - "moduleName": "Sentry", - "name": "Set", - "printedName": "Set()", - "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)setEnableSessionReplayInUnreliableEnvironment:" - } - ], - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declAttributes": [ - "Final", - "HasStorage", - "ObjC" - ], - "declKind": "Var", - "hasStorage": true, - "kind": "Var", - "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvp", - "moduleName": "Sentry", - "name": "enableSessionReplayInUnreliableEnvironment", - "printedName": "enableSessionReplayInUnreliableEnvironment", - "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(py)enableSessionReplayInUnreliableEnvironment" - }, { "accessors": [ {