diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift index ee9dffa75..1c2a7160c 100644 --- a/Sources/Core/Tracker/Tracker.swift +++ b/Sources/Core/Tracker/Tracker.swift @@ -43,7 +43,6 @@ class Tracker: NSObject { private var builderFinished = false - /// The object used for sessionization, i.e. it characterizes user activity. private(set) var session: Session? /// Previous screen view state. @@ -385,6 +384,44 @@ class Tracker: NSObject { emitter.resumeTimer() session?.startChecker() } + + @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() + let sessionId = extendedParameters.sessionId ? self.session?.state?.sessionId ?? "" : "" + let sourceId = extendedParameters.sourceId ? self.appId : "" + let sourcePlatform = extendedParameters.sourcePlatform ? devicePlatformToString(self.devicePlatform) ?? "" : "" + let subjectUserId = extendedParameters.subjectUserId ? self.subject?.domainUserId ?? "" : "" + let reason = extendedParameters.reason ?? "" + + let spParameters = [ + userId, + String(Date().timeIntervalSince1970), + sessionId, + subjectUserId, + sourceId, + sourcePlatform, + reason + ].joined(separator: ".") + + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + if var existingSp = components?.queryItems?.first(where: { $0.name == "_sp" }) { + existingSp.value = spParameters + } else { + components?.queryItems?.append(URLQueryItem(name: "_sp", value: spParameters)) + } + + return components?.url + } + // MARK: - Notifications management diff --git a/Sources/Core/Tracker/TrackerControllerImpl.swift b/Sources/Core/Tracker/TrackerControllerImpl.swift index afc0a5f90..97a3d2fe6 100644 --- a/Sources/Core/Tracker/TrackerControllerImpl.swift +++ b/Sources/Core/Tracker/TrackerControllerImpl.swift @@ -73,6 +73,10 @@ class TrackerControllerImpl: Controller, TrackerController { func track(_ event: Event) -> UUID? { return tracker.track(event) } + + func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration? = nil) -> URL? { + return tracker.decorateLink(url, extendedParameters: extendedParameters) + } // MARK: - Properties' setters and getters diff --git a/Sources/Snowplow/Controllers/TrackerController.swift b/Sources/Snowplow/Controllers/TrackerController.swift index 3c5fcdf4c..657f33ce1 100644 --- a/Sources/Snowplow/Controllers/TrackerController.swift +++ b/Sources/Snowplow/Controllers/TrackerController.swift @@ -75,4 +75,7 @@ public protocol TrackerController: TrackerConfigurationProtocol { /// The tracker will start tracking again. @objc func resume() + + @objc + func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration?) -> URL? } diff --git a/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift b/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift new file mode 100644 index 000000000..510d04c65 --- /dev/null +++ b/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift @@ -0,0 +1,36 @@ +// 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 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 = false, + sourcePlatform: Bool = false, + reason: String? = nil + ) { + self.sessionId = sessionId + self.subjectUserId = subjectUserId + self.sourceId = sourceId + self.sourcePlatform = sourcePlatform + self.reason = reason + } +} diff --git a/Tests/TestLinkDecorator.swift b/Tests/TestLinkDecorator.swift new file mode 100644 index 000000000..ed783e424 --- /dev/null +++ b/Tests/TestLinkDecorator.swift @@ -0,0 +1,46 @@ +// 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 XCTest +@testable import SnowplowTracker + +class TestLinkDecorator : XCTest { + + func testTest() { + let tracker = getTracker() + + let test = URL(string: "https://example.com")! + let result = tracker.decorateLink(test) + } + + func getTracker() -> TrackerController { + let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 200) + let emitterConfig = EmitterConfiguration() + emitterConfig.eventStore = MockEventStore() + emitterConfig.bufferOption = .single + let networkConfig = NetworkConfiguration(networkConnection: networkConnection) + + return createTracker(networkConfig: networkConfig, emitterConfig: emitterConfig) + } + + private func createTracker(networkConfig: NetworkConfiguration, emitterConfig: EmitterConfiguration) -> TrackerController { + let trackerConfig = TrackerConfiguration() + trackerConfig.installAutotracking = false + trackerConfig.screenViewAutotracking = false + trackerConfig.lifecycleAutotracking = false + let namespace = "testEmitter" + String(describing: Int.random(in: 0..<100)) + return Snowplow.createTracker(namespace: namespace, + network: networkConfig, + configurations: [trackerConfig, emitterConfig])! + } +}