Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Start/Stop session replay #4414

Merged
merged 11 commits into from
Oct 11, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- feat: API to manually start/stop Session Replay (#4414)
- Custom redact modifier for SwiftUI (#4362, #4392)

### Removal of Experimental API
Expand Down
14 changes: 14 additions & 0 deletions Sources/Sentry/Public/SentryReplayApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -804,5 +804,4 @@ - (NSString *)debugDescription
return [NSString stringWithFormat:@"<%@: {\n%@\n}>", self, propertiesDescription];
}
#endif // defined(DEBUG) || defined(TEST) || defined(TESTCI)

@end
39 changes: 34 additions & 5 deletions Sources/Sentry/SentryReplayApi.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 <UIKit/UIKit.h>

Expand All @@ -22,18 +23,46 @@

- (void)pause
{
SentrySessionReplayIntegration *replayIntegration =
[SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class];
SentrySessionReplayIntegration *replayIntegration
= (SentrySessionReplayIntegration *)[SentrySDK.currentHub
getInstalledIntegration:SentrySessionReplayIntegration.class];

Check warning on line 28 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L26-L28

Added lines #L26 - L28 were not covered by tests
[replayIntegration pause];
}

- (void)resume
{
SentrySessionReplayIntegration *replayIntegration =
[SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class];
SentrySessionReplayIntegration *replayIntegration
= (SentrySessionReplayIntegration *)[SentrySDK.currentHub
getInstalledIntegration:SentrySessionReplayIntegration.class];

Check warning on line 36 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L34-L36

Added lines #L34 - L36 were not covered by tests
[replayIntegration resume];
}

- (void)start
brustolin marked this conversation as resolved.
Show resolved Hide resolved
{
SentrySessionReplayIntegration *replayIntegration
= (SentrySessionReplayIntegration *)[SentrySDK.currentHub
getInstalledIntegration:SentrySessionReplayIntegration.class];

Check warning on line 44 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L42-L44

Added lines #L42 - L44 were not covered by tests

if (replayIntegration == nil) {
brustolin marked this conversation as resolved.
Show resolved Hide resolved
SentryOptions *currentOptions = SentrySDK.currentHub.client.options;
replayIntegration =
[[SentrySessionReplayIntegration alloc] initForManualUse:currentOptions];

Check warning on line 49 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L47-L49

Added lines #L47 - L49 were not covered by tests

[SentrySDK.currentHub addInstalledIntegration:replayIntegration
name:NSStringFromClass(SentrySessionReplay.class)];

Check warning on line 52 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L51-L52

Added lines #L51 - L52 were not covered by tests
}

[replayIntegration start];

Check warning on line 55 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L55

Added line #L55 was not covered by tests
}

- (void)stop
{
SentrySessionReplayIntegration *replayIntegration
= (SentrySessionReplayIntegration *)[SentrySDK.currentHub
getInstalledIntegration:SentrySessionReplayIntegration.class];
[replayIntegration stop];

Check warning on line 63 in Sources/Sentry/SentryReplayApi.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryReplayApi.m#L60-L63

Added lines #L60 - L63 were not covered by tests
}

@end

#endif
81 changes: 57 additions & 24 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
*/
static SentryTouchTracker *_touchTracker;

static SentrySessionReplayIntegration *_installedInstance;

@interface SentrySessionReplayIntegration () <SentryReachabilityObserver>
- (void)newSceneActivate;
@end
Expand All @@ -50,9 +48,20 @@
SentryOnDemandReplay *_resumeReplayMaker;
}

+ (nullable SentrySessionReplayIntegration *)installed
- (instancetype)init
{
self = [super init];
return self;
}

- (instancetype)initForManualUse:(nonnull SentryOptions *)options
{
return _installedInstance;
if (self = [super init]) {
[self setupWith:options.experimental.sessionReplay
enableTouchTracker:options.enableSwizzling];
[self startWithOptions:options.experimental.sessionReplay fullSession:YES];

Check warning on line 62 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L60-L62

Added lines #L60 - L62 were not covered by tests
}
return self;

Check warning on line 64 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L64

Added line #L64 was not covered by tests
}

- (BOOL)installWithOptions:(nonnull SentryOptions *)options
Expand All @@ -61,14 +70,19 @@
return NO;
}

_replayOptions = options.experimental.sessionReplay;
_viewPhotographer =
[[SentryViewPhotographer alloc] initWithRedactOptions:options.experimental.sessionReplay];
[self setupWith:options.experimental.sessionReplay enableTouchTracker:options.enableSwizzling];
return YES;
}

- (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)touchTracker
{
_replayOptions = replayOptions;
_viewPhotographer = [[SentryViewPhotographer alloc] initWithRedactOptions:replayOptions];

Check warning on line 80 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L79-L80

Added lines #L79 - L80 were not covered by tests

if (options.enableSwizzling) {
if (touchTracker) {
_touchTracker = [[SentryTouchTracker alloc]
initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider
scale:options.experimental.sessionReplay.sizeScale];
scale:replayOptions.sizeScale];

Check warning on line 85 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L85

Added line #L85 was not covered by tests
[self swizzleApplicationTouch];
}

Expand All @@ -87,9 +101,6 @@
}];

[SentryDependencyContainer.sharedInstance.reachability addObserver:self];

_installedInstance = self;
return YES;
}

/**
Expand Down Expand Up @@ -212,23 +223,30 @@
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];

Check warning on line 238 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L236-L238

Added lines #L236 - L238 were not covered by tests
}
}

- (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];

Check warning on line 248 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L245-L248

Added lines #L245 - L248 were not covered by tests
}
}

- (void)startWithOptions:(SentryReplayOptions *)replayOptions
Expand Down Expand Up @@ -351,6 +369,25 @@
[self.sessionReplay resume];
}

- (void)start
{
if (self.sessionReplay != nil) {
if (self.sessionReplay.isFullSession == NO) {
[self.sessionReplay captureReplay];

Check warning on line 376 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L376

Added line #L376 was not covered by tests
}
return;

Check warning on line 378 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L378

Added line #L378 was not covered by tests
}

_startedAsFullSession = YES;
[self runReplayForAvailableWindow];

Check warning on line 382 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L382

Added line #L382 was not covered by tests
}

- (void)stop
{
[self.sessionReplay pause];
brustolin marked this conversation as resolved.
Show resolved Hide resolved
self.sessionReplay = nil;

Check warning on line 388 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L387-L388

Added lines #L387 - L388 were not covered by tests
}

- (void)sentrySessionEnded:(SentrySession *)session
{
[self pause];
Expand Down Expand Up @@ -395,10 +432,6 @@
[SentrySDK.currentHub unregisterSessionListener:self];
_touchTracker = nil;
[self pause];

if (_installedInstance == self) {
_installedInstance = nil;
}
}

- (void)dealloc
Expand Down
9 changes: 5 additions & 4 deletions Sources/Sentry/include/SentrySessionReplayIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -32,6 +29,10 @@ NS_ASSUME_NONNULL_BEGIN

- (void)resume;

- (void)stop;

- (void)start;

@end
#endif // SENTRY_TARGET_REPLAY_SUPPORTED
NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class SentrySessionReplay: NSObject {
videoSegmentStart = nil
displayLink.link(withTarget: self, selector: #selector(newFrame(_:)))
}

func captureReplayFor(event: Event) {
guard isRunning else { return }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
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)
Expand Down Expand Up @@ -309,6 +309,55 @@
XCTAssertTrue(redactBuilder.containsIgnoreClass(AnotherLabel.self))
}

func testStop() throws {
startSDK(sessionSampleRate: 1, errorSampleRate: 1)
let sut = try getSut()
let sessionReplay = sut.sessionReplay

Check warning on line 315 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L313-L315

Added lines #L313 - L315 were not covered by tests
XCTAssertTrue(sessionReplay?.isRunning ?? false)

SentrySDK.replay.stop()

Check warning on line 318 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L318

Added line #L318 was not covered by tests

XCTAssertFalse(sessionReplay?.isRunning ?? true)
XCTAssertNil(sut.sessionReplay)
}

func testStartWithNoSessionReplay() throws {
startSDK(sessionSampleRate: 0, errorSampleRate: 0, noIntegrations: true)
var sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration

Check warning on line 326 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L325-L326

Added lines #L325 - L326 were not covered by tests
XCTAssertNil(sut)
SentrySDK.replay.start()
sut = try getSut()

Check warning on line 329 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L328-L329

Added lines #L328 - L329 were not covered by tests

let sessionReplay = sut?.sessionReplay

Check warning on line 331 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L331

Added line #L331 was not covered by tests
XCTAssertTrue(sessionReplay?.isRunning ?? false)
XCTAssertTrue(sessionReplay?.isFullSession ?? false)
XCTAssertNotNil(sut?.sessionReplay)
}

func testStartWithSessionReplayRunning() throws {
startSDK(sessionSampleRate: 1, errorSampleRate: 1)
let sut = try getSut()

Check warning on line 339 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L338-L339

Added lines #L338 - L339 were not covered by tests
let sessionReplay = try XCTUnwrap(sut.sessionReplay)
let replayId = sessionReplay.sessionReplayId

Check warning on line 341 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L341

Added line #L341 was not covered by tests

SentrySDK.replay.start()

Check warning on line 343 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L343

Added line #L343 was not covered by tests

//Test whether the integration keeps the same instance of the session replay

Check warning on line 345 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L345

Added line #L345 was not covered by tests
XCTAssertEqual(sessionReplay, sut.sessionReplay)
//Test whether the session Id is still the same

Check warning on line 347 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L347

Added line #L347 was not covered by tests
XCTAssertEqual(sessionReplay.sessionReplayId, replayId)
}

func testStartWithBufferSessionReplay() throws {
startSDK(sessionSampleRate: 0, errorSampleRate: 1)
let sut = try getSut()

Check warning on line 353 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L352-L353

Added lines #L352 - L353 were not covered by tests
let sessionReplay = try XCTUnwrap(sut.sessionReplay)

XCTAssertFalse(sessionReplay.isFullSession)
SentrySDK.replay.start()

Check warning on line 357 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L357

Added line #L357 was not covered by tests
XCTAssertTrue(sessionReplay.isFullSession)
}

func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
let options = Options()
options.dsn = "https://user@test.com/test"
Expand Down
Loading