Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ final class ObservabilityService: InternalObserve {
)
self.tracer = appTraceClient

let userInteractionManager = UserInteractionManager(options: options) { interaction in
let userInteractionManager = UserInteractionManager(options: options, sessionManaging: sessionManager) { interaction in
interaction.startEndSpan(tracer: tracerDecorator)
}
self.userInteractionManager = userInteractionManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ extension SessionManaging {
func start(sessionId: String) {
start(sessionId: sessionId, isCustomSession: true)
}

public var sessionIdProvider: @Sendable () -> String {
{ [self] in self.sessionInfo.id }
}
}

final class SessionManager: SessionManaging {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ struct TouchSample: Sendable {
// relative to system startup time
let timestamp: TimeInterval
let target: TouchTarget?
/// Session id fixed at main-thread capture time (with location/target).
let sessionId: String

init(touch: UITouch, window: UIWindow, target: TouchTarget?) {
init(touch: UITouch, window: UIWindow, target: TouchTarget?, sessionId: String) {
self.id = ObjectIdentifier(touch)
self.location = touch.location(in: window)
self.timestamp = touch.timestamp
self.target = target
self.sessionId = sessionId
self.phase = switch touch.phase {
case .began: .began
case .moved: .moved
Expand All @@ -47,16 +50,19 @@ final class InputCaptureCoordinator {
private let touchInterpreter: TouchInterpreter
private let pressInterpreter: PressInterpreter
private let receiverChecker: UIEventReceiverChecker
private let sessionIdProvider: @Sendable () -> String
var onTouch: TouchInteractionYield?
var onPress: PressInteractionYield?

init(targetResolver: TargetResolving = TargetResolver(),
receiverChecker: UIEventReceiverChecker = UIEventReceiverChecker()) {
receiverChecker: UIEventReceiverChecker = UIEventReceiverChecker(),
sessionIdProvider: @Sendable @escaping () -> String) {
Comment thread
mario-launchdarkly marked this conversation as resolved.
self.targetResolver = targetResolver
self.touchInterpreter = TouchInterpreter()
self.pressInterpreter = PressInterpreter()
self.source = UIWindowSwizzleSource()
self.receiverChecker = receiverChecker
self.sessionIdProvider = sessionIdProvider
}

func start() {
Expand Down Expand Up @@ -108,14 +114,16 @@ final class InputCaptureCoordinator {
continuation: AsyncStream<InteractionCaptureItem>.Continuation
) {
guard let touches = event.allTouches else { return }
let sessionId = sessionIdProvider()
for touch in touches {
guard touch.phase == .began else { continue }
let target = targetResolver.resolve(view: touch.view, window: window, event: event)
let interaction = PressInteraction(
phase: PressInteraction.phase(forTouch: touch.phase),
kind: .untrackedWindowTouch,
timestamp: touch.timestamp,
target: target
target: target,
sessionId: sessionId
)
continuation.yield(.press(interaction))
}
Expand All @@ -127,6 +135,7 @@ final class InputCaptureCoordinator {
continuation: AsyncStream<InteractionCaptureItem>.Continuation
) {
guard let touches = event.allTouches else { return }
let sessionId = sessionIdProvider()
for touch in touches {
let target: TouchTarget?
if touch.phase == .began || touch.phase == .ended {
Expand All @@ -135,7 +144,12 @@ final class InputCaptureCoordinator {
target = nil
}

let touchSample = TouchSample(touch: touch, window: window, target: target)
let touchSample = TouchSample(
touch: touch,
window: window,
target: target,
sessionId: sessionId
)
Comment thread
cursor[bot] marked this conversation as resolved.
continuation.yield(.touch(touchSample))
}
}
Expand All @@ -146,10 +160,11 @@ final class InputCaptureCoordinator {
continuation: AsyncStream<InteractionCaptureItem>.Continuation
) {
guard let pressesEvent = event as? UIPressesEvent else { return }
let sessionId = sessionIdProvider()
for press in pressesEvent.allPresses {
guard press.phase == .began else { continue }
let target = targetResolver.resolve(press: press, window: window)
let interaction = PressInteraction(press: press, target: target)
let interaction = PressInteraction(press: press, target: target, sessionId: sessionId)
if case .other = interaction.kind { continue }

continuation.yield(.press(interaction))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,26 @@ public struct PressInteraction: Sendable {
public let kind: RemotePressKind
public let timestamp: TimeInterval
public let target: TouchTarget?
public let sessionId: String

public var isKeyboard: Bool {
kind == .keyboard || kind == .untrackedWindowTouch
}

init(press: UIPress, target: TouchTarget?) {
init(press: UIPress, target: TouchTarget?, sessionId: String) {
self.phase = Self.phase(for: press.phase)
self.kind = press.key != nil ? .keyboard : RemotePressKind(pressType: press.type)
self.timestamp = press.timestamp
self.target = target
self.sessionId = sessionId
}

init(phase: Phase, kind: RemotePressKind = .untrackedWindowTouch, timestamp: TimeInterval, target: TouchTarget?) {
init(phase: Phase, kind: RemotePressKind = .untrackedWindowTouch, timestamp: TimeInterval, target: TouchTarget?, sessionId: String) {
self.phase = phase
self.kind = kind
self.timestamp = timestamp
self.target = target
self.sessionId = sessionId
}

static func phase(forTouch touchPhase: UITouch.Phase) -> Phase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ final class PressInterpreter {
phase: pressInteraction.phase,
kind: pressInteraction.kind,
timestamp: pressInteraction.timestamp + uptimeDifference,
target: pressInteraction.target
target: pressInteraction.target,
sessionId: pressInteraction.sessionId
)
yield(corrected)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public struct TouchInteraction: Sendable {
public let startTimestamp: TimeInterval
public let timestamp: TimeInterval
public let target: TouchTarget?
public let sessionId: String
}

public enum SwipeDirection: Sendable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class TouchInterpreter {
}

func process(touchSample: TouchSample, yield: TouchInteractionYield) {
let sessionId = touchSample.sessionId
// UITouch and UIEvent use time based on systemUptime getting and we needed adjustment for proper time
let uptimeDifference = Date().timeIntervalSince1970 - ProcessInfo.processInfo.systemUptime
switch touchSample.phase {
Expand All @@ -35,86 +36,91 @@ final class TouchInterpreter {
points: [TouchPoint(position: touchSample.location, timestamp: touchSample.timestamp + uptimeDifference)],
target: touchSample.target)
tracks[touchSample.id] = track

let downInteraction = TouchInteraction(id: incrementingId,
kind: .touchDown(touchSample.location),
startTimestamp: touchSample.timestamp + uptimeDifference,
timestamp: touchSample.timestamp + uptimeDifference,
target: touchSample.target)
target: touchSample.target,
sessionId: sessionId)
yield(downInteraction)

case .moved:
guard var track = tracks[touchSample.id] else { return }

let trackDuration = touchSample.timestamp - track.start
guard trackDuration <= TouchConstants.touchPathDuration else {
// flush movements of long touch path do not have dead time in the replay player
let lastPoint = TouchPoint(position: touchSample.location, timestamp: touchSample.timestamp + uptimeDifference)
track.points.append(lastPoint)
track.target = touchSample.target

let moveInteraction = TouchInteraction(id: incrementingId,
kind: .touchPath(points: track.points),
startTimestamp: track.start + uptimeDifference,
timestamp: touchSample.timestamp + uptimeDifference,
target: touchSample.target)
target: touchSample.target,
sessionId: sessionId)
track.points.removeAll()
track.start = lastPoint.timestamp - uptimeDifference
track.startPoint = touchSample.location
tracks[touchSample.id] = track
yield(moveInteraction)
return
}

if let prevPoint = tracks[touchSample.id]?.points.last {
let duration = touchSample.timestamp + uptimeDifference - prevPoint.timestamp
guard duration >= TouchConstants.touchMoveThrottle else {
return
}
}

let distance = squaredDistance(from: track.startPoint, to: touchSample.location)
guard distance >= TouchConstants.tapMaxDistanceSquared else {
return
}

track.end = touchSample.timestamp
track.target = touchSample.target
track.points.append(TouchPoint(position: touchSample.location, timestamp: touchSample.timestamp + uptimeDifference))
tracks[touchSample.id] = track

case .ended, .cancelled:
// touchUp
let startTimestamp = tracks[touchSample.id]?.start ?? touchSample.timestamp
let upInteraction = TouchInteraction(id: incrementingId,
kind: .touchUp(touchSample.location),
startTimestamp: startTimestamp + uptimeDifference,
timestamp: touchSample.timestamp + uptimeDifference,
target: touchSample.target)
target: touchSample.target,
sessionId: sessionId)
yield(upInteraction)

// touchPath
guard let track = tracks.removeValue(forKey: touchSample.id), track.points.isNotEmpty else { return }

let moveInteraction = TouchInteraction(id: incrementingId,
kind: .touchPath(points: track.points),
startTimestamp: startTimestamp + uptimeDifference,
timestamp: touchSample.timestamp + uptimeDifference,
target: touchSample.target)
target: touchSample.target,
sessionId: sessionId)
yield(moveInteraction)
case .unknown:
() //NOOP
}
}

func flushMovements(touchSample: TouchSample, uptimeDifference: TimeInterval, startTimestamp: TimeInterval, yield: TouchInteractionYield) {
guard var track = tracks[touchSample.id], track.points.isNotEmpty else { return }

let moveInteraction = TouchInteraction(id: incrementingId,
kind: .touchPath(points: track.points),
startTimestamp: startTimestamp + uptimeDifference,
timestamp: touchSample.timestamp + uptimeDifference,
target: touchSample.target)
target: touchSample.target,
sessionId: touchSample.sessionId)
if let lastPoint = track.points.last {
track.points.removeAll()
track.start = lastPoint.timestamp - uptimeDifference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ public final class UserInteractionManager {
interactionEventSubject.eraseToAnyPublisher()
}

init(options: ObservabilityOptions, yield: @escaping TouchInteractionYield) {
init(options: ObservabilityOptions, sessionManaging: SessionManaging, yield: @escaping TouchInteractionYield) {
let targetResolver = TargetResolver()
self.inputCaptureCoordinator = InputCaptureCoordinator(targetResolver: targetResolver)
self.inputCaptureCoordinator = InputCaptureCoordinator(
targetResolver: targetResolver,
sessionIdProvider: sessionManaging.sessionIdProvider
)
self.inputCaptureCoordinator.onTouch = { [interactionEventSubject] interaction in
yield(interaction)
interactionEventSubject.send(.touch(interaction))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public final class CompressionBenchmarkRunner {
var captureTime: TimeInterval = 0

let start = CFAbsoluteTimeGetCurrent()
let sessionId = SessionInfo().id

for frame in frames {
let captureStart = CFAbsoluteTimeGetCurrent()
Expand All @@ -27,7 +28,7 @@ public final class CompressionBenchmarkRunner {
continue
}

let item = EventQueueItem(payload: ImageItemPayload(exportFrame: exportFrame))
let item = EventQueueItem(payload: ImageItemPayload(exportFrame: exportFrame, sessionId: sessionId))
let events = await eventGenerator.generateEvents(items: [item])

if let data = try? encoder.encode(events) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import LaunchDarklyObservability
struct IdentifyItemPayload: EventQueueItemPayload {
let attributes: [String: String]
var timestamp: TimeInterval
let sessionId: String

var exporterClass: AnyClass {
SessionReplayExporter.self
}

func cost() -> Int {
attributes.count * 100
}
Expand Down Expand Up @@ -57,7 +58,7 @@ extension IdentifyItemPayload {
}

@MainActor
init(options: ObservabilityOptions, sessionAttributes: [String: AttributeValue]?, ldContext: LDContext? = nil, timestamp: TimeInterval) {
init(options: ObservabilityOptions, sessionAttributes: [String: AttributeValue]?, ldContext: LDContext? = nil, timestamp: TimeInterval, sessionId: String) {
let canonicalKey = ldContext?.fullyQualifiedKey() ?? "unknown"
let contextKeys = ldContext?.contextKeys() ?? [:]

Expand All @@ -68,18 +69,20 @@ extension IdentifyItemPayload {
canonicalKey: canonicalKey
)
self.timestamp = timestamp
self.sessionId = sessionId
}

/// Proxy-friendly initialiser that accepts pre-extracted context keys
/// instead of LDContext, so the MAUI bridge can call it with simple types.
init(options: ObservabilityOptions, sessionAttributes: [String: AttributeValue]?, contextKeys: [String: String], canonicalKey: String, timestamp: TimeInterval) {
init(options: ObservabilityOptions, sessionAttributes: [String: AttributeValue]?, contextKeys: [String: String], canonicalKey: String, timestamp: TimeInterval, sessionId: String) {
self.attributes = Self.buildAttributes(
options: options,
sessionAttributes: sessionAttributes,
contextKeys: contextKeys,
canonicalKey: canonicalKey
)
self.timestamp = timestamp
self.sessionId = sessionId
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ struct ImageItemPayload: EventQueueItemPayload {
var exporterClass: AnyClass {
SessionReplayExporter.self
}

var timestamp: TimeInterval {
exportFrame.timestamp
}

func cost() -> Int {
exportFrame.addImages.reduce(0) { $0 + $1.data.count }
}

let exportFrame: ExportFrame
let sessionId: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ actor SessionReplayExporter: EventExporting {
let session = try await initializeSession(sessionSecureId: sessionInfo.id)
var identifyPayload = self.identifyPayload
if identifyPayload == nil {
identifyPayload = await IdentifyItemPayload(options: context.observabilityContext.options, sessionAttributes: context.observabilityContext.sessionAttributes, timestamp: Date().timeIntervalSince1970)
identifyPayload = await IdentifyItemPayload(options: context.observabilityContext.options, sessionAttributes: context.observabilityContext.sessionAttributes, timestamp: Date().timeIntervalSince1970, sessionId: sessionInfo.id)
}
if let identifyPayload {
try await identifySession(sessionSecureId: session.secureId, userObject: identifyPayload.attributes)
Expand Down
Loading
Loading