Skip to content

Commit

Permalink
fix: Capture all touches with session replay (#4477)
Browse files Browse the repository at this point in the history
Some touches may happening in between segments and were not sent to Sentry
  • Loading branch information
brustolin authored Oct 29, 2024
1 parent df9fb5b commit 2095ae0
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add option to report uncaught NSExceptions on macOS (#4471)
- Build visionOS project with static Sentry SDK (#4462)
- Capture all touches with session replay (#4477)

### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ class SentrySessionReplay: NSObject {

var events = convertBreadcrumbs(breadcrumbs: breadcrumbs, from: video.start, until: video.end)
if let touchTracker = touchTracker {
events.append(contentsOf: touchTracker.replayEvents(from: video.start, until: video.end))
events.append(contentsOf: touchTracker.replayEvents(from: videoSegmentStart ?? video.start, until: video.end))
touchTracker.flushFinishedEvents()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ class SentrySessionReplayTests: XCTestCase {
}
}

private class TestTouchTracker: SentryTouchTracker {
var replayEventsCallback: ((Date, Date) -> Void)?

override func replayEvents(from: Date, until: Date) -> [SentryRRWebEvent] {
replayEventsCallback?(from, until)
return super.replayEvents(from: from, until: until)
}
}

private class TestReplayMaker: NSObject, SentryReplayVideoMaker {
var screens = [String]()

var createVideoCallBack: ((SentryVideoInfo) -> Void)?
var overrideBeginning: Date?

struct CreateVideoCall {
var beginning: Date
Expand All @@ -30,7 +40,7 @@ class SentrySessionReplayTests: XCTestCase {
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)
let videoInfo = SentryVideoInfo(path: outputFileURL, height: 1_024, width: 480, duration: end.timeIntervalSince(overrideBeginning ?? beginning), frameCount: 5, frameRate: 1, start: overrideBeginning ?? beginning, end: end, fileSize: 10, screens: screens)

createVideoCallBack?(videoInfo)
return [videoInfo]
Expand Down Expand Up @@ -66,13 +76,13 @@ class SentrySessionReplayTests: XCTestCase {
var lastReplayId: SentryId?
var currentScreen: String?

func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, onErrorSampleRate: 0), dispatchQueue: SentryDispatchQueueWrapper = TestSentryDispatchQueueWrapper() ) -> SentrySessionReplay {
func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, onErrorSampleRate: 0), dispatchQueue: SentryDispatchQueueWrapper = TestSentryDispatchQueueWrapper(), touchTracker: SentryTouchTracker? = nil) -> SentrySessionReplay {
return SentrySessionReplay(replayOptions: options,
replayFolderPath: cacheFolder,
screenshotProvider: screenshotProvider,
replayMaker: replayMaker,
breadcrumbConverter: SentrySRDefaultBreadcrumbConverter(),
touchTracker: SentryTouchTracker(dateProvider: dateProvider, scale: 0),
touchTracker: touchTracker ?? SentryTouchTracker(dateProvider: dateProvider, scale: 0),
dateProvider: dateProvider,
delegate: self,
dispatchQueue: dispatchQueue,
Expand Down Expand Up @@ -317,6 +327,50 @@ class SentrySessionReplayTests: XCTestCase {
XCTAssertNil(fixture.screenshotProvider.lastImageCall)
}

func testCaptureAllTouches() {
let fixture = Fixture()
let touchTracker = TestTouchTracker(dateProvider: fixture.dateProvider, scale: 1)
let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1), touchTracker: touchTracker)
sut.start(rootView: fixture.rootView, fullSession: true)

//Starting session replay at time 0
Dynamic(sut).newFrame(nil)

//Advancing one second and capturing another frame
fixture.dateProvider.advance(by: 1)
Dynamic(sut).newFrame(nil)

//Advancing 5 more second to complete one segment
fixture.dateProvider.advance(by: 5)
Dynamic(sut).newFrame(nil)

let endOfFirstSegment = fixture.dateProvider.date()

//Advancing 2 seconds to start another segment at second 7
//This means session replay didnt capture screens between seconds 5 and 7
fixture.dateProvider.advance(by: 2)
Dynamic(sut).newFrame(nil)

let expect = expectation(description: "Touch Tracker called")
touchTracker.replayEventsCallback = { begin, end in
// Even though the second segment started at second 7,
// we should capture all touch events since the end of the first segment.

XCTAssertEqual(begin, endOfFirstSegment)
XCTAssertEqual(end, fixture.dateProvider.date())
expect.fulfill()
}

// This will make the mock videoInfo starts at second 7 as well
fixture.replayMaker.overrideBeginning = Date(timeIntervalSinceReferenceDate: 7)

//Advancing another 5 seconds to close the second segment
fixture.dateProvider.advance(by: 5)
Dynamic(sut).newFrame(nil)

wait(for: [expect], timeout: 1)
}

@available(iOS 16.0, tvOS 16, *)
func testDealloc_CallsStop() {
let fixture = Fixture()
Expand Down

0 comments on commit 2095ae0

Please sign in to comment.