diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 7e95d580ea5..70f59c1b107 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -3,7 +3,8 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, SentryReplaySettings; +@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, + SentryReplaySettings; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -269,13 +270,13 @@ 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) SentryReplaySettings * sessionReplaySettings API_AVAILABLE(ios(16.0), tvos(16.0)); +@property (nonatomic, strong) + SentryReplaySettings *sessionReplaySettings API_AVAILABLE(ios(16.0), tvos(16.0)); #endif // SENTRY_UIKIT_AVAILABLE diff --git a/Sources/Sentry/Public/SentryReplaySettings.h b/Sources/Sentry/Public/SentryReplaySettings.h index 1c66a0f2712..16455041d7e 100644 --- a/Sources/Sentry/Public/SentryReplaySettings.h +++ b/Sources/Sentry/Public/SentryReplaySettings.h @@ -24,11 +24,14 @@ NS_ASSUME_NONNULL_BEGIN /** * Inittialize the settings of session replay - * - * @param sessionSampleRate Indicates the percentage in which the replay for the session will be created. - * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with error events. + * + * @param sessionSampleRate Indicates the percentage in which the replay for the session will be + * created. + * @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with + * error events. */ -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate; +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate + replaysOnErrorSampleRate:(CGFloat)errorSampleRate; @end diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index 8bb3107908b..cdd47fb0e0f 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,10 +1,10 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" +#import "SentryReplaySettings.h" #import #import #import -#import "SentryReplaySettings.h" NS_ASSUME_NONNULL_BEGIN @@ -33,119 +33,119 @@ - (void)logWithReason:(NSString *)reason - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options { SentryIntegrationOption integrationOptions = [self integrationOptions]; - + if (integrationOptions & kIntegrationOptionNone) { return YES; } - + if ((integrationOptions & kIntegrationOptionEnableAutoSessionTracking) && !options.enableAutoSessionTracking) { [self logWithOptionName:@"enableAutoSessionTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableWatchdogTerminationTracking) && !options.enableWatchdogTerminationTracking) { [self logWithOptionName:@"enableWatchdogTerminationTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoPerformanceTracing) && !options.enableAutoPerformanceTracing) { [self logWithOptionName:@"enableAutoPerformanceTracing"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionEnableUIViewControllerTracing) && !options.enableUIViewControllerTracing) { [self logWithOptionName:@"enableUIViewControllerTracing"]; return NO; } - + # if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachScreenshot) && !options.attachScreenshot) { [self logWithOptionName:@"attachScreenshot"]; return NO; } # endif // SENTRY_HAS_UIKIT - + if ((integrationOptions & kIntegrationOptionEnableUserInteractionTracing) && !options.enableUserInteractionTracing) { [self logWithOptionName:@"enableUserInteractionTracing"]; return NO; } #endif - + if (integrationOptions & kIntegrationOptionEnableAppHangTracking) { if (!options.enableAppHangTracking) { [self logWithOptionName:@"enableAppHangTracking"]; return NO; } - + if (options.appHangTimeoutInterval == 0) { [self logWithReason:@"because appHangTimeoutInterval is 0"]; return NO; } } - + if ((integrationOptions & kIntegrationOptionEnableNetworkTracking) && !options.enableNetworkTracking) { [self logWithOptionName:@"enableNetworkTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableFileIOTracing) && !options.enableFileIOTracing) { [self logWithOptionName:@"enableFileIOTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableNetworkBreadcrumbs) && !options.enableNetworkBreadcrumbs) { [self logWithOptionName:@"enableNetworkBreadcrumbs"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableCoreDataTracing) && !options.enableCoreDataTracing) { [self logWithOptionName:@"enableCoreDataTracing"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableSwizzling) && !options.enableSwizzling) { [self logWithOptionName:@"enableSwizzling"]; return NO; } - + if ((integrationOptions & kIntegrationOptionEnableAutoBreadcrumbTracking) && !options.enableAutoBreadcrumbTracking) { [self logWithOptionName:@"enableAutoBreadcrumbTracking"]; return NO; } - + if ((integrationOptions & kIntegrationOptionIsTracingEnabled) && !options.isTracingEnabled) { [self logWithOptionName:@"isTracingEnabled"]; return NO; } - + if ((integrationOptions & kIntegrationOptionDebuggerNotAttached) && [SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced]) { [self logWithReason:@"because the debugger is attached"]; return NO; } - + #if SENTRY_HAS_UIKIT if ((integrationOptions & kIntegrationOptionAttachViewHierarchy) && !options.attachViewHierarchy) { [self logWithOptionName:@"attachViewHierarchy"]; return NO; } - + if (integrationOptions & kIntegrationOptionEnableReplay) { if (@available(iOS 16.0, *)) { if (options.sessionReplaySettings.replaysOnErrorSampleRate == 0 - && options.sessionReplaySettings.replaysSessionSampleRate == 0 ){ + && options.sessionReplaySettings.replaysSessionSampleRate == 0) { [self logWithOptionName:@"sessionReplaySettings"]; return NO; } diff --git a/Sources/Sentry/SentryOnDemandReplay.m b/Sources/Sentry/SentryOnDemandReplay.m index 36c55b93154..e6302a5b4dc 100644 --- a/Sources/Sentry/SentryOnDemandReplay.m +++ b/Sources/Sentry/SentryOnDemandReplay.m @@ -1,20 +1,21 @@ #import "SentryOnDemandReplay.h" #if SENTRY_HAS_UIKIT -#import -#import -#import "SentryLog.h" +# import "SentryLog.h" +# import +# import @interface SentryReplayFrame : NSObject @property (nonatomic, strong) NSString *imagePath; @property (nonatomic, strong) NSDate *time; --(instancetype) initWithPath:(NSString *)path time:(NSDate*)time; +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time; @end @implementation SentryReplayFrame --(instancetype) initWithPath:(NSString *)path time:(NSDate*)time { +- (instancetype)initWithPath:(NSString *)path time:(NSDate *)time +{ if (self = [super init]) { self.imagePath = path; self.time = time; @@ -24,16 +25,16 @@ -(instancetype) initWithPath:(NSString *)path time:(NSDate*)time { @end -@implementation SentryOnDemandReplay -{ - NSString * _outputPath; - NSDate * _startTime; - NSMutableArray * _frames; +@implementation SentryOnDemandReplay { + NSString *_outputPath; + NSDate *_startTime; + NSMutableArray *_frames; CGSize _videoSize; dispatch_queue_t _onDemandDispatchQueue; } -- (instancetype)initWithOutputPath:(NSString *)outputPath { +- (instancetype)initWithOutputPath:(NSString *)outputPath +{ if (self = [super init]) { _outputPath = outputPath; _startTime = [[NSDate alloc] init]; @@ -43,30 +44,32 @@ - (instancetype)initWithOutputPath:(NSString *)outputPath { _bitRate = 20000; _cacheMaxSize = NSUIntegerMax; _onDemandDispatchQueue = dispatch_queue_create("io.sentry.sessionreplay.ondemand", NULL); - } return self; } -- (void)addFrame:(UIImage *)image { +- (void)addFrame:(UIImage *)image +{ dispatch_async(_onDemandDispatchQueue, ^{ - NSData * data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); - NSDate* date = [[NSDate alloc] init]; + NSData *data = UIImagePNGRepresentation([self resizeImage:image withMaxWidth:300]); + NSDate *date = [[NSDate alloc] init]; NSTimeInterval interval = [date timeIntervalSinceDate:self->_startTime]; - NSString *imagePath = [self->_outputPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; - + NSString *imagePath = [self->_outputPath + stringByAppendingPathComponent:[NSString stringWithFormat:@"%lf.png", interval]]; + [data writeToFile:imagePath atomically:YES]; - + SentryReplayFrame *frame = [[SentryReplayFrame alloc] initWithPath:imagePath time:date]; [self->_frames addObject:frame]; - + while (self->_frames.count > self->_cacheMaxSize) { [self removeOldestFrame]; } }); } -- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth { +- (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth +{ CGSize originalSize = originalImage.size; CGFloat aspectRatio = originalSize.width / originalSize.height; @@ -83,124 +86,149 @@ - (UIImage *)resizeImage:(UIImage *)originalImage withMaxWidth:(CGFloat)maxWidth return resizedImage; } -- (void)releaseFramesUntil:(NSDate *)date { +- (void)releaseFramesUntil:(NSDate *)date +{ dispatch_async(_onDemandDispatchQueue, ^{ - while (self->_frames.count > 0 && [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { + while (self->_frames.count > 0 && + [self->_frames.firstObject.time compare:date] != NSOrderedDescending) { [self removeOldestFrame]; } }); } -- (void)removeOldestFrame { - NSError * error; - if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath error:&error]){ - SENTRY_LOG_DEBUG(@"Could not delete replay frame at: %@. %@",_frames.firstObject.imagePath, error); +- (void)removeOldestFrame +{ + NSError *error; + if (![NSFileManager.defaultManager removeItemAtPath:_frames.firstObject.imagePath + error:&error]) { + SENTRY_LOG_DEBUG( + @"Could not delete replay frame at: %@. %@", _frames.firstObject.imagePath, error); } [_frames removeObjectAtIndex:0]; } -- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL - completion:(void (^)(BOOL success, NSError *error))completion { + completion:(void (^)(BOOL success, NSError *error))completion +{ // Set up AVAssetWriter with appropriate settings AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:outputFileURL fileType:AVFileTypeQuickTimeMovie error:nil]; - + NSDictionary *videoSettings = @{ - AVVideoCodecKey: AVVideoCodecTypeH264, - AVVideoWidthKey: @(_videoSize.width), - AVVideoHeightKey: @(_videoSize.height), - AVVideoCompressionPropertiesKey: @{ - AVVideoAverageBitRateKey: @(_bitRate), - AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel, + AVVideoCodecKey : AVVideoCodecTypeH264, + AVVideoWidthKey : @(_videoSize.width), + AVVideoHeightKey : @(_videoSize.height), + AVVideoCompressionPropertiesKey : @ { + AVVideoAverageBitRateKey : @(_bitRate), + AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel, }, }; - - AVAssetWriterInput *videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; + + AVAssetWriterInput *videoWriterInput = + [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo + outputSettings:videoSettings]; NSDictionary *bufferAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB), + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32ARGB), }; - - AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:bufferAttributes]; - + + AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput + sourcePixelBufferAttributes:bufferAttributes]; + [videoWriter addInput:videoWriterInput]; - + // Start writing video [videoWriter startWriting]; [videoWriter startSessionAtSourceTime:kCMTimeZero]; - - NSDate* end = [beginning dateByAddingTimeInterval:duration]; + + NSDate *end = [beginning dateByAddingTimeInterval:duration]; __block NSInteger frameCount = 0; - NSMutableArray * frames = [NSMutableArray array]; + NSMutableArray *frames = [NSMutableArray array]; for (SentryReplayFrame *frame in self->_frames) { - if ([frame.time compare:beginning] == NSOrderedAscending) { - continue;; + if ([frame.time compare:beginning] == NSOrderedAscending) { + continue; + ; } else if ([frame.time compare:end] == NSOrderedDescending) { break; } [frames addObject:frame.imagePath]; } - - [videoWriterInput requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue usingBlock:^{ - UIImage *image = [UIImage imageWithContentsOfFile:frames[frameCount]]; - if (image) { - CMTime presentTime = CMTimeMake(frameCount++, 1); - - if (![self appendPixelBufferForImage:image pixelBufferAdaptor:pixelBufferAdaptor presentationTime:presentTime]) { - if (completion) { - completion(NO, videoWriter.error); - } - } - } - - if (frameCount >= frames.count){ - [videoWriterInput markAsFinished]; - [videoWriter finishWritingWithCompletionHandler:^{ - if (completion) { - completion(videoWriter.status == AVAssetWriterStatusCompleted, videoWriter.error); - } - }]; - } - }]; + + [videoWriterInput + requestMediaDataWhenReadyOnQueue:_onDemandDispatchQueue + usingBlock:^{ + UIImage *image = + [UIImage imageWithContentsOfFile:frames[frameCount]]; + if (image) { + CMTime presentTime = CMTimeMake(frameCount++, 1); + + if (![self appendPixelBufferForImage:image + pixelBufferAdaptor:pixelBufferAdaptor + presentationTime:presentTime]) { + if (completion) { + completion(NO, videoWriter.error); + } + } + } + + if (frameCount >= frames.count) { + [videoWriterInput markAsFinished]; + [videoWriter finishWritingWithCompletionHandler:^{ + if (completion) { + completion(videoWriter.status + == AVAssetWriterStatusCompleted, + videoWriter.error); + } + }]; + } + }]; } -- (BOOL)appendPixelBufferForImage:(UIImage *)image pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor presentationTime:(CMTime)presentationTime { +- (BOOL)appendPixelBufferForImage:(UIImage *)image + pixelBufferAdaptor:(AVAssetWriterInputPixelBufferAdaptor *)pixelBufferAdaptor + presentationTime:(CMTime)presentationTime +{ CVReturn status = kCVReturnSuccess; - + CVPixelBufferRef pixelBuffer = NULL; - status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); - + status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)image.size.width, + (size_t)image.size.height, kCVPixelFormatType_32ARGB, NULL, &pixelBuffer); + if (status != kCVReturnSuccess) { return NO; } - + CVPixelBufferLockBaseAddress(pixelBuffer, 0); void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer); - + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); - + CGContextRef context = CGBitmapContextCreate(pixelData, (size_t)image.size.width, + (size_t)image.size.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, + (CGBitmapInfo)kCGImageAlphaNoneSkipFirst); + CGContextTranslateCTM(context, 0, image.size.height); CGContextScaleCTM(context, 1.0, -1.0); - + UIGraphicsPushContext(context); [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; UIGraphicsPopContext(); - + CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); - + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - + // Append the pixel buffer with the current image to the video - BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]; - + BOOL success = [pixelBufferAdaptor appendPixelBuffer:pixelBuffer + withPresentationTime:presentationTime]; + CVPixelBufferRelease(pixelBuffer); - + return success; } - @end -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 65fc53a7a16..556593d3330 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -23,12 +23,12 @@ # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" +# import "SentryReplaySettings+Private.h" # import "SentryScreenshotIntegration.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" -# import "SentryReplaySettings+Private.h" -# import "SentrySessionReplayIntegration.h" #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @@ -265,7 +265,6 @@ - (void)setDsn:(NSString *)dsn } } - /** * Populates all @c SentryOptions values from @c options dict using fallbacks/defaults if needed. */ @@ -412,13 +411,14 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; - + if (@available(iOS 16.0, *)) { if ([options[@"sessionReplaySettings"] isKindOfClass:NSDictionary.class]) { - self.sessionReplaySettings = [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; + self.sessionReplaySettings = + [[SentryReplaySettings alloc] initWithDictionary:options[@"sessionReplaySettings"]]; } } - + #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] diff --git a/Sources/Sentry/SentryReplaySettings.m b/Sources/Sentry/SentryReplaySettings.m index b2a67f474fd..82a2113fad4 100644 --- a/Sources/Sentry/SentryReplaySettings.m +++ b/Sources/Sentry/SentryReplaySettings.m @@ -1,7 +1,7 @@ #import "SentryReplaySettings.h" - -@interface SentryReplaySettings () +@interface +SentryReplaySettings () @property (nonatomic) NSInteger replayBitRate; @@ -9,7 +9,8 @@ @interface SentryReplaySettings () @implementation SentryReplaySettings --(instancetype)init { +- (instancetype)init +{ if (self = [super init]) { self.replaysSessionSampleRate = 0; self.replaysOnErrorSampleRate = 0; @@ -18,21 +19,24 @@ -(instancetype)init { return self; } -- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate replaysOnErrorSampleRate:(CGFloat)errorSampleRate { +- (instancetype)initWithReplaySessionSampleRate:(CGFloat)sessionSampleRate + replaysOnErrorSampleRate:(CGFloat)errorSampleRate +{ if (self = [self init]) { self.replaysSessionSampleRate = sessionSampleRate; self.replaysOnErrorSampleRate = errorSampleRate; } - + return self; } -- (instancetype)initWithDictionary:(NSDictionary*)dictionary { +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ if (self = [self init]) { if ([dictionary[@"replaysSessionSampleRate"] isKindOfClass:NSNumber.class]) { self.replaysSessionSampleRate = [dictionary[@"replaysSessionSampleRate"] floatValue]; } - + if ([dictionary[@"replaysOnErrorSampleRate"] isKindOfClass:NSNumber.class]) { self.replaysOnErrorSampleRate = [dictionary[@"replaysOnErrorSampleRate"] floatValue]; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index 1936890cc2d..4b7aa7da009 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -1,71 +1,78 @@ #import "SentrySessionReplay.h" -#import "SentryViewPhotographer.h" -#import "SentryOndemandReplay.h" #import "SentryAttachment+Private.h" #import "SentryLog.h" +#import "SentryOndemandReplay.h" #import "SentryReplaySettings+Private.h" +#import "SentryViewPhotographer.h" @implementation SentrySessionReplay { - UIView * _rootView; + UIView *_rootView; BOOL _processingScreenshot; - CADisplayLink * _displayLink; - NSDate * _lastScreenShot; - NSDate * _videoSegmentStart; - NSURL * _urlToCache; - NSDate * _sessionStart; - SentryReplaySettings * _settings; - SentryOnDemandReplay * _replayMaker; - - NSMutableArray* imageCollection; + CADisplayLink *_displayLink; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSURL *_urlToCache; + NSDate *_sessionStart; + SentryReplaySettings *_settings; + SentryOnDemandReplay *_replayMaker; + + NSMutableArray *imageCollection; } -- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings { +- (instancetype)initWithSettings:(SentryReplaySettings *)replaySettings +{ if (self = [super init]) { _settings = replaySettings; } return self; } -- (void)start:(UIView *)rootView fullSession:(BOOL)full { +- (void)start:(UIView *)rootView fullSession:(BOOL)full +{ if (rootView == nil) { SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); return; } - - @synchronized (self) { + + @synchronized(self) { if (_displayLink == nil) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(newFrame:)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } else { - //Session display is already running. + // Session display is already running. return; } - + _rootView = rootView; _lastScreenShot = [[NSDate alloc] init]; _videoSegmentStart = nil; _sessionStart = _lastScreenShot; - - NSURL * docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject URLByAppendingPathComponent:@"io.sentry"]; - - NSString * currentSession = [NSUUID UUID].UUIDString; + + NSURL *docs = [[NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory + inDomains:NSUserDomainMask] + .firstObject URLByAppendingPathComponent:@"io.sentry"]; + + NSString *currentSession = [NSUUID UUID].UUIDString; _urlToCache = [docs URLByAppendingPathComponent:currentSession]; - + if (![NSFileManager.defaultManager fileExistsAtPath:_urlToCache.path]) { - [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache withIntermediateDirectories:YES attributes:nil error:nil]; + [NSFileManager.defaultManager createDirectoryAtURL:_urlToCache + withIntermediateDirectories:YES + attributes:nil + error:nil]; } - - _replayMaker = - [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; + + _replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:_urlToCache.path]; _replayMaker.bitRate = _settings.replayBitRate; _replayMaker.cacheMaxSize = full ? NSUIntegerMax : 32; imageCollection = [NSMutableArray array]; - - NSLog(@"Recording session to %@",_urlToCache); + + NSLog(@"Recording session to %@", _urlToCache); } } -- (void)stop { +- (void)stop +{ [_displayLink invalidate]; _displayLink = nil; } @@ -76,99 +83,108 @@ - (void)stop { if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { return attachments; } - + NSLog(@"Recording session event id %@", event.eventId); NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; - - NSURL * finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; - + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + dispatch_group_t _wait_for_render = dispatch_group_create(); - + dispatch_group_enter(_wait_for_render); [_replayMaker createVideoOf:30 - from:[NSDate dateWithTimeIntervalSinceNow:-30] - outputFileURL:finalPath - completion:^(BOOL success, NSError * _Nonnull error) { - dispatch_group_leave(_wait_for_render); - }]; + from:[NSDate dateWithTimeIntervalSinceNow:-30] + outputFileURL:finalPath + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + }]; dispatch_group_wait(_wait_for_render, DISPATCH_TIME_FOREVER); - - SentryAttachment *attachment = - [[SentryAttachment alloc] initWithPath:finalPath.path - filename:@"replay.mp4" - contentType:@"video/mp4"]; + + SentryAttachment *attachment = [[SentryAttachment alloc] initWithPath:finalPath.path + filename:@"replay.mp4" + contentType:@"video/mp4"]; [result addObject:attachment]; - + return result; } -- (void)sendReplayForEvent:(SentryEvent *)event { - +- (void)sendReplayForEvent:(SentryEvent *)event +{ } -- (void)newFrame:(CADisplayLink *)sender { - NSDate * now = [[NSDate alloc] init]; - +- (void)newFrame:(CADisplayLink *)sender +{ + NSDate *now = [[NSDate alloc] init]; + if ([now timeIntervalSinceDate:_lastScreenShot] > 1) { [self takeScreenshot]; _lastScreenShot = now; - + if (_videoSegmentStart == nil) { _videoSegmentStart = now; } else if ([now timeIntervalSinceDate:_videoSegmentStart] >= 5) { - [self prepareSegmentUntil: now]; + [self prepareSegmentUntil:now]; } } } -- (void)prepareSegmentUntil:(NSDate *)date { +- (void)prepareSegmentUntil:(NSDate *)date +{ NSTimeInterval from = [_videoSegmentStart timeIntervalSinceDate:_sessionStart]; NSTimeInterval to = [date timeIntervalSinceDate:_sessionStart]; - NSURL * pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; - + NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { - NSError * error; - if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path withIntermediateDirectories:YES attributes:nil error:&error]){ - SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", error.localizedDescription); + NSError *error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", + error.localizedDescription); return; } } - - pathToSegment = [pathToSegment URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4",from, to]]; - + + pathToSegment = [pathToSegment + URLByAppendingPathComponent:[NSString stringWithFormat:@"%f-%f.mp4", from, to]]; + dispatch_group_t _wait_for_render = dispatch_group_create(); - + dispatch_group_enter(_wait_for_render); [_replayMaker createVideoOf:5 - from:[date dateByAddingTimeInterval:-5] - outputFileURL:pathToSegment - completion:^(BOOL success, NSError * _Nonnull error) { - dispatch_group_leave(_wait_for_render); - - //Need to send the segment here - - [self->_replayMaker releaseFramesUntil:date]; - self->_videoSegmentStart = nil; - }]; - + from:[date dateByAddingTimeInterval:-5] + outputFileURL:pathToSegment + completion:^(BOOL success, NSError *_Nonnull error) { + dispatch_group_leave(_wait_for_render); + + // Need to send the segment here + + [self->_replayMaker releaseFramesUntil:date]; + self->_videoSegmentStart = nil; + }]; } -- (void)takeScreenshot { - if (_processingScreenshot) { return; } - @synchronized (self) { - if (_processingScreenshot) { return; } +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } _processingScreenshot = YES; } - - UIImage* screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; - + + UIImage *screenshot = [SentryViewPhotographer.shared imageFromUIView:_rootView]; + _processingScreenshot = NO; - - dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(backgroundQueue, ^{ - [self->_replayMaker addFrame:screenshot]; - }); + + dispatch_queue_t backgroundQueue + = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrame:screenshot]; }); } @end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index eb4fe00cfe3..f6953ae919c 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -1,18 +1,16 @@ #import "SentrySessionReplayIntegration.h" -#import "SentrySessionReplay.h" -#import "SentryDependencyContainer.h" -#import "SentryUIApplication.h" -#import "SentrySDK+Private.h" #import "SentryClient+Private.h" +#import "SentryDependencyContainer.h" #import "SentryHub+Private.h" -#import "SentrySDK+Private.h" -#import "SentryReplaySettings.h" -#import "SentryRandom.h" #import "SentryOptions.h" - +#import "SentryRandom.h" +#import "SentryReplaySettings.h" +#import "SentrySDK+Private.h" +#import "SentrySessionReplay.h" +#import "SentryUIApplication.h" @implementation SentrySessionReplayIntegration { - SentrySessionReplay * sessionReplay; + SentrySessionReplay *sessionReplay; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -20,28 +18,36 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options if ([super installWithOptions:options] == NO) { return NO; } - + if (@available(iOS 16.0, *)) { - if (options.sessionReplaySettings.replaysSessionSampleRate == 0 && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { + if (options.sessionReplaySettings.replaysSessionSampleRate == 0 + && options.sessionReplaySettings.replaysOnErrorSampleRate == 0) { return NO; } - - sessionReplay = [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; - - [sessionReplay start:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:[self shouldReplayFullSession:options.sessionReplaySettings.replaysSessionSampleRate]]; - + + sessionReplay = + [[SentrySessionReplay alloc] initWithSettings:options.sessionReplaySettings]; + + [sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:options.sessionReplaySettings + .replaysSessionSampleRate]]; + SentryClient *client = [SentrySDK.currentHub getClient]; [client addAttachmentProcessor:sessionReplay]; - - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(stop) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; return YES; } else { return NO; } } --(void)stop { +- (void)stop +{ [sessionReplay stop]; } @@ -52,10 +58,10 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { - } -- (BOOL)shouldReplayFullSession:(CGFloat)rate { +- (BOOL)shouldReplayFullSession:(CGFloat)rate +{ return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; } diff --git a/Sources/Sentry/SentryViewPhotographer.m b/Sources/Sentry/SentryViewPhotographer.m index aaeca74ec0c..698ba9c53f7 100644 --- a/Sources/Sentry/SentryViewPhotographer.m +++ b/Sources/Sentry/SentryViewPhotographer.m @@ -3,75 +3,74 @@ #if SENTRY_HAS_UIKIT @implementation SentryViewPhotographer { - NSMutableArray * _ignoreClasses; - NSMutableArray * _redactClasses; + NSMutableArray *_ignoreClasses; + NSMutableArray *_redactClasses; } -+(SentryViewPhotographer *)shared { - static SentryViewPhotographer* _shared = nil; ++ (SentryViewPhotographer *)shared +{ + static SentryViewPhotographer *_shared = nil; static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _shared = [[SentryViewPhotographer alloc] init]; - }); - + dispatch_once(&onceToken, ^{ _shared = [[SentryViewPhotographer alloc] init]; }); + return _shared; } --(instancetype)init { +- (instancetype)init +{ if (self = [super init]) { - _ignoreClasses = @[ - UISlider.class, - UISwitch.class - ].mutableCopy; - - _redactClasses = @[ - UILabel.class, - UITextView.class, - UITextField.class - ].mutableCopy; - - - NSArray * extraClasses = @[ + _ignoreClasses = @[ UISlider.class, UISwitch.class ].mutableCopy; + + _redactClasses = @[ UILabel.class, UITextView.class, UITextField.class ].mutableCopy; + + NSArray *extraClasses = @[ @"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", @"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - @"SwiftUI._UIGraphicsView", - @"SwiftUI.ImageLayer" + @"SwiftUI._UIGraphicsView", @"SwiftUI.ImageLayer" ]; - - for (NSString * className in extraClasses) { + + for (NSString *className in extraClasses) { Class viewClass = NSClassFromString(className); - if (viewClass != nil) {[_redactClasses addObject:viewClass];} + if (viewClass != nil) { + [_redactClasses addObject:viewClass]; + } } } return self; } --(UIImage*)imageFromUIView:(UIView *)view { +- (UIImage *)imageFromUIView:(UIView *)view +{ UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, 0); CGContextRef currentContext = UIGraphicsGetCurrentContext(); - + [view.layer renderInContext:currentContext]; - + [self maskText:view context:currentContext]; - - UIImage* screenshot = UIGraphicsGetImageFromCurrentImageContext(); + + UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - + return screenshot; } -- (void)maskText:(UIView *)view context:(CGContextRef)context { +- (void)maskText:(UIView *)view context:(CGContextRef)context +{ [UIColor.blackColor setFill]; - CGPathRef maskPath = [self buildPathForView:view inPath:CGPathCreateMutable() visibleArea:view.frame]; + CGPathRef maskPath = [self buildPathForView:view + inPath:CGPathCreateMutable() + visibleArea:view.frame]; CGContextAddPath(context, maskPath); CGContextFillPath(context); } -- (BOOL)shouldIgnoreView:(UIView *)view { +- (BOOL)shouldIgnoreView:(UIView *)view +{ return [view isKindOfClass:UISwitch.class]; } -- (BOOL)shouldIgnore:(UIView *)view { +- (BOOL)shouldIgnore:(UIView *)view +{ for (Class class in _ignoreClasses) { if ([view isKindOfClass:class]) { return true; @@ -80,33 +79,41 @@ - (BOOL)shouldIgnore:(UIView *)view { return false; } -- (BOOL)shouldRedact:(UIView *)view { +- (BOOL)shouldRedact:(UIView *)view +{ for (Class class in _redactClasses) { if ([view isKindOfClass:class]) { return true; } } - - return ([view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); + + return ( + [view isKindOfClass:UIImageView.class] && [self shouldRedactImageView:(UIImageView *)view]); } -- (BOOL)shouldRedactImageView:(UIImageView *)imageView { - return imageView.image != nil - && [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil - && (imageView.image.size.width > 10 && imageView.image.size.height > 10); //This is to avoid redact gradient backgroud that are usually small lines repeating +- (BOOL)shouldRedactImageView:(UIImageView *)imageView +{ + return imageView.image != nil && + [imageView.image.imageAsset valueForKey:@"_containingBundle"] == nil + && (imageView.image.size.width > 10 + && imageView.image.size.height > 10); // This is to avoid redact gradient backgroud that + // are usually small lines repeating } -- (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)path visibleArea:(CGRect)area { +- (CGMutablePathRef)buildPathForView:(UIView *)view + inPath:(CGMutablePathRef)path + visibleArea:(CGRect)area +{ CGRect rectInWindow = [view convertRect:view.bounds toView:nil]; if (!CGRectIntersectsRect(area, rectInWindow)) { return path; } - + if (view.hidden || view.alpha == 0) { return path; } - + BOOL ignore = [self shouldIgnore:view]; if (!ignore && [self shouldRedact:view]) { CGPathAddRect(path, NULL, rectInWindow); @@ -116,7 +123,7 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)pat CGPathRelease(path); path = newPath; } - + if (!ignore) { for (UIView *subview in view.subviews) { path = [self buildPathForView:subview inPath:path visibleArea:area]; @@ -126,19 +133,22 @@ - (CGMutablePathRef)buildPathForView:(UIView *)view inPath:(CGMutablePathRef)pat return path; } -- (CGMutablePathRef) excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path { +- (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ if (@available(iOS 16.0, *)) { CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); return CGPathCreateMutableCopy(newPath); - } + } return path; } -- (BOOL)isOpaqueOrHasBackground:(UIView *)view { - return view.isOpaque || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); +- (BOOL)isOpaqueOrHasBackground:(UIView *)view +{ + return view.isOpaque + || (view.backgroundColor != nil && CGColorGetAlpha(view.backgroundColor.CGColor) > 0.9); } @end -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h index 8c799c15004..8aae0a4b713 100644 --- a/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h +++ b/Sources/Sentry/include/HybridPublic/SentryReplaySettings+Private.h @@ -3,7 +3,8 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentryReplaySettings (Private) +@interface +SentryReplaySettings (Private) /** * Defines the quality of the session replay. @@ -12,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) NSInteger replayBitRate; -- (instancetype)initWithDictionary:(NSDictionary*)dictionary; +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; @end diff --git a/Sources/Sentry/include/SentryOnDemandReplay.h b/Sources/Sentry/include/SentryOnDemandReplay.h index dc4d78ada3f..e1e9127cf77 100644 --- a/Sources/Sentry/include/SentryOnDemandReplay.h +++ b/Sources/Sentry/include/SentryOnDemandReplay.h @@ -2,8 +2,7 @@ #import #if SENTRY_HAS_UIKIT -#import - +# import NS_ASSUME_NONNULL_BEGIN @@ -17,7 +16,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)addFrame:(UIImage *)image; -- (void)createVideoOf:(NSTimeInterval)duration from:(NSDate *)beginning +- (void)createVideoOf:(NSTimeInterval)duration + from:(NSDate *)beginning outputFileURL:(NSURL *)outputFileURL completion:(void (^)(BOOL success, NSError *error))completion; @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 43d89925ba5..d8437c32c71 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -1,8 +1,8 @@ -#import -#import -#import "SentryEvent.h" #import "SentryClient+Private.h" +#import "SentryEvent.h" #import "SentryReplaySettings.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index 6d0dbb0389b..b789b91009a 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -1,6 +1,6 @@ -#import #import "SentryBaseIntegration.h" #import "SentryIntegrationProtocol.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/SentryViewPhotographer.h b/Sources/Sentry/include/SentryViewPhotographer.h index e03a8749fbf..2425af8794c 100644 --- a/Sources/Sentry/include/SentryViewPhotographer.h +++ b/Sources/Sentry/include/SentryViewPhotographer.h @@ -2,18 +2,18 @@ #import #if SENTRY_HAS_UIKIT -#import +# import NS_ASSUME_NONNULL_BEGIN @interface SentryViewPhotographer : NSObject -@property (nonatomic, readonly, class) SentryViewPhotographer* shared; +@property (nonatomic, readonly, class) SentryViewPhotographer *shared; --(UIImage*)imageFromUIView:(UIView *)view; +- (UIImage *)imageFromUIView:(UIView *)view; @end NS_ASSUME_NONNULL_END -#endif //SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT