Skip to content

Commit

Permalink
feat(autocapture): add support for debouncing events for
Browse files Browse the repository at this point in the history
  • Loading branch information
ioannisj committed Oct 23, 2024
1 parent 8b1406f commit 3a0b5f6
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 10 deletions.
48 changes: 42 additions & 6 deletions PostHog/Autocapture/AutocaptureEventProcessing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
}

class PostHogAutocaptureEventProcessor: AutocaptureEventProcessing {
private static let viewHierarchyDelimiter = ";"

private unowned var postHogInstance: PostHogSDK
private weak var postHogInstance: PostHogSDK?
private var debounceTimers: [Int: Timer] = [:]

init(postHogInstance: PostHogSDK) {
self.postHogInstance = postHogInstance
Expand All @@ -26,8 +25,47 @@
PostHogAutocaptureIntegration.removeEventProcessor(self)
}

/**
Processes an autocapture event, with optional debounce logic for controls that emit frequent events.

- Parameters:
- source: The source of the event (e.g., gesture recognizer, action method, or notification).
- event: The autocapture event data, containing properties, screen name, and other metadata.

If the event has a `debounceInterval` greater than 0, the event is debounced.
This is useful for UIControls like `UISlider` that emit frequent value changes, ensuring only the last value is captured.
The debounce interval is defined per UIControl by the `ph_autocaptureDebounceInterval` property of `AutoCapturable`
*/
func process(source: PostHogAutocaptureIntegration.EventData.EventSource, event: PostHogAutocaptureIntegration.EventData) {

assert(Thread.isMainThread, "Event captured off main thread")

let eventHash = event.hashValue
// debounce frequent UIControl events (e.g., UISlider) to reduce event noise
if event.debounceInterval > 0 {
debounceTimers[eventHash]?.invalidate() // Keep cancelling existing
debounceTimers[eventHash] = Timer.scheduledTimer(withTimeInterval: event.debounceInterval, repeats: false) { [weak self] _ in
self?.handleEventProcessing(source: source, event: event)
self?.debounceTimers.removeValue(forKey: eventHash) // Clean up once fired
}
} else {
handleEventProcessing(source: source, event: event)
}
}

private static let viewHierarchyDelimiter = ";"
/**
Handles the processing of autocapture events by extracting event details, building properties, and sending them to PostHog.

- Parameters:
- source: The source of the event, which could be an action method, gesture recognizer, or notification. Associated values are already mapped to `$event_type` earlier in the chain
- event: The event data including view hierarchy, screen name, and other metadata.

This function extracts event details such as the event type, view hierarchy, and touch coordinates.
It creates a structured payload with relevant properties (e.g., tag_name, elements, element_chain) and sends it to the associated PostHog instance for further processing.
*/
private func handleEventProcessing(source: PostHogAutocaptureIntegration.EventData.EventSource, event: PostHogAutocaptureIntegration.EventData) {
guard let postHogInstance else { return }

let eventType: String = switch source {
case let .actionMethod(description): description
case let .gestureRecognizer(description): description
Expand Down Expand Up @@ -59,8 +97,6 @@
properties["$touch_x"] = coordinates.x
properties["$touch_y"] = coordinates.y
}

hedgeLog("autocaptured \"\(eventType)\" in \(elements.first!.description) with \(properties) ")

postHogInstance.autocapture(
eventType: eventType,
Expand Down
19 changes: 15 additions & 4 deletions PostHog/Autocapture/PostHogAutocaptureIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
// TODO: Rage Clicks - possible?
// TODO: Dead Clicks - possible?
class PostHogAutocaptureIntegration {
struct EventData {
struct ViewNode: CustomStringConvertible {
struct EventData: Hashable {
struct ViewNode: CustomStringConvertible, Hashable {
let text: String
let targetClass: String
let index: Int
Expand All @@ -40,6 +40,12 @@
let targetClass: String
let accessibilityLabel: String?
let accessibilityIdentifier: String?
// values >0 means that this event will be debounced for `debounceInterval`
let debounceInterval: TimeInterval

func hash(into hasher: inout Hasher) {
hasher.combine(viewHierarchy.map(\.targetClass))
}
}

// static -> won't be added twice
Expand Down Expand Up @@ -166,7 +172,8 @@
.map { $1.viewNode(index: $0) },
targetClass: descriptiveTypeName,
accessibilityLabel: accessibilityLabel,
accessibilityIdentifier: accessibilityIdentifier
accessibilityIdentifier: accessibilityIdentifier,
debounceInterval: ph_autocaptureDebounceInterval
)
}
}
Expand Down Expand Up @@ -287,12 +294,14 @@
protocol AutoCapturable {
var ph_autocaptureText: String? { get }
var ph_autocaptureEvents: UIControl.Event { get }
var ph_autocaptureDebounceInterval: TimeInterval { get }
func ph_shouldTrack(_ action: Selector, for target: Any?) -> Bool
}

extension UIView: AutoCapturable {
@objc var ph_autocaptureEvents: UIControl.Event { .touchUpInside }
@objc var ph_autocaptureText: String? { nil }
@objc var ph_autocaptureDebounceInterval: TimeInterval { 0 }
@objc func ph_shouldTrack(_: Selector, for _: Any?) -> Bool {
false // by default views are not tracked. Can be overriden in subclasses
}
Expand Down Expand Up @@ -341,7 +350,9 @@
}

extension UISlider {
override var ph_autocaptureEvents: UIControl.Event { .touchUpInside }
override var ph_autocaptureDebounceInterval: TimeInterval { 0.3 }
override var ph_autocaptureEvents: UIControl.Event { .valueChanged }
override var ph_autocaptureText: String? { "\(value)" }
}

#if !os(tvOS)
Expand Down

0 comments on commit 3a0b5f6

Please sign in to comment.