Skip to content

Commit

Permalink
Add API to decorate link with user/session info
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-el committed Sep 22, 2023
1 parent 1f76199 commit 8e7babb
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 1 deletion.
76 changes: 75 additions & 1 deletion Sources/Core/Tracker/Tracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,81 @@ class Tracker: NSObject {
emitter.resumeTimer()
session?.startChecker()
}


/// - Returns: The associated value of the extended parameter if enabled and set in the tracker, an empty string if the parameter is not enabled, or nil if parameter is enabled without an aviliable value
func tryExtendedParameter(_ extendedParameterSet: Bool, _ value: String?) -> String? {
switch (extendedParameterSet, value) {
case (true, .some(let val)):
return val
case (true, .none):
return nil
case (false, _):
return ""
}
}

func decorateLinkErrorTemplate(_ extendedParameterName: String) -> String {
"Cannot decorate link: \(extendedParameterName) has been requested in CrossDeviceParameterConfiguration, but it is not set."
}

@objc func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration? = nil) -> URL? {
var userId: String
switch self.session {
case .none:
logError(message: "\(url) could not be decorated as session.userId is nil")
return nil
case .some(let s):
userId = s.userId
}

let extendedParameters = extendedParameters ?? CrossDeviceParameterConfiguration()

guard let sessionId = tryExtendedParameter(extendedParameters.sessionId, self.session?.state?.sessionId) else {
logError(message: "\(decorateLinkErrorTemplate("sessionId")) Ensure an event has been tracked to generate a session before calling this method.")
return nil
}

guard let sourceId = tryExtendedParameter(extendedParameters.sourceId, self.appId) else {
logError(message: decorateLinkErrorTemplate("appId"))
return nil
}

guard let sourcePlatform = tryExtendedParameter(extendedParameters.sourcePlatform, devicePlatformToString(self.devicePlatform)) else {
logError(message: decorateLinkErrorTemplate("sourcePlatform"))
return nil
}

guard let subjectUserId = tryExtendedParameter(extendedParameters.subjectUserId, self.subject?.userId) else {
logError(message: "\(decorateLinkErrorTemplate("subjectUserId")) Ensure SubjectConfiguration.userId has been passed set on your tracker.")
return nil
}

let reason = extendedParameters.reason ?? ""

let spParameters = [
userId,
String(Int(Date().timeIntervalSince1970 * 1000)),
sessionId,
subjectUserId.toBase64(),
sourceId.toBase64(),
sourcePlatform,
reason.toBase64()
].joined(separator: ".").trimmingCharacters(in: ["."])

var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let spQueryParam = URLQueryItem(name: "_sp", value: spParameters)

if let index = components?.queryItems?.firstIndex(where: { $0.name == "_sp" }) {
// Replace the old query item with the new one
components?.queryItems?[index] = spQueryParam
} else {
let queryItems = components?.queryItems ?? []
components?.queryItems = queryItems + [spQueryParam]
}

return components?.url
}

// MARK: - Notifications management

@objc func receiveScreenViewNotification(_ notification: Notification) {
Expand Down
8 changes: 8 additions & 0 deletions Sources/Core/Tracker/TrackerControllerImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ class TrackerControllerImpl: Controller, TrackerController {
func track(_ event: Event) -> UUID? {
return tracker.track(event)
}

func decorateLink(_ url: URL) -> URL? {
return tracker.decorateLink(url)
}

func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration) -> URL? {
return tracker.decorateLink(url, extendedParameters: extendedParameters)
}

// MARK: - Properties' setters and getters

Expand Down
36 changes: 36 additions & 0 deletions Sources/Snowplow/Controllers/TrackerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,40 @@ public protocol TrackerController: TrackerConfigurationProtocol {
/// The tracker will start tracking again.
@objc
func resume()
/// Adds user and session information to a URL.
///
/// For example, calling decorateLink on `appSchema://path/to/page` will return:
///
/// `appSchema://path/to/page?_sp=domainUserId.timestamp.sessionId..sourceId`
///
/// Filled by this method:
/// - `domainUserId`: Value of ``SessionController.userId``
/// - `timestamp`: ms precision epoch timestamp
/// - `sessionId`: Value of ``SessionController.sessionId``
/// - `sourceId`: Value of ``Tracker.appId``
///
/// - Parameter uri The URI to add the query string to
///
/// - Returns Optional URL
/// - nil if ``SnowplowTracker/SessionController/userId`` is null from `sessionContext(false)` being passed in ``TrackerConfiguration``
/// - otherwise, decorated URL
@objc
func decorateLink(_ url: URL) -> URL?
/// Adds user and session information to a URL.
///
/// For example, calling decorateLink on `appSchema://path/to/page` with all extended parameters enabled will return:
///
/// `appSchema://path/to/page?_sp=domainUserId.timestamp.sessionId.subjectUserId.sourceId.platform.reason`
///
/// - Parameter url The URL to add the query string to
/// - Parameter extendedParameters Any optional parameters to include in the query string.
///
/// - Returns Optional URL
/// - nil if:
///
/// - ``SnowplowTracker/SessionController/userId`` is null from `sessionContext(false)` being passed in ``TrackerConfiguration``
/// - An enabled CrossDeviceParameter isn't set in the tracker
/// - otherwise, decorated URL
@objc
func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration) -> URL?
}
47 changes: 47 additions & 0 deletions Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2013-2023 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

///
/// Configuration object for ``TrackerController/decorateLink``
///
/// Whether to include the following values when decorating a link:
/// - `sessionId`: Value of ``SessionController.sessionId``
/// - `subjectUserId`: Value of ``Subject.userId``
/// - `sourceId`: Value of ``Tracker.appId``
/// - `platform`: Value of ``Tracker.platform``
/// - `reason`: Optional identifier/information for cross-navigation
///
/// ``sourceId`` and ``sessionId`` are enabled by default.
public class CrossDeviceParameterConfiguration : NSObject {
var sessionId: Bool
var subjectUserId: Bool
var sourceId: Bool
var sourcePlatform: Bool
var reason: String?

init(
sessionId: Bool = true,
subjectUserId: Bool = false,
sourceId: Bool = true,
sourcePlatform: Bool = false,
reason: String? = nil
) {
self.sessionId = sessionId
self.subjectUserId = subjectUserId
self.sourceId = sourceId
self.sourcePlatform = sourcePlatform
self.reason = reason
}
}
33 changes: 33 additions & 0 deletions Sources/Snowplow/Utils/Stringb64.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2013-2023 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

public extension String {
func toBase64(urlSafe: Bool = true) -> String {
var encoded = Data(self.utf8).base64EncodedString()
if urlSafe {
// We need URL safe with no padding. Since there is no built-in way to do this, we transform
// the encoded payload to make it URL safe by replacing chars that are different in the URL-safe
// alphabet. Namely, 62 is - instead of +, and 63 _ instead of /.
// See: https://tools.ietf.org/html/rfc4648#section-5
encoded = encoded
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "+", with: "-")

// There is also no padding since the length is implicitly known.
encoded = encoded.trimmingCharacters(in: CharacterSet(charactersIn: "="))
}
return encoded
}
}
Loading

0 comments on commit 8e7babb

Please sign in to comment.