Skip to content

Commit c1c91e2

Browse files
philprimephilipphofmann
authored andcommitted
feat(session-replay): refactor session replay types and added logging (#5082)
1 parent e655d15 commit c1c91e2

File tree

7 files changed

+89
-48
lines changed

7 files changed

+89
-48
lines changed

Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ struct SentrySDKWrapper {
1919
options.debug = true
2020

2121
if #available(iOS 16.0, *), enableSessionReplay {
22-
options.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true)
22+
options.sessionReplay = SentryReplayOptions(
23+
sessionSampleRate: 0,
24+
onErrorSampleRate: 1,
25+
maskAllText: true,
26+
maskAllImages: true
27+
)
2328
options.sessionReplay.quality = .high
2429
}
2530

Sentry.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,8 @@
821821
D43B26D62D70964C007747FD /* SentrySpanOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D52D709648007747FD /* SentrySpanOperation.m */; };
822822
D43B26D82D70A550007747FD /* SentryTraceOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D72D70A54A007747FD /* SentryTraceOrigin.m */; };
823823
D43B26DA2D70A612007747FD /* SentrySpanDataKey.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D92D70A60E007747FD /* SentrySpanDataKey.m */; };
824+
D451ED5D2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */; };
825+
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */; };
824826
D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4312D706BDD007068CB /* SentrySpanOperation.h */; };
825827
D456B4362D706BF2007068CB /* SentryTraceOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4352D706BEE007068CB /* SentryTraceOrigin.h */; };
826828
D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4372D706BFB007068CB /* SentrySpanDataKey.h */; };
@@ -1981,6 +1983,8 @@
19811983
D43B26D52D709648007747FD /* SentrySpanOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanOperation.m; sourceTree = "<group>"; };
19821984
D43B26D72D70A54A007747FD /* SentryTraceOrigin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceOrigin.m; sourceTree = "<group>"; };
19831985
D43B26D92D70A60E007747FD /* SentrySpanDataKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanDataKey.m; sourceTree = "<group>"; };
1986+
D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayError.swift; sourceTree = "<group>"; };
1987+
D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayFrame.swift; sourceTree = "<group>"; };
19841988
D456B4312D706BDD007068CB /* SentrySpanOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanOperation.h; path = include/SentrySpanOperation.h; sourceTree = "<group>"; };
19851989
D456B4352D706BEE007068CB /* SentryTraceOrigin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTraceOrigin.h; path = include/SentryTraceOrigin.h; sourceTree = "<group>"; };
19861990
D456B4372D706BFB007068CB /* SentrySpanDataKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanDataKey.h; path = include/SentrySpanDataKey.h; sourceTree = "<group>"; };
@@ -4265,6 +4269,8 @@
42654269
D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */,
42664270
D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */,
42674271
D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */,
4272+
D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */,
4273+
D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */,
42684274
D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */,
42694275
D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */,
42704276
D8F67B1A2BE9728600C9197B /* SentrySRDefaultBreadcrumbConverter.swift */,
@@ -5032,6 +5038,7 @@
50325038
7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */,
50335039
63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */,
50345040
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
5041+
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */,
50355042
D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */,
50365043
630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */,
50375044
6281C5722D3E4F12009D0978 /* DecodeArbitraryData.swift in Sources */,
@@ -5124,6 +5131,7 @@
51245131
D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */,
51255132
7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */,
51265133
63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */,
5134+
D451ED5D2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift in Sources */,
51275135
D4E829D82D75E57900D375AD /* SentryMaskRenderer.swift in Sources */,
51285136
7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */,
51295137
628308612D50ADAC00EAEF77 /* SentryRequestCodable.swift in Sources */,

Sources/Sentry/SentrySessionReplayIntegration.m

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ @implementation SentrySessionReplayIntegration {
5757
// This is the easiest way to ensure segment 0 will always reach the server, because session
5858
// replay absolutely needs segment 0 to make replay work.
5959
BOOL _rateLimited;
60+
id<SentryCurrentDateProvider> _dateProvider;
6061
}
6162

6263
- (instancetype)init
@@ -97,6 +98,8 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions
9798
{
9899
_replayOptions = replayOptions;
99100
_rateLimits = SentryDependencyContainer.sharedInstance.rateLimits;
101+
_dateProvider = SentryDependencyContainer.sharedInstance.dateProvider;
102+
100103
id<SentryViewRenderer> viewRenderer;
101104
if (enableExperimentalRenderer) {
102105
viewRenderer = [[SentryExperimentalViewRenderer alloc]
@@ -113,14 +116,15 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions
113116
enableExperimentalMaskRenderer:enableExperimentalRenderer];
114117

115118
if (touchTracker) {
116-
_touchTracker = [[SentryTouchTracker alloc]
117-
initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider
118-
scale:replayOptions.sizeScale];
119+
_touchTracker = [[SentryTouchTracker alloc] initWithDateProvider:_dateProvider
120+
scale:replayOptions.sizeScale];
119121
[self swizzleApplicationTouch];
120122
}
121123

122124
_notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper;
123125

126+
// The asset worker queue is used to work on video and frames data.
127+
124128
[self moveCurrentReplay];
125129
[self cleanUp];
126130

@@ -196,23 +200,23 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event
196200
NSDate *beginning = hasCrashInfo
197201
? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd]
198202
: [resumeReplayMaker oldestFrameDate];
199-
200203
if (beginning == nil) {
201204
return; // no frames to send
202205
}
203-
204-
SentryReplayType _type = type;
205-
int _segmentId = segmentId;
206+
NSDate *end = [beginning dateByAddingTimeInterval:duration];
206207

207208
NSError *error;
208-
NSArray<SentryVideoInfo *> *videos =
209-
[resumeReplayMaker createVideoWithBeginning:beginning
210-
end:[beginning dateByAddingTimeInterval:duration]
211-
error:&error];
209+
NSArray<SentryVideoInfo *> *videos = [resumeReplayMaker createVideoWithBeginning:beginning
210+
end:end
211+
error:&error];
212212
if (videos == nil) {
213-
SENTRY_LOG_ERROR(@"Could not create replay video: %@", error);
213+
SENTRY_LOG_ERROR(@"Could not create replay video, reason: %@", error);
214214
return;
215215
}
216+
217+
// For each segment we need to create a new event with the video.
218+
int _segmentId = segmentId;
219+
SentryReplayType _type = type;
216220
for (SentryVideoInfo *video in videos) {
217221
[self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type];
218222
// type buffer is only for the first segment
@@ -224,8 +228,11 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event
224228
[NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil];
225229
event.context = eventContext;
226230

227-
if ([NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&error] == NO) {
228-
SENTRY_LOG_ERROR(@"Can`t delete '%@': %@", SENTRY_LAST_REPLAY, error);
231+
NSError *_Nullable removeError;
232+
BOOL result = [NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&removeError];
233+
if (result == NO) {
234+
SENTRY_LOG_ERROR(@"Can't delete '%@' with file item at url: '%@', reason: %@",
235+
SENTRY_LAST_REPLAY, lastReplayURL, removeError);
229236
}
230237
}
231238

Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,6 @@ import CoreGraphics
77
import Foundation
88
import UIKit
99

10-
struct SentryReplayFrame {
11-
let imagePath: String
12-
let time: Date
13-
let screenName: String?
14-
}
15-
16-
enum SentryOnDemandReplayError: Error {
17-
case cantReadVideoSize
18-
case cantCreatePixelBuffer
19-
case errorRenderingVideo
20-
}
21-
2210
@objcMembers
2311
class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
2412

@@ -27,7 +15,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
2715
private let dateProvider: SentryCurrentDateProvider
2816
private let workingQueue: SentryDispatchQueueWrapper
2917
private var _frames = [SentryReplayFrame]()
30-
18+
3119
#if SENTRY_TEST || SENTRY_TEST_CI || DEBUG
3220
//This is exposed only for tests, no need to make it thread safe.
3321
var frames: [SentryReplayFrame] {
@@ -48,20 +36,28 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
4836

4937
convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) {
5038
self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider)
51-
39+
loadFrames(fromPath: outputPath)
40+
}
41+
42+
/// Loads the frames from the given path.
43+
///
44+
/// - Parameter path: The path to the directory containing the frames.
45+
private func loadFrames(fromPath path: String) {
46+
SentryLog.debug("[Session Replay] Loading frames from path: \(path)")
5247
do {
53-
let content = try FileManager.default.contentsOfDirectory(atPath: outputPath)
54-
_frames = content.compactMap {
55-
guard $0.hasSuffix(".png") else { return SentryReplayFrame?.none }
56-
guard let time = Double($0.dropLast(4)) else { return nil }
57-
return SentryReplayFrame(imagePath: "\(outputPath)/\($0)", time: Date(timeIntervalSinceReferenceDate: time), screenName: nil)
48+
let content = try FileManager.default.contentsOfDirectory(atPath: path)
49+
_frames = content.compactMap { frameFilePath -> SentryReplayFrame? in
50+
guard frameFilePath.hasSuffix(".png") else { return nil }
51+
guard let time = Double(frameFilePath.dropLast(4)) else { return nil }
52+
let timestamp = Date(timeIntervalSinceReferenceDate: time)
53+
return SentryReplayFrame(imagePath: "\(path)/\(frameFilePath)", time: timestamp, screenName: nil)
5854
}.sorted { $0.time < $1.time }
55+
SentryLog.debug("[Session Replay] Loaded \(content.count) files into \(_frames.count) frames from path: \(path)")
5956
} catch {
60-
SentryLog.debug("Could not list frames from replay: \(error.localizedDescription)")
61-
return
57+
SentryLog.error("[Session Replay] Could not list frames from replay: \(error.localizedDescription)")
6258
}
6359
}
64-
60+
6561
convenience init(outputPath: String) {
6662
self.init(outputPath: outputPath,
6763
workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil),
@@ -73,7 +69,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
7369
workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil),
7470
dateProvider: SentryDefaultCurrentDateProvider())
7571
}
76-
72+
7773
func addFrameAsync(image: UIImage, forScreen: String?) {
7874
workingQueue.dispatchAsync({
7975
self.addFrame(image: image, forScreen: forScreen)
@@ -88,11 +84,12 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
8884
do {
8985
try data.write(to: URL(fileURLWithPath: imagePath))
9086
} catch {
91-
SentryLog.debug("Could not save replay frame. Error: \(error)")
87+
SentryLog.error("[Session Replay] Could not save replay frame. Error: \(error)")
9288
return
9389
}
9490
_frames.append(SentryReplayFrame(imagePath: imagePath, time: date, screenName: forScreen))
95-
91+
92+
// Remove the oldest frames if the cache size exceeds the maximum size.
9693
while _frames.count > cacheMaxSize {
9794
let first = _frames.removeFirst()
9895
try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath))
@@ -111,10 +108,17 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
111108
}
112109

113110
func releaseFramesUntil(_ date: Date) {
111+
SentryLog.debug("[Session Replay] Releasing frames until date: \(date)")
114112
workingQueue.dispatchAsync ({
115113
while let first = self._frames.first, first.time < date {
116114
self._frames.removeFirst()
117-
try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath))
115+
let fileUrl = URL(fileURLWithPath: first.imagePath)
116+
do {
117+
try FileManager.default.removeItem(at: fileUrl)
118+
SentryLog.debug("[Session Replay] Removed frame at url: \(fileUrl.path)")
119+
} catch {
120+
SentryLog.error("[Session Replay] Failed to remove frame at: \(fileUrl.path), reason: \(error.localizedDescription), ignoring error")
121+
}
118122
}
119123
})
120124
}
@@ -239,7 +243,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
239243
})
240244
return frames
241245
}
242-
246+
243247
private func createVideoSettings(width: CGFloat, height: CGFloat) -> [String: Any] {
244248
return [
245249
AVVideoCodecKey: AVVideoCodecType.h264,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
enum SentryOnDemandReplayError: Error {
2+
case cantReadVideoSize
3+
case cantCreatePixelBuffer
4+
case errorRenderingVideo
5+
case cantReadVideoStartTime
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
3+
struct SentryReplayFrame {
4+
let imagePath: String
5+
let time: Date
6+
let screenName: String?
7+
}

Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class SentrySessionReplay: NSObject {
131131
videoSegmentStart = nil
132132
displayLink.link(withTarget: self, selector: #selector(newFrame(_:)))
133133
}
134-
134+
135135
func captureReplayFor(event: Event) {
136136
guard isRunning else { return }
137137

@@ -140,8 +140,9 @@ class SentrySessionReplay: NSObject {
140140
return
141141
}
142142

143-
guard (event.error != nil || event.exceptions?.isEmpty == false)
144-
&& captureReplay() else { return }
143+
guard (event.error != nil || event.exceptions?.isEmpty == false) && captureReplay() else {
144+
return
145+
}
145146

146147
setEventContext(event: event)
147148
}
@@ -223,6 +224,7 @@ class SentrySessionReplay: NSObject {
223224
}
224225

225226
private func createAndCapture(startedAt: Date, replayType: SentryReplayType) {
227+
SentryLog.debug("[Session Replay] Creating replay video started at date: \(startedAt), replayType: \(replayType)")
226228
//Creating a video is heavy and blocks the thread
227229
//Since this function is always called in the main thread
228230
//we dispatch it to a background thread.
@@ -232,13 +234,15 @@ class SentrySessionReplay: NSObject {
232234
for video in videos {
233235
self.newSegmentAvailable(videoInfo: video, replayType: replayType)
234236
}
237+
SentryLog.debug("[Session Replay] Finished replay video creation with \(videos.count) segments")
235238
} catch {
236239
SentryLog.debug("Could not create replay video - \(error.localizedDescription)")
237240
}
238241
}
239242
}
240243

241244
private func newSegmentAvailable(videoInfo: SentryVideoInfo, replayType: SentryReplayType) {
245+
SentryLog.debug("[Session Replay] New segment available for replayType: \(replayType), videoInfo: \(videoInfo)")
242246
guard let sessionReplayId = sessionReplayId else { return }
243247
captureSegment(segment: currentSegmentId, video: videoInfo, replayId: sessionReplayId, replayType: replayType)
244248
replayMaker.releaseFramesUntil(videoInfo.end)
@@ -270,7 +274,7 @@ class SentrySessionReplay: NSObject {
270274
}
271275

272276
let recording = SentryReplayRecording(segmentId: segment, video: video, extraEvents: events)
273-
277+
274278
delegate?.sessionReplayNewSegment(replayEvent: replayEvent, replayRecording: recording, videoUrl: video.path)
275279

276280
do {
@@ -302,15 +306,15 @@ class SentrySessionReplay: NSObject {
302306

303307
private func takeScreenshot() {
304308
guard let rootView = rootView, !processingScreenshot else { return }
305-
309+
306310
lock.lock()
307311
guard !processingScreenshot else {
308312
lock.unlock()
309313
return
310314
}
311315
processingScreenshot = true
312316
lock.unlock()
313-
317+
314318
let screenName = delegate?.currentScreenNameForSessionReplay()
315319

316320
screenshotProvider.image(view: rootView) { [weak self] screenshot in

0 commit comments

Comments
 (0)