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: Restart replay session with mobile session #4085

Merged
merged 14 commits into from
Jun 20, 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

- 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
Expand Down
5 changes: 4 additions & 1 deletion Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1972,6 +1973,7 @@
D8C66A352A77B1F70015696A /* SentryPropagationContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPropagationContext.m; sourceTree = "<group>"; };
D8C67E9928000E23007E326E /* SentryUIApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryUIApplication.h; path = include/SentryUIApplication.h; sourceTree = "<group>"; };
D8C67E9A28000E23007E326E /* SentryScreenshot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshot.h; path = include/SentryScreenshot.h; sourceTree = "<group>"; };
D8CA12942C203E71005894F4 /* SentrySessionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionListener.swift; sourceTree = "<group>"; };
D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = "<group>"; };
D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = "<group>"; };
D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3601,7 +3603,6 @@
D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */,
D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */,
D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */,
D82DD1CC2BEEB1A0001AB556 /* SentryBreadcrumbReplayConverterTests.swift */,
D8DBE0C92C0E093000FAB1FD /* SentryTouchTrackerTests.swift */,
D8DBE0D12C0EFFC300FAB1FD /* SentryReplayOptionsTests.swift */,
);
Expand Down Expand Up @@ -3896,6 +3897,7 @@
D8F016B22B9622D6007B9AFB /* SentryId.swift */,
D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */,
D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */,
D8CA12942C203E71005894F4 /* SentrySessionListener.swift */,
);
path = Protocol;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
2 changes: 1 addition & 1 deletion SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions SentryTestUtils/TestTransportAdapter.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _SentryPrivate
import Foundation

public class TestTransportAdapter: SentryTransportAdapter {
Expand Down
18 changes: 18 additions & 0 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@property (nonatomic, strong) SentryCrashWrapper *crashWrapper;
@property (nonatomic, strong) NSMutableSet<NSString *> *installedIntegrationNames;
@property (nonatomic) NSUInteger errorsBeforeSession;
@property (nonatomic, weak) id<SentrySessionListener> sessionListener;

@end

Expand Down Expand Up @@ -107,6 +108,7 @@ - (void)startSession
andLevel:kSentryLevelError];
return;
}

@synchronized(_sessionLock) {
if (_session != nil) {
lastSession = _session;
Expand All @@ -133,6 +135,8 @@ - (void)startSession
[lastSession
endSessionExitedWithTimestamp:[SentryDependencyContainer.sharedInstance.dateProvider date]];
[self captureSession:lastSession];

[_sessionListener sentrySessionStarted:_session];
}

- (void)endSession
Expand All @@ -156,6 +160,8 @@ - (void)endSessionWithTimestamp:(NSDate *)timestamp
}
[currentSession endSessionExitedWithTimestamp:timestamp];
[self captureSession:currentSession];

[_sessionListener sentrySessionEnded:currentSession];
}

- (void)storeCurrentSession:(SentrySession *)session
Expand Down Expand Up @@ -767,6 +773,18 @@ - (LocalMetricsAggregator *_Nullable)getLocalMetricsAggregatorWithSpan:(id<Sentr
return nil;
}

- (void)registerSessionListener:(id<SentrySessionListener>)listener
{
_sessionListener = listener;
}

- (void)unregisterSessionListener:(id<SentrySessionListener>)listener
{
if (_sessionListener == listener) {
_sessionListener = nil;
}
}

#pragma mark - Protected

- (NSMutableArray<NSString *> *)trimmedInstalledIntegrationNames
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/SentrySessionReplay.m
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ - (void)startFullReplay
- (void)stop
{
@synchronized(self) {
if (_isRunning == NO) {
return;
}
[_displayLink invalidate];
_isRunning = NO;
[self prepareSegmentUntil:_dateProvider.date];
Expand Down
178 changes: 103 additions & 75 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
@implementation SentrySessionReplayIntegration {
BOOL _startedAsFullSession;
SentryReplayOptions *_replayOptions;
SentryNSNotificationCenterWrapper *_notificationCenter;
}

- (BOOL)installWithOptions:(nonnull SentryOptions *)options
Expand All @@ -45,36 +46,46 @@
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];
}
}
}

Expand All @@ -98,58 +109,49 @@
breadcrumbConverter:(id<SentryReplayBreadcrumbConverter>)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
Expand All @@ -162,6 +164,26 @@
[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;

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

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L182

Added line #L182 was not covered by tests
}
[self startSession];
}

- (void)captureReplay
{
[self.sessionReplay captureReplay];
Expand All @@ -186,10 +208,16 @@

- (void)uninstall
{
[SentrySDK.currentHub unregisterSessionListener:self];
brustolin marked this conversation as resolved.
Show resolved Hide resolved
_touchTracker = nil;
[self stop];
}

- (void)dealloc
{
[self uninstall];
}

- (BOOL)shouldReplayFullSession:(CGFloat)rate
{
return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate;
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/include/SentryHub+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@class SentryReplayEvent;
@class SentryReplayRecording;
@protocol SentryIntegrationProtocol;
@protocol SentrySessionListener;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -62,6 +63,8 @@ SentryHub ()

- (void)captureEnvelope:(SentryEnvelope *)envelope;

- (void)registerSessionListener:(id<SentrySessionListener>)listener;
- (void)unregisterSessionListener:(id<SentrySessionListener>)listener;
- (nullable id<SentryIntegrationProtocol>)getInstalledIntegration:(Class)integrationClass;

@end
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
#import "SentryDateUtil.h"
#import "SentryLevelHelper.h"
#import "SentrySdkInfo.h"
#import "SentrySession.h"
3 changes: 1 addition & 2 deletions Sources/Sentry/include/SentrySession.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#import "SentryDefines.h"
#import "SentrySerializable.h"

@class SentryUser;

Expand All @@ -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 <SentrySerializable, NSCopying>
@interface SentrySession : NSObject <NSCopying>
SENTRY_NO_INIT

- (instancetype)initWithReleaseName:(NSString *)releaseName distinctId:(NSString *)distinctId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@class SentrySessionReplay;

@interface
SentrySessionReplayIntegration () <SentryIntegrationProtocol>
SentrySessionReplayIntegration () <SentryIntegrationProtocol, SentrySessionListener>

@property (nonatomic, strong) SentrySessionReplay *sessionReplay;

Expand Down
Loading
Loading