Skip to content

Commit

Permalink
Merge branch 'release/6.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
mscwilson committed Jan 20, 2025
2 parents c105d9f + bf5e944 commit a6bcc21
Show file tree
Hide file tree
Showing 11 changed files with 563 additions and 30 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Version 6.1.0 (2025-01-16)
--------------------------
Add new WebView interface (#913)

Version 6.0.9 (2024-11-21)
--------------------------
Handle nan values and other non-serializable data in events from the WebView tracker (#909)
Expand Down
2 changes: 1 addition & 1 deletion Examples
2 changes: 1 addition & 1 deletion SnowplowTracker.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SnowplowTracker"
s.version = "6.0.9"
s.version = "6.1.0"
s.summary = "Snowplow event tracker for iOS, macOS, tvOS, watchOS for apps and games."
s.description = <<-DESC
Snowplow is a mobile and event analytics platform with a difference: rather than tell our users how they should analyze their data, we deliver their event-level data in their own data warehouse, on their own Amazon Redshift or Postgres database, so they can analyze it any way they choose. Snowplow mobile is used by data-savvy games companies and app developers to better understand their users and how they engage with their games and applications. Snowplow is open source using the business-friendly Apache License, Version 2.0 and scales horizontally to many billions of events.
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
Loading

0 comments on commit a6bcc21

Please sign in to comment.