From 4412fcc3a94c547ef7c3f0d4def79d2f03726426 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 9 Jul 2024 08:36:00 +0200 Subject: [PATCH 01/47] wip --- .../SessionReplay/SentrySessionReplay.swift | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index b6f08f17fd6..e8b04989be7 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -143,7 +143,7 @@ class SentrySessionReplay: NSObject { startFullReplay() guard let finalPath = urlToCache?.appendingPathComponent("replay.mp4") else { - print("[SentrySessionReplay:\(#line)] Could not create replay video path") + print("[\(#file):\(#line)] Could not create replay video path") return false } let replayStart = dateProvider.date().addingTimeInterval(-replayOptions.errorReplayDuration) @@ -200,7 +200,7 @@ class SentrySessionReplay: NSObject { do { try fileManager.createDirectory(atPath: pathToSegment.path, withIntermediateDirectories: true, attributes: nil) } catch { - print("[SentrySessionReplay:\(#line)] Can't create session replay segment folder. Error: \(error.localizedDescription)") + print("[\(#file):\(#line)] Can't create session replay segment folder. Error: \(error.localizedDescription)") return } } @@ -216,13 +216,13 @@ class SentrySessionReplay: NSObject { try replayMaker.createVideoWith(duration: duration, beginning: startedAt, outputFileURL: videoUrl) { [weak self] videoInfo, error in guard let _self = self else { return } if let error = error { - print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") + print("[\(#file):\(#line)] Could not create replay video - \(error.localizedDescription)") } else if let videoInfo = videoInfo { _self.newSegmentAvailable(videoInfo: videoInfo) } } } catch { - print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") + print("[\(#file):\(#line)] Could not create replay video - \(error.localizedDescription)") } } @@ -236,7 +236,6 @@ class SentrySessionReplay: NSObject { private func captureSegment(segment: Int, video: SentryVideoInfo, replayId: SentryId, replayType: SentryReplayType) { let replayEvent = SentryReplayEvent(eventId: replayId, replayStartTimestamp: video.start, replayType: replayType, segmentId: segment) - print("### eventId: \(replayId), replayStartTimestamp: \(video.start), replayType: \(replayType), segmentId: \(segment)") replayEvent.timestamp = video.end @@ -255,7 +254,7 @@ class SentrySessionReplay: NSObject { do { try FileManager.default.removeItem(at: video.path) } catch { - print("[SentrySessionReplay:\(#line)] Could not delete replay segment from disk: \(error.localizedDescription)") + print("[\(#file):\(#line)] Could not delete replay segment from disk: \(error.localizedDescription)") } } @@ -266,15 +265,18 @@ class SentrySessionReplay: NSObject { } .compactMap(breadcrumbConverter.convert(from:)) } - + private func takeScreenshot() { guard let rootView = rootView, !processingScreenshot else { return } - - lock.synchronized { - guard !processingScreenshot else { return } - processingScreenshot = true + + lock.lock() + guard !processingScreenshot else { + lock.unlock() + return } - + processingScreenshot = true + lock.unlock() + screenshotProvider.image(view: rootView, options: replayOptions) { [weak self] screenshot in self?.newImage(image: screenshot) } From fcb7c9e1b7a5b100e8263f2ba715cbebf78c0a0e Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 9 Jul 2024 17:26:49 +0200 Subject: [PATCH 02/47] wip --- .../Sentry/SentrySessionReplayIntegration.m | 18 ++++++++++++++++-- .../include/SentrySessionReplayIntegration.h | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index ad919f34555..486ce73caa6 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -17,6 +17,7 @@ # import "SentrySwizzle.h" # import "SentryUIApplication.h" # import +# import "SentrySerialization.h" NS_ASSUME_NONNULL_BEGIN @@ -153,6 +154,19 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions selector:@selector(resume) name:UIApplicationWillEnterForegroundNotification object:nil]; + + [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId path:docs.path options:replayOptions]; +} + +- (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { + NSDictionary * info = @{ @"sessionId":sessionId.sentryIdString, @"path":path, @"errorSampleRate":@(options.errorSampleRate) }; + NSData * data = [SentrySerialization dataWithJSONObject:info]; + + NSString * infoPath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"lastreplay"]; + if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) { + [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil]; + } + [data writeToFile:infoPath atomically:YES]; } - (void)stop @@ -185,9 +199,9 @@ - (void)sentrySessionStarted:(SentrySession *)session [self startSession]; } -- (void)captureReplay +- (BOOL)captureReplay { - //[self.sessionReplay captureReplay]; + return [self.sessionReplay captureReplay]; } - (void)configureReplayWith:(nullable id)breadcrumbConverter diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index 47237126b32..dcb2ffc121c 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Captures Replay. Used by the Hybrid SDKs. */ -- (void)captureReplay; +- (BOOL)captureReplay; /** * Configure session replay with different breadcrumb converter From 709c5768df1b470603e99b3d21cd29d44715f0de Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 15 Jul 2024 14:39:21 +0200 Subject: [PATCH 03/47] wip --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 8 ++ Sources/Sentry/SentryClient.m | 14 ++- Sources/Sentry/SentryNetworkTracker.m | 3 +- Sources/Sentry/SentryOptions.m | 7 +- .../Sentry/SentrySessionReplayIntegration.m | 87 ++++++++++++++++--- Sources/Sentry/SentrySessionReplaySyncC.c | 83 ++++++++++++++++++ Sources/Sentry/SentryTraceContext.m | 3 +- .../Sentry/include/SentrySessionReplaySyncC.h | 19 ++++ Sources/Sentry/include/SentryTraceContext.h | 4 +- Sources/SentryCrash/Recording/SentryCrashC.c | 2 + .../SessionReplay/SentryOnDemandReplay.swift | 33 +++++-- .../SessionReplay/SentryReplayRecording.swift | 4 + .../SessionReplay/SentrySessionReplay.swift | 20 +++-- .../Network/SentryNetworkTrackerTests.swift | 4 +- .../SentryOnDemandReplayTests.swift | 2 +- .../PrivateSentrySDKOnlyTests.swift | 3 +- Tests/SentryTests/SentryClientTests.swift | 2 +- Tests/SentryTests/SentryOptionsTest.m | 4 +- .../Transaction/SentryTraceStateTests.swift | 5 +- 20 files changed, 267 insertions(+), 42 deletions(-) create mode 100644 Sources/Sentry/SentrySessionReplaySyncC.c create mode 100644 Sources/Sentry/include/SentrySessionReplaySyncC.h diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 6c0b94f7dc7..29cff468b21 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *), !args.contains("--disable-session-replay") { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: true, redactAllImages: true) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1, redactAllText: true, redactAllImages: true) options.experimental.sessionReplay.quality = .high } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 2f29759e99b..9449b83e0f6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -800,6 +800,8 @@ D81FDF12280EA1060045E0E4 /* SentryScreenShotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */; }; D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; + D82859432C3E753C009A28AA /* SentrySessionReplaySyncC.c in Sources */ = {isa = PBXBuildFile; fileRef = D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */; }; + D82859442C3E753C009A28AA /* SentrySessionReplaySyncC.h in Headers */ = {isa = PBXBuildFile; fileRef = D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D82DD1CD2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; @@ -1850,6 +1852,8 @@ D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenShotTests.swift; sourceTree = ""; }; D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplaySyncC.h; path = include/SentrySessionReplaySyncC.h; sourceTree = ""; }; + D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentrySessionReplaySyncC.c; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySRDefaultBreadcrumbConverterTests.swift; sourceTree = ""; }; @@ -3594,6 +3598,8 @@ D80382BE2C09C6FD0090E048 /* SentrySessionReplayIntegration-Hybrid.h */, D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */, D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, + D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */, + D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */, ); name = SessionReplay; sourceTree = ""; @@ -4062,6 +4068,7 @@ 7B85DC1E24EFAFCD007D01D2 /* SentryClient+Private.h in Headers */, A8AFFCCF2906C03700967CD7 /* SentryRequest.h in Headers */, 7BD4BD4327EB29BA0071F4FF /* SentryClientReport.h in Headers */, + D82859442C3E753C009A28AA /* SentrySessionReplaySyncC.h in Headers */, 8459FCBE2BD73E820038E9C9 /* SentryProfilerSerialization.h in Headers */, 7B2BB0032966F55900A1E102 /* SentryOptions+HybridSDKs.h in Headers */, 7BF9EF762722B34700B5BBEF /* SentrySubClassFinder.h in Headers */, @@ -4582,6 +4589,7 @@ 15E0A8E5240C457D00F044E3 /* SentryEnvelope.m in Sources */, 8EC3AE7A25CA23B600E7591A /* SentrySpan.m in Sources */, 6360850E1ED2AFE100E8599E /* SentryBreadcrumb.m in Sources */, + D82859432C3E753C009A28AA /* SentrySessionReplaySyncC.c in Sources */, 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */, 7B56D73324616D9500B842DA /* SentryConcurrentRateLimitsDictionary.m in Sources */, 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 7ecd4a1cbbc..6feeef0cf0c 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -11,7 +11,7 @@ #import "SentryDsn.h" #import "SentryEnvelope+Private.h" #import "SentryEnvelopeItemType.h" -#import "SentryEvent.h" +#import "SentryEvent+Private.h" #import "SentryException.h" #import "SentryExtraContextProvider.h" #import "SentryFileManager.h" @@ -378,13 +378,14 @@ - (nullable SentryTraceContext *)getTraceStateWithEvent:(SentryEvent *)event if (tracer != nil) { return [[SentryTraceContext alloc] initWithTracer:tracer scope:scope options:_options]; } - + if (event.error || event.exceptions.count > 0) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [[SentryTraceContext alloc] initWithTraceId:scope.propagationContext.traceId options:self.options - userSegment:scope.userObject.segment]; + userSegment:scope.userObject.segment + replayId:scope.replayId]; #pragma clang diagnostic pop } @@ -414,6 +415,8 @@ - (SentryId *)sendEvent:(SentryEvent *)event alwaysAttachStacktrace:alwaysAttachStacktrace isCrashEvent:isCrashEvent]; + + if (preparedEvent == nil) { return SentryId.empty; } @@ -448,6 +451,11 @@ - (SentryId *)sendEvent:(SentryEvent *)event attachments = [attachmentProcessor processAttachments:attachments forEvent:event]; } } + + if (event.isCrashEvent && event.context[@"replay"] && [event.context[@"replay"] isKindOfClass:NSDictionary.class]) { + NSDictionary* replay = event.context[@"replay"]; + scope.replayId = replay[@"replay_id"]; + } SentryTraceContext *traceContext = [self getTraceStateWithEvent:event withScope:scope]; diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index a781d83958e..8e1e823b22d 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -239,7 +239,8 @@ - (void)addTraceWithoutTransactionToTask:(NSURLSessionTask *)sessionTask SentryTraceContext *traceContext = [[SentryTraceContext alloc] initWithTraceId:propagationContext.traceId options:SentrySDK.currentHub.client.options - userSegment:SentrySDK.currentHub.scope.userObject.segment]; + userSegment:SentrySDK.currentHub.scope.userObject.segment + replayId:SentrySDK.currentHub.scope.replayId]; #pragma clang diagnostic pop [self addBaggageHeader:[traceContext toBaggage] diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index b27fef1cbee..2ae9fa638da 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -44,8 +44,12 @@ @implementation SentryOptions { { // The order of integrations here is important. // SentryCrashIntegration needs to be initialized before SentryAutoSessionTrackingIntegration. + // And SentrySessionReplayIntegration before SentryCrashIntegration. NSMutableArray *defaultIntegrations = @[ +# if SENTRY_HAS_UIKIT && !TARGET_OS_VISION + NSStringFromClass([SentrySessionReplayIntegration class]), +# endif NSStringFromClass([SentryCrashIntegration class]), #if SENTRY_HAS_UIKIT NSStringFromClass([SentryAppStartTrackingIntegration class]), @@ -55,9 +59,6 @@ @implementation SentryOptions { NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), -# if !TARGET_OS_VISION - NSStringFromClass([SentrySessionReplayIntegration class]), -# endif #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 486ce73caa6..e782ebfca55 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -18,6 +18,9 @@ # import "SentryUIApplication.h" # import # import "SentrySerialization.h" +# import "SentrySessionReplaySyncC.h" +# import "SentryEvent+Private.h" +# import "SentryLog.h" NS_ASSUME_NONNULL_BEGIN @@ -60,16 +63,72 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [SentrySDK.currentHub registerSessionListener:self]; - [SentryGlobalEventProcessor.shared - addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + if (event.isCrashEvent) { + [self resumePreviousSessionReplay:event]; + } else { [self.sessionReplay captureReplayForEvent:event]; - - return event; - }]; + } + return event; + }]; return YES; } +- (void)resumePreviousSessionReplay:(SentryEvent *)event { + NSURL* dir = [self replayDirectory]; + NSData* lastReplay = [NSData dataWithContentsOfURL:[dir URLByAppendingPathComponent:@"lastreplay"]]; + if (lastReplay == nil) { return; } + + NSDictionary * jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay options:0 error:nil]; + + SentryId * replayId = [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]]; + + + NSURL * lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; + + SentryCrashReplay crashInfo = { 0 }; + bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path cStringUsingEncoding:NSUTF8StringEncoding]); + + SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; + + SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom: lastReplayURL.path]; + + [replayMaker createVideoWithDuration:_replayOptions.sessionSegmentDuration + beginning:[NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:nil + completion:^(SentryVideoInfo * video, NSError * error) { + + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + } else { + [self captureVideo:video replayId:replayId segmentId:crashInfo.segmentId + 1 type:type]; + } + }]; + + NSMutableDictionary * eventContext = event.context.mutableCopy; + eventContext[@"replay"] = @{@"replay_id": replayId.sentryIdString}; + event.context = eventContext; +} + +- (void)captureVideo:(SentryVideoInfo *)video replayId:(SentryId *)replayId segmentId:(int)segment type:(SentryReplayType)type { + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId: replayId + replayStartTimestamp:video.start + replayType:type + segmentId:segment]; + replayEvent.timestamp = video.end; + SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment video:video extraEvents:@[]]; + + [self sessionReplayNewSegmentWithReplayEvent:replayEvent replayRecording:recording videoUrl:video.path]; + + NSError *error = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { + NSLog(@"[SentrySessionReplay:%d] Could not delete replay segment from disk: %@", __LINE__, error.localizedDescription); + } +} + + - (void)startSession { _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; @@ -111,9 +170,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions breadcrumbConverter:(id)breadcrumbConverter fullSession:(BOOL)shouldReplayFullSession { - NSURL *docs = - [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; - docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; + NSURL *docs = [self replayDirectory]; NSString *currentSession = [NSUUID UUID].UUIDString; docs = [docs URLByAppendingPathComponent:currentSession]; @@ -158,15 +215,23 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId path:docs.path options:replayOptions]; } +- (NSURL*)replayDirectory { + NSURL *dir = [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; +} + + - (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { - NSDictionary * info = @{ @"sessionId":sessionId.sentryIdString, @"path":path, @"errorSampleRate":@(options.errorSampleRate) }; + NSDictionary * info = @{ @"replayId":sessionId.sentryIdString, @"path":path.lastPathComponent, @"errorSampleRate":@(options.errorSampleRate) }; NSData * data = [SentrySerialization dataWithJSONObject:info]; - + NSString * infoPath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"lastreplay"]; if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) { [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil]; } [data writeToFile:infoPath atomically:YES]; + + sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"] cStringUsingEncoding:NSUTF8StringEncoding]); } - (void)stop @@ -305,6 +370,8 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:replayRecording video:videoUrl]; + + sentrySessionReplaySync_updateInfo((unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSince1970); } - (void)sessionReplayStartedWithReplayId:(SentryId *)replayId diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c new file mode 100644 index 00000000000..53bcb79538e --- /dev/null +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -0,0 +1,83 @@ +#include "SentrySessionReplaySyncC.h" +#include +#include +#include +#include +#include "SentryAsyncSafeLog.h" +#include +#include +#include + +static SentryCrashReplay crashReplay = { 0 }; + + +void sentrySessionReplaySync_start(const char *const path) { + crashReplay.lastSegmentEnd = 0; + crashReplay.segmentId = 0; + + if (crashReplay.path != NULL) { + free(crashReplay.path); + } + + crashReplay.path = malloc(strlen(path)); + strcpy(crashReplay.path, path); +} + +void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd) { + crashReplay.segmentId = segmentId; + crashReplay.lastSegmentEnd = lastSegmentEnd; +} + +void sentrySessionReplaySync_writeInfo(void) { + int fd = open(crashReplay.path, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 1) { + SENTRY_ASYNC_SAFE_LOG_ERROR( + "Could not open replay info crash for file %s: %s", crashReplay.path, strerror(errno)); + return; + } + + if (write(fd, &crashReplay.segmentId, sizeof(crashReplay.segmentId)) != sizeof(crashReplay.segmentId)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); + close(fd); + return; + } + + if (write(fd, &crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd)) != sizeof(crashReplay.lastSegmentEnd)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); + close(fd); + return; + } + + close(fd); +} + +bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * const path) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + SENTRY_ASYNC_SAFE_LOG_ERROR( + "Could not open replay info crash file %s: %s", path, strerror(errno)); + return false; + } + + unsigned int segmentId; + double lastSegmentEnd; + + if (read(fd, &segmentId, sizeof(segmentId)) != sizeof(segmentId)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading segmentId from replay info crash file."); + close(fd); + return false; + } + + if (read(fd, &lastSegmentEnd, sizeof(lastSegmentEnd)) != sizeof(lastSegmentEnd)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading lastSegmentEnd from replay info crash file."); + close(fd); + return false; + } + + close(fd); + + // Assign read values to crashReplay struct or process them as needed + output->segmentId = segmentId; + output->lastSegmentEnd = lastSegmentEnd; + return true; +} diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m index afe2a1b541f..c14486b23dd 100644 --- a/Sources/Sentry/SentryTraceContext.m +++ b/Sources/Sentry/SentryTraceContext.m @@ -92,6 +92,7 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer - (instancetype)initWithTraceId:(SentryId *)traceId options:(SentryOptions *)options userSegment:(nullable NSString *)userSegment + replayId:(nullable NSString *)replayId; { return [[SentryTraceContext alloc] initWithTraceId:traceId publicKey:options.parsedDsn.url.user @@ -101,7 +102,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId userSegment:userSegment sampleRate:nil sampled:nil - replayId:nil]; + replayId:replayId]; } - (nullable instancetype)initWithDict:(NSDictionary *)dictionary diff --git a/Sources/Sentry/include/SentrySessionReplaySyncC.h b/Sources/Sentry/include/SentrySessionReplaySyncC.h new file mode 100644 index 00000000000..3146ca0d398 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplaySyncC.h @@ -0,0 +1,19 @@ +#ifndef SentrySessionReplaySyncC_h +#define SentrySessionReplaySyncC_h +#include + +typedef struct { + unsigned int segmentId; + double lastSegmentEnd; + char* path; +} SentryCrashReplay; + +void sentrySessionReplaySync_start(const char *const path); + +void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd); + +void sentrySessionReplaySync_writeInfo(void); + +bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * const path); + +#endif /* SentrySessionReplaySyncC_h */ diff --git a/Sources/Sentry/include/SentryTraceContext.h b/Sources/Sentry/include/SentryTraceContext.h index 9ed6a67816a..878d49038bd 100644 --- a/Sources/Sentry/include/SentryTraceContext.h +++ b/Sources/Sentry/include/SentryTraceContext.h @@ -86,6 +86,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithTracer:(SentryTracer *)tracer scope:(nullable SentryScope *)scope options:(SentryOptions *)options; + /** * Initializes a SentryTraceContext with data from a traceID, options and userSegment. @@ -96,7 +97,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithTraceId:(SentryId *)traceId options:(SentryOptions *)options - userSegment:(nullable NSString *)userSegment; + userSegment:(nullable NSString *)userSegment + replayId:(nullable NSString *)replayId; /** * Create a SentryBaggage with the information of this SentryTraceContext. diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index c201a944379..7f1eb5635ad 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -45,6 +45,7 @@ #include #include #include +#include "SentrySessionReplaySyncC.h" // ============================================================================ #pragma mark - Globals - @@ -83,6 +84,7 @@ onCrash(struct SentryCrash_MonitorContext *monitorContext) sentrycrashcrs_getNextCrashReportPath(crashReportFilePath); strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath)); sentrycrashreport_writeStandardReport(monitorContext, crashReportFilePath); + sentrySessionReplaySync_writeInfo(); } // Report is saved to disk, now we try to take screenshots diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 4b299e46fa5..85277fad1b5 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -23,6 +23,7 @@ enum SentryOnDemandReplayError: Error { @objcMembers class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { + private let _outputPath: String private var _currentPixelBuffer: SentryPixelBuffer? private var _totalFrames = 0 @@ -43,6 +44,26 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max + + init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + self._outputPath = outputPath + self.dateProvider = dateProvider + self.workingQueue = workingQueue + } + + convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider) + guard let content = try? FileManager.default.contentsOfDirectory(atPath: outputPath) else { return } + + _frames = content.compactMap { + guard let extensionIndex = $0.lastIndex(of: "."), $0[extensionIndex...] == ".png" + else { return SentryReplayFrame?.none } + + guard let time = Double($0[.. Bool @@ -143,7 +147,7 @@ class SentrySessionReplay: NSObject { startFullReplay() guard let finalPath = urlToCache?.appendingPathComponent("replay.mp4") else { - print("[\(#file):\(#line)] Could not create replay video path") + print("[SentrySessionReplay:\(#line)] Could not create replay video path") return false } let replayStart = dateProvider.date().addingTimeInterval(-replayOptions.errorReplayDuration) @@ -169,11 +173,11 @@ class SentrySessionReplay: NSObject { @objc private func newFrame(_ sender: CADisplayLink) { - guard let sessionStart = sessionStart, let lastScreenShot = lastScreenShot, isRunning else { return } + guard let lastScreenShot = lastScreenShot, isRunning else { return } let now = dateProvider.date() - if isFullSession && now.timeIntervalSince(sessionStart) > replayOptions.maximumDuration { + if let sessionStart = sessionStart, isFullSession && now.timeIntervalSince(sessionStart) > replayOptions.maximumDuration { reachedMaximumDuration = true stop() return @@ -200,7 +204,7 @@ class SentrySessionReplay: NSObject { do { try fileManager.createDirectory(atPath: pathToSegment.path, withIntermediateDirectories: true, attributes: nil) } catch { - print("[\(#file):\(#line)] Can't create session replay segment folder. Error: \(error.localizedDescription)") + print("[SentrySessionReplay:\(#line)] Can't create session replay segment folder. Error: \(error.localizedDescription)") return } } @@ -216,13 +220,13 @@ class SentrySessionReplay: NSObject { try replayMaker.createVideoWith(duration: duration, beginning: startedAt, outputFileURL: videoUrl) { [weak self] videoInfo, error in guard let _self = self else { return } if let error = error { - print("[\(#file):\(#line)] Could not create replay video - \(error.localizedDescription)") + print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") } else if let videoInfo = videoInfo { _self.newSegmentAvailable(videoInfo: videoInfo) } } } catch { - print("[\(#file):\(#line)] Could not create replay video - \(error.localizedDescription)") + print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") } } @@ -247,14 +251,14 @@ class SentrySessionReplay: NSObject { touchTracker.flushFinishedEvents() } - let recording = SentryReplayRecording(segmentId: replayEvent.segmentId, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width, extraEvents: events) + let recording = SentryReplayRecording(segmentId: segment, video: video, extraEvents: events) delegate?.sessionReplayNewSegment(replayEvent: replayEvent, replayRecording: recording, videoUrl: video.path) do { try FileManager.default.removeItem(at: video.path) } catch { - print("[\(#file):\(#line)] Could not delete replay segment from disk: \(error.localizedDescription)") + print("[SentrySessionReplay:\(#line)] Could not delete replay segment from disk: \(error.localizedDescription)") } } diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 04e56bb1740..25bfb399d53 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -715,7 +715,7 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTaskResume(task) let expectedTraceHeader = SentrySDK.currentHub().scope.propagationContext.traceHeader.value() - let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment) + let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment, replayId: nil) let expectedBaggageHeader = traceContext.toBaggage().toHTTPHeader(withOriginalBaggage: nil) XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["baggage"] ?? "", expectedBaggageHeader) XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", expectedTraceHeader) @@ -728,7 +728,7 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTaskResume(task) let expectedTraceHeader = SentrySDK.currentHub().scope.propagationContext.traceHeader.value() - let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment) + let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment, replayId: nil) let expectedBaggageHeader = traceContext.toBaggage().toHTTPHeader(withOriginalBaggage: nil) XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["baggage"] ?? "", expectedBaggageHeader) XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", expectedTraceHeader) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index 40e69496b10..8fe1bc06870 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -93,7 +93,7 @@ class SentryOnDemandReplayTests: XCTestCase { group.wait() queue.queue.sync {} //Wait for all enqueued operation to finish - XCTAssertEqual(sut.frames.map({ ($0.imagePath as NSString).lastPathComponent }), (0..<10).map { "\($0).png" }) + XCTAssertEqual(sut.frames.map({ ($0.imagePath as NSString).lastPathComponent }), (0..<10).map { "\($0).0.png" }) } func testReleaseIsThreadSafe() { diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index 8e4b84773dc..691734878ed 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -373,8 +373,9 @@ class PrivateSentrySDKOnlyTests: XCTestCase { return true } - override func captureReplay() { + override func captureReplay() -> Bool { TestSentrySessionReplayIntegration.captureReplayCalledTimes += 1 + return true } static func captureReplayShouldBeCalledAtLeastOnce() -> Bool { diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 43a69c416dc..109e6fbcf52 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -659,7 +659,7 @@ class SentryClientTest: XCTestCase { try assertValidErrorEvent(eventWithSessionArguments.event, error) XCTAssertEqual(fixture.session, eventWithSessionArguments.session) - let expectedTraceContext = SentryTraceContext(trace: scope.propagationContext.traceId, options: Options(), userSegment: "segment") + let expectedTraceContext = SentryTraceContext(trace: scope.propagationContext.traceId, options: Options(), userSegment: "segment", replayId: nil) XCTAssertEqual(eventWithSessionArguments.traceContext?.traceId, expectedTraceContext.traceId) } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 004d9680035..b78393b0d05 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -459,9 +459,11 @@ - (void)testDefaultIntegrations @"Default integrations are not set correctly"); } -- (void)testSentryCrashIntegrationIsFirst +- (void)testIntegrationOrder { XCTAssertEqualObjects(SentryOptions.defaultIntegrations.firstObject, + NSStringFromClass([SentrySessionReplayIntegration class])); + XCTAssertEqualObjects(SentryOptions.defaultIntegrations[1], NSStringFromClass([SentryCrashIntegration class])); } diff --git a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift index 7b549815040..3d5233b4c08 100644 --- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift +++ b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift @@ -99,7 +99,7 @@ class SentryTraceContextTests: XCTestCase { options.dsn = TestConstants.realDSN let traceId = SentryId() - let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: "segment") + let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: "segment", replayId: "replayId") XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey) XCTAssertEqual(traceId, traceContext.traceId) @@ -107,6 +107,7 @@ class SentryTraceContextTests: XCTestCase { XCTAssertEqual(options.environment, traceContext.environment) XCTAssertNil(traceContext.transaction) XCTAssertEqual("segment", traceContext.userSegment) + XCTAssertEqual(traceContext.replayId, "replayId") XCTAssertNil(traceContext.sampleRate) XCTAssertNil(traceContext.sampled) } @@ -116,7 +117,7 @@ class SentryTraceContextTests: XCTestCase { options.dsn = TestConstants.realDSN let traceId = SentryId() - let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: nil) + let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: nil, replayId: nil) XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey) XCTAssertEqual(traceId, traceContext.traceId) From b172c302aa90bfbf34ca39f8859b21e6193cf081 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 15 Jul 2024 15:45:00 +0200 Subject: [PATCH 04/47] wip --- .../Sentry/SentrySessionReplayIntegration.m | 24 +++++++++++-------- Sources/Sentry/SentrySessionReplaySyncC.c | 6 ++--- .../SessionReplay/SentryOnDemandReplay.swift | 10 ++++---- .../SessionReplay/SentrySessionReplay.swift | 1 + 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 21b1d6bd56e..6db0116de25 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -81,29 +81,33 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event { if (lastReplay == nil) { return; } NSDictionary * jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay options:0 error:nil]; - - SentryId * replayId = [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]]; - - + SentryId * replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] : [[SentryId alloc] init]; ; NSURL * lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; SentryCrashReplay crashInfo = { 0 }; bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path cStringUsingEncoding:NSUTF8StringEncoding]); SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; + NSTimeInterval duration = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; + int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom: lastReplayURL.path]; + NSDate * beginning = hasCrashInfo ? [NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] : [replayMaker oldestFrameDate]; + if (beginning == nil) { + //no frames to send + return; + } - [replayMaker createVideoWithDuration:_replayOptions.sessionSegmentDuration - beginning:[NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] - error:nil - completion:^(SentryVideoInfo * video, NSError * error) { + [replayMaker createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:nil + completion:^(SentryVideoInfo * video, NSError * error) { if (error != nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); } else { - [self captureVideo:video replayId:replayId segmentId:crashInfo.segmentId + 1 type:type]; + [self captureVideo:video replayId:replayId segmentId:segmentId type:type]; } }]; diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index 53bcb79538e..4c8e757d472 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -59,8 +59,8 @@ bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * co return false; } - unsigned int segmentId; - double lastSegmentEnd; + unsigned int segmentId = 0; + double lastSegmentEnd = 0; if (read(fd, &segmentId, sizeof(segmentId)) != sizeof(segmentId)) { SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading segmentId from replay info crash file."); @@ -79,5 +79,5 @@ bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * co // Assign read values to crashReplay struct or process them as needed output->segmentId = segmentId; output->lastSegmentEnd = lastSegmentEnd; - return true; + return lastSegmentEnd != 0; } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index af526450218..f9c39514cd2 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -61,11 +61,9 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { _frames = content.compactMap { guard let extensionIndex = $0.lastIndex(of: "."), $0[extensionIndex...] == ".png" else { return SentryReplayFrame?.none } - guard let time = Double($0[.. Void) throws { var frameCount = 0 let videoFrames = filterFrames(beginning: beginning, end: end) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 38b00a2b5d1..79b53cd9dc3 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -5,6 +5,7 @@ import UIKit enum SessionReplayError : Error { case cantCreateReplayDirectory + case noFramesAvailable } @objc From 05d2879927d0763705ba42c0776ff60629aef635 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Jul 2024 14:50:36 +0200 Subject: [PATCH 05/47] Is working --- .../xcshareddata/xcschemes/iOS-Swift.xcscheme | 4 ++-- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- .../Sentry/SentrySessionReplayIntegration.m | 18 +++++++++++------- Sources/Sentry/SentrySessionReplaySyncC.c | 3 ++- .../SessionReplay/SentryOnDemandReplay.swift | 17 +++++++++++++---- .../SessionReplay/SentryReplayRecording.swift | 6 +++++- .../SessionReplay/SentrySessionReplay.swift | 4 ++-- 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme index b4ea1e92fba..14d07de67ab 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme @@ -50,8 +50,8 @@ _resumeReplayMaker = nil; }]; NSMutableDictionary * eventContext = event.context.mutableCopy; @@ -117,14 +122,14 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event { } - (void)captureVideo:(SentryVideoInfo *)video replayId:(SentryId *)replayId segmentId:(int)segment type:(SentryReplayType)type { - SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId: replayId + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId:replayId replayStartTimestamp:video.start replayType:type segmentId:segment]; replayEvent.timestamp = video.end; SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment video:video extraEvents:@[]]; - [self sessionReplayNewSegmentWithReplayEvent:replayEvent replayRecording:recording videoUrl:video.path]; + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:video.path]; NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { @@ -132,7 +137,6 @@ - (void)captureVideo:(SentryVideoInfo *)video replayId:(SentryId *)replayId segm } } - - (void)startSession { _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; @@ -187,6 +191,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; replayMaker.bitRate = replayOptions.replayBitRate; + replayMaker.videoScale = replayOptions.sizeScale; replayMaker.cacheMaxSize = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 : replayOptions.errorReplayDuration + 1); @@ -224,7 +229,6 @@ - (NSURL*)replayDirectory { return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; } - - (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { NSDictionary * info = @{ @"replayId":sessionId.sentryIdString, @"path":path.lastPathComponent, @"errorSampleRate":@(options.errorSampleRate) }; NSData * data = [SentrySerialization dataWithJSONObject:info]; diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index 4c8e757d472..0423d5e346b 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -30,6 +30,7 @@ void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegme void sentrySessionReplaySync_writeInfo(void) { int fd = open(crashReplay.path, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 1) { SENTRY_ASYNC_SAFE_LOG_ERROR( "Could not open replay info crash for file %s: %s", crashReplay.path, strerror(errno)); @@ -73,7 +74,7 @@ bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * co close(fd); return false; } - + close(fd); // Assign read values to crashReplay struct or process them as needed diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index f9c39514cd2..5cc632cd8ef 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -44,9 +44,14 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var videoWidth = 200 var videoHeight = 434 + var videoScale : Float = 1 var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max + + private var actualWidth: Int { Int(Float(videoWidth) * videoScale) } + private var actualHeight: Int { Int(Float(videoHeight) * videoScale) } + init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self._outputPath = outputPath @@ -76,6 +81,10 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { self.init(withContentFrom: outputPath, workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil), dateProvider: SentryCurrentDateProvider()) + + guard let last = _frames.last, let image = UIImage(contentsOfFile: last.imagePath) else { return } + videoWidth = Int(image.size.width) + videoHeight = Int(image.size.height) } func addFrameAsync(image: UIImage, forScreen: String?) { @@ -135,7 +144,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mp4) let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: createVideoSettings()) - _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight), videoWriterInput: videoWriterInput) + _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: actualWidth, height: actualHeight), videoWriterInput: videoWriterInput) if _currentPixelBuffer == nil { return } videoWriter.add(videoWriterInput) @@ -169,7 +178,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { completion(nil, SentryOnDemandReplayError.cantReadVideoSize) return } - videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(videoFrames.framesPaths.count / self.frameRate), frameCount: videoFrames.framesPaths.count, frameRate: self.frameRate, start: videoFrames.start, end: videoFrames.end, fileSize: fileSize, screens: videoFrames.screens) + videoInfo = SentryVideoInfo(path: outputFileURL, height: self.actualHeight, width: self.actualWidth, duration: TimeInterval(videoFrames.framesPaths.count / self.frameRate), frameCount: videoFrames.framesPaths.count, frameRate: self.frameRate, start: videoFrames.start, end: videoFrames.end, fileSize: fileSize, screens: videoFrames.screens) } catch { completion(nil, error) } @@ -207,8 +216,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { private func createVideoSettings() -> [String: Any] { return [ AVVideoCodecKey: AVVideoCodecType.h264, - AVVideoWidthKey: videoWidth, - AVVideoHeightKey: videoHeight, + AVVideoWidthKey: actualWidth, + AVVideoHeightKey: actualHeight, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: bitRate, AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift index 206226f38a9..10f7fc4e332 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift @@ -9,15 +9,19 @@ class SentryReplayRecording: NSObject { static let SentryReplayFrameRateType = "constant" let segmentId: Int - let events: [any SentryRRWebEventProtocol] + let height: Int + let width: Int + convenience init(segmentId: Int, video: SentryVideoInfo, extraEvents: [any SentryRRWebEventProtocol]) { self.init(segmentId: segmentId, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width, extraEvents: extraEvents) } init(segmentId: Int, size: Int, start: Date, duration: TimeInterval, frameCount: Int, frameRate: Int, height: Int, width: Int, extraEvents: [any SentryRRWebEventProtocol]?) { self.segmentId = segmentId + self.width = width + self.height = height let meta = SentryRRWebMetaEvent(timestamp: start, height: height, width: width) let video = SentryRRWebVideoEvent(timestamp: start, segmentId: segmentId, size: size, duration: duration, encoding: SentryReplayRecording.SentryReplayEncoding, container: SentryReplayRecording.SentryReplayContainer, height: height, width: width, frameCount: frameCount, frameRateType: SentryReplayRecording.SentryReplayFrameRateType, frameRate: frameRate, left: 0, top: 0) diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 79b53cd9dc3..14eae75c0d4 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -82,8 +82,8 @@ class SentrySessionReplay: NSObject { videoSegmentStart = nil currentSegmentId = 0 sessionReplayId = SentryId() - replayMaker.videoWidth = Int(Float(rootView.frame.size.width) * replayOptions.sizeScale) - replayMaker.videoHeight = Int(Float(rootView.frame.size.height) * replayOptions.sizeScale) + replayMaker.videoWidth = Int(rootView.frame.size.width) + replayMaker.videoHeight = Int(rootView.frame.size.height) imageCollection = [] if fullSession { From 30fab873b0787f0d7e9a6ba9ce1841e70d5c15eb Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Jul 2024 16:29:23 +0200 Subject: [PATCH 06/47] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f60cc1c18..2245ea9ee0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Replay for crashes () + ## 8.31.1 ### Fixes From 412e5c4bf70becaa8607a2be4ac0254edf5dfd2e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 16 Jul 2024 14:31:48 +0000 Subject: [PATCH 07/47] Format code --- Sources/Sentry/SentryClient.m | 11 +- Sources/Sentry/SentryOptions.m | 4 +- .../Sentry/SentrySessionReplayIntegration.m | 179 +++++++++++------- Sources/Sentry/SentrySessionReplaySyncC.c | 55 +++--- .../Sentry/include/SentrySessionReplaySyncC.h | 4 +- Sources/Sentry/include/SentryTraceContext.h | 1 - Sources/SentryCrash/Recording/SentryCrashC.c | 2 +- .../SessionReplay/SentryOnDemandReplay.swift | 9 +- .../SessionReplay/SentrySessionReplay.swift | 2 +- Tests/SentryTests/SentryOptionsTest.m | 4 +- 10 files changed, 159 insertions(+), 112 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8327ff8dfb0..734a61269f0 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -397,7 +397,7 @@ - (nullable SentryTraceContext *)getTraceStateWithEvent:(SentryEvent *)event if (tracer != nil) { return [[SentryTraceContext alloc] initWithTracer:tracer scope:scope options:_options]; } - + if (event.error || event.exceptions.count > 0) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -434,8 +434,6 @@ - (SentryId *)sendEvent:(SentryEvent *)event alwaysAttachStacktrace:alwaysAttachStacktrace isCrashEvent:isCrashEvent]; - - if (preparedEvent == nil) { return SentryId.empty; } @@ -470,9 +468,10 @@ - (SentryId *)sendEvent:(SentryEvent *)event attachments = [attachmentProcessor processAttachments:attachments forEvent:event]; } } - - if (event.isCrashEvent && event.context[@"replay"] && [event.context[@"replay"] isKindOfClass:NSDictionary.class]) { - NSDictionary* replay = event.context[@"replay"]; + + if (event.isCrashEvent && event.context[@"replay"] && + [event.context[@"replay"] isKindOfClass:NSDictionary.class]) { + NSDictionary *replay = event.context[@"replay"]; scope.replayId = replay[@"replay_id"]; } diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 2ae9fa638da..8004d144fca 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -47,9 +47,9 @@ @implementation SentryOptions { // And SentrySessionReplayIntegration before SentryCrashIntegration. NSMutableArray *defaultIntegrations = @[ -# if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION NSStringFromClass([SentrySessionReplayIntegration class]), -# endif +#endif NSStringFromClass([SentryCrashIntegration class]), #if SENTRY_HAS_UIKIT NSStringFromClass([SentryAppStartTrackingIntegration class]), diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 90b773fa0a7..6222fc2ea7f 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -5,22 +5,22 @@ # import "SentryClient+Private.h" # import "SentryDependencyContainer.h" # import "SentryDisplayLinkWrapper.h" +# import "SentryEvent+Private.h" # import "SentryFileManager.h" # import "SentryGlobalEventProcessor.h" # import "SentryHub+Private.h" +# import "SentryLog.h" # import "SentryNSNotificationCenterWrapper.h" # import "SentryOptions.h" # import "SentryRandom.h" # import "SentrySDK+Private.h" # import "SentryScope+Private.h" +# import "SentrySerialization.h" +# import "SentrySessionReplaySyncC.h" # import "SentrySwift.h" # import "SentrySwizzle.h" # import "SentryUIApplication.h" # import -# import "SentrySerialization.h" -# import "SentrySessionReplaySyncC.h" -# import "SentryEvent+Private.h" -# import "SentryLog.h" NS_ASSUME_NONNULL_BEGIN @@ -42,7 +42,7 @@ @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; SentryNSNotificationCenterWrapper *_notificationCenter; - SentryOnDemandReplay * _resumeReplayMaker; + SentryOnDemandReplay *_resumeReplayMaker; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -64,76 +64,103 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [SentrySDK.currentHub registerSessionListener:self]; - [SentryGlobalEventProcessor.shared addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - if (event.isCrashEvent) { - [self resumePreviousSessionReplay:event]; - } else { - [self.sessionReplay captureReplayForEvent:event]; - } - return event; - }]; + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + if (event.isCrashEvent) { + [self resumePreviousSessionReplay:event]; + } else { + [self.sessionReplay captureReplayForEvent:event]; + } + return event; + }]; return YES; } -- (void)resumePreviousSessionReplay:(SentryEvent *)event { - NSURL* dir = [self replayDirectory]; - NSData* lastReplay = [NSData dataWithContentsOfURL:[dir URLByAppendingPathComponent:@"lastreplay"]]; - if (lastReplay == nil) { return; } - - NSDictionary * jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay options:0 error:nil]; - SentryId * replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] : [[SentryId alloc] init]; ; - NSURL * lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; - +- (void)resumePreviousSessionReplay:(SentryEvent *)event +{ + NSURL *dir = [self replayDirectory]; + NSData *lastReplay = + [NSData dataWithContentsOfURL:[dir URLByAppendingPathComponent:@"lastreplay"]]; + if (lastReplay == nil) { + return; + } + + NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay + options:0 + error:nil]; + SentryId *replayId = jsonObject[@"replayId"] + ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] + : [[SentryId alloc] init]; + ; + NSURL *lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; + SentryCrashReplay crashInfo = { 0 }; - bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path cStringUsingEncoding:NSUTF8StringEncoding]); - + bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, + [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path + cStringUsingEncoding:NSUTF8StringEncoding]); + SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; - NSTimeInterval duration = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; + NSTimeInterval duration + = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; - - _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom: lastReplayURL.path]; + + _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; _resumeReplayMaker.bitRate = _replayOptions.replayBitRate; _resumeReplayMaker.videoScale = _replayOptions.sizeScale; - - NSDate * beginning = hasCrashInfo ? [NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] : [_resumeReplayMaker oldestFrameDate]; + + NSDate *beginning = hasCrashInfo + ? [NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] + : [_resumeReplayMaker oldestFrameDate]; if (beginning == nil) { - //no frames to send + // no frames to send return; } - - [_resumeReplayMaker createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] - error:nil - completion:^(SentryVideoInfo * video, NSError * error) { - - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - } else { - [self captureVideo:video replayId:replayId segmentId:segmentId type:type]; - } - self->_resumeReplayMaker = nil; - }]; - NSMutableDictionary * eventContext = event.context.mutableCopy; - eventContext[@"replay"] = @{@"replay_id": replayId.sentryIdString}; + [_resumeReplayMaker + createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:nil + completion:^(SentryVideoInfo *video, NSError *error) { + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + } else { + [self captureVideo:video + replayId:replayId + segmentId:segmentId + type:type]; + } + self->_resumeReplayMaker = nil; + }]; + + NSMutableDictionary *eventContext = event.context.mutableCopy; + eventContext[@"replay"] = @{ @"replay_id" : replayId.sentryIdString }; event.context = eventContext; } -- (void)captureVideo:(SentryVideoInfo *)video replayId:(SentryId *)replayId segmentId:(int)segment type:(SentryReplayType)type { - SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId:replayId - replayStartTimestamp:video.start - replayType:type - segmentId:segment]; +- (void)captureVideo:(SentryVideoInfo *)video + replayId:(SentryId *)replayId + segmentId:(int)segment + type:(SentryReplayType)type +{ + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId:replayId + replayStartTimestamp:video.start + replayType:type + segmentId:segment]; replayEvent.timestamp = video.end; - SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment video:video extraEvents:@[]]; - - [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:video.path]; - + SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment + video:video + extraEvents:@[]]; + + [SentrySDK.currentHub captureReplayEvent:replayEvent + replayRecording:recording + video:video.path]; + NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { - NSLog(@"[SentrySessionReplay:%d] Could not delete replay segment from disk: %@", __LINE__, error.localizedDescription); + NSLog(@"[SentrySessionReplay:%d] Could not delete replay segment from disk: %@", __LINE__, + error.localizedDescription); } } @@ -220,26 +247,39 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions selector:@selector(resume) name:UIApplicationWillEnterForegroundNotification object:nil]; - - [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId path:docs.path options:replayOptions]; + + [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId + path:docs.path + options:replayOptions]; } -- (NSURL*)replayDirectory { - NSURL *dir = [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; +- (NSURL *)replayDirectory +{ + NSURL *dir = + [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; } -- (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { - NSDictionary * info = @{ @"replayId":sessionId.sentryIdString, @"path":path.lastPathComponent, @"errorSampleRate":@(options.errorSampleRate) }; - NSData * data = [SentrySerialization dataWithJSONObject:info]; - - NSString * infoPath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"lastreplay"]; +- (void)saveCurrentSessionInfo:(SentryId *)sessionId + path:(NSString *)path + options:(SentryReplayOptions *)options +{ + NSDictionary *info = @{ + @"replayId" : sessionId.sentryIdString, + @"path" : path.lastPathComponent, + @"errorSampleRate" : @(options.errorSampleRate) + }; + NSData *data = [SentrySerialization dataWithJSONObject:info]; + + NSString *infoPath = + [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"lastreplay"]; if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) { [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil]; } [data writeToFile:infoPath atomically:YES]; - - sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"] cStringUsingEncoding:NSUTF8StringEncoding]); + + sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"] + cStringUsingEncoding:NSUTF8StringEncoding]); } - (void)stop @@ -378,8 +418,9 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:replayRecording video:videoUrl]; - - sentrySessionReplaySync_updateInfo((unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSince1970); + + sentrySessionReplaySync_updateInfo( + (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSince1970); } - (void)sessionReplayStartedWithReplayId:(SentryId *)replayId diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index 0423d5e346b..82220839d38 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -1,58 +1,67 @@ #include "SentrySessionReplaySyncC.h" +#include "SentryAsyncSafeLog.h" +#include +#include #include #include #include -#include -#include "SentryAsyncSafeLog.h" -#include #include #include static SentryCrashReplay crashReplay = { 0 }; - -void sentrySessionReplaySync_start(const char *const path) { +void +sentrySessionReplaySync_start(const char *const path) +{ crashReplay.lastSegmentEnd = 0; crashReplay.segmentId = 0; - + if (crashReplay.path != NULL) { free(crashReplay.path); } - + crashReplay.path = malloc(strlen(path)); strcpy(crashReplay.path, path); } -void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd) { +void +sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd) +{ crashReplay.segmentId = segmentId; crashReplay.lastSegmentEnd = lastSegmentEnd; } -void sentrySessionReplaySync_writeInfo(void) { +void +sentrySessionReplaySync_writeInfo(void) +{ int fd = open(crashReplay.path, O_RDWR | O_CREAT | O_TRUNC, 0644); - + if (fd < 1) { SENTRY_ASYNC_SAFE_LOG_ERROR( "Could not open replay info crash for file %s: %s", crashReplay.path, strerror(errno)); return; } - if (write(fd, &crashReplay.segmentId, sizeof(crashReplay.segmentId)) != sizeof(crashReplay.segmentId)) { - SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); - close(fd); - return; - } + if (write(fd, &crashReplay.segmentId, sizeof(crashReplay.segmentId)) + != sizeof(crashReplay.segmentId)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); + close(fd); + return; + } - if (write(fd, &crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd)) != sizeof(crashReplay.lastSegmentEnd)) { - SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); - close(fd); - return; - } + if (write(fd, &crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd)) + != sizeof(crashReplay.lastSegmentEnd)) { + SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); + close(fd); + return; + } - close(fd); + close(fd); } -bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * const path) { +bool +sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const path) +{ int fd = open(path, O_RDONLY); if (fd < 0) { SENTRY_ASYNC_SAFE_LOG_ERROR( @@ -74,7 +83,7 @@ bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * co close(fd); return false; } - + close(fd); // Assign read values to crashReplay struct or process them as needed diff --git a/Sources/Sentry/include/SentrySessionReplaySyncC.h b/Sources/Sentry/include/SentrySessionReplaySyncC.h index 3146ca0d398..faadac62a50 100644 --- a/Sources/Sentry/include/SentrySessionReplaySyncC.h +++ b/Sources/Sentry/include/SentrySessionReplaySyncC.h @@ -5,7 +5,7 @@ typedef struct { unsigned int segmentId; double lastSegmentEnd; - char* path; + char *path; } SentryCrashReplay; void sentrySessionReplaySync_start(const char *const path); @@ -14,6 +14,6 @@ void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegme void sentrySessionReplaySync_writeInfo(void); -bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char * const path); +bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const path); #endif /* SentrySessionReplaySyncC_h */ diff --git a/Sources/Sentry/include/SentryTraceContext.h b/Sources/Sentry/include/SentryTraceContext.h index 878d49038bd..9eed40b1f60 100644 --- a/Sources/Sentry/include/SentryTraceContext.h +++ b/Sources/Sentry/include/SentryTraceContext.h @@ -86,7 +86,6 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithTracer:(SentryTracer *)tracer scope:(nullable SentryScope *)scope options:(SentryOptions *)options; - /** * Initializes a SentryTraceContext with data from a traceID, options and userSegment. diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 7f1eb5635ad..1225728bf65 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -41,11 +41,11 @@ #include "SentryAsyncSafeLog.h" +#include "SentrySessionReplaySyncC.h" #include #include #include #include -#include "SentrySessionReplaySyncC.h" // ============================================================================ #pragma mark - Globals - diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 5cc632cd8ef..7d18374b881 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -44,22 +44,21 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var videoWidth = 200 var videoHeight = 434 - var videoScale : Float = 1 + var videoScale: Float = 1 var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max private var actualWidth: Int { Int(Float(videoWidth) * videoScale) } private var actualHeight: Int { Int(Float(videoHeight) * videoScale) } - - init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self._outputPath = outputPath self.dateProvider = dateProvider self.workingQueue = workingQueue } - convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider) guard let content = try? FileManager.default.contentsOfDirectory(atPath: outputPath) else { return } @@ -132,7 +131,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { }) } - var oldestFrameDate : Date? { + var oldestFrameDate: Date? { return _frames.first?.time } diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 149fa2d256e..2b217fc3d5b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -3,7 +3,7 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit -enum SessionReplayError : Error { +enum SessionReplayError: Error { case cantCreateReplayDirectory case noFramesAvailable } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index b78393b0d05..f24d73e056c 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -463,8 +463,8 @@ - (void)testIntegrationOrder { XCTAssertEqualObjects(SentryOptions.defaultIntegrations.firstObject, NSStringFromClass([SentrySessionReplayIntegration class])); - XCTAssertEqualObjects(SentryOptions.defaultIntegrations[1], - NSStringFromClass([SentryCrashIntegration class])); + XCTAssertEqualObjects( + SentryOptions.defaultIntegrations[1], NSStringFromClass([SentryCrashIntegration class])); } - (void)testSampleRateWithDict From a5f57e3fd41aed4d22654322b43236566e8463a5 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Jul 2024 16:33:38 +0200 Subject: [PATCH 08/47] lint --- .../SessionReplay/SentryOnDemandReplay.swift | 9 ++++----- .../Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 5cc632cd8ef..7d18374b881 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -44,22 +44,21 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var videoWidth = 200 var videoHeight = 434 - var videoScale : Float = 1 + var videoScale: Float = 1 var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max private var actualWidth: Int { Int(Float(videoWidth) * videoScale) } private var actualHeight: Int { Int(Float(videoHeight) * videoScale) } - - init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self._outputPath = outputPath self.dateProvider = dateProvider self.workingQueue = workingQueue } - convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { + convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider) guard let content = try? FileManager.default.contentsOfDirectory(atPath: outputPath) else { return } @@ -132,7 +131,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { }) } - var oldestFrameDate : Date? { + var oldestFrameDate: Date? { return _frames.first?.time } diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 149fa2d256e..2b217fc3d5b 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -3,7 +3,7 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit -enum SessionReplayError : Error { +enum SessionReplayError: Error { case cantCreateReplayDirectory case noFramesAvailable } From f261481baf6599e784c567ad44cc2bf8ad312c67 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Jul 2024 16:42:57 +0200 Subject: [PATCH 09/47] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2245ea9ee0d..cdeb470ac72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Replay for crashes () +- Replay for crashes (#4171) ## 8.31.1 From d01cbaa15945cb1febe2df1a1540af44d7e5647e Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 17 Jul 2024 09:20:13 +0200 Subject: [PATCH 10/47] Update SentrySessionReplayTests.swift --- .../Integrations/SessionReplay/SentrySessionReplayTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 4e9b7e6d468..adbf7d9d482 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -136,8 +136,8 @@ class SentrySessionReplayTests: XCTestCase { view.frame = CGRect(x: 0, y: 0, width: 320, height: 900) sut.start(rootView: fixture.rootView, fullSession: true) - XCTAssertEqual(Int(320 * options.sizeScale), fixture.replayMaker.videoWidth) - XCTAssertEqual(Int(900 * options.sizeScale), fixture.replayMaker.videoHeight) + XCTAssertEqual(320, fixture.replayMaker.videoWidth) + XCTAssertEqual(900, fixture.replayMaker.videoHeight) } func testSentReplay_FullSession() { From dd6958a7e896dab3e8693f1641196c18205bd2b7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 17 Jul 2024 10:02:56 +0200 Subject: [PATCH 11/47] Update SentryOptionsTest.m --- Tests/SentryTests/SentryOptionsTest.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index f24d73e056c..96eb5ba9b90 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -459,6 +459,7 @@ - (void)testDefaultIntegrations @"Default integrations are not set correctly"); } +#if SENTRY_HAS_UIKIT - (void)testIntegrationOrder { XCTAssertEqualObjects(SentryOptions.defaultIntegrations.firstObject, @@ -466,6 +467,7 @@ - (void)testIntegrationOrder XCTAssertEqualObjects( SentryOptions.defaultIntegrations[1], NSStringFromClass([SentryCrashIntegration class])); } +#endif - (void)testSampleRateWithDict { From 0a092a07df714c9b6bee9b2b6397ed41988a04bf Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 18 Jul 2024 15:47:19 +0200 Subject: [PATCH 12/47] test replayintegration --- SentryTestUtils/TestHub.swift | 8 ++ Sources/Sentry/SentryGlobalEventProcessor.m | 10 ++ .../Sentry/SentrySessionReplayIntegration.m | 50 +++++----- .../include/SentryGlobalEventProcessor.h | 2 + .../SentrySessionReplayIntegrationTests.swift | 94 +++++++++++++++++-- Tests/SentryTests/SentryClientTests.swift | 10 ++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 7 files changed, 144 insertions(+), 31 deletions(-) diff --git a/SentryTestUtils/TestHub.swift b/SentryTestUtils/TestHub.swift index f8ca3b8751a..363aa99db21 100644 --- a/SentryTestUtils/TestHub.swift +++ b/SentryTestUtils/TestHub.swift @@ -49,4 +49,12 @@ public class TestHub: SentryHub { capturedTransactionsWithScope.record((transaction.serialize(), scope)) super.capture(transaction, with: scope) } + + + public var onReplayCapture: (()->Void)? + public var capturedReplayRecordingVideo = Invocations<(replay: SentryReplayEvent, recording: SentryReplayRecording, video: URL)>() + public override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) { + capturedReplayRecordingVideo.record((replayEvent, replayRecording, videoURL)) + onReplayCapture?() + } } diff --git a/Sources/Sentry/SentryGlobalEventProcessor.m b/Sources/Sentry/SentryGlobalEventProcessor.m index a41a4995e9b..bb2b2327fbc 100644 --- a/Sources/Sentry/SentryGlobalEventProcessor.m +++ b/Sources/Sentry/SentryGlobalEventProcessor.m @@ -32,4 +32,14 @@ - (void)removeAllProcessors [self.processors removeAllObjects]; } +- (SentryEvent *)reportAll:(SentryEvent *)event { + for (SentryEventProcessor proc in self.processors) { + event = proc(event); + if (event == nil) { + return nil; + } + } + return event; +} + @end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 6222fc2ea7f..e1a5ea4b648 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -92,7 +92,6 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event SentryId *replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] : [[SentryId alloc] init]; - ; NSURL *lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; SentryCrashReplay crashInfo = { 0 }; @@ -110,33 +109,38 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event _resumeReplayMaker.videoScale = _replayOptions.sizeScale; NSDate *beginning = hasCrashInfo - ? [NSDate dateWithTimeIntervalSince1970:crashInfo.lastSegmentEnd] + ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] : [_resumeReplayMaker oldestFrameDate]; if (beginning == nil) { // no frames to send return; } - [_resumeReplayMaker - createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] - error:nil - completion:^(SentryVideoInfo *video, NSError *error) { - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - } else { - [self captureVideo:video - replayId:replayId - segmentId:segmentId - type:type]; - } - self->_resumeReplayMaker = nil; - }]; - - NSMutableDictionary *eventContext = event.context.mutableCopy; - eventContext[@"replay"] = @{ @"replay_id" : replayId.sentryIdString }; - event.context = eventContext; + NSError *replayError = nil; + + [_resumeReplayMaker createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:&replayError + completion:^(SentryVideoInfo *video, NSError *error) { + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + } else { + [self captureVideo:video + replayId:replayId + segmentId:segmentId + type:type]; + } + self->_resumeReplayMaker = nil; + }]; + + if (replayError != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", replayError); + } else { + NSMutableDictionary *eventContext = event.context.mutableCopy; + eventContext[@"replay"] = @{ @"replay_id" : replayId.sentryIdString }; + event.context = eventContext; + } } - (void)captureVideo:(SentryVideoInfo *)video @@ -420,7 +424,7 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent video:videoUrl]; sentrySessionReplaySync_updateInfo( - (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSince1970); + (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate); } - (void)sessionReplayStartedWithReplayId:(SentryId *)replayId diff --git a/Sources/Sentry/include/SentryGlobalEventProcessor.h b/Sources/Sentry/include/SentryGlobalEventProcessor.h index c2571f29f28..65f122060a3 100644 --- a/Sources/Sentry/include/SentryGlobalEventProcessor.h +++ b/Sources/Sentry/include/SentryGlobalEventProcessor.h @@ -15,6 +15,8 @@ SENTRY_NO_INIT - (void)addEventProcessor:(SentryEventProcessor)newProcessor; +- (SentryEvent *)reportAll:(SentryEvent *)event; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 5aacd3ed010..f10e1562180 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -43,15 +43,14 @@ class SentrySessionReplayIntegrationTests: XCTestCase { } private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true) { - if #available(iOS 16.0, tvOS 16.0, *) { - SentrySDK.start { - $0.dsn = "https://user@test.com/test" - $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate) - $0.setIntegrations([SentrySessionReplayIntegration.self]) - $0.enableSwizzling = enableSwizzling - } - SentrySDK.currentHub().startSession() + SentrySDK.start { + $0.dsn = "https://user@test.com/test" + $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + $0.enableSwizzling = enableSwizzling + $0.cacheDirectoryPath = FileManager.default.temporaryDirectory.path } + SentrySDK.currentHub().startSession() } func testNoInstall() { @@ -184,6 +183,85 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertEqual(sut.currentScreenNameForSessionReplay(), "Scope Screen") } + func testSessionReplayForCrash() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + + let client = SentryClient(options: try XCTUnwrap(SentrySDK.options)) + let scope = Scope() + let hub = TestHub(client: client, andScope: scope) + SentrySDK.setCurrentHub(hub) + let expectation = expectation(description: "Replay to be capture") + hub.onReplayCapture = { + expectation.fulfill() + } + + try createLastSessionReplay() + let crash = Event(error: NSError(domain: "Error", code: 1)) + crash.context = [:] + crash.isCrashEvent = true + SentryGlobalEventProcessor.shared().reportAll(crash) + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 1) + + let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) + XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.session) + XCTAssertEqual(replayInfo.recording.segmentId,2) + XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) + } + + func testBufferReplayForCrash() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + + let client = SentryClient(options: try XCTUnwrap(SentrySDK.options)) + let scope = Scope() + let hub = TestHub(client: client, andScope: scope) + SentrySDK.setCurrentHub(hub) + let expectation = expectation(description: "Replay to be capture") + hub.onReplayCapture = { + expectation.fulfill() + } + + try createLastSessionReplay(writeSessionInfo: false) + let crash = Event(error: NSError(domain: "Error", code: 1)) + crash.context = [:] + crash.isCrashEvent = true + SentryGlobalEventProcessor.shared().reportAll(crash) + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 1) + + let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) + XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.buffer) + XCTAssertEqual(replayInfo.recording.segmentId,0) + XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) + } + + func createLastSessionReplay(writeSessionInfo : Bool = true) throws { + let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay" + let jsonPath = replayFolder + "/lastreplay" + var sessionFolder = UUID().uuidString + let info : [String: Any] = ["replayId": SentryId().sentryIdString, + "path": sessionFolder, + "errorSampleRate": 1] + let data = SentrySerialization.data(withJSONObject: info) + try data?.write(to: URL(fileURLWithPath: jsonPath)) + + sessionFolder = "\(replayFolder)/\(sessionFolder)" + try FileManager.default.createDirectory(atPath: sessionFolder, withIntermediateDirectories: true) + + + for i in 5...9 { + let image = UIImage.add.jpegData(compressionQuality: 1) + try image?.write(to: URL(fileURLWithPath: "\(sessionFolder)/\(i).png") ) + } + + if writeSessionInfo { + sentrySessionReplaySync_start("\(sessionFolder)/crashInfo") + sentrySessionReplaySync_updateInfo(1, Double(4)) + sentrySessionReplaySync_writeInfo() + } + } } #endif diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 01773a26369..d187f4ab40f 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1720,6 +1720,16 @@ class SentryClientTest: XCTestCase { XCTAssertNil(replayEvent.threads) XCTAssertNil(replayEvent.debugMeta) } + + func testCaptureCrashEventSetReplayInScope() { + let sut = fixture.getSut() + let event = Event() + event.isCrashEvent = true + let scope = Scope() + event.context = ["replay": ["replay_id": "someReplay"]] + sut.captureCrash(event, with: SentrySession(releaseName: "", distinctId: ""), with: scope) + XCTAssertEqual(scope.replayId, "someReplay") + } } private extension SentryClientTest { diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index ae34d5580fc..b187a747604 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -243,3 +243,4 @@ #import "SentryCrashCachedData.h" #import "SentryCrashInstallation+Private.h" #import "SentryCrashMonitor_MachException.h" +#import "SentrySessionReplaySyncC.h" From 92078df5001dbf114af8c3dec3ebe74034fee4ee Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 18 Jul 2024 13:48:23 +0000 Subject: [PATCH 13/47] Format code --- SentryTestUtils/TestHub.swift | 3 +- Sources/Sentry/SentryGlobalEventProcessor.m | 3 +- .../Sentry/SentrySessionReplayIntegration.m | 35 ++++++++++--------- .../SentrySessionReplayIntegrationTests.swift | 9 +++-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/SentryTestUtils/TestHub.swift b/SentryTestUtils/TestHub.swift index 363aa99db21..eb6a29c6ece 100644 --- a/SentryTestUtils/TestHub.swift +++ b/SentryTestUtils/TestHub.swift @@ -50,8 +50,7 @@ public class TestHub: SentryHub { super.capture(transaction, with: scope) } - - public var onReplayCapture: (()->Void)? + public var onReplayCapture: (() -> Void)? public var capturedReplayRecordingVideo = Invocations<(replay: SentryReplayEvent, recording: SentryReplayRecording, video: URL)>() public override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) { capturedReplayRecordingVideo.record((replayEvent, replayRecording, videoURL)) diff --git a/Sources/Sentry/SentryGlobalEventProcessor.m b/Sources/Sentry/SentryGlobalEventProcessor.m index bb2b2327fbc..c284686297b 100644 --- a/Sources/Sentry/SentryGlobalEventProcessor.m +++ b/Sources/Sentry/SentryGlobalEventProcessor.m @@ -32,7 +32,8 @@ - (void)removeAllProcessors [self.processors removeAllObjects]; } -- (SentryEvent *)reportAll:(SentryEvent *)event { +- (SentryEvent *)reportAll:(SentryEvent *)event +{ for (SentryEventProcessor proc in self.processors) { event = proc(event); if (event == nil) { diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index e1a5ea4b648..0b30f68d179 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -117,23 +117,24 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event } NSError *replayError = nil; - - [_resumeReplayMaker createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] - error:&replayError - completion:^(SentryVideoInfo *video, NSError *error) { - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - } else { - [self captureVideo:video - replayId:replayId - segmentId:segmentId - type:type]; - } - self->_resumeReplayMaker = nil; - }]; - + + [_resumeReplayMaker + createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:&replayError + completion:^(SentryVideoInfo *video, NSError *error) { + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + } else { + [self captureVideo:video + replayId:replayId + segmentId:segmentId + type:type]; + } + self->_resumeReplayMaker = nil; + }]; + if (replayError != nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", replayError); } else { diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index f10e1562180..9c3a63bfffc 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -206,7 +206,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.session) - XCTAssertEqual(replayInfo.recording.segmentId,2) + XCTAssertEqual(replayInfo.recording.segmentId, 2) XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) } @@ -233,15 +233,15 @@ class SentrySessionReplayIntegrationTests: XCTestCase { let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.buffer) - XCTAssertEqual(replayInfo.recording.segmentId,0) + XCTAssertEqual(replayInfo.recording.segmentId, 0) XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) } - func createLastSessionReplay(writeSessionInfo : Bool = true) throws { + func createLastSessionReplay(writeSessionInfo: Bool = true) throws { let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay" let jsonPath = replayFolder + "/lastreplay" var sessionFolder = UUID().uuidString - let info : [String: Any] = ["replayId": SentryId().sentryIdString, + let info: [String: Any] = ["replayId": SentryId().sentryIdString, "path": sessionFolder, "errorSampleRate": 1] let data = SentrySerialization.data(withJSONObject: info) @@ -249,7 +249,6 @@ class SentrySessionReplayIntegrationTests: XCTestCase { sessionFolder = "\(replayFolder)/\(sessionFolder)" try FileManager.default.createDirectory(atPath: sessionFolder, withIntermediateDirectories: true) - for i in 5...9 { let image = UIImage.add.jpegData(compressionQuality: 1) From 488ce8272f37efb96c702422e27fcd1a52ebbe4b Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 18 Jul 2024 15:57:41 +0200 Subject: [PATCH 14/47] lint --- SentryTestUtils/TestHub.swift | 5 ++--- .../SentrySessionReplayIntegrationTests.swift | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/SentryTestUtils/TestHub.swift b/SentryTestUtils/TestHub.swift index 363aa99db21..87f1a25356d 100644 --- a/SentryTestUtils/TestHub.swift +++ b/SentryTestUtils/TestHub.swift @@ -49,9 +49,8 @@ public class TestHub: SentryHub { capturedTransactionsWithScope.record((transaction.serialize(), scope)) super.capture(transaction, with: scope) } - - - public var onReplayCapture: (()->Void)? + + public var onReplayCapture: (() -> Void)? public var capturedReplayRecordingVideo = Invocations<(replay: SentryReplayEvent, recording: SentryReplayRecording, video: URL)>() public override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) { capturedReplayRecordingVideo.record((replayEvent, replayRecording, videoURL)) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index f10e1562180..0578cc2de02 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -206,7 +206,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.session) - XCTAssertEqual(replayInfo.recording.segmentId,2) + XCTAssertEqual(replayInfo.recording.segmentId, 2) XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) } @@ -233,15 +233,15 @@ class SentrySessionReplayIntegrationTests: XCTestCase { let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first) XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.buffer) - XCTAssertEqual(replayInfo.recording.segmentId,0) + XCTAssertEqual(replayInfo.recording.segmentId, 0) XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) } - func createLastSessionReplay(writeSessionInfo : Bool = true) throws { + func createLastSessionReplay(writeSessionInfo: Bool = true) throws { let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay" let jsonPath = replayFolder + "/lastreplay" var sessionFolder = UUID().uuidString - let info : [String: Any] = ["replayId": SentryId().sentryIdString, + let info: [String: Any] = ["replayId": SentryId().sentryIdString, "path": sessionFolder, "errorSampleRate": 1] let data = SentrySerialization.data(withJSONObject: info) @@ -249,8 +249,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { sessionFolder = "\(replayFolder)/\(sessionFolder)" try FileManager.default.createDirectory(atPath: sessionFolder, withIntermediateDirectories: true) - - + for i in 5...9 { let image = UIImage.add.jpegData(compressionQuality: 1) try image?.write(to: URL(fileURLWithPath: "\(sessionFolder)/\(i).png") ) From 936f00c3e96bd563c58dcdf5e0cbed7b2cedd8b0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 10:36:57 +0200 Subject: [PATCH 15/47] Apply suggestions from code review Co-authored-by: Andrew McKnight --- Sources/Sentry/SentryGlobalEventProcessor.m | 2 +- Sources/Sentry/SentrySessionReplayIntegration.m | 11 ++++++----- Sources/Sentry/SentrySessionReplaySyncC.c | 6 +++++- Sources/Sentry/include/SentryGlobalEventProcessor.h | 2 +- .../SessionReplay/SentryOnDemandReplay.swift | 5 ++--- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/Sentry/SentryGlobalEventProcessor.m b/Sources/Sentry/SentryGlobalEventProcessor.m index c284686297b..7963173630f 100644 --- a/Sources/Sentry/SentryGlobalEventProcessor.m +++ b/Sources/Sentry/SentryGlobalEventProcessor.m @@ -32,7 +32,7 @@ - (void)removeAllProcessors [self.processors removeAllObjects]; } -- (SentryEvent *)reportAll:(SentryEvent *)event +- (nullable SentryEvent *)reportAll:(SentryEvent *)event { for (SentryEventProcessor proc in self.processors) { event = proc(event); diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 0b30f68d179..ba3ffccc350 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -137,11 +137,12 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event if (replayError != nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", replayError); - } else { - NSMutableDictionary *eventContext = event.context.mutableCopy; - eventContext[@"replay"] = @{ @"replay_id" : replayId.sentryIdString }; - event.context = eventContext; - } + return; + } + + NSMutableDictionary *eventContext = event.context.mutableCopy; + eventContext[@"replay"] = [NSDictionary dictionaryWithObjectsAndKeys: replayId.sentryIdString, @"replay_id", nil]; + event.context = eventContext; } - (void)captureVideo:(SentryVideoInfo *)video diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index 82220839d38..ff2067d5bb9 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -86,8 +86,12 @@ sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const pa close(fd); + if (lastSegmentEnd != 0) { + return false; + } + // Assign read values to crashReplay struct or process them as needed output->segmentId = segmentId; output->lastSegmentEnd = lastSegmentEnd; - return lastSegmentEnd != 0; + return true; } diff --git a/Sources/Sentry/include/SentryGlobalEventProcessor.h b/Sources/Sentry/include/SentryGlobalEventProcessor.h index 65f122060a3..be540f80b8b 100644 --- a/Sources/Sentry/include/SentryGlobalEventProcessor.h +++ b/Sources/Sentry/include/SentryGlobalEventProcessor.h @@ -15,7 +15,7 @@ SENTRY_NO_INIT - (void)addEventProcessor:(SentryEventProcessor)newProcessor; -- (SentryEvent *)reportAll:(SentryEvent *)event; +- (nullable SentryEvent *)reportAll:(SentryEvent *)event; @end diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 87919785741..2ff32090c6f 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -64,9 +64,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { guard let content = try? FileManager.default.contentsOfDirectory(atPath: outputPath) else { return } _frames = content.compactMap { - guard let extensionIndex = $0.lastIndex(of: "."), $0[extensionIndex...] == ".png" - else { return SentryReplayFrame?.none } - guard let time = Double($0[.. Date: Wed, 24 Jul 2024 08:38:18 +0000 Subject: [PATCH 16/47] Format code --- Sources/Sentry/SentrySessionReplayIntegration.m | 11 ++++++----- Sources/Sentry/SentrySessionReplaySyncC.c | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index ba3ffccc350..514afb56c78 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -138,11 +138,12 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event if (replayError != nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", replayError); return; - } - - NSMutableDictionary *eventContext = event.context.mutableCopy; - eventContext[@"replay"] = [NSDictionary dictionaryWithObjectsAndKeys: replayId.sentryIdString, @"replay_id", nil]; - event.context = eventContext; + } + + NSMutableDictionary *eventContext = event.context.mutableCopy; + eventContext[@"replay"] = + [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; + event.context = eventContext; } - (void)captureVideo:(SentryVideoInfo *)video diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index ff2067d5bb9..dc808875473 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -89,7 +89,7 @@ sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const pa if (lastSegmentEnd != 0) { return false; } - + // Assign read values to crashReplay struct or process them as needed output->segmentId = segmentId; output->lastSegmentEnd = lastSegmentEnd; From c72b8db61d6079021900e5371f5182764468cf5c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 10:46:44 +0200 Subject: [PATCH 17/47] wip --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 5 + .../Sentry/SentrySessionReplayIntegration.m | 4 +- .../SessionReplay/SentryOnDemandReplay.swift | 125 ++++++++---------- .../SentryReplayVideoMaker.swift | 5 +- .../SessionReplay/SentrySessionReplay.swift | 15 +-- .../SentryOnDemandReplayTests.swift | 31 ++++- .../SentrySessionReplayTests.swift | 22 +-- 7 files changed, 96 insertions(+), 111 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index a2d845bf219..680d302e7f6 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -20,6 +20,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DSNStorage.shared.saveDSN(dsn: dsn) SentrySDK.start(configureOptions: { options in + //... + options.experimental.sessionReplay.redactAllText = false + options.experimental.sessionReplay.redactAllImages = false + + }) options.dsn = dsn options.beforeSend = { event in return event diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 0b30f68d179..780400f19a5 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -102,7 +102,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; NSTimeInterval duration = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; - int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; + __block int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; _resumeReplayMaker.bitRate = _replayOptions.replayBitRate; @@ -121,7 +121,6 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event [_resumeReplayMaker createVideoWithBeginning:beginning end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] error:&replayError completion:^(SentryVideoInfo *video, NSError *error) { if (error != nil) { @@ -131,6 +130,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event replayId:replayId segmentId:segmentId type:type]; + segmentId += 1; } self->_resumeReplayMaker = nil; }]; diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 87919785741..d08c6add408 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -13,23 +13,14 @@ struct SentryReplayFrame { let screenName: String? } -private struct VideoFrames { - let framesPaths: [String] - let screens: [String] - let start: Date - let end: Date -} - enum SentryOnDemandReplayError: Error { case cantReadVideoSize - case assetWriterNotReady } @objcMembers class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { private let _outputPath: String - private var _currentPixelBuffer: SentryPixelBuffer? private var _totalFrames = 0 private let dateProvider: SentryCurrentDateProvider private let workingQueue: SentryDispatchQueueWrapper @@ -42,16 +33,11 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { set { _frames = newValue } } #endif // TEST || TESTCI || DEBUG - - var videoWidth = 200 - var videoHeight = 434 + var videoScale: Float = 1 var bitRate = 20_000 var frameRate = 1 var cacheMaxSize = UInt.max - - private var actualWidth: Int { Int(Float(videoWidth) * videoScale) } - private var actualHeight: Int { Int(Float(videoHeight) * videoScale) } init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self._outputPath = outputPath @@ -81,10 +67,6 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { self.init(withContentFrom: outputPath, workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil), dateProvider: SentryCurrentDateProvider()) - - guard let last = _frames.last, let image = UIImage(contentsOfFile: last.imagePath) else { return } - videoWidth = Int(image.size.width) - videoHeight = Int(image.size.height) } func addFrameAsync(image: UIImage, forScreen: String?) { @@ -136,92 +118,101 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { return _frames.first?.time } - func createVideoWith(beginning: Date, end: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { + func createVideoWith(beginning: Date, end: Date, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { var frameCount = 0 let videoFrames = filterFrames(beginning: beginning, end: end) - if videoFrames.framesPaths.isEmpty { return } - + guard let firstFrame = videoFrames.first, let image = UIImage(contentsOfFile: firstFrame.imagePath) else { return } + let videoWidth = image.size.width * CGFloat(videoScale) + let videoHeight = image.size.height * CGFloat(videoScale) + let outputFileURL = URL(fileURLWithPath: _outputPath.appending("/\(beginning.timeIntervalSinceReferenceDate).mp4")) let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mp4) - let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: createVideoSettings()) + let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: createVideoSettings(width: videoWidth, height: videoHeight)) - _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: actualWidth, height: actualHeight), videoWriterInput: videoWriterInput) + let _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight), videoWriterInput: videoWriterInput) if _currentPixelBuffer == nil { return } videoWriter.add(videoWriterInput) videoWriter.startWriting() videoWriter.startSession(atSourceTime: .zero) + var lastVideoSize: CGSize = CGSize(width: videoWidth, height: videoHeight) + var usedFrames = [SentryReplayFrame]() videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { [weak self] in guard let self = self, videoWriter.status == .writing else { videoWriter.cancelWriting() - completion(nil, SentryOnDemandReplayError.assetWriterNotReady) + completion(nil, videoWriter.error) return } - if frameCount < videoFrames.framesPaths.count { - let imagePath = videoFrames.framesPaths[frameCount] - if let image = UIImage(contentsOfFile: imagePath) { + if frameCount < videoFrames.count { + let frame = videoFrames[frameCount] + if let image = UIImage(contentsOfFile: frame.imagePath) { + if lastVideoSize != image.size { + videoWriterInput.markAsFinished() + finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) + + workingQueue.dispatchAsyncOnMainQueue { + if let previousEnd = usedFrames.min(by: { $0.time > $1.time })?.time { + try? self.createVideoWith(beginning: previousEnd.addingTimeInterval(0.5 / Double(self.frameRate)), end: end, completion: completion) + } + } + + return + } + lastVideoSize = image.size + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) - guard self._currentPixelBuffer?.append(image: image, presentationTime: presentTime) == true + guard _currentPixelBuffer?.append(image: image, presentationTime: presentTime) == true else { completion(nil, videoWriter.error) - videoWriterInput.markAsFinished() + videoWriter.cancelWriting() return } + usedFrames.append(frame) } frameCount += 1 } else { videoWriterInput.markAsFinished() - videoWriter.finishWriting { - var videoInfo: SentryVideoInfo? - if videoWriter.status == .completed { - do { - let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path) - guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else { - completion(nil, SentryOnDemandReplayError.cantReadVideoSize) - return - } - videoInfo = SentryVideoInfo(path: outputFileURL, height: self.actualHeight, width: self.actualWidth, duration: TimeInterval(videoFrames.framesPaths.count / self.frameRate), frameCount: videoFrames.framesPaths.count, frameRate: self.frameRate, start: videoFrames.start, end: videoFrames.end, fileSize: fileSize, screens: videoFrames.screens) - } catch { - completion(nil, error) - } + finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) + } + } + } + + private func finishVideo(outputFileURL: URL, usedFrames: [SentryReplayFrame], videoHeight: Int, videoWidth: Int, videoWriter: AVAssetWriter, completion: @escaping (SentryVideoInfo?, Error?) -> Void) { + videoWriter.finishWriting { + var videoInfo: SentryVideoInfo? + if videoWriter.status == .completed { + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path) + guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else { + completion(nil, SentryOnDemandReplayError.cantReadVideoSize) + return } - completion(videoInfo, videoWriter.error) + guard let start = usedFrames.min(by: { $0.time < $1.time })?.time else { return } + let duration = TimeInterval(usedFrames.count / self.frameRate) + videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration , frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName })) + } catch { + completion(nil, error) } } + completion(videoInfo, videoWriter.error) } } - private func filterFrames(beginning: Date, end: Date) -> VideoFrames { - var framesPaths = [String]() - - var screens = [String]() - - var start = dateProvider.date() - var actualEnd = start + private func filterFrames(beginning: Date, end: Date) -> [SentryReplayFrame] { + var frames = [SentryReplayFrame]() workingQueue.dispatchSync({ - for frame in self._frames { - if frame.time < beginning { continue } else if frame.time > end { break } - - if frame.time < start { start = frame.time } - - if let screenName = frame.screenName { - screens.append(screenName) - } - - actualEnd = frame.time - framesPaths.append(frame.imagePath) - } + frames = self._frames.filter { $0.time >= beginning && $0.time <= end } }) - return VideoFrames(framesPaths: framesPaths, screens: screens, start: start, end: actualEnd + TimeInterval((1 / Double(frameRate)))) + return frames } - private func createVideoSettings() -> [String: Any] { + private func createVideoSettings(width: CGFloat, height: CGFloat) -> [String: Any] { return [ AVVideoCodecKey: AVVideoCodecType.h264, - AVVideoWidthKey: actualWidth, - AVVideoHeightKey: actualHeight, + AVVideoWidthKey: width, + AVVideoHeightKey: height, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: bitRate, AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift index 2df207a6602..4c5f4a4b8f2 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift @@ -4,12 +4,9 @@ import UIKit @objc protocol SentryReplayVideoMaker: NSObjectProtocol { - var videoWidth: Int { get set } - var videoHeight: Int { get set } - func addFrameAsync(image: UIImage, forScreen: String?) func releaseFramesUntil(_ date: Date) - func createVideoWith(beginning: Date, end: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws + func createVideoWith(beginning: Date, end: Date, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws } extension SentryReplayVideoMaker { diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index a797906c59c..91989f17577 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -82,8 +82,6 @@ class SentrySessionReplay: NSObject { videoSegmentStart = nil currentSegmentId = 0 sessionReplayId = SentryId() - replayMaker.videoWidth = Int(rootView.frame.size.width) - replayMaker.videoHeight = Int(rootView.frame.size.height) imageCollection = [] if fullSession { @@ -148,14 +146,9 @@ class SentrySessionReplay: NSObject { } startFullReplay() - - guard let finalPath = urlToCache?.appendingPathComponent("replay.mp4") else { - print("[SentrySessionReplay:\(#line)] Could not create replay video path") - return false - } let replayStart = dateProvider.date().addingTimeInterval(-replayOptions.errorReplayDuration - (Double(replayOptions.frameRate) / 2.0)) - createAndCapture(videoUrl: finalPath, startedAt: replayStart) + createAndCapture(startedAt: replayStart) return true } @@ -215,12 +208,12 @@ class SentrySessionReplay: NSObject { pathToSegment = pathToSegment.appendingPathComponent("\(currentSegmentId).mp4") let segmentStart = videoSegmentStart ?? dateProvider.date().addingTimeInterval(-replayOptions.sessionSegmentDuration) - createAndCapture(videoUrl: pathToSegment, startedAt: segmentStart) + createAndCapture(startedAt: segmentStart) } - private func createAndCapture(videoUrl: URL, startedAt: Date) { + private func createAndCapture(startedAt: Date) { do { - try replayMaker.createVideoWith(beginning: startedAt, end: dateProvider.date(), outputFileURL: videoUrl) { [weak self] videoInfo, error in + try replayMaker.createVideoWith(beginning: startedAt, end: dateProvider.date()) { [weak self] videoInfo, error in guard let _self = self else { return } if let error = error { print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index 07b166bf893..d3882b274e1 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -7,7 +7,18 @@ import XCTest class SentryOnDemandReplayTests: XCTestCase { let dateProvider = TestCurrentDateProvider() - let outputPath = FileManager.default.temporaryDirectory + var outputPath : URL = { + let temp = FileManager.default.temporaryDirectory.appendingPathComponent("replayTest") + try? FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true) + return temp + }() + + override func tearDownWithError() throws { + let files = try FileManager.default.contentsOfDirectory(atPath: outputPath.path) + for file in files { + try? FileManager.default.removeItem(at: outputPath.appendingPathComponent(file)) + } + } func getSut() -> SentryOnDemandReplay { let sut = SentryOnDemandReplay(outputPath: outputPath.path, @@ -72,19 +83,23 @@ class SentryOnDemandReplayTests: XCTestCase { sut.addFrameAsync(image: UIImage.add) } - let output = FileManager.default.temporaryDirectory.appendingPathComponent("video.mp4") let videoExpectation = expectation(description: "Wait for video render") - try? sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10), outputFileURL: output) { info, error in + try? sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) { info, error in XCTAssertNil(error) XCTAssertEqual(info?.duration, 10) XCTAssertEqual(info?.start, Date(timeIntervalSinceReferenceDate: 0)) XCTAssertEqual(info?.end, Date(timeIntervalSinceReferenceDate: 10)) - XCTAssertEqual(FileManager.default.fileExists(atPath: output.path), true) + guard let videoPath = info?.path else { + XCTFail("No video path for replay") + return + } + + XCTAssertEqual(FileManager.default.fileExists(atPath: videoPath.path), true) videoExpectation.fulfill() - try? FileManager.default.removeItem(at: output) + try? FileManager.default.removeItem(at: videoPath) } wait(for: [videoExpectation], timeout: 1) @@ -149,9 +164,11 @@ class SentryOnDemandReplayTests: XCTestCase { dateProvider.advance(by: 1) let end = dateProvider.date() - try? sut.createVideoWith(beginning: start, end: end, outputFileURL: URL(fileURLWithPath: "/invalidPath/video.mp3")) { _, error in + //Creating a file where the replay would be written to cause an error in the writer + try? "tempFile".data(using: .utf8)?.write(to: outputPath.appendingPathComponent("0.0.mp4")) + + try? sut.createVideoWith(beginning: start, end: end) { _, error in XCTAssertNotNil(error) - XCTAssertEqual(error as? SentryOnDemandReplayError, SentryOnDemandReplayError.assetWriterNotReady) expect.fulfill() } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index adbf7d9d482..f97da17c928 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -15,24 +15,20 @@ class SentrySessionReplayTests: XCTestCase { } private class TestReplayMaker: NSObject, SentryReplayVideoMaker { - var videoWidth: Int = 0 - var videoHeight: Int = 0 - var screens = [String]() struct CreateVideoCall { var beginning: Date var end: Date - var outputFileURL: URL var completion: ((Sentry.SentryVideoInfo?, Error?) -> Void) } var lastCallToCreateVideo: CreateVideoCall? - func createVideoWith(beginning: Date, end: Date, outputFileURL: URL, completion: @escaping (Sentry.SentryVideoInfo?, (Error)?) -> Void) throws { + func createVideoWith(beginning: Date, end: Date, completion: @escaping (Sentry.SentryVideoInfo?, (Error)?) -> Void) throws { lastCallToCreateVideo = CreateVideoCall(beginning: beginning, end: end, - outputFileURL: outputFileURL, completion: completion) + let outputFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("tempvideo.mp4") try? "Video Data".write(to: outputFileURL, atomically: true, encoding: .utf8) @@ -128,18 +124,6 @@ class SentrySessionReplayTests: XCTestCase { XCTAssertNil(fixture.lastReplayEvent) } - func testVideoSize() { - let fixture = Fixture() - let options = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1) - let sut = fixture.getSut(options: options) - let view = fixture.rootView - view.frame = CGRect(x: 0, y: 0, width: 320, height: 900) - sut.start(rootView: fixture.rootView, fullSession: true) - - XCTAssertEqual(320, fixture.replayMaker.videoWidth) - XCTAssertEqual(900, fixture.replayMaker.videoHeight) - } - func testSentReplay_FullSession() { let fixture = Fixture() @@ -162,10 +146,8 @@ class SentrySessionReplayTests: XCTestCase { XCTAssertEqual(videoArguments.end, startEvent.addingTimeInterval(5)) XCTAssertEqual(videoArguments.beginning, startEvent) - XCTAssertEqual(videoArguments.outputFileURL, fixture.cacheFolder.appendingPathComponent("segments/0.mp4")) XCTAssertNotNil(fixture.lastReplayRecording) - XCTAssertEqual(fixture.lastVideoUrl, videoArguments.outputFileURL) assertFullSession(sut, expected: true) } From 44f43436099b10fff807358594cc3dc31fd23de0 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 12:36:47 +0200 Subject: [PATCH 18/47] some fixes --- .../Sentry/SentrySessionReplayIntegration.m | 52 ++++++++++--------- Sources/Sentry/SentrySessionReplaySyncC.c | 16 +++--- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 514afb56c78..e143df70d36 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -85,10 +85,15 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event if (lastReplay == nil) { return; } - + NSError *error = nil; NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay options:0 - error:nil]; + error:&error]; + if (jsonObject == nil) { + SENTRY_LOG_DEBUG(@"Can't open last session replay: %@", error); + return; + } + SentryId *replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] : [[SentryId alloc] init]; @@ -111,32 +116,29 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event NSDate *beginning = hasCrashInfo ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] : [_resumeReplayMaker oldestFrameDate]; + if (beginning == nil) { - // no frames to send - return; + return; // no frames to send } - NSError *replayError = nil; - - [_resumeReplayMaker - createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] - error:&replayError - completion:^(SentryVideoInfo *video, NSError *error) { - if (error != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - } else { - [self captureVideo:video - replayId:replayId - segmentId:segmentId - type:type]; - } - self->_resumeReplayMaker = nil; - }]; - - if (replayError != nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", replayError); + if (![_resumeReplayMaker + createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] + error:&error + completion:^(SentryVideoInfo *video, NSError *renderError) { + if (renderError != nil) { + SENTRY_LOG_ERROR( + @"Could not create replay video: %@", renderError); + } else { + [self captureVideo:video + replayId:replayId + segmentId:segmentId + type:type]; + } + self->_resumeReplayMaker = nil; + }]) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); return; } diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c index dc808875473..952a38f3b6b 100644 --- a/Sources/Sentry/SentrySessionReplaySyncC.c +++ b/Sources/Sentry/SentrySessionReplaySyncC.c @@ -1,5 +1,6 @@ #include "SentrySessionReplaySyncC.h" #include "SentryAsyncSafeLog.h" +#include #include #include #include @@ -42,15 +43,15 @@ sentrySessionReplaySync_writeInfo(void) return; } - if (write(fd, &crashReplay.segmentId, sizeof(crashReplay.segmentId)) - != sizeof(crashReplay.segmentId)) { + if (!sentrycrashfu_writeBytesToFD( + fd, (char *)&crashReplay.segmentId, sizeof(crashReplay.segmentId))) { SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); close(fd); return; } - if (write(fd, &crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd)) - != sizeof(crashReplay.lastSegmentEnd)) { + if (!sentrycrashfu_writeBytesToFD( + fd, (char *)&crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd))) { SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash."); close(fd); return; @@ -72,13 +73,13 @@ sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const pa unsigned int segmentId = 0; double lastSegmentEnd = 0; - if (read(fd, &segmentId, sizeof(segmentId)) != sizeof(segmentId)) { + if (!sentrycrashfu_readBytesFromFD(fd, (char *)&segmentId, sizeof(segmentId))) { SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading segmentId from replay info crash file."); close(fd); return false; } - if (read(fd, &lastSegmentEnd, sizeof(lastSegmentEnd)) != sizeof(lastSegmentEnd)) { + if (!sentrycrashfu_readBytesFromFD(fd, (char *)&lastSegmentEnd, sizeof(lastSegmentEnd))) { SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading lastSegmentEnd from replay info crash file."); close(fd); return false; @@ -86,11 +87,10 @@ sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const pa close(fd); - if (lastSegmentEnd != 0) { + if (lastSegmentEnd == 0) { return false; } - // Assign read values to crashReplay struct or process them as needed output->segmentId = segmentId; output->lastSegmentEnd = lastSegmentEnd; return true; From d3c999891a2dae76a1b75756da3081eaba7e55d5 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 13:24:22 +0200 Subject: [PATCH 19/47] refs --- Sources/Sentry/SentrySessionReplayIntegration.m | 10 +++++----- .../SessionReplay/SentryOnDemandReplay.swift | 17 +++++++++++------ .../SessionReplay/SentrySessionReplay.swift | 6 ++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index e143df70d36..ecdac195b59 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -273,11 +273,11 @@ - (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { - NSDictionary *info = @{ - @"replayId" : sessionId.sentryIdString, - @"path" : path.lastPathComponent, - @"errorSampleRate" : @(options.errorSampleRate) - }; + NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys: + sessionId.sentryIdString, @"replayId", + path.lastPathComponent, @"path", + @(options.errorSampleRate), @"errorSampleRate", nil]; + NSData *data = [SentrySerialization dataWithJSONObject:info]; NSString *infoPath = diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 2ff32090c6f..dd08caa4e28 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -61,13 +61,18 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider) - guard let content = try? FileManager.default.contentsOfDirectory(atPath: outputPath) else { return } - _frames = content.compactMap { - guard $0.hasSuffix(".png") else { return SentryReplayFrame?.none } - guard let time = Double($0.dropLast(4)) else { return nil } - return SentryReplayFrame(imagePath: "\(outputPath)/\($0)", time: Date(timeIntervalSinceReferenceDate: time), screenName: nil) - }.sorted { $0.time < $1.time } + do { + let content = try FileManager.default.contentsOfDirectory(atPath: outputPath) + _frames = content.compactMap { + guard $0.hasSuffix(".png") else { return SentryReplayFrame?.none } + guard let time = Double($0.dropLast(4)) else { return nil } + return SentryReplayFrame(imagePath: "\(outputPath)/\($0)", time: Date(timeIntervalSinceReferenceDate: time), screenName: nil) + }.sorted { $0.time < $1.time } + } catch { + print("[SentryOnDemandReplay:\(#line)] Could not list frames from replay: \(error.localizedDescription)") + return + } } convenience init(outputPath: String) { diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index a797906c59c..a72aeddfa70 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -293,8 +293,10 @@ class SentrySessionReplay: NSObject { } private func newImage(image: UIImage, forScreen screen: String?) { - processingScreenshot = false - replayMaker.addFrameAsync(image: image, forScreen: screen) + lock.synchronized { + processingScreenshot = false + replayMaker.addFrameAsync(image: image, forScreen: screen) + } } } From ebb769a636dc6fb254fe094fa9b13045ba2e7cc6 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 24 Jul 2024 11:25:49 +0000 Subject: [PATCH 20/47] Format code --- Sources/Sentry/SentrySessionReplayIntegration.m | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index ecdac195b59..1f17a83df0f 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -273,11 +273,10 @@ - (void)saveCurrentSessionInfo:(SentryId *)sessionId path:(NSString *)path options:(SentryReplayOptions *)options { - NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys: - sessionId.sentryIdString, @"replayId", - path.lastPathComponent, @"path", - @(options.errorSampleRate), @"errorSampleRate", nil]; - + NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, + @"replayId", path.lastPathComponent, @"path", + @(options.errorSampleRate), @"errorSampleRate", nil]; + NSData *data = [SentrySerialization dataWithJSONObject:info]; NSString *infoPath = From 1c27e317b7dcb753cfb1277df02306ecad928d00 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 15:49:56 +0200 Subject: [PATCH 21/47] respect sample rate --- .../Sentry/SentrySessionReplayIntegration.m | 7 +++++ .../SentrySessionReplayIntegrationTests.swift | 27 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 1f17a83df0f..93eec414229 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -109,6 +109,13 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; + if (type == SentryReplayTypeBuffer) { + float errorSampleRate = [jsonObject[@"errorSampleRate"] floatValue]; + if ([SentryDependencyContainer.sharedInstance.random nextNumber] >= errorSampleRate) { + return; + } + } + _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; _resumeReplayMaker.bitRate = _replayOptions.replayBitRate; _resumeReplayMaker.videoScale = _replayOptions.sizeScale; diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 0578cc2de02..d330daa7e58 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -237,13 +237,36 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5)) } - func createLastSessionReplay(writeSessionInfo: Bool = true) throws { + func testBufferReplayIgnoredBecauseSampleRateForCrash() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 1) + + let client = SentryClient(options: try XCTUnwrap(SentrySDK.options)) + let scope = Scope() + let hub = TestHub(client: client, andScope: scope) + SentrySDK.setCurrentHub(hub) + let expectation = expectation(description: "Replay to be capture") + expectation.isInverted = true + hub.onReplayCapture = { + expectation.fulfill() + } + + try createLastSessionReplay(writeSessionInfo: false, errorSampleRate: 0) + let crash = Event(error: NSError(domain: "Error", code: 1)) + crash.context = [:] + crash.isCrashEvent = true + SentryGlobalEventProcessor.shared().reportAll(crash) + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 0) + } + + func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws { let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay" let jsonPath = replayFolder + "/lastreplay" var sessionFolder = UUID().uuidString let info: [String: Any] = ["replayId": SentryId().sentryIdString, "path": sessionFolder, - "errorSampleRate": 1] + "errorSampleRate": errorSampleRate] let data = SentrySerialization.data(withJSONObject: info) try data?.write(to: URL(fileURLWithPath: jsonPath)) From d83f4eec29a87d8b6037243d329a8e544ab45b28 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 16:42:56 +0200 Subject: [PATCH 22/47] Update SentryOnDemandReplayTests.swift --- .../SentryOnDemandReplayTests.swift | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index d3882b274e1..788f3ffa301 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -20,9 +20,9 @@ class SentryOnDemandReplayTests: XCTestCase { } } - func getSut() -> SentryOnDemandReplay { - let sut = SentryOnDemandReplay(outputPath: outputPath.path, - workingQueue: TestSentryDispatchQueueWrapper(), + func getSut(trueDispatchQueueWrapper : Bool = false) -> SentryOnDemandReplay { + let sut = SentryOnDemandReplay(outputPath: outputPath.path, + workingQueue: trueDispatchQueueWrapper ? SentryDispatchQueueWrapper() : TestSentryDispatchQueueWrapper(), dateProvider: dateProvider) return sut } @@ -175,5 +175,52 @@ class SentryOnDemandReplayTests: XCTestCase { wait(for: [expect], timeout: 1) } + func testGenerateVideoForEachSize() throws { + let sut = getSut(trueDispatchQueueWrapper: true) + dateProvider.driftTimeForEveryRead = true + dateProvider.driftTimeInterval = 1 + + let image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 10)).image { context in + } + + for i in 0..<10 { + sut.addFrameAsync(image: i < 5 ? UIImage.add : image) + } + + let videoExpectation = expectation(description: "Wait for video render") + videoExpectation.expectedFulfillmentCount = 2 + + var videos = [SentryVideoInfo]() + + try? sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) { info, error in + XCTAssertNil(error) + guard let info = info else { return } + videos.append(info) + videoExpectation.fulfill() + } + + wait(for: [videoExpectation], timeout: 1) + + XCTAssertEqual(videos.count, 2) + + let firstVideo = try XCTUnwrap(videos.first) + let secondVideo = try XCTUnwrap(videos.last) + + XCTAssertEqual(firstVideo.duration, 5) + XCTAssertEqual(secondVideo.duration, 5) + + XCTAssertEqual(firstVideo.start, Date(timeIntervalSinceReferenceDate: 0)) + XCTAssertEqual(secondVideo.start, Date(timeIntervalSinceReferenceDate: 5)) + + XCTAssertEqual(firstVideo.end, Date(timeIntervalSinceReferenceDate: 5)) + XCTAssertEqual(secondVideo.end, Date(timeIntervalSinceReferenceDate: 10)) + + XCTAssertEqual(firstVideo.width, 20) + XCTAssertEqual(firstVideo.height, 19) + + XCTAssertEqual(secondVideo.width, 20) + XCTAssertEqual(secondVideo.height, 10) + } + } #endif From ae028411c3f66c6d9c2d387d8fb4e0105fc9074e Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 24 Jul 2024 16:57:13 +0200 Subject: [PATCH 23/47] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f7468dd58..0a0273dae6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Support orientation change for session replay (#4194) + ## 8.32.0 ### Features @@ -9,6 +15,7 @@ ### Fixes - Session replay crash when writing the replay (#4186) + ### Features - Replay for crashes (#4171) From e764f02f0fdc44070549972c6cb9e0b527bad02d Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 24 Jul 2024 14:58:33 +0000 Subject: [PATCH 24/47] Format code --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../SessionReplay/SentryOnDemandReplayTests.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index d08c6add408..3f2ee289589 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -191,7 +191,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } guard let start = usedFrames.min(by: { $0.time < $1.time })?.time else { return } let duration = TimeInterval(usedFrames.count / self.frameRate) - videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration , frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName })) + videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration, frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName })) } catch { completion(nil, error) } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index 788f3ffa301..ec718cae832 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -7,7 +7,7 @@ import XCTest class SentryOnDemandReplayTests: XCTestCase { let dateProvider = TestCurrentDateProvider() - var outputPath : URL = { + var outputPath: URL = { let temp = FileManager.default.temporaryDirectory.appendingPathComponent("replayTest") try? FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true) return temp @@ -20,7 +20,7 @@ class SentryOnDemandReplayTests: XCTestCase { } } - func getSut(trueDispatchQueueWrapper : Bool = false) -> SentryOnDemandReplay { + func getSut(trueDispatchQueueWrapper: Bool = false) -> SentryOnDemandReplay { let sut = SentryOnDemandReplay(outputPath: outputPath.path, workingQueue: trueDispatchQueueWrapper ? SentryDispatchQueueWrapper() : TestSentryDispatchQueueWrapper(), dateProvider: dateProvider) @@ -180,7 +180,7 @@ class SentryOnDemandReplayTests: XCTestCase { dateProvider.driftTimeForEveryRead = true dateProvider.driftTimeInterval = 1 - let image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 10)).image { context in + let image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 10)).image { _ in } for i in 0..<10 { From 357f3e903c8e9b112dce891880a9bb1a46eeb02c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 10:47:33 +0200 Subject: [PATCH 25/47] session reset --- Sources/Sentry/SentrySessionReplayIntegration.m | 9 ++++----- .../SentrySessionReplayIntegrationTests.swift | 12 ++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 780400f19a5..d6bccc27db0 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -164,13 +164,15 @@ - (void)captureVideo:(SentryVideoInfo *)video NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { - NSLog(@"[SentrySessionReplay:%d] Could not delete replay segment from disk: %@", __LINE__, - error.localizedDescription); + SENTRY_LOG_DEBUG( + @"Could not delete replay segment from disk: %@", error.localizedDescription); } } - (void)startSession { + [self.sessionReplay stop]; + _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; if (!_startedAsFullSession && _replayOptions.errorSampleRate == 0) { @@ -311,9 +313,6 @@ - (void)sentrySessionEnded:(SentrySession *)session - (void)sentrySessionStarted:(SentrySession *)session { - if (_sessionReplay) { - return; - } [self startSession]; } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 0578cc2de02..e034283f755 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -164,6 +164,18 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNotNil(sut.sessionReplay) } + func testRestartReplayWithNewSessionClosePreviusReplay() throws { + startSDK(sessionSampleRate: 1, errorSampleRate: 0) + + let sut = try getSut() + SentrySDK.currentHub().startSession() + XCTAssertNotNil(sut.sessionReplay) + let oldSessionReplay = sut.sessionReplay + XCTAssertTrue(oldSessionReplay?.isRunning ?? false) + SentrySDK.currentHub().startSession() + XCTAssertFalse(oldSessionReplay?.isRunning ?? true) + } + func testScreenNameFromSentryUIApplication() throws { startSDK(sessionSampleRate: 1, errorSampleRate: 1) let sut: SentrySessionReplayDelegate = try getSut() From cd1661d41bc898a00e271cc22b192229fe740d61 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 10:51:29 +0200 Subject: [PATCH 26/47] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e63fee4b8fd..68b2db23462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,12 @@ ### Features - Support orientation change for session replay (#4194) +- Replay for crashes (#4171) ## 8.32.0 ### Features -- Replay for crashes (#4171) - Record dropped spans (#4172) ### Fixes From 72aa5ef695e5fca646979a71f7d529729df0b275 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 10:57:17 +0200 Subject: [PATCH 27/47] Update SentrySessionReplayIntegration.m --- Sources/Sentry/SentrySessionReplayIntegration.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 73581787171..9af5d7462e4 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -131,7 +131,6 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event if (![_resumeReplayMaker createVideoWithBeginning:beginning end:[beginning dateByAddingTimeInterval:duration] - outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"] error:&error completion:^(SentryVideoInfo *video, NSError *renderError) { if (renderError != nil) { From d4293161b99f881dab2f271cd4c5df08409dcacf Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 11:02:34 +0200 Subject: [PATCH 28/47] Update AppDelegate.swift --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 62cf0c02d28..506cd695acf 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -20,11 +20,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DSNStorage.shared.saveDSN(dsn: dsn) SentrySDK.start(configureOptions: { options in - //... - options.experimental.sessionReplay.redactAllText = false - options.experimental.sessionReplay.redactAllImages = false - - }) options.dsn = dsn options.beforeSend = { event in return event From db270a33689a2f618c76bc0e3f943b378efd4e71 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 13:30:33 +0200 Subject: [PATCH 29/47] Update SentryOnDemandReplay.swift --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index a28edc6d8ae..f486a0804c6 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -139,7 +139,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { videoWriter.startWriting() videoWriter.startSession(atSourceTime: .zero) - var lastVideoSize: CGSize = CGSize(width: videoWidth, height: videoHeight) + var lastImageSize: CGSize = image.size var usedFrames = [SentryReplayFrame]() videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { [weak self] in guard let self = self, videoWriter.status == .writing else { @@ -151,7 +151,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { if frameCount < videoFrames.count { let frame = videoFrames[frameCount] if let image = UIImage(contentsOfFile: frame.imagePath) { - if lastVideoSize != image.size { + if lastImageSize != image.size { videoWriterInput.markAsFinished() finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) @@ -163,7 +163,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { return } - lastVideoSize = image.size + lastImageSize = image.size let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) From 5355385ba099eaac152e0abc78786873e4ba6541 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 13:52:58 +0200 Subject: [PATCH 30/47] ref --- CHANGELOG.md | 7 ++++++- Sources/Sentry/SentryHttpTransport.m | 2 +- Sources/Sentry/SentryHub.m | 2 +- Sources/Sentry/SentrySerialization.m | 6 +++--- Sources/Sentry/SentrySessionReplayIntegration.m | 12 ++++-------- Sources/Sentry/include/SentrySerialization.h | 2 +- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc968cea37..0f0b7f900c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Changelog -## 8.32.0 +## Unreleased ### Features - Replay for crashes (#4171) + +## 8.32.0 + +### Features + - Record dropped spans (#4172) ### Fixes diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 612ec264ca0..118e0de13e3 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -408,7 +408,7 @@ - (void)recordLostSpans:(SentryEnvelopeItem *)envelopeItem reason:(SentryDiscard { if ([SentryEnvelopeItemTypeTransaction isEqualToString:envelopeItem.header.type]) { NSDictionary *transactionJson = - [SentrySerialization deserializeEventEnvelopeItem:envelopeItem.data]; + [SentrySerialization deserializeDictionaryFromJsonData:envelopeItem.data]; if (transactionJson == nil) { return; } diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index e7a3c14baac..b8718088c4d 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -705,7 +705,7 @@ - (BOOL)envelopeContainsEventWithErrorOrHigher:(NSArray *) for (SentryEnvelopeItem *item in items) { if ([item.header.type isEqualToString:SentryEnvelopeItemTypeEvent]) { // If there is no level the default is error - NSDictionary *eventJson = [SentrySerialization deserializeEventEnvelopeItem:item.data]; + NSDictionary *eventJson = [SentrySerialization deserializeDictionaryFromJsonData:item.data]; if (eventJson == nil) { return NO; } diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m index 418f101dbac..49198cec6ba 100644 --- a/Sources/Sentry/SentrySerialization.m +++ b/Sources/Sentry/SentrySerialization.m @@ -297,16 +297,16 @@ + (SentryAppState *_Nullable)appStateWithData:(NSData *)data return [[SentryAppState alloc] initWithJSONObject:appSateDictionary]; } -+ (NSDictionary *)deserializeEventEnvelopeItem:(NSData *)eventEnvelopeItemData ++ (NSDictionary *)deserializeDictionaryFromJsonData:(NSData *)data { NSError *error = nil; - NSDictionary *eventDictionary = [NSJSONSerialization JSONObjectWithData:eventEnvelopeItemData + NSDictionary *eventDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (nil != error) { [SentryLog logWithMessage:[NSString - stringWithFormat:@"Failed to deserialize envelope item data: %@", + stringWithFormat:@"Failed to deserialize json item dictionary: %@", error] andLevel:kSentryLevelError]; } diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 93eec414229..005fd2ccd1c 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -85,14 +85,9 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event if (lastReplay == nil) { return; } - NSError *error = nil; - NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:lastReplay - options:0 - error:&error]; - if (jsonObject == nil) { - SENTRY_LOG_DEBUG(@"Can't open last session replay: %@", error); - return; - } + + NSDictionary *jsonObject = [SentrySerialization deserializeDictionaryFromJsonData:lastReplay]; + if (jsonObject == nil) { return; } SentryId *replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] @@ -128,6 +123,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; // no frames to send } + NSError * error; if (![_resumeReplayMaker createVideoWithBeginning:beginning end:[beginning dateByAddingTimeInterval:duration] diff --git a/Sources/Sentry/include/SentrySerialization.h b/Sources/Sentry/include/SentrySerialization.h index a5149e3ce64..32c4873fd43 100644 --- a/Sources/Sentry/include/SentrySerialization.h +++ b/Sources/Sentry/include/SentrySerialization.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Retrieves the json object from an event envelope item data. */ -+ (NSDictionary *)deserializeEventEnvelopeItem:(NSData *)eventEnvelopeItemData; ++ (NSDictionary *)deserializeDictionaryFromJsonData:(NSData *)data; /** * Extract the level from data of an envelopte item containing an event. Default is the 'error' From 6b9f527dd28052e5cde9ead664bbbd76b8eb1235 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 25 Jul 2024 11:53:57 +0000 Subject: [PATCH 31/47] Format code --- Sources/Sentry/SentryHub.m | 3 ++- Sources/Sentry/SentrySessionReplayIntegration.m | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index b8718088c4d..7c317152f81 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -705,7 +705,8 @@ - (BOOL)envelopeContainsEventWithErrorOrHigher:(NSArray *) for (SentryEnvelopeItem *item in items) { if ([item.header.type isEqualToString:SentryEnvelopeItemTypeEvent]) { // If there is no level the default is error - NSDictionary *eventJson = [SentrySerialization deserializeDictionaryFromJsonData:item.data]; + NSDictionary *eventJson = + [SentrySerialization deserializeDictionaryFromJsonData:item.data]; if (eventJson == nil) { return NO; } diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 005fd2ccd1c..31034eab69e 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -86,8 +86,11 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; } - NSDictionary *jsonObject = [SentrySerialization deserializeDictionaryFromJsonData:lastReplay]; - if (jsonObject == nil) { return; } + NSDictionary *jsonObject = + [SentrySerialization deserializeDictionaryFromJsonData:lastReplay]; + if (jsonObject == nil) { + return; + } SentryId *replayId = jsonObject[@"replayId"] ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] @@ -123,7 +126,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; // no frames to send } - NSError * error; + NSError *error; if (![_resumeReplayMaker createVideoWithBeginning:beginning end:[beginning dateByAddingTimeInterval:duration] From 0860e15e079c803d862dcd3fcde6e4fbb3306a5f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 13:57:25 +0200 Subject: [PATCH 32/47] Update SentryOnDemandReplayTests.swift --- .../SessionReplay/SentryOnDemandReplayTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index ec718cae832..4e80a445894 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -180,11 +180,11 @@ class SentryOnDemandReplayTests: XCTestCase { dateProvider.driftTimeForEveryRead = true dateProvider.driftTimeInterval = 1 - let image = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 10)).image { _ in - } + let image1 = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 19)).image { _ in } + let image2 = UIGraphicsImageRenderer(size: CGSize(width: 20, height: 10)).image { _ in } for i in 0..<10 { - sut.addFrameAsync(image: i < 5 ? UIImage.add : image) + sut.addFrameAsync(image: i < 5 ? image1 : image2) } let videoExpectation = expectation(description: "Wait for video render") From 49fa6abc50cb4a2ed29eb430c76d0486207df2d7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 25 Jul 2024 14:18:12 +0200 Subject: [PATCH 33/47] Update SentryOnDemandReplay.swift --- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index f486a0804c6..ac85b33c601 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -153,9 +153,9 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { if let image = UIImage(contentsOfFile: frame.imagePath) { if lastImageSize != image.size { videoWriterInput.markAsFinished() - finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) + self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) - workingQueue.dispatchAsyncOnMainQueue { + self.workingQueue.dispatchAsyncOnMainQueue { if let previousEnd = usedFrames.min(by: { $0.time > $1.time })?.time { try? self.createVideoWith(beginning: previousEnd.addingTimeInterval(0.5 / Double(self.frameRate)), end: end, completion: completion) } @@ -178,7 +178,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { frameCount += 1 } else { videoWriterInput.markAsFinished() - finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) + self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) } } } From 98d477f8da6517ff50644785920a88b3927c058e Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 26 Jul 2024 14:00:37 +0200 Subject: [PATCH 34/47] ondemand replay improvement --- Sentry.xcodeproj/project.pbxproj | 4 + .../Sentry/SentrySessionReplayIntegration.m | 53 ++++---- .../SessionReplay/SentryOnDemandReplay.swift | 128 ++++++++++++------ .../SentryReplayVideoMaker.swift | 2 +- .../SessionReplay/SentrySessionReplay.swift | 18 +-- .../SessionReplay/VideoRenderer.swift | 11 ++ .../SentryOnDemandReplayTests.swift | 61 +++------ .../SentrySessionReplayTests.swift | 10 +- 8 files changed, 161 insertions(+), 126 deletions(-) create mode 100644 Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 61b33b538ba..65e15e4c7a1 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -869,6 +869,7 @@ D8ACE3CD2762187D00F5A213 /* SentryNSDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */; }; D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; + D8AE48672C527D730092A2A6 /* VideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48662C527D730092A2A6 /* VideoRenderer.swift */; }; D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; }; D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; }; D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; }; @@ -1929,6 +1930,7 @@ D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataSwizzling.h; path = include/SentryNSDataSwizzling.h; sourceTree = ""; }; D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; + D8AE48662C527D730092A2A6 /* VideoRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRenderer.swift; sourceTree = ""; }; D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = ""; }; D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = ""; }; D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = ""; }; @@ -3853,6 +3855,7 @@ D84D2CC22C29AD120011AF8A /* SentrySessionReplay.swift */, D84D2CDC2C2BF7370011AF8A /* SentryReplayEvent.swift */, D84D2CDE2C2BF9370011AF8A /* SentryReplayType.swift */, + D8AE48662C527D730092A2A6 /* VideoRenderer.swift */, ); path = SessionReplay; sourceTree = ""; @@ -4673,6 +4676,7 @@ 63FE714120DA4C1100CDBAE8 /* SentryCrashDate.c in Sources */, 63FE70DB20DA4C1000CDBAE8 /* SentryCrashMonitor_System.m in Sources */, 7BA61CBB247BC5D800C130A8 /* SentryCrashDefaultBinaryImageProvider.m in Sources */, + D8AE48672C527D730092A2A6 /* VideoRenderer.swift in Sources */, 63FE713120DA4C1100CDBAE8 /* SentryCrashDynamicLinker.c in Sources */, 8E25C95325F836D000DC215B /* SentryRandom.m in Sources */, 7BC85231245812EC005A70F0 /* SentryFileContents.m in Sources */, diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 9a954e47827..90bf11813e7 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -4,6 +4,7 @@ # import "SentryClient+Private.h" # import "SentryDependencyContainer.h" +# import "SentryDispatchQueueWrapper.h" # import "SentryDisplayLinkWrapper.h" # import "SentryEvent+Private.h" # import "SentryFileManager.h" @@ -21,7 +22,6 @@ # import "SentrySwizzle.h" # import "SentryUIApplication.h" # import - NS_ASSUME_NONNULL_BEGIN static NSString *SENTRY_REPLAY_FOLDER = @"replay"; @@ -42,7 +42,7 @@ @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; SentryNSNotificationCenterWrapper *_notificationCenter; - SentryOnDemandReplay *_resumeReplayMaker; + SentryDispatchQueueWrapper *_dispatchQueue; } - (BOOL)installWithOptions:(nonnull SentryOptions *)options @@ -105,7 +105,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; NSTimeInterval duration = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; - __block int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; + int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; if (type == SentryReplayTypeBuffer) { float errorSampleRate = [jsonObject[@"errorSampleRate"] floatValue]; @@ -114,38 +114,41 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event } } - _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; - _resumeReplayMaker.bitRate = _replayOptions.replayBitRate; - _resumeReplayMaker.videoScale = _replayOptions.sizeScale; + SentryOnDemandReplay *resumeReplayMaker = + [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; + resumeReplayMaker.bitRate = _replayOptions.replayBitRate; + resumeReplayMaker.videoScale = _replayOptions.sizeScale; NSDate *beginning = hasCrashInfo ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] - : [_resumeReplayMaker oldestFrameDate]; + : [resumeReplayMaker oldestFrameDate]; if (beginning == nil) { return; // no frames to send } - NSError *error; - if (![_resumeReplayMaker + _dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; + + [_dispatchQueue dispatchAsyncWithBlock:^{ + SentryReplayType _type = type; + int _segmentId = segmentId; + + NSError *error; + NSArray *videos = [resumeReplayMaker createVideoWithBeginning:beginning end:[beginning dateByAddingTimeInterval:duration] - error:&error - completion:^(SentryVideoInfo *video, NSError *renderError) { - if (renderError != nil) { - SENTRY_LOG_ERROR( - @"Could not create replay video: %@", renderError); - } else { - [self captureVideo:video - replayId:replayId - segmentId:segmentId - type:type]; - } - self->_resumeReplayMaker = nil; - }]) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - return; - } + error:&error]; + if (videos == nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + return; + } + + for (SentryVideoInfo *video in videos) { + [self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type]; + // type buffer is only for the first segment + _type = SentryReplayTypeSession; + } + }]; NSMutableDictionary *eventContext = event.context.mutableCopy; eventContext[@"replay"] = diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index ac85b33c601..f86201a2fdc 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -15,6 +15,7 @@ struct SentryReplayFrame { enum SentryOnDemandReplayError: Error { case cantReadVideoSize + case cantCreatePixelBuffer } @objcMembers @@ -122,18 +123,33 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { return _frames.first?.time } - func createVideoWith(beginning: Date, end: Date, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { - var frameCount = 0 + func createVideoWith(beginning: Date, end: Date) throws -> [SentryVideoInfo] { let videoFrames = filterFrames(beginning: beginning, end: end) - guard let firstFrame = videoFrames.first, let image = UIImage(contentsOfFile: firstFrame.imagePath) else { return } + var frameCount = 0 + + var videos = [SentryVideoInfo]() + + while frameCount < videoFrames.count { + let outputFileURL = URL(fileURLWithPath: _outputPath.appending("/\(videoFrames[frameCount].time.timeIntervalSinceReferenceDate).mp4")) + if let info = try renderVideo(with: videoFrames, from: &frameCount, at: outputFileURL) { + videos.append(info) + } else { + frameCount++ + } + } + return videos + } + + private func renderVideo(with videoFrames: [SentryReplayFrame], from: inout Int, at outputFileURL: URL) throws -> SentryVideoInfo? { + guard from < videoFrames.count, let image = UIImage(contentsOfFile: videoFrames[from].imagePath) else { return nil } let videoWidth = image.size.width * CGFloat(videoScale) let videoHeight = image.size.height * CGFloat(videoScale) - let outputFileURL = URL(fileURLWithPath: _outputPath.appending("/\(beginning.timeIntervalSinceReferenceDate).mp4")) + let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mp4) let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: createVideoSettings(width: videoWidth, height: videoHeight)) - let _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight), videoWriterInput: videoWriterInput) - if _currentPixelBuffer == nil { return } + guard let currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight), videoWriterInput: videoWriterInput) + else { throw SentryOnDemandReplayError.cantCreatePixelBuffer } videoWriter.add(videoWriterInput) videoWriter.startWriting() @@ -141,67 +157,93 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var lastImageSize: CGSize = image.size var usedFrames = [SentryReplayFrame]() + let group = DispatchGroup() + + var renderError: Error? + var result: SentryVideoInfo? + var frameCount = from + + group.enter() videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { [weak self] in + guard let self = self, videoWriter.status == .writing else { videoWriter.cancelWriting() - completion(nil, videoWriter.error) + renderError = videoWriter.error + group.leave() return } - if frameCount < videoFrames.count { - let frame = videoFrames[frameCount] - if let image = UIImage(contentsOfFile: frame.imagePath) { - if lastImageSize != image.size { - videoWriterInput.markAsFinished() - self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) - - self.workingQueue.dispatchAsyncOnMainQueue { - if let previousEnd = usedFrames.min(by: { $0.time > $1.time })?.time { - try? self.createVideoWith(beginning: previousEnd.addingTimeInterval(0.5 / Double(self.frameRate)), end: end, completion: completion) - } - } - - return - } - lastImageSize = image.size - - let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) - - guard _currentPixelBuffer?.append(image: image, presentationTime: presentTime) == true - else { - completion(nil, videoWriter.error) - videoWriter.cancelWriting() - return + if frameCount >= videoFrames.count { + do { + result = try self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) + } catch { + renderError = error + } + group.leave() + return + } + + let frame = videoFrames[frameCount] + if let image = UIImage(contentsOfFile: frame.imagePath) { + if lastImageSize != image.size { + do { + result = try self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) + } catch { + renderError = error } - usedFrames.append(frame) + group.leave() + return } - frameCount += 1 - } else { - videoWriterInput.markAsFinished() - self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter, completion: completion) + lastImageSize = image.size + + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) + + if currentPixelBuffer.append(image: image, presentationTime: presentTime) != true { + renderError = videoWriter.error + videoWriter.cancelWriting() + group.leave() + return + } + usedFrames.append(frame) } + frameCount += 1 } + group.wait() + from = frameCount + if let renderError = renderError { + throw renderError + } + return result } - - private func finishVideo(outputFileURL: URL, usedFrames: [SentryReplayFrame], videoHeight: Int, videoWidth: Int, videoWriter: AVAssetWriter, completion: @escaping (SentryVideoInfo?, Error?) -> Void) { + + private func finishVideo(outputFileURL: URL, usedFrames: [SentryReplayFrame], videoHeight: Int, videoWidth: Int, videoWriter: AVAssetWriter) throws -> SentryVideoInfo? { + let group = DispatchGroup() + var finishError: Error? + var result: SentryVideoInfo? + + group.enter() + videoWriter.inputs.forEach { $0.markAsFinished() } videoWriter.finishWriting { - var videoInfo: SentryVideoInfo? + defer { group.leave() } if videoWriter.status == .completed { do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path) guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else { - completion(nil, SentryOnDemandReplayError.cantReadVideoSize) + finishError = SentryOnDemandReplayError.cantReadVideoSize return } guard let start = usedFrames.min(by: { $0.time < $1.time })?.time else { return } let duration = TimeInterval(usedFrames.count / self.frameRate) - videoInfo = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration, frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName })) + result = SentryVideoInfo(path: outputFileURL, height: Int(videoHeight), width: Int(videoWidth), duration: duration, frameCount: usedFrames.count, frameRate: self.frameRate, start: start, end: start.addingTimeInterval(duration), fileSize: fileSize, screens: usedFrames.compactMap({ $0.screenName })) } catch { - completion(nil, error) + finishError = error } } - completion(videoInfo, videoWriter.error) } + group.wait() + + if let finishError { throw finishError } + return result } private func filterFrames(beginning: Date, end: Date) -> [SentryReplayFrame] { diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift index 4c5f4a4b8f2..32831f38642 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift @@ -6,7 +6,7 @@ import UIKit protocol SentryReplayVideoMaker: NSObjectProtocol { func addFrameAsync(image: UIImage, forScreen: String?) func releaseFramesUntil(_ date: Date) - func createVideoWith(beginning: Date, end: Date, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws + func createVideoWith(beginning: Date, end: Date) throws -> [SentryVideoInfo] } extension SentryReplayVideoMaker { diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 83b59c882a4..090e6835e25 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -39,6 +39,7 @@ class SentrySessionReplay: NSObject { private let displayLink: SentryDisplayLinkWrapper private let dateProvider: SentryCurrentDateProvider private let touchTracker: SentryTouchTracker? + private let dispatchQueue: SentryDispatchQueueWrapper private let lock = NSLock() var screenshotProvider: SentryViewScreenshotProvider @@ -54,6 +55,7 @@ class SentrySessionReplay: NSObject { delegate: SentrySessionReplayDelegate, displayLinkWrapper: SentryDisplayLinkWrapper) { + dispatchQueue = SentryDispatchQueueWrapper() self.replayOptions = replayOptions self.dateProvider = dateProvider self.delegate = delegate @@ -212,17 +214,15 @@ class SentrySessionReplay: NSObject { } private func createAndCapture(startedAt: Date) { - do { - try replayMaker.createVideoWith(beginning: startedAt, end: dateProvider.date()) { [weak self] videoInfo, error in - guard let _self = self else { return } - if let error = error { - print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") - } else if let videoInfo = videoInfo { - _self.newSegmentAvailable(videoInfo: videoInfo) + dispatchQueue.dispatchAsync { + do { + let videos = try self.replayMaker.createVideoWith(beginning: startedAt, end: self.dateProvider.date()) + for video in videos { + self.newSegmentAvailable(videoInfo: video) } + } catch { + print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") } - } catch { - print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") } } diff --git a/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift b/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift new file mode 100644 index 00000000000..aabd2e3d6ca --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift @@ -0,0 +1,11 @@ +import AVFoundation +import Foundation +import UIKit + +class VideoRenderer { + + func videoFrom(images: [UIImage], ofSize size: CGSize, at: URL) throws { + + } + +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift index 4e80a445894..e913afef13c 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift @@ -74,7 +74,7 @@ class SentryOnDemandReplayTests: XCTestCase { } } - func testGenerateVideo() { + func testGenerateVideo() throws { let sut = getSut() dateProvider.driftTimeForEveryRead = true dateProvider.driftTimeInterval = 1 @@ -85,23 +85,20 @@ class SentryOnDemandReplayTests: XCTestCase { let videoExpectation = expectation(description: "Wait for video render") - try? sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) { info, error in - XCTAssertNil(error) - - XCTAssertEqual(info?.duration, 10) - XCTAssertEqual(info?.start, Date(timeIntervalSinceReferenceDate: 0)) - XCTAssertEqual(info?.end, Date(timeIntervalSinceReferenceDate: 10)) - - guard let videoPath = info?.path else { - XCTFail("No video path for replay") - return - } - - XCTAssertEqual(FileManager.default.fileExists(atPath: videoPath.path), true) - videoExpectation.fulfill() - try? FileManager.default.removeItem(at: videoPath) - } + let videos = try sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) + XCTAssertEqual(videos.count, 1) + let info = try XCTUnwrap(videos.first) + + XCTAssertEqual(info.duration, 10) + XCTAssertEqual(info.start, Date(timeIntervalSinceReferenceDate: 0)) + XCTAssertEqual(info.end, Date(timeIntervalSinceReferenceDate: 10)) + + let videoPath = info.path + XCTAssertTrue(FileManager.default.fileExists(atPath: videoPath.path)) + + videoExpectation.fulfill() + try FileManager.default.removeItem(at: videoPath) wait(for: [videoExpectation], timeout: 1) } @@ -152,12 +149,11 @@ class SentryOnDemandReplayTests: XCTestCase { XCTAssertEqual(sut.frames.count, 0) } - func testInvalidWriter() { - let queue = SentryDispatchQueueWrapper() + func testInvalidWriter() throws { + let queue = TestSentryDispatchQueueWrapper() let sut = SentryOnDemandReplay(outputPath: outputPath.path, workingQueue: queue, dateProvider: dateProvider) - let expect = expectation(description: "Video render") let start = dateProvider.date() sut.addFrameAsync(image: UIImage.add) @@ -165,18 +161,13 @@ class SentryOnDemandReplayTests: XCTestCase { let end = dateProvider.date() //Creating a file where the replay would be written to cause an error in the writer - try? "tempFile".data(using: .utf8)?.write(to: outputPath.appendingPathComponent("0.0.mp4")) - - try? sut.createVideoWith(beginning: start, end: end) { _, error in - XCTAssertNotNil(error) - expect.fulfill() - } + try "tempFile".data(using: .utf8)?.write(to: outputPath.appendingPathComponent("0.0.mp4")) - wait(for: [expect], timeout: 1) + XCTAssertThrowsError(try sut.createVideoWith(beginning: start, end: end)) } func testGenerateVideoForEachSize() throws { - let sut = getSut(trueDispatchQueueWrapper: true) + let sut = getSut() dateProvider.driftTimeForEveryRead = true dateProvider.driftTimeInterval = 1 @@ -187,19 +178,7 @@ class SentryOnDemandReplayTests: XCTestCase { sut.addFrameAsync(image: i < 5 ? image1 : image2) } - let videoExpectation = expectation(description: "Wait for video render") - videoExpectation.expectedFulfillmentCount = 2 - - var videos = [SentryVideoInfo]() - - try? sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) { info, error in - XCTAssertNil(error) - guard let info = info else { return } - videos.append(info) - videoExpectation.fulfill() - } - - wait(for: [videoExpectation], timeout: 1) + let videos = try sut.createVideoWith(beginning: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 10)) XCTAssertEqual(videos.count, 2) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index f97da17c928..67df826a44a 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -20,21 +20,17 @@ class SentrySessionReplayTests: XCTestCase { struct CreateVideoCall { var beginning: Date var end: Date - var completion: ((Sentry.SentryVideoInfo?, Error?) -> Void) } var lastCallToCreateVideo: CreateVideoCall? - func createVideoWith(beginning: Date, end: Date, completion: @escaping (Sentry.SentryVideoInfo?, (Error)?) -> Void) throws { - lastCallToCreateVideo = CreateVideoCall(beginning: beginning, - end: end, - completion: completion) + func createVideoWith(beginning: Date, end: Date) throws -> [SentryVideoInfo] { + lastCallToCreateVideo = CreateVideoCall(beginning: beginning, end: end) let outputFileURL = FileManager.default.temporaryDirectory.appendingPathComponent("tempvideo.mp4") try? "Video Data".write(to: outputFileURL, atomically: true, encoding: .utf8) - let videoInfo = SentryVideoInfo(path: outputFileURL, height: 1_024, width: 480, duration: end.timeIntervalSince(beginning), frameCount: 5, frameRate: 1, start: beginning, end: end, fileSize: 10, screens: screens) - completion(videoInfo, nil) + return [videoInfo] } var lastFrame: UIImage? From bd1403d33d947afb27af31f3a8d975b8b24f07ff Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 26 Jul 2024 14:06:48 +0200 Subject: [PATCH 35/47] Remove file for test --- Sentry.xcodeproj/project.pbxproj | 4 ---- .../Integrations/SessionReplay/VideoRenderer.swift | 11 ----------- 2 files changed, 15 deletions(-) delete mode 100644 Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 65e15e4c7a1..61b33b538ba 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -869,7 +869,6 @@ D8ACE3CD2762187D00F5A213 /* SentryNSDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */; }; D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; - D8AE48672C527D730092A2A6 /* VideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48662C527D730092A2A6 /* VideoRenderer.swift */; }; D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; }; D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; }; D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; }; @@ -1930,7 +1929,6 @@ D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataSwizzling.h; path = include/SentryNSDataSwizzling.h; sourceTree = ""; }; D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; - D8AE48662C527D730092A2A6 /* VideoRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRenderer.swift; sourceTree = ""; }; D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = ""; }; D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = ""; }; D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = ""; }; @@ -3855,7 +3853,6 @@ D84D2CC22C29AD120011AF8A /* SentrySessionReplay.swift */, D84D2CDC2C2BF7370011AF8A /* SentryReplayEvent.swift */, D84D2CDE2C2BF9370011AF8A /* SentryReplayType.swift */, - D8AE48662C527D730092A2A6 /* VideoRenderer.swift */, ); path = SessionReplay; sourceTree = ""; @@ -4676,7 +4673,6 @@ 63FE714120DA4C1100CDBAE8 /* SentryCrashDate.c in Sources */, 63FE70DB20DA4C1000CDBAE8 /* SentryCrashMonitor_System.m in Sources */, 7BA61CBB247BC5D800C130A8 /* SentryCrashDefaultBinaryImageProvider.m in Sources */, - D8AE48672C527D730092A2A6 /* VideoRenderer.swift in Sources */, 63FE713120DA4C1100CDBAE8 /* SentryCrashDynamicLinker.c in Sources */, 8E25C95325F836D000DC215B /* SentryRandom.m in Sources */, 7BC85231245812EC005A70F0 /* SentryFileContents.m in Sources */, diff --git a/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift b/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift deleted file mode 100644 index aabd2e3d6ca..00000000000 --- a/Sources/Swift/Integrations/SessionReplay/VideoRenderer.swift +++ /dev/null @@ -1,11 +0,0 @@ -import AVFoundation -import Foundation -import UIKit - -class VideoRenderer { - - func videoFrom(images: [UIImage], ofSize size: CGSize, at: URL) throws { - - } - -} From a534e2f8c8788eafb46c3f2496ed15a8cf8e7b0b Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 26 Jul 2024 14:28:40 +0200 Subject: [PATCH 36/47] Update SentryOnDemandReplay.swift --- .../SessionReplay/SentryOnDemandReplay.swift | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index f86201a2fdc..5211bb4a437 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -16,6 +16,7 @@ struct SentryReplayFrame { enum SentryOnDemandReplayError: Error { case cantReadVideoSize case cantCreatePixelBuffer + case errorRenderingVideo } @objcMembers @@ -159,26 +160,20 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var usedFrames = [SentryReplayFrame]() let group = DispatchGroup() - var renderError: Error? - var result: SentryVideoInfo? + var result: Result? var frameCount = from group.enter() videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { [weak self] in - guard let self = self, videoWriter.status == .writing else { videoWriter.cancelWriting() - renderError = videoWriter.error + result = .failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo ) group.leave() return } if frameCount >= videoFrames.count { - do { - result = try self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) - } catch { - renderError = error - } + result = self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) group.leave() return } @@ -186,11 +181,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { let frame = videoFrames[frameCount] if let image = UIImage(contentsOfFile: frame.imagePath) { if lastImageSize != image.size { - do { - result = try self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) - } catch { - renderError = error - } + result = self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) group.leave() return } @@ -199,8 +190,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) if currentPixelBuffer.append(image: image, presentationTime: presentTime) != true { - renderError = videoWriter.error videoWriter.cancelWriting() + result = .failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo ) group.leave() return } @@ -210,13 +201,11 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } group.wait() from = frameCount - if let renderError = renderError { - throw renderError - } - return result + + return try result?.get() } - private func finishVideo(outputFileURL: URL, usedFrames: [SentryReplayFrame], videoHeight: Int, videoWidth: Int, videoWriter: AVAssetWriter) throws -> SentryVideoInfo? { + private func finishVideo(outputFileURL: URL, usedFrames: [SentryReplayFrame], videoHeight: Int, videoWidth: Int, videoWriter: AVAssetWriter) -> Result { let group = DispatchGroup() var finishError: Error? var result: SentryVideoInfo? @@ -242,8 +231,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } group.wait() - if let finishError { throw finishError } - return result + if let finishError { return .failure(finishError) } + return .success(result) } private func filterFrames(beginning: Date, end: Date) -> [SentryReplayFrame] { From f5cacda4ff06d4f79d6ec7ca02b6e240705ed0f5 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 26 Jul 2024 14:33:01 +0200 Subject: [PATCH 37/47] Update SentryOnDemandReplay.swift --- .../Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 5211bb4a437..0d5deef9d21 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -231,7 +231,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } group.wait() - if let finishError { return .failure(finishError) } + if let finishError = finishError { return .failure(finishError) } return .success(result) } From 6118782169284972fdc67e168b1eebae349032bd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 26 Jul 2024 15:24:01 +0200 Subject: [PATCH 38/47] async test --- Sources/Sentry/SentrySessionReplayIntegration.m | 1 + .../Swift/Integrations/SessionReplay/SentrySessionReplay.swift | 3 ++- .../Integrations/SessionReplay/SentrySessionReplayTests.swift | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 90bf11813e7..aa4f6227807 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -251,6 +251,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions touchTracker:_touchTracker dateProvider:SentryDependencyContainer.sharedInstance.dateProvider delegate:self + dispatchQueue:[[SentryDispatchQueueWrapper alloc] init] displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; [self.sessionReplay diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 090e6835e25..be042d6cdc1 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -53,9 +53,10 @@ class SentrySessionReplay: NSObject { touchTracker: SentryTouchTracker?, dateProvider: SentryCurrentDateProvider, delegate: SentrySessionReplayDelegate, + dispatchQueue: SentryDispatchQueueWrapper, displayLinkWrapper: SentryDisplayLinkWrapper) { - dispatchQueue = SentryDispatchQueueWrapper() + self.dispatchQueue = dispatchQueue self.replayOptions = replayOptions self.dateProvider = dateProvider self.delegate = delegate diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 67df826a44a..5ce0727ba7e 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -72,6 +72,7 @@ class SentrySessionReplayTests: XCTestCase { touchTracker: SentryTouchTracker(dateProvider: dateProvider, scale: 0), dateProvider: dateProvider, delegate: self, + dispatchQueue: TestSentryDispatchQueueWrapper(), displayLinkWrapper: displayLink) } From 762e6bc83b8db6266c0498a327b0b6d924a66df1 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 29 Jul 2024 09:33:38 +0200 Subject: [PATCH 39/47] Update SentryOnDemandReplay.swift --- .../SessionReplay/SentryOnDemandReplay.swift | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index e89e0ab5a0b..218c36221b2 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -62,31 +62,6 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } } - private var actualWidth: Int { Int(Float(videoWidth) * videoScale) } - private var actualHeight: Int { Int(Float(videoHeight) * videoScale) } - - init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { - self._outputPath = outputPath - self.dateProvider = dateProvider - self.workingQueue = workingQueue - } - - convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) { - self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider) - - do { - let content = try FileManager.default.contentsOfDirectory(atPath: outputPath) - _frames = content.compactMap { - guard $0.hasSuffix(".png") else { return SentryReplayFrame?.none } - guard let time = Double($0.dropLast(4)) else { return nil } - return SentryReplayFrame(imagePath: "\(outputPath)/\($0)", time: Date(timeIntervalSinceReferenceDate: time), screenName: nil) - }.sorted { $0.time < $1.time } - } catch { - print("[SentryOnDemandReplay:\(#line)] Could not list frames from replay: \(error.localizedDescription)") - return - } - } - convenience init(outputPath: String) { self.init(outputPath: outputPath, workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil), From 4b0ec8561161c269b4f6a1782a51bc54dc9afd88 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 31 Jul 2024 14:45:12 +0200 Subject: [PATCH 40/47] Update Sources/Sentry/SentrySessionReplayIntegration.m --- Sources/Sentry/SentrySessionReplayIntegration.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 9e07cda4d81..5e525917d85 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -143,7 +143,6 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); return; } - for (SentryVideoInfo *video in videos) { [self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type]; // type buffer is only for the first segment From 9c50e4ffddb9660c1f43eb53d7b98ddbbdf200de Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 1 Aug 2024 14:54:51 +0200 Subject: [PATCH 41/47] Apply suggestions from code review Co-authored-by: Philipp Hofmann --- .../Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../SessionReplay/SentrySessionReplayIntegrationTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index ebc5aed2ea8..e626789e113 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -131,7 +131,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { while frameCount < videoFrames.count { let outputFileURL = URL(fileURLWithPath: _outputPath.appending("/\(videoFrames[frameCount].time.timeIntervalSinceReferenceDate).mp4")) - if let info = try renderVideo(with: videoFrames, from: &frameCount, at: outputFileURL) { + if let videoInfo = try renderVideo(with: videoFrames, from: &frameCount, at: outputFileURL) { videos.append(info) } else { frameCount++ diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift index 88f5712d940..706f27fa00c 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -164,7 +164,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase { XCTAssertNotNil(sut.sessionReplay) } - func testRestartReplayWithNewSessionClosePreviusReplay() throws { + func testRestartReplayWithNewSessionClosePreviousReplay() throws { startSDK(sessionSampleRate: 1, errorSampleRate: 0) let sut = try getSut() From 29037f0bc44361fcc74ddd9d80d79c5447b222bd Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 1 Aug 2024 14:55:03 +0200 Subject: [PATCH 42/47] fixes --- CHANGELOG.md | 1 - Sources/Sentry/SentrySessionReplayIntegration.m | 5 ++--- .../Integrations/SessionReplay/SentryOnDemandReplay.swift | 4 +++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a4572bf0f3..7815897d038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,6 @@ ### Features -- Replay for crashes (#4171) - Collect only unique UIWindow references (#4159) ### Deprecated diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 5e525917d85..2c665cbdb37 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -42,7 +42,6 @@ @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; SentryNSNotificationCenterWrapper *_notificationCenter; - SentryDispatchQueueWrapper *_dispatchQueue; SentryOnDemandReplay *_resumeReplayMaker; } @@ -128,9 +127,9 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; // no frames to send } - _dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; + SentryDispatchQueueWrapper * dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; - [_dispatchQueue dispatchAsyncWithBlock:^{ + [dispatchQueue dispatchAsyncWithBlock:^{ SentryReplayType _type = type; int _segmentId = segmentId; diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index ebc5aed2ea8..d3c8c0d03d0 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -198,7 +198,9 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } frameCount += 1 } - group.wait() + if group.wait(timeout: .now() + 2) == .timedOut { + throw SentryOnDemandReplayError.errorRenderingVideo + } from = frameCount return try result?.get() From 5b3f454e424c4ca9621fc15d1170b6cee8dc2929 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 1 Aug 2024 14:57:42 +0200 Subject: [PATCH 43/47] chore --- .../Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift | 2 +- .../Swift/Integrations/SessionReplay/SentrySessionReplay.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 2e82779f139..f72626a1aec 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -132,7 +132,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { while frameCount < videoFrames.count { let outputFileURL = URL(fileURLWithPath: _outputPath.appending("/\(videoFrames[frameCount].time.timeIntervalSinceReferenceDate).mp4")) if let videoInfo = try renderVideo(with: videoFrames, from: &frameCount, at: outputFileURL) { - videos.append(info) + videos.append(videoInfo) } else { frameCount++ } diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index e3cbe2bf787..0b22c70a160 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -222,7 +222,7 @@ class SentrySessionReplay: NSObject { self.newSegmentAvailable(videoInfo: video) } } catch { - print("[SentrySessionReplay:\(#line)] Could not create replay video - \(error.localizedDescription)") + SentryLog.debug("Could not create replay video - \(error.localizedDescription)") } } } From 6d99f45bb91dbc340e003f1c5887feb6cc9e66d0 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 1 Aug 2024 12:58:54 +0000 Subject: [PATCH 44/47] Format code --- Sources/Sentry/SentrySessionReplayIntegration.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index dbf700e570e..bd6dc71386b 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -127,7 +127,7 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; // no frames to send } - SentryDispatchQueueWrapper * dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; + SentryDispatchQueueWrapper *dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; [dispatchQueue dispatchAsyncWithBlock:^{ SentryReplayType _type = type; From 028bfbb200c85ea3c39cd38b7c34374ffccaf138 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 1 Aug 2024 15:01:32 +0200 Subject: [PATCH 45/47] Update Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift --- .../Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index f72626a1aec..97cb2081ace 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -238,6 +238,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { private func filterFrames(beginning: Date, end: Date) -> [SentryReplayFrame] { var frames = [SentryReplayFrame]() + //Using dispatch queue as sync mechanism since we need a queue already to generate the video. workingQueue.dispatchSync({ frames = self._frames.filter { $0.time >= beginning && $0.time <= end } }) From b6e226cecbed8aa80be9eee4b2732f4e88b96666 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 2 Aug 2024 16:03:47 +0200 Subject: [PATCH 46/47] fix --- .../Sentry/SentrySessionReplayIntegration.m | 38 +++++++++---------- .../SessionReplay/SentryOnDemandReplay.swift | 11 ++---- .../SessionReplay/SentrySessionReplay.swift | 3 ++ 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index bd6dc71386b..f57275abf4a 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -127,27 +127,23 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event return; // no frames to send } - SentryDispatchQueueWrapper *dispatchQueue = [[SentryDispatchQueueWrapper alloc] init]; - - [dispatchQueue dispatchAsyncWithBlock:^{ - SentryReplayType _type = type; - int _segmentId = segmentId; - - NSError *error; - NSArray *videos = [resumeReplayMaker - createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - error:&error]; - if (videos == nil) { - SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); - return; - } - for (SentryVideoInfo *video in videos) { - [self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type]; - // type buffer is only for the first segment - _type = SentryReplayTypeSession; - } - }]; + SentryReplayType _type = type; + int _segmentId = segmentId; + + NSError *error; + NSArray *videos = + [resumeReplayMaker createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + error:&error]; + if (videos == nil) { + SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); + return; + } + for (SentryVideoInfo *video in videos) { + [self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type]; + // type buffer is only for the first segment + _type = SentryReplayTypeSession; + } NSMutableDictionary *eventContext = event.context.mutableCopy; eventContext[@"replay"] = diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index 97cb2081ace..19243b93716 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -163,20 +163,18 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { var frameCount = from group.enter() - videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { [weak self] in - guard let self = self, videoWriter.status == .writing else { + videoWriterInput.requestMediaDataWhenReady(on: workingQueue.queue) { + guard videoWriter.status == .writing else { videoWriter.cancelWriting() result = .failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo ) group.leave() return } - if frameCount >= videoFrames.count { result = self.finishVideo(outputFileURL: outputFileURL, usedFrames: usedFrames, videoHeight: Int(videoHeight), videoWidth: Int(videoWidth), videoWriter: videoWriter) group.leave() return } - let frame = videoFrames[frameCount] if let image = UIImage(contentsOfFile: frame.imagePath) { if lastImageSize != image.size { @@ -187,7 +185,6 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { lastImageSize = image.size let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate)) - if currentPixelBuffer.append(image: image, presentationTime: presentTime) != true { videoWriter.cancelWriting() result = .failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo ) @@ -198,9 +195,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { } frameCount += 1 } - if group.wait(timeout: .now() + 2) == .timedOut { - throw SentryOnDemandReplayError.errorRenderingVideo - } + guard group.wait(timeout: .now() + 2) == .success else { throw SentryOnDemandReplayError.errorRenderingVideo } from = frameCount return try result?.get() diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index 0b22c70a160..e56291c1543 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -215,6 +215,9 @@ class SentrySessionReplay: NSObject { } private func createAndCapture(startedAt: Date) { + //Creating a video is heavy and blocks the thread + //Since this function is always called in the main thread + //we dispatch it to a background thread. dispatchQueue.dispatchAsync { do { let videos = try self.replayMaker.createVideoWith(beginning: startedAt, end: self.dateProvider.date()) From 748b30bd45428897475c0f819d943f5a2e61d542 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 5 Aug 2024 13:31:50 +0200 Subject: [PATCH 47/47] Update SentrySessionReplayIntegration.m --- Sources/Sentry/SentrySessionReplayIntegration.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index f57275abf4a..cfcc449741e 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -77,6 +77,12 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options return YES; } +/** + * Send the cached frames from a previous session that eventually crashed. + * This function is called when processing an event created by SentryCrashIntegration, + * which runs in the background. That's why we don't need to dispatch the generation of the + * replay to the background in this function. + */ - (void)resumePreviousSessionReplay:(SentryEvent *)event { NSURL *dir = [self replayDirectory];