Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUM 6569 clean #2089

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions DatadogSessionReplay/Sources/Recorder/Recorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ public class Recorder: Recording {
windowObserver: windowObserver,
snapshotBuilder: ViewTreeSnapshotBuilder(additionalNodeRecorders: additionalNodeRecorders)
)
let touchSnapshotProducer = WindowTouchSnapshotProducer(
windowObserver: windowObserver
)
let touchSnapshotProducer = WindowTouchSnapshotProducer(windowObserver: windowObserver)

self.init(
uiApplicationSwizzler: try UIApplicationSwizzler(handler: touchSnapshotProducer),
Expand Down Expand Up @@ -117,7 +115,7 @@ public class Recorder: Recording {
return
}

let touchSnapshot = recorderContext.touchPrivacy == .show ? touchSnapshotProducer.takeSnapshot(context: recorderContext) : nil
let touchSnapshot = touchSnapshotProducer.takeSnapshot(context: recorderContext)
snapshotProcessor.process(viewTreeSnapshot: viewTreeSnapshot, touchSnapshot: touchSnapshot)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ internal struct TouchSnapshot {
var date: Date
/// The position of this touch in application window.
let position: CGPoint
/// The touch override associated with the touch's view
let touchOverride: TouchPrivacyLevel?
}

enum TouchPhase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,45 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle
private let windowObserver: AppWindowObserver
/// Generates persisted IDs for `UITouch` objects.
private let idsGenerator = TouchIdentifierGenerator()
/// Keeps track of the privacy override for each touch event
internal var overrideForTouch: [TouchIdentifier: (privacyLevel: TouchPrivacyLevel, timestamp: Date)] = [:]
/// Timeout duration for cleaning up stale touches
internal let touchTimeout: TimeInterval = 10.0 // in seconds

/// Touches recorded since last call to `takeSnapshot()`
private var buffer: [TouchSnapshot.Touch] = []

init(windowObserver: AppWindowObserver) {
init(
windowObserver: AppWindowObserver
) {
self.windowObserver = windowObserver
}

func takeSnapshot(context: Recorder.Context) -> TouchSnapshot? {
if let offset = context.viewServerTimeOffset {
buffer = buffer.compactMap {
var touch = $0
touch.date.addTimeInterval(offset)
return touch
let currentTime = Date()
// Remove stale entries from the cache to handle cases where a touch
// never reaches an `.end` phase, preventing leftover entries.
overrideForTouch = overrideForTouch.filter { _, value in
currentTime.timeIntervalSince(value.timestamp) < touchTimeout
}

buffer = buffer.compactMap { touch in
var updatedTouch = touch
if let offset = context.viewServerTimeOffset {
updatedTouch.date.addTimeInterval(offset)
}

// Filter the buffer to only include touches that should be recorded
let shouldRecord = shouldRecordTouch(touch.id, in: context)

// Clean up cache when the touch ends
if touch.phase == .up {
overrideForTouch.removeValue(forKey: touch.id)
}

return shouldRecord ? updatedTouch : nil
}

guard let firstTouch = buffer.first else {
return nil
}
Expand All @@ -40,7 +63,12 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle

// MARK: - UIEventHandler

/// Delegate of `UIApplicationSwizzler` - called each time when `UIApplication` receives an `UIEvent`.
/// Delegate of `UIApplicationSwizzler`.
/// This method is triggered whenever `UIApplication` receives an `UIEvent`.
/// It captures `UITouch` events, determines if the touch should be recorded
/// based on the view hierarchy's touch privacy settings, and appends valid
/// touches to a buffer for later snapshot creation. Touches are only recorded
/// if they are not excluded by any `touchPrivacy` override set on the view or its ancestors.
func notify_sendEvent(application: UIApplication, event: UIEvent) {
guard event.type == .touches,
let window = windowObserver.relevantWindow,
Expand All @@ -54,16 +82,55 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle
continue
}

let touchId = idsGenerator.touchIdentifier(for: touch)

// Capture the touch privacy override when the touch begins
if phase == .down, let privacyOverride = resolveTouchOverride(for: touch) {
overrideForTouch[touchId] = (privacyLevel: privacyOverride, timestamp: Date())
}

buffer.append(
TouchSnapshot.Touch(
id: idsGenerator.touchIdentifier(for: touch),
id: touchId,
phase: phase,
date: Date(),
position: touch.location(in: window)
position: touch.location(in: window),
touchOverride: overrideForTouch[touchId]?.privacyLevel
)
)
}
}

/// Determines whether the touch event should be recorded based on its privacy override and the global privacy settings.
/// If the touch has a specific privacy override, that override is used.
/// Otherwise, the global touch privacy setting is applied.
/// - Parameter touchId: The unique identifier for the touch event.
/// - Returns: `true` if the touch should be recorded, `false` otherwise.
internal func shouldRecordTouch(_ touchId: TouchIdentifier, in context: Recorder.Context
) -> Bool {
let privacy: TouchPrivacyLevel = overrideForTouch[touchId]?.privacyLevel ?? context.touchPrivacy
return privacy == .show
}

/// Resolves the touch privacy override for the given touch by traversing the view hierarchy.
/// It checks the `dd.sessionReplayOverrides.touchPrivacy` property for the view where the touch occurred
/// and its ancestors, if needed. The first non-nil override encountered is returned.
/// - Parameter touch: The touch event to check.
/// - Returns: The `TouchPrivacyLevel` for the view, or `nil` if no override is found.
internal func resolveTouchOverride(for touch: UITouch) -> TouchPrivacyLevel? {
guard let initialView = touch.view else {
return nil
}

var view: UIView? = initialView
while view != nil {
if let touchPrivacy = view?.dd.sessionReplayOverrides.touchPrivacy {
return touchPrivacy
}
view = view?.superview
}
return nil
}
}

internal extension UITouch.Phase {
Expand Down
6 changes: 4 additions & 2 deletions DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ extension TouchSnapshot.Touch: AnyMockable, RandomMockable {
id: .mockRandom(),
phase: [.down, .move, .up].randomElement()!,
date: .mockRandom(),
position: .mockRandom()
position: .mockRandom(),
touchOverride: nil
)
}

Expand All @@ -447,7 +448,8 @@ extension TouchSnapshot.Touch: AnyMockable, RandomMockable {
id: id,
phase: phase,
date: date,
position: position
position: position,
touchOverride: nil
)
}
}
Expand Down
8 changes: 7 additions & 1 deletion DatadogSessionReplay/Tests/Mocks/UIKitMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ extension UIView: AnyMockable, RandomMockable {
class UITouchMock: UITouch {
var _phase: UITouch.Phase
var _location: CGPoint
var _mockedView: UIView

init(phase: UITouch.Phase = .began, location: CGPoint = .zero) {
init(phase: UITouch.Phase = .began, location: CGPoint = .zero, view: UIView = UIView()) {
self._phase = phase
self._location = location
self._mockedView = view
}

override var phase: UITouch.Phase {
Expand All @@ -60,6 +62,10 @@ class UITouchMock: UITouch {
override func location(in view: UIView?) -> CGPoint {
return _location
}

override var view: UIView {
return _mockedView
}
}

class UITouchEventMock: UIEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ class SnapshotProcessorTests: XCTestCase {
id: .mockRandom(min: 0, max: TouchIdentifier(numberOfTouches)),
phase: [.down, .move, .up].randomElement()!,
date: startTime.addingTimeInterval(Double(index) * (dt / Double(numberOfTouches))),
position: .mockRandom()
position: .mockRandom(),
touchOverride: nil
)
}
)
Expand Down
Loading