Skip to content

Commit

Permalink
Merge 40e71d9 into 0a23401
Browse files Browse the repository at this point in the history
  • Loading branch information
brustolin authored Oct 9, 2024
2 parents 0a23401 + 40e71d9 commit 814655e
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 37 deletions.
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)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) {
SentryOptions *currentOptions = SentrySDK.currentHub.client.options;
replayIntegration =
[[SentrySessionReplayIntegration alloc] initForManualUse:currentOptions];

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

[replayIntegration start];
}

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

@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 @@ @implementation SentrySessionReplayIntegration {
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];
}
return self;
}

- (BOOL)installWithOptions:(nonnull SentryOptions *)options
Expand All @@ -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;
}

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

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

Expand All @@ -87,9 +101,6 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
}];

[SentryDependencyContainer.sharedInstance.reachability addObserver:self];

_installedInstance = self;
return YES;
}

/**
Expand Down Expand Up @@ -212,23 +223,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
Expand Down Expand Up @@ -351,6 +369,25 @@ - (void)resume
[self.sessionReplay resume];
}

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

_startedAsFullSession = YES;
[self runReplayForAvailableWindow];
}

- (void)stop
{
[self.sessionReplay pause];
self.sessionReplay = nil;
}

- (void)sentrySessionEnded:(SentrySession *)session
{
[self pause];
Expand Down Expand Up @@ -395,10 +432,6 @@ - (void)uninstall
[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 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
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 @@ 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 testStartWithNoSessionReplay() throws {
startSDK(sessionSampleRate: 0, errorSampleRate: 0, 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 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"
Expand Down

0 comments on commit 814655e

Please sign in to comment.