Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new WebView Interface #913

Open
wants to merge 11 commits into
base: release/6.1.0
Choose a base branch
from
Open
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
126 changes: 126 additions & 0 deletions Sources/Core/Events/WebViewReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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[kSPWebViewEventData] = 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[kSPStructCategory] = category
}
if let action = action {
payload[kSPStructAction] = action
}
if let label = label {
payload[kSPStructLabel] = label
}
if let property = property {
payload[kSPStructProperty] = property
}
if let value = value {
payload[kSPStructValue] = String(value)
}
if let pingXOffsetMin = pingXOffsetMin {
payload[kSPPingXOffsetMin] = String(pingXOffsetMin)
}
if let pingXOffsetMax = pingXOffsetMax {
payload[kSPPingXOffsetMax] = String(pingXOffsetMax)
}
if let pingYOffsetMin = pingYOffsetMin {
payload[kSPPingYOffsetMin] = String(pingYOffsetMin)
}
if let pingYOffsetMax = pingYOffsetMax {
payload[kSPPingYOffsetMax] = String(pingYOffsetMax)
}
return payload
}
}
54 changes: 41 additions & 13 deletions Sources/Core/Tracker/TrackerEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ 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 = false

init(event: Event, eventId: UUID = UUID(), state: TrackerStateSnapshot? = nil) {
self.eventId = eventId
timestamp = Int64(Date().timeIntervalSince1970 * 1000)
Expand All @@ -48,12 +50,19 @@ class TrackerEvent : InspectableEvent, StateMachineEvent {
self.state = state ?? TrackerState()

isService = (event is TrackerError)
if let abstractEvent = event as? PrimitiveAbstract {
eventName = abstractEvent.eventName

switch event {
case _ as WebViewReader:
eventName = (payload[kSPEvent] as? String) ?? kSPEventUnstructured
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
}
}

Expand Down Expand Up @@ -90,24 +99,43 @@ 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?.schema
}

private func addSelfDescribingDataToPayload(to payload: Payload, base64Encoded: Bool, data: SelfDescribingJson) {
let unstructuredEventPayload = SelfDescribingJson.dictionary(
schema: kSPUnstructSchema,
data: data.dictionary)
payload.addDictionaryToPayload(
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)
}
}
138 changes: 138 additions & 0 deletions Sources/Core/Tracker/WebViewMessageHandlerV2.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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? String {

guard let atomicJson = parseAtomicPropertiesFromMessage(atomicProperties) else { return }
let selfDescribingDataJson = parseSelfDescribingEventDataFromMessage(body["selfDescribingEventData"] as? String) ?? [:]
let entitiesJson = parseEntitiesFromMessage(body["entities"] as? String) ?? []
let trackers = body["trackers"] as? [String] ?? []

let event = WebViewReader(
selfDescribingEventData: createSelfDescribingJson(selfDescribingDataJson),
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,
pingXOffsetMax: atomicJson["pingXOffsetMax"] as? Int,
pingYOffsetMin: atomicJson["pingYOffsetMin"] as? Int,
pingYOffsetMax: atomicJson["pingYOffsetMax"] as? Int
)

track(event, withEntities: entitiesJson, andTrackers: trackers)
}
}

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) {
_ = tracker.track(event)
} else {
logError(message: "WebView: Tracker with namespace \(namespace) not found.")
}
}
} else {
_ = Snowplow.defaultTracker()?.track(event)
}
}

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 entities {
if let entity = createSelfDescribingJson(entityJson) {
contextEntities.append(entity)
}
}
return contextEntities
}

func parseAtomicPropertiesFromMessage(_ messageString: String?) -> [String : Any]? {
guard let atomicData = messageString?.data(using: .utf8) else {
logError(message: "WebView: No atomic properties provided, skipping.")
return 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]? {
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
}
return eventJson
}

func parseEntitiesFromMessage(_ messageString: String?) -> [[AnyHashable : Any]]? {
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
}
return entitiesJson
}
}


#endif
7 changes: 7 additions & 0 deletions Sources/Core/TrackerConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 4 additions & 2 deletions Sources/Snowplow/Snowplow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "snowplowV2")
}

#endif
Expand Down
Loading
Loading