diff --git a/CHANGELOG.md b/CHANGELOG.md index 489ce48df48..316d5f61996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Restart replay session with mobile session (#4085) - Add pause and resume AppHangTracking API (#4077). You can now pause and resume app hang tracking with `SentrySDK.pauseAppHangTracking()` and `SentrySDK.resumeAppHangTracking()`. ## 8.29.1 diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index f0f7a9f86df..3e3ad01be4b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -902,6 +902,7 @@ D8C66A372A77B1F70015696A /* SentryPropagationContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D8C66A352A77B1F70015696A /* SentryPropagationContext.m */; }; D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9928000E23007E326E /* SentryUIApplication.h */; }; D8C67E9C28000E24007E326E /* SentryScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9A28000E23007E326E /* SentryScreenshot.h */; }; + D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CA12942C203E71005894F4 /* SentrySessionListener.swift */; }; D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; @@ -1972,6 +1973,7 @@ D8C66A352A77B1F70015696A /* SentryPropagationContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPropagationContext.m; sourceTree = ""; }; D8C67E9928000E23007E326E /* SentryUIApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryUIApplication.h; path = include/SentryUIApplication.h; sourceTree = ""; }; D8C67E9A28000E23007E326E /* SentryScreenshot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshot.h; path = include/SentryScreenshot.h; sourceTree = ""; }; + D8CA12942C203E71005894F4 /* SentrySessionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionListener.swift; sourceTree = ""; }; D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; @@ -3601,7 +3603,6 @@ D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */, D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */, D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */, - D82DD1CC2BEEB1A0001AB556 /* SentryBreadcrumbReplayConverterTests.swift */, D8DBE0C92C0E093000FAB1FD /* SentryTouchTrackerTests.swift */, D8DBE0D12C0EFFC300FAB1FD /* SentryReplayOptionsTests.swift */, ); @@ -3896,6 +3897,7 @@ D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */, + D8CA12942C203E71005894F4 /* SentrySessionListener.swift */, ); path = Protocol; sourceTree = ""; @@ -4536,6 +4538,7 @@ 63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */, 63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */, 7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */, + D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */, 63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */, 62A2F43E2BA9AC10000C9FDD /* DistributionMetric.swift in Sources */, 7BA0C0482805600A003E0326 /* SentryTransportAdapter.m in Sources */, diff --git a/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h b/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h index 5d94b2d0654..00372234869 100644 --- a/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h +++ b/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h @@ -51,7 +51,7 @@ #import "SentrySDK+Private.h" #import "SentrySDK+Tests.h" #import "SentryScopeSyncC.h" -#import "SentrySession.h" + #import "SentrySwizzleWrapper.h" #import "SentrySystemWrapper.h" #import "SentryThreadInspector.h" diff --git a/SentryTestUtils/TestTransportAdapter.swift b/SentryTestUtils/TestTransportAdapter.swift index 73ce0473dfd..ce542ff88a0 100644 --- a/SentryTestUtils/TestTransportAdapter.swift +++ b/SentryTestUtils/TestTransportAdapter.swift @@ -1,3 +1,4 @@ +import _SentryPrivate import Foundation public class TestTransportAdapter: SentryTransportAdapter { diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 19aa7b96b3a..feda90b3e86 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -44,6 +44,7 @@ @property (nonatomic, strong) SentryCrashWrapper *crashWrapper; @property (nonatomic, strong) NSMutableSet *installedIntegrationNames; @property (nonatomic) NSUInteger errorsBeforeSession; +@property (nonatomic, weak) id sessionListener; @end @@ -107,6 +108,7 @@ - (void)startSession andLevel:kSentryLevelError]; return; } + @synchronized(_sessionLock) { if (_session != nil) { lastSession = _session; @@ -133,6 +135,8 @@ - (void)startSession [lastSession endSessionExitedWithTimestamp:[SentryDependencyContainer.sharedInstance.dateProvider date]]; [self captureSession:lastSession]; + + [_sessionListener sentrySessionStarted:_session]; } - (void)endSession @@ -156,6 +160,8 @@ - (void)endSessionWithTimestamp:(NSDate *)timestamp } [currentSession endSessionExitedWithTimestamp:timestamp]; [self captureSession:currentSession]; + + [_sessionListener sentrySessionEnded:currentSession]; } - (void)storeCurrentSession:(SentrySession *)session @@ -767,6 +773,18 @@ - (LocalMetricsAggregator *_Nullable)getLocalMetricsAggregatorWithSpan:(id)listener +{ + _sessionListener = listener; +} + +- (void)unregisterSessionListener:(id)listener +{ + if (_sessionListener == listener) { + _sessionListener = nil; + } +} + #pragma mark - Protected - (NSMutableArray *)trimmedInstalledIntegrationNames diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index bd6938453cb..20a2da8d7a3 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -115,6 +115,9 @@ - (void)startFullReplay - (void)stop { @synchronized(self) { + if (_isRunning == NO) { + return; + } [_displayLink invalidate]; _isRunning = NO; [self prepareSegmentUntil:_dateProvider.date]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index cfc3378c5cd..7b3e8de98c6 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -37,6 +37,7 @@ - (void)newSceneActivate; @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; + SentryNSNotificationCenterWrapper *_notificationCenter; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -45,36 +46,46 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - if (@available(iOS 16.0, tvOS 16.0, *)) { - _replayOptions = options.experimental.sessionReplay; + _replayOptions = options.experimental.sessionReplay; - _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; + if (options.enableSwizzling) { + _touchTracker = [[SentryTouchTracker alloc] + initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider + scale:options.experimental.sessionReplay.sizeScale]; + [self swizzleApplicationTouch]; + } - if (!_startedAsFullSession && _replayOptions.errorSampleRate == 0) { - return NO; - } + _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; - if (options.enableSwizzling) { - _touchTracker = [[SentryTouchTracker alloc] - initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider - scale:options.experimental.sessionReplay.sizeScale]; - [self swizzleApplicationTouch]; - } + [SentrySDK.currentHub registerSessionListener:self]; - if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) { - // If a window its already available start replay right away - [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; - } else { - // Wait for a scene to be available to started the replay - [SentryDependencyContainer.sharedInstance.notificationCenterWrapper - addObserver:self - selector:@selector(newSceneActivate) - name:UISceneDidActivateNotification]; - } + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [self.sessionReplay captureReplayForEvent:event]; + return event; + }]; - return YES; + return YES; +} + +- (void)startSession +{ + _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; + + if (!_startedAsFullSession && _replayOptions.errorSampleRate == 0) { + return; + } + + if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) { + // If a window its already available start replay right away + [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; } else { - return NO; + // Wait for a scene to be available to started the replay + if (@available(iOS 13.0, tvOS 13.0, *)) { + [_notificationCenter addObserver:self + selector:@selector(newSceneActivate) + name:UISceneDidActivateNotification]; + } } } @@ -98,58 +109,49 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions breadcrumbConverter:(id)breadcrumbConverter fullSession:(BOOL)shouldReplayFullSession { - if (@available(iOS 16.0, tvOS 16.0, *)) { - NSURL *docs = [NSURL - fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; - docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; - NSString *currentSession = [NSUUID UUID].UUIDString; - docs = [docs URLByAppendingPathComponent:currentSession]; - - if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:docs - withIntermediateDirectories:YES - attributes:nil - error:nil]; - } - - SentryOnDemandReplay *replayMaker = - [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; - replayMaker.bitRate = replayOptions.replayBitRate; - replayMaker.cacheMaxSize - = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration - : replayOptions.errorReplayDuration); - - self.sessionReplay = [[SentrySessionReplay alloc] - initWithSettings:replayOptions - replayFolderPath:docs - screenshotProvider:screenshotProvider - replayMaker:replayMaker - breadcrumbConverter:breadcrumbConverter - touchTracker:_touchTracker - dateProvider:SentryDependencyContainer.sharedInstance.dateProvider - random:SentryDependencyContainer.sharedInstance.random - displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; - - [self.sessionReplay - start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(stop) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(resume) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - - [SentryGlobalEventProcessor.shared - addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - [self.sessionReplay captureReplayForEvent:event]; - return event; - }]; + NSURL *docs = + [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; + NSString *currentSession = [NSUUID UUID].UUIDString; + docs = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:docs + withIntermediateDirectories:YES + attributes:nil + error:nil]; } + + SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; + replayMaker.bitRate = replayOptions.replayBitRate; + replayMaker.cacheMaxSize + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + : replayOptions.errorReplayDuration); + + self.sessionReplay = [[SentrySessionReplay alloc] + initWithSettings:replayOptions + replayFolderPath:docs + screenshotProvider:screenshotProvider + replayMaker:replayMaker + breadcrumbConverter:breadcrumbConverter + touchTracker:_touchTracker + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider + random:SentryDependencyContainer.sharedInstance.random + displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; + + [self.sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; + + [_notificationCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [_notificationCenter addObserver:self + selector:@selector(resume) + name:UIApplicationWillEnterForegroundNotification + object:nil]; } - (void)stop @@ -162,6 +164,26 @@ - (void)resume [self.sessionReplay resume]; } +- (void)sentrySessionEnded:(SentrySession *)session +{ + [self stop]; + [_notificationCenter removeObserver:self + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [_notificationCenter removeObserver:self + name:UIApplicationWillEnterForegroundNotification + object:nil]; + _sessionReplay = nil; +} + +- (void)sentrySessionStarted:(SentrySession *)session +{ + if (_sessionReplay) { + return; + } + [self startSession]; +} + - (void)captureReplay { [self.sessionReplay captureReplay]; @@ -186,10 +208,16 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { + [SentrySDK.currentHub unregisterSessionListener:self]; _touchTracker = nil; [self stop]; } +- (void)dealloc +{ + [self uninstall]; +} + - (BOOL)shouldReplayFullSession:(CGFloat)rate { return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index cf643e217c6..dced57ca3f8 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -13,6 +13,7 @@ @class SentryReplayEvent; @class SentryReplayRecording; @protocol SentryIntegrationProtocol; +@protocol SentrySessionListener; NS_ASSUME_NONNULL_BEGIN @@ -62,6 +63,8 @@ SentryHub () - (void)captureEnvelope:(SentryEnvelope *)envelope; +- (void)registerSessionListener:(id)listener; +- (void)unregisterSessionListener:(id)listener; - (nullable id)getInstalledIntegration:(Class)integrationClass; @end diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 23fb7856a16..a58c32da6fb 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -13,3 +13,4 @@ #import "SentryDateUtil.h" #import "SentryLevelHelper.h" #import "SentrySdkInfo.h" +#import "SentrySession.h" diff --git a/Sources/Sentry/include/SentrySession.h b/Sources/Sentry/include/SentrySession.h index 3778f8ec5e7..f852f4a0d5f 100644 --- a/Sources/Sentry/include/SentrySession.h +++ b/Sources/Sentry/include/SentrySession.h @@ -1,5 +1,4 @@ #import "SentryDefines.h" -#import "SentrySerializable.h" @class SentryUser; @@ -15,7 +14,7 @@ typedef NS_ENUM(NSUInteger, SentrySessionStatus) { /** * The SDK uses SentrySession to inform Sentry about release and project associated project health. */ -@interface SentrySession : NSObject +@interface SentrySession : NSObject SENTRY_NO_INIT - (instancetype)initWithReleaseName:(NSString *)releaseName distinctId:(NSString *)distinctId; diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration+Private.h b/Sources/Sentry/include/SentrySessionReplayIntegration+Private.h index 277b9a6baff..b0f70e1aa4c 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration+Private.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration+Private.h @@ -7,7 +7,7 @@ @class SentrySessionReplay; @interface -SentrySessionReplayIntegration () +SentrySessionReplayIntegration () @property (nonatomic, strong) SentrySessionReplay *sessionReplay; diff --git a/Sources/Swift/Protocol/SentrySessionListener.swift b/Sources/Swift/Protocol/SentrySessionListener.swift new file mode 100644 index 00000000000..06e320efa33 --- /dev/null +++ b/Sources/Swift/Protocol/SentrySessionListener.swift @@ -0,0 +1,8 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +@objc +protocol SentrySessionListener: NSObjectProtocol { + func sentrySessionEnded(_ session: SentrySession) + func sentrySessionStarted(_ session: SentrySession) +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 9c17ff434b5..b558ab3516f 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -1,5 +1,4 @@ import Foundation -import Nimble @testable import Sentry import SentryTestUtils import XCTest @@ -32,28 +31,34 @@ class SentrySessionReplayIntegrationTests: XCTestCase { clearTestState() } - func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true) { + private func getSut() throws -> SentrySessionReplayIntegration { + return try XCTUnwrap(SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration) + } + + private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true) { if #available(iOS 16.0, tvOS 16.0, *) { SentrySDK.start { + $0.dsn = "https://user@test.com/test" $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate) $0.setIntegrations([SentrySessionReplayIntegration.self]) $0.enableSwizzling = enableSwizzling } + SentrySDK.currentHub().startSession() } } func testNoInstall() { startSDK(sessionSampleRate: 0, errorSampleRate: 0) - expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 - expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + XCTAssertEqual(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count, 0) + XCTAssertEqual(SentryGlobalEventProcessor.shared().processors.count, 0) } func testInstallFullSessionReplay() { startSDK(sessionSampleRate: 1, errorSampleRate: 0) - expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 - expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + XCTAssertEqual(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count, 1) + XCTAssertEqual(SentryGlobalEventProcessor.shared().processors.count, 1) } func testInstallNoSwizzlingNoTouchTracker() { @@ -66,69 +71,92 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNil(Dynamic(integration).getTouchTracker().asObject) } - func testInstallWithSwizzlingHasTouchTracker() { + func testInstallWithSwizzlingHasTouchTracker() throws { startSDK(sessionSampleRate: 1, errorSampleRate: 0) - guard let integration = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration - else { - XCTFail("Could not find session replay integration") - return - } - XCTAssertNotNil(Dynamic(integration).getTouchTracker().asObject) + let sut = try getSut() + XCTAssertNotNil(Dynamic(sut).getTouchTracker().asObject) } - func testNoInstallFullSessionReplayBecauseOfRandom() { + func testInstallFullSessionReplayButDontRunBecauseOfRandom() throws { SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.3) startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) - expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 - expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + XCTAssertEqual(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count, 1) + XCTAssertEqual(SentryGlobalEventProcessor.shared().processors.count, 1) + let sut = try getSut() + XCTAssertNil(sut.sessionReplay) } - func testInstallFullSessionReplayBecauseOfRandom() { + func testInstallFullSessionReplayBecauseOfRandom() throws { SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.1) startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) - expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 - expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + XCTAssertEqual(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count, 1) + XCTAssertEqual(SentryGlobalEventProcessor.shared().processors.count, 1) + let sut = try getSut() + XCTAssertNotNil(sut.sessionReplay) } func testInstallErrorReplay() { startSDK(sessionSampleRate: 0, errorSampleRate: 0.1) - expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 - expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + XCTAssertEqual(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count, 1) + XCTAssertEqual(SentryGlobalEventProcessor.shared().processors.count, 1) } - func testWaitForNotificationWithNoWindow() { + func testWaitForNotificationWithNoWindow() throws { uiApplication.windowsMock = nil startSDK(sessionSampleRate: 1, errorSampleRate: 0) - guard let sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration else { - fail("Did not installed replay integration") - return - } + let sut = try getSut() - expect(Dynamic(sut).sessionReplay.asObject) == nil + XCTAssertNil(sut.sessionReplay) uiApplication.windowsMock = [UIWindow()] NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil) - expect(Dynamic(sut).sessionReplay.asObject) != nil + XCTAssertNotNil(sut.sessionReplay) } - func testPauseAndResumeForApplicationStateChange() { + func testPauseAndResumeForApplicationStateChange() throws { startSDK(sessionSampleRate: 1, errorSampleRate: 0) - guard let sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration else { - XCTFail("Did not find Session Replay Integration") - return - } + let sut = try getSut() NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) XCTAssertFalse(Dynamic(sut.sessionReplay).isRunning.asBool ?? true) NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) XCTAssertTrue(Dynamic(sut.sessionReplay).isRunning.asBool ?? false) } + + func testStopReplayAtEndOfSession() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 0) + + let sut = try getSut() + XCTAssertNotNil(sut.sessionReplay) + SentrySDK.currentHub().endSession() + XCTAssertNil(sut.sessionReplay) + } + + func testStartFullSessionForError() throws { + startSDK(sessionSampleRate: 0, errorSampleRate: 1) + let sut = try getSut() + + XCTAssertFalse(Dynamic(sut.sessionReplay).isFullSession.asBool ?? true) + SentrySDK.capture(error: NSError(domain: "", code: 1)) + XCTAssertTrue(Dynamic(sut.sessionReplay).isFullSession.asBool ?? false) + } + + func testRestartReplayWithNewSession() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 0) + + let sut = try getSut() + XCTAssertNotNil(sut.sessionReplay) + SentrySDK.currentHub().endSession() + XCTAssertNil(sut.sessionReplay) + SentrySDK.currentHub().startSession() + XCTAssertNotNil(sut.sessionReplay) + } } #endif