From c0644cfec5fbe2b38bab54d03f9ee91aaaf4e6ce Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Fri, 24 Oct 2025 17:17:17 -0300 Subject: [PATCH 1/3] fix: Disable SessionSentryReplayIntegration if the environment is unsafe --- .../Sentry/SentrySessionReplayIntegration.m | 46 ++++++++----------- .../SessionReplay/SentrySessionReplay.swift | 27 +++++------ 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 8f8f70f36e..dc98db0d55 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -5,6 +5,7 @@ # import "SentryClient+Private.h" # import "SentryEvent+Private.h" # import "SentryHub+Private.h" +# import "SentryInternalDefines.h" # import "SentryLogC.h" # import "SentryOptions.h" # import "SentrySDK+Private.h" @@ -45,7 +46,6 @@ - (void)newSceneActivate; @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; - SentryExperimentalOptions *_experimentalOptions; id _notificationCenter; id _rateLimits; id _currentScreenshotProvider; @@ -57,7 +57,6 @@ @implementation SentrySessionReplayIntegration { // replay absolutely needs segment 0 to make replay work. BOOL _rateLimited; id _dateProvider; - id _environmentChecker; } - (instancetype)init @@ -70,25 +69,26 @@ - (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 - experimentalOptions:options.experimental - fullSession:YES]; + [self startWithOptions:options.sessionReplay fullSession:YES]; } return self; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options { - if ([super installWithOptions:options] == NO) { + if ([super installWithOptions:options] == NO || + [SentrySessionReplay + shouldEnableSessionReplayWithEnvironmentChecker:SentryDependencies + .sessionReplayEnvironmentChecker + experimentalOptions:options.experimental] + == NO) { return NO; } [self setupWith:options.sessionReplay - experimentalOptions:options.experimental enableTouchTracker:options.enableSwizzling enableViewRendererV2:options.sessionReplay.enableViewRendererV2 enableFastViewRendering:options.sessionReplay.enableFastViewRendering]; @@ -96,13 +96,11 @@ - (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; @@ -133,7 +131,6 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; _dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; - _environmentChecker = SentryDependencies.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. @@ -331,9 +328,7 @@ - (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 - experimentalOptions:_experimentalOptions - fullSession:_startedAsFullSession]; + [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; } else { SENTRY_LOG_DEBUG( @"[Session Replay] Waiting for a scene to be available to started the replay"); @@ -352,18 +347,14 @@ - (void)newSceneActivate removeObserver:self name:UISceneDidActivateNotification object:nil]; - [self startWithOptions:_replayOptions - experimentalOptions:_experimentalOptions - fullSession:_startedAsFullSession]; + [self startWithOptions:_replayOptions 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] @@ -371,7 +362,6 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions } - (void)startWithOptions:(SentryReplayOptions *)replayOptions - experimentalOptions:(SentryExperimentalOptions *)experimentalOptions screenshotProvider:(id)screenshotProvider breadcrumbConverter:(id)breadcrumbConverter fullSession:(BOOL)shouldReplayFullSession @@ -406,7 +396,6 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions SentryDisplayLinkWrapper *displayLinkWrapper = [[SentryDisplayLinkWrapper alloc] init]; self.sessionReplay = [[SentrySessionReplay alloc] initWithReplayOptions:replayOptions - experimentalOptions:experimentalOptions replayFolderPath:docs screenshotProvider:screenshotProvider replayMaker:replayMaker @@ -414,8 +403,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions touchTracker:_touchTracker dateProvider:_dateProvider delegate:self - displayLinkWrapper:displayLinkWrapper - environmentChecker:_environmentChecker]; + displayLinkWrapper:displayLinkWrapper]; [self.sessionReplay startWithRootView:[SentryDependencyContainer.sharedInstance.application getWindows] @@ -447,15 +435,19 @@ - (nullable NSURL *)replayDirectory return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; } -- (void)saveCurrentSessionInfo:(SentryId *)sessionId +- (void)saveCurrentSessionInfo:(SentryId *_Nullable)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { SENTRY_LOG_DEBUG(@"[Session Replay] Saving current session info for session: %@ to path: %@", sessionId, path); - NSDictionary *info = - [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, @"replayId", - path.lastPathComponent, @"path", @(options.onErrorSampleRate), @"errorSampleRate", nil]; + NSMutableDictionary *info = [NSMutableDictionary new]; + if (sessionId != nil) { + [info setObject:SENTRY_UNWRAP_NULLABLE(SentryId, sessionId).sentryIdString + forKey:@"replayId"]; + } + [info setObject:path.lastPathComponent forKey:@"path"]; + [info setObject:@(options.onErrorSampleRate) forKey:@"errorSampleRate"]; NSData *data = [SentrySerializationSwift dataWithJSONObject:info]; diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 3a0800d736..e9c4e1ed77 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -30,14 +30,12 @@ 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 environmentChecker: SentrySessionReplayEnvironmentCheckerProvider var isRunning: Bool { displayLink.isRunning() @@ -48,7 +46,6 @@ import UIKit public init( replayOptions: SentryReplayOptions, - experimentalOptions: SentryExperimentalOptions, replayFolderPath: URL, screenshotProvider: SentryViewScreenshotProvider, replayMaker: SentryReplayVideoMaker, @@ -57,10 +54,8 @@ import UIKit dateProvider: SentryCurrentDateProvider, delegate: SentrySessionReplayDelegate, displayLinkWrapper: SentryReplayDisplayLinkWrapper, - environmentChecker: SentrySessionReplayEnvironmentCheckerProvider ) { self.replayOptions = replayOptions - self.experimentalOptions = experimentalOptions self.dateProvider = dateProvider self.delegate = delegate self.screenshotProvider = screenshotProvider @@ -69,28 +64,30 @@ import UIKit self.replayMaker = replayMaker self.breadcrumbConverter = breadcrumbConverter self.touchTracker = touchTracker - self.environmentChecker = environmentChecker } deinit { displayLink.invalidate() } - - public func start(rootView: UIView, fullSession: Bool) { - SentrySDKLog.debug("[Session Replay] Starting session replay with full session: \(fullSession)") - guard !isRunning else { - SentrySDKLog.debug("[Session Replay] Session replay is already running, not starting again") - return - } - + + static public func shouldEnableSessionReplay(environmentChecker: SentrySessionReplayEnvironmentCheckerProvider, experimentalOptions: SentryExperimentalOptions) -> Bool { // Detect if we are running on iOS 26.0 with Liquid Glass and disable session replay. // This needs to be done until masking for session replay is properly supported, as it can lead // to PII leaks otherwise. if !environmentChecker.isReliable() { 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 + return false } SentrySDKLog.warning("[Session Replay] Detected environment potentially causing PII leaks, but `options.experimental.enableSessionReplayInUnreliableEnvironment` is set to `true`, ignoring and enabling Session Replay.") } + return true + } + + public func start(rootView: UIView, fullSession: Bool) { + SentrySDKLog.debug("[Session Replay] Starting session replay with full session: \(fullSession)") + guard !isRunning else { + SentrySDKLog.debug("[Session Replay] Session replay is already running, not starting again") + return + } displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) self.rootView = rootView From 82adc05b63bb120e0e8c523027fbabd6396c924f Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Fri, 24 Oct 2025 17:40:37 -0300 Subject: [PATCH 2/3] Fix tests --- Sources/Swift/Helper/Dependencies.swift | 2 +- .../SessionReplay/SentrySessionReplay.swift | 2 +- .../SentrySessionReplayIntegrationTests.swift | 51 ++++++++++++++++++ .../SentrySessionReplayTests.swift | 53 +------------------ 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Sources/Swift/Helper/Dependencies.swift b/Sources/Swift/Helper/Dependencies.swift index 8f1b41eafb..ebf7a0b4b1 100644 --- a/Sources/Swift/Helper/Dependencies.swift +++ b/Sources/Swift/Helper/Dependencies.swift @@ -5,7 +5,7 @@ @objc public static let threadWrapper = SentryThreadWrapper() @objc public static let processInfoWrapper: SentryProcessInfoSource = ProcessInfo.processInfo static let infoPlistWrapper: SentryInfoPlistWrapperProvider = SentryInfoPlistWrapper() - @objc public static let sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentChecker = { + @objc public static var sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentCheckerProvider = { SentrySessionReplayEnvironmentChecker(infoPlistWrapper: Dependencies.infoPlistWrapper) }() @objc public static let dispatchQueueWrapper = SentryDispatchQueueWrapper() diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index e9c4e1ed77..d62e8c955e 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -53,7 +53,7 @@ import UIKit touchTracker: SentryTouchTracker?, dateProvider: SentryCurrentDateProvider, delegate: SentrySessionReplayDelegate, - displayLinkWrapper: SentryReplayDisplayLinkWrapper, + displayLinkWrapper: SentryReplayDisplayLinkWrapper ) { self.replayOptions = replayOptions self.dateProvider = dateProvider diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 21f63826ba..0d7ef6b5a7 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -736,6 +736,57 @@ class SentrySessionReplayIntegrationTests: XCTestCase { // -- Assert -- XCTAssertNil(weakSut, "SentrySessionReplayIntegration should be deallocated") } + + func testInstallWithOptions_WithUnsafe_withoutOverrideOptionEnabled_shouldReturnFalse() { + // -- Arrange -- + let instance = SentrySessionReplayIntegration() + + let options = Options() + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + options.experimental.enableSessionReplayInUnreliableEnvironment = false + + Dependencies.sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) + + // -- Act -- + let result = instance.install(with: options) + + // -- Assert -- + XCTAssertFalse(result) + } + + func testInstallWithOptions_WithUnsafe_withOverrideOptionEnabled_shouldReturnTrue() { + // -- Arrange -- + let instance = SentrySessionReplayIntegration() + + let options = Options() + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + options.experimental.enableSessionReplayInUnreliableEnvironment = true + + Dependencies.sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) + + // -- Act -- + let result = instance.install(with: options) + + // -- Assert -- + XCTAssertTrue(result) + } + + func testInstallWithOptions_WithoutUnsafe_shouldReturnTrue() { + // -- Arrange -- + let instance = SentrySessionReplayIntegration() + + let options = Options() + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + options.experimental.enableSessionReplayInUnreliableEnvironment = false + + Dependencies.sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: true) + + // -- Act -- + let result = instance.install(with: options) + + // -- Assert -- + XCTAssertTrue(result) + } private func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws { let replayFolder = replayFolder() diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 38043b7620..9e42a81a58 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -80,9 +80,6 @@ class SentrySessionReplayTests: XCTestCase { let rootView = UIView() let replayMaker = TestReplayMaker() let cacheFolder = FileManager.default.temporaryDirectory - let environmentChecker = TestSessionReplayEnvironmentChecker( - mockedIsReliableReturnValue: true - ) var breadcrumbs: [Breadcrumb]? var isFullSession = true @@ -94,19 +91,14 @@ class SentrySessionReplayTests: XCTestCase { override init() { super.init() - - // By default we are testing a reliable environment so all of the functionality is enabled - environmentChecker.mockIsReliableReturnValue(true) } 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, @@ -114,8 +106,7 @@ class SentrySessionReplayTests: XCTestCase { touchTracker: touchTracker ?? SentryTouchTracker(dateProvider: dateProvider, scale: 0), dateProvider: dateProvider, delegate: self, - displayLinkWrapper: displayLink, - environmentChecker: environmentChecker + displayLinkWrapper: displayLink ) } @@ -566,48 +557,6 @@ class SentrySessionReplayTests: XCTestCase { XCTAssertEqual(fixture.displayLink.invalidateInvocations.count, 1) } - func testStart_withUnreliableEnvironment_withoutOverrideOptionEnabled_shouldNotStart() { - // -- Arrange -- - let fixture = Fixture() - fixture.environmentChecker.mockIsReliableReturnValue(false) - - 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 start - // (it should have been blocked by isInUnreliableEnvironment) - XCTAssertFalse(fixture.displayLink.isRunning()) - } - - func testStart_withUnreliableEnvironment_withOverrideOptionEnabled_shouldStart() { - // -- Arrange -- - let fixture = Fixture() - fixture.environmentChecker.mockIsReliableReturnValue(false) - - let options = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) - 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 unreliable environment - // (override option is enabled) - XCTAssertTrue(fixture.displayLink.isRunning(), "Session replay should start when override option is enabled") - } - // MARK: - Helpers private func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { From f9580270d4bf02eeb906b13ab2bf9b40437a43d5 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Tue, 28 Oct 2025 11:57:37 -0300 Subject: [PATCH 3/3] Ensure SesionReplay unreliable environment is not bypassed by SentryReplayApi --- Sentry.xcodeproj/project.pbxproj | 4 + SentryTestUtils/ClearTestState.swift | 1 + Sources/Sentry/SentryReplayApi.m | 6 +- .../Sentry/SentrySessionReplayIntegration.m | 14 ++- .../SentrySessionReplayIntegration.h | 6 + Sources/Swift/Helper/Dependencies.swift | 5 + .../SessionReplay/SentryReplayApiTests.swift | 106 ++++++++++++++++++ scripts/.swiftlint-version | 2 +- 8 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index b9a229f279..418b60c9bd 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -982,6 +982,7 @@ F41362112E1C55AF00B84443 /* SentryScopePersistentStore+Tags.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41362102E1C55AF00B84443 /* SentryScopePersistentStore+Tags.swift */; }; F41362132E1C566100B84443 /* SentryScopePersistentStore+User.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41362122E1C566100B84443 /* SentryScopePersistentStore+User.swift */; }; F41362152E1C568400B84443 /* SentryScopePersistentStore+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41362142E1C568400B84443 /* SentryScopePersistentStore+Context.swift */; }; + F42674482EB0FBA600E09150 /* SentryReplayApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42674472EB0FBA600E09150 /* SentryReplayApiTests.swift */; }; F429D37F2E8532A300DBF387 /* HttpDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429D37D2E8532A300DBF387 /* HttpDateParser.swift */; }; F429D39A2E85360F00DBF387 /* RetryAfterHeaderParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429D3992E85360F00DBF387 /* RetryAfterHeaderParser.swift */; }; F429D3AA2E8562EF00DBF387 /* RateLimitParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429D3A82E8562EF00DBF387 /* RateLimitParser.swift */; }; @@ -2363,6 +2364,7 @@ F41362102E1C55AF00B84443 /* SentryScopePersistentStore+Tags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Tags.swift"; sourceTree = ""; }; F41362122E1C566100B84443 /* SentryScopePersistentStore+User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+User.swift"; sourceTree = ""; }; F41362142E1C568400B84443 /* SentryScopePersistentStore+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Context.swift"; sourceTree = ""; }; + F42674472EB0FBA600E09150 /* SentryReplayApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayApiTests.swift; sourceTree = ""; }; F429D37D2E8532A300DBF387 /* HttpDateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpDateParser.swift; sourceTree = ""; }; F429D3992E85360F00DBF387 /* RetryAfterHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryAfterHeaderParser.swift; sourceTree = ""; }; F429D3A82E8562EF00DBF387 /* RateLimitParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitParser.swift; sourceTree = ""; }; @@ -4423,6 +4425,7 @@ D80694C12B7CC85800B820E6 /* SessionReplay */ = { isa = PBXGroup; children = ( + F42674472EB0FBA600E09150 /* SentryReplayApiTests.swift */, D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */, D49480D22DC23E8E00A3B6E9 /* SentryReplayTypeTests.swift */, D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, @@ -6285,6 +6288,7 @@ 62EF86A12C626D39004E058B /* SentryANRTrackerV2Tests.swift in Sources */, D434DB0A2DE09CDB00DD6F82 /* TestSentryWatchdogTerminationBreadcrumbProcessor.swift in Sources */, D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */, + F42674482EB0FBA600E09150 /* SentryReplayApiTests.swift in Sources */, 7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */, 7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */, D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */, diff --git a/SentryTestUtils/ClearTestState.swift b/SentryTestUtils/ClearTestState.swift index cfcf7b6966..c92f810a52 100644 --- a/SentryTestUtils/ClearTestState.swift +++ b/SentryTestUtils/ClearTestState.swift @@ -69,5 +69,6 @@ class TestCleanup: NSObject { SentrySdkPackage.resetPackageManager() SentryExtraPackages.clear() + Dependencies.reset() } } diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index 7472e31f68..146e6d20a9 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -56,8 +56,12 @@ - (void)start SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false replayIntegration = (SentrySessionReplayIntegration *)[SentrySDKInternal.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; if (replayIntegration == nil) { - SENTRY_LOG_DEBUG(@"[Session Replay] Initializing replay integration"); SentryOptions *currentOptions = SentrySDKInternal.currentHub.client.options; + if (![SentrySessionReplayIntegration shouldEnabledForOptions:currentOptions]) { + return; + } + SENTRY_LOG_DEBUG(@"[Session Replay] Initializing replay integration"); + replayIntegration = [[SentrySessionReplayIntegration alloc] initForManualUse:currentOptions]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index dc98db0d55..92d77793f8 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -77,14 +77,18 @@ - (instancetype)initForManualUse:(nonnull SentryOptions *)options return self; } ++ (BOOL)shouldEnabledForOptions:(SentryOptions *)options +{ + return [SentrySessionReplay + shouldEnableSessionReplayWithEnvironmentChecker:SentryDependencies + .sessionReplayEnvironmentChecker + experimentalOptions:options.experimental]; +} + - (BOOL)installWithOptions:(nonnull SentryOptions *)options { if ([super installWithOptions:options] == NO || - [SentrySessionReplay - shouldEnableSessionReplayWithEnvironmentChecker:SentryDependencies - .sessionReplayEnvironmentChecker - experimentalOptions:options.experimental] - == NO) { + [SentrySessionReplayIntegration shouldEnabledForOptions:options] == NO) { return NO; } diff --git a/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h b/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h index 02243685a3..896a28eb19 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h @@ -45,6 +45,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)hideMaskPreview; +/** + * Verifies the device environment and options and returns wether it is safe to enable + * SessionReplay or not + */ ++ (BOOL)shouldEnabledForOptions:(SentryOptions *)options; + @end #endif // SENTRY_TARGET_REPLAY_SUPPORTED NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/Dependencies.swift b/Sources/Swift/Helper/Dependencies.swift index ebf7a0b4b1..689de1edf1 100644 --- a/Sources/Swift/Helper/Dependencies.swift +++ b/Sources/Swift/Helper/Dependencies.swift @@ -22,4 +22,9 @@ @objc public static var threadInspector = SentryThreadInspector() @objc public static var fileIOTracker = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + static func reset() { + Self.sessionReplayEnvironmentChecker = SentrySessionReplayEnvironmentChecker(infoPlistWrapper: Dependencies.infoPlistWrapper) + Self.threadInspector = SentryThreadInspector() + Self.fileIOTracker = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + } } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift new file mode 100644 index 0000000000..9bd3faffba --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayApiTests.swift @@ -0,0 +1,106 @@ +import Foundation +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) + +class SentryReplayApiTests: XCTestCase { + override func tearDown() { + super.tearDown() + clearTestState() + } + + // MARK: - Tests + + func testStart_whenReplayIntegrationAlreadyInstalled_shouldCallStartOnExistingIntegration() { + // Arrange + let mockClient = TestClient(options: Options()) + let mockReplayIntegration = MockSessionReplayIntegration() + let mockHub = TestHub(client: mockClient, andScope: Scope()) + mockHub.removeAllIntegrations() + mockHub.addInstalledIntegration(mockReplayIntegration, name: "SentrySessionReplayIntegration") + SentrySDKInternal.setCurrentHub(mockHub) + + let sut = SentryReplayApi() + + // Act + sut.start() + + // Assert + XCTAssertTrue(mockReplayIntegration.startCalled) + XCTAssertEqual(mockHub.installedIntegrations().count, 1) // No new integration added + } + + func testStart_whenReplayIntegrationNilAndUnreliableToEnable_shouldNotCreateIntegration() { + // Arrange + let options = Options() + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + options.experimental.enableSessionReplayInUnreliableEnvironment = false + let mockClient = TestClient(options: options) + let mockHub = TestHub(client: mockClient, andScope: Scope()) + mockHub.removeAllIntegrations() + SentrySDKInternal.setCurrentHub(mockHub) + + let sut = SentryReplayApi() + + Dependencies.sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) + + // Act + sut.start() + + // Assert + XCTAssertTrue(mockHub.installedIntegrations().isEmpty) + } + + func testStart_whenReplayIntegrationNilWithUnreliableEnvironmentAndExperimental_shouldCreateAndInstallIntegration() throws { + // Arrange + let options = Options() + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1.0) + options.experimental.enableSessionReplayInUnreliableEnvironment = true + options.dsn = "https://user@test.com/test" + let mockClient = TestClient(options: options) + let mockHub = TestHub(client: mockClient, andScope: Scope()) + mockHub.removeAllIntegrations() + SentrySDKInternal.setCurrentHub(mockHub) + + let sut = SentryReplayApi() + + Dependencies.sessionReplayEnvironmentChecker = TestSessionReplayEnvironmentChecker(mockedIsReliableReturnValue: false) + + let dispatchQueue = TestSentryDispatchQueueWrapper() + SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueue + SentryDependencyContainer.sharedInstance().fileManager = try SentryFileManager( + options: options, + dateProvider: SentryDependencyContainer.sharedInstance().dateProvider, + dispatchQueueWrapper: dispatchQueue + ) + + // Act + sut.start() + + // Assert + XCTAssertEqual(mockHub.installedIntegrations().count, 1) + let hub = try XCTUnwrap(mockHub.installedIntegrations().first as? SentrySessionReplayIntegration) + XCTAssertNotNil(hub.sessionReplay) + XCTAssertTrue(hub.sessionReplay.isRunning) + SentrySDKInternal.currentHub().endSession() + XCTAssertTrue(hub.sessionReplay.isFullSession) + } +} + +// MARK: - Mock Classes + +private class MockSessionReplayIntegration: SentrySessionReplayIntegration { + var startCalled = false + + func install(with hub: SentryHub) -> Bool { + return true + } + + @objc override func start() { + startCalled = true + } +} + +#endif // os(iOS) || os(tvOS) diff --git a/scripts/.swiftlint-version b/scripts/.swiftlint-version index 0b09455034..e2050de802 100644 --- a/scripts/.swiftlint-version +++ b/scripts/.swiftlint-version @@ -1 +1 @@ -0.61.0 +0.62.1