-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add configuration to send requests with user ID to a Focal Meter endp…
- Loading branch information
1 parent
da1ee50
commit 09180bd
Showing
2 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
Sources/Snowplow/Configurations/FocalMeterConfiguration.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// | ||
// FocalMeterConfiguration.swift | ||
// Snowplow | ||
// | ||
// 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. | ||
// | ||
// License: Apache License Version 2.0 | ||
// | ||
|
||
import Foundation | ||
|
||
/// This configuration tells the tracker to send requests with the user ID in session context entity | ||
/// to a Kantar endpoint used with FocalMeter. | ||
/// The request is made when the first event with a new user ID is tracked. | ||
/// The requests are only made if session context is enabled (default). | ||
@objc(SPFocalMeterConfiguration) | ||
public class FocalMeterConfiguration: NSObject, PluginAfterTrackCallable, PluginIdentifiable, ConfigurationProtocol { | ||
public private(set) var identifier = "KantarFocalMeter" | ||
public private(set) var afterTrackConfiguration: PluginAfterTrackConfiguration? | ||
|
||
/// URL of the Kantar endpoint to send the requests to | ||
public private(set) var kantarEndpoint: String | ||
|
||
/// Callback to process user ID before sending it in a request. This may be used to apply hashing to the value. | ||
public private(set) var processUserId: ((String) -> String)? = nil | ||
|
||
private var lastUserId: String? | ||
|
||
/// Creates a configuration for the Kantar FocalMeter. | ||
/// - Parameters: | ||
/// - endpoint: URL of the Kantar endpoint to send the requests to | ||
/// - processUserId: Callback to process user ID before sending it in a request. This may be used to apply hashing to the value. | ||
@objc | ||
public init(kantarEndpoint: String, processUserId: ((String) -> String)? = nil) { | ||
self.kantarEndpoint = kantarEndpoint | ||
super.init() | ||
|
||
self.afterTrackConfiguration = PluginAfterTrackConfiguration { event in | ||
let session = event.entities.first { entity in | ||
entity.schema == kSPSessionContextSchema | ||
} | ||
if let userId = session?.data[kSPSessionUserId] as? String { | ||
if self.shouldUpdate(userId) { | ||
if let processUserId = processUserId { | ||
self.makeRequest(userId: processUserId(userId)) | ||
} else { | ||
self.makeRequest(userId: userId) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func shouldUpdate(_ newUserId: String) -> Bool { | ||
objc_sync_enter(self) | ||
defer { objc_sync_exit(self) } | ||
|
||
if lastUserId == nil || newUserId != lastUserId { | ||
lastUserId = newUserId | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
private func makeRequest(userId: String) { | ||
var components = URLComponents(string: kantarEndpoint) | ||
components?.queryItems = [ | ||
URLQueryItem(name: "vendor", value: "snowplow"), | ||
URLQueryItem(name: "cs_fpid", value: userId), | ||
URLQueryItem(name: "c12", value: "not_set"), | ||
] | ||
|
||
guard let url = components?.url else { | ||
logError(message: "Failed to build URL to request Kantar endpoint") | ||
return | ||
} | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = "GET" | ||
let task = URLSession.shared.dataTask(with: request) { data, response, error in | ||
if let error = error { | ||
logError(message: "Request to Kantar endpoint failed: \(error)") | ||
} | ||
else if let httpResponse = response as? HTTPURLResponse { | ||
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { | ||
logDebug(message: "Request to Kantar endpoint sent with user ID: \(userId)") | ||
return | ||
} else { | ||
logError(message: "Request to Kantar endpoint was not successful") | ||
} | ||
} | ||
} | ||
task.resume() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// | ||
// TestFocalMeterConfiguration.swift | ||
// Snowplow-iOSTests | ||
// | ||
// 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. | ||
// | ||
// Authors: Alex Benini, Matus Tomlein | ||
// License: Apache License Version 2.0 | ||
// | ||
|
||
import XCTest | ||
import Mocker | ||
@testable import SnowplowTracker | ||
|
||
class TestFocalMeterConfiguration: XCTestCase { | ||
let endpoint = "https://fake-snowplow.io" | ||
|
||
#if !os(watchOS) && !os(macOS) // Mocker seems not to currently work on watchOS and macOS | ||
|
||
override class func setUp() { | ||
Mocker.removeAll() | ||
} | ||
|
||
override class func tearDown() { | ||
Mocker.removeAll() | ||
Snowplow.removeAllTrackers() | ||
super.tearDown() | ||
} | ||
|
||
func testMakesRequestToKantarEndpointWithUserId() { | ||
let tracker = createTracker() | ||
|
||
let requestExpectation = expectation(description: "Request made") | ||
mockRequest { query in | ||
let userId = tracker.session!.userId! | ||
XCTAssertTrue(query!.contains(userId)) | ||
requestExpectation.fulfill() | ||
} | ||
|
||
_ = tracker.track(Structured(category: "cat", action: "act")) | ||
wait(for: [requestExpectation], timeout: 1) | ||
} | ||
|
||
func testMakesRequestToKantarEndpointWithProcessedUserId() { | ||
let configuration = FocalMeterConfiguration(kantarEndpoint: endpoint) { userId in | ||
return "processed-" + userId | ||
} | ||
let tracker = createTracker(configuration) | ||
|
||
let requestExpectation = expectation(description: "Request made") | ||
mockRequest { query in | ||
let userId = tracker.session!.userId! | ||
XCTAssertTrue(query!.contains("processed-" + userId)) | ||
requestExpectation.fulfill() | ||
} | ||
|
||
_ = tracker.track(Structured(category: "cat", action: "act")) | ||
wait(for: [requestExpectation], timeout: 1) | ||
} | ||
|
||
func testMakesRequestToKantarEndpointWhenUserIdChanges() { | ||
// log queries of requests | ||
var kantarRequestQueries: [String] = [] | ||
let tracker = createTracker() | ||
var requestExpectation: XCTestExpectation? = expectation(description: "Anonymous request made") | ||
mockRequest { query in | ||
kantarRequestQueries.append(query!) | ||
requestExpectation?.fulfill() | ||
} | ||
|
||
// enable user anonymisation, should trigger request with anonymous user id | ||
tracker.userAnonymisation = true | ||
_ = tracker.track(Structured(category: "cat", action: "act")) | ||
wait(for: [requestExpectation!], timeout: 1) | ||
XCTAssertEqual(1, kantarRequestQueries.count) | ||
XCTAssertTrue(kantarRequestQueries.first!.contains("00000000-0000-0000-0000-000000000000")) | ||
kantarRequestQueries.removeAll() | ||
|
||
// disable user anonymisation, should trigger new request | ||
requestExpectation = expectation(description: "Second request made") | ||
tracker.userAnonymisation = false | ||
_ = tracker.track(ScreenView(name: "sv")) | ||
wait(for: [requestExpectation!], timeout: 1) | ||
XCTAssertEqual(1, kantarRequestQueries.count) | ||
let userId = tracker.session!.userId! | ||
XCTAssertTrue(kantarRequestQueries.first!.contains(userId)) | ||
kantarRequestQueries.removeAll() | ||
|
||
// tracking another should not trigger a request as user ID did not change | ||
requestExpectation = nil | ||
_ = tracker.track(Structured(category: "cat", action: "act")) | ||
let sleep = expectation(description: "Wait for events to be tracked") | ||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { () -> Void in | ||
sleep.fulfill() | ||
} | ||
wait(for: [sleep], timeout: 1) | ||
XCTAssertEqual(0, kantarRequestQueries.count) | ||
} | ||
|
||
private func mockRequest(callback: @escaping (String?) -> Void) { | ||
var mock = Mock(url: URL(string: endpoint)!, ignoreQuery: true, dataType: .json, statusCode: 200, data: [ | ||
.get: Data() | ||
]) | ||
mock.onRequest = { (request, body) in | ||
callback(request.url?.query) | ||
} | ||
mock.register() | ||
} | ||
|
||
private func createTracker(_ focalMeterConfig: FocalMeterConfiguration? = nil) -> TrackerController { | ||
let connection = MockNetworkConnection(requestOption: .post, statusCode: 200) | ||
let networkConfig = NetworkConfiguration(networkConnection: connection) | ||
let trackerConfig = TrackerConfiguration() | ||
trackerConfig.installAutotracking = false | ||
trackerConfig.diagnosticAutotracking = false | ||
let namespace = "testFocalMeter" + String(describing: Int.random(in: 0..<100)) | ||
return Snowplow.createTracker(namespace: namespace, | ||
network: networkConfig, | ||
configurations: [ | ||
trackerConfig, | ||
focalMeterConfig ?? FocalMeterConfiguration(kantarEndpoint: endpoint) | ||
])! | ||
} | ||
|
||
#endif | ||
} |