Skip to content

Commit fcf4c9e

Browse files
authored
[HEAP-37223] Add HeapBridgeSupport.trackInteraction (#97)
1 parent 8175343 commit fcf4c9e

File tree

7 files changed

+597
-20
lines changed

7 files changed

+597
-20
lines changed

Demos/Webview Demos/Webview Demos/javascript-bridge.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,42 @@
7676
new Date(),
7777
source
7878
);
79+
80+
document.addEventListener('click', (e) => {
81+
82+
const eTarget = e.target;
83+
if (!eTarget) { return; }
84+
85+
const nodes = [];
86+
const attributeNames = ['aria-role', 'type'];
87+
88+
for (let target = eTarget; target && target.nodeName; target = target.parentElement) {
89+
90+
const attributes = {};
91+
for (const name of attributeNames) {
92+
attributes[name] = target.getAttribute(name)
93+
}
94+
95+
96+
nodes.push({
97+
nodeName: target.nodeName,
98+
nodeId: target.id || undefined,
99+
nodeHtmlClass: target.className || undefined,
100+
accessibilityLabel: target.getAttribute('aria-label'),
101+
attributes,
102+
});
103+
}
104+
105+
nodes[0].nodeText = eTarget.textContent;
106+
107+
Heap.trackInteraction(
108+
'click',
109+
nodes,
110+
null,
111+
new Date(),
112+
source
113+
);
114+
});
79115
}
80116

81117
document.getElementById('startAutocapture').addEventListener('click', function (e) {

Development/Sources/HeapSwiftCore/HeapBridgeSupport/HeapBridgeSupport.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class HeapBridgeSupport
3232
return try track(arguments: arguments)
3333
case "trackPageview":
3434
return try trackPageview(arguments: arguments)
35+
case "trackInteraction":
36+
return try trackInteraction(arguments: arguments)
3537
case "identify":
3638
return try identify(arguments: arguments)
3739
case "resetIdentity":
@@ -118,6 +120,17 @@ public class HeapBridgeSupport
118120
]
119121
}
120122

123+
func trackInteraction(arguments: [String: Any]) throws -> JSONEncodable? {
124+
let interaction = try getRequiredInteraction(from: arguments, methodName: "trackInteraction")
125+
let nodes = try getRequiredInteractionNodes(from: arguments, methodName: "trackInteraction")
126+
let callbackName = try getOptionalString(named: "callbackName", from: arguments, message: "HeapBridgeSupport.trackInteraction received an invalid callback name and will not complete the bridged method call.")
127+
let timestamp = try getOptionalTimestamp(arguments, methodName: "trackInteraction")
128+
let sourceInfo = try getOptionalSourceLibrary(arguments, methodName: "trackInteraction")
129+
let pageview = try getOptionalPageview(from: arguments, methodName: "trackInteraction")
130+
eventConsumer.trackInteraction(interaction: interaction, nodes: nodes, callbackName: callbackName, timestamp: timestamp, sourceInfo: sourceInfo, pageview: pageview)
131+
return nil
132+
}
133+
121134
func identify(arguments: [String: Any]) throws -> JSONEncodable? {
122135
let identity = try getRequiredString(named: "identity", from: arguments, message: "HeapBridgeSupport.identify received an invalid identity and will not complete the bridged method call.")
123136
eventConsumer.identify(identity)
@@ -284,6 +297,71 @@ extension HeapBridgeSupport {
284297
return pageview
285298
}
286299

300+
func getRequiredInteraction(from arguments: [String: Any], methodName: String) throws -> Interaction {
301+
guard let rawValue = arguments["interaction"] else {
302+
HeapLogger.shared.debug("HeapBridgeSupport.\(methodName) received an event without an interaction type and will not complete the bridged method call.")
303+
throw InvocationError.invalidParameters
304+
}
305+
306+
if let builtinName = rawValue as? String {
307+
switch builtinName {
308+
case "unspecified": return .unspecified
309+
case "click": return .click
310+
case "touch": return .touch
311+
case "change": return .change
312+
case "submit": return .submit
313+
default:
314+
HeapLogger.shared.debug("HeapBridgeSupport.\(methodName) received an an unknown interaction type, \(builtinName), and will not complete the bridged method call.")
315+
throw InvocationError.invalidParameters
316+
}
317+
}
318+
319+
if let rawDictionary = rawValue as? [String: Any] {
320+
if let name = rawDictionary["custom"] as? String {
321+
return .custom(name)
322+
}
323+
324+
if let value = rawDictionary["builtin"] as? Int {
325+
return .builtin(value)
326+
}
327+
}
328+
329+
HeapLogger.shared.debug("HeapBridgeSupport.\(methodName) received an an invalid interaction type and will not complete the bridged method call.")
330+
throw InvocationError.invalidParameters
331+
}
332+
333+
func getRequiredInteractionNodes(from arguments: [String: Any], methodName: String) throws -> [InteractionNode] {
334+
335+
guard
336+
let rawArray = arguments["nodes"] as? [Any],
337+
!rawArray.isEmpty
338+
else {
339+
HeapLogger.shared.debug("HeapBridgeSupport.\(methodName) received an event without a list of nodes and will not complete the bridged method call.")
340+
throw InvocationError.invalidParameters
341+
}
342+
343+
return try rawArray.map(getInteractionNode(_:))
344+
345+
func message() -> String { "HeapBridgeSupport.\(methodName) received an invalid list of nodes and will not complete the bridged method call." }
346+
347+
func getInteractionNode(_ rawNode: Any) throws -> InteractionNode {
348+
guard let rawObject = rawNode as? [String: Any] else {
349+
HeapLogger.shared.debug(message())
350+
throw InvocationError.invalidParameters
351+
}
352+
353+
var node = InteractionNode(nodeName: try getRequiredString(named: "nodeName", from: rawObject, message: message()))
354+
node.nodeText = try getOptionalString(named: "nodeText", from: rawObject, message: message())
355+
node.nodeHtmlClass = try getOptionalString(named: "nodeHtmlClass", from: rawObject, message: message())
356+
node.nodeId = try getOptionalString(named: "nodeId", from: rawObject, message: message())
357+
node.href = try getOptionalString(named: "href", from: rawObject, message: message())
358+
node.accessibilityLabel = try getOptionalString(named: "accessibilityLabel", from: rawObject, message: message())
359+
node.referencingPropertyName = try getOptionalString(named: "referencingPropertyName", from: rawObject, message: message())
360+
node.attributes = try getOptionalParameterDictionary(named: "attributes", from: rawObject, message: message())
361+
return node
362+
}
363+
}
364+
287365
func getOptionalArrayOfStrings(named name: String, from arguments: [String: Any], message: @autoclosure () -> String) throws -> [String] {
288366
guard let rawArray = arguments[name] else {
289367
return []

Development/Sources/HeapSwiftCore/Implementations/WebviewBridge/HeapWebviewBridgeJavaScript.swift

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

Development/Sources/HeapSwiftCoreTestSupport/Helpers/DataStoreProtocol.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import Foundation
22
@testable import HeapSwiftCore
33

4+
struct SessionMessages {
5+
let sessionMessage: Message?
6+
let initialPageviewMessage: Message?
7+
let versionChangeMessage: Message?
8+
let postStartMessages: [Message]
9+
}
10+
411
extension DataStoreProtocol {
512

613
func getPendingMessages(for user: UserToUpload, sessionId: String?, messageLimit: Int = .max, file: StaticString = #file, line: UInt = #line) throws -> [Message] {
@@ -40,6 +47,64 @@ extension DataStoreProtocol {
4047
return messages
4148
}
4249

50+
@discardableResult
51+
func assertOnlySession(hasPostStartMessageCount count: Int, file: StaticString = #file, line: UInt = #line) throws -> SessionMessages {
52+
53+
let users = usersToUpload().filter({ !$0.sessionIds.isEmpty })
54+
55+
guard users.count == 1 else {
56+
throw TestFailure("Expected a single user with sessions but found \(users.map(\.userId))", file: file, line: line)
57+
}
58+
59+
return try assertOnlySession(for: users[0], hasPostStartMessageCount: count, file: file, line: line)
60+
}
61+
62+
@discardableResult
63+
func assertOnlySession(for user: UserToUpload, hasPostStartMessageCount count: Int, file: StaticString = #file, line: UInt = #line) throws -> SessionMessages {
64+
guard user.sessionIds.count == 1 else {
65+
throw TestFailure("Expected a single session but found \(user.sessionIds)", file: file, line: line)
66+
}
67+
return try assertSession(for: user, sessionId: user.sessionIds[0], hasPostStartMessageCount: count, file: file, line: line)
68+
}
69+
70+
@discardableResult
71+
func assertSession(for user: UserToUpload, sessionId: String?, hasPostStartMessageCount count: Int, file: StaticString = #file, line: UInt = #line) throws -> SessionMessages {
72+
73+
var messages = try getPendingMessages(for: user, sessionId: sessionId, file: file, line: line)
74+
75+
var sessionMessage: Message?
76+
let initialPageviewMessage: Message?
77+
let versionChangeMessage: Message?
78+
79+
if case .session(_) = messages.first?.kind {
80+
sessionMessage = messages.removeFirst()
81+
} else {
82+
sessionMessage = nil
83+
}
84+
85+
if let message = messages.first,
86+
case .pageview(_) = message.kind,
87+
!message.pageviewInfo.hasComponentOrClassName,
88+
!message.pageviewInfo.hasTitle,
89+
!message.pageviewInfo.hasURL {
90+
initialPageviewMessage = messages.removeFirst()
91+
} else {
92+
initialPageviewMessage = nil
93+
}
94+
95+
if case .versionChange(_) = messages.first?.event.kind {
96+
versionChangeMessage = messages.removeFirst()
97+
} else {
98+
versionChangeMessage = nil
99+
}
100+
101+
guard messages.count == count else {
102+
throw TestFailure("Expected exactly \(count) messages after session start messages, but got \(messages.count)", file: file, line: line)
103+
}
104+
105+
return .init(sessionMessage: sessionMessage, initialPageviewMessage: initialPageviewMessage, versionChangeMessage: versionChangeMessage, postStartMessages: messages)
106+
}
107+
43108
@discardableResult
44109
func assertOnlyOneUserToUpload(message: String? = nil, file: StaticString = #file, line: UInt = #line) throws -> UserToUpload {
45110

Development/Sources/HeapSwiftCoreTestSupport/Helpers/Message.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ extension Message {
117117
}
118118
validateBaseMessage(file: file, line: line, user: user, id: nil, timestamp: timestamp, hasSourceLibrary: hasSourceLibrary, sourceLibrary: sourceLibrary, eventProperties: eventProperties)
119119

120-
expect(event.interaction.kind).to(equal(interaction), description: "The event does not match expected Kind")
121-
expect(event.interaction.nodes).to(equal(nodes), description: "The event does not match expected Nodes")
120+
expect(file: file, line: line, event.interaction.kind).to(equal(interaction), description: "The event does not match expected Kind")
121+
expect(file: file, line: line, event.interaction.nodes).to(equal(nodes), description: "The event does not match expected Nodes")
122122
if let callbackName = callbackName {
123-
expect(event.interaction.callbackName).to(equal(callbackName), description: "The event does not match expected callbackName")
123+
expect(file: file, line: line, event.interaction.callbackName).to(equal(callbackName), description: "The event does not match expected callbackName")
124124
} else {
125-
expect(event.interaction.hasCallbackName).to(beFalse(), description: "The event flag hasCallbackName for callbackName is mismatched, expected false flag for nil value")
125+
expect(file: file, line: line, event.interaction.hasCallbackName).to(beFalse(), description: "The event flag hasCallbackName for callbackName is mismatched, expected false flag for nil value")
126126
}
127127

128128
expect(file: file, line: line, hasPageviewInfo).to(beTrue(), description: "The event must have pageview info")

0 commit comments

Comments
 (0)