From edfab344eb4b4922920c7245c52cac7627d2b846 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 7 Oct 2024 17:10:00 +0200 Subject: [PATCH 1/7] wip --- Sources/Sentry/Public/SentryReplayApi.h | 14 +++++++ Sources/Sentry/SentryReplayApi.m | 42 ++++++++++++++++--- .../Sentry/SentrySessionReplayIntegration.m | 29 +++++++++++++ .../include/SentrySessionReplayIntegration.h | 4 ++ .../SessionReplay/SentrySessionReplay.swift | 10 +++++ .../SentrySessionReplayIntegrationTests.swift | 31 ++++++++++++-- 6 files changed, 122 insertions(+), 8 deletions(-) diff --git a/Sources/Sentry/Public/SentryReplayApi.h b/Sources/Sentry/Public/SentryReplayApi.h index 800dbb0c976..f9f001e9c1a 100644 --- a/Sources/Sentry/Public/SentryReplayApi.h +++ b/Sources/Sentry/Public/SentryReplayApi.h @@ -42,6 +42,20 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)resume; +/** + * Start recording a session replay if not started. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)start; + +/** + * Stop the current session replay recording. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)stop; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index 9d2e7ba4b10..f9de2ae6fbe 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -3,8 +3,9 @@ #if SENTRY_TARGET_REPLAY_SUPPORTED # import "SentryHub+Private.h" +# import "SentryOptions+Private.h" # import "SentrySDK+Private.h" -# import "SentrySessionReplayIntegration.h" +# import "SentrySessionReplayIntegration+Private.h" # import "SentrySwift.h" # import @@ -22,18 +23,49 @@ - (void)unmaskView:(UIView *)view - (void)pause { - SentrySessionReplayIntegration *replayIntegration = - [SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; + SentrySessionReplayIntegration *replayIntegration + = (SentrySessionReplayIntegration *)[SentrySDK.currentHub + getInstalledIntegration:SentrySessionReplayIntegration.class]; [replayIntegration pause]; } - (void)resume { - SentrySessionReplayIntegration *replayIntegration = - [SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; + SentrySessionReplayIntegration *replayIntegration + = (SentrySessionReplayIntegration *)[SentrySDK.currentHub + getInstalledIntegration:SentrySessionReplayIntegration.class]; [replayIntegration resume]; } +- (void)start +{ + SentrySessionReplayIntegration *replayIntegration + = (SentrySessionReplayIntegration *)[SentrySDK.currentHub + getInstalledIntegration:SentrySessionReplayIntegration.class]; + + if (replayIntegration == nil) { + replayIntegration = [[SentrySessionReplayIntegration alloc] init]; + + SentryOptions *options = [[SentryOptions alloc] init]; + options.enableSwizzling = SentrySDK.currentHub.client.options.enableSwizzling; + options.experimental.sessionReplay.sessionSampleRate = 1; + __unused BOOL installed = [replayIntegration installWithOptions:options]; + + [SentrySDK.currentHub addInstalledIntegration:replayIntegration + name:NSStringFromClass(SentrySessionReplay.class)]; + } + + [replayIntegration start]; +} + +- (void)stop +{ + SentrySessionReplayIntegration *replayIntegration + = (SentrySessionReplayIntegration *)[SentrySDK.currentHub + getInstalledIntegration:SentrySessionReplayIntegration.class]; + [replayIntegration stop]; +} + @end #endif diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index be03538ee4b..58b3710e545 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -351,6 +351,35 @@ - (void)resume [self.sessionReplay resume]; } +- (void)start +{ + if (self.sessionReplay != nil && self.sessionReplay.isRunning) { + return; + } else if (self.sessionReplay.isFullSession == NO) { + [self.sessionReplay captureReplay]; + return; + } + + _startedAsFullSession = YES; + if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) { + // If a window its already available start replay right away + [self startWithOptions:_replayOptions fullSession:YES]; + } else { + // 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]; + } + } +} + +- (void)stop +{ + [self.sessionReplay pause]; + self.sessionReplay = nil; +} + - (void)sentrySessionEnded:(SentrySession *)session { [self pause]; diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index f5fd4183a52..f7fe65e76d0 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -32,6 +32,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)resume; +- (void)stop; + +- (void)start; + @end #endif // SENTRY_TARGET_REPLAY_SUPPORTED NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 4fab0b3a52f..a56f1e9c6f0 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -130,6 +130,16 @@ class SentrySessionReplay: NSObject { videoSegmentStart = nil displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) } + + func stop() { + lock.lock() + defer { lock.unlock() } + displayLink.invalidate() + if isFullSession { + prepareSegmentUntil(date: dateProvider.date()) + } + + } func captureReplayFor(event: Event) { guard isRunning else { return } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index a308c8eb2ec..f59d17ca19a 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -39,15 +39,15 @@ class SentrySessionReplayIntegrationTests: XCTestCase { clearTestState() } - private func getSut() throws -> SentrySessionReplayIntegration { + private func getSut() throws -> SentrySessionReplayIntegration { return try XCTUnwrap(SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration) } - private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true, configure: ((Options) -> Void)? = nil) { + private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true, noIntegrations: Bool = false, configure: ((Options) -> Void)? = nil) { SentrySDK.start { $0.dsn = "https://user@test.com/test" $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, onErrorSampleRate: errorSampleRate) - $0.setIntegrations([SentrySessionReplayIntegration.self]) + $0.setIntegrations(noIntegrations ? [] : [SentrySessionReplayIntegration.self]) $0.enableSwizzling = enableSwizzling $0.cacheDirectoryPath = FileManager.default.temporaryDirectory.path configure?($0) @@ -309,6 +309,31 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertTrue(redactBuilder.containsIgnoreClass(AnotherLabel.self)) } + func testStop() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = sut.sessionReplay + XCTAssertTrue(sessionReplay?.isRunning ?? false) + + SentrySDK.replay.stop() + + XCTAssertFalse(sessionReplay?.isRunning ?? true) + XCTAssertNil(sut.sessionReplay) + } + + func testStart() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1, noIntegrations: true) + var sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration + XCTAssertNil(sut) + SentrySDK.replay.start() + sut = try getSut() + + let sessionReplay = sut?.sessionReplay + XCTAssertTrue(sessionReplay?.isRunning ?? false) + XCTAssertTrue(sessionReplay?.isFullSession ?? false) + XCTAssertNotNil(sut?.sessionReplay) + } + func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws { let options = Options() options.dsn = "https://user@test.com/test" From 24e56bc589e43e919ba8a28707a0d0edd58de6ef Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 8 Oct 2024 11:10:16 +0200 Subject: [PATCH 2/7] More tests --- .../Sentry/SentrySessionReplayIntegration.m | 8 +++--- .../SentrySessionReplayIntegrationTests.swift | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 58b3710e545..85f677775a1 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -353,10 +353,10 @@ - (void)resume - (void)start { - if (self.sessionReplay != nil && self.sessionReplay.isRunning) { - return; - } else if (self.sessionReplay.isFullSession == NO) { - [self.sessionReplay captureReplay]; + if (self.sessionReplay != nil) { + if (self.sessionReplay.isFullSession == NO) { + [self.sessionReplay captureReplay]; + } return; } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index f59d17ca19a..2c888df517b 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -321,8 +321,8 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNil(sut.sessionReplay) } - func testStart() throws { - startSDK(sessionSampleRate: 1, errorSampleRate: 1, noIntegrations: true) + func testStartWithNoSessionReplay() throws { + startSDK(sessionSampleRate: 0, errorSampleRate: 0, noIntegrations: true) var sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration XCTAssertNil(sut) SentrySDK.replay.start() @@ -334,6 +334,30 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNotNil(sut?.sessionReplay) } + func testStartWithSessionReplayRunning() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = try XCTUnwrap(sut.sessionReplay) + let replayId = sessionReplay.sessionReplayId + + SentrySDK.replay.start() + + //Test whether the integration keeps the same instance of the session replay + XCTAssertEqual(sessionReplay, sut.sessionReplay) + //Test whether the session Id is still the same + XCTAssertEqual(sessionReplay.sessionReplayId, replayId) + } + + func testStartWithBufferSessionReplay() throws { + startSDK(sessionSampleRate: 0, errorSampleRate: 1) + let sut = try getSut() + let sessionReplay = try XCTUnwrap(sut.sessionReplay) + + XCTAssertFalse(sessionReplay.isFullSession) + SentrySDK.replay.start() + XCTAssertTrue(sessionReplay.isFullSession) + } + func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws { let options = Options() options.dsn = "https://user@test.com/test" From e890fbe349d53a1d05ec7cc439496ef05f33151f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 8 Oct 2024 11:18:50 +0200 Subject: [PATCH 3/7] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1736a243191..41c0af9f88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- feat: Start/Stop session replay (#4414) + ## 8.38.0-beta.1 ### Features From 5afc257b77c2f5e42d956a18485601a6f11e843c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 9 Oct 2024 09:26:58 +0200 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Andrew McKnight --- CHANGELOG.md | 2 +- .../SessionReplay/SentrySessionReplayIntegrationTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c0af9f88f..de11b500537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- feat: Start/Stop session replay (#4414) +- feat: API to manually start/stop Session Replay (#4414) ## 8.38.0-beta.1 diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 2c888df517b..156ea6eb7d2 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -39,7 +39,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { clearTestState() } - private func getSut() throws -> SentrySessionReplayIntegration { + private func getSut() throws -> SentrySessionReplayIntegration { return try XCTUnwrap(SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration) } From 858c9f1b5b759fd4b2f77d65890e51d126648fea Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 9 Oct 2024 09:27:20 +0200 Subject: [PATCH 5/7] Update SentrySessionReplay.swift --- .../SessionReplay/SentrySessionReplay.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index a56f1e9c6f0..3b8b60497bf 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -130,17 +130,7 @@ class SentrySessionReplay: NSObject { videoSegmentStart = nil displayLink.link(withTarget: self, selector: #selector(newFrame(_:))) } - - func stop() { - lock.lock() - defer { lock.unlock() } - displayLink.invalidate() - if isFullSession { - prepareSegmentUntil(date: dateProvider.date()) - } - - } - + func captureReplayFor(event: Event) { guard isRunning else { return } From 669467a8047207895b77b61e56d078297af32dfa Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 9 Oct 2024 09:33:15 +0200 Subject: [PATCH 6/7] Update SentrySessionReplayIntegration.m --- .../Sentry/SentrySessionReplayIntegration.m | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 85f677775a1..b3ae19f3e20 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -212,23 +212,30 @@ - (void)startSession return; } + [self runReplayForAvailableWindow]; +} + +- (void)runReplayForAvailableWindow +{ if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) { // If a window its already available start replay right away [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; - } else { + } else if (@available(iOS 13.0, tvOS 13.0, *)) { // 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]; - } + [_notificationCenter addObserver:self + selector:@selector(newSceneActivate) + name:UISceneDidActivateNotification]; } } - (void)newSceneActivate { - [SentryDependencyContainer.sharedInstance.notificationCenterWrapper removeObserver:self]; - [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; + if (@available(iOS 13.0, tvOS 13.0, *)) { + [SentryDependencyContainer.sharedInstance.notificationCenterWrapper + removeObserver:self + name:UISceneDidActivateNotification]; + [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; + } } - (void)startWithOptions:(SentryReplayOptions *)replayOptions @@ -361,17 +368,7 @@ - (void)start } _startedAsFullSession = YES; - if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) { - // If a window its already available start replay right away - [self startWithOptions:_replayOptions fullSession:YES]; - } else { - // 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]; - } - } + [self runReplayForAvailableWindow]; } - (void)stop From 6eb502c2e662e4c49ea48c4f6194b71f058e4c58 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 9 Oct 2024 11:38:06 +0200 Subject: [PATCH 7/7] ref --- Sources/Sentry/SentryOptions.m | 1 - Sources/Sentry/SentryReplayApi.m | 9 ++--- .../Sentry/SentrySessionReplayIntegration.m | 39 +++++++++++-------- .../include/SentrySessionReplayIntegration.h | 5 +-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 554eb37b43b..64647bb2b19 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -819,5 +819,4 @@ - (NSString *)debugDescription return [NSString stringWithFormat:@"<%@: {\n%@\n}>", self, propertiesDescription]; } #endif // defined(DEBUG) || defined(TEST) || defined(TESTCI) - @end diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index f9de2ae6fbe..3aad160229c 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -44,12 +44,9 @@ - (void)start getInstalledIntegration:SentrySessionReplayIntegration.class]; if (replayIntegration == nil) { - replayIntegration = [[SentrySessionReplayIntegration alloc] init]; - - SentryOptions *options = [[SentryOptions alloc] init]; - options.enableSwizzling = SentrySDK.currentHub.client.options.enableSwizzling; - options.experimental.sessionReplay.sessionSampleRate = 1; - __unused BOOL installed = [replayIntegration installWithOptions:options]; + SentryOptions *currentOptions = SentrySDK.currentHub.client.options; + replayIntegration = + [[SentrySessionReplayIntegration alloc] initForManualUse:currentOptions]; [SentrySDK.currentHub addInstalledIntegration:replayIntegration name:NSStringFromClass(SentrySessionReplay.class)]; diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index b3ae19f3e20..cd6df2e838c 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -37,8 +37,6 @@ */ static SentryTouchTracker *_touchTracker; -static SentrySessionReplayIntegration *_installedInstance; - @interface SentrySessionReplayIntegration () - (void)newSceneActivate; @end @@ -50,9 +48,20 @@ @implementation SentrySessionReplayIntegration { SentryOnDemandReplay *_resumeReplayMaker; } -+ (nullable SentrySessionReplayIntegration *)installed +- (instancetype)init { - return _installedInstance; + self = [super init]; + return self; +} + +- (instancetype)initForManualUse:(nonnull SentryOptions *)options +{ + if (self = [super init]) { + [self setupWith:options.experimental.sessionReplay + enableTouchTracker:options.enableSwizzling]; + [self startWithOptions:options.experimental.sessionReplay fullSession:YES]; + } + return self; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -61,14 +70,19 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return NO; } - _replayOptions = options.experimental.sessionReplay; - _viewPhotographer = - [[SentryViewPhotographer alloc] initWithRedactOptions:options.experimental.sessionReplay]; + [self setupWith:options.experimental.sessionReplay enableTouchTracker:options.enableSwizzling]; + return YES; +} - if (options.enableSwizzling) { +- (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)touchTracker +{ + _replayOptions = replayOptions; + _viewPhotographer = [[SentryViewPhotographer alloc] initWithRedactOptions:replayOptions]; + + if (touchTracker) { _touchTracker = [[SentryTouchTracker alloc] initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider - scale:options.experimental.sessionReplay.sizeScale]; + scale:replayOptions.sizeScale]; [self swizzleApplicationTouch]; } @@ -87,9 +101,6 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options }]; [SentryDependencyContainer.sharedInstance.reachability addObserver:self]; - - _installedInstance = self; - return YES; } /** @@ -421,10 +432,6 @@ - (void)uninstall [SentrySDK.currentHub unregisterSessionListener:self]; _touchTracker = nil; [self pause]; - - if (_installedInstance == self) { - _installedInstance = nil; - } } - (void)dealloc diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index f7fe65e76d0..1520e6d3302 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -10,10 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SentrySessionReplayIntegration : SentryBaseIntegration -/** - * The last instance of the installed integration - */ -@property (class, nonatomic, readonly, nullable) SentrySessionReplayIntegration *installed; +- (instancetype)initForManualUse:(nonnull SentryOptions *)options; /** * Captures Replay. Used by the Hybrid SDKs.