From 558cdc3bba904841d1384041b223072f13ea8ef3 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 11 Dec 2024 14:21:58 +0000 Subject: [PATCH 01/11] Start adding new webview classes --- Package.resolved | 11 +- Sources/Core/Events/WebViewReader.swift | 130 +++++++++++++ .../Tracker/WebViewMessageHandlerV2.swift | 171 ++++++++++++++++++ 3 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 Sources/Core/Events/WebViewReader.swift create mode 100644 Sources/Core/Tracker/WebViewMessageHandlerV2.swift diff --git a/Package.resolved b/Package.resolved index 4fc33c6bc..3cbaceed3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,7 +14,16 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" } } diff --git a/Sources/Core/Events/WebViewReader.swift b/Sources/Core/Events/WebViewReader.swift new file mode 100644 index 000000000..d8203a0cc --- /dev/null +++ b/Sources/Core/Events/WebViewReader.swift @@ -0,0 +1,130 @@ +// Copyright (c) 2013-present Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation + +/// Allows the tracking of JavaScript events from WebViews. +class WebViewReader: Event { + let selfDescribingEventData: SelfDescribingJson? + let eventName: String? + let trackerVersion: String? + let useragent: String? + let pageUrl: String? + let pageTitle: String? + let referrer: String? + let category: String? + let action: String? + let label: String? + let property: String? + let value: Double? + let pingXOffsetMin: Int? + let pingXOffsetMax: Int? + let pingYOffsetMin: Int? + let pingYOffsetMax: Int? + + init( + selfDescribingEventData: SelfDescribingJson? = nil, + eventName: String? = nil, + trackerVersion: String? = nil, + useragent: String? = nil, + pageUrl: String? = nil, + pageTitle: String? = nil, + referrer: String? = nil, + category: String? = nil, + action: String? = nil, + label: String? = nil, + property: String? = nil, + value: Double? = nil, + pingXOffsetMin: Int? = nil, + pingXOffsetMax: Int? = nil, + pingYOffsetMin: Int? = nil, + pingYOffsetMax: Int? = nil + ) { + self.selfDescribingEventData = selfDescribingEventData + self.eventName = eventName + self.trackerVersion = trackerVersion + self.useragent = useragent + self.pageUrl = pageUrl + self.pageTitle = pageTitle + self.referrer = referrer + self.category = category + self.action = action + self.label = label + self.property = property + self.value = value + self.pingXOffsetMin = pingXOffsetMin + self.pingXOffsetMax = pingXOffsetMax + self.pingYOffsetMin = pingYOffsetMin + self.pingYOffsetMax = pingYOffsetMax + + super.init() + } + + override var payload: [String : Any] { + var payload: [String: Any] = [:] + + if let selfDescribingEventData = selfDescribingEventData { + payload[Parameters.WEBVIEW_EVENT_DATA] = selfDescribingEventData + } + if let eventName = eventName { + payload[kSPEvent] = eventName + } + if let trackerVersion = trackerVersion { + payload[kSPTrackerVersion] = trackerVersion + } + if let useragent = useragent { + payload[kSPUseragent] = useragent + } + if let pageUrl = pageUrl { + payload[kSPPageUrl] = pageUrl + } + if let pageTitle = pageTitle { + payload[kSPPageTitle] = pageTitle + } + if let referrer = referrer { + payload[kSPPageRefr] = referrer + } + if let category = category { + payload[kSPStuctCategory] = category + } + if let action = action { + payload[kSPStuctAction] = action + } + if let label = label { + payload[Parameters.SE_LABEL] = label + } + if let property = property { + payload[Parameters.SE_PROPERTY] = property + } + if let value = value { + payload[Parameters.SE_VALUE] = value + } + if let pingXOffsetMin = pingXOffsetMin { + payload[Parameters.PING_XOFFSET_MIN] = pingXOffsetMin + } + if let pingXOffsetMax = pingXOffsetMax { + payload[Parameters.PING_XOFFSET_MAX] = pingXOffsetMax + } + if let pingYOffsetMin = pingYOffsetMin { + payload[Parameters.PING_YOFFSET_MIN] = pingYOffsetMin + } + if let pingYOffsetMax = pingYOffsetMax { + payload[Parameters.PING_YOFFSET_MAX] = pingYOffsetMax + } + + return payload +// } +//return [:] + } + +} diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift new file mode 100644 index 000000000..ee9724d9a --- /dev/null +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -0,0 +1,171 @@ +// Copyright (c) 2013-present Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation + +#if os(iOS) || os(macOS) || os(visionOS) +import WebKit + +/// Handler for messages from the JavaScript library embedded in WebViews. +/// This V2 interface works with the WebView tracker v0.3.0+. +/// +/// The handler parses messages from the JavaScript library calls and forwards the tracked events to be tracked by the mobile tracker. +class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { + /// Callback called when the message handler receives a new message. + /// + /// The message dictionary should contain three properties: + /// 1. "event" with a dictionary containing the event information (structure depends on the tracked event) + /// 2. "context" (optional) with a list of self-describing JSONs + /// 3. "trackers" (optional) with a list of tracker namespaces to track the event with + func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + receivedMessage(message) + } + + func receivedMessage(_ message: WKScriptMessage) { + if let body = message.body as? [AnyHashable : Any], + let atomicProperties = body["atomicProperties"] as? [AnyHashable : Any] { + let selfDescribingEventData = body["selfDescribingEventData"] as? [AnyHashable : Any] + let entities = body["entities"] as? [[AnyHashable : Any]] ?? [] + let trackers = body["trackers"] as? [String] ?? [] + + if !JSONSerialization.isValidJSONObject(atomicProperties) || !JSONSerialization.isValidJSONObject(entities) { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return + } + + if let selfDescribingEventData = selfDescribingEventData { + if !JSONSerialization.isValidJSONObject(selfDescribingEventData) { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return + } + } + + trackWebView(atomicProperties, withEventData: selfDescribingEventData, withEntities: entities, andTrackers: trackers) + } + } + + func trackWebView(_ atomicProperties: [AnyHashable : Any], withEventData eventData: [AnyHashable : Any]?, withEntities entities: [[AnyHashable : Any]], andTrackers trackers: [String]) { + } + + func trackSelfDescribing(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + if let schema = event["schema"] as? String, + let payload = event["data"] as? [String : Any] { + let selfDescribing = SelfDescribing(schema: schema, payload: payload) + track(selfDescribing, withContext: context, andTrackers: trackers) + } + } + + func trackStructEvent(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + let category = event["category"] as? String + let action = event["action"] as? String + let label = event["label"] as? String + let property = event["property"] as? String + let value = event["value"] as? NSNumber + + if let category = category, let action = action { + let structured = Structured(category: category, action: action) + if let label = label { + structured.label = label + } + if let property = property { + structured.property = property + } + if let value = value { + structured.value = value + } + track(structured, withContext: context, andTrackers: trackers) + } + } + + func trackPageView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + let url = event["url"] as? String + let title = event["title"] as? String + let referrer = event["referrer"] as? String + + if let url = url { + let pageView = PageView(pageUrl: url) + if let title = title { + pageView.pageTitle = title + } + if let referrer = referrer { + pageView.referrer = referrer + } + track(pageView, withContext: context, andTrackers: trackers) + } + } + + func trackScreenView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + let name = event["name"] as? String + let screenId = event["id"] as? String + let type = event["type"] as? String + let previousName = event["previousName"] as? String + let previousId = event["previousId"] as? String + let previousType = event["previousType"] as? String + let transitionType = event["transitionType"] as? String + + if let name = name, let screenId = screenId { + let screenUuid = UUID(uuidString: screenId) + let screenView = ScreenView(name: name, screenId: screenUuid) + if let type = type { + screenView.type = type + } + if let previousName = previousName { + screenView.previousName = previousName + } + if let previousId = previousId { + screenView.previousId = previousId + } + if let previousType = previousType { + screenView.previousType = previousType + } + if let transitionType = transitionType { + screenView.transitionType = transitionType + } + track(screenView, withContext: context, andTrackers: trackers) + } + } + + func track(_ event: Event, withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + event.entities = parseContext(context) + if trackers.count > 0 { + for namespace in trackers { + if let tracker = Snowplow.tracker(namespace: namespace) { + _ = tracker.track(event) + } + } + } else { + _ = Snowplow.defaultTracker()?.track(event) + } + } + + func parseContext(_ context: [[AnyHashable : Any]]) -> [SelfDescribingJson] { + var contextEntities: [SelfDescribingJson] = [] + + for entityJson in context { + if let schema = entityJson["schema"] as? String, + let payload = entityJson["data"] as? [String : Any] { + let entity = SelfDescribingJson(schema: schema, andDictionary: payload) + contextEntities.append(entity) + } + } + + return contextEntities + } +} + + + +#endif From c6ab38d7874e192af2ade552839ba110c1867f0e Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Thu, 12 Dec 2024 14:08:31 +0000 Subject: [PATCH 02/11] Add tests --- Sources/Core/Events/WebViewReader.swift | 20 +- .../Tracker/WebViewMessageHandlerV2.swift | 2 - Sources/Core/TrackerConstants.swift | 7 + Tests/TestWebViewMessageHandlerV2.swift | 213 ++++++++++++++++++ 4 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 Tests/TestWebViewMessageHandlerV2.swift diff --git a/Sources/Core/Events/WebViewReader.swift b/Sources/Core/Events/WebViewReader.swift index d8203a0cc..25be6735d 100644 --- a/Sources/Core/Events/WebViewReader.swift +++ b/Sources/Core/Events/WebViewReader.swift @@ -74,7 +74,7 @@ class WebViewReader: Event { var payload: [String: Any] = [:] if let selfDescribingEventData = selfDescribingEventData { - payload[Parameters.WEBVIEW_EVENT_DATA] = selfDescribingEventData + payload[kSPWebViewEventData] = selfDescribingEventData } if let eventName = eventName { payload[kSPEvent] = eventName @@ -101,30 +101,26 @@ class WebViewReader: Event { payload[kSPStuctAction] = action } if let label = label { - payload[Parameters.SE_LABEL] = label + payload[kSPStuctLabel] = label } if let property = property { - payload[Parameters.SE_PROPERTY] = property + payload[kSPStuctProperty] = property } if let value = value { - payload[Parameters.SE_VALUE] = value + payload[kSPStuctValue] = value } if let pingXOffsetMin = pingXOffsetMin { - payload[Parameters.PING_XOFFSET_MIN] = pingXOffsetMin + payload[kSPPingXOffsetMin] = pingXOffsetMin } if let pingXOffsetMax = pingXOffsetMax { - payload[Parameters.PING_XOFFSET_MAX] = pingXOffsetMax + payload[kSPPingXOffsetMax] = pingXOffsetMax } if let pingYOffsetMin = pingYOffsetMin { - payload[Parameters.PING_YOFFSET_MIN] = pingYOffsetMin + payload[kSPPingYOffsetMin] = pingYOffsetMin } if let pingYOffsetMax = pingYOffsetMax { - payload[Parameters.PING_YOFFSET_MAX] = pingYOffsetMax + payload[kSPPingYOffsetMax] = pingYOffsetMax } - return payload -// } -//return [:] } - } diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index ee9724d9a..74a69f593 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -45,14 +45,12 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return } - if let selfDescribingEventData = selfDescribingEventData { if !JSONSerialization.isValidJSONObject(selfDescribingEventData) { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return } } - trackWebView(atomicProperties, withEventData: selfDescribingEventData, withEntities: entities, andTrackers: trackers) } } diff --git a/Sources/Core/TrackerConstants.swift b/Sources/Core/TrackerConstants.swift index 6e691e628..b160154f5 100644 --- a/Sources/Core/TrackerConstants.swift +++ b/Sources/Core/TrackerConstants.swift @@ -289,3 +289,10 @@ let kSPDiagnosticErrorMessage = "message" let kSPDiagnosticErrorStack = "stackTrace" let kSPDiagnosticErrorClassName = "className" let kSPDiagnosticErrorExceptionName = "exceptionName" + +// --- Page Pings (for WebView tracking) +let kSPPingXOffsetMin = "pp_mix" +let kSPPingXOffsetMax = "pp_max" +let kSPPingYOffsetMin = "pp_miy" +let kSPPingYOffsetMax = "pp_may" +let kSPWebViewEventData = "selfDescribingEventData" diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift new file mode 100644 index 000000000..26cbd7460 --- /dev/null +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -0,0 +1,213 @@ +// Copyright (c) 2013-present Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +#if os(iOS) || os(macOS) || os(visionOS) +import XCTest +@testable import SnowplowTracker + +class TestWebViewMessageHandlerV2: XCTestCase { + var webViewMessageHandler: WebViewMessageHandlerV2? + + override func setUp() { + webViewMessageHandler = WebViewMessageHandlerV2() + } + + override func tearDown() { + Snowplow.removeAllTrackers() + } + + func testTracksEventWithAllProperties() { + let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 200) + let networkConfig = NetworkConfiguration(networkConnection: networkConnection) + let trackerConfig = TrackerConfiguration().base64Encoding(false) + + Snowplow.removeAllTrackers() + _ = Snowplow.createTracker( + namespace: UUID().uuidString, + network: networkConfig, + configurations: [trackerConfig] + ) + + let data = "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}" + let atomic = "{\"eventName\":\"pv\",\"trackerVersion\":\"webview\"," + + "\"useragent\":\"Chrome\",\"pageUrl\":\"http://snowplow.com\"," + + "\"pageTitle\":\"Snowplow\",\"referrer\":\"http://google.com\"," + + "\"pingXOffsetMin\":10,\"pingXOffsetMax\":20,\"pingYOffsetMin\":30," + + "\"pingYOffsetMax\":40,\"category\":\"cat\",\"action\":\"act\"," + + "\"property\":\"prop\",\"label\":\"lbl\",\"value\":10.0}" + + let message = MockWKScriptMessage( + body: [ + "atomicProperties": atomic, + "selfDescribingEventData": data, + ]) + webViewMessageHandler?.receivedMessage(message) + + waitForEventsToBeTracked() + + XCTAssertEqual(1, networkConnection.sendingCount) + XCTAssertEqual(1, (networkConnection.previousRequests)[0].count) + + let request = (networkConnection.previousRequests)[0][0] + let payload = (request.payload?["data"] as? [[String: Any]])?[0] + + XCTAssert(payload?[kSPEvent] as? String == "pv") + XCTAssert(payload?[kSPTrackerVersion] as? String == "webview") + XCTAssert(payload?[kSPUseragent] as? String == "Chrome") + XCTAssert(payload?[kSPPageUrl] as? String == "http://snowplow.com") + XCTAssert(payload?[kSPPageTitle] as? String == "Snowplow") + XCTAssert(payload?[kSPPageRefr] as? String == "http://google.com") + XCTAssert(payload?[kSPPingXOffsetMin] as? Int == 10) + XCTAssert(payload?[kSPPingXOffsetMax] as? Int == 20) + XCTAssert(payload?[kSPPingYOffsetMin] as? Int == 30) + XCTAssert(payload?[kSPPingYOffsetMax] as? Int == 40) + XCTAssert(payload?[kSPStuctCategory] as? String == "cat") + XCTAssert(payload?[kSPStuctAction] as? String == "act") + XCTAssert(payload?[kSPStuctProperty] as? String == "prop") + XCTAssert(payload?[kSPStuctLabel] as? String == "lbl") + XCTAssert(payload?[kSPStuctValue] as? Double == 10.0) + + XCTAssertTrue(payload?[kSPUnstructured] != nil) + + if let unstructuredJson = payload?[kSPUnstructured] as? String, + let jsonData = unstructuredJson.data(using: .utf8), + let selfDescJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { + XCTAssert(selfDescJson["schema"] as? String == kSPUnstructSchema) + XCTAssert(selfDescJson["data"] as? String == data) + } + } + + func testAddsDefaultPropertiesIfNotProvided() { + let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 200) + let networkConfig = NetworkConfiguration(networkConnection: networkConnection) + let trackerConfig = TrackerConfiguration().base64Encoding(false) + + Snowplow.removeAllTrackers() + _ = Snowplow.createTracker( + namespace: UUID().uuidString, + network: networkConfig, + configurations: [trackerConfig] + ) + + let message = MockWKScriptMessage(body: ["atomicProperties": "{}"]) + webViewMessageHandler?.receivedMessage(message) + + waitForEventsToBeTracked() + + XCTAssertEqual(1, (networkConnection.previousRequests)[0].count) + + let request = (networkConnection.previousRequests)[0][0] + let payload = (request.payload?["data"] as? [[String: Any]])?[0] + + XCTAssert(payload?[kSPEvent] as? String == "ue") + XCTAssertTrue((payload?[kSPTrackerVersion] as? String)?.starts(with: "ios") ?? false) + } + + func testTracksEventWithCorrectTracker() { + let eventSink1 = EventSink() + let eventSink2 = EventSink() + + _ = createTracker("ns1", eventSink1) + _ = createTracker("ns2", eventSink2) + Thread.sleep(forTimeInterval: 0.2) + + // track an event using the second tracker + let message = MockWKScriptMessage( + body: [ + "atomicProperties": "{}", + "trackers": ["ns2"] + ]) + webViewMessageHandler?.receivedMessage(message) + waitForEventsToBeTracked() + + XCTAssertEqual(0, eventSink1.trackedEvents.count) + XCTAssertEqual(1, eventSink2.trackedEvents.count) + + // tracks using default tracker if not specified + let message2 = MockWKScriptMessage(body: ["atomicProperties": "{}"]) + webViewMessageHandler?.receivedMessage(message2) + waitForEventsToBeTracked() + + XCTAssertEqual(1, eventSink1.trackedEvents.count) + XCTAssertEqual(1, eventSink2.trackedEvents.count) + } + + func testTracksEventWithEntity() { + let eventSink = EventSink() + _ = createTracker("ns" + String(describing: Int.random(in: 0..<100)), eventSink) + Thread.sleep(forTimeInterval: 0.2) + + let message = MockWKScriptMessage( + body: [ + "atomicProperties": "{}", + "entities": "[{\"schema\":\"iglu:com.example/etc\",\"data\":{\"key\":\"val\"}}]" + ]) + webViewMessageHandler?.receivedMessage(message) + waitForEventsToBeTracked() + + XCTAssertEqual(1, eventSink.trackedEvents.count) + let relevantEntities = eventSink.trackedEvents[0].entities.filter { $0.data["schema"] as? String == "iglu:com.example/etc" } + + XCTAssertEqual(1, relevantEntities.count) + + let entityData = relevantEntities[0].data["data"] as? [String : Any] + XCTAssertEqual("val", entityData?["key"] as? String) + } + + func testAddsEventNameAndSchemaForInspection() { + let eventSink = EventSink() + _ = createTracker("ns" + String(describing: Int.random(in: 0..<100)), eventSink) + Thread.sleep(forTimeInterval: 0.2) + + let message = MockWKScriptMessage( + body: [ + "atomicProperties": "{\"eventName\":\"se\"}", + "selfDescribingEventData": "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}" + ]) + webViewMessageHandler?.receivedMessage(message) + waitForEventsToBeTracked() + + let events = eventSink.trackedEvents + + XCTAssertEqual(1, events.count) + XCTAssertEqual("se", events[0].eventName) + XCTAssertEqual("iglu:etc", events[0].schema) + } + + func testHandlesNonJSONSerializableDataInEvent() { + let message = MockWKScriptMessage( + body: [ + "atomicProperties": "{\"eventName\":\"se\"}", + "selfDescribingEventData": Double.nan + ]) + webViewMessageHandler?.receivedMessage(message) // shouldn't crash + } + + private func waitForEventsToBeTracked() { + let expect = expectation(description: "Wait for events to be tracked") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { () -> Void in + expect.fulfill() + } + wait(for: [expect], timeout: 1) + } + + private func createTracker(_ namespace: String, _ eventSink: EventSink) -> TrackerController { + let networkConfig = NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200)) + let trackerConfig = TrackerConfiguration().base64Encoding(false) + + return Snowplow.createTracker(namespace: namespace, + network: networkConfig, + configurations: [trackerConfig, eventSink]) + } +} +#endif From b836ff9139eeae82b3286ecf9ae9bb3958512024 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 16 Dec 2024 16:54:52 +0000 Subject: [PATCH 03/11] Parse the webview message --- Sources/Core/Events/WebViewReader.swift | 10 +- .../Tracker/WebViewMessageHandlerV2.swift | 254 +++++++++++------- Tests/TestWebViewMessageHandlerV2.swift | 10 +- 3 files changed, 164 insertions(+), 110 deletions(-) diff --git a/Sources/Core/Events/WebViewReader.swift b/Sources/Core/Events/WebViewReader.swift index 25be6735d..85d0dcbe8 100644 --- a/Sources/Core/Events/WebViewReader.swift +++ b/Sources/Core/Events/WebViewReader.swift @@ -95,19 +95,19 @@ class WebViewReader: Event { payload[kSPPageRefr] = referrer } if let category = category { - payload[kSPStuctCategory] = category + payload[kSPStructCategory] = category } if let action = action { - payload[kSPStuctAction] = action + payload[kSPStructAction] = action } if let label = label { - payload[kSPStuctLabel] = label + payload[kSPStructLabel] = label } if let property = property { - payload[kSPStuctProperty] = property + payload[kSPStructProperty] = property } if let value = value { - payload[kSPStuctValue] = value + payload[kSPStructValue] = value } if let pingXOffsetMin = pingXOffsetMin { payload[kSPPingXOffsetMin] = pingXOffsetMin diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index 74a69f593..4118de7b9 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -36,108 +36,66 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { func receivedMessage(_ message: WKScriptMessage) { if let body = message.body as? [AnyHashable : Any], - let atomicProperties = body["atomicProperties"] as? [AnyHashable : Any] { - let selfDescribingEventData = body["selfDescribingEventData"] as? [AnyHashable : Any] - let entities = body["entities"] as? [[AnyHashable : Any]] ?? [] - let trackers = body["trackers"] as? [String] ?? [] + let atomicProperties = body["atomicProperties"] as? String { - if !JSONSerialization.isValidJSONObject(atomicProperties) || !JSONSerialization.isValidJSONObject(entities) { + guard let atomicData = atomicProperties.data(using: .utf8) else { return } + guard let atomicJson = try? JSONSerialization.jsonObject(with: atomicData) as? [String : Any] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return } - if let selfDescribingEventData = selfDescribingEventData { - if !JSONSerialization.isValidJSONObject(selfDescribingEventData) { + + let selfDescribingEventData = body["selfDescribingEventData"] as? String? ?? nil + var selfDescribingDataJson: [AnyHashable : Any] = [:] + + if (selfDescribingEventData != nil) { + let data = selfDescribingEventData?.data(using: .utf8) ?? Data() + guard let json = try? JSONSerialization.jsonObject(with: data) as? [AnyHashable : Any] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return } + selfDescribingDataJson = json } - trackWebView(atomicProperties, withEventData: selfDescribingEventData, withEntities: entities, andTrackers: trackers) - } - } - - func trackWebView(_ atomicProperties: [AnyHashable : Any], withEventData eventData: [AnyHashable : Any]?, withEntities entities: [[AnyHashable : Any]], andTrackers trackers: [String]) { - } - - func trackSelfDescribing(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - if let schema = event["schema"] as? String, - let payload = event["data"] as? [String : Any] { - let selfDescribing = SelfDescribing(schema: schema, payload: payload) - track(selfDescribing, withContext: context, andTrackers: trackers) - } - } - - func trackStructEvent(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - let category = event["category"] as? String - let action = event["action"] as? String - let label = event["label"] as? String - let property = event["property"] as? String - let value = event["value"] as? NSNumber - - if let category = category, let action = action { - let structured = Structured(category: category, action: action) - if let label = label { - structured.label = label - } - if let property = property { - structured.property = property - } - if let value = value { - structured.value = value - } - track(structured, withContext: context, andTrackers: trackers) - } - } - - func trackPageView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - let url = event["url"] as? String - let title = event["title"] as? String - let referrer = event["referrer"] as? String - - if let url = url { - let pageView = PageView(pageUrl: url) - if let title = title { - pageView.pageTitle = title - } - if let referrer = referrer { - pageView.referrer = referrer - } - track(pageView, withContext: context, andTrackers: trackers) - } - } - - func trackScreenView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - let name = event["name"] as? String - let screenId = event["id"] as? String - let type = event["type"] as? String - let previousName = event["previousName"] as? String - let previousId = event["previousId"] as? String - let previousType = event["previousType"] as? String - let transitionType = event["transitionType"] as? String - - if let name = name, let screenId = screenId { - let screenUuid = UUID(uuidString: screenId) - let screenView = ScreenView(name: name, screenId: screenUuid) - if let type = type { - screenView.type = type - } - if let previousName = previousName { - screenView.previousName = previousName - } - if let previousId = previousId { - screenView.previousId = previousId - } - if let previousType = previousType { - screenView.previousType = previousType - } - if let transitionType = transitionType { - screenView.transitionType = transitionType + + let entitiesData = body["entities"] as? String? ?? nil + var entitiesJson: [[AnyHashable : Any]] = [] + + if (entitiesData != nil) { + let data = selfDescribingEventData?.data(using: .utf8) ?? Data() + guard let json = try? JSONSerialization.jsonObject(with: data) as? [[AnyHashable : Any]] else { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return + } + entitiesJson = json } - track(screenView, withContext: context, andTrackers: trackers) + + let trackers = body["trackers"] as? [String] ?? [] + + let event = WebViewReader( + selfDescribingEventData: createSelfDescribingJson(selfDescribingDataJson), + eventName: atomicJson["eventName"] as? String? ?? nil, + trackerVersion: atomicJson["trackerVersion"] as? String? ?? nil, + useragent: atomicJson["useragent"] as? String? ?? nil, + pageUrl: atomicJson["pageUrl"] as? String? ?? nil, + pageTitle: atomicJson["pageTitle"] as? String? ?? nil, + referrer: atomicJson["referrer"] as? String? ?? nil, + category: atomicJson["category"] as? String? ?? nil, + action: atomicJson["action"] as? String? ?? nil, + label: atomicJson["label"] as? String? ?? nil, + property: atomicJson["property"] as? String? ?? nil, + value: atomicJson["value"] as? Double? ?? nil, + pingXOffsetMin: atomicJson["pingXOffsetMin"] as? Int? ?? nil, + pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int? ?? nil, + pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int? ?? nil, + pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int? ?? nil + ) + + track(event, withEntities: entitiesJson, andTrackers: trackers) } } - - func track(_ event: Event, withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - event.entities = parseContext(context) + + func track(_ event: Event, withEntities entities: [[AnyHashable : Any]], andTrackers trackers: [String]) { + event.entities = parseEntities(entities) + if trackers.count > 0 { for namespace in trackers { if let tracker = Snowplow.tracker(namespace: namespace) { @@ -148,22 +106,118 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { _ = Snowplow.defaultTracker()?.track(event) } } - - func parseContext(_ context: [[AnyHashable : Any]]) -> [SelfDescribingJson] { + + func getProperty(atomicProperties: [AnyHashable : Any], property: String) { + + } + + func createSelfDescribingJson(_ map: [AnyHashable : Any]) -> SelfDescribingJson? { + if let schema = map["schema"] as? String, + let payload = map["data"] as? [String : Any] { + return SelfDescribingJson(schema: schema, andDictionary: payload) + } + return nil + } + + func parseEntities(_ entities: [[AnyHashable : Any]]) -> [SelfDescribingJson] { var contextEntities: [SelfDescribingJson] = [] - - for entityJson in context { - if let schema = entityJson["schema"] as? String, - let payload = entityJson["data"] as? [String : Any] { - let entity = SelfDescribingJson(schema: schema, andDictionary: payload) + + for entityJson in entities { + if let entity = createSelfDescribingJson(entityJson) { contextEntities.append(entity) } } - return contextEntities } + + func parseSelfDescribingEventData(_ eventData: [AnyHashable : Any]) -> SelfDescribingJson? { + // TODO this function not used? + return createSelfDescribingJson(eventData) + } + + + // + // + // + // func trackSelfDescribing(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + // if let schema = event["schema"] as? String, + // let payload = event["data"] as? [String : Any] { + // let selfDescribing = SelfDescribing(schema: schema, payload: payload) + // track(selfDescribing, withEntities: context, andTrackers: trackers) + // } + // } + // + // func trackStructEvent(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + // let category = event["category"] as? String + // let action = event["action"] as? String + // let label = event["label"] as? String + // let property = event["property"] as? String + // let value = event["value"] as? NSNumber + // + // if let category = category, let action = action { + // let structured = Structured(category: category, action: action) + // if let label = label { + // structured.label = label + // } + // if let property = property { + // structured.property = property + // } + // if let value = value { + // structured.value = value + // } + // track(structured, withEntities: context, andTrackers: trackers) + // } + // } + // + // func trackPageView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + // let url = event["url"] as? String + // let title = event["title"] as? String + // let referrer = event["referrer"] as? String + // + // if let url = url { + // let pageView = PageView(pageUrl: url) + // if let title = title { + // pageView.pageTitle = title + // } + // if let referrer = referrer { + // pageView.referrer = referrer + // } + // track(pageView, withEntities: context, andTrackers: trackers) + // } + // } + // + // func trackScreenView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { + // let name = event["name"] as? String + // let screenId = event["id"] as? String + // let type = event["type"] as? String + // let previousName = event["previousName"] as? String + // let previousId = event["previousId"] as? String + // let previousType = event["previousType"] as? String + // let transitionType = event["transitionType"] as? String + // + // if let name = name, let screenId = screenId { + // let screenUuid = UUID(uuidString: screenId) + // let screenView = ScreenView(name: name, screenId: screenUuid) + // if let type = type { + // screenView.type = type + // } + // if let previousName = previousName { + // screenView.previousName = previousName + // } + // if let previousId = previousId { + // screenView.previousId = previousId + // } + // if let previousType = previousType { + // screenView.previousType = previousType + // } + // if let transitionType = transitionType { + // screenView.transitionType = transitionType + // } + // track(screenView, withEntities: context, andTrackers: trackers) + // } + // } + //} } - #endif diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift index 26cbd7460..e19774d68 100644 --- a/Tests/TestWebViewMessageHandlerV2.swift +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -71,11 +71,11 @@ class TestWebViewMessageHandlerV2: XCTestCase { XCTAssert(payload?[kSPPingXOffsetMax] as? Int == 20) XCTAssert(payload?[kSPPingYOffsetMin] as? Int == 30) XCTAssert(payload?[kSPPingYOffsetMax] as? Int == 40) - XCTAssert(payload?[kSPStuctCategory] as? String == "cat") - XCTAssert(payload?[kSPStuctAction] as? String == "act") - XCTAssert(payload?[kSPStuctProperty] as? String == "prop") - XCTAssert(payload?[kSPStuctLabel] as? String == "lbl") - XCTAssert(payload?[kSPStuctValue] as? Double == 10.0) + XCTAssert(payload?[kSPStructCategory] as? String == "cat") + XCTAssert(payload?[kSPStructAction] as? String == "act") + XCTAssert(payload?[kSPStructProperty] as? String == "prop") + XCTAssert(payload?[kSPStructLabel] as? String == "lbl") + XCTAssert(payload?[kSPStructValue] as? Double == 10.0) XCTAssertTrue(payload?[kSPUnstructured] != nil) From 00ea5ee996b7b5a4824a2edf201abfce3a84abde Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 16 Dec 2024 20:28:46 +0000 Subject: [PATCH 04/11] Make one of the tests work --- Sources/Core/Events/WebViewReader.swift | 10 ++-- Sources/Core/Tracker/TrackerEvent.swift | 54 ++++++++++++++----- .../Tracker/WebViewMessageHandlerV2.swift | 14 ++--- Tests/TestWebViewMessageHandlerV2.swift | 19 ++++--- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/Sources/Core/Events/WebViewReader.swift b/Sources/Core/Events/WebViewReader.swift index 85d0dcbe8..fc1876c62 100644 --- a/Sources/Core/Events/WebViewReader.swift +++ b/Sources/Core/Events/WebViewReader.swift @@ -107,19 +107,19 @@ class WebViewReader: Event { payload[kSPStructProperty] = property } if let value = value { - payload[kSPStructValue] = value + payload[kSPStructValue] = String(value) } if let pingXOffsetMin = pingXOffsetMin { - payload[kSPPingXOffsetMin] = pingXOffsetMin + payload[kSPPingXOffsetMin] = String(pingXOffsetMin) } if let pingXOffsetMax = pingXOffsetMax { - payload[kSPPingXOffsetMax] = pingXOffsetMax + payload[kSPPingXOffsetMax] = String(pingXOffsetMax) } if let pingYOffsetMin = pingYOffsetMin { - payload[kSPPingYOffsetMin] = pingYOffsetMin + payload[kSPPingYOffsetMin] = String(pingYOffsetMin) } if let pingYOffsetMax = pingYOffsetMax { - payload[kSPPingYOffsetMax] = pingYOffsetMax + payload[kSPPingYOffsetMax] = String(pingYOffsetMax) } return payload } diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift index 371708464..9f39e79e2 100644 --- a/Sources/Core/Tracker/TrackerEvent.swift +++ b/Sources/Core/Tracker/TrackerEvent.swift @@ -39,6 +39,8 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { private(set) var isService: Bool + private(set) var isWebView: Bool + init(event: Event, eventId: UUID = UUID(), state: TrackerStateSnapshot? = nil) { self.eventId = eventId timestamp = Int64(Date().timeIntervalSince1970 * 1000) @@ -48,12 +50,21 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { self.state = state ?? TrackerState() isService = (event is TrackerError) - if let abstractEvent = event as? PrimitiveAbstract { - eventName = abstractEvent.eventName + isWebView = false + isPrimitive = false + + switch event { + case _ as WebViewReader: + eventName = (payload[kSPEvent] as? String) + schema = getWebViewSchema() + isWebView = true + + case let primitive as PrimitiveAbstract: + eventName = primitive.eventName isPrimitive = true - } else { - schema = (event as! SelfDescribingAbstract).schema - isPrimitive = false + + default: + schema = (event as? SelfDescribingAbstract)?.schema } } @@ -90,17 +101,21 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { } func wrapProperties(to payload: Payload, base64Encoded: Bool) { - if isPrimitive { + if isWebView { + wrapWebViewToPayload(to: payload, base64Encoded: base64Encoded) + } else if isPrimitive { payload.addDictionaryToPayload(self.payload) } else { - wrapSelfDescribing(to: payload, base64Encoded: base64Encoded) + wrapSelfDescribingEventToPayload(to: payload, base64Encoded: base64Encoded) } } - private func wrapSelfDescribing(to payload: Payload, base64Encoded: Bool) { - guard let schema = schema else { return } - - let data = SelfDescribingJson(schema: schema, andData: self.payload) + private func getWebViewSchema() -> String? { + let selfDescribingData = payload[kSPWebViewEventData] as? SelfDescribingJson + return selfDescribingData?.data[kSPSchema] as? String + } + + private func addSelfDescribingDataToPayload(to payload: Payload, base64Encoded: Bool, data: SelfDescribingJson) { let unstructuredEventPayload = SelfDescribingJson.dictionary( schema: kSPUnstructSchema, data: data.dictionary) @@ -108,6 +123,21 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { unstructuredEventPayload, base64Encoded: base64Encoded, typeWhenEncoded: kSPUnstructuredEncoded, - typeWhenNotEncoded: kSPUnstructured) + typeWhenNotEncoded: kSPUnstructured + ) + } + + private func wrapWebViewToPayload(to payload: Payload, base64Encoded: Bool) { + let selfDescribingData = self.payload[kSPWebViewEventData] as? SelfDescribingJson + if let data = selfDescribingData { + addSelfDescribingDataToPayload(to: payload, base64Encoded: base64Encoded, data: data) + } + payload.addDictionaryToPayload(self.payload.filter { $0.key != kSPWebViewEventData }) + } + + private func wrapSelfDescribingEventToPayload(to payload: Payload, base64Encoded: Bool) { + guard let schema = schema else { return } + let data = SelfDescribingJson(schema: schema, andData: self.payload) + addSelfDescribingDataToPayload(to: payload, base64Encoded: base64Encoded, data: data) } } diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index 4118de7b9..bc4d16118 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -82,11 +82,11 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { action: atomicJson["action"] as? String? ?? nil, label: atomicJson["label"] as? String? ?? nil, property: atomicJson["property"] as? String? ?? nil, - value: atomicJson["value"] as? Double? ?? nil, - pingXOffsetMin: atomicJson["pingXOffsetMin"] as? Int? ?? nil, - pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int? ?? nil, - pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int? ?? nil, - pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int? ?? nil + value: atomicJson["value"] as? Double, + pingXOffsetMin: atomicJson["pingXOffsetMin"] as? Int ?? nil, + pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int ?? nil, + pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int ?? nil, + pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int ?? nil ) track(event, withEntities: entitiesJson, andTrackers: trackers) @@ -107,10 +107,6 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { } } - func getProperty(atomicProperties: [AnyHashable : Any], property: String) { - - } - func createSelfDescribingJson(_ map: [AnyHashable : Any]) -> SelfDescribingJson? { if let schema = map["schema"] as? String, let payload = map["data"] as? [String : Any] { diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift index e19774d68..fc9899806 100644 --- a/Tests/TestWebViewMessageHandlerV2.swift +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -67,15 +67,15 @@ class TestWebViewMessageHandlerV2: XCTestCase { XCTAssert(payload?[kSPPageUrl] as? String == "http://snowplow.com") XCTAssert(payload?[kSPPageTitle] as? String == "Snowplow") XCTAssert(payload?[kSPPageRefr] as? String == "http://google.com") - XCTAssert(payload?[kSPPingXOffsetMin] as? Int == 10) - XCTAssert(payload?[kSPPingXOffsetMax] as? Int == 20) - XCTAssert(payload?[kSPPingYOffsetMin] as? Int == 30) - XCTAssert(payload?[kSPPingYOffsetMax] as? Int == 40) + XCTAssert(payload?[kSPPingXOffsetMin] as? String == "10") + XCTAssert(payload?[kSPPingXOffsetMax] as? String == "20") + XCTAssert(payload?[kSPPingYOffsetMin] as? String == "30") + XCTAssert(payload?[kSPPingYOffsetMax] as? String == "40") XCTAssert(payload?[kSPStructCategory] as? String == "cat") XCTAssert(payload?[kSPStructAction] as? String == "act") XCTAssert(payload?[kSPStructProperty] as? String == "prop") XCTAssert(payload?[kSPStructLabel] as? String == "lbl") - XCTAssert(payload?[kSPStructValue] as? Double == 10.0) + XCTAssert(payload?[kSPStructValue] as? String == "10.0") XCTAssertTrue(payload?[kSPUnstructured] != nil) @@ -83,7 +83,14 @@ class TestWebViewMessageHandlerV2: XCTestCase { let jsonData = unstructuredJson.data(using: .utf8), let selfDescJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { XCTAssert(selfDescJson["schema"] as? String == kSPUnstructSchema) - XCTAssert(selfDescJson["data"] as? String == data) + + if let data = selfDescJson["data"] as? [String : Any] { + XCTAssert(data["schema"] as? String == "iglu:etc") + + if let customData = data["data"] as? [String : Any] { + XCTAssert(customData["key"] as? String == "val") + } + } } } From 2873763741b843f1959079d591f637c224d0db87 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 17 Dec 2024 11:46:07 +0000 Subject: [PATCH 05/11] Make more tests work --- Sources/Core/Tracker/TrackerEvent.swift | 2 +- .../Tracker/WebViewMessageHandlerV2.swift | 85 +------------------ Tests/TestWebViewMessageHandlerV2.swift | 30 +++++-- 3 files changed, 27 insertions(+), 90 deletions(-) diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift index 9f39e79e2..216b39798 100644 --- a/Sources/Core/Tracker/TrackerEvent.swift +++ b/Sources/Core/Tracker/TrackerEvent.swift @@ -112,7 +112,7 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { private func getWebViewSchema() -> String? { let selfDescribingData = payload[kSPWebViewEventData] as? SelfDescribingJson - return selfDescribingData?.data[kSPSchema] as? String + return selfDescribingData?.schema } private func addSelfDescribingDataToPayload(to payload: Payload, base64Encoded: Bool, data: SelfDescribingJson) { diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index bc4d16118..22ea076b2 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -60,7 +60,7 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { var entitiesJson: [[AnyHashable : Any]] = [] if (entitiesData != nil) { - let data = selfDescribingEventData?.data(using: .utf8) ?? Data() + let data = entitiesData?.data(using: .utf8) ?? Data() guard let json = try? JSONSerialization.jsonObject(with: data) as? [[AnyHashable : Any]] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return @@ -130,89 +130,6 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { // TODO this function not used? return createSelfDescribingJson(eventData) } - - - // - // - // - // func trackSelfDescribing(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - // if let schema = event["schema"] as? String, - // let payload = event["data"] as? [String : Any] { - // let selfDescribing = SelfDescribing(schema: schema, payload: payload) - // track(selfDescribing, withEntities: context, andTrackers: trackers) - // } - // } - // - // func trackStructEvent(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - // let category = event["category"] as? String - // let action = event["action"] as? String - // let label = event["label"] as? String - // let property = event["property"] as? String - // let value = event["value"] as? NSNumber - // - // if let category = category, let action = action { - // let structured = Structured(category: category, action: action) - // if let label = label { - // structured.label = label - // } - // if let property = property { - // structured.property = property - // } - // if let value = value { - // structured.value = value - // } - // track(structured, withEntities: context, andTrackers: trackers) - // } - // } - // - // func trackPageView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - // let url = event["url"] as? String - // let title = event["title"] as? String - // let referrer = event["referrer"] as? String - // - // if let url = url { - // let pageView = PageView(pageUrl: url) - // if let title = title { - // pageView.pageTitle = title - // } - // if let referrer = referrer { - // pageView.referrer = referrer - // } - // track(pageView, withEntities: context, andTrackers: trackers) - // } - // } - // - // func trackScreenView(_ event: [AnyHashable : Any], withContext context: [[AnyHashable : Any]], andTrackers trackers: [String]) { - // let name = event["name"] as? String - // let screenId = event["id"] as? String - // let type = event["type"] as? String - // let previousName = event["previousName"] as? String - // let previousId = event["previousId"] as? String - // let previousType = event["previousType"] as? String - // let transitionType = event["transitionType"] as? String - // - // if let name = name, let screenId = screenId { - // let screenUuid = UUID(uuidString: screenId) - // let screenView = ScreenView(name: name, screenId: screenUuid) - // if let type = type { - // screenView.type = type - // } - // if let previousName = previousName { - // screenView.previousName = previousName - // } - // if let previousId = previousId { - // screenView.previousId = previousId - // } - // if let previousType = previousType { - // screenView.previousType = previousType - // } - // if let transitionType = transitionType { - // screenView.transitionType = transitionType - // } - // track(screenView, withEntities: context, andTrackers: trackers) - // } - // } - //} } diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift index fc9899806..21e28f1d3 100644 --- a/Tests/TestWebViewMessageHandlerV2.swift +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -20,6 +20,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { override func setUp() { webViewMessageHandler = WebViewMessageHandlerV2() + Snowplow.removeAllTrackers() } override func tearDown() { @@ -31,7 +32,6 @@ class TestWebViewMessageHandlerV2: XCTestCase { let networkConfig = NetworkConfiguration(networkConnection: networkConnection) let trackerConfig = TrackerConfiguration().base64Encoding(false) - Snowplow.removeAllTrackers() _ = Snowplow.createTracker( namespace: UUID().uuidString, network: networkConfig, @@ -117,7 +117,27 @@ class TestWebViewMessageHandlerV2: XCTestCase { let payload = (request.payload?["data"] as? [[String: Any]])?[0] XCTAssert(payload?[kSPEvent] as? String == "ue") - XCTAssertTrue((payload?[kSPTrackerVersion] as? String)?.starts(with: "ios") ?? false) + + if let trackerVersion = payload?[kSPTrackerVersion] as? String { + XCTAssertTrue(trackerVersion.range(of: #"^(ios|watchos|tvos|osx)"#, options: .regularExpression) != nil) + } + + // TODO delete this part + let eventSink = EventSink() + _ = createTracker("ns2", eventSink) + Thread.sleep(forTimeInterval: 0.2) + + let message2 = MockWKScriptMessage( + body: [ + "atomicProperties": "{}", + "trackers": ["ns2"] + ]) + webViewMessageHandler?.receivedMessage(message2) + waitForEventsToBeTracked() + + let events = eventSink.trackedEvents + + XCTAssertEqual(1, events.count) } func testTracksEventWithCorrectTracker() { @@ -136,7 +156,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { ]) webViewMessageHandler?.receivedMessage(message) waitForEventsToBeTracked() - + XCTAssertEqual(0, eventSink1.trackedEvents.count) XCTAssertEqual(1, eventSink2.trackedEvents.count) @@ -163,11 +183,11 @@ class TestWebViewMessageHandlerV2: XCTestCase { waitForEventsToBeTracked() XCTAssertEqual(1, eventSink.trackedEvents.count) - let relevantEntities = eventSink.trackedEvents[0].entities.filter { $0.data["schema"] as? String == "iglu:com.example/etc" } + let relevantEntities = eventSink.trackedEvents[0].entities.filter { $0.schema == "iglu:com.example/etc" } XCTAssertEqual(1, relevantEntities.count) - let entityData = relevantEntities[0].data["data"] as? [String : Any] + let entityData = relevantEntities[0].dictionary["data"] as? [String : Any] XCTAssertEqual("val", entityData?["key"] as? String) } From 210f56344a2a99a554c552cf89c9812a7c6bf3fe Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 17 Dec 2024 12:34:14 +0000 Subject: [PATCH 06/11] Make all tests pass --- Sources/Core/Tracker/TrackerEvent.swift | 2 +- .../Tracker/WebViewMessageHandlerV2.swift | 26 +++++++++++++------ Tests/TestWebViewMessageHandlerV2.swift | 25 +++--------------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift index 216b39798..83200f89b 100644 --- a/Sources/Core/Tracker/TrackerEvent.swift +++ b/Sources/Core/Tracker/TrackerEvent.swift @@ -55,7 +55,7 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { switch event { case _ as WebViewReader: - eventName = (payload[kSPEvent] as? String) + eventName = (payload[kSPEvent] as? String) ?? "ue" schema = getWebViewSchema() isWebView = true diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index 22ea076b2..a48e8a17f 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -38,11 +38,12 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { if let body = message.body as? [AnyHashable : Any], let atomicProperties = body["atomicProperties"] as? String { - guard let atomicData = atomicProperties.data(using: .utf8) else { return } - guard let atomicJson = try? JSONSerialization.jsonObject(with: atomicData) as? [String : Any] else { - logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") - return - } +// guard let atomicData = atomicProperties.data(using: .utf8) else { return } +// guard let atomicJson = try? JSONSerialization.jsonObject(with: atomicData) as? [String : Any] else { +// logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") +// return +// } + guard let atomicJson = parseAtomicPropertiesFromMessage(atomicProperties) else { return } let selfDescribingEventData = body["selfDescribingEventData"] as? String? ?? nil var selfDescribingDataJson: [AnyHashable : Any] = [:] @@ -126,9 +127,18 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { return contextEntities } - func parseSelfDescribingEventData(_ eventData: [AnyHashable : Any]) -> SelfDescribingJson? { - // TODO this function not used? - return createSelfDescribingJson(eventData) + func parseAtomicPropertiesFromMessage(_ messageString: String?) -> [String : Any]? { + let atomicData = messageString?.data(using: .utf8) + + var atomicJson: [String : Any]? = [:] + if let data = atomicData { + atomicJson = try? JSONSerialization.jsonObject(with: data) as? [String : Any] + } + if (atomicData == nil) || (atomicJson == nil) { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return nil + } + return atomicJson } } diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift index 21e28f1d3..732d2c0cb 100644 --- a/Tests/TestWebViewMessageHandlerV2.swift +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -121,23 +121,6 @@ class TestWebViewMessageHandlerV2: XCTestCase { if let trackerVersion = payload?[kSPTrackerVersion] as? String { XCTAssertTrue(trackerVersion.range(of: #"^(ios|watchos|tvos|osx)"#, options: .regularExpression) != nil) } - - // TODO delete this part - let eventSink = EventSink() - _ = createTracker("ns2", eventSink) - Thread.sleep(forTimeInterval: 0.2) - - let message2 = MockWKScriptMessage( - body: [ - "atomicProperties": "{}", - "trackers": ["ns2"] - ]) - webViewMessageHandler?.receivedMessage(message2) - waitForEventsToBeTracked() - - let events = eventSink.trackedEvents - - XCTAssertEqual(1, events.count) } func testTracksEventWithCorrectTracker() { @@ -155,7 +138,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { "trackers": ["ns2"] ]) webViewMessageHandler?.receivedMessage(message) - waitForEventsToBeTracked() + Thread.sleep(forTimeInterval: 0.2) XCTAssertEqual(0, eventSink1.trackedEvents.count) XCTAssertEqual(1, eventSink2.trackedEvents.count) @@ -163,7 +146,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { // tracks using default tracker if not specified let message2 = MockWKScriptMessage(body: ["atomicProperties": "{}"]) webViewMessageHandler?.receivedMessage(message2) - waitForEventsToBeTracked() + Thread.sleep(forTimeInterval: 0.2) XCTAssertEqual(1, eventSink1.trackedEvents.count) XCTAssertEqual(1, eventSink2.trackedEvents.count) @@ -180,7 +163,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { "entities": "[{\"schema\":\"iglu:com.example/etc\",\"data\":{\"key\":\"val\"}}]" ]) webViewMessageHandler?.receivedMessage(message) - waitForEventsToBeTracked() + Thread.sleep(forTimeInterval: 0.2) XCTAssertEqual(1, eventSink.trackedEvents.count) let relevantEntities = eventSink.trackedEvents[0].entities.filter { $0.schema == "iglu:com.example/etc" } @@ -202,7 +185,7 @@ class TestWebViewMessageHandlerV2: XCTestCase { "selfDescribingEventData": "{\"schema\":\"iglu:etc\",\"data\":{\"key\":\"val\"}}" ]) webViewMessageHandler?.receivedMessage(message) - waitForEventsToBeTracked() + Thread.sleep(forTimeInterval: 0.2) let events = eventSink.trackedEvents From 624ee44b4f6f5a6f89b0d0722f389613fa825ad2 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 17 Dec 2024 12:59:27 +0000 Subject: [PATCH 07/11] Refactor message handler --- .../Tracker/WebViewMessageHandlerV2.swift | 68 +++++++++---------- Tests/TestWebViewMessageHandlerV2.swift | 5 ++ 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index a48e8a17f..2e4dd6296 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -38,37 +38,9 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { if let body = message.body as? [AnyHashable : Any], let atomicProperties = body["atomicProperties"] as? String { -// guard let atomicData = atomicProperties.data(using: .utf8) else { return } -// guard let atomicJson = try? JSONSerialization.jsonObject(with: atomicData) as? [String : Any] else { -// logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") -// return -// } guard let atomicJson = parseAtomicPropertiesFromMessage(atomicProperties) else { return } - - let selfDescribingEventData = body["selfDescribingEventData"] as? String? ?? nil - var selfDescribingDataJson: [AnyHashable : Any] = [:] - - if (selfDescribingEventData != nil) { - let data = selfDescribingEventData?.data(using: .utf8) ?? Data() - guard let json = try? JSONSerialization.jsonObject(with: data) as? [AnyHashable : Any] else { - logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") - return - } - selfDescribingDataJson = json - } - - let entitiesData = body["entities"] as? String? ?? nil - var entitiesJson: [[AnyHashable : Any]] = [] - - if (entitiesData != nil) { - let data = entitiesData?.data(using: .utf8) ?? Data() - guard let json = try? JSONSerialization.jsonObject(with: data) as? [[AnyHashable : Any]] else { - logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") - return - } - entitiesJson = json - } - + let selfDescribingDataJson = parseSelfDescribingEventDataFromMessage(body["selfDescribingEventData"] as? String) ?? [:] + let entitiesJson = parseEntitiesFromMessage(body["entities"] as? String) ?? [] let trackers = body["trackers"] as? [String] ?? [] let event = WebViewReader( @@ -128,18 +100,42 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { } func parseAtomicPropertiesFromMessage(_ messageString: String?) -> [String : Any]? { - let atomicData = messageString?.data(using: .utf8) - - var atomicJson: [String : Any]? = [:] - if let data = atomicData { - atomicJson = try? JSONSerialization.jsonObject(with: data) as? [String : Any] + guard let atomicData = messageString?.data(using: .utf8) else { + logError(message: "WebView: No atomic properties provided, skipping.") + return nil } - if (atomicData == nil) || (atomicJson == nil) { + guard let atomicJson = try? JSONSerialization.jsonObject(with: atomicData) as? [String : Any] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return nil } return atomicJson } + + func parseSelfDescribingEventDataFromMessage(_ messageString: String?) -> [String : Any]? { + guard (messageString != nil) else { return nil } + guard let eventData = messageString?.data(using: .utf8) else { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return nil + } + guard let eventJson = try? JSONSerialization.jsonObject(with: eventData) as? [String : Any] else { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return nil + } + return eventJson + } + + func parseEntitiesFromMessage(_ messageString: String?) -> [[AnyHashable : Any]]? { + guard (messageString != nil) else { return nil } + guard let entitiesData = messageString?.data(using: .utf8) else { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return nil + } + guard let entitiesJson = try? JSONSerialization.jsonObject(with: entitiesData) as? [[AnyHashable : Any]] else { + logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") + return nil + } + return entitiesJson + } } diff --git a/Tests/TestWebViewMessageHandlerV2.swift b/Tests/TestWebViewMessageHandlerV2.swift index 732d2c0cb..51c2118db 100644 --- a/Tests/TestWebViewMessageHandlerV2.swift +++ b/Tests/TestWebViewMessageHandlerV2.swift @@ -202,6 +202,11 @@ class TestWebViewMessageHandlerV2: XCTestCase { ]) webViewMessageHandler?.receivedMessage(message) // shouldn't crash } + + func testHandlesMissingAtomicPropertiesInEvent() { + let message = MockWKScriptMessage(body: []) + webViewMessageHandler?.receivedMessage(message) // shouldn't crash + } private func waitForEventsToBeTracked() { let expect = expectation(description: "Wait for events to be tracked") From c2fb7808885dea9a721a22ee46c0735971d49e45 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Tue, 17 Dec 2024 13:01:49 +0000 Subject: [PATCH 08/11] Add subscription to Snowplow main class --- Sources/Snowplow/Snowplow.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Snowplow/Snowplow.swift b/Sources/Snowplow/Snowplow.swift index 82d6d864f..1d6f332f2 100644 --- a/Sources/Snowplow/Snowplow.swift +++ b/Sources/Snowplow/Snowplow.swift @@ -342,9 +342,11 @@ public class Snowplow: NSObject { /// - Parameter webViewConfiguration: Configuration of the Web view to subscribe to events from @objc public class func subscribeToWebViewEvents(with webViewConfiguration: WKWebViewConfiguration) { - let messageHandler = WebViewMessageHandler() + let messageHandlerOld = WebViewMessageHandler() + let messageHandlerV2 = WebViewMessageHandlerV2() - webViewConfiguration.userContentController.add(messageHandler, name: "snowplow") + webViewConfiguration.userContentController.add(messageHandlerOld, name: "snowplow") + webViewConfiguration.userContentController.add(messageHandlerV2, name: "snowplow") } #endif From f6ea23a8dfc3b5ec8c7d2621b764c3426e0206e2 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 18 Dec 2024 11:31:04 +0000 Subject: [PATCH 09/11] Update based on review comments --- Sources/Core/Tracker/TrackerEvent.swift | 8 +++----- .../Core/Tracker/WebViewMessageHandlerV2.swift | 18 ++++++------------ Sources/Snowplow/Snowplow.swift | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift index 83200f89b..d29d9c1ef 100644 --- a/Sources/Core/Tracker/TrackerEvent.swift +++ b/Sources/Core/Tracker/TrackerEvent.swift @@ -35,11 +35,11 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { var trueTimestamp: Date? - private(set) var isPrimitive: Bool + private(set) var isPrimitive: Bool = false private(set) var isService: Bool - private(set) var isWebView: Bool + private(set) var isWebView: Bool = false init(event: Event, eventId: UUID = UUID(), state: TrackerStateSnapshot? = nil) { self.eventId = eventId @@ -50,12 +50,10 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { self.state = state ?? TrackerState() isService = (event is TrackerError) - isWebView = false - isPrimitive = false switch event { case _ as WebViewReader: - eventName = (payload[kSPEvent] as? String) ?? "ue" + eventName = (payload[kSPEvent] as? String) ?? kSPEventUnstructured schema = getWebViewSchema() isWebView = true diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index 2e4dd6296..51ff250ee 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -112,12 +112,9 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { } func parseSelfDescribingEventDataFromMessage(_ messageString: String?) -> [String : Any]? { - guard (messageString != nil) else { return nil } - guard let eventData = messageString?.data(using: .utf8) else { - logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") - return nil - } - guard let eventJson = try? JSONSerialization.jsonObject(with: eventData) as? [String : Any] else { + if messageString == nil { return nil } + guard let eventData = messageString?.data(using: .utf8), + let eventJson = try? JSONSerialization.jsonObject(with: eventData) as? [String : Any] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return nil } @@ -125,12 +122,9 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { } func parseEntitiesFromMessage(_ messageString: String?) -> [[AnyHashable : Any]]? { - guard (messageString != nil) else { return nil } - guard let entitiesData = messageString?.data(using: .utf8) else { - logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") - return nil - } - guard let entitiesJson = try? JSONSerialization.jsonObject(with: entitiesData) as? [[AnyHashable : Any]] else { + if messageString == nil { return nil } + guard let entitiesData = messageString?.data(using: .utf8), + let entitiesJson = try? JSONSerialization.jsonObject(with: entitiesData) as? [[AnyHashable : Any]] else { logError(message: "WebView: Received event payload is not serializable to JSON, skipping.") return nil } diff --git a/Sources/Snowplow/Snowplow.swift b/Sources/Snowplow/Snowplow.swift index 1d6f332f2..8bb999a5a 100644 --- a/Sources/Snowplow/Snowplow.swift +++ b/Sources/Snowplow/Snowplow.swift @@ -346,7 +346,7 @@ public class Snowplow: NSObject { let messageHandlerV2 = WebViewMessageHandlerV2() webViewConfiguration.userContentController.add(messageHandlerOld, name: "snowplow") - webViewConfiguration.userContentController.add(messageHandlerV2, name: "snowplow") + webViewConfiguration.userContentController.add(messageHandlerV2, name: "snowplowV2") } #endif From 52940cea34f0d8e0ae7de59f582cb355134fbab4 Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 18 Dec 2024 11:38:51 +0000 Subject: [PATCH 10/11] Remove unnecessary null checks --- .../Tracker/WebViewMessageHandlerV2.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index 51ff250ee..dc42d5e73 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -45,21 +45,21 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { let event = WebViewReader( selfDescribingEventData: createSelfDescribingJson(selfDescribingDataJson), - eventName: atomicJson["eventName"] as? String? ?? nil, - trackerVersion: atomicJson["trackerVersion"] as? String? ?? nil, - useragent: atomicJson["useragent"] as? String? ?? nil, - pageUrl: atomicJson["pageUrl"] as? String? ?? nil, - pageTitle: atomicJson["pageTitle"] as? String? ?? nil, - referrer: atomicJson["referrer"] as? String? ?? nil, - category: atomicJson["category"] as? String? ?? nil, - action: atomicJson["action"] as? String? ?? nil, - label: atomicJson["label"] as? String? ?? nil, - property: atomicJson["property"] as? String? ?? nil, + eventName: atomicJson["eventName"] as? String, + trackerVersion: atomicJson["trackerVersion"] as? String, + useragent: atomicJson["useragent"] as? String, + pageUrl: atomicJson["pageUrl"] as? String, + pageTitle: atomicJson["pageTitle"] as? String, + referrer: atomicJson["referrer"] as? String, + category: atomicJson["category"] as? String, + action: atomicJson["action"] as? String, + label: atomicJson["label"] as? String, + property: atomicJson["property"] as? String, value: atomicJson["value"] as? Double, - pingXOffsetMin: atomicJson["pingXOffsetMin"] as? Int ?? nil, - pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int ?? nil, - pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int ?? nil, - pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int ?? nil + pingXOffsetMin: atomicJson["pingXOffsetMin"] as? Int, + pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int, + pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int, + pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int ) track(event, withEntities: entitiesJson, andTrackers: trackers) From 36c3147b590470d04ef06fdc314036e43881dd8b Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Mon, 6 Jan 2025 17:35:33 +0000 Subject: [PATCH 11/11] Add an error log if the namespace doesn't exist --- Sources/Core/Tracker/WebViewMessageHandlerV2.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift index dc42d5e73..77088c74e 100644 --- a/Sources/Core/Tracker/WebViewMessageHandlerV2.swift +++ b/Sources/Core/Tracker/WebViewMessageHandlerV2.swift @@ -73,6 +73,8 @@ class WebViewMessageHandlerV2: NSObject, WKScriptMessageHandler { for namespace in trackers { if let tracker = Snowplow.tracker(namespace: namespace) { _ = tracker.track(event) + } else { + logError(message: "WebView: Tracker with namespace \(namespace) not found.") } } } else {