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(Session Replay): Experimental Options #3816

Merged
merged 20 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options.debug = true

if #available(iOS 16.0, *) {
options.sessionReplayOptions = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true)
}

if #available(iOS 15.0, *) {
Expand Down
10 changes: 9 additions & 1 deletion Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,8 @@
D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; };
D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; };
D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; };
D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; };
D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; };
D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; };
D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */; };
D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */; };
Expand Down Expand Up @@ -1808,6 +1810,8 @@
D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = "<group>"; };
D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = "<group>"; };
D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = "<group>"; };
D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactOptions.swift; sourceTree = "<group>"; };
D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExperimentalOptions.swift; sourceTree = "<group>"; };
D880E3A628573E87008A90DB /* SentryBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageTests.swift; sourceTree = "<group>"; };
D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = "<group>"; };
D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3409,6 +3413,7 @@
D8F016B12B9622B7007B9AFB /* Protocol */,
D856272A2A374A6800FB8062 /* Tools */,
D800942628F82F3A005D3943 /* SwiftDescriptor.swift */,
D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */,
D8B665BB2B95F5A100BD0E7B /* module.modulemap */,
);
path = Swift;
Expand Down Expand Up @@ -3522,6 +3527,7 @@
children = (
D856272B2A374A8600FB8062 /* UrlSanitized.swift */,
D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */,
D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */,
);
path = Tools;
sourceTree = "<group>";
Expand Down Expand Up @@ -3599,7 +3605,6 @@
children = (
D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */,
D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */,
D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */,
D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */,
D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */,
);
Expand All @@ -3619,6 +3624,7 @@
children = (
D8F016B22B9622D6007B9AFB /* SentryId.swift */,
D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */,
D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */,
);
path = Protocol;
sourceTree = "<group>";
Expand Down Expand Up @@ -4213,6 +4219,7 @@
0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */,
7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */,
7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */,
D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */,
631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */,
7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */,
7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */,
Expand Down Expand Up @@ -4284,6 +4291,7 @@
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */,
62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */,
D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */,
7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */,
639FCFA11EBC804600778193 /* SentryException.m in Sources */,
D80CD8D42B75144B002F710B /* SwiftDescriptor.swift in Sources */,
Expand Down
15 changes: 7 additions & 8 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN

@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope,
SentryReplayOptions;
@class SentryExperimentalOptions;

NS_SWIFT_NAME(Options)
@interface SentryOptions : NSObject
Expand Down Expand Up @@ -271,14 +272,6 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing;

/**
* @warning This is an experimental feature and may still have bugs.
* Settings to configure the session replay.
* @node Default value is @c nil .
*/
@property (nonatomic, strong)
SentryReplayOptions *sessionReplayOptions API_AVAILABLE(ios(16.0), tvos(16.0));

#endif // SENTRY_UIKIT_AVAILABLE

/**
Expand Down Expand Up @@ -567,6 +560,12 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, copy) NSString *spotlightUrl;

/**
* This will agreggate options for all experimental features.
* Be aware that the options available for experimental can change at any time.
*/
@property (nonatomic, readonly) SentryExperimentalOptions *experimental;

@end

NS_ASSUME_NONNULL_END
4 changes: 2 additions & 2 deletions Sources/Sentry/SentryBaseIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options

if (integrationOptions & kIntegrationOptionEnableReplay) {
if (@available(iOS 16.0, tvOS 16.0, *)) {
if (options.sessionReplayOptions.replaysOnErrorSampleRate == 0
&& options.sessionReplayOptions.replaysSessionSampleRate == 0) {
if (options.experimental.sessionReplay.errorSampleRate == 0
&& options.experimental.sessionReplay.sessionSampleRate == 0) {
[self logWithOptionName:@"sessionReplaySettings"];
return NO;
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryEnvelope.m
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ - (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent
}

NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl];

NSError *error;
if (![NSFileManager.defaultManager removeItemAtURL:envelopeContentUrl error:&error]) {
SENTRY_LOG_ERROR(@"Cound not delete temporary replay content from disk: %@", error);
}
return [self initWithHeader:[[SentryEnvelopeItemHeader alloc]
initWithType:SentryEnvelopeItemTypeReplayVideo
length:envelopeItemContent.length]
Expand Down
16 changes: 7 additions & 9 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import "SentrySDK.h"
#import "SentryScope.h"
#import "SentrySessionReplayIntegration.h"
#import "SentrySwift.h"
#import "SentrySwiftAsyncIntegration.h"
#import <objc/runtime.h>

Expand Down Expand Up @@ -105,7 +106,7 @@ - (instancetype)init
self.enableTimeToFullDisplayTracing = NO;

self.initialScope = ^SentryScope *(SentryScope *scope) { return scope; };

_experimental = [[SentryExperimentalOptions alloc] init];
_enableTracing = NO;
_enableTracingManual = NO;
#if SENTRY_HAS_UIKIT
Expand Down Expand Up @@ -403,13 +404,6 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enablePreWarmedAppStartTracing"]
block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }];

if (@available(iOS 16.0, tvOS 16.0, *)) {
if ([options[@"sessionReplayOptions"] isKindOfClass:NSDictionary.class]) {
self.sessionReplayOptions =
[[SentryReplayOptions alloc] initWithDictionary:options[@"sessionReplayOptions"]];
}
}

#endif // SENTRY_HAS_UIKIT

[self setBool:options[@"enableAppHangTracking"]
Expand Down Expand Up @@ -505,6 +499,10 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
self.spotlightUrl = options[@"spotlightUrl"];
}

if ([options[@"experimental"] isKindOfClass:NSDictionary.class]) {
[self.experimental validateOptions:options[@"experimental"]];
}

return YES;
}

Expand Down Expand Up @@ -745,4 +743,4 @@ - (NSString *)debugDescription
}
#endif // defined(DEBUG) || defined(TEST) || defined(TESTCI)

@end
@end
49 changes: 38 additions & 11 deletions Sources/Sentry/SentrySessionReplay.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
@interface
SentrySessionReplay ()

@property (nonatomic) BOOL isRunning;

@property (nonatomic) BOOL isFullSession;

@end
Expand All @@ -37,8 +39,8 @@ @implementation SentrySessionReplay {
id<SentryRandom> _sentryRandom;
id<SentryViewScreenshotProvider> _screenshotProvider;
int _currentSegmentId;
BOOL _isRunning;
BOOL _processingScreenshot;
BOOL _reachedMaximumDuration;
}

- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions
Expand All @@ -55,9 +57,10 @@ - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions
_sentryRandom = random;
_screenshotProvider = screenshotProvider;
_displayLink = displayLinkWrapper;
_isRunning = false;
_isRunning = NO;
_urlToCache = folderPath;
_replayMaker = replayMaker;
_reachedMaximumDuration = NO;
}
return self;
}
Expand All @@ -72,22 +75,31 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full
if (_isRunning) {
return;
}

@synchronized(self) {
if (_isRunning) {
return;
}
[_displayLink linkWithTarget:self selector:@selector(newFrame:)];
_isRunning = true;
_isRunning = YES;
}

_rootView = rootView;
_lastScreenShot = _dateProvider.date;
_videoSegmentStart = nil;
_sessionStart = _lastScreenShot;
_currentSegmentId = 0;
sessionReplayId = [[SentryId alloc] init];

imageCollection = [NSMutableArray array];
_isFullSession = full;
if (full) {
[self startFullReplay];
}
}

- (void)startFullReplay
{
_sessionStart = _lastScreenShot;
_isFullSession = YES;
}

- (void)stop
Expand All @@ -108,7 +120,7 @@ - (void)captureReplayForEvent:(SentryEvent *)event;
return;
}

if ([_sentryRandom nextNumber] > _replayOptions.replaysOnErrorSampleRate) {
if ([_sentryRandom nextNumber] > _replayOptions.errorSampleRate) {
return;
}

Expand All @@ -120,13 +132,25 @@ - (void)captureReplayForEvent:(SentryEvent *)event;
duration:_replayOptions.errorReplayDuration
startedAt:replayStart];

self->_isFullSession = YES;
[self startFullReplay];
}

- (void)newFrame:(CADisplayLink *)sender
{
if (!_isRunning) {
return;
}

NSDate *now = _dateProvider.date;

if (_isFullSession &&
[now timeIntervalSinceDate:_sessionStart] > _replayOptions.maximumDuration) {
_reachedMaximumDuration = YES;
[self prepareSegmentUntil:now];
[self stop];
return;
}

if ([now timeIntervalSinceDate:_lastScreenShot] >= 1) {
[self takeScreenshot];
_lastScreenShot = now;
Expand All @@ -143,8 +167,6 @@ - (void)newFrame:(CADisplayLink *)sender

- (void)prepareSegmentUntil:(NSDate *)date
{
NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart];
NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart];
NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"];

if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) {
Expand All @@ -160,7 +182,7 @@ - (void)prepareSegmentUntil:(NSDate *)date
}

pathToSegment = [pathToSegment
URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]];
URLByAppendingPathComponent:[NSString stringWithFormat:@"%i.mp4", _currentSegmentId]];

NSDate *segmentStart =
[_dateProvider.date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration];
Expand Down Expand Up @@ -219,6 +241,11 @@ - (void)captureSegment:(NSInteger)segment
[SentrySDK.currentHub captureReplayEvent:replayEvent
replayRecording:recording
video:videoInfo.path];

NSError *error;
if (![NSFileManager.defaultManager removeItemAtURL:videoInfo.path error:&error]) {
SENTRY_LOG_ERROR(@"Cound not delete replay segment from disk: %@", error);
}
}

- (void)takeScreenshot
Expand All @@ -233,7 +260,7 @@ - (void)takeScreenshot
_processingScreenshot = YES;
}

UIImage *screenshot = [_screenshotProvider imageWithView:_rootView];
UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions];

_processingScreenshot = NO;

Expand Down
20 changes: 10 additions & 10 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
}

if (@available(iOS 16.0, tvOS 16.0, *)) {
SentryReplayOptions *replayOptions = options.experimental.sessionReplay;

BOOL shouldReplayFullSession =
[self shouldReplayFullSession:options.sessionReplayOptions.replaysSessionSampleRate];
[self shouldReplayFullSession:replayOptions.sessionSampleRate];

if (!shouldReplayFullSession
&& options.sessionReplayOptions.replaysOnErrorSampleRate == 0) {
if (!shouldReplayFullSession && replayOptions.errorSampleRate == 0) {
brustolin marked this conversation as resolved.
Show resolved Hide resolved
return NO;
}

Expand All @@ -67,13 +68,13 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options

SentryOnDemandReplay *replayMaker =
[[SentryOnDemandReplay alloc] initWithOutputPath:docs.path];
replayMaker.bitRate = options.sessionReplayOptions.replayBitRate;
replayMaker.cacheMaxSize = (NSInteger)(shouldReplayFullSession
? options.sessionReplayOptions.sessionSegmentDuration
: options.sessionReplayOptions.errorReplayDuration);
replayMaker.bitRate = replayOptions.replayBitRate;
replayMaker.cacheMaxSize
= (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration
: replayOptions.errorReplayDuration);

self.sessionReplay = [[SentrySessionReplay alloc]
initWithSettings:options.sessionReplayOptions
initWithSettings:replayOptions
replayFolderPath:docs
screenshotProvider:SentryViewPhotographer.shared
replayMaker:replayMaker
Expand All @@ -84,8 +85,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options

[self.sessionReplay
start:SentryDependencyContainer.sharedInstance.application.windows.firstObject
fullSession:[self shouldReplayFullSession:options.sessionReplayOptions
.replaysSessionSampleRate]];
fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]];

[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(stop)
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sentry/include/SentrySessionReplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@class SentryVideoInfo;

@protocol SentryRandom;
@protocol SentryRedactOptions;

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -28,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
@end

@protocol SentryViewScreenshotProvider <NSObject>
- (UIImage *)imageWithView:(UIView *)view;
- (UIImage *)imageWithView:(UIView *)view options:(id<SentryRedactOptions>)options;
@end

API_AVAILABLE(ios(16.0), tvos(16.0))
Expand Down
Loading
Loading