From 5bd0e5e334bb8f971734e1d55a02af89d0862167 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:51:43 +0000 Subject: [PATCH 01/22] chore(deps): Update clang-format version (#6385) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> (cherry picked from commit 3e57e15d64ea72a9b735f3ae3dd7f9dbf2173aef) --- scripts/.clang-format-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index 9fcf356b68f..0ecacc9d29a 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -21.1.2 +21.1.3 From df7abfef3d918c969479082579ea53818c995e0b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 9 Oct 2025 16:45:10 +0200 Subject: [PATCH 02/22] fix(session-replay): Add detection for potential PII leaks disabling session replay --- .../SentrySampleShared/SentrySDKWrapper.swift | 2 + .../SessionReplay/SentryReplayOptions.swift | 20 ++++++- .../SessionReplay/SentrySessionReplay.swift | 57 +++++++++++++++++++ .../SentryReplayOptionsTests.swift | 55 ++++++++++++++++++ .../SentrySessionReplayIntegrationTests.swift | 2 +- .../SentrySessionReplayTests.swift | 47 +++++++++++++++ 6 files changed, 181 insertions(+), 2 deletions(-) diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index bfa70239185..235e2f51fa7 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -61,6 +61,8 @@ public struct SentrySDKWrapper { ) let defaultReplayQuality = options.sessionReplay.quality options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality(rawValue: (SentrySDKOverrides.SessionReplay.quality.stringValue as? NSString)?.integerValue ?? defaultReplayQuality.rawValue) ?? defaultReplayQuality + + options.sessionReplay.disableInDangerousEnvironment = false } #if !os(tvOS) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 6520f49b1c2..fad43c755a7 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -16,6 +16,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { public static let enableViewRendererV2: Bool = true public static let enableFastViewRendering: Bool = false public static let quality: SentryReplayQuality = .medium + public static let disableInDangerousEnvironment: Bool = true // The following properties are public because they are used by SentrySwiftUI. @@ -215,6 +216,17 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { */ public var enableFastViewRendering: Bool + /** + * 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 session replay integration is disabled by default starting with `8.57.0` as a defensive mechanism. + * + * - 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 #1234](https://github.com/getsentry/sentry-cocoa/issue/1234) for more information. + */ + public var disableInDangerousEnvironment: Bool + /** * Defines the quality of the session replay. * @@ -290,6 +302,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: nil, enableViewRendererV2: nil, enableFastViewRendering: nil, + disableInDangerousEnvironment: nil, maskedViewClasses: nil, unmaskedViewClasses: nil, quality: nil, @@ -319,6 +332,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { enableViewRendererV2: (dictionary["enableViewRendererV2"] as? NSNumber)?.boolValue ?? (dictionary["enableExperimentalViewRenderer"] as? NSNumber)?.boolValue, enableFastViewRendering: (dictionary["enableFastViewRendering"] as? NSNumber)?.boolValue, + disableInDangerousEnvironment: (dictionary["disableInDangerousEnvironment"] as? NSNumber)?.boolValue, maskedViewClasses: (dictionary["maskedViewClasses"] as? NSArray)?.compactMap({ element in NSClassFromString((element as? String) ?? "") }), @@ -353,7 +367,8 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllText: Bool = DefaultValues.maskAllText, maskAllImages: Bool = DefaultValues.maskAllImages, enableViewRendererV2: Bool = DefaultValues.enableViewRendererV2, - enableFastViewRendering: Bool = DefaultValues.enableFastViewRendering + enableFastViewRendering: Bool = DefaultValues.enableFastViewRendering, + disableInDangerousEnvironment: Bool = DefaultValues.disableInDangerousEnvironment ) { // - This initializer is publicly available for Swift, but not for Objective-C, because automatically bridged Swift initializers // with default values result in a single initializer requiring all parameters. @@ -368,6 +383,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: maskAllImages, enableViewRendererV2: enableViewRendererV2, enableFastViewRendering: enableFastViewRendering, + disableInDangerousEnvironment: disableInDangerousEnvironment, maskedViewClasses: nil, unmaskedViewClasses: nil, quality: nil, @@ -387,6 +403,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: Bool?, enableViewRendererV2: Bool?, enableFastViewRendering: Bool?, + disableInDangerousEnvironment: Bool?, maskedViewClasses: [AnyClass]?, unmaskedViewClasses: [AnyClass]?, quality: SentryReplayQuality?, @@ -402,6 +419,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { self.maskAllImages = maskAllImages ?? DefaultValues.maskAllImages self.enableViewRendererV2 = enableViewRendererV2 ?? DefaultValues.enableViewRendererV2 self.enableFastViewRendering = enableFastViewRendering ?? DefaultValues.enableFastViewRendering + self.disableInDangerousEnvironment = disableInDangerousEnvironment ?? DefaultValues.disableInDangerousEnvironment self.maskedViewClasses = maskedViewClasses ?? DefaultValues.maskedViewClasses self.unmaskedViewClasses = unmaskedViewClasses ?? DefaultValues.unmaskedViewClasses self.quality = quality ?? DefaultValues.quality diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 10279ac9f07..f9dd8290838 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -1,3 +1,4 @@ +// swiftlint:disable file_length import Foundation #if (os(iOS) || os(tvOS)) && !SENTRY_NO_UIKIT @_implementationOnly import _SentryPrivate @@ -73,6 +74,18 @@ import UIKit SentrySDKLog.debug("[Session Replay] Session replay is already running, not starting again") return } + + // 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 isRunningInDangerousEnvironment() { + if replayOptions.disableInDangerousEnvironment { + SentrySDKLog.fatal("[Session Replay] Detected environment potentially causing PII leaks, disabling Session Replay. To override this mechanism, set `options.disableInDangerousEnvironment` to `false`") + return + } + SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.disableInDangerousEnvironment` is set to `false`, ignoring and enabling Session Replay.") + } + displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) self.rootView = rootView lastScreenShot = dateProvider.date() @@ -369,7 +382,51 @@ import UIKit replayMaker.addFrameAsync(timestamp: timestamp, maskedViewImage: maskedViewImage, forScreen: screen) } } + + private func isRunningInDangerousEnvironment() -> Bool { + // Defensive programming: Assume dangerous 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) + + // First check: Are we even on iOS 26.0+? + guard #available(iOS 26.0, *) else { + // Not on iOS 26.0+ - safe to use Session Replay + return false + } + + // We're on iOS 26.0+ - assume dangerous unless proven otherwise + guard let infoDictionary = Bundle.main.infoDictionary else { + // Can't read Info.plist - stay defensive + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - treating as dangerous") + return true + } + + // Safety check 1: Is compatibility mode explicitly enabled? + if let requiresCompatibility = infoDictionary["UIDesignRequiresCompatibility"] as? Bool, + requiresCompatibility == true { + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ with UIDesignRequiresCompatibility=YES - safe to use") + return false + } + + // Safety check 2: Was the app built with an older Xcode version? + // DTXcode format: Xcode 16.4 = "1640", Xcode 26.0 = "2600" + if let xcodeVersionString = infoDictionary["DTXcode"] as? String, + let xcodeVersion = Int(xcodeVersionString), + xcodeVersion < 2_600 { + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but built with Xcode \(xcodeVersionString) (< 26.0) - safe to use") + return false + } + + // No safety conditions met - treat as dangerous + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ with Liquid Glass likely active - blocking Session Replay") + return true + } } // swiftlint:enable type_body_length #endif +// swiftlint:enable file_length diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift index 11404bf1f61..efb4df017e6 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift @@ -16,6 +16,7 @@ class SentryReplayOptionsTests: XCTestCase { XCTAssertTrue(options.maskAllImages) XCTAssertTrue(options.enableViewRendererV2) XCTAssertFalse(options.enableFastViewRendering) + XCTAssertTrue(options.disableInDangerousEnvironment) XCTAssertEqual(options.maskedViewClasses.count, 0) XCTAssertEqual(options.unmaskedViewClasses.count, 0) @@ -850,4 +851,58 @@ class SentryReplayOptionsTests: XCTestCase { // -- Assert -- XCTAssertNil(options.sdkInfo) } + + // MARK: disableInDangerousEnvironment + + func testInit_disableInDangerousEnvironment_shouldDefaultToTrue() { + // -- Act -- + let options = SentryReplayOptions() + + // -- Assert -- + XCTAssertTrue(options.disableInDangerousEnvironment) + } + + func testInit_disableInDangerousEnvironment_whenSetToFalse_shouldAllowOptIn() { + // -- Act -- + let options = SentryReplayOptions( + sessionSampleRate: 1.0, + onErrorSampleRate: 1.0, + disableInDangerousEnvironment: false + ) + + // -- Assert -- + XCTAssertFalse(options.disableInDangerousEnvironment) + } + + func testInitFromDict_disableInDangerousEnvironment_whenValidValue_shouldSetValue() { + // -- Act -- + let optionsTrue = SentryReplayOptions(dictionary: [ + "disableInDangerousEnvironment": true + ]) + let optionsFalse = SentryReplayOptions(dictionary: [ + "disableInDangerousEnvironment": false + ]) + + // -- Assert -- + XCTAssertTrue(optionsTrue.disableInDangerousEnvironment) + XCTAssertFalse(optionsFalse.disableInDangerousEnvironment) + } + + func testInitFromDict_disableInDangerousEnvironment_whenInvalidValue_shouldUseDefaultValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [ + "disableInDangerousEnvironment": "invalid_value" + ]) + + // -- Assert -- + XCTAssertTrue(options.disableInDangerousEnvironment) + } + + func testInitFromDict_disableInDangerousEnvironment_whenNotSpecified_shouldUseDefaultValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [:]) + + // -- Assert -- + XCTAssertTrue(options.disableInDangerousEnvironment) + } } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 0700833769d..21f63826ba0 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -30,7 +30,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { } if #available(iOS 26.0, tvOS 26.0, macCatalyst 26.0, *) { - throw XCTSkip("When running the unit tests on iOS 26.0, tvOS 26 or macCatalyst 26.0 with Xcode 26.0, we get warning log messages on the console: 'nw_socket_set_connection_idle [C1.1.1.1:3] setsockopt SO_CONNECTION_IDLE failed [42: Protocol not available]'. This leads to test failures in CI. Therefore, we skip these for now. We are going to fix this with https://github.com/getsentry/sentry-cocoa/issues/6165.") + throw XCTSkip("When running the unit tests on iOS 26.0, tvOS 26 or macCatalyst 26.0 with Xcode 26.0, we get warning log messages on the console: 'nw_socket_set_connection_idle [C1.1.1.1:3] setsockopt SO_CONNECTION_IDLE failed [42: Protocol not available]'. This leads to test failures in CI. Therefore, we skip these for now. We are going to fix this with https://github.com/getsentry/sentry-cocoa/issues/6165. Note: Session Replay is also disabled by default on iOS 26 due to Liquid Glass rendering changes.") } uiApplication = TestSentryUIApplication() diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 0546163b446..e1fd3bdbc51 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -551,6 +551,53 @@ class SentrySessionReplayTests: XCTestCase { private func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { XCTAssertEqual(sessionReplay.isFullSession, expected) } + + // MARK: - iOS 26 Liquid Glass Detection Tests + + @available(iOS 26.0, *) + func testBlocksSessionReplayOnIOS26WithLiquidGlass() { + // This test will only run on iOS 26.0+ + // It tests that session replay is blocked when Liquid Glass is detected + let fixture = Fixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)) + + // Attempt to start session replay + sut.start(rootView: fixture.rootView, fullSession: true) + + // Verify that session replay did not actually start + // (it should have been blocked by isRunningInDangerousEnvironment) + XCTAssertFalse(fixture.displayLink.isRunning()) + } + + @available(iOS 26.0, *) + func testAllowsSessionReplayOnIOS26WhenDisabledViaOption() { + // This test verifies that users can explicitly opt-in to session replay on iOS 26 + let fixture = Fixture() + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + options.disableInDangerousEnvironment = false + let sut = fixture.getSut(options: options) + + // Attempt to start session replay + sut.start(rootView: fixture.rootView, fullSession: true) + + // Verify that session replay started despite iOS 26 + XCTAssertTrue(fixture.displayLink.isRunning()) + } + + func testAllowsSessionReplayOnIOS25AndEarlier() throws { + // This test runs on iOS < 26 and verifies session replay works normally + if #available(iOS 26.0, *) { + throw XCTSkip("This test is for iOS < 26.0") + } + + let fixture = Fixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)) + + // Session replay should start normally on older iOS versions + sut.start(rootView: fixture.rootView, fullSession: true) + + XCTAssertTrue(fixture.displayLink.isRunning()) + } } #endif From 34b0577141acc3af1af50e81f9018dd9692ed6b3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 9 Oct 2025 17:12:30 +0200 Subject: [PATCH 03/22] update changelog & docs; cleanup --- CHANGELOG.md | 6 + .../SentrySDKOverrides.swift | 6 +- .../SentrySampleShared/SentrySDKWrapper.swift | 4 +- Samples/Shared/feature-flags.yml | 1 + .../SessionReplay/SentryReplayOptions.swift | 6 +- sdk_api.json | 137 +++++++++++++++++- sdk_api_V9.json | 137 +++++++++++++++++- 7 files changed, 283 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47abc29db1..41403f29222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- fix(session-replay): Add detection for potential PII leaks disabling session replay (#6389) + ## 8.56.2 ### Fixes diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift index f98701f99c7..dd1fdd03fc2 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift @@ -110,6 +110,8 @@ 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 disableInDangerousEnvironment = "--io.sentry.session-replay.disable-in-dangerous-environment" } case sessionReplay = "Session Replay" @@ -324,7 +326,7 @@ extension SentrySDKOverrides.Performance { extension SentrySDKOverrides.SessionReplay { public var overrideType: OverrideType { switch self { - case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages: return .boolean + case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .disableInDangerousEnvironment: return .boolean case .onErrorSampleRate, .sessionSampleRate: return .float case .quality: return .string } @@ -412,7 +414,7 @@ extension SentrySDKOverrides.SessionReplay { public var ignoresDisableEverything: Bool { switch self { case .disable: return false - case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality: return true + case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality, .disableInDangerousEnvironment: return true } } } diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 235e2f51fa7..3cdbab84300 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -62,7 +62,9 @@ public struct SentrySDKWrapper { let defaultReplayQuality = options.sessionReplay.quality options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality(rawValue: (SentrySDKOverrides.SessionReplay.quality.stringValue as? NSString)?.integerValue ?? defaultReplayQuality.rawValue) ?? defaultReplayQuality - options.sessionReplay.disableInDangerousEnvironment = false + // Allow configuring dangerous environment protection via SDK override. + // Default to false for the sample app to allow testing on iOS 26+ with Liquid Glass. + options.sessionReplay.disableInDangerousEnvironment = !SentrySDKOverrides.SessionReplay.disableInDangerousEnvironment.boolValue } #if !os(tvOS) diff --git a/Samples/Shared/feature-flags.yml b/Samples/Shared/feature-flags.yml index 6d0c0951bb3..678ed94a7cc 100644 --- a/Samples/Shared/feature-flags.yml +++ b/Samples/Shared/feature-flags.yml @@ -22,6 +22,7 @@ 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.disable-in-dangerous-environment": false # user feedback "--io.sentry.feedback.use-custom-feedback-button": false diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index fad43c755a7..f4791c37106 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -218,12 +218,12 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { /** * 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 session replay integration is disabled by default starting with `8.57.0` as a defensive mechanism. + + * Therefore the session replay integration is disabled by default unless the environment is detected as safe. * * - 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 #1234](https://github.com/getsentry/sentry-cocoa/issue/1234) for more information. + * - Note: See [GitHub issues #6389](https://github.com/getsentry/sentry-cocoa/issues/6389) for more information. */ public var disableInDangerousEnvironment: Bool diff --git a/sdk_api.json b/sdk_api.json index 8a3d480ccdd..29cce330b73 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -48946,6 +48946,55 @@ } ] }, + { + "kind": "Var", + "name": "disableInDangerousEnvironment", + "printedName": "disableInDangerousEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Var", "name": "maskedViewClasses", @@ -50478,6 +50527,79 @@ } ] }, + { + "kind": "Var", + "name": "disableInDangerousEnvironment", + "printedName": "disableInDangerousEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)disableInDangerousEnvironment", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvp", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)disableInDangerousEnvironment", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setDisableInDangerousEnvironment:", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Constructor", "name": "init", @@ -50506,7 +50628,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:)", + "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:)", "children": [ { "kind": "TypeNominal", @@ -50549,6 +50671,13 @@ "hasDefaultArg": true, "usr": "s:Sb" }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "hasDefaultArg": true, + "usr": "s:Sb" + }, { "kind": "TypeNominal", "name": "Bool", @@ -50558,10 +50687,10 @@ } ], "declKind": "Constructor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", - "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9RenderingACSf_SfS4btcfc", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", + "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9Rendering29disableInDangerousEnvironmentACSf_SfS5btcfc", "moduleName": "Sentry", - "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", + "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", "declAttributes": [ "ObjC" ], diff --git a/sdk_api_V9.json b/sdk_api_V9.json index 9869b162c09..099055b4ca1 100644 --- a/sdk_api_V9.json +++ b/sdk_api_V9.json @@ -45422,6 +45422,55 @@ } ] }, + { + "kind": "Var", + "name": "disableInDangerousEnvironment", + "printedName": "disableInDangerousEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Var", "name": "maskedViewClasses", @@ -46954,6 +47003,79 @@ } ] }, + { + "kind": "Var", + "name": "disableInDangerousEnvironment", + "printedName": "disableInDangerousEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)disableInDangerousEnvironment", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvp", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)disableInDangerousEnvironment", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setDisableInDangerousEnvironment:", + "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Constructor", "name": "init", @@ -46982,7 +47104,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:)", + "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:)", "children": [ { "kind": "TypeNominal", @@ -47025,6 +47147,13 @@ "hasDefaultArg": true, "usr": "s:Sb" }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "hasDefaultArg": true, + "usr": "s:Sb" + }, { "kind": "TypeNominal", "name": "Bool", @@ -47034,10 +47163,10 @@ } ], "declKind": "Constructor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", - "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9RenderingACSf_SfS4btcfc", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", + "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9Rendering29disableInDangerousEnvironmentACSf_SfS5btcfc", "moduleName": "Sentry", - "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", + "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", "declAttributes": [ "ObjC" ], From 826ff75ddb144dd2d6063b27a1c514a9151bb5f2 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 9 Oct 2025 17:33:28 +0200 Subject: [PATCH 04/22] Fixes and cleanup --- CHANGELOG.md | 24 + .../SentrySDKOverrides.swift | 6 +- .../SentrySampleShared/SentrySDKWrapper.swift | 4 +- Samples/Shared/feature-flags.yml | 2 +- Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist | 2 + Sentry.xcodeproj/project.pbxproj | 51 ++ SentryTestUtils/TestInfoPlistWrapper.swift | 55 +++ .../TestInfoPlistWrapperTests.swift | 295 ++++++++++++ Sources/Sentry/SentryDependencyContainer.m | 1 + .../Sentry/SentrySessionReplayIntegration.m | 28 +- .../HybridPublic/SentryDependencyContainer.h | 2 + .../Swift/Helper/SentryInfoPlistError.swift | 5 + Sources/Swift/Helper/SentryInfoPlistKey.swift | 15 + .../Swift/Helper/SentryInfoPlistWrapper.swift | 40 ++ .../SentryInfoPlistWrapperProvider.swift | 27 ++ .../SessionReplay/SentryReplayOptions.swift | 20 +- .../SessionReplay/SentrySessionReplay.swift | 97 ++-- Sources/Swift/SentryExperimentalOptions.swift | 19 +- .../Helper/SentryInfoPlistWrapperTests.swift | 194 ++++++++ .../SentryReplayOptionsTests.swift | 55 --- .../SentrySessionReplayTests.swift | 173 +++++-- sdk_api.json | 441 ++++++++++++------ sdk_api_V9.json | 441 ++++++++++++------ 23 files changed, 1584 insertions(+), 413 deletions(-) create mode 100644 SentryTestUtils/TestInfoPlistWrapper.swift create mode 100644 SentryTestUtilsTests/TestInfoPlistWrapperTests.swift create mode 100644 Sources/Swift/Helper/SentryInfoPlistError.swift create mode 100644 Sources/Swift/Helper/SentryInfoPlistKey.swift create mode 100644 Sources/Swift/Helper/SentryInfoPlistWrapper.swift create mode 100644 Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift create mode 100644 Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 41403f29222..091cba3de34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,33 @@ ## Unreleased +> [!Warning] +> **Session Replay is disabled by default on iOS 26.0+ with Xcode 26.0+ to prevent PII leaks** +> +> Due to masking issues introduced by Apple's Liquid Glass rendering changes in iOS 26.0, session replay is now **automatically disabled** on apps running iOS 26.0+ when built with Xcode 26.0 or later. This is a defensive measure to protect user privacy and prevent potential PII leaks until masking is reliably supported. +> +> Session replay will work normally if: +> +> - Your app runs on iOS versions older than 26.0, OR +> - Your app is built with Xcode versions older than 26.0, OR +> - Your app explicitly sets `UIDesignRequiresCompatibility` to `YES` in `Info.plist` +> +> **Override (use with caution):** If you understand the PII risks and want to enable session replay anyway, you can set: +> +> ```swift +> options.experimental.enableSessionReplayInUnreliableEnvironment = true +> ``` +> +> This experimental override option will be removed in a future minor version once the masking issues are resolved. + ### Fixes - fix(session-replay): Add detection for potential PII leaks disabling session replay (#6389) +- Session replay is now automatically disabled in environments with unreliable masking to prevent PII leaks (#6389) + - Detects iOS 26.0+ runtime with Xcode 26.0+ builds (DTXcode >= 2600) + - Detects missing or disabled `UIDesignRequiresCompatibility` + - Uses defensive approach: assumes unsafe unless proven safe +- Add `options.experimental.enableSessionReplayInUnreliableEnvironment` to allow overriding the automatic disabling (#6389) ## 8.56.2 diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift index dd1fdd03fc2..9ad8e077cb7 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift @@ -111,7 +111,7 @@ 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 disableInDangerousEnvironment = "--io.sentry.session-replay.disable-in-dangerous-environment" + case enableInUnreliableEnvironment = "--io.sentry.session-replay.enable-in-unreliable-environment" } case sessionReplay = "Session Replay" @@ -326,7 +326,7 @@ extension SentrySDKOverrides.Performance { extension SentrySDKOverrides.SessionReplay { public var overrideType: OverrideType { switch self { - case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .disableInDangerousEnvironment: return .boolean + case .disable, .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .enableInUnreliableEnvironment: return .boolean case .onErrorSampleRate, .sessionSampleRate: return .float case .quality: return .string } @@ -414,7 +414,7 @@ extension SentrySDKOverrides.SessionReplay { public var ignoresDisableEverything: Bool { switch self { case .disable: return false - case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality, .disableInDangerousEnvironment: return true + case .disableViewRendererV2, .enableFastViewRendering, .disableMaskAllText, .disableMaskAllImages, .onErrorSampleRate, .sessionSampleRate, .quality, .enableInUnreliableEnvironment: return true } } } diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 3cdbab84300..5438e8d9ebb 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -62,9 +62,9 @@ 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 dangerous environment protection via SDK override. + // 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.sessionReplay.disableInDangerousEnvironment = !SentrySDKOverrides.SessionReplay.disableInDangerousEnvironment.boolValue + options.experimental.enableSessionReplayInUnreliableEnvironment = SentrySDKOverrides.SessionReplay.enableInUnreliableEnvironment.boolValue } #if !os(tvOS) diff --git a/Samples/Shared/feature-flags.yml b/Samples/Shared/feature-flags.yml index 678ed94a7cc..9d66c7419e6 100644 --- a/Samples/Shared/feature-flags.yml +++ b/Samples/Shared/feature-flags.yml @@ -22,7 +22,7 @@ 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.disable-in-dangerous-environment": false + "--io.sentry.session-replay.enable-in-unreliable-environment": false # user feedback "--io.sentry.feedback.use-custom-feedback-button": false diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist b/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist index b4f367bf79b..853173fc8b2 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist @@ -33,6 +33,8 @@ UIApplicationSupportsIndirectInputEvents + UIDesignRequiresCompatibility + UILaunchScreen UIRequiredDeviceCapabilities diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index abd431afb76..1cb763872c2 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -814,6 +814,11 @@ D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4312D706BDD007068CB /* SentrySpanOperation.h */; }; D456B4362D706BF2007068CB /* SentryTraceOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4352D706BEE007068CB /* SentryTraceOrigin.h */; }; D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4372D706BFB007068CB /* SentrySpanDataKey.h */; }; + D4599F892E98F4750045BB95 /* SentryInfoPlistKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */; }; + D4599F8B2E98FE9F0045BB95 /* SentryInfoPlistWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */; }; + D4599F8D2E990F960045BB95 /* TestInfoPlistWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */; }; + D4599F8F2E99113E0045BB95 /* TestInfoPlistWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */; }; + D4599F922E9913B20045BB95 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D4599F912E9913B20045BB95 /* CwlPreconditionTesting */; }; D45B4AF52E019E1A00C31DFB /* TestSentryViewRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45B4AF42E019E1500C31DFB /* TestSentryViewRenderer.swift */; }; D45B4AF72E01A10100C31DFB /* TestSentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45B4AF62E01A0FA00C31DFB /* TestSentryViewPhotographer.swift */; }; D45CE9752E5F454E00BFEDB2 /* SentryScreenshotSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45CE9742E5F454300BFEDB2 /* SentryScreenshotSource.swift */; }; @@ -827,6 +832,9 @@ D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; + D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */; }; + D48891CE2E98F28E00212823 /* SentryInfoPlistWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */; }; + D48891D02E98F2E700212823 /* SentryInfoPlistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */; }; D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */; }; D490648A2DFAE1F600555785 /* SentryScreenshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */; }; @@ -2141,6 +2149,10 @@ D456B4312D706BDD007068CB /* SentrySpanOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanOperation.h; path = include/SentrySpanOperation.h; sourceTree = ""; }; D456B4352D706BEE007068CB /* SentryTraceOrigin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTraceOrigin.h; path = include/SentryTraceOrigin.h; sourceTree = ""; }; D456B4372D706BFB007068CB /* SentrySpanDataKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanDataKey.h; path = include/SentrySpanDataKey.h; sourceTree = ""; }; + D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistKey.swift; sourceTree = ""; }; + D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapperTests.swift; sourceTree = ""; }; + D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInfoPlistWrapper.swift; sourceTree = ""; }; + D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInfoPlistWrapperTests.swift; sourceTree = ""; }; D45B4AF42E019E1500C31DFB /* TestSentryViewRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryViewRenderer.swift; sourceTree = ""; }; D45B4AF62E01A0FA00C31DFB /* TestSentryViewPhotographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryViewPhotographer.swift; sourceTree = ""; }; D45CE9742E5F454300BFEDB2 /* SentryScreenshotSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotSource.swift; sourceTree = ""; }; @@ -2156,6 +2168,9 @@ D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScopePersistentStoreTests.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; + D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapperProvider.swift; sourceTree = ""; }; + D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapper.swift; sourceTree = ""; }; + D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistError.swift; sourceTree = ""; }; D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptions.swift; sourceTree = ""; }; @@ -2517,6 +2532,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D4599F922E9913B20045BB95 /* CwlPreconditionTesting in Frameworks */, D4CBA2472DE06D0200581618 /* libSentryTestUtils.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2604,6 +2620,10 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( + D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */, + D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */, + D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */, + D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */, FAE579872E7D9D4900B710F9 /* SentrySysctl.swift */, FAE579BC2E7DDDE400B710F9 /* SentryThreadWrapper.swift */, FAE5797E2E7CF21300B710F9 /* SentryMigrateSessionInit.swift */, @@ -3568,6 +3588,7 @@ 7BD7299B24654CD500EA3610 /* Helper */ = { isa = PBXGroup; children = ( + D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */, F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */, D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */, D8AE48BE2C578D540092A2A6 /* SentrySDKLog.swift */, @@ -4022,6 +4043,7 @@ 84AC61DA29F7654A009EEF61 /* TestDispatchSourceWrapper.swift */, 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */, 7B30B68126527C55006B2752 /* TestDisplayLinkWrapper.swift */, + D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */, 8E25C97425F8511A00DC215B /* TestRandom.swift */, 7BE3C7762445E50A00A38442 /* TestCurrentDateProvider.swift */, 7BDB03BE25136A7D00BAE198 /* TestSentryDispatchQueueWrapper.swift */, @@ -4317,6 +4339,7 @@ D4CBA2512DE06D1600581618 /* TestConstantTests.swift */, D43B0E5F2DE7416600EE3759 /* TestFileManagerTests.swift */, 62E75EB82E152953002EC91B /* InvocationsTests.swift */, + D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */, ); path = SentryTestUtilsTests; sourceTree = ""; @@ -5338,6 +5361,7 @@ ); name = SentryTestUtilsTests; packageProductDependencies = ( + D4599F912E9913B20045BB95 /* CwlPreconditionTesting */, ); productName = SentryTestUtilsTests; productReference = D4CBA2432DE06D0200581618 /* SentryTestUtilsTests.xctest */; @@ -5450,6 +5474,7 @@ ); mainGroup = 6327C5C91EB8A783004E799B; packageReferences = ( + D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */, ); productRefGroup = 6327C5D41EB8A783004E799B /* Products */; projectDirPath = ""; @@ -5628,6 +5653,7 @@ 03BCC38C27E1C01A003232C7 /* SentryTime.mm in Sources */, A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */, 62C97D3A2CC64E6B00DDA204 /* SentryUncaughtNSExceptions.m in Sources */, + D4599F892E98F4750045BB95 /* SentryInfoPlistKey.swift in Sources */, 03F84D3727DD4191008FE43F /* SentrySamplingProfiler.cpp in Sources */, 8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */, 6292585B2DAFA5F70049388F /* SentryCrashCxaThrowSwapper.c in Sources */, @@ -5662,6 +5688,7 @@ D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */, 92ECD73C2E05ACE00063EC10 /* SentryLog.swift in Sources */, F458D1152E1869AD0028273E /* SentryScopePersistentStore+String.swift in Sources */, + D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */, F458D1172E186DF20028273E /* SentryScopePersistentStore+Fingerprint.swift in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, @@ -5729,6 +5756,7 @@ 7BBD188B244841FB00427C76 /* SentryHttpDateParser.m in Sources */, D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */, 840A11122B61E27500650D02 /* SentrySamplerDecision.m in Sources */, + D48891CE2E98F28E00212823 /* SentryInfoPlistWrapper.swift in Sources */, FAE579C22E7DDDE700B710F9 /* SentryThreadWrapper.swift in Sources */, F458D1132E180BB00028273E /* SentryFileManagerProtocol.swift in Sources */, 8E4E7C8225DAB2A5006AB9E2 /* SentryTracer.m in Sources */, @@ -5954,6 +5982,7 @@ 63FE710B20DA4C1000CDBAE8 /* SentryCrashMach.c in Sources */, 63FE707720DA4C1000CDBAE8 /* SentryDictionaryDeepSearch.m in Sources */, FACEED132E3179A10007B4AC /* SentyOptionsInternal.m in Sources */, + D48891D02E98F2E700212823 /* SentryInfoPlistError.swift in Sources */, 8482FA9C2DD7C397000E9283 /* SentryFeedbackAPI.m in Sources */, 7BC9A20628F41781001E7C4C /* SentryMeasurementUnit.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, @@ -6120,6 +6149,7 @@ D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */, 7B0002322477F0520035FEF1 /* SentrySessionTests.m in Sources */, 62CFD9AD2C99770B00834E1B /* SentryInvalidJSONString.m in Sources */, + D4599F8B2E98FE9F0045BB95 /* SentryInfoPlistWrapperTests.swift in Sources */, 62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */, FA8AFDAC2E84FAEE007A0E18 /* TestSentryUIApplication.swift in Sources */, 7BC6EC08255C36DE0059822A /* SentryStacktraceTests.swift in Sources */, @@ -6362,6 +6392,7 @@ 84AC61DB29F7654A009EEF61 /* TestDispatchSourceWrapper.swift in Sources */, D452FE6F2DDC890A00AFF56F /* TestFileManager.swift in Sources */, 8431F01729B2851500D8DC56 /* TestSentrySystemWrapper.swift in Sources */, + D4599F8D2E990F960045BB95 /* TestInfoPlistWrapper.swift in Sources */, 84281C632A579D0700EE88F2 /* SentryProfilerMocks.mm in Sources */, D45B4AF72E01A10100C31DFB /* TestSentryViewPhotographer.swift in Sources */, D8FC98AB2CD0DAB30009824C /* BreadcrumbExtension.swift in Sources */, @@ -6380,6 +6411,7 @@ D44B16722DE464AD006DBDB3 /* TestDispatchFactoryTests.swift in Sources */, D4EE12D22DE9AC3800385BAF /* TestNSNotificationCenterWrapperTests.swift in Sources */, 62E75EB92E152953002EC91B /* InvocationsTests.swift in Sources */, + D4599F8F2E99113E0045BB95 /* TestInfoPlistWrapperTests.swift in Sources */, D4CBA2532DE06D1600581618 /* TestConstantTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8869,6 +8901,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mattgallagher/CwlPreconditionTesting.git"; + requirement = { + kind = exactVersion; + version = 2.2.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D4599F912E9913B20045BB95 /* CwlPreconditionTesting */ = { + isa = XCSwiftPackageProductDependency; + package = D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */; + productName = CwlPreconditionTesting; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 6327C5CA1EB8A783004E799B /* Project object */; } diff --git a/SentryTestUtils/TestInfoPlistWrapper.swift b/SentryTestUtils/TestInfoPlistWrapper.swift new file mode 100644 index 00000000000..afb3335ea0f --- /dev/null +++ b/SentryTestUtils/TestInfoPlistWrapper.swift @@ -0,0 +1,55 @@ +@_spi(Private) @testable import Sentry + +@_spi(Private) public class TestInfoPlistWrapper: SentryInfoPlistWrapperProvider { + + public init() {} + + public var getAppValueStringInvocations = Invocations() + private var mockedGetAppValueStringReturnValue: [String: Result] = [:] + + public func mockGetAppValueStringReturnValue(forKey key: String, value: String) { + mockedGetAppValueStringReturnValue[key] = .success(value) + } + + public func mockGetAppValueStringThrowError(forKey key: String, error: Error) { + mockedGetAppValueStringReturnValue[key] = .failure(error) + } + + public func getAppValueString(for key: String) throws -> String { + getAppValueStringInvocations.record(key) + guard let result = mockedGetAppValueStringReturnValue[key] else { + preconditionFailure("TestInfoPlistWrapper: No mocked return value set for getAppValueString(for:) for key: \(key)") + } + switch result { + case .success(let value): + return value + case .failure(let error): + throw error + } + } + + public var getAppValueBooleanInvocations = Invocations<(String, NSErrorPointer)>() + private var mockedGetAppValueBooleanReturnValue: [String: Result] = [:] + + public func mockGetAppValueBooleanReturnValue(forKey key: String, value: Bool) { + mockedGetAppValueBooleanReturnValue[key] = .success(value) + } + + public func mockGetAppValueBooleanThrowError(forKey key: String, error: NSError) { + mockedGetAppValueBooleanReturnValue[key] = .failure(error) + } + + public func getAppValueBoolean(for key: String, errorPtr: NSErrorPointer) -> Bool { + getAppValueBooleanInvocations.record((key, errorPtr)) + guard let result = mockedGetAppValueBooleanReturnValue[key] else { + preconditionFailure("TestInfoPlistWrapper: No mocked return value set for getAppValueBoolean(for:) for key: \(key)") + } + switch result { + case .success(let value): + return value + case .failure(let error): + errorPtr?.pointee = error + return false + } + } +} diff --git a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift new file mode 100644 index 00000000000..bed8de4694e --- /dev/null +++ b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift @@ -0,0 +1,295 @@ +import CwlPreconditionTesting +@_spi(Private) @testable import Sentry +@_spi(Private) @testable import SentryTestUtils +import XCTest + +class TestInfoPlistWrapperTests: XCTestCase { + + // MARK: - getAppValueString(for:) + + func testGetAppValueString_withoutMockedValue_shouldFailWithPreconditionFailure() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + // Don't mock any value for this key + + // -- Act -- + let e = catchBadInstruction { + do { + _ = try sut.getAppValueString(for: "unmockedKey") + } catch { + // noop + } + } + + // -- Assert -- + XCTAssertNotNil(e) + } + + func testGetAppValueString_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringReturnValue(forKey: "key", value: "value") + + // -- Act -- + let result = try sut.getAppValueString(for: "key") + + // -- Assert -- + XCTAssertEqual(result, "value") + } + + func testGetAppValueString_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringReturnValue(forKey: "key1", value: "value1") + + // -- Act -- + let result1 = try sut.getAppValueString(for: "key1") + let result2 = try sut.getAppValueString(for: "key1") + + // -- Assert -- + XCTAssertEqual(result1, "value1") + XCTAssertEqual(result2, "value1") + } + + func testGetAppValueString_shouldRecordInvocations() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringReturnValue(forKey: "key1", value: "value1") + sut.mockGetAppValueStringReturnValue(forKey: "key2", value: "value2") + sut.mockGetAppValueStringReturnValue(forKey: "key3", value: "value3") + + // -- Act -- + _ = try sut.getAppValueString(for: "key1") + _ = try sut.getAppValueString(for: "key2") + _ = try sut.getAppValueString(for: "key3") + + // -- Assert -- + XCTAssertEqual(sut.getAppValueStringInvocations.count, 3) + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 1), "key2") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 2), "key3") + } + + func testGetAppValueString_withDifferentKeys_shouldReturnDifferentValues() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringReturnValue(forKey: "key1", value: "value1") + sut.mockGetAppValueStringReturnValue(forKey: "key2", value: "value2") + + // -- Act -- + let result1 = try sut.getAppValueString(for: "key1") + let result2 = try sut.getAppValueString(for: "key2") + + // -- Assert -- + XCTAssertEqual(result1, "value1") + XCTAssertEqual(result2, "value2") + XCTAssertEqual(sut.getAppValueStringInvocations.count, 2) + } + + func testGetAppValueString_withFailureResult_shouldThrowError() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringThrowError(forKey: "key", error: SentryInfoPlistError.keyNotFound(key: "testKey")) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.getAppValueString(for: "key")) { error in + guard case SentryInfoPlistError.keyNotFound(let key) = error else { + XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)") + return + } + XCTAssertEqual(key, "testKey") + } + } + + func testGetAppValueString_withDifferentErrorTypes_shouldThrowCorrectError() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + + // Test mainInfoPlistNotFound + sut.mockGetAppValueStringThrowError(forKey: "key1", error: SentryInfoPlistError.mainInfoPlistNotFound) + XCTAssertThrowsError(try sut.getAppValueString(for: "key1")) { error in + guard case SentryInfoPlistError.mainInfoPlistNotFound = error else { + XCTFail("Expected SentryInfoPlistError.mainInfoPlistNotFound, got \(error)") + return + } + } + + // Test unableToCastValue + sut.mockGetAppValueStringThrowError(forKey: "key2", error: SentryInfoPlistError.unableToCastValue(key: "castKey", value: 123, type: String.self)) + XCTAssertThrowsError(try sut.getAppValueString(for: "key2")) { error in + guard case SentryInfoPlistError.unableToCastValue(let key, let value, let type) = error else { + XCTFail("Expected SentryInfoPlistError.unableToCastValue, got \(error)") + return + } + XCTAssertEqual(key, "castKey") + XCTAssertEqual(value as? Int, 123) + XCTAssertTrue(type == String.self) + } + } + + func testGetAppValueString_afterThrowingError_shouldRecordInvocation() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueStringThrowError(forKey: "key1", error: SentryInfoPlistError.keyNotFound(key: "testKey")) + + // -- Act -- + _ = try? sut.getAppValueString(for: "key1") + + // -- Assert -- + XCTAssertEqual(sut.getAppValueStringInvocations.count, 1) + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1") + } + + // MARK: - getAppValueBoolean(for:errorPtr:) + + func testGetAppValueBoolean_withoutMockedValue_shouldFailWithPreconditionFailure() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + // Don't mock any value for this key + + // -- Act -- + let e = catchBadInstruction { + var error: NSError? + _ = sut.getAppValueBoolean(for: "unmockedKey", errorPtr: &error) + } + + // -- Assert -- + XCTAssertNotNil(e) + } + + func testGetAppValueBoolean_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key", value: true) + + // -- Act -- + var error: NSError? + let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) + + // -- Assert -- + XCTAssertTrue(result) + XCTAssertNil(error) + } + + func testGetAppValueBoolean_withMockedValue_withMultipleInvocations_shouldReturnSameValue() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key1", value: true) + + // -- Act -- + var error1: NSError? + let result1 = sut.getAppValueBoolean(for: "key1", errorPtr: &error1) + + var error2: NSError? + let result2 = sut.getAppValueBoolean(for: "key1", errorPtr: &error2) + + // -- Assert -- + XCTAssertTrue(result1) + XCTAssertNil(error1) + XCTAssertTrue(result2) + XCTAssertNil(error2) + } + + func testGetAppValueBoolean_withFalseValue_shouldReturnFalse() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key", value: false) + + // -- Act -- + var error: NSError? + let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) + + // -- Assert -- + XCTAssertFalse(result) + XCTAssertNil(error) + } + + func testGetAppValueBoolean_withFailureResult_shouldReturnFalseAndSetError() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + let expectedError = NSError(domain: "TestDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "Test error"]) + sut.mockGetAppValueBooleanThrowError(forKey: "key", error: expectedError) + + // -- Act -- + var error: NSError? + let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) + + // -- Assert -- + XCTAssertFalse(result) + XCTAssertNotNil(error) + XCTAssertEqual(error?.domain, "TestDomain") + XCTAssertEqual(error?.code, 123) + XCTAssertEqual(error?.localizedDescription, "Test error") + } + + func testGetAppValueBoolean_withFailureResult_withNilErrorPointer_shouldReturnFalse() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + let expectedError = NSError(domain: "TestDomain", code: 123) + sut.mockGetAppValueBooleanThrowError(forKey: "key", error: expectedError) + + // -- Act -- + let result = sut.getAppValueBoolean(for: "key", errorPtr: nil) + + // -- Assert -- + XCTAssertFalse(result) + // No crash should occur when error pointer is nil + } + + func testGetAppValueBoolean_shouldRecordInvocations() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key1", value: true) + sut.mockGetAppValueBooleanReturnValue(forKey: "key2", value: false) + sut.mockGetAppValueBooleanReturnValue(forKey: "key3", value: true) + + // -- Act -- + var error1: NSError? + _ = sut.getAppValueBoolean(for: "key1", errorPtr: &error1) + + var error2: NSError? + _ = sut.getAppValueBoolean(for: "key2", errorPtr: &error2) + + var error3: NSError? + _ = sut.getAppValueBoolean(for: "key3", errorPtr: &error3) + + // -- Assert -- + XCTAssertEqual(sut.getAppValueBooleanInvocations.count, 3) + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 0)?.0, "key1") + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 1)?.0, "key2") + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 2)?.0, "key3") + } + + func testGetAppValueBoolean_withSuccessResult_withNilErrorPointer_shouldReturnTrue() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key", value: true) + + // -- Act -- + let result = sut.getAppValueBoolean(for: "key", errorPtr: nil) + + // -- Assert -- + XCTAssertTrue(result) + // No crash should occur when error pointer is nil + } + + func testGetAppValueBoolean_withDifferentKeys_shouldReturnDifferentValues() { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + sut.mockGetAppValueBooleanReturnValue(forKey: "key1", value: true) + sut.mockGetAppValueBooleanReturnValue(forKey: "key2", value: false) + + // -- Act -- + var error1: NSError? + let result1 = sut.getAppValueBoolean(for: "key1", errorPtr: &error1) + + var error2: NSError? + let result2 = sut.getAppValueBoolean(for: "key2", errorPtr: &error2) + + // -- Assert -- + XCTAssertTrue(result1) + XCTAssertNil(error1) + XCTAssertFalse(result2) + XCTAssertNil(error2) + } +} diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 8cc5a0354d3..de8e4fe7140 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -189,6 +189,7 @@ - (instancetype)init [[SentryDefaultRateLimits alloc] initWithRetryAfterHeaderParser:retryAfterHeaderParser andRateLimitParser:rateLimitParser currentDateProvider:_dateProvider]; + _infoPlistWrapper = [[SentryInfoPlistWrapper alloc] init]; #if SENTRY_HAS_REACHABILITY _reachability = [[SentryReachability alloc] init]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index c906ff48974..36e7efb7cfa 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -52,6 +52,7 @@ - (void)newSceneActivate; @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; + SentryExperimentalOptions *_experimentalOptions; id _notificationCenter; id _rateLimits; id _currentScreenshotProvider; @@ -63,6 +64,7 @@ @implementation SentrySessionReplayIntegration { // replay absolutely needs segment 0 to make replay work. BOOL _rateLimited; id _dateProvider; + id _infoPlistWrapper; } - (instancetype)init @@ -75,10 +77,13 @@ - (instancetype)initForManualUse:(nonnull SentryOptions *)options { if (self = [super init]) { [self setupWith:options.sessionReplay + experimentalOptions:options.experimental enableTouchTracker:options.enableSwizzling enableViewRendererV2:options.sessionReplay.enableViewRendererV2 enableFastViewRendering:options.sessionReplay.enableFastViewRendering]; - [self startWithOptions:options.sessionReplay fullSession:YES]; + [self startWithOptions:options.sessionReplay + experimentalOptions:options.experimental + fullSession:YES]; } return self; } @@ -90,6 +95,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options } [self setupWith:options.sessionReplay + experimentalOptions:options.experimental enableTouchTracker:options.enableSwizzling enableViewRendererV2:options.sessionReplay.enableViewRendererV2 enableFastViewRendering:options.sessionReplay.enableFastViewRendering]; @@ -97,11 +103,13 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options } - (void)setupWith:(SentryReplayOptions *)replayOptions + experimentalOptions:(SentryExperimentalOptions *)experimentalOptions enableTouchTracker:(BOOL)touchTracker enableViewRendererV2:(BOOL)enableViewRendererV2 enableFastViewRendering:(BOOL)enableFastViewRendering { _replayOptions = replayOptions; + _experimentalOptions = experimentalOptions; _rateLimits = SentryDependencyContainer.sharedInstance.rateLimits; _dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; @@ -132,6 +140,7 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; _dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; + _infoPlistWrapper = SentryDependencyContainer.sharedInstance.infoPlistWrapper; // We use the dispatch queue provider as a factory to create the queues, but store the queues // directly in this instance, so they get deallocated when the integration is deallocated. @@ -150,8 +159,6 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions [dispatchQueueProvider createUtilityQueue:"io.sentry.session-replay.processing" relativePriority:-2]; - // The asset worker queue is used to work on video and frames data. - [self moveCurrentReplay]; [self cleanUp]; @@ -331,7 +338,9 @@ - (void)runReplayForAvailableWindow if ([SentryDependencyContainer.sharedInstance.application getWindows].count > 0) { SENTRY_LOG_DEBUG(@"[Session Replay] Running replay for available window"); // If a window its already available start replay right away - [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; + [self startWithOptions:_replayOptions + experimentalOptions:_experimentalOptions + fullSession:_startedAsFullSession]; } else if (@available(iOS 13.0, tvOS 13.0, *)) { SENTRY_LOG_DEBUG( @"[Session Replay] Waiting for a scene to be available to started the replay"); @@ -351,15 +360,19 @@ - (void)newSceneActivate removeObserver:self name:UISceneDidActivateNotification object:nil]; - [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; + [self startWithOptions:_replayOptions + experimentalOptions:_experimentalOptions + fullSession:_startedAsFullSession]; } } - (void)startWithOptions:(SentryReplayOptions *)replayOptions + experimentalOptions:(SentryExperimentalOptions *)experimentalOptions fullSession:(BOOL)shouldReplayFullSession { SENTRY_LOG_DEBUG(@"[Session Replay] Starting session"); [self startWithOptions:replayOptions + experimentalOptions:experimentalOptions screenshotProvider:_currentScreenshotProvider ?: _viewPhotographer breadcrumbConverter:_currentBreadcrumbConverter ?: [[SentrySRDefaultBreadcrumbConverter alloc] init] @@ -367,6 +380,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions } - (void)startWithOptions:(SentryReplayOptions *)replayOptions + experimentalOptions:(SentryExperimentalOptions *)experimentalOptions screenshotProvider:(id)screenshotProvider breadcrumbConverter:(id)breadcrumbConverter fullSession:(BOOL)shouldReplayFullSession @@ -401,6 +415,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions SentryDisplayLinkWrapper *displayLinkWrapper = [[SentryDisplayLinkWrapper alloc] init]; self.sessionReplay = [[SentrySessionReplay alloc] initWithReplayOptions:replayOptions + experimentalOptions:experimentalOptions replayFolderPath:docs screenshotProvider:screenshotProvider replayMaker:replayMaker @@ -408,7 +423,8 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions touchTracker:_touchTracker dateProvider:_dateProvider delegate:self - displayLinkWrapper:displayLinkWrapper]; + displayLinkWrapper:displayLinkWrapper + infoPlistWrapper:_infoPlistWrapper]; [self.sessionReplay startWithRootView:[SentryDependencyContainer.sharedInstance.application getWindows] diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index 6e599e57bea..c2fe71a5c40 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -35,6 +35,7 @@ @protocol SentryDispatchQueueProviderProtocol; @protocol SentryNSNotificationCenterWrapper; @protocol SentryObjCRuntimeWrapper; +@protocol SentryInfoPlistWrapperProvider; #if SENTRY_HAS_METRIC_KIT @class SentryMXManager; @@ -94,6 +95,7 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentrySysctl *sysctlWrapper; @property (nonatomic, strong) id rateLimits; @property (nonatomic, strong) SentryThreadsafeApplication *threadsafeApplication; +@property (nonatomic, strong) id infoPlistWrapper; #if SENTRY_HAS_REACHABILITY @property (nonatomic, strong) SentryReachability *reachability; diff --git a/Sources/Swift/Helper/SentryInfoPlistError.swift b/Sources/Swift/Helper/SentryInfoPlistError.swift new file mode 100644 index 00000000000..8b7b790741f --- /dev/null +++ b/Sources/Swift/Helper/SentryInfoPlistError.swift @@ -0,0 +1,5 @@ +enum SentryInfoPlistError: Error { + case mainInfoPlistNotFound + case keyNotFound(key: String) + case unableToCastValue(key: String, value: Any, type: Any.Type) +} diff --git a/Sources/Swift/Helper/SentryInfoPlistKey.swift b/Sources/Swift/Helper/SentryInfoPlistKey.swift new file mode 100644 index 00000000000..01406d863da --- /dev/null +++ b/Sources/Swift/Helper/SentryInfoPlistKey.swift @@ -0,0 +1,15 @@ +public 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" +} diff --git a/Sources/Swift/Helper/SentryInfoPlistWrapper.swift b/Sources/Swift/Helper/SentryInfoPlistWrapper.swift new file mode 100644 index 00000000000..f390e9f3a59 --- /dev/null +++ b/Sources/Swift/Helper/SentryInfoPlistWrapper.swift @@ -0,0 +1,40 @@ +@objc @_spi(Private) public class SentryInfoPlistWrapper: NSObject, SentryInfoPlistWrapperProvider { + // MARK: - Bridge to ObjC + + public func getAppValueBoolean(for key: String, errorPtr errPtr: NSErrorPointer) -> Bool { + do { + guard let value = try getAppValue(for: key, type: Bool.self) else { + throw SentryInfoPlistError.keyNotFound(key: key) + } + return value + } catch { + errPtr?.pointee = error as NSError + return false + } + } + + public func getAppValueString(for key: String) throws -> String { + guard let value = try getAppValue(for: key, type: String.self) else { + throw SentryInfoPlistError.keyNotFound(key: key) + } + return value + } + + // MARK: - Swift Implementation + + private func getAppValue(for key: String, type: T.Type) throws -> T? { + // As soon as this class is not consumed from Objective-C anymore, we can use this method directly to reduce + // unnecessary duplicate code. In addition this method can be adapted to use `SentryInfoPlistKey` as the type + // of the parameter `key` + guard let infoDictionary = Bundle.main.infoDictionary else { + throw SentryInfoPlistError.mainInfoPlistNotFound + } + guard let value = infoDictionary[key] else { + return nil + } + guard let typedValue = value as? T else { + throw SentryInfoPlistError.unableToCastValue(key: key, value: value, type: T.self) + } + return typedValue + } +} diff --git a/Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift b/Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift new file mode 100644 index 00000000000..7e2d1427664 --- /dev/null +++ b/Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift @@ -0,0 +1,27 @@ +@_spi(Private) @objc public protocol SentryInfoPlistWrapperProvider { + /** + * Retrieves a value from the app's `Info.plist` file for the given key and trys to cast it to a ``String``. + * + * - Parameter key: The key for which to retrieve the value from the `Info.plist`. + * - Throws: An error if the value cannot be cast to type ``String`` or ``SentryInfoPlistError.keyNotFound`` if the key was not found or the value is `nil` + * - Returns: The value associated with the specified key cast to type ``String`` + * - Note: The return value can not be nullable, because a throwing function in Objective-C uses `nil` to indicate an error: + * + * Throwing method cannot be a member of an '@objc' protocol because it returns a value of optional type 'String?'; 'nil' indicates failure to Objective-C + */ + func getAppValueString(for key: String) throws -> String + + /** + * Retrieves a value from the app's `Info.plist` file for the given key and trys to cast it to a ``Bool``. + * + * - Parameters + * - key: The key for which to retrieve the value from the `Info.plist`. + * - error: A pointer to a an `NSError` to return an error value. + * - Throws: An error if the value cannot be cast to type ``String`` or ``SentryInfoPlistError.keyNotFound`` if the value is `nil` + * - Returns: The value associated with the specified key cast to type ``String`` + * - Note: This method can not use `throws` because a falsy return value would indicate an error in Objective-C: + * + * Throwing method cannot be a member of an '@objc' protocol because it returns a value of type 'Bool'; return 'Void' or a type that bridges to an Objective-C class + */ + func getAppValueBoolean(for key: String, errorPtr: NSErrorPointer) -> Bool +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index f4791c37106..6520f49b1c2 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -16,7 +16,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { public static let enableViewRendererV2: Bool = true public static let enableFastViewRendering: Bool = false public static let quality: SentryReplayQuality = .medium - public static let disableInDangerousEnvironment: Bool = true // The following properties are public because they are used by SentrySwiftUI. @@ -216,17 +215,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { */ public var enableFastViewRendering: Bool - /** - * 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 session replay integration is disabled by default unless the environment is detected as safe. - * - * - 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 disableInDangerousEnvironment: Bool - /** * Defines the quality of the session replay. * @@ -302,7 +290,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: nil, enableViewRendererV2: nil, enableFastViewRendering: nil, - disableInDangerousEnvironment: nil, maskedViewClasses: nil, unmaskedViewClasses: nil, quality: nil, @@ -332,7 +319,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { enableViewRendererV2: (dictionary["enableViewRendererV2"] as? NSNumber)?.boolValue ?? (dictionary["enableExperimentalViewRenderer"] as? NSNumber)?.boolValue, enableFastViewRendering: (dictionary["enableFastViewRendering"] as? NSNumber)?.boolValue, - disableInDangerousEnvironment: (dictionary["disableInDangerousEnvironment"] as? NSNumber)?.boolValue, maskedViewClasses: (dictionary["maskedViewClasses"] as? NSArray)?.compactMap({ element in NSClassFromString((element as? String) ?? "") }), @@ -367,8 +353,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllText: Bool = DefaultValues.maskAllText, maskAllImages: Bool = DefaultValues.maskAllImages, enableViewRendererV2: Bool = DefaultValues.enableViewRendererV2, - enableFastViewRendering: Bool = DefaultValues.enableFastViewRendering, - disableInDangerousEnvironment: Bool = DefaultValues.disableInDangerousEnvironment + enableFastViewRendering: Bool = DefaultValues.enableFastViewRendering ) { // - This initializer is publicly available for Swift, but not for Objective-C, because automatically bridged Swift initializers // with default values result in a single initializer requiring all parameters. @@ -383,7 +368,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: maskAllImages, enableViewRendererV2: enableViewRendererV2, enableFastViewRendering: enableFastViewRendering, - disableInDangerousEnvironment: disableInDangerousEnvironment, maskedViewClasses: nil, unmaskedViewClasses: nil, quality: nil, @@ -403,7 +387,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { maskAllImages: Bool?, enableViewRendererV2: Bool?, enableFastViewRendering: Bool?, - disableInDangerousEnvironment: Bool?, maskedViewClasses: [AnyClass]?, unmaskedViewClasses: [AnyClass]?, quality: SentryReplayQuality?, @@ -419,7 +402,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { self.maskAllImages = maskAllImages ?? DefaultValues.maskAllImages self.enableViewRendererV2 = enableViewRendererV2 ?? DefaultValues.enableViewRendererV2 self.enableFastViewRendering = enableFastViewRendering ?? DefaultValues.enableFastViewRendering - self.disableInDangerousEnvironment = disableInDangerousEnvironment ?? DefaultValues.disableInDangerousEnvironment self.maskedViewClasses = maskedViewClasses ?? DefaultValues.maskedViewClasses self.unmaskedViewClasses = unmaskedViewClasses ?? DefaultValues.unmaskedViewClasses self.quality = quality ?? DefaultValues.quality diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index f9dd8290838..1efc09a1262 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -30,13 +30,15 @@ import UIKit private(set) var isSessionPaused = false private let replayOptions: SentryReplayOptions + private let experimentalOptions: SentryExperimentalOptions private let replayMaker: SentryReplayVideoMaker private let displayLink: SentryReplayDisplayLinkWrapper private let dateProvider: SentryCurrentDateProvider private let touchTracker: SentryTouchTracker? private let lock = NSLock() public var replayTags: [String: Any]? - + private let infoPlistWrapper: SentryInfoPlistWrapperProvider + var isRunning: Bool { displayLink.isRunning() } @@ -46,6 +48,7 @@ import UIKit public init( replayOptions: SentryReplayOptions, + experimentalOptions: SentryExperimentalOptions, replayFolderPath: URL, screenshotProvider: SentryViewScreenshotProvider, replayMaker: SentryReplayVideoMaker, @@ -53,9 +56,11 @@ import UIKit touchTracker: SentryTouchTracker?, dateProvider: SentryCurrentDateProvider, delegate: SentrySessionReplayDelegate, - displayLinkWrapper: SentryReplayDisplayLinkWrapper + displayLinkWrapper: SentryReplayDisplayLinkWrapper, + infoPlistWrapper: SentryInfoPlistWrapperProvider ) { self.replayOptions = replayOptions + self.experimentalOptions = experimentalOptions self.dateProvider = dateProvider self.delegate = delegate self.screenshotProvider = screenshotProvider @@ -64,6 +69,7 @@ import UIKit self.replayMaker = replayMaker self.breadcrumbConverter = breadcrumbConverter self.touchTracker = touchTracker + self.infoPlistWrapper = infoPlistWrapper } deinit { displayLink.invalidate() } @@ -78,12 +84,12 @@ import UIKit // 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 isRunningInDangerousEnvironment() { - if replayOptions.disableInDangerousEnvironment { - SentrySDKLog.fatal("[Session Replay] Detected environment potentially causing PII leaks, disabling Session Replay. To override this mechanism, set `options.disableInDangerousEnvironment` to `false`") + if isEnvironmentUnreliable() { + 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 `false`") return } - SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.disableInDangerousEnvironment` is set to `false`, ignoring and enabling Session Replay.") + SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.enableInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") } displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) @@ -383,8 +389,9 @@ import UIKit } } - private func isRunningInDangerousEnvironment() -> Bool { - // Defensive programming: Assume dangerous environment by default on iOS 26.0+ + // swiftlint:disable:next cyclomatic_complexity function_body_length + private func isEnvironmentUnreliable() -> 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. @@ -395,34 +402,68 @@ import UIKit // First check: Are we even on iOS 26.0+? guard #available(iOS 26.0, *) else { // Not on iOS 26.0+ - safe to use Session Replay + SentrySDKLog.debug("[Session Replay] Running on iOS version prior to 26.0+ - detected reliable environment") return false } - - // We're on iOS 26.0+ - assume dangerous unless proven otherwise - guard let infoDictionary = Bundle.main.infoDictionary else { + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+") + + // Safety check 1: Is compatibility mode explicitly enabled? + do { + var error: NSError? + let requiresCompatibility = infoPlistWrapper.getAppValueBoolean( + for: SentryInfoPlistKey.designRequiresCompatibility.rawValue, + errorPtr: &error + ) + if let error = error as Error? { + throw error + } else if requiresCompatibility { + SentrySDKLog.debug("[Session Replay] Running with UIDesignRequiresCompatibility set to YES - detected as reliable") + return false + } else { + SentrySDKLog.debug("[Session Replay] Running with UIDesignRequiresCompatibility is set to NO") + } + } catch SentryInfoPlistError.mainInfoPlistNotFound { // Can't read Info.plist - stay defensive - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - treating as dangerous") + SentrySDKLog.warning("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - detected as unreliable") return true + } catch SentryInfoPlistError.keyNotFound { + // Due to Objective-C using a return value of `nil` as an indicator for an error, + // we need to throw an error when the key was not found + SentrySDKLog.debug("[Session Replay] No UIDesignRequiresCompatibility found in Info.plist") + } catch { + SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error)") } - - // Safety check 1: Is compatibility mode explicitly enabled? - if let requiresCompatibility = infoDictionary["UIDesignRequiresCompatibility"] as? Bool, - requiresCompatibility == true { - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ with UIDesignRequiresCompatibility=YES - safe to use") - return false - } - + // Safety check 2: Was the app built with an older Xcode version? // DTXcode format: Xcode 16.4 = "1640", Xcode 26.0 = "2600" - if let xcodeVersionString = infoDictionary["DTXcode"] as? String, - let xcodeVersion = Int(xcodeVersionString), - xcodeVersion < 2_600 { - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but built with Xcode \(xcodeVersionString) (< 26.0) - safe to use") - return false + do { + let xcodeVersionString = try infoPlistWrapper.getAppValueString( + for: SentryInfoPlistKey.xcodeVersion.rawValue + ) + if let xcodeVersion = Int(xcodeVersionString) { + if xcodeVersion < 2_600 { + SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but built with Xcode \(xcodeVersionString) (< 26.0) - detected as reliable") + return false + } else { + SentrySDKLog.debug("[Session Replay] Detected built with Xcode version: \(xcodeVersionString)") + } + } else { + SentrySDKLog.warning("[Session Replay] Found xcode version key but could not parse as Int: \(xcodeVersionString)") + } + } catch SentryInfoPlistError.mainInfoPlistNotFound { + // Can't read Info.plist - stay defensive + SentrySDKLog.warning("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - detected as unreliable") + return true + } catch SentryInfoPlistError.keyNotFound { + // Due to Objective-C using a return value of `nil` as an indicator for an error, + // we need to throw an error when the key was not found. + SentrySDKLog.warning("[Session Replay] Could not find xcode version key in Info.plist") + } catch { + SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error)") } - - // No safety conditions met - treat as dangerous - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ with Liquid Glass likely active - blocking Session Replay") + + // No safety conditions met - treat as unreliable + SentrySDKLog.warning("[Session Replay] Detected environment as unreliable") return true } } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 926f5782b6f..895303c1f25 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -30,7 +30,24 @@ public class SentryExperimentalOptions: NSObject { * - Experiment: This is an experimental feature and is therefore disabled by default. We'll enable it by default in a future major release. */ 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 + /** * Logs are considered beta. */ diff --git a/Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift b/Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift new file mode 100644 index 00000000000..fd3849268fd --- /dev/null +++ b/Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift @@ -0,0 +1,194 @@ +@_spi(Private) @testable import Sentry +import XCTest + +class SentryInfoPlistWrapperTests: XCTestCase { + + private var sut: SentryInfoPlistWrapper! + + override func setUp() { + super.setUp() + sut = SentryInfoPlistWrapper() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - getAppValueString Tests + + func testGetAppValueString_whenKeyExists_shouldReturnValue() throws { + // Arrange + // CFBundleName is a standard key that should exist in any bundle + let key = "CFBundleName" + + // Act + let value = try sut.getAppValueString(for: key) + + // Assert + XCTAssertFalse(value.isEmpty, "Bundle name should not be empty") + } + + func testGetAppValueString_whenKeyDoesNotExist_shouldThrowKeyNotFoundError() { + // Arrange + let nonExistentKey = "NonExistentKey_12345_XYZ" + + // Act & Assert + XCTAssertThrowsError(try sut.getAppValueString(for: nonExistentKey)) { error in + guard case SentryInfoPlistError.keyNotFound(let key) = error else { + XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)") + return + } + XCTAssertEqual(key, nonExistentKey) + } + } + + func testGetAppValueString_whenValueIsNotString_shouldThrowUnableToCastError() { + // Arrange + // CFBundleVersion is typically a number or can be a mixed type + // We'll use a key that we know exists but might not be a string + // Note: This test might be skipped if we can't find a suitable non-string key + // Let's try with UIDeviceFamily which is typically an array + let key = "UIDeviceFamily" + + // Act & Assert + do { + _ = try sut.getAppValueString(for: key) + // If we get here, the key happened to be a string or doesn't exist in test bundle + // This is not a test failure, just means the key wasn't suitable for this test + } catch SentryInfoPlistError.unableToCastValue(let errorKey, _, let type) { + XCTAssertEqual(errorKey, key) + XCTAssertTrue(type == String.self) + } catch SentryInfoPlistError.keyNotFound { + // Key doesn't exist in test bundle, which is acceptable for this test + } catch { + XCTFail("Expected SentryInfoPlistError.unableToCastValue or keyNotFound, got \(error)") + } + } + + // MARK: - getAppValueBoolean Tests + + func testGetAppValueBoolean_whenKeyExistsAndIsTrue_shouldReturnTrue() { + // Arrange + // For this test, we'll use a key that might exist and be a boolean + // UIApplicationExitsOnSuspend is a boolean key (if it exists) + let key = "UIApplicationExitsOnSuspend" + var error: NSError? + + // Act + let value = sut.getAppValueBoolean(for: key, errorPtr: &error) + + // Assert + // If the key exists and is a boolean, it should work without error + // If the key doesn't exist, error should be set + if error == nil { + // Success case - value is valid + XCTAssertTrue(value == true || value == false, "Boolean value should be true or false") + } else { + // Key not found is acceptable for this test setup + XCTAssertTrue(error?.domain == "SentryInfoPlistError" || error != nil) + } + } + + func testGetAppValueBoolean_whenKeyDoesNotExist_shouldReturnFalseAndSetError() { + // Arrange + let nonExistentKey = "NonExistentBooleanKey_12345_XYZ" + var error: NSError? + + // Act + let value = sut.getAppValueBoolean(for: nonExistentKey, errorPtr: &error) + + // Assert + XCTAssertFalse(value, "Should return false when key is not found") + XCTAssertNotNil(error, "Error should be set when key is not found") + } + + func testGetAppValueBoolean_whenValueIsNotBoolean_shouldReturnFalseAndSetError() { + // Arrange + // CFBundleName is a string, not a boolean + let key = "CFBundleName" + var error: NSError? + + // Act + let value = sut.getAppValueBoolean(for: key, errorPtr: &error) + + // Assert + XCTAssertFalse(value, "Should return false when value cannot be cast to Boolean") + XCTAssertNotNil(error, "Error should be set when type casting fails") + } + + func testGetAppValueBoolean_withNullErrorPointer_shouldNotCrash() { + // Arrange + let key = "CFBundleName" // A key that exists but is not a boolean + + // Act & Assert + // This should not crash even with a null error pointer + let value = sut.getAppValueBoolean(for: key, errorPtr: nil) + XCTAssertFalse(value, "Should return false when casting fails") + } + + // MARK: - Edge Cases + + func testGetAppValueString_withEmptyKey_shouldThrowKeyNotFoundError() { + // Arrange + let emptyKey = "" + + // Act & Assert + XCTAssertThrowsError(try sut.getAppValueString(for: emptyKey)) { error in + guard case SentryInfoPlistError.keyNotFound = error else { + XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)") + return + } + } + } + + func testGetAppValueString_withSentryInfoPlistKey_shouldWork() throws { + // Arrange + // Test with the actual enum keys used in production + // Note: These keys might not exist in the test bundle, which is expected + let xcodeKey = SentryInfoPlistKey.xcodeVersion.rawValue + + // Act & Assert + do { + let value = try sut.getAppValueString(for: xcodeKey) + // If the key exists, value should not be empty + XCTAssertFalse(value.isEmpty, "Xcode version should not be empty if present") + } catch SentryInfoPlistError.keyNotFound { + // This is expected in test environment - DTXcode might not be set + XCTAssertTrue(true, "Key not found is acceptable for test bundle") + } + } + + func testGetAppValueBoolean_withSentryInfoPlistKey_shouldWork() { + // Arrange + let compatibilityKey = SentryInfoPlistKey.designRequiresCompatibility.rawValue + var error: NSError? + + // Act + let value = sut.getAppValueBoolean(for: compatibilityKey, errorPtr: &error) + + // Assert + // In test environment, this key likely doesn't exist + if error == nil { + // If no error, we successfully read a boolean value + XCTAssertTrue(value == true || value == false) + } else { + // Expected to not find this key in test bundle + XCTAssertNotNil(error) + } + } + + // MARK: - Multiple Consecutive Calls + + func testMultipleConsecutiveCalls_shouldReturnConsistentResults() throws { + // Arrange + let key = "CFBundleName" + + // Act + let value1 = try sut.getAppValueString(for: key) + let value2 = try sut.getAppValueString(for: key) + + // Assert + XCTAssertEqual(value1, value2, "Multiple calls should return the same value") + } +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift index efb4df017e6..11404bf1f61 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift @@ -16,7 +16,6 @@ class SentryReplayOptionsTests: XCTestCase { XCTAssertTrue(options.maskAllImages) XCTAssertTrue(options.enableViewRendererV2) XCTAssertFalse(options.enableFastViewRendering) - XCTAssertTrue(options.disableInDangerousEnvironment) XCTAssertEqual(options.maskedViewClasses.count, 0) XCTAssertEqual(options.unmaskedViewClasses.count, 0) @@ -851,58 +850,4 @@ class SentryReplayOptionsTests: XCTestCase { // -- Assert -- XCTAssertNil(options.sdkInfo) } - - // MARK: disableInDangerousEnvironment - - func testInit_disableInDangerousEnvironment_shouldDefaultToTrue() { - // -- Act -- - let options = SentryReplayOptions() - - // -- Assert -- - XCTAssertTrue(options.disableInDangerousEnvironment) - } - - func testInit_disableInDangerousEnvironment_whenSetToFalse_shouldAllowOptIn() { - // -- Act -- - let options = SentryReplayOptions( - sessionSampleRate: 1.0, - onErrorSampleRate: 1.0, - disableInDangerousEnvironment: false - ) - - // -- Assert -- - XCTAssertFalse(options.disableInDangerousEnvironment) - } - - func testInitFromDict_disableInDangerousEnvironment_whenValidValue_shouldSetValue() { - // -- Act -- - let optionsTrue = SentryReplayOptions(dictionary: [ - "disableInDangerousEnvironment": true - ]) - let optionsFalse = SentryReplayOptions(dictionary: [ - "disableInDangerousEnvironment": false - ]) - - // -- Assert -- - XCTAssertTrue(optionsTrue.disableInDangerousEnvironment) - XCTAssertFalse(optionsFalse.disableInDangerousEnvironment) - } - - func testInitFromDict_disableInDangerousEnvironment_whenInvalidValue_shouldUseDefaultValue() { - // -- Act -- - let options = SentryReplayOptions(dictionary: [ - "disableInDangerousEnvironment": "invalid_value" - ]) - - // -- Assert -- - XCTAssertTrue(options.disableInDangerousEnvironment) - } - - func testInitFromDict_disableInDangerousEnvironment_whenNotSpecified_shouldUseDefaultValue() { - // -- Act -- - let options = SentryReplayOptions(dictionary: [:]) - - // -- Assert -- - XCTAssertTrue(options.disableInDangerousEnvironment) - } } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index e1fd3bdbc51..adc1bab34cf 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -80,7 +80,8 @@ class SentrySessionReplayTests: XCTestCase { let rootView = UIView() let replayMaker = TestReplayMaker() let cacheFolder = FileManager.default.temporaryDirectory - + let infoPlistWrapper = TestInfoPlistWrapper() + var breadcrumbs: [Breadcrumb]? var isFullSession = true var lastReplayEvent: SentryReplayEvent? @@ -88,17 +89,33 @@ class SentrySessionReplayTests: XCTestCase { var lastVideoUrl: URL? var lastReplayId: SentryId? var currentScreen: String? - - func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, onErrorSampleRate: 0), touchTracker: SentryTouchTracker? = nil) -> SentrySessionReplay { - return SentrySessionReplay(replayOptions: options, - replayFolderPath: cacheFolder, - screenshotProvider: screenshotProvider, - replayMaker: replayMaker, - breadcrumbConverter: SentrySRDefaultBreadcrumbConverter(), - touchTracker: touchTracker ?? SentryTouchTracker(dateProvider: dateProvider, scale: 0), - dateProvider: dateProvider, - delegate: self, - displayLinkWrapper: displayLink) + + override init() { + super.init() + + // Configure the SUT with fields available in real apps + infoPlistWrapper.mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "1640") + infoPlistWrapper.mockGetAppValueBooleanReturnValue(forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, value: false) + } + + func getSut( + options: SentryReplayOptions = .init(sessionSampleRate: 0, onErrorSampleRate: 0), + experimentalOptions: SentryExperimentalOptions = .init(), + touchTracker: SentryTouchTracker? = nil + ) -> SentrySessionReplay { + return SentrySessionReplay( + replayOptions: options, + experimentalOptions: experimentalOptions, + replayFolderPath: cacheFolder, + screenshotProvider: screenshotProvider, + replayMaker: replayMaker, + breadcrumbConverter: SentrySRDefaultBreadcrumbConverter(), + touchTracker: touchTracker ?? SentryTouchTracker(dateProvider: dateProvider, scale: 0), + dateProvider: dateProvider, + delegate: self, + displayLinkWrapper: displayLink, + infoPlistWrapper: infoPlistWrapper + ) } func sessionReplayShouldCaptureReplayForError() -> Bool { @@ -397,7 +414,7 @@ class SentrySessionReplayTests: XCTestCase { let touchTracker = TestTouchTracker(dateProvider: fixture.dateProvider, scale: 1) let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1), touchTracker: touchTracker) sut.start(rootView: fixture.rootView, fullSession: true) - + //Starting session replay at time 0 Dynamic(sut).newFrame(nil) @@ -554,49 +571,141 @@ class SentrySessionReplayTests: XCTestCase { // MARK: - iOS 26 Liquid Glass Detection Tests - @available(iOS 26.0, *) - func testBlocksSessionReplayOnIOS26WithLiquidGlass() { + func testStart_withIOS26WithLiquidGlass_withDefaultConfiguration_shouldNotStartSessionReplay() throws { // This test will only run on iOS 26.0+ // It tests that session replay is blocked when Liquid Glass is detected + guard #available(iOS 26.0, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + // -- Arrange -- let fixture = Fixture() - let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)) - + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let sut = fixture.getSut(options: options) + + // -- Act -- // Attempt to start session replay sut.start(rootView: fixture.rootView, fullSession: true) - - // Verify that session replay did not actually start - // (it should have been blocked by isRunningInDangerousEnvironment) + + // -- Assert -- + // Verify that session replay did not actually starti + // (it should have been blocked by isInUnreliableEnvironment) XCTAssertFalse(fixture.displayLink.isRunning()) } - @available(iOS 26.0, *) - func testAllowsSessionReplayOnIOS26WhenDisabledViaOption() { + func testStart_withIOS26WithLiquidGlass_withEnableInUnreliableEnvironment_shouldStartSessionReplay() throws { // This test verifies that users can explicitly opt-in to session replay on iOS 26 + guard #available(iOS 26.0, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + + // -- Arrange -- let fixture = Fixture() + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - options.disableInDangerousEnvironment = false - let sut = fixture.getSut(options: options) - + let experimentalOptions = SentryExperimentalOptions() + experimentalOptions.enableSessionReplayInUnreliableEnvironment = true + let sut = fixture.getSut(options: options, experimentalOptions: experimentalOptions) + + // -- Act -- // Attempt to start session replay sut.start(rootView: fixture.rootView, fullSession: true) - + + // -- Assert -- // Verify that session replay started despite iOS 26 XCTAssertTrue(fixture.displayLink.isRunning()) } - func testAllowsSessionReplayOnIOS25AndEarlier() throws { + func testStart_withIOS18_withDefaultConfiguration_shouldStartSessionReplay() throws { // This test runs on iOS < 26 and verifies session replay works normally - if #available(iOS 26.0, *) { - throw XCTSkip("This test is for iOS < 26.0") + guard #unavailable(iOS 26.0) else { + throw XCTSkip("Test is disabled for this OS version") + } + + // -- Arrange -- + let fixture = Fixture() + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let sut = fixture.getSut(options: options) + + // -- Act -- + // Session replay should start normally on older iOS versions + sut.start(rootView: fixture.rootView, fullSession: true) + + // -- Assert -- + XCTAssertTrue(fixture.displayLink.isRunning()) + } + + func testStart_withIOS26BuiltWithOlderXcode_shouldStartSessionReplay() throws { + // This test verifies that session replay works on iOS 26 when built with Xcode < 26 + // (Liquid Glass is not used in this case) + guard #available(iOS 26.0, *) else { + throw XCTSkip("Test requires iOS 26.0+") } + // -- Arrange -- let fixture = Fixture() - let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)) + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "1640") // Xcode 16.4 - // Session replay should start normally on older iOS versions + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let sut = fixture.getSut(options: options) + + // -- Act -- sut.start(rootView: fixture.rootView, fullSession: true) - XCTAssertTrue(fixture.displayLink.isRunning()) + // -- Assert -- + XCTAssertTrue(fixture.displayLink.isRunning(), "SR should start when built with Xcode < 26") + } + + func testStart_withIOS26WithCompatibilityMode_shouldStartSessionReplay() throws { + // This test verifies that session replay works on iOS 26 when UIDesignRequiresCompatibility is YES + guard #available(iOS 26.0, *) else { + throw XCTSkip("Test requires iOS 26.0+") + } + + // -- Arrange -- + let fixture = Fixture() + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + fixture.infoPlistWrapper + .mockGetAppValueBooleanReturnValue(forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, value: true) + + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let sut = fixture.getSut(options: options) + + // -- Act -- + sut.start(rootView: fixture.rootView, fullSession: true) + + // -- Assert -- + XCTAssertTrue(fixture.displayLink.isRunning(), "SR should start when UIDesignRequiresCompatibility is YES") + } + + func testStart_withIOS26WithInvalidXcodeVersion_shouldNotStartSessionReplay() throws { + // This test verifies defensive behavior when Xcode version cannot be parsed + guard #available(iOS 26.0, *) else { + throw XCTSkip("Test requires iOS 26.0+") + } + + // -- Arrange -- + let fixture = Fixture() + fixture.infoPlistWrapper + .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "invalid_version") + + let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let sut = fixture.getSut(options: options) + + // -- Act -- + sut.start(rootView: fixture.rootView, fullSession: true) + + // -- Assert -- + XCTAssertFalse(fixture.displayLink.isRunning(), "SR should be blocked when Xcode version cannot be parsed (defensive approach)") } } diff --git a/sdk_api.json b/sdk_api.json index 29cce330b73..d38113ed0e7 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -46391,6 +46391,237 @@ } ] }, + { + "kind": "TypeDecl", + "name": "SentryInfoPlistKey", + "printedName": "SentryInfoPlistKey", + "children": [ + { + "kind": "Var", + "name": "xcodeVersion", + "printedName": "xcodeVersion", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryInfoPlistKey.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", + "mangledName": "$s6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", + "moduleName": "Sentry" + }, + { + "kind": "Var", + "name": "designRequiresCompatibility", + "printedName": "designRequiresCompatibility", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryInfoPlistKey.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", + "mangledName": "$s6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", + "moduleName": "Sentry" + }, + { + "kind": "Constructor", + "name": "init", + "printedName": "init(rawValue:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Sentry.SentryInfoPlistKey?", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Constructor", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", + "moduleName": "Sentry", + "init_kind": "Designated" + }, + { + "kind": "TypeAlias", + "name": "RawValue", + "printedName": "RawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "TypeAlias", + "usr": "s:6Sentry0A12InfoPlistKeyO8RawValuea", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8RawValuea", + "moduleName": "Sentry" + }, + { + "kind": "Var", + "name": "rawValue", + "printedName": "rawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvp", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvp", + "moduleName": "Sentry", + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvg", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvg", + "moduleName": "Sentry", + "accessorKind": "get" + } + ] + } + ], + "declKind": "Enum", + "usr": "s:6Sentry0A12InfoPlistKeyO", + "mangledName": "$s6Sentry0A12InfoPlistKeyO", + "moduleName": "Sentry", + "enumRawTypeName": "String", + "conformances": [ + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + }, + { + "kind": "Conformance", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ", + "mangledName": "$sSQ" + }, + { + "kind": "Conformance", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH", + "mangledName": "$sSH" + }, + { + "kind": "Conformance", + "name": "RawRepresentable", + "printedName": "RawRepresentable", + "children": [ + { + "kind": "TypeWitness", + "name": "RawValue", + "printedName": "RawValue", + "children": [ + { + "kind": "TypeNameAlias", + "name": "RawValue", + "printedName": "Sentry.SentryInfoPlistKey.RawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ] + } + ] + } + ], + "usr": "s:SY", + "mangledName": "$sSY" + } + ] + }, { "kind": "TypeDecl", "name": "SentryLog", @@ -48946,55 +49177,6 @@ } ] }, - { - "kind": "Var", - "name": "disableInDangerousEnvironment", - "printedName": "disableInDangerousEnvironment", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Var", - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", - "moduleName": "Sentry", - "static": true, - "declAttributes": [ - "Final", - "HasStorage" - ], - "isLet": true, - "hasStorage": true, - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", - "moduleName": "Sentry", - "static": true, - "implicit": true, - "declAttributes": [ - "Final" - ], - "accessorKind": "get" - } - ] - }, { "kind": "Var", "name": "maskedViewClasses", @@ -50527,79 +50709,6 @@ } ] }, - { - "kind": "Var", - "name": "disableInDangerousEnvironment", - "printedName": "disableInDangerousEnvironment", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Var", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)disableInDangerousEnvironment", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvp", - "moduleName": "Sentry", - "declAttributes": [ - "ObjC", - "HasStorage" - ], - "hasStorage": true, - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)disableInDangerousEnvironment", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvg", - "moduleName": "Sentry", - "implicit": true, - "declAttributes": [ - "ObjC" - ], - "accessorKind": "get" - }, - { - "kind": "Accessor", - "name": "Set", - "printedName": "Set()", - "children": [ - { - "kind": "TypeNominal", - "name": "Void", - "printedName": "()" - }, - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setDisableInDangerousEnvironment:", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvs", - "moduleName": "Sentry", - "implicit": true, - "declAttributes": [ - "ObjC" - ], - "accessorKind": "set" - } - ] - }, { "kind": "Constructor", "name": "init", @@ -50628,7 +50737,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:)", + "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:)", "children": [ { "kind": "TypeNominal", @@ -50671,13 +50780,6 @@ "hasDefaultArg": true, "usr": "s:Sb" }, - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "hasDefaultArg": true, - "usr": "s:Sb" - }, { "kind": "TypeNominal", "name": "Bool", @@ -50687,10 +50789,10 @@ } ], "declKind": "Constructor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", - "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9Rendering29disableInDangerousEnvironmentACSf_SfS5btcfc", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", + "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9RenderingACSf_SfS4btcfc", "moduleName": "Sentry", - "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", + "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", "declAttributes": [ "ObjC" ], @@ -57019,6 +57121,79 @@ } ] }, + { + "kind": "Var", + "name": "enableSessionReplayInUnreliableEnvironment", + "printedName": "enableSessionReplayInUnreliableEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(py)enableSessionReplayInUnreliableEnvironment", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvp", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)enableSessionReplayInUnreliableEnvironment", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)setEnableSessionReplayInUnreliableEnvironment:", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Var", "name": "enableLogs", diff --git a/sdk_api_V9.json b/sdk_api_V9.json index 099055b4ca1..c8118912cdb 100644 --- a/sdk_api_V9.json +++ b/sdk_api_V9.json @@ -42867,6 +42867,237 @@ } ] }, + { + "kind": "TypeDecl", + "name": "SentryInfoPlistKey", + "printedName": "SentryInfoPlistKey", + "children": [ + { + "kind": "Var", + "name": "xcodeVersion", + "printedName": "xcodeVersion", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryInfoPlistKey.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", + "mangledName": "$s6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", + "moduleName": "Sentry" + }, + { + "kind": "Var", + "name": "designRequiresCompatibility", + "printedName": "designRequiresCompatibility", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryInfoPlistKey.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", + "mangledName": "$s6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", + "moduleName": "Sentry" + }, + { + "kind": "Constructor", + "name": "init", + "printedName": "init(rawValue:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Sentry.SentryInfoPlistKey?", + "children": [ + { + "kind": "TypeNominal", + "name": "SentryInfoPlistKey", + "printedName": "Sentry.SentryInfoPlistKey", + "usr": "s:6Sentry0A12InfoPlistKeyO" + } + ], + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Constructor", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", + "moduleName": "Sentry", + "init_kind": "Designated" + }, + { + "kind": "TypeAlias", + "name": "RawValue", + "printedName": "RawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "TypeAlias", + "usr": "s:6Sentry0A12InfoPlistKeyO8RawValuea", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8RawValuea", + "moduleName": "Sentry" + }, + { + "kind": "Var", + "name": "rawValue", + "printedName": "rawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvp", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvp", + "moduleName": "Sentry", + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvg", + "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvg", + "moduleName": "Sentry", + "accessorKind": "get" + } + ] + } + ], + "declKind": "Enum", + "usr": "s:6Sentry0A12InfoPlistKeyO", + "mangledName": "$s6Sentry0A12InfoPlistKeyO", + "moduleName": "Sentry", + "enumRawTypeName": "String", + "conformances": [ + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + }, + { + "kind": "Conformance", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ", + "mangledName": "$sSQ" + }, + { + "kind": "Conformance", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH", + "mangledName": "$sSH" + }, + { + "kind": "Conformance", + "name": "RawRepresentable", + "printedName": "RawRepresentable", + "children": [ + { + "kind": "TypeWitness", + "name": "RawValue", + "printedName": "RawValue", + "children": [ + { + "kind": "TypeNameAlias", + "name": "RawValue", + "printedName": "Sentry.SentryInfoPlistKey.RawValue", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ] + } + ] + } + ], + "usr": "s:SY", + "mangledName": "$sSY" + } + ] + }, { "kind": "TypeDecl", "name": "SentryLog", @@ -45422,55 +45653,6 @@ } ] }, - { - "kind": "Var", - "name": "disableInDangerousEnvironment", - "printedName": "disableInDangerousEnvironment", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Var", - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvpZ", - "moduleName": "Sentry", - "static": true, - "declAttributes": [ - "Final", - "HasStorage" - ], - "isLet": true, - "hasStorage": true, - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC29disableInDangerousEnvironmentSbvgZ", - "moduleName": "Sentry", - "static": true, - "implicit": true, - "declAttributes": [ - "Final" - ], - "accessorKind": "get" - } - ] - }, { "kind": "Var", "name": "maskedViewClasses", @@ -47003,79 +47185,6 @@ } ] }, - { - "kind": "Var", - "name": "disableInDangerousEnvironment", - "printedName": "disableInDangerousEnvironment", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Var", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)disableInDangerousEnvironment", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvp", - "moduleName": "Sentry", - "declAttributes": [ - "ObjC", - "HasStorage" - ], - "hasStorage": true, - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)disableInDangerousEnvironment", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvg", - "moduleName": "Sentry", - "implicit": true, - "declAttributes": [ - "ObjC" - ], - "accessorKind": "get" - }, - { - "kind": "Accessor", - "name": "Set", - "printedName": "Set()", - "children": [ - { - "kind": "TypeNominal", - "name": "Void", - "printedName": "()" - }, - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" - } - ], - "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setDisableInDangerousEnvironment:", - "mangledName": "$s6Sentry0A13ReplayOptionsC29disableInDangerousEnvironmentSbvs", - "moduleName": "Sentry", - "implicit": true, - "declAttributes": [ - "ObjC" - ], - "accessorKind": "set" - } - ] - }, { "kind": "Constructor", "name": "init", @@ -47104,7 +47213,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:)", + "printedName": "init(sessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:)", "children": [ { "kind": "TypeNominal", @@ -47147,13 +47256,6 @@ "hasDefaultArg": true, "usr": "s:Sb" }, - { - "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "hasDefaultArg": true, - "usr": "s:Sb" - }, { "kind": "TypeNominal", "name": "Bool", @@ -47163,10 +47265,10 @@ } ], "declKind": "Constructor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", - "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9Rendering29disableInDangerousEnvironmentACSf_SfS5btcfc", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", + "mangledName": "$s6Sentry0A13ReplayOptionsC17sessionSampleRate07onErroreF011maskAllText0iJ6Images20enableViewRendererV20m4FastN9RenderingACSf_SfS4btcfc", "moduleName": "Sentry", - "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:disableInDangerousEnvironment:", + "objc_name": "initWithSessionSampleRate:onErrorSampleRate:maskAllText:maskAllImages:enableViewRendererV2:enableFastViewRendering:", "declAttributes": [ "ObjC" ], @@ -53464,6 +53566,79 @@ } ] }, + { + "kind": "Var", + "name": "enableSessionReplayInUnreliableEnvironment", + "printedName": "enableSessionReplayInUnreliableEnvironment", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(py)enableSessionReplayInUnreliableEnvironment", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvp", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)enableSessionReplayInUnreliableEnvironment", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)setEnableSessionReplayInUnreliableEnvironment:", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC42enableSessionReplayInUnreliableEnvironmentSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Var", "name": "enableLogs", From 84a933d8729382cb2e010cdea3001fabf184a6b0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 10 Oct 2025 14:17:37 +0200 Subject: [PATCH 05/22] cleanup --- Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist | 2 -- .../Swift/Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist b/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist index 853173fc8b2..b4f367bf79b 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/Info.plist @@ -33,8 +33,6 @@ UIApplicationSupportsIndirectInputEvents - UIDesignRequiresCompatibility - UILaunchScreen UIRequiredDeviceCapabilities diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 1efc09a1262..b21fdcde5aa 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -86,7 +86,7 @@ import UIKit // to PII leaks otherwise. if isEnvironmentUnreliable() { 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 `false`") + SentrySDKLog.fatal("[Session Replay] Detected environment potentially causing PII leaks, disabling Session Replay. To override this mechanism, set `options.experimental.enableSessionReplayInUnreliableEnvironment` to `true`") return } SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.enableInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") From 4d3f57a585c413e54bff2e9c5123a1013d09289b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 10 Oct 2025 14:21:04 +0200 Subject: [PATCH 06/22] add warnings to changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091cba3de34..f9469baaf75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ ## 8.56.2 +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Fixes - Fix crash from null UIApplication in SwiftUI apps (#6264) @@ -41,6 +44,9 @@ > [!Warning] > This version can cause runtime crashes because the `UIApplication.sharedApplication`/`NSApplication.sharedApplication` is not yet available during SDK initialization, due to the changes in [PR #5900](https://github.com/getsentry/sentry-cocoa/pull/5900), released in [8.56.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.56.0). +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Fixes - Fix potential app launch hang caused by the SentrySDK (#6181) @@ -52,6 +58,9 @@ > [!Warning] > This version can cause runtime crashes because the `UIApplication.sharedApplication`/`NSApplication.sharedApplication` is not yet available during SDK initialization, due to the changes in [PR #5900](https://github.com/getsentry/sentry-cocoa/pull/5900), released in [8.56.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.56.0). +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Features - Structured Logs: Flush logs on SDK flush/close (#5834) @@ -134,6 +143,9 @@ ## 8.55.1 +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Features ### Fixes @@ -160,6 +172,9 @@ > If your app does not need arm64e, you don't need to make any changes. > But if your app _needs arm64e_ please use `Sentry-Dynamic-WithARM64e` or `Sentry-WithoutUIKitOrAppKit-WithARM64e` from 8.55.0 so you don't have issues uploading to the App Store. +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Features - Add a new prebuilt framework with arm64e and remove it from the regular one (#5788) @@ -183,6 +198,9 @@ ## 8.54.0 +> [!Warning] +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. + ### Features - Add experimental support for capturing structured logs via `SentrySDK.logger` (#5532, #5593, #5639, #5628, #5637, #5643) From f30316e7e528e9c8c88194d46a5b97890e6ab251 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 10 Oct 2025 14:43:05 +0200 Subject: [PATCH 07/22] Update CHANGELOG.md Co-authored-by: Karl Heinz Struggl --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9469baaf75..439316cde1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ > [!Warning] > **Session Replay is disabled by default on iOS 26.0+ with Xcode 26.0+ to prevent PII leaks** > -> Due to masking issues introduced by Apple's Liquid Glass rendering changes in iOS 26.0, session replay is now **automatically disabled** on apps running iOS 26.0+ when built with Xcode 26.0 or later. This is a defensive measure to protect user privacy and prevent potential PII leaks until masking is reliably supported. +> Due to potential masking issues introduced by Apple's Liquid Glass rendering changes in iOS 26.0, Session Replay is now **automatically disabled** on apps running iOS 26.0+ when built with Xcode 26.0 or later. This is a defensive measure to protect user privacy and prevent potential PII leaks until masking is reliably supported. > > Session replay will work normally if: > From d9f7d6c158e526ec159ef506732e16fec2797541 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 10:42:29 +0200 Subject: [PATCH 08/22] refactor: Move Info.plist wrapper to subdirectory --- Sentry.xcodeproj/project.pbxproj | 17 +++++++++++++---- .../{ => InfoPlist}/SentryInfoPlistError.swift | 0 .../{ => InfoPlist}/SentryInfoPlistKey.swift | 0 .../SentryInfoPlistWrapper.swift | 0 .../SentryInfoPlistWrapperProvider.swift | 0 5 files changed, 13 insertions(+), 4 deletions(-) rename Sources/Swift/Helper/{ => InfoPlist}/SentryInfoPlistError.swift (100%) rename Sources/Swift/Helper/{ => InfoPlist}/SentryInfoPlistKey.swift (100%) rename Sources/Swift/Helper/{ => InfoPlist}/SentryInfoPlistWrapper.swift (100%) rename Sources/Swift/Helper/{ => InfoPlist}/SentryInfoPlistWrapperProvider.swift (100%) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 1cb763872c2..196c1c98102 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -2620,10 +2620,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( - D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */, - D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */, - D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */, - D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */, + D4F56C3C2E9CEFFB00D57DAB /* InfoPlist */, FAE579872E7D9D4900B710F9 /* SentrySysctl.swift */, FAE579BC2E7DDDE400B710F9 /* SentryThreadWrapper.swift */, FAE5797E2E7CF21300B710F9 /* SentryMigrateSessionInit.swift */, @@ -4352,6 +4349,18 @@ path = Recording; sourceTree = ""; }; + D4F56C3C2E9CEFFB00D57DAB /* InfoPlist */ = { + isa = PBXGroup; + children = ( + D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */, + D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */, + D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */, + D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */, + D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */, + ); + path = InfoPlist; + sourceTree = ""; + }; D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( diff --git a/Sources/Swift/Helper/SentryInfoPlistError.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistError.swift similarity index 100% rename from Sources/Swift/Helper/SentryInfoPlistError.swift rename to Sources/Swift/Helper/InfoPlist/SentryInfoPlistError.swift diff --git a/Sources/Swift/Helper/SentryInfoPlistKey.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift similarity index 100% rename from Sources/Swift/Helper/SentryInfoPlistKey.swift rename to Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift diff --git a/Sources/Swift/Helper/SentryInfoPlistWrapper.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift similarity index 100% rename from Sources/Swift/Helper/SentryInfoPlistWrapper.swift rename to Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift diff --git a/Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift similarity index 100% rename from Sources/Swift/Helper/SentryInfoPlistWrapperProvider.swift rename to Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift From d51937351aadd0ef99690748a10d7d1078a4b9b5 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 10:42:59 +0200 Subject: [PATCH 09/22] refactor: Add xcode versions as named constants --- Sentry.xcodeproj/project.pbxproj | 5 ++++- Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift | 3 +++ .../Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 196c1c98102..08224467982 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -859,6 +859,7 @@ D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4EE12D22DE9AC3800385BAF /* TestNSNotificationCenterWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; + D4F56C432E9CF1DC00D57DAB /* SentryXcodeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */; }; D4F7BD822E4373BF004A2D77 /* SentryLevelMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */; }; D4FC68172DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D4FC68162DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h */; }; D4FC681A2DD63465001B74FF /* SentryDispatchQueueWrapperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4FC68192DD63465001B74FF /* SentryDispatchQueueWrapperTests.m */; }; @@ -2195,6 +2196,7 @@ D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSNotificationCenterWrapperTests.swift; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; + D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryXcodeVersion.swift; sourceTree = ""; }; D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevelMapperTests.swift; sourceTree = ""; }; D4FC68162DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDispatchSourceProviderProtocol.h; path = include/SentryDispatchSourceProviderProtocol.h; sourceTree = ""; }; D4FC68192DD63465001B74FF /* SentryDispatchQueueWrapperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDispatchQueueWrapperTests.m; sourceTree = ""; }; @@ -4352,7 +4354,6 @@ D4F56C3C2E9CEFFB00D57DAB /* InfoPlist */ = { isa = PBXGroup; children = ( - D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */, D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */, D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */, D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */, @@ -4776,6 +4777,7 @@ F474CB872E2EC5040001DF41 /* Recovered References */ = { isa = PBXGroup; children = ( + D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */, ); name = "Recovered References"; sourceTree = ""; @@ -5653,6 +5655,7 @@ FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */, FAF1201A2E70C0EE006E1DA3 /* SentryEnvelopeHeaderHelper.m in Sources */, F49D419E2DEA3D0600D9244E /* SentryCrashExceptionApplicationHelper.m in Sources */, + D4F56C432E9CF1DC00D57DAB /* SentryXcodeVersion.swift in Sources */, D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */, 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, diff --git a/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift b/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift new file mode 100644 index 00000000000..383baedf2ff --- /dev/null +++ b/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift @@ -0,0 +1,3 @@ +enum SentryXcodeVersion: Int { + case xcode26 = 2_600 +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index b21fdcde5aa..673784c4526 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -441,7 +441,7 @@ import UIKit for: SentryInfoPlistKey.xcodeVersion.rawValue ) if let xcodeVersion = Int(xcodeVersionString) { - if xcodeVersion < 2_600 { + if xcodeVersion < SentryXcodeVersion.xcode26.rawValue { SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but built with Xcode \(xcodeVersionString) (< 26.0) - detected as reliable") return false } else { From aeaccb026dbc27b0219810c733812ad86cb17245 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 10:45:11 +0200 Subject: [PATCH 10/22] fix: add missing reference to SentryXcodeVersion --- Sentry.xcodeproj/project.pbxproj | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 08224467982..7fcbb4ef744 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -859,7 +859,7 @@ D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4EE12D22DE9AC3800385BAF /* TestNSNotificationCenterWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; - D4F56C432E9CF1DC00D57DAB /* SentryXcodeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */; }; + D4F56C5D2E9CF38900D57DAB /* SentryXcodeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F56C5C2E9CF38900D57DAB /* SentryXcodeVersion.swift */; }; D4F7BD822E4373BF004A2D77 /* SentryLevelMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */; }; D4FC68172DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D4FC68162DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h */; }; D4FC681A2DD63465001B74FF /* SentryDispatchQueueWrapperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4FC68192DD63465001B74FF /* SentryDispatchQueueWrapperTests.m */; }; @@ -2196,7 +2196,7 @@ D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4EE12D12DE9AC3300385BAF /* TestNSNotificationCenterWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSNotificationCenterWrapperTests.swift; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; - D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryXcodeVersion.swift; sourceTree = ""; }; + D4F56C5C2E9CF38900D57DAB /* SentryXcodeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryXcodeVersion.swift; sourceTree = ""; }; D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevelMapperTests.swift; sourceTree = ""; }; D4FC68162DD632E7001B74FF /* SentryDispatchSourceProviderProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDispatchSourceProviderProtocol.h; path = include/SentryDispatchSourceProviderProtocol.h; sourceTree = ""; }; D4FC68192DD63465001B74FF /* SentryDispatchQueueWrapperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDispatchQueueWrapperTests.m; sourceTree = ""; }; @@ -2816,7 +2816,6 @@ 6304360C1EC05CEF00C4D3FA /* Frameworks */, 6327C5D41EB8A783004E799B /* Products */, 7D826E3C2390840E00EED93D /* Utils */, - F474CB872E2EC5040001DF41 /* Recovered References */, ); indentWidth = 4; sourceTree = ""; @@ -4354,6 +4353,7 @@ D4F56C3C2E9CEFFB00D57DAB /* InfoPlist */ = { isa = PBXGroup; children = ( + D4F56C5C2E9CF38900D57DAB /* SentryXcodeVersion.swift */, D4599F832E98F4710045BB95 /* SentryInfoPlistKey.swift */, D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */, D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */, @@ -4774,14 +4774,6 @@ path = Tools; sourceTree = ""; }; - F474CB872E2EC5040001DF41 /* Recovered References */ = { - isa = PBXGroup; - children = ( - D4F56C422E9CF1D900D57DAB /* SentryXcodeVersion.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; F4FE9E062E6248BB0014FED5 /* SentryCrash */ = { isa = PBXGroup; children = ( @@ -5655,7 +5647,7 @@ FAEC270E2DF3526000878871 /* SentryUserFeedback.swift in Sources */, FAF1201A2E70C0EE006E1DA3 /* SentryEnvelopeHeaderHelper.m in Sources */, F49D419E2DEA3D0600D9244E /* SentryCrashExceptionApplicationHelper.m in Sources */, - D4F56C432E9CF1DC00D57DAB /* SentryXcodeVersion.swift in Sources */, + D4F56C5D2E9CF38900D57DAB /* SentryXcodeVersion.swift in Sources */, D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */, 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, From 8eafe999b30fd37b515eb0131de06ca6f8f5b790 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 11:03:52 +0200 Subject: [PATCH 11/22] fix: change keypath to option in suggestion --- .../Swift/Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 673784c4526..54c4fdc3599 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -89,7 +89,7 @@ import UIKit SentrySDKLog.fatal("[Session Replay] Detected environment potentially causing PII leaks, disabling Session Replay. To override this mechanism, set `options.experimental.enableSessionReplayInUnreliableEnvironment` to `true`") return } - SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.enableInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") + SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.experimental.enableInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") } displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) From 4ac19c788fada490e6f75469519f07708c3ecc89 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 11:05:18 +0200 Subject: [PATCH 12/22] fix: typo --- .../Swift/Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 54c4fdc3599..048ccf1c6da 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -448,7 +448,7 @@ import UIKit SentrySDKLog.debug("[Session Replay] Detected built with Xcode version: \(xcodeVersionString)") } } else { - SentrySDKLog.warning("[Session Replay] Found xcode version key but could not parse as Int: \(xcodeVersionString)") + SentrySDKLog.warning("[Session Replay] Found Xcode version key but could not parse as Int: \(xcodeVersionString)") } } catch SentryInfoPlistError.mainInfoPlistNotFound { // Can't read Info.plist - stay defensive From 5b3c2d3cbc88ce2e5be8f26163187854d72f1f9b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 16:15:02 +0200 Subject: [PATCH 13/22] refactored solution --- Sentry.xcodeproj/project.pbxproj | 20 + ...SessionReplayEnvironmentCheckerTests.swift | 21 + ...SessionReplayEnvironmentCheckerTests.swift | 63 +++ Sources/Sentry/SentryDependencyContainer.m | 2 + .../Sentry/SentrySessionReplayIntegration.m | 6 +- .../HybridPublic/SentryDependencyContainer.h | 3 + .../Helper/InfoPlist/SentryXcodeVersion.swift | 1 + .../SessionReplay/SentrySessionReplay.swift | 86 +--- ...entrySessionReplayEnvironmentChecker.swift | 182 ++++++++ ...sionReplayEnvironmentCheckerProvider.swift | 6 + ...SessionReplayEnvironmentCheckerTests.swift | 407 ++++++++++++++++++ .../SentrySessionReplayTests.swift | 140 +----- 12 files changed, 734 insertions(+), 203 deletions(-) create mode 100644 SentryTestUtils/TestSessionReplayEnvironmentCheckerTests.swift create mode 100644 SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 7fcbb4ef744..417259390df 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -787,6 +787,8 @@ D41415A72DEEE532003B14D5 /* SentryRedactViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41415A62DEEE532003B14D5 /* SentryRedactViewHelper.swift */; }; D4291A692DD61A3F00772088 /* SentryDispatchQueueProviderProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D4291A672DD61A3F00772088 /* SentryDispatchQueueProviderProtocol.h */; }; D4291A6D2DD62ACE00772088 /* SentryDispatchFactoryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4291A6C2DD62AC800772088 /* SentryDispatchFactoryTests.m */; }; + D42ADEEF2E9CF43200753166 /* SentrySessionReplayEnvironmentChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ADEE92E9CF42800753166 /* SentrySessionReplayEnvironmentChecker.swift */; }; + D42ADF372E9CF95700753166 /* SentrySessionReplayEnvironmentCheckerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42ADF362E9CF95500753166 /* SentrySessionReplayEnvironmentCheckerProvider.swift */; }; D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; D434DB092DE09CD000DD6F82 /* TestSentryWatchdogTerminationAttributesProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE722DDC8DB700AFF56F /* TestSentryWatchdogTerminationAttributesProcessor.swift */; }; D434DB0A2DE09CDB00DD6F82 /* TestSentryWatchdogTerminationBreadcrumbProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE742DDC8DC400AFF56F /* TestSentryWatchdogTerminationBreadcrumbProcessor.swift */; }; @@ -850,10 +852,13 @@ D4CBA2532DE06D1600581618 /* TestConstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CBA2512DE06D1600581618 /* TestConstantTests.swift */; }; D4CD2A802DE9F91900DA9F59 /* SentryRedactRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CD2A7C2DE9F91900DA9F59 /* SentryRedactRegion.swift */; }; D4CD2A812DE9F91900DA9F59 /* SentryRedactRegionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4CD2A7D2DE9F91900DA9F59 /* SentryRedactRegionType.swift */; }; + D4D0E1E82E9D040A00358814 /* SentrySessionReplayEnvironmentCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */; }; D4D12E7A2DFC608800DC45C4 /* SentryScreenshotOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */; }; D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; + D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */; }; + 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 */; }; D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; @@ -2121,6 +2126,8 @@ D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryNSDictionarySanitize+Tests.m"; sourceTree = ""; }; D4291A672DD61A3F00772088 /* SentryDispatchQueueProviderProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDispatchQueueProviderProtocol.h; path = include/SentryDispatchQueueProviderProtocol.h; sourceTree = ""; }; D4291A6C2DD62AC800772088 /* SentryDispatchFactoryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDispatchFactoryTests.m; sourceTree = ""; }; + D42ADEE92E9CF42800753166 /* SentrySessionReplayEnvironmentChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayEnvironmentChecker.swift; sourceTree = ""; }; + D42ADF362E9CF95500753166 /* SentrySessionReplayEnvironmentCheckerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayEnvironmentCheckerProvider.swift; sourceTree = ""; }; D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; D434DB0B2DE09CE700DD6F82 /* TestSentryWatchdogTerminationBreadcrumbProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryWatchdogTerminationBreadcrumbProcessorTests.swift; sourceTree = ""; }; @@ -2189,8 +2196,11 @@ D4CBA2512DE06D1600581618 /* TestConstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstantTests.swift; sourceTree = ""; }; D4CD2A7C2DE9F91900DA9F59 /* SentryRedactRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactRegion.swift; sourceTree = ""; }; D4CD2A7D2DE9F91900DA9F59 /* SentryRedactRegionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactRegionType.swift; sourceTree = ""; }; + D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptionsTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; + D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; 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 = ""; }; D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; @@ -4042,6 +4052,7 @@ 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */, 7B30B68126527C55006B2752 /* TestDisplayLinkWrapper.swift */, D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */, + D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */, 8E25C97425F8511A00DC215B /* TestRandom.swift */, 7BE3C7762445E50A00A38442 /* TestCurrentDateProvider.swift */, 7BDB03BE25136A7D00BAE198 /* TestSentryDispatchQueueWrapper.swift */, @@ -4338,6 +4349,7 @@ D43B0E5F2DE7416600EE3759 /* TestFileManagerTests.swift */, 62E75EB82E152953002EC91B /* InvocationsTests.swift */, D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */, + D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */, ); path = SentryTestUtilsTests; sourceTree = ""; @@ -4384,6 +4396,7 @@ D80694C12B7CC85800B820E6 /* SessionReplay */ = { isa = PBXGroup; children = ( + D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */, D49480D22DC23E8E00A3B6E9 /* SentryReplayTypeTests.swift */, D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, @@ -4701,6 +4714,8 @@ D8CAC02C2BA0663E00E38F34 /* SessionReplay */ = { isa = PBXGroup; children = ( + D42ADF362E9CF95500753166 /* SentrySessionReplayEnvironmentCheckerProvider.swift */, + D42ADEE92E9CF42800753166 /* SentrySessionReplayEnvironmentChecker.swift */, D81988C12BEC18710020E36C /* RRWeb */, D88B30A72D48D87F008DE513 /* Preview */, D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, @@ -5729,6 +5744,7 @@ 63FE70E720DA4C1000CDBAE8 /* SentryCrashMonitor.c in Sources */, 84354E1229BF944900CDBB8B /* SentryProfileTimeseries.m in Sources */, FA94E6912E6B92C100576666 /* SentryClientReport.swift in Sources */, + D42ADEEF2E9CF43200753166 /* SentrySessionReplayEnvironmentChecker.swift in Sources */, 7D5C441C237C2E1F00DAB0A3 /* SentryHub.m in Sources */, 84E13B842CBF1D91003B52EC /* SentryUserFeedbackWidgetButtonMegaphoneIconView.swift in Sources */, 63FE715920DA4C1100CDBAE8 /* SentryCrashCPU_x86_32.c in Sources */, @@ -5832,6 +5848,7 @@ FA21F0B42E4A2A80008B4E5A /* SentryAppState.swift in Sources */, FA67DD132DDBD4EA00896B02 /* NumberExtensions.swift in Sources */, FA67DD142DDBD4EA00896B02 /* SentryGraphicsImageRenderer.swift in Sources */, + D42ADF372E9CF95700753166 /* SentrySessionReplayEnvironmentCheckerProvider.swift in Sources */, FA4C32972DF7513F001D7B00 /* SentryExperimentalOptions.swift in Sources */, FA67DD152DDBD4EA00896B02 /* UrlSanitized.swift in Sources */, FA67DD162DDBD4EA00896B02 /* SentryViewPhotographer.swift in Sources */, @@ -6089,6 +6106,7 @@ 62CFD9A92C99741100834E1B /* SentryInvalidJSONStringTests.swift in Sources */, 7BB42EF124F3B7B700D7B39A /* SentrySession+Equality.m in Sources */, 7BF9EF8B2722D58700B5BBEF /* SentryInitializeForGettingSubclassesNotCalled.m in Sources */, + D4D0E1E82E9D040A00358814 /* SentrySessionReplayEnvironmentCheckerTests.swift in Sources */, 33042A1729DC2C4300C60085 /* SentryExtraContextProviderTests.swift in Sources */, D8137D54272B53070082656C /* TestSentrySpan.m in Sources */, 7BECF432261463E600D9826E /* SentryMechanismMetaTests.swift in Sources */, @@ -6367,6 +6385,7 @@ buildActionMask = 2147483647; files = ( 841325DF2BFED0510029228F /* TestFramesTracker.swift in Sources */, + D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */, 8431F01629B2851500D8DC56 /* TestSentryNSProcessInfoWrapper.swift in Sources */, 841325BC2BF4184B0029228F /* TestHub.swift in Sources */, 84EB21942BF01C6C00EDDA28 /* TestNSNotificationCenterWrapper.swift in Sources */, @@ -6415,6 +6434,7 @@ 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/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtils/TestSessionReplayEnvironmentCheckerTests.swift new file mode 100644 index 00000000000..5179000d7c6 --- /dev/null +++ b/SentryTestUtils/TestSessionReplayEnvironmentCheckerTests.swift @@ -0,0 +1,21 @@ +@_spi(Private) @testable import Sentry + +@_spi(Private) public class TestSessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider { + + public var isReliableInvocations = Invocations() + private var mockedIsReliableReturnValue: Bool? + + public init() {} + + public func isReliable() -> Bool { + isReliableInvocations.record(()) + guard let result = mockedIsReliableReturnValue else { + preconditionFailure("\(Self.self): No mocked return value set for isReliable()") + } + return result + } + + public func mockIsReliableReturnValue(_ returnValue: Bool) { + mockedIsReliableReturnValue = returnValue + } +} diff --git a/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift new file mode 100644 index 00000000000..8f8b839b61e --- /dev/null +++ b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift @@ -0,0 +1,63 @@ +import CwlPreconditionTesting +@_spi(Private) @testable import Sentry +@_spi(Private) @testable import SentryTestUtils +import XCTest + +class TestSessionReplayEnvironmentCheckerTests: XCTestCase { + + // MARK: - isReliable() + + func testGetAppValueString_withoutMockedValue_shouldFailWithPreconditionFailure() throws { + // -- Arrange -- + let sut = TestSessionReplayEnvironmentChecker() + // Don't mock any value for this key + + // -- Act -- + let e = catchBadInstruction { + _ = sut.isReliable() + } + + // -- Assert -- + XCTAssertNotNil(e) + } + + func testIsReliable_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { + // -- Arrange -- + let sut = TestSessionReplayEnvironmentChecker() + 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() + 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() + sut.mockIsReliableReturnValue(true) + + // -- Act -- + _ = sut.isReliable() + _ = sut.isReliable() + _ = sut.isReliable() + + // -- Assert -- + XCTAssertEqual(sut.isReliableInvocations.count, 3) + } +} diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index de8e4fe7140..3120abc4697 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -190,6 +190,8 @@ - (instancetype)init andRateLimitParser:rateLimitParser currentDateProvider:_dateProvider]; _infoPlistWrapper = [[SentryInfoPlistWrapper alloc] init]; + _sessionReplayEnvironmentChecker = [[SentrySessionReplayEnvironmentChecker alloc] + initWithInfoPlistWrapper:_infoPlistWrapper]; #if SENTRY_HAS_REACHABILITY _reachability = [[SentryReachability alloc] init]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 36e7efb7cfa..1f445244621 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -64,7 +64,7 @@ @implementation SentrySessionReplayIntegration { // replay absolutely needs segment 0 to make replay work. BOOL _rateLimited; id _dateProvider; - id _infoPlistWrapper; + id _environmentChecker; } - (instancetype)init @@ -140,7 +140,7 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; _dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; - _infoPlistWrapper = SentryDependencyContainer.sharedInstance.infoPlistWrapper; + _environmentChecker = SentryDependencyContainer.sharedInstance.sessionReplayEnvironmentChecker; // We use the dispatch queue provider as a factory to create the queues, but store the queues // directly in this instance, so they get deallocated when the integration is deallocated. @@ -424,7 +424,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions dateProvider:_dateProvider delegate:self displayLinkWrapper:displayLinkWrapper - infoPlistWrapper:_infoPlistWrapper]; + environmentChecker:_environmentChecker]; [self.sessionReplay startWithRootView:[SentryDependencyContainer.sharedInstance.application getWindows] diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index c2fe71a5c40..b1b628e6f82 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -36,6 +36,7 @@ @protocol SentryNSNotificationCenterWrapper; @protocol SentryObjCRuntimeWrapper; @protocol SentryInfoPlistWrapperProvider; +@protocol SentrySessionReplayEnvironmentCheckerProvider; #if SENTRY_HAS_METRIC_KIT @class SentryMXManager; @@ -96,6 +97,8 @@ SENTRY_NO_INIT @property (nonatomic, strong) id rateLimits; @property (nonatomic, strong) SentryThreadsafeApplication *threadsafeApplication; @property (nonatomic, strong) id infoPlistWrapper; +@property (nonatomic, strong) id + sessionReplayEnvironmentChecker; #if SENTRY_HAS_REACHABILITY @property (nonatomic, strong) SentryReachability *reachability; diff --git a/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift b/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift index 383baedf2ff..f745575eacc 100644 --- a/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift +++ b/Sources/Swift/Helper/InfoPlist/SentryXcodeVersion.swift @@ -1,3 +1,4 @@ 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 048ccf1c6da..778686f535d 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -37,7 +37,7 @@ import UIKit private let touchTracker: SentryTouchTracker? private let lock = NSLock() public var replayTags: [String: Any]? - private let infoPlistWrapper: SentryInfoPlistWrapperProvider + private let environmentChecker: SentrySessionReplayEnvironmentCheckerProvider var isRunning: Bool { displayLink.isRunning() @@ -57,7 +57,7 @@ import UIKit dateProvider: SentryCurrentDateProvider, delegate: SentrySessionReplayDelegate, displayLinkWrapper: SentryReplayDisplayLinkWrapper, - infoPlistWrapper: SentryInfoPlistWrapperProvider + environmentChecker: SentrySessionReplayEnvironmentCheckerProvider ) { self.replayOptions = replayOptions self.experimentalOptions = experimentalOptions @@ -69,7 +69,7 @@ import UIKit self.replayMaker = replayMaker self.breadcrumbConverter = breadcrumbConverter self.touchTracker = touchTracker - self.infoPlistWrapper = infoPlistWrapper + self.environmentChecker = environmentChecker } deinit { displayLink.invalidate() } @@ -84,7 +84,7 @@ import UIKit // 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 isEnvironmentUnreliable() { + if environmentChecker.isReliable() { 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 @@ -388,84 +388,6 @@ import UIKit replayMaker.addFrameAsync(timestamp: timestamp, maskedViewImage: maskedViewImage, forScreen: screen) } } - - // swiftlint:disable:next cyclomatic_complexity function_body_length - private func isEnvironmentUnreliable() -> 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) - - // First check: Are we even on iOS 26.0+? - guard #available(iOS 26.0, *) else { - // Not on iOS 26.0+ - safe to use Session Replay - SentrySDKLog.debug("[Session Replay] Running on iOS version prior to 26.0+ - detected reliable environment") - return false - } - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+") - - // Safety check 1: Is compatibility mode explicitly enabled? - do { - var error: NSError? - let requiresCompatibility = infoPlistWrapper.getAppValueBoolean( - for: SentryInfoPlistKey.designRequiresCompatibility.rawValue, - errorPtr: &error - ) - if let error = error as Error? { - throw error - } else if requiresCompatibility { - SentrySDKLog.debug("[Session Replay] Running with UIDesignRequiresCompatibility set to YES - detected as reliable") - return false - } else { - SentrySDKLog.debug("[Session Replay] Running with UIDesignRequiresCompatibility is set to NO") - } - } catch SentryInfoPlistError.mainInfoPlistNotFound { - // Can't read Info.plist - stay defensive - SentrySDKLog.warning("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - detected as unreliable") - return true - } catch SentryInfoPlistError.keyNotFound { - // Due to Objective-C using a return value of `nil` as an indicator for an error, - // we need to throw an error when the key was not found - SentrySDKLog.debug("[Session Replay] No UIDesignRequiresCompatibility found in Info.plist") - } catch { - SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error)") - } - - // Safety check 2: Was the app built with an older Xcode version? - // DTXcode format: Xcode 16.4 = "1640", Xcode 26.0 = "2600" - do { - let xcodeVersionString = try infoPlistWrapper.getAppValueString( - for: SentryInfoPlistKey.xcodeVersion.rawValue - ) - if let xcodeVersion = Int(xcodeVersionString) { - if xcodeVersion < SentryXcodeVersion.xcode26.rawValue { - SentrySDKLog.debug("[Session Replay] Running on iOS 26.0+ but built with Xcode \(xcodeVersionString) (< 26.0) - detected as reliable") - return false - } else { - SentrySDKLog.debug("[Session Replay] Detected built with Xcode version: \(xcodeVersionString)") - } - } else { - SentrySDKLog.warning("[Session Replay] Found Xcode version key but could not parse as Int: \(xcodeVersionString)") - } - } catch SentryInfoPlistError.mainInfoPlistNotFound { - // Can't read Info.plist - stay defensive - SentrySDKLog.warning("[Session Replay] Running on iOS 26.0+ but cannot read Info.plist - detected as unreliable") - return true - } catch SentryInfoPlistError.keyNotFound { - // Due to Objective-C using a return value of `nil` as an indicator for an error, - // we need to throw an error when the key was not found. - SentrySDKLog.warning("[Session Replay] Could not find xcode version key in Info.plist") - } catch { - SentrySDKLog.error("[Session Replay] Failed to read Info.plist: \(error)") - } - - // No safety conditions met - treat as unreliable - SentrySDKLog.warning("[Session Replay] Detected environment as unreliable") - return true - } } // swiftlint:enable type_body_length diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift new file mode 100644 index 00000000000..aa0b139b831 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift @@ -0,0 +1,182 @@ +#if canImport(MachO) +import MachO +#endif + +@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 + + @objc public 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) + // 3. The app was built with SDK < 26.0 + + // 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 + } + if checkSDKVersion() == .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 + } + } + + private func checkSDKVersion() -> Reliability { + // Check the LC_BUILD_VERSION load command in the main executable's Mach-O header + // to determine the SDK version used to build the app. + // SDK versions < 26.0 don't have Liquid Glass, so they're reliable. + + // Get the main executable's Mach-O header + guard let header = _dyld_get_image_header(0) else { + SentrySDKLog.warning("[Session Replay] Failed to get Mach-O header - unclear") + return .unclear + } + + // Determine header size based on magic number + let headerPtr = UnsafeRawPointer(header) + let magic = header.pointee.magic + let headerSize: Int + + // We only support 64-bit architectures for simplicity + guard magic == MH_MAGIC_64 || magic == MH_CIGAM_64 else { + SentrySDKLog.warning("[Session Replay] Unexpected Mach-O magic: 0x\(String(magic, radix: 16)) - unclear") + return .unclear + } + headerSize = MemoryLayout.size + + // Find the LC_BUILD_VERSION load command + var currentCmd = headerPtr.advanced(by: headerSize) + let ncmds = header.pointee.ncmds + + for _ in 0..> 16) & 0xFFFF + let minorVersion = (sdkVersion >> 8) & 0xFF + let patchVersion = sdkVersion & 0xFF + + SentrySDKLog.debug("[Session Replay] LC_BUILD_VERSION SDK: \(majorVersion).\(minorVersion).\(patchVersion)") + + // SDK version < 26.0 is reliable (no Liquid Glass) + if majorVersion >= 26 { + SentrySDKLog.debug("[Session Replay] SDK \(majorVersion).\(minorVersion) (>= 26.0) - unreliable") + return .unreliable + } + SentrySDKLog.debug("[Session Replay] SDK \(majorVersion).\(minorVersion) (< 26.0) - reliable") + return .reliable + } + + // LC_BUILD_VERSION not found - could be old binary format + SentrySDKLog.debug("[Session Replay] LC_BUILD_VERSION not found in Mach-O header - unclear") + return .unclear + } +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift new file mode 100644 index 00000000000..6a07a339337 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerProvider.swift @@ -0,0 +1,6 @@ +@_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 +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift new file mode 100644 index 00000000000..dc422a1f82b --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayEnvironmentCheckerTests.swift @@ -0,0 +1,407 @@ +@_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/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index adc1bab34cf..ffd7b5a0883 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -80,7 +80,7 @@ class SentrySessionReplayTests: XCTestCase { let rootView = UIView() let replayMaker = TestReplayMaker() let cacheFolder = FileManager.default.temporaryDirectory - let infoPlistWrapper = TestInfoPlistWrapper() + let environmentChecker = TestSessionReplayEnvironmentChecker() var breadcrumbs: [Breadcrumb]? var isFullSession = true @@ -93,9 +93,8 @@ class SentrySessionReplayTests: XCTestCase { override init() { super.init() - // Configure the SUT with fields available in real apps - infoPlistWrapper.mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "1640") - infoPlistWrapper.mockGetAppValueBooleanReturnValue(forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, value: false) + // By default we are testing a reliable environment so all of the functionality is enabled + environmentChecker.mockIsReliableReturnValue(true) } func getSut( @@ -114,7 +113,7 @@ class SentrySessionReplayTests: XCTestCase { dateProvider: dateProvider, delegate: self, displayLinkWrapper: displayLink, - infoPlistWrapper: infoPlistWrapper + environmentChecker: environmentChecker ) } @@ -565,50 +564,36 @@ class SentrySessionReplayTests: XCTestCase { XCTAssertEqual(fixture.displayLink.invalidateInvocations.count, 1) } - private func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { - XCTAssertEqual(sessionReplay.isFullSession, expected) - } - - // MARK: - iOS 26 Liquid Glass Detection Tests - - func testStart_withIOS26WithLiquidGlass_withDefaultConfiguration_shouldNotStartSessionReplay() throws { - // This test will only run on iOS 26.0+ - // It tests that session replay is blocked when Liquid Glass is detected - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test is disabled for this OS version") - } + func testStart_withUnreliableEnvironment_withoutOverrideOptionEnabled_shouldNotStart() { // -- Arrange -- let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + fixture.environmentChecker.mockIsReliableReturnValue(false) - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - let sut = fixture.getSut(options: options) + let options = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + let experimentalOptions = SentryExperimentalOptions() + experimentalOptions.enableSessionReplayInUnreliableEnvironment = false + + let sut = fixture.getSut(options: options, experimentalOptions: experimentalOptions) // -- Act -- // Attempt to start session replay sut.start(rootView: fixture.rootView, fullSession: true) // -- Assert -- - // Verify that session replay did not actually starti + // Verify that session replay did not actually start // (it should have been blocked by isInUnreliableEnvironment) XCTAssertFalse(fixture.displayLink.isRunning()) } - - func testStart_withIOS26WithLiquidGlass_withEnableInUnreliableEnvironment_shouldStartSessionReplay() throws { - // This test verifies that users can explicitly opt-in to session replay on iOS 26 - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test is disabled for this OS version") - } + func testStart_withUnreliableEnvironment_withOverrideOptionEnabled_shouldStart() { // -- Arrange -- let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") + fixture.environmentChecker.mockIsReliableReturnValue(false) - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) + let options = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) let experimentalOptions = SentryExperimentalOptions() - experimentalOptions.enableSessionReplayInUnreliableEnvironment = true + experimentalOptions.enableSessionReplayInUnreliableEnvironment = false + let sut = fixture.getSut(options: options, experimentalOptions: experimentalOptions) // -- Act -- @@ -616,96 +601,15 @@ class SentrySessionReplayTests: XCTestCase { sut.start(rootView: fixture.rootView, fullSession: true) // -- Assert -- - // Verify that session replay started despite iOS 26 + // Verify that session replay did not actually starti + // (it should have been blocked by isInUnreliableEnvironment) XCTAssertTrue(fixture.displayLink.isRunning()) } - - func testStart_withIOS18_withDefaultConfiguration_shouldStartSessionReplay() throws { - // This test runs on iOS < 26 and verifies session replay works normally - guard #unavailable(iOS 26.0) else { - throw XCTSkip("Test is disabled for this OS version") - } - // -- Arrange -- - let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") - - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - let sut = fixture.getSut(options: options) - - // -- Act -- - // Session replay should start normally on older iOS versions - sut.start(rootView: fixture.rootView, fullSession: true) + // MARK: - Helpers - // -- Assert -- - XCTAssertTrue(fixture.displayLink.isRunning()) - } - - func testStart_withIOS26BuiltWithOlderXcode_shouldStartSessionReplay() throws { - // This test verifies that session replay works on iOS 26 when built with Xcode < 26 - // (Liquid Glass is not used in this case) - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // -- Arrange -- - let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "1640") // Xcode 16.4 - - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - let sut = fixture.getSut(options: options) - - // -- Act -- - sut.start(rootView: fixture.rootView, fullSession: true) - - // -- Assert -- - XCTAssertTrue(fixture.displayLink.isRunning(), "SR should start when built with Xcode < 26") - } - - func testStart_withIOS26WithCompatibilityMode_shouldStartSessionReplay() throws { - // This test verifies that session replay works on iOS 26 when UIDesignRequiresCompatibility is YES - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // -- Arrange -- - let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "2600") - fixture.infoPlistWrapper - .mockGetAppValueBooleanReturnValue(forKey: SentryInfoPlistKey.designRequiresCompatibility.rawValue, value: true) - - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - let sut = fixture.getSut(options: options) - - // -- Act -- - sut.start(rootView: fixture.rootView, fullSession: true) - - // -- Assert -- - XCTAssertTrue(fixture.displayLink.isRunning(), "SR should start when UIDesignRequiresCompatibility is YES") - } - - func testStart_withIOS26WithInvalidXcodeVersion_shouldNotStartSessionReplay() throws { - // This test verifies defensive behavior when Xcode version cannot be parsed - guard #available(iOS 26.0, *) else { - throw XCTSkip("Test requires iOS 26.0+") - } - - // -- Arrange -- - let fixture = Fixture() - fixture.infoPlistWrapper - .mockGetAppValueStringReturnValue(forKey: SentryInfoPlistKey.xcodeVersion.rawValue, value: "invalid_version") - - let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1) - let sut = fixture.getSut(options: options) - - // -- Act -- - sut.start(rootView: fixture.rootView, fullSession: true) - - // -- Assert -- - XCTAssertFalse(fixture.displayLink.isRunning(), "SR should be blocked when Xcode version cannot be parsed (defensive approach)") + private func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { + XCTAssertEqual(sessionReplay.isFullSession, expected) } } From c04500dca2edc7cbc7df7a9b9c6dc0d102943e78 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 16:26:04 +0200 Subject: [PATCH 14/22] added assertion messages; cleanup --- CHANGELOG.md | 12 +-- Sentry.xcodeproj/project.pbxproj | 8 +- ...TestSessionReplayEnvironmentChecker.swift} | 0 .../TestInfoPlistWrapperTests.swift | 82 +++++++++---------- 4 files changed, 51 insertions(+), 51 deletions(-) rename SentryTestUtils/{TestSessionReplayEnvironmentCheckerTests.swift => TestSessionReplayEnvironmentChecker.swift} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439316cde1c..5bdeebc7ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ ## 8.56.2 > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Fixes @@ -45,7 +45,7 @@ > This version can cause runtime crashes because the `UIApplication.sharedApplication`/`NSApplication.sharedApplication` is not yet available during SDK initialization, due to the changes in [PR #5900](https://github.com/getsentry/sentry-cocoa/pull/5900), released in [8.56.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.56.0). > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Fixes @@ -59,7 +59,7 @@ > This version can cause runtime crashes because the `UIApplication.sharedApplication`/`NSApplication.sharedApplication` is not yet available during SDK initialization, due to the changes in [PR #5900](https://github.com/getsentry/sentry-cocoa/pull/5900), released in [8.56.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.56.0). > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Features @@ -144,7 +144,7 @@ ## 8.55.1 > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Features @@ -173,7 +173,7 @@ > But if your app _needs arm64e_ please use `Sentry-Dynamic-WithARM64e` or `Sentry-WithoutUIKitOrAppKit-WithARM64e` from 8.55.0 so you don't have issues uploading to the App Store. > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Features @@ -199,7 +199,7 @@ ## 8.54.0 > [!Warning] -> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later. +> Session Replay in this version does not correctly mask views when built with Xcode 26 and running on iOS 26 with Liquid Glass, which may lead to PII leaks. Please upgrade to 8.57.0 or later, which automatically **disables session replay** in such environments. ### Features diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 417259390df..8afdc24bb0f 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -857,7 +857,7 @@ D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; - D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */; }; + D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */; }; 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 */; }; @@ -2199,7 +2199,7 @@ D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptionsTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; - D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; + D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentChecker.swift; 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 = ""; }; @@ -4052,7 +4052,7 @@ 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */, 7B30B68126527C55006B2752 /* TestDisplayLinkWrapper.swift */, D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */, - D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */, + D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */, 8E25C97425F8511A00DC215B /* TestRandom.swift */, 7BE3C7762445E50A00A38442 /* TestCurrentDateProvider.swift */, 7BDB03BE25136A7D00BAE198 /* TestSentryDispatchQueueWrapper.swift */, @@ -6385,7 +6385,7 @@ buildActionMask = 2147483647; files = ( 841325DF2BFED0510029228F /* TestFramesTracker.swift in Sources */, - D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift in Sources */, + D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentChecker.swift in Sources */, 8431F01629B2851500D8DC56 /* TestSentryNSProcessInfoWrapper.swift in Sources */, 841325BC2BF4184B0029228F /* TestHub.swift in Sources */, 84EB21942BF01C6C00EDDA28 /* TestNSNotificationCenterWrapper.swift in Sources */, diff --git a/SentryTestUtils/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtils/TestSessionReplayEnvironmentChecker.swift similarity index 100% rename from SentryTestUtils/TestSessionReplayEnvironmentCheckerTests.swift rename to SentryTestUtils/TestSessionReplayEnvironmentChecker.swift diff --git a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift index bed8de4694e..d8c209bfd93 100644 --- a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift +++ b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift @@ -22,7 +22,7 @@ class TestInfoPlistWrapperTests: XCTestCase { } // -- Assert -- - XCTAssertNotNil(e) + XCTAssertNotNil(e, "Should trigger precondition failure when accessing unmocked key") } func testGetAppValueString_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { @@ -34,7 +34,7 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = try sut.getAppValueString(for: "key") // -- Assert -- - XCTAssertEqual(result, "value") + XCTAssertEqual(result, "value", "Should return the mocked value") } func testGetAppValueString_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws { @@ -47,8 +47,8 @@ class TestInfoPlistWrapperTests: XCTestCase { let result2 = try sut.getAppValueString(for: "key1") // -- Assert -- - XCTAssertEqual(result1, "value1") - XCTAssertEqual(result2, "value1") + XCTAssertEqual(result1, "value1", "First invocation should return mocked value") + XCTAssertEqual(result2, "value1", "Second invocation should return same mocked value") } func testGetAppValueString_shouldRecordInvocations() throws { @@ -64,10 +64,10 @@ class TestInfoPlistWrapperTests: XCTestCase { _ = try sut.getAppValueString(for: "key3") // -- Assert -- - XCTAssertEqual(sut.getAppValueStringInvocations.count, 3) - XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1") - XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 1), "key2") - XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 2), "key3") + XCTAssertEqual(sut.getAppValueStringInvocations.count, 3, "Should record all three invocations") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1", "First invocation should be for key1") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 1), "key2", "Second invocation should be for key2") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 2), "key3", "Third invocation should be for key3") } func testGetAppValueString_withDifferentKeys_shouldReturnDifferentValues() throws { @@ -81,9 +81,9 @@ class TestInfoPlistWrapperTests: XCTestCase { let result2 = try sut.getAppValueString(for: "key2") // -- Assert -- - XCTAssertEqual(result1, "value1") - XCTAssertEqual(result2, "value2") - XCTAssertEqual(sut.getAppValueStringInvocations.count, 2) + XCTAssertEqual(result1, "value1", "Should return value1 for key1") + XCTAssertEqual(result2, "value2", "Should return value2 for key2") + XCTAssertEqual(sut.getAppValueStringInvocations.count, 2, "Should record both invocations") } func testGetAppValueString_withFailureResult_shouldThrowError() { @@ -97,7 +97,7 @@ class TestInfoPlistWrapperTests: XCTestCase { XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)") return } - XCTAssertEqual(key, "testKey") + XCTAssertEqual(key, "testKey", "Error should contain the expected key") } } @@ -121,9 +121,9 @@ class TestInfoPlistWrapperTests: XCTestCase { XCTFail("Expected SentryInfoPlistError.unableToCastValue, got \(error)") return } - XCTAssertEqual(key, "castKey") - XCTAssertEqual(value as? Int, 123) - XCTAssertTrue(type == String.self) + XCTAssertEqual(key, "castKey", "Error should contain the correct key") + XCTAssertEqual(value as? Int, 123, "Error should contain the correct value") + XCTAssertTrue(type == String.self, "Error should contain the correct type") } } @@ -136,8 +136,8 @@ class TestInfoPlistWrapperTests: XCTestCase { _ = try? sut.getAppValueString(for: "key1") // -- Assert -- - XCTAssertEqual(sut.getAppValueStringInvocations.count, 1) - XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1") + XCTAssertEqual(sut.getAppValueStringInvocations.count, 1, "Should record invocation even when throwing error") + XCTAssertEqual(sut.getAppValueStringInvocations.invocations.element(at: 0), "key1", "Should record the correct key") } // MARK: - getAppValueBoolean(for:errorPtr:) @@ -154,7 +154,7 @@ class TestInfoPlistWrapperTests: XCTestCase { } // -- Assert -- - XCTAssertNotNil(e) + XCTAssertNotNil(e, "Should trigger precondition failure when accessing unmocked key") } func testGetAppValueBoolean_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { @@ -167,8 +167,8 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) // -- Assert -- - XCTAssertTrue(result) - XCTAssertNil(error) + XCTAssertTrue(result, "Should return the mocked boolean value") + XCTAssertNil(error, "Should not set error when returning success") } func testGetAppValueBoolean_withMockedValue_withMultipleInvocations_shouldReturnSameValue() { @@ -184,10 +184,10 @@ class TestInfoPlistWrapperTests: XCTestCase { let result2 = sut.getAppValueBoolean(for: "key1", errorPtr: &error2) // -- Assert -- - XCTAssertTrue(result1) - XCTAssertNil(error1) - XCTAssertTrue(result2) - XCTAssertNil(error2) + XCTAssertTrue(result1, "First invocation should return mocked value") + XCTAssertNil(error1, "First invocation should not set error") + XCTAssertTrue(result2, "Second invocation should return same mocked value") + XCTAssertNil(error2, "Second invocation should not set error") } func testGetAppValueBoolean_withFalseValue_shouldReturnFalse() { @@ -200,8 +200,8 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) // -- Assert -- - XCTAssertFalse(result) - XCTAssertNil(error) + XCTAssertFalse(result, "Should return false when mocked with false") + XCTAssertNil(error, "Should not set error when returning success") } func testGetAppValueBoolean_withFailureResult_shouldReturnFalseAndSetError() { @@ -215,11 +215,11 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = sut.getAppValueBoolean(for: "key", errorPtr: &error) // -- Assert -- - XCTAssertFalse(result) - XCTAssertNotNil(error) - XCTAssertEqual(error?.domain, "TestDomain") - XCTAssertEqual(error?.code, 123) - XCTAssertEqual(error?.localizedDescription, "Test error") + XCTAssertFalse(result, "Should return false when mocked to throw error") + XCTAssertNotNil(error, "Should set error pointer when returning failure") + XCTAssertEqual(error?.domain, "TestDomain", "Error should have correct domain") + XCTAssertEqual(error?.code, 123, "Error should have correct code") + XCTAssertEqual(error?.localizedDescription, "Test error", "Error should have correct description") } func testGetAppValueBoolean_withFailureResult_withNilErrorPointer_shouldReturnFalse() { @@ -232,7 +232,7 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = sut.getAppValueBoolean(for: "key", errorPtr: nil) // -- Assert -- - XCTAssertFalse(result) + XCTAssertFalse(result, "Should return false even when error pointer is nil") // No crash should occur when error pointer is nil } @@ -254,10 +254,10 @@ class TestInfoPlistWrapperTests: XCTestCase { _ = sut.getAppValueBoolean(for: "key3", errorPtr: &error3) // -- Assert -- - XCTAssertEqual(sut.getAppValueBooleanInvocations.count, 3) - XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 0)?.0, "key1") - XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 1)?.0, "key2") - XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 2)?.0, "key3") + XCTAssertEqual(sut.getAppValueBooleanInvocations.count, 3, "Should record all three invocations") + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 0)?.0, "key1", "First invocation should be for key1") + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 1)?.0, "key2", "Second invocation should be for key2") + XCTAssertEqual(sut.getAppValueBooleanInvocations.invocations.element(at: 2)?.0, "key3", "Third invocation should be for key3") } func testGetAppValueBoolean_withSuccessResult_withNilErrorPointer_shouldReturnTrue() { @@ -269,7 +269,7 @@ class TestInfoPlistWrapperTests: XCTestCase { let result = sut.getAppValueBoolean(for: "key", errorPtr: nil) // -- Assert -- - XCTAssertTrue(result) + XCTAssertTrue(result, "Should return true even when error pointer is nil") // No crash should occur when error pointer is nil } @@ -287,9 +287,9 @@ class TestInfoPlistWrapperTests: XCTestCase { let result2 = sut.getAppValueBoolean(for: "key2", errorPtr: &error2) // -- Assert -- - XCTAssertTrue(result1) - XCTAssertNil(error1) - XCTAssertFalse(result2) - XCTAssertNil(error2) + XCTAssertTrue(result1, "Should return true for key1") + XCTAssertNil(error1, "Should not set error for key1") + XCTAssertFalse(result2, "Should return false for key2") + XCTAssertNil(error2, "Should not set error for key2") } } From 35023e3d895c6dea349fe3b43ce574f0393541b9 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 16:32:09 +0200 Subject: [PATCH 15/22] fix inverted reliability check --- .../Integrations/SessionReplay/SentrySessionReplay.swift | 4 ++-- .../SessionReplay/SentrySessionReplayTests.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 778686f535d..3a0800d736d 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -84,12 +84,12 @@ import UIKit // 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() { + if !environmentChecker.isReliable() { 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 } - SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.experimental.enableInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") + SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.experimental.enableSessionReplayInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") } displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index ffd7b5a0883..4bcd0bfd7e6 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -592,7 +592,7 @@ class SentrySessionReplayTests: XCTestCase { let options = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) let experimentalOptions = SentryExperimentalOptions() - experimentalOptions.enableSessionReplayInUnreliableEnvironment = false + experimentalOptions.enableSessionReplayInUnreliableEnvironment = true let sut = fixture.getSut(options: options, experimentalOptions: experimentalOptions) @@ -601,9 +601,9 @@ class SentrySessionReplayTests: XCTestCase { sut.start(rootView: fixture.rootView, fullSession: true) // -- Assert -- - // Verify that session replay did not actually starti - // (it should have been blocked by isInUnreliableEnvironment) - XCTAssertTrue(fixture.displayLink.isRunning()) + // Verify that session replay started despite unreliable environment + // (override option is enabled) + XCTAssertTrue(fixture.displayLink.isRunning(), "Session replay should start when override option is enabled") } // MARK: - Helpers From dc14e528992290ee2901d1c8e84e8b628210c756 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 16:40:05 +0200 Subject: [PATCH 16/22] remove public visibility of SentryInfoPlistKey --- Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift index 01406d863da..10cf6607acd 100644 --- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift +++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift @@ -1,4 +1,4 @@ -public enum SentryInfoPlistKey: String { +enum SentryInfoPlistKey: String { /// Key used to set the Xcode version used to build app case xcodeVersion = "DTXcode" From 34ae80841dcbf2a03af73083e8b1f06889854bec Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 16:52:54 +0200 Subject: [PATCH 17/22] Remove SDK detection fallback using LC_BUILD_VERSION --- ...entrySessionReplayEnvironmentChecker.swift | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift index aa0b139b831..d4f099797e4 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplayEnvironmentChecker.swift @@ -1,7 +1,3 @@ -#if canImport(MachO) -import MachO -#endif - @objc @_spi(Private) public class SentrySessionReplayEnvironmentChecker: NSObject, SentrySessionReplayEnvironmentCheckerProvider { /// Represents the reliability assessment of the environment for Session Replay. private enum Reliability { @@ -30,7 +26,6 @@ import MachO // 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) - // 3. The app was built with SDK < 26.0 // Run all checks and return true (reliable) if ANY check confirms reliability if checkIOSVersion() == .reliable { @@ -42,9 +37,6 @@ import MachO if checkXcodeVersion() == .reliable { return true } - if checkSDKVersion() == .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") @@ -118,65 +110,4 @@ import MachO return .unclear } } - - private func checkSDKVersion() -> Reliability { - // Check the LC_BUILD_VERSION load command in the main executable's Mach-O header - // to determine the SDK version used to build the app. - // SDK versions < 26.0 don't have Liquid Glass, so they're reliable. - - // Get the main executable's Mach-O header - guard let header = _dyld_get_image_header(0) else { - SentrySDKLog.warning("[Session Replay] Failed to get Mach-O header - unclear") - return .unclear - } - - // Determine header size based on magic number - let headerPtr = UnsafeRawPointer(header) - let magic = header.pointee.magic - let headerSize: Int - - // We only support 64-bit architectures for simplicity - guard magic == MH_MAGIC_64 || magic == MH_CIGAM_64 else { - SentrySDKLog.warning("[Session Replay] Unexpected Mach-O magic: 0x\(String(magic, radix: 16)) - unclear") - return .unclear - } - headerSize = MemoryLayout.size - - // Find the LC_BUILD_VERSION load command - var currentCmd = headerPtr.advanced(by: headerSize) - let ncmds = header.pointee.ncmds - - for _ in 0..> 16) & 0xFFFF - let minorVersion = (sdkVersion >> 8) & 0xFF - let patchVersion = sdkVersion & 0xFF - - SentrySDKLog.debug("[Session Replay] LC_BUILD_VERSION SDK: \(majorVersion).\(minorVersion).\(patchVersion)") - - // SDK version < 26.0 is reliable (no Liquid Glass) - if majorVersion >= 26 { - SentrySDKLog.debug("[Session Replay] SDK \(majorVersion).\(minorVersion) (>= 26.0) - unreliable") - return .unreliable - } - SentrySDKLog.debug("[Session Replay] SDK \(majorVersion).\(minorVersion) (< 26.0) - reliable") - return .reliable - } - - // LC_BUILD_VERSION not found - could be old binary format - SentrySDKLog.debug("[Session Replay] LC_BUILD_VERSION not found in Mach-O header - unclear") - return .unclear - } } From 8c4c5484dfcdc6a97ae593ee7b21768f13bdcd4d Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 17:27:21 +0200 Subject: [PATCH 18/22] Remove CwlPreconditionTesting due to incompatibility with tvOS and not enough reason to adopt it --- Sentry.xcodeproj/project.pbxproj | 42 +++++++++++-------- .../TestInfoPlistWrapperTests.swift | 34 --------------- ...SessionReplayEnvironmentCheckerTests.swift | 15 ------- 3 files changed, 25 insertions(+), 66 deletions(-) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 8afdc24bb0f..c57670e6f0a 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -820,7 +820,6 @@ D4599F8B2E98FE9F0045BB95 /* SentryInfoPlistWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */; }; D4599F8D2E990F960045BB95 /* TestInfoPlistWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8C2E990F920045BB95 /* TestInfoPlistWrapper.swift */; }; D4599F8F2E99113E0045BB95 /* TestInfoPlistWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4599F8E2E9911380045BB95 /* TestInfoPlistWrapperTests.swift */; }; - D4599F922E9913B20045BB95 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D4599F912E9913B20045BB95 /* CwlPreconditionTesting */; }; D45B4AF52E019E1A00C31DFB /* TestSentryViewRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45B4AF42E019E1500C31DFB /* TestSentryViewRenderer.swift */; }; D45B4AF72E01A10100C31DFB /* TestSentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45B4AF62E01A0FA00C31DFB /* TestSentryViewPhotographer.swift */; }; D45CE9752E5F454E00BFEDB2 /* SentryScreenshotSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45CE9742E5F454300BFEDB2 /* SentryScreenshotSource.swift */; }; @@ -832,6 +831,10 @@ D473ACD72D8090FC000F1CC6 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */; }; D480F9D92DE47A50009A0594 /* TestSentryScopePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */; }; D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */; }; + D483AF9A2E9D4E3D00B43C27 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */; }; + D483AF9C2E9D4E3D00B43C27 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */; }; + D483AF9F2E9D4E5500B43C27 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */; }; + D483AFA22E9D4E6600B43C27 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = D483AFA12E9D4E6600B43C27 /* CwlCatchException */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */; }; @@ -2544,8 +2547,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D4599F922E9913B20045BB95 /* CwlPreconditionTesting in Frameworks */, + D483AFA22E9D4E6600B43C27 /* CwlCatchException in Frameworks */, + D483AF9F2E9D4E5500B43C27 /* CwlPosixPreconditionTesting in Frameworks */, + D483AF9A2E9D4E3D00B43C27 /* CwlPosixPreconditionTesting in Frameworks */, D4CBA2472DE06D0200581618 /* libSentryTestUtils.a in Frameworks */, + D483AF9C2E9D4E3D00B43C27 /* CwlPreconditionTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5379,7 +5385,10 @@ ); name = SentryTestUtilsTests; packageProductDependencies = ( - D4599F912E9913B20045BB95 /* CwlPreconditionTesting */, + D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */, + D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */, + D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */, + D483AFA12E9D4E6600B43C27 /* CwlCatchException */, ); productName = SentryTestUtilsTests; productReference = D4CBA2432DE06D0200581618 /* SentryTestUtilsTests.xctest */; @@ -5492,7 +5501,6 @@ ); mainGroup = 6327C5C91EB8A783004E799B; packageReferences = ( - D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */, ); productRefGroup = 6327C5D41EB8A783004E799B /* Products */; projectDirPath = ""; @@ -8926,23 +8934,23 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/mattgallagher/CwlPreconditionTesting.git"; - requirement = { - kind = exactVersion; - version = 2.2.2; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - D4599F912E9913B20045BB95 /* CwlPreconditionTesting */ = { + D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */ = { + isa = XCSwiftPackageProductDependency; + productName = CwlPosixPreconditionTesting; + }; + D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */ = { isa = XCSwiftPackageProductDependency; - package = D4599F902E9913B20045BB95 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */; productName = CwlPreconditionTesting; }; + D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */ = { + isa = XCSwiftPackageProductDependency; + productName = CwlPosixPreconditionTesting; + }; + D483AFA12E9D4E6600B43C27 /* CwlCatchException */ = { + isa = XCSwiftPackageProductDependency; + productName = CwlCatchException; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 6327C5CA1EB8A783004E799B /* Project object */; diff --git a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift index d8c209bfd93..22af913ebdf 100644 --- a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift +++ b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift @@ -1,4 +1,3 @@ -import CwlPreconditionTesting @_spi(Private) @testable import Sentry @_spi(Private) @testable import SentryTestUtils import XCTest @@ -7,24 +6,6 @@ class TestInfoPlistWrapperTests: XCTestCase { // MARK: - getAppValueString(for:) - func testGetAppValueString_withoutMockedValue_shouldFailWithPreconditionFailure() throws { - // -- Arrange -- - let sut = TestInfoPlistWrapper() - // Don't mock any value for this key - - // -- Act -- - let e = catchBadInstruction { - do { - _ = try sut.getAppValueString(for: "unmockedKey") - } catch { - // noop - } - } - - // -- Assert -- - XCTAssertNotNil(e, "Should trigger precondition failure when accessing unmocked key") - } - func testGetAppValueString_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { // -- Arrange -- let sut = TestInfoPlistWrapper() @@ -142,21 +123,6 @@ class TestInfoPlistWrapperTests: XCTestCase { // MARK: - getAppValueBoolean(for:errorPtr:) - func testGetAppValueBoolean_withoutMockedValue_shouldFailWithPreconditionFailure() throws { - // -- Arrange -- - let sut = TestInfoPlistWrapper() - // Don't mock any value for this key - - // -- Act -- - let e = catchBadInstruction { - var error: NSError? - _ = sut.getAppValueBoolean(for: "unmockedKey", errorPtr: &error) - } - - // -- Assert -- - XCTAssertNotNil(e, "Should trigger precondition failure when accessing unmocked key") - } - func testGetAppValueBoolean_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { // -- Arrange -- let sut = TestInfoPlistWrapper() diff --git a/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift index 8f8b839b61e..4669c76dc49 100644 --- a/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift +++ b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift @@ -1,4 +1,3 @@ -import CwlPreconditionTesting @_spi(Private) @testable import Sentry @_spi(Private) @testable import SentryTestUtils import XCTest @@ -7,20 +6,6 @@ class TestSessionReplayEnvironmentCheckerTests: XCTestCase { // MARK: - isReliable() - func testGetAppValueString_withoutMockedValue_shouldFailWithPreconditionFailure() throws { - // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker() - // Don't mock any value for this key - - // -- Act -- - let e = catchBadInstruction { - _ = sut.isReliable() - } - - // -- Assert -- - XCTAssertNotNil(e) - } - func testIsReliable_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { // -- Arrange -- let sut = TestSessionReplayEnvironmentChecker() From eaf3fb91986bf1700f4b54d6b22343d5501709a9 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Oct 2025 17:44:49 +0200 Subject: [PATCH 19/22] smaller fixes and cleanup --- CHANGELOG.md | 2 +- Sentry.xcodeproj/project.pbxproj | 35 +--- SentryTestUtils/TestInfoPlistWrapper.swift | 17 +- .../TestSessionReplayEnvironmentChecker.swift | 13 +- .../TestInfoPlistWrapperTests.swift | 21 ++ ...SessionReplayEnvironmentCheckerTests.swift | 25 ++- Sources/Sentry/SentryDelayedFramesTracker.m | 27 ++- Sources/Sentry/SentryFramesTracker.m | 9 +- .../include/SentryDelayedFramesTracker.h | 4 +- .../SentryFramesTrackerTests.swift | 198 +++++++++++++++--- .../SentrySessionReplayTests.swift | 4 +- 11 files changed, 260 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdeebc7ab7..0ad653695c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ ### Fixes -- fix(session-replay): Add detection for potential PII leaks disabling session replay (#6389) +- Fix wrong Frame Delay when becoming active, which lead to false reported app hangs when the app moves to the foreground after being in the background (#6393) - Session replay is now automatically disabled in environments with unreliable masking to prevent PII leaks (#6389) - Detects iOS 26.0+ runtime with Xcode 26.0+ builds (DTXcode >= 2600) - Detects missing or disabled `UIDesignRequiresCompatibility` diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c57670e6f0a..ae680e8b537 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -831,10 +831,7 @@ D473ACD72D8090FC000F1CC6 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */; }; D480F9D92DE47A50009A0594 /* TestSentryScopePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */; }; D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */; }; - D483AF9A2E9D4E3D00B43C27 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */; }; - D483AF9C2E9D4E3D00B43C27 /* CwlPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */; }; - D483AF9F2E9D4E5500B43C27 /* CwlPosixPreconditionTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */; }; - D483AFA22E9D4E6600B43C27 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = D483AFA12E9D4E6600B43C27 /* CwlCatchException */; }; + D483AFA42E9D555300B43C27 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D483AFA32E9D555300B43C27 /* XCTest.framework */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */; }; @@ -2177,6 +2174,7 @@ D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SentryTracing.swift"; sourceTree = ""; }; D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryScopePersistentStore.swift; sourceTree = ""; }; D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScopePersistentStoreTests.swift; sourceTree = ""; }; + D483AFA32E9D555300B43C27 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/WatchOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapperProvider.swift; sourceTree = ""; }; @@ -2540,6 +2538,7 @@ buildActionMask = 2147483647; files = ( 84B7FA3529B285FC00AD93B1 /* Sentry.framework in Frameworks */, + D483AFA42E9D555300B43C27 /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2547,11 +2546,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D483AFA22E9D4E6600B43C27 /* CwlCatchException in Frameworks */, - D483AF9F2E9D4E5500B43C27 /* CwlPosixPreconditionTesting in Frameworks */, - D483AF9A2E9D4E3D00B43C27 /* CwlPosixPreconditionTesting in Frameworks */, D4CBA2472DE06D0200581618 /* libSentryTestUtils.a in Frameworks */, - D483AF9C2E9D4E3D00B43C27 /* CwlPreconditionTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2808,6 +2803,7 @@ 6304360C1EC05CEF00C4D3FA /* Frameworks */ = { isa = PBXGroup; children = ( + D483AFA32E9D555300B43C27 /* XCTest.framework */, 84F994E72A6894BD00EC0190 /* SystemConfiguration.framework */, 84F994E52A6894B500EC0190 /* CoreData.framework */, 6387B82F1ED851970045A84C /* libz.tbd */, @@ -5385,10 +5381,6 @@ ); name = SentryTestUtilsTests; packageProductDependencies = ( - D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */, - D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */, - D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */, - D483AFA12E9D4E6600B43C27 /* CwlCatchException */, ); productName = SentryTestUtilsTests; productReference = D4CBA2432DE06D0200581618 /* SentryTestUtilsTests.xctest */; @@ -8933,25 +8925,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - D483AF992E9D4E3D00B43C27 /* CwlPosixPreconditionTesting */ = { - isa = XCSwiftPackageProductDependency; - productName = CwlPosixPreconditionTesting; - }; - D483AF9B2E9D4E3D00B43C27 /* CwlPreconditionTesting */ = { - isa = XCSwiftPackageProductDependency; - productName = CwlPreconditionTesting; - }; - D483AF9E2E9D4E5500B43C27 /* CwlPosixPreconditionTesting */ = { - isa = XCSwiftPackageProductDependency; - productName = CwlPosixPreconditionTesting; - }; - D483AFA12E9D4E6600B43C27 /* CwlCatchException */ = { - isa = XCSwiftPackageProductDependency; - productName = CwlCatchException; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 6327C5CA1EB8A783004E799B /* Project object */; } diff --git a/SentryTestUtils/TestInfoPlistWrapper.swift b/SentryTestUtils/TestInfoPlistWrapper.swift index afb3335ea0f..0160f2cef94 100644 --- a/SentryTestUtils/TestInfoPlistWrapper.swift +++ b/SentryTestUtils/TestInfoPlistWrapper.swift @@ -1,12 +1,16 @@ @_spi(Private) @testable import Sentry +import XCTest @_spi(Private) public class TestInfoPlistWrapper: SentryInfoPlistWrapperProvider { - public init() {} - public var getAppValueStringInvocations = Invocations() private var mockedGetAppValueStringReturnValue: [String: Result] = [:] + public var getAppValueBooleanInvocations = Invocations<(String, NSErrorPointer)>() + private var mockedGetAppValueBooleanReturnValue: [String: Result] = [:] + + public init() {} + public func mockGetAppValueStringReturnValue(forKey key: String, value: String) { mockedGetAppValueStringReturnValue[key] = .success(value) } @@ -18,7 +22,8 @@ public func getAppValueString(for key: String) throws -> String { getAppValueStringInvocations.record(key) guard let result = mockedGetAppValueStringReturnValue[key] else { - preconditionFailure("TestInfoPlistWrapper: No mocked return value set for getAppValueString(for:) for key: \(key)") + XCTFail("TestInfoPlistWrapper: No mocked return value set for getAppValueString(for:) for key: \(key)") + return "" } switch result { case .success(let value): @@ -28,9 +33,6 @@ } } - public var getAppValueBooleanInvocations = Invocations<(String, NSErrorPointer)>() - private var mockedGetAppValueBooleanReturnValue: [String: Result] = [:] - public func mockGetAppValueBooleanReturnValue(forKey key: String, value: Bool) { mockedGetAppValueBooleanReturnValue[key] = .success(value) } @@ -42,7 +44,8 @@ public func getAppValueBoolean(for key: String, errorPtr: NSErrorPointer) -> Bool { getAppValueBooleanInvocations.record((key, errorPtr)) guard let result = mockedGetAppValueBooleanReturnValue[key] else { - preconditionFailure("TestInfoPlistWrapper: No mocked return value set for getAppValueBoolean(for:) for key: \(key)") + XCTFail("TestInfoPlistWrapper: No mocked return value set for getAppValueBoolean(for:) for key: \(key)") + return false } switch result { case .success(let value): diff --git a/SentryTestUtils/TestSessionReplayEnvironmentChecker.swift b/SentryTestUtils/TestSessionReplayEnvironmentChecker.swift index 5179000d7c6..95bf23abb32 100644 --- a/SentryTestUtils/TestSessionReplayEnvironmentChecker.swift +++ b/SentryTestUtils/TestSessionReplayEnvironmentChecker.swift @@ -3,16 +3,17 @@ @_spi(Private) public class TestSessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider { public var isReliableInvocations = Invocations() - private var mockedIsReliableReturnValue: Bool? + private var mockedIsReliableReturnValue: Bool - public init() {} + public init( + mockedIsReliableReturnValue: Bool + ) { + self.mockedIsReliableReturnValue = mockedIsReliableReturnValue + } public func isReliable() -> Bool { isReliableInvocations.record(()) - guard let result = mockedIsReliableReturnValue else { - preconditionFailure("\(Self.self): No mocked return value set for isReliable()") - } - return result + return mockedIsReliableReturnValue } public func mockIsReliableReturnValue(_ returnValue: Bool) { diff --git a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift index 22af913ebdf..de85b628ce5 100644 --- a/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift +++ b/SentryTestUtilsTests/TestInfoPlistWrapperTests.swift @@ -6,6 +6,16 @@ class TestInfoPlistWrapperTests: XCTestCase { // MARK: - getAppValueString(for:) + func testGetAppValueString_withoutMockedValue_shouldFail() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + // Don't mock any value for this key + + // -- Act & Assert -- + XCTExpectFailure("We are expecting a failure when accessing an unmocked key, as it indicates the test setup is incomplete") + _ = try sut.getAppValueString(for: "unmockedKey") + } + func testGetAppValueString_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { // -- Arrange -- let sut = TestInfoPlistWrapper() @@ -123,6 +133,17 @@ class TestInfoPlistWrapperTests: XCTestCase { // MARK: - getAppValueBoolean(for:errorPtr:) + func testGetAppValueBoolean_withoutMockedValue_shouldFail() throws { + // -- Arrange -- + let sut = TestInfoPlistWrapper() + // Don't mock any value for this key + + // -- Act & Assert -- + XCTExpectFailure("We are expecting a failure when accessing an unmocked key, as it indicates the test setup is incomplete") + var error: NSError? + _ = sut.getAppValueBoolean(for: "unmockedKey", errorPtr: &error) + } + func testGetAppValueBoolean_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws { // -- Arrange -- let sut = TestInfoPlistWrapper() diff --git a/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift index 4669c76dc49..96949c7ad5f 100644 --- a/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift +++ b/SentryTestUtilsTests/TestSessionReplayEnvironmentCheckerTests.swift @@ -6,9 +6,24 @@ 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() + let sut = TestSessionReplayEnvironmentChecker( + mockedIsReliableReturnValue: false + ) sut.mockIsReliableReturnValue(true) // -- Act -- @@ -20,7 +35,9 @@ class TestSessionReplayEnvironmentCheckerTests: XCTestCase { func testIsReliable_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws { // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker() + let sut = TestSessionReplayEnvironmentChecker( + mockedIsReliableReturnValue: false + ) sut.mockIsReliableReturnValue(true) // -- Act -- @@ -34,7 +51,9 @@ class TestSessionReplayEnvironmentCheckerTests: XCTestCase { func testIsReliable_shouldRecordInvocations() throws { // -- Arrange -- - let sut = TestSessionReplayEnvironmentChecker() + let sut = TestSessionReplayEnvironmentChecker( + mockedIsReliableReturnValue: false + ) sut.mockIsReliableReturnValue(true) // -- Act -- diff --git a/Sources/Sentry/SentryDelayedFramesTracker.m b/Sources/Sentry/SentryDelayedFramesTracker.m index 01e64425d1a..4974b2b4a41 100644 --- a/Sources/Sentry/SentryDelayedFramesTracker.m +++ b/Sources/Sentry/SentryDelayedFramesTracker.m @@ -28,11 +28,26 @@ - (instancetype)initWithKeepDelayedFramesDuration:(CFTimeInterval)keepDelayedFra if (self = [super init]) { _keepDelayedFramesDuration = keepDelayedFramesDuration; _dateProvider = dateProvider; - [self resetDelayedFramesTimeStamps]; + _delayedFrames = [NSMutableArray new]; + [self reset]; } return self; } +- (void)reset +{ + @synchronized(self.delayedFrames) { + _previousFrameSystemTimestamp = 0; + SentryDelayedFrame *initialFrame = + [[SentryDelayedFrame alloc] initWithStartTimestamp:[self.dateProvider systemTime] + expectedDuration:0 + actualDuration:0]; + + [_delayedFrames removeAllObjects]; + [_delayedFrames addObject:initialFrame]; + } +} + - (void)setPreviousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp SENTRY_DISABLE_THREAD_SANITIZER("We don't want to synchronize the access to this property to " "avoid slowing down the main thread.") @@ -47,16 +62,6 @@ - (uint64_t)getPreviousFrameSystemTimestamp SENTRY_DISABLE_THREAD_SANITIZER( return _previousFrameSystemTimestamp; } -- (void)resetDelayedFramesTimeStamps -{ - _delayedFrames = [NSMutableArray array]; - SentryDelayedFrame *initialFrame = - [[SentryDelayedFrame alloc] initWithStartTimestamp:[self.dateProvider systemTime] - expectedDuration:0 - actualDuration:0]; - [_delayedFrames addObject:initialFrame]; -} - - (void)recordDelayedFrame:(uint64_t)startSystemTimestamp thisFrameSystemTimestamp:(uint64_t)thisFrameSystemTimestamp expectedDuration:(CFTimeInterval)expectedDuration diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index 3fdc8c69a8c..32df110f337 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -106,7 +106,7 @@ - (void)resetFrames [self resetProfilingTimestampsInternal]; # endif // SENTRY_TARGET_PROFILING_SUPPORTED - [self.delayedFramesTracker resetDelayedFramesTimeStamps]; + [self.delayedFramesTracker reset]; } # if SENTRY_TARGET_PROFILING_SUPPORTED @@ -166,6 +166,7 @@ - (void)unpause } _isRunning = YES; + // Reset the previous frame timestamp to avoid wrong metrics being collected self.previousFrameTimestamp = SentryPreviousFrameInitialValue; [_displayLinkWrapper linkWithTarget:self selector:@selector(displayLinkCallback)]; @@ -321,6 +322,11 @@ - (void)removeListener:(id)listener - (void)pause { _isRunning = NO; + + // When the frames tracker is paused, we must reset the delayed frames tracker since accurate + // frame delay statistics cannot be provided, as we can't account for events during the pause. + [self.delayedFramesTracker reset]; + [self.displayLinkWrapper invalidate]; } @@ -333,7 +339,6 @@ - (void)stop _isStarted = NO; [self pause]; - [self.delayedFramesTracker resetDelayedFramesTimeStamps]; // Remove the observers with the most specific detail possible, see // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1413994-removeobserver diff --git a/Sources/Sentry/include/SentryDelayedFramesTracker.h b/Sources/Sentry/include/SentryDelayedFramesTracker.h index efd9f7b5a70..ab36c529005 100644 --- a/Sources/Sentry/include/SentryDelayedFramesTracker.h +++ b/Sources/Sentry/include/SentryDelayedFramesTracker.h @@ -21,8 +21,6 @@ SENTRY_NO_INIT - (instancetype)initWithKeepDelayedFramesDuration:(CFTimeInterval)keepDelayedFramesDuration dateProvider:(id)dateProvider; -- (void)resetDelayedFramesTimeStamps; - - (void)recordDelayedFrame:(uint64_t)startSystemTimestamp thisFrameSystemTimestamp:(uint64_t)thisFrameSystemTimestamp expectedDuration:(CFTimeInterval)expectedDuration @@ -30,6 +28,8 @@ SENTRY_NO_INIT - (void)setPreviousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp; +- (void)reset; + /** * This method returns the duration of all delayed frames between startSystemTimestamp and * endSystemTimestamp. diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index 448c5623cc0..7f220bbe593 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -3,6 +3,9 @@ @_spi(Private) import SentryTestUtils import XCTest +// swiftlint:disable file_length +// This test class also includes tests for delayed frames calculation which is quite complex. + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) class SentryFramesTrackerTests: XCTestCase { @@ -518,7 +521,167 @@ class SentryFramesTrackerTests: XCTestCase { let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) XCTAssertEqual(actualFrameDelay.delayDuration, -1.0) } - + + func testGetFramesDelay_WhenMovingFromBackgroundToForeground_BeforeDisplayLinkCalled() { + // Arrange + let sut = fixture.sut + sut.start() + + let displayLink = fixture.displayLinkWrapper + displayLink.call() + _ = displayLink.slowestSlowFrame() + + let startSystemTime = fixture.dateProvider.systemTime() + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + // Simulate app staying in background for 2 seconds + fixture.dateProvider.advance(by: 2.0) + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) + let endSystemTime = fixture.dateProvider.systemTime() + + // Act + let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) + + // Assert + + // The frames tracer starts subscribing to the display link when an app moves to the foreground. Since + // display link callbacks only occur when a new frame is drawn, it can take a couple of milliseconds + // for the first display link callback to occur. We can only calculate frame statistics when having at + // least one display link callback, as this marks the start of a new frame. + XCTAssertEqual(actualFrameDelay.delayDuration, -1.0, accuracy: 0.0001) + } + + func testGetFramesDelay_WhenMovingFromBackgroundToForeground_FirstFrameIsDrawing() { + // Arrange + let sut = fixture.sut + sut.start() + + // Simulate some frames to establish system timestamps + let displayLink = fixture.displayLinkWrapper + displayLink.call() + _ = displayLink.slowestSlowFrame() + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + // Simulate app staying in background for 2 seconds + fixture.dateProvider.advance(by: 2.0) + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) + + displayLink.call() + + let startSystemTime = fixture.dateProvider.systemTime() + fixture.dateProvider.advance(by: 0.01) + let endSystemTime = fixture.dateProvider.systemTime() + + // Act + let frameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) + + // The first is currently drawn, but it's not delayed yet. Therefore, 0 frame delay. + XCTAssertEqual(frameDelay.delayDuration, 0.0, accuracy: 0.0001) + } + + func testGetFramesDelay_WhenMovingFromBackgroundToForeground_FirstNormalFrameDrawn() { + // Arrange + let sut = fixture.sut + sut.start() + + // Simulate some frames to establish system timestamps + let displayLink = fixture.displayLinkWrapper + displayLink.call() + _ = displayLink.slowestSlowFrame() + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + // Simulate app staying in background for 2 seconds + fixture.dateProvider.advance(by: 2.0) + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) + + displayLink.call() + + // The delayed frames tracker should also have its previous frame system timestamp reset + // This prevents false delay calculations after unpausing + let startSystemTime = fixture.dateProvider.systemTime() + displayLink.normalFrame() + let endSystemTime = fixture.dateProvider.systemTime() + + // Act + let frameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) + + // Assert + // Normal frame is drawn, no delay + XCTAssertEqual(frameDelay.delayDuration, 0.0, accuracy: 0.0001) + } + + func testGetFramesDelay_WhenMovingFromBackgroundToForeground_FirstFrameIsSlow() { + // Arrange + let sut = fixture.sut + sut.start() + + // Simulate some frames to establish system timestamps + let displayLink = fixture.displayLinkWrapper + displayLink.call() + _ = displayLink.slowestSlowFrame() + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + // Simulate app staying in background for 2 seconds + fixture.dateProvider.advance(by: 2.0) + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) + + displayLink.call() + + // The delayed frames tracker should also have its previous frame system timestamp reset + // This prevents false delay calculations after unpausing + let startSystemTime = fixture.dateProvider.systemTime() + _ = displayLink.slowestSlowFrame() + let endSystemTime = fixture.dateProvider.systemTime() + + // Act + let frameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) + + let expectedDelay = fixture.displayLinkWrapper.slowestSlowFrameDuration - slowFrameThreshold(fixture.displayLinkWrapper.currentFrameRate.rawValue) + + // Assert + XCTAssertEqual(frameDelay.delayDuration, expectedDelay, accuracy: 0.0001) + } + + func testGetFramesDelay_WhenMovingFromBackgroundToForeground_DelayBeforeBackgroundNotIncluded() { + // Arrange + let sut = fixture.sut + sut.start() + + // Simulate some frames to establish system timestamps + let displayLink = fixture.displayLinkWrapper + displayLink.call() + + let startSystemTime = fixture.dateProvider.systemTime() + + _ = displayLink.slowestSlowFrame() + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) + + // Simulate app staying in background for 2 seconds + fixture.dateProvider.advance(by: 2.0) + + fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) + + displayLink.call() + + _ = displayLink.slowestSlowFrame() + let endSystemTime = fixture.dateProvider.systemTime() + + // Act + let frameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) + + // Assert + XCTAssertEqual(frameDelay.delayDuration, -1.0, accuracy: 0.0001) + } + func testFrameDelay_GetInfoFromBackgroundThreadWhileAdding() { let sut = fixture.sut sut.start() @@ -580,7 +743,7 @@ class SentryFramesTrackerTests: XCTestCase { wait(for: [expectation], timeout: 3.0) } - + func testAddMultipleListeners_AllCalledWithSameDate() { let sut = fixture.sut let listener1 = FrameTrackerListener() @@ -804,35 +967,6 @@ class SentryFramesTrackerTests: XCTestCase { // Should not detect any slow or frozen frames from the pauses try assert(slow: 0, frozen: 0, total: 4) } - - func testUnpause_WithDelayedFramesTracker_ResetsPreviousFrameSystemTimestamp() { - let sut = fixture.sut - sut.start() - - // Simulate some frames to establish system timestamps - fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.normalFrame() - - // Pause the tracker - fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.willResignActiveNotification)) - - // Advance time significantly - fixture.dateProvider.advance(by: 5.0) - - // Unpause the tracker - fixture.notificationCenter.post(Notification(name: CrossPlatformApplication.didBecomeActiveNotification)) - - // The delayed frames tracker should also have its previous frame system timestamp reset - // This prevents false delay calculations after unpausing - let startSystemTime = fixture.dateProvider.systemTime() - fixture.dateProvider.advance(by: 0.001) - let endSystemTime = fixture.dateProvider.systemTime() - - let frameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime) - - // Should not report any delay from the pause period - XCTAssertEqual(frameDelay.delayDuration, 0.001, accuracy: 0.0001) - } #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) func testResetProfilingTimestamps_FromBackgroundThread() { @@ -941,3 +1075,5 @@ private extension SentryFramesTrackerTests { } #endif + +// swiftlint:enable file_length diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 4bcd0bfd7e6..38043b76206 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -80,7 +80,9 @@ class SentrySessionReplayTests: XCTestCase { let rootView = UIView() let replayMaker = TestReplayMaker() let cacheFolder = FileManager.default.temporaryDirectory - let environmentChecker = TestSessionReplayEnvironmentChecker() + let environmentChecker = TestSessionReplayEnvironmentChecker( + mockedIsReliableReturnValue: true + ) var breadcrumbs: [Breadcrumb]? var isFullSession = true From 0f9ac1b0360d0401884c44fa9f0e941eb44ef5c2 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 14 Oct 2025 10:42:45 +0200 Subject: [PATCH 20/22] feat: Add tests for info plist wrapper --- Sentry.xcodeproj/project.pbxproj | 26 ++- .../InfoPlist/SentryInfoPlistWrapper.swift | 9 +- .../InfoPlist/SentryInfoPlistKeyTests.swift | 12 ++ .../SentryInfoPlistWrapperTests.swift | 109 +++++++------ .../InfoPlist/SentryXcodeVersionTests.swift | 12 ++ .../Helper/InfoPlist/TestBundle.swift | 149 ++++++++++++++++++ .../Helper/InfoPlist/TestInfoPlist.plist | 36 +++++ 7 files changed, 299 insertions(+), 54 deletions(-) create mode 100644 Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift rename Tests/SentryTests/Helper/{ => InfoPlist}/SentryInfoPlistWrapperTests.swift (60%) create mode 100644 Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift create mode 100644 Tests/SentryTests/Helper/InfoPlist/TestBundle.swift create mode 100644 Tests/SentryTests/Helper/InfoPlist/TestInfoPlist.plist diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ae680e8b537..96bc6f38a81 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -842,6 +842,10 @@ D490648A2DFAE1F600555785 /* SentryScreenshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */; }; D49480D32DC23E9300A3B6E9 /* SentryReplayTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49480D22DC23E8E00A3B6E9 /* SentryReplayTypeTests.swift */; }; D49480D72DC23FE300A3B6E9 /* SentrySessionReplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49480D62DC23FE200A3B6E9 /* SentrySessionReplayDelegate.swift */; }; + D4A0C2312E9E3D0700791353 /* SentryXcodeVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A0C2302E9E3CFF00791353 /* SentryXcodeVersionTests.swift */; }; + D4A0C2332E9E3D1400791353 /* SentryInfoPlistKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A0C2322E9E3D0C00791353 /* SentryInfoPlistKeyTests.swift */; }; + D4A0C2372E9E3F4400791353 /* TestInfoPlist.plist in Resources */ = {isa = PBXBuildFile; fileRef = D4A0C2362E9E3F4400791353 /* TestInfoPlist.plist */; }; + D4A0C2392E9E3F5800791353 /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A0C2382E9E3F5800791353 /* TestBundle.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; @@ -2185,6 +2189,10 @@ D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptions.swift; sourceTree = ""; }; D49480D22DC23E8E00A3B6E9 /* SentryReplayTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayTypeTests.swift; sourceTree = ""; }; D49480D62DC23FE200A3B6E9 /* SentrySessionReplayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayDelegate.swift; sourceTree = ""; }; + D4A0C2302E9E3CFF00791353 /* SentryXcodeVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryXcodeVersionTests.swift; sourceTree = ""; }; + D4A0C2322E9E3D0C00791353 /* SentryInfoPlistKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistKeyTests.swift; sourceTree = ""; }; + D4A0C2362E9E3F4400791353 /* TestInfoPlist.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TestInfoPlist.plist; sourceTree = ""; }; + D4A0C2382E9E3F5800791353 /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = ""; }; D4A2360A2D5F84FA00D55C58 /* SwiftUITestSample_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftUITestSample_Base.xctestplan; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; @@ -3598,7 +3606,7 @@ 7BD7299B24654CD500EA3610 /* Helper */ = { isa = PBXGroup; children = ( - D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */, + D4A0C22A2E9E3CE100791353 /* InfoPlist */, F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */, D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */, D8AE48BE2C578D540092A2A6 /* SentrySDKLog.swift */, @@ -4339,6 +4347,18 @@ path = Screenshot; sourceTree = ""; }; + D4A0C22A2E9E3CE100791353 /* InfoPlist */ = { + isa = PBXGroup; + children = ( + D4A0C2322E9E3D0C00791353 /* SentryInfoPlistKeyTests.swift */, + D4A0C2302E9E3CFF00791353 /* SentryXcodeVersionTests.swift */, + D4599F8A2E98FE970045BB95 /* SentryInfoPlistWrapperTests.swift */, + D4A0C2382E9E3F5800791353 /* TestBundle.swift */, + D4A0C2362E9E3F4400791353 /* TestInfoPlist.plist */, + ); + path = InfoPlist; + sourceTree = ""; + }; D4CBA2522DE06D1600581618 /* SentryTestUtilsTests */ = { isa = PBXGroup; children = ( @@ -5568,6 +5588,7 @@ buildActionMask = 2147483647; files = ( 630C01961EC341D600C52CEF /* Resources in Resources */, + D4A0C2372E9E3F4400791353 /* TestInfoPlist.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6185,6 +6206,7 @@ D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */, F4DC35582E1FFE1F0077CE89 /* SentryVideoFrameProcessorTests.swift in Sources */, F48F74F32E5F9959009D4E7D /* SentryCrashBinaryImageCacheTests.m in Sources */, + D4A0C2312E9E3D0700791353 /* SentryXcodeVersionTests.swift in Sources */, 7BC6EC18255C44540059822A /* SentryDebugMetaTests.swift in Sources */, A811D867248E2770008A41EA /* SentrySystemEventBreadcrumbsTest.swift in Sources */, 7B82D54924E2A2D400EE670F /* SentryIdTests.swift in Sources */, @@ -6218,6 +6240,7 @@ 7BBD18A2244EE2FD00427C76 /* TestResponseFactory.swift in Sources */, 628B89022D841D7F004B6F2A /* SentryDateUtilsTests.swift in Sources */, D808FB8B281BCE96009A2A33 /* TestSentrySwizzleWrapper.swift in Sources */, + D4A0C2392E9E3F5800791353 /* TestBundle.swift in Sources */, 630C01941EC3402C00C52CEF /* SentryKSCrashReportConverterTests.m in Sources */, 7B59398424AB481B0003AAD2 /* NotificationCenterTestCase.swift in Sources */, 7B0A542E2521C62400A71716 /* SentryFrameRemoverTests.swift in Sources */, @@ -6290,6 +6313,7 @@ D875ED0B276CC84700422FAC /* SentryFileIOTrackerTests.swift in Sources */, D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */, 7BC6EC04255C235F0059822A /* SentryFrameTests.swift in Sources */, + D4A0C2332E9E3D1400791353 /* SentryInfoPlistKeyTests.swift in Sources */, D82DD1CD2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift in Sources */, 0AE455AD28F584D2006680E5 /* SentryReachabilityTests.m in Sources */, 63FE720420DA66EC00CDBAE8 /* SentryCrashString_Tests.m in Sources */, diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift index f390e9f3a59..1ba0ba37068 100644 --- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift +++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift @@ -1,4 +1,11 @@ @objc @_spi(Private) public class SentryInfoPlistWrapper: NSObject, SentryInfoPlistWrapperProvider { + + private let bundle: Bundle + + public init(bundle: Bundle = Bundle.main) { + self.bundle = bundle + } + // MARK: - Bridge to ObjC public func getAppValueBoolean(for key: String, errorPtr errPtr: NSErrorPointer) -> Bool { @@ -26,7 +33,7 @@ // As soon as this class is not consumed from Objective-C anymore, we can use this method directly to reduce // unnecessary duplicate code. In addition this method can be adapted to use `SentryInfoPlistKey` as the type // of the parameter `key` - guard let infoDictionary = Bundle.main.infoDictionary else { + guard let infoDictionary = bundle.infoDictionary else { throw SentryInfoPlistError.mainInfoPlistNotFound } guard let value = infoDictionary[key] else { diff --git a/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift new file mode 100644 index 00000000000..35c8a3f4b2e --- /dev/null +++ b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistKeyTests.swift @@ -0,0 +1,12 @@ +@_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/SentryInfoPlistWrapperTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift similarity index 60% rename from Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift rename to Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift index fd3849268fd..6619ec25384 100644 --- a/Tests/SentryTests/Helper/SentryInfoPlistWrapperTests.swift +++ b/Tests/SentryTests/Helper/InfoPlist/SentryInfoPlistWrapperTests.swift @@ -1,16 +1,35 @@ @_spi(Private) @testable import Sentry import XCTest +/// Tests for `SentryInfoPlistWrapper`. +/// +/// This test suite uses a custom test bundle (`TestBundle`) with a predefined Info.plist file +/// (`TestInfoPlist.plist`) to ensure consistent and predictable testing. This approach eliminates +/// the need to rely on the environment's Info.plist, which may vary across different test contexts. +/// +/// ## Test Setup +/// +/// - `TestInfoPlist.plist`: Contains known key-value pairs for testing (strings, booleans, arrays, etc.) +/// - `TestBundle.swift`: Helper class that creates a temporary bundle from the test plist +/// - The bundle is created in `setUp()` and cleaned up in `tearDown()` class SentryInfoPlistWrapperTests: XCTestCase { private var sut: SentryInfoPlistWrapper! + private var testBundle: Bundle! override func setUp() { super.setUp() - sut = SentryInfoPlistWrapper() + + // Create a test bundle with our custom Info.plist + testBundle = TestBundle.createTestBundle() + XCTAssertNotNil(testBundle, "Test bundle should be created successfully") + + sut = SentryInfoPlistWrapper(bundle: testBundle) } override func tearDown() { + TestBundle.cleanup(testBundle) + testBundle = nil sut = nil super.tearDown() } @@ -19,14 +38,13 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueString_whenKeyExists_shouldReturnValue() throws { // Arrange - // CFBundleName is a standard key that should exist in any bundle - let key = "CFBundleName" + let key = "TestStringKey" // Act let value = try sut.getAppValueString(for: key) // Assert - XCTAssertFalse(value.isEmpty, "Bundle name should not be empty") + XCTAssertEqual(value, "TestStringValue", "Should return the correct string value") } func testGetAppValueString_whenKeyDoesNotExist_shouldThrowKeyNotFoundError() { @@ -45,24 +63,17 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueString_whenValueIsNotString_shouldThrowUnableToCastError() { // Arrange - // CFBundleVersion is typically a number or can be a mixed type - // We'll use a key that we know exists but might not be a string - // Note: This test might be skipped if we can't find a suitable non-string key - // Let's try with UIDeviceFamily which is typically an array - let key = "UIDeviceFamily" + // TestArrayKey is an array in our test plist, not a string + let key = "TestArrayKey" // Act & Assert - do { - _ = try sut.getAppValueString(for: key) - // If we get here, the key happened to be a string or doesn't exist in test bundle - // This is not a test failure, just means the key wasn't suitable for this test - } catch SentryInfoPlistError.unableToCastValue(let errorKey, _, let type) { + XCTAssertThrowsError(try sut.getAppValueString(for: key)) { error in + guard case SentryInfoPlistError.unableToCastValue(let errorKey, _, let type) = error else { + XCTFail("Expected SentryInfoPlistError.unableToCastValue, got \(error)") + return + } XCTAssertEqual(errorKey, key) XCTAssertTrue(type == String.self) - } catch SentryInfoPlistError.keyNotFound { - // Key doesn't exist in test bundle, which is acceptable for this test - } catch { - XCTFail("Expected SentryInfoPlistError.unableToCastValue or keyNotFound, got \(error)") } } @@ -70,24 +81,28 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueBoolean_whenKeyExistsAndIsTrue_shouldReturnTrue() { // Arrange - // For this test, we'll use a key that might exist and be a boolean - // UIApplicationExitsOnSuspend is a boolean key (if it exists) - let key = "UIApplicationExitsOnSuspend" + let key = "TestBooleanTrue" var error: NSError? // Act let value = sut.getAppValueBoolean(for: key, errorPtr: &error) // Assert - // If the key exists and is a boolean, it should work without error - // If the key doesn't exist, error should be set - if error == nil { - // Success case - value is valid - XCTAssertTrue(value == true || value == false, "Boolean value should be true or false") - } else { - // Key not found is acceptable for this test setup - XCTAssertTrue(error?.domain == "SentryInfoPlistError" || error != nil) - } + XCTAssertNil(error, "Should not have an error when reading a valid boolean") + XCTAssertTrue(value, "Should return true for TestBooleanTrue key") + } + + func testGetAppValueBoolean_whenKeyExistsAndIsFalse_shouldReturnFalse() { + // Arrange + let key = "TestBooleanFalse" + var error: NSError? + + // Act + let value = sut.getAppValueBoolean(for: key, errorPtr: &error) + + // Assert + XCTAssertNil(error, "Should not have an error when reading a valid boolean") + XCTAssertFalse(value, "Should return false for TestBooleanFalse key") } func testGetAppValueBoolean_whenKeyDoesNotExist_shouldReturnFalseAndSetError() { @@ -105,8 +120,8 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueBoolean_whenValueIsNotBoolean_shouldReturnFalseAndSetError() { // Arrange - // CFBundleName is a string, not a boolean - let key = "CFBundleName" + // TestStringKey is a string, not a boolean + let key = "TestStringKey" var error: NSError? // Act @@ -119,7 +134,7 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueBoolean_withNullErrorPointer_shouldNotCrash() { // Arrange - let key = "CFBundleName" // A key that exists but is not a boolean + let key = "TestStringKey" // A key that exists but is not a boolean // Act & Assert // This should not crash even with a null error pointer @@ -145,18 +160,13 @@ class SentryInfoPlistWrapperTests: XCTestCase { func testGetAppValueString_withSentryInfoPlistKey_shouldWork() throws { // Arrange // Test with the actual enum keys used in production - // Note: These keys might not exist in the test bundle, which is expected let xcodeKey = SentryInfoPlistKey.xcodeVersion.rawValue - // Act & Assert - do { - let value = try sut.getAppValueString(for: xcodeKey) - // If the key exists, value should not be empty - XCTAssertFalse(value.isEmpty, "Xcode version should not be empty if present") - } catch SentryInfoPlistError.keyNotFound { - // This is expected in test environment - DTXcode might not be set - XCTAssertTrue(true, "Key not found is acceptable for test bundle") - } + // Act + let value = try sut.getAppValueString(for: xcodeKey) + + // Assert + XCTAssertEqual(value, "1610", "Should return the DTXcode value from test bundle") } func testGetAppValueBoolean_withSentryInfoPlistKey_shouldWork() { @@ -168,21 +178,15 @@ class SentryInfoPlistWrapperTests: XCTestCase { let value = sut.getAppValueBoolean(for: compatibilityKey, errorPtr: &error) // Assert - // In test environment, this key likely doesn't exist - if error == nil { - // If no error, we successfully read a boolean value - XCTAssertTrue(value == true || value == false) - } else { - // Expected to not find this key in test bundle - XCTAssertNotNil(error) - } + XCTAssertNil(error, "Should not have an error when reading a valid boolean") + XCTAssertFalse(value, "Should return false for UIDesignRequiresCompatibility key") } // MARK: - Multiple Consecutive Calls func testMultipleConsecutiveCalls_shouldReturnConsistentResults() throws { // Arrange - let key = "CFBundleName" + let key = "TestStringKey" // Act let value1 = try sut.getAppValueString(for: key) @@ -190,5 +194,6 @@ class SentryInfoPlistWrapperTests: XCTestCase { // Assert XCTAssertEqual(value1, value2, "Multiple calls should return the same value") + XCTAssertEqual(value1, "TestStringValue") } } diff --git a/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift b/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift new file mode 100644 index 00000000000..3a27f5801cc --- /dev/null +++ b/Tests/SentryTests/Helper/InfoPlist/SentryXcodeVersionTests.swift @@ -0,0 +1,12 @@ +@_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/Helper/InfoPlist/TestBundle.swift b/Tests/SentryTests/Helper/InfoPlist/TestBundle.swift new file mode 100644 index 00000000000..a1e83bc3ceb --- /dev/null +++ b/Tests/SentryTests/Helper/InfoPlist/TestBundle.swift @@ -0,0 +1,149 @@ +import Foundation + +/** + Helper class to create test bundles with custom Info.plist files for testing. + + This class provides utilities to create temporary bundles that can be used in tests, + allowing you to test code that depends on `Bundle.infoDictionary` without relying + on the actual test bundle's Info.plist. + + ## Usage Examples + + ### Using the predefined TestInfoPlist.plist: + ```swift + let testBundle = TestBundle.createTestBundle() + let wrapper = SentryInfoPlistWrapper(bundle: testBundle) + ``` + + ### Creating a custom bundle on-the-fly: + ```swift + let customInfo = [ + "CustomKey": "CustomValue", + "EnableFeature": true + ] as [String: Any] + let testBundle = TestBundle.createBundle(withInfoDictionary: customInfo) + ``` + + ### Cleanup: + ```swift + TestBundle.cleanup(testBundle) + ``` + + ## Note + The bundles created by this class are temporary and stored in the system's + temporary directory. Always call `cleanup(_:)` in your test's `tearDown()` method. + */ +class TestBundle { + + /// Creates a temporary bundle with the test Info.plist + /// - Returns: A Bundle configured with the test Info.plist, or nil if creation fails + static func createTestBundle() -> Bundle? { + guard let plistURL = locateTestInfoPlist() else { + print("⚠️ TestInfoPlist.plist not found in bundle resources") + return nil + } + return createBundleFromPlist(at: plistURL) + } + + // MARK: - Private Helpers + + /// Locates the TestInfoPlist.plist file in the test bundle + /// - Returns: URL to the plist file, or nil if not found + private static func locateTestInfoPlist() -> URL? { + let testBundle = Bundle(for: TestBundle.self) + + // Try with subdirectory first + if let url = testBundle.url(forResource: "TestInfoPlist", withExtension: "plist", subdirectory: "Helper/InfoPlist") { + return url + } + + // Try without subdirectory (Xcode might flatten the structure) + return testBundle.url(forResource: "TestInfoPlist", withExtension: "plist") + } + + /// Creates a temporary bundle directory + /// - Parameter name: The name of the bundle directory + /// - Returns: URL to the created directory, or nil if creation fails + private static func createTempBundleDirectory(name: String) -> URL? { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathComponent(name) + + do { + try FileManager.default.createDirectory( + at: tempDir, + withIntermediateDirectories: true, + attributes: nil + ) + return tempDir + } catch { + print("⚠️ Failed to create bundle directory: \(error)") + return nil + } + } + + /// Creates a Bundle from a URL + /// - Parameter url: The URL of the bundle directory + /// - Returns: A Bundle instance, or nil if creation fails + private static func createBundle(at url: URL) -> Bundle? { + guard let bundle = Bundle(url: url) else { + print("⚠️ Failed to create Bundle from directory at: \(url.path)") + return nil + } + return bundle + } + + /// Creates a bundle by copying an existing Info.plist file + /// - Parameter plistURL: URL to the Info.plist file to copy + /// - Returns: A Bundle configured with the copied Info.plist, or nil if creation fails + private static func createBundleFromPlist(at plistURL: URL) -> Bundle? { + guard let tempDir = createTempBundleDirectory(name: "TestBundle.bundle") else { + return nil + } + + do { + let destURL = tempDir.appendingPathComponent("Info.plist") + try FileManager.default.copyItem(at: plistURL, to: destURL) + return createBundle(at: tempDir) + } catch { + print("⚠️ Failed to copy Info.plist: \(error)") + return nil + } + } + + /// Creates an in-memory bundle with a custom Info.plist dictionary + /// - Parameter infoDictionary: The dictionary to use as Info.plist + /// - Returns: A Bundle with the specified infoDictionary + static func createBundle(withInfoDictionary infoDictionary: [String: Any]) -> Bundle? { + guard let tempDir = createTempBundleDirectory(name: "TestBundle.bundle") else { + return nil + } + + do { + let plistURL = tempDir.appendingPathComponent("Info.plist") + let plistData = try PropertyListSerialization.data( + fromPropertyList: infoDictionary, + format: .xml, + options: 0 + ) + try plistData.write(to: plistURL) + return createBundle(at: tempDir) + } catch { + print("⚠️ Failed to create Info.plist: \(error)") + return nil + } + } + + /// Cleans up a temporary test bundle + /// - Parameter bundle: The bundle to clean up + static func cleanup(_ bundle: Bundle?) { + guard let bundle = bundle else { + return + } + + // Only delete if it's in the temp directory (safety check) + if bundle.bundleURL.path.contains(FileManager.default.temporaryDirectory.path) { + try? FileManager.default.removeItem(at: bundle.bundleURL) + } + } +} diff --git a/Tests/SentryTests/Helper/InfoPlist/TestInfoPlist.plist b/Tests/SentryTests/Helper/InfoPlist/TestInfoPlist.plist new file mode 100644 index 00000000000..724aae47d11 --- /dev/null +++ b/Tests/SentryTests/Helper/InfoPlist/TestInfoPlist.plist @@ -0,0 +1,36 @@ + + + + + CFBundleName + TestBundle + CFBundleIdentifier + io.sentry.test + TestStringKey + TestStringValue + DTXcode + 1610 + DTXcodeBuild + 16B40 + TestBooleanTrue + + TestBooleanFalse + + DTPlatformBuild + 21A5303d + TestNumberKey + 42 + TestArrayKey + + item1 + item2 + + TestDictionaryKey + + nestedKey + nestedValue + + UIDesignRequiresCompatibility + + + From 27aefce9e73d4d2a9bdbaec17ad3229afd432668 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 14 Oct 2025 10:45:43 +0200 Subject: [PATCH 21/22] update api --- sdk_api.json | 231 ------------------------------------------------ sdk_api_V9.json | 231 ------------------------------------------------ 2 files changed, 462 deletions(-) diff --git a/sdk_api.json b/sdk_api.json index d38113ed0e7..b049e90f73e 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -46391,237 +46391,6 @@ } ] }, - { - "kind": "TypeDecl", - "name": "SentryInfoPlistKey", - "printedName": "SentryInfoPlistKey", - "children": [ - { - "kind": "Var", - "name": "xcodeVersion", - "printedName": "xcodeVersion", - "children": [ - { - "kind": "TypeFunc", - "name": "Function", - "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - }, - { - "kind": "TypeNominal", - "name": "Metatype", - "printedName": "Sentry.SentryInfoPlistKey.Type", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ] - } - ] - } - ], - "declKind": "EnumElement", - "usr": "s:6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", - "mangledName": "$s6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", - "moduleName": "Sentry" - }, - { - "kind": "Var", - "name": "designRequiresCompatibility", - "printedName": "designRequiresCompatibility", - "children": [ - { - "kind": "TypeFunc", - "name": "Function", - "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - }, - { - "kind": "TypeNominal", - "name": "Metatype", - "printedName": "Sentry.SentryInfoPlistKey.Type", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ] - } - ] - } - ], - "declKind": "EnumElement", - "usr": "s:6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", - "mangledName": "$s6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", - "moduleName": "Sentry" - }, - { - "kind": "Constructor", - "name": "init", - "printedName": "init(rawValue:)", - "children": [ - { - "kind": "TypeNominal", - "name": "Optional", - "printedName": "Sentry.SentryInfoPlistKey?", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ], - "usr": "s:Sq" - }, - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Constructor", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", - "moduleName": "Sentry", - "init_kind": "Designated" - }, - { - "kind": "TypeAlias", - "name": "RawValue", - "printedName": "RawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "TypeAlias", - "usr": "s:6Sentry0A12InfoPlistKeyO8RawValuea", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8RawValuea", - "moduleName": "Sentry" - }, - { - "kind": "Var", - "name": "rawValue", - "printedName": "rawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Var", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvp", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvp", - "moduleName": "Sentry", - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Accessor", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvg", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvg", - "moduleName": "Sentry", - "accessorKind": "get" - } - ] - } - ], - "declKind": "Enum", - "usr": "s:6Sentry0A12InfoPlistKeyO", - "mangledName": "$s6Sentry0A12InfoPlistKeyO", - "moduleName": "Sentry", - "enumRawTypeName": "String", - "conformances": [ - { - "kind": "Conformance", - "name": "Copyable", - "printedName": "Copyable", - "usr": "s:s8CopyableP", - "mangledName": "$ss8CopyableP" - }, - { - "kind": "Conformance", - "name": "Escapable", - "printedName": "Escapable", - "usr": "s:s9EscapableP", - "mangledName": "$ss9EscapableP" - }, - { - "kind": "Conformance", - "name": "Equatable", - "printedName": "Equatable", - "usr": "s:SQ", - "mangledName": "$sSQ" - }, - { - "kind": "Conformance", - "name": "Hashable", - "printedName": "Hashable", - "usr": "s:SH", - "mangledName": "$sSH" - }, - { - "kind": "Conformance", - "name": "RawRepresentable", - "printedName": "RawRepresentable", - "children": [ - { - "kind": "TypeWitness", - "name": "RawValue", - "printedName": "RawValue", - "children": [ - { - "kind": "TypeNameAlias", - "name": "RawValue", - "printedName": "Sentry.SentryInfoPlistKey.RawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ] - } - ] - } - ], - "usr": "s:SY", - "mangledName": "$sSY" - } - ] - }, { "kind": "TypeDecl", "name": "SentryLog", diff --git a/sdk_api_V9.json b/sdk_api_V9.json index c8118912cdb..9b27b6759b6 100644 --- a/sdk_api_V9.json +++ b/sdk_api_V9.json @@ -42867,237 +42867,6 @@ } ] }, - { - "kind": "TypeDecl", - "name": "SentryInfoPlistKey", - "printedName": "SentryInfoPlistKey", - "children": [ - { - "kind": "Var", - "name": "xcodeVersion", - "printedName": "xcodeVersion", - "children": [ - { - "kind": "TypeFunc", - "name": "Function", - "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - }, - { - "kind": "TypeNominal", - "name": "Metatype", - "printedName": "Sentry.SentryInfoPlistKey.Type", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ] - } - ] - } - ], - "declKind": "EnumElement", - "usr": "s:6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", - "mangledName": "$s6Sentry0A12InfoPlistKeyO12xcodeVersionyA2CmF", - "moduleName": "Sentry" - }, - { - "kind": "Var", - "name": "designRequiresCompatibility", - "printedName": "designRequiresCompatibility", - "children": [ - { - "kind": "TypeFunc", - "name": "Function", - "printedName": "(Sentry.SentryInfoPlistKey.Type) -> Sentry.SentryInfoPlistKey", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - }, - { - "kind": "TypeNominal", - "name": "Metatype", - "printedName": "Sentry.SentryInfoPlistKey.Type", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ] - } - ] - } - ], - "declKind": "EnumElement", - "usr": "s:6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", - "mangledName": "$s6Sentry0A12InfoPlistKeyO27designRequiresCompatibilityyA2CmF", - "moduleName": "Sentry" - }, - { - "kind": "Constructor", - "name": "init", - "printedName": "init(rawValue:)", - "children": [ - { - "kind": "TypeNominal", - "name": "Optional", - "printedName": "Sentry.SentryInfoPlistKey?", - "children": [ - { - "kind": "TypeNominal", - "name": "SentryInfoPlistKey", - "printedName": "Sentry.SentryInfoPlistKey", - "usr": "s:6Sentry0A12InfoPlistKeyO" - } - ], - "usr": "s:Sq" - }, - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Constructor", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueACSgSS_tcfc", - "moduleName": "Sentry", - "init_kind": "Designated" - }, - { - "kind": "TypeAlias", - "name": "RawValue", - "printedName": "RawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "TypeAlias", - "usr": "s:6Sentry0A12InfoPlistKeyO8RawValuea", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8RawValuea", - "moduleName": "Sentry" - }, - { - "kind": "Var", - "name": "rawValue", - "printedName": "rawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Var", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvp", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvp", - "moduleName": "Sentry", - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ], - "declKind": "Accessor", - "usr": "s:6Sentry0A12InfoPlistKeyO8rawValueSSvg", - "mangledName": "$s6Sentry0A12InfoPlistKeyO8rawValueSSvg", - "moduleName": "Sentry", - "accessorKind": "get" - } - ] - } - ], - "declKind": "Enum", - "usr": "s:6Sentry0A12InfoPlistKeyO", - "mangledName": "$s6Sentry0A12InfoPlistKeyO", - "moduleName": "Sentry", - "enumRawTypeName": "String", - "conformances": [ - { - "kind": "Conformance", - "name": "Copyable", - "printedName": "Copyable", - "usr": "s:s8CopyableP", - "mangledName": "$ss8CopyableP" - }, - { - "kind": "Conformance", - "name": "Escapable", - "printedName": "Escapable", - "usr": "s:s9EscapableP", - "mangledName": "$ss9EscapableP" - }, - { - "kind": "Conformance", - "name": "Equatable", - "printedName": "Equatable", - "usr": "s:SQ", - "mangledName": "$sSQ" - }, - { - "kind": "Conformance", - "name": "Hashable", - "printedName": "Hashable", - "usr": "s:SH", - "mangledName": "$sSH" - }, - { - "kind": "Conformance", - "name": "RawRepresentable", - "printedName": "RawRepresentable", - "children": [ - { - "kind": "TypeWitness", - "name": "RawValue", - "printedName": "RawValue", - "children": [ - { - "kind": "TypeNameAlias", - "name": "RawValue", - "printedName": "Sentry.SentryInfoPlistKey.RawValue", - "children": [ - { - "kind": "TypeNominal", - "name": "String", - "printedName": "Swift.String", - "usr": "s:SS" - } - ] - } - ] - } - ], - "usr": "s:SY", - "mangledName": "$sSY" - } - ] - }, { "kind": "TypeDecl", "name": "SentryLog", From 5bdb59392c9d4a71d74e58cc4544f872097b8da4 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 14 Oct 2025 10:57:51 +0200 Subject: [PATCH 22/22] refactor: Update SentryInfoPlistWrapper initializer to support Objective-C usage - Changed the default initializer to override and set the bundle to Bundle.main. - Added a new public initializer to allow custom bundle injection while maintaining compatibility with Objective-C. --- .../Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift index 1ba0ba37068..be7bb0fa2d0 100644 --- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift +++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift @@ -2,8 +2,15 @@ private let bundle: Bundle - public init(bundle: Bundle = Bundle.main) { + public override init() { + // We can not use defaults in the initializer because this class is used from Objective-C + self.bundle = Bundle.main + super.init() + } + + public init(bundle: Bundle) { self.bundle = bundle + super.init() } // MARK: - Bridge to ObjC