Skip to content

Commit 0a40a02

Browse files
authored
fix(session-replay): Fix conversion of frame rate to time interval (#6623)
1 parent 9f8640f commit 0a40a02

File tree

3 files changed

+116
-1
lines changed

3 files changed

+116
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
- Add layer class filtering for views used in multiple contexts (e.g., SwiftUI._UIGraphicsView)
5151
- Improve transform calculations for views with custom anchor points
5252
- Fix axis-aligned transform detection for optimized opaque view clipping
53+
- Fix conversion of frame rate to time interval for session replay (#6623)
5354

5455
### Improvements
5556

Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ import UIKit
233233
return
234234
}
235235

236-
if now.timeIntervalSince(lastScreenShot) >= Double(1 / replayOptions.frameRate) {
236+
if now.timeIntervalSince(lastScreenShot) >= 1.0 / Double(replayOptions.frameRate) {
237237
takeScreenshot()
238238
self.lastScreenShot = now
239239

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,120 @@ class SentrySessionReplayTests: XCTestCase {
577577
XCTAssertTrue(SentrySessionReplay.shouldEnableSessionReplay(environmentChecker: environmentChecker, experimentalOptions: experimentalOptions))
578578
}
579579

580+
// MARK: - Frame Rate Tests
581+
582+
func testFrameRate_1FPS_takesScreenshotsAtCorrectInterval() {
583+
// Arrange
584+
let fixture = Fixture()
585+
let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
586+
options.frameRate = 1
587+
let sut = fixture.getSut(options: options)
588+
sut.start(rootView: fixture.rootView, fullSession: true)
589+
590+
fixture.screenshotProvider.lastImageCall = nil
591+
592+
// Act & Assert - advance by 0.9 seconds, screenshot should NOT be taken
593+
fixture.dateProvider.advance(by: 0.9)
594+
Dynamic(sut).newFrame(nil)
595+
XCTAssertNil(fixture.screenshotProvider.lastImageCall, "Screenshot should not be taken before 1 second interval")
596+
597+
// Act & Assert - advance to exactly 1.0 seconds, screenshot SHOULD be taken
598+
fixture.dateProvider.advance(by: 0.1)
599+
Dynamic(sut).newFrame(nil)
600+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "Screenshot should be taken at 1 second interval for 1 FPS")
601+
}
602+
603+
func testFrameRate_2FPS_takesScreenshotsAtCorrectInterval() {
604+
// Arrange
605+
let fixture = Fixture()
606+
let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
607+
options.frameRate = 2
608+
let sut = fixture.getSut(options: options)
609+
sut.start(rootView: fixture.rootView, fullSession: true)
610+
611+
fixture.screenshotProvider.lastImageCall = nil
612+
613+
// Act & Assert - advance by 0.4 seconds, screenshot should NOT be taken
614+
fixture.dateProvider.advance(by: 0.4)
615+
Dynamic(sut).newFrame(nil)
616+
XCTAssertNil(fixture.screenshotProvider.lastImageCall, "Screenshot should not be taken before 0.5 second interval")
617+
618+
// Act & Assert - advance to 0.5 seconds, screenshot SHOULD be taken
619+
fixture.dateProvider.advance(by: 0.1)
620+
Dynamic(sut).newFrame(nil)
621+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "Screenshot should be taken at 0.5 second interval for 2 FPS")
622+
623+
// Act & Assert - reset and test second screenshot
624+
fixture.screenshotProvider.lastImageCall = nil
625+
fixture.dateProvider.advance(by: 0.4)
626+
Dynamic(sut).newFrame(nil)
627+
XCTAssertNil(fixture.screenshotProvider.lastImageCall, "Screenshot should not be taken before another 0.5 seconds")
628+
629+
fixture.dateProvider.advance(by: 0.1)
630+
Dynamic(sut).newFrame(nil)
631+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "Screenshot should be taken at next 0.5 second interval")
632+
}
633+
634+
func testFrameRate_10FPS_takesScreenshotsAtCorrectInterval() {
635+
// Arrange
636+
let fixture = Fixture()
637+
let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
638+
options.frameRate = 10
639+
let sut = fixture.getSut(options: options)
640+
sut.start(rootView: fixture.rootView, fullSession: true)
641+
642+
// Expected interval: 1.0 / 10.0 = 0.1 seconds
643+
// Take first screenshot to establish baseline
644+
fixture.dateProvider.advance(by: 0.1)
645+
Dynamic(sut).newFrame(nil)
646+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "First screenshot should be taken")
647+
648+
fixture.screenshotProvider.lastImageCall = nil
649+
650+
// Act & Assert - advance by 0.09 seconds, screenshot should NOT be taken
651+
fixture.dateProvider.advance(by: 0.09)
652+
Dynamic(sut).newFrame(nil)
653+
XCTAssertNil(fixture.screenshotProvider.lastImageCall, "Screenshot should not be taken before 0.1 second interval")
654+
655+
// Act & Assert - advance to reach 0.1 second interval, screenshot SHOULD be taken
656+
fixture.dateProvider.advance(by: 0.01)
657+
Dynamic(sut).newFrame(nil)
658+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "Screenshot should be taken at 0.1 second interval for 10 FPS")
659+
}
660+
661+
func testFrameRate_multipleScreenshots_respectsInterval() {
662+
// Arrange
663+
let fixture = Fixture()
664+
let options = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
665+
options.frameRate = 5
666+
let sut = fixture.getSut(options: options)
667+
sut.start(rootView: fixture.rootView, fullSession: true)
668+
669+
// Expected interval: 1.0 / 5.0 = 0.2 seconds
670+
var screenshotCount = 0
671+
672+
// Act & Assert - take 5 screenshots over 1 second
673+
// Each screenshot resets the timer, so we need to advance by the full interval each time
674+
for i in 0..<5 {
675+
// Advance by full interval
676+
fixture.dateProvider.advance(by: 0.2)
677+
Dynamic(sut).newFrame(nil)
678+
679+
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall, "Screenshot #\(i + 1) should be taken at \(Double(i + 1) * 0.2) seconds")
680+
screenshotCount += 1
681+
fixture.screenshotProvider.lastImageCall = nil
682+
683+
// Advance by less than interval and verify no screenshot
684+
if i < 4 { // Don't test after the last screenshot
685+
fixture.dateProvider.advance(by: 0.1)
686+
Dynamic(sut).newFrame(nil)
687+
XCTAssertNil(fixture.screenshotProvider.lastImageCall, "No screenshot should be taken at \(Double(i + 1) * 0.2 + 0.1) seconds")
688+
}
689+
}
690+
691+
XCTAssertEqual(screenshotCount, 5, "Should have taken exactly 5 screenshots in 1 second for 5 FPS")
692+
}
693+
580694
// MARK: - Helpers
581695

582696
private func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) {

0 commit comments

Comments
 (0)