Skip to content

Commit 025ea17

Browse files
feat: add tap handler, send span
1 parent df712ab commit 025ea17

File tree

8 files changed

+181
-0
lines changed

8 files changed

+181
-0
lines changed

Sources/Client/InstrumentationManager.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import OpenTelemetrySdk
44
import StdoutExporter
55
import OpenTelemetryProtocolExporterHttp
66

7+
import UIKit.UIWindow
8+
79
import API
810
import Interfaces
911
import Common
@@ -26,6 +28,7 @@ public final class InstrumentationManager {
2628
private var cachedLongCounters = AtomicDictionary<String, LongCounter>()
2729
private var cachedHistograms = AtomicDictionary<String, DoubleHistogram>()
2830
private var cachedUpDownCounters = AtomicDictionary<String, DoubleUpDownCounter>()
31+
private let lock: NSLock = NSLock()
2932

3033
public init(sdkKey: String, options: Options, sessionManager: SessionManager) {
3134
self.sdkKey = sdkKey
@@ -143,8 +146,23 @@ public final class InstrumentationManager {
143146
self.otelMeter = OpenTelemetry.instance.meterProvider.get(
144147
name: options.serviceName
145148
)
149+
150+
// Task { @MainActor [weak self] in
151+
// for await touchEvent in TapHandler.stream() {
152+
// print("touch event: \(touchEvent)")
153+
// var attributes = [String: AttributeValue]()
154+
// attributes["screen.name"] = .string(touchEvent.viewName)
155+
// attributes["target.id"] = .string(touchEvent.accessibilityIdentifier ?? touchEvent.viewName)
156+
// // sending location in points (since it is preferred over pixels)
157+
// attributes["position.x"] = .string(touchEvent.locationInPoints.x.toString())
158+
// attributes["position.y"] = .string(touchEvent.locationInPoints.y.toString())
159+
// self?.startSpan(name: "", attributes: attributes)
160+
// }
161+
// }
146162
}
147163

164+
// MARK: - Instrumentation
165+
148166
func recordMetric(metric: Metric) {
149167
var gauge = cachedGauges[metric.name]
150168
if gauge == nil {

Sources/Client/ObservabilityClient.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,27 @@ public final class ObservabilityClient: Observe {
6060
onWillEndSession: onWillEndSession,
6161
onDidStartSession: onDidStartSession
6262
)
63+
64+
nonisolated(unsafe) let obj = self
65+
66+
Task { @MainActor in
67+
for await touchEvent in TapHandler.stream() {
68+
print("touch event: \(touchEvent)")
69+
var attributes = [String: AttributeValue]()
70+
attributes["screen.name"] = .string(touchEvent.viewName)
71+
attributes["target.id"] = .string(touchEvent.accessibilityIdentifier ?? touchEvent.viewName)
72+
// sending location in points (since it is preferred over pixels)
73+
attributes["position.x"] = .string(touchEvent.locationInPoints.x.toString())
74+
attributes["position.y"] = .string(touchEvent.locationInPoints.y.toString())
75+
obj
76+
.startSpan(name: "user.tap", attributes: attributes)
77+
.end()
78+
}
79+
}
6380
}
6481

82+
// MARK: - Instrumentation
83+
6584
public func recordMetric(metric: Metric) {
6685
instrumentationManager.recordMetric(metric: metric)
6786
}

Sources/Client/TapHandler.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import UIKit.UIWindow
2+
import Common
3+
4+
@MainActor public final class TapHandler {
5+
private var startPoint: CGPoint?
6+
7+
public init() {}
8+
9+
public func handle(event: UIEvent, window: UIWindow, completion: (TouchEvent) -> Void) {
10+
if let touches = event.allTouches, let touch = touches.first, let targetView = touch.view {
11+
switch touch.phase {
12+
case .began:
13+
startPoint = touch.location(in: window)
14+
case .ended:
15+
if let startPoint {
16+
let endPoint = touch.location(in: window)
17+
let dx = endPoint.x - startPoint.x
18+
let dy = endPoint.y - startPoint.y
19+
if abs(dx) < 10 && abs(dy) < 10 {
20+
print("Tap")
21+
let accessibilityIdentifier = targetView.accessibilityIdentifier
22+
let targetClass = type(of: targetView)
23+
24+
let viewName = accessibilityIdentifier ?? String(describing: targetClass)
25+
26+
completion(
27+
.init(
28+
location: endPoint,
29+
viewName: viewName,
30+
accessibilityIdentifier: accessibilityIdentifier,
31+
scale: targetView.window?.screen.scale ?? UIScreen.main.scale)
32+
)
33+
}
34+
}
35+
startPoint = nil
36+
default:
37+
break
38+
}
39+
}
40+
}
41+
}
42+
43+
extension TapHandler {
44+
public static func stream() -> AsyncStream<TouchEvent> {
45+
AsyncStream<TouchEvent> { continuation in
46+
let handler = TapHandler()
47+
UIWindowSendEvent.inject { uiWindow, uiEvent in
48+
handler.handle(event: uiEvent, window: uiWindow) { touchEvent in
49+
continuation.yield(touchEvent)
50+
}
51+
}
52+
}
53+
}
54+
}

Sources/Client/TouchEvent.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import UIKit
2+
3+
public struct TouchEvent: Sendable, CustomStringConvertible {
4+
public let location: CGPoint
5+
public let viewName: String
6+
public let accessibilityIdentifier: String?
7+
public let scale: CGFloat
8+
9+
public var locationInPoints: CGPoint {
10+
return location
11+
}
12+
13+
public var locationInPixels: CGPoint {
14+
return CGPoint(x: Int((location.x * scale).rounded()), y: Int((location.y * scale).rounded()))
15+
}
16+
17+
public var description: String {
18+
return "TouchEvent(\(location), \(viewName), \(accessibilityIdentifier ?? "nil"), \(scale)) coordinates In pixels: \(locationInPixels.x), \(locationInPixels.y)"
19+
}
20+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
3+
extension CGFloat {
4+
public func toString() -> String {
5+
String(format: "%.2f", self)
6+
}
7+
}

Sources/Common/Swizzler.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
3+
public struct Swizzle<Signature, Block> {
4+
private let baseClass: AnyClass
5+
6+
public init(baseClass: AnyClass) {
7+
self.baseClass = baseClass
8+
}
9+
10+
public func swizzle(selector: Selector, block: (Signature) -> Block) {
11+
guard let method = class_getInstanceMethod(baseClass, selector) else {
12+
return
13+
}
14+
let existingImplementation = method_getImplementation(method)
15+
let existingImplementationType = unsafeBitCast(
16+
existingImplementation,
17+
to: Signature.self)
18+
let blockImplementation: Block = block(existingImplementationType)
19+
let newImplementation = imp_implementationWithBlock(blockImplementation)
20+
method_setImplementation(method, newImplementation)
21+
}
22+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import UIKit.UIWindow
2+
3+
public final class UIWindowSendEvent {
4+
typealias SendEventRef = @convention(c) (UIWindow, Selector, UIEvent) -> Void
5+
private static let sendEvenSelector = #selector(UIWindow.sendEvent(_:))
6+
7+
public static func inject(
8+
into subclasses: [UIWindow.Type] = [],
9+
block: @escaping (UIWindow, UIEvent) -> Void
10+
) {
11+
guard let originalMethod = class_getInstanceMethod(UIWindow.self, sendEvenSelector) else { return }
12+
13+
var originalIMP = Optional<IMP>.none
14+
15+
let swizzledSendEventBlock: @convention(block) (UIWindow, UIEvent) -> Void = { window, event in
16+
if let originalIMP = originalIMP {
17+
let castedIMP = unsafeBitCast(originalIMP, to: SendEventRef.self)
18+
castedIMP(window, sendEvenSelector, event)
19+
}
20+
21+
guard !subclasses.isEmpty else {
22+
return block(window, event)
23+
}
24+
if UIWindowSendEvent.shouldInject(into: window, subclasses: subclasses) {
25+
block(window, event)
26+
}
27+
}
28+
29+
let swizzledIMP = imp_implementationWithBlock(unsafeBitCast(swizzledSendEventBlock, to: AnyObject.self))
30+
originalIMP = method_setImplementation(originalMethod, swizzledIMP)
31+
}
32+
33+
private static func shouldInject(into receiver: Any, subclasses: [UIWindow.Type]) -> Bool {
34+
let ids = subclasses.map { ObjectIdentifier($0) }
35+
let receiverType = type(of: receiver)
36+
return ids.contains(ObjectIdentifier(receiverType))
37+
}
38+
}

Sources/CrashReporterLive/LaunchDarklyCrashFilter.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ final class LaunchDarklyCrashFilter: NSObject, CrashReportFilter {
6262
let reportSections = crashReportString.components(separatedBy: "\\n")
6363
let incidentIdentifier = reportSections.first(where: { $0.contains(ReportSection.incidentIdentifier.rawValue) }) ?? ""
6464
let exceptionType = reportSections.first(where: { $0.contains(ReportSection.exceptionType.rawValue) }) ?? ""
65+
let exceptionCodes = reportSections.first(where: { $0.contains(ReportSection.exceptionCodes.rawValue) }) ?? ""
6566

6667
var attributes = [String: AttributeValue]()
6768
attributes[SemanticAttributes.exceptionType.rawValue] = .string(exceptionType.replacingOccurrences(of: "\"", with: ""))
6869
attributes[SemanticAttributes.exceptionStacktrace.rawValue] = .string(crashReportString)
70+
attributes[SemanticAttributes.exceptionMessage.rawValue] = .string(exceptionCodes)
71+
6972
logger?.logRecordBuilder()
7073
.setAttributes(attributes)
7174
.setBody(.string(incidentIdentifier.replacingOccurrences(of: "\"", with: "")))

0 commit comments

Comments
 (0)