-
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…
…oint (close #745)
- Loading branch information
1 parent
88467ff
commit 7d54982
Showing
2 changed files
with
206 additions
and
3 deletions.
There are no files selected for viewing
95 changes: 92 additions & 3 deletions
95
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 |
---|---|---|
@@ -1,8 +1,97 @@ | ||
// | ||
// File.swift | ||
// | ||
// FocalMeterConfiguration.swift | ||
// Snowplow | ||
// | ||
// Created by Matus Tomlein on 30/12/2022. | ||
// Copyright (c) 2013-2022 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, PluginConfigurationProtocol, ConfigurationProtocol { | ||
public private(set) var identifier = "KantarFocalMeter" | ||
public private(set) var entitiesConfiguration: PluginEntitiesConfiguration? = nil | ||
public private(set) var afterTrackConfiguration: PluginAfterTrackConfiguration? | ||
/// URL of the Kantar endpoint to send the requests to | ||
public private(set) var kantarEndpoint: String | ||
private var lastUserId: String? | ||
|
||
/// Creates a configuration for the Kantar FocalMeter. | ||
/// - Parameters: | ||
/// - endpoint: URL of the Kantar endpoint to send the requests to | ||
@objc | ||
public init(kantarEndpoint: String) { | ||
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) { | ||
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,114 @@ | ||
// | ||
// TestFocalMeterConfiguration.swift | ||
// Snowplow-iOSTests | ||
// | ||
// Copyright (c) 2013-2022 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) // Mocker seems not to currently work on watchOS | ||
|
||
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 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() -> TrackerController { | ||
let connection = MockNetworkConnection(requestOption: .post, statusCode: 200) | ||
let networkConfig = NetworkConfiguration(networkConnection: connection) | ||
let trackerConfig = TrackerConfiguration() | ||
trackerConfig.installAutotracking = false | ||
trackerConfig.diagnosticAutotracking = false | ||
let focalMeterConfig = FocalMeterConfiguration(kantarEndpoint: endpoint) | ||
let namespace = "testFocalMeter" + String(describing: Int.random(in: 0..<100)) | ||
return Snowplow.createTracker(namespace: namespace, | ||
network: networkConfig, | ||
configurations: [trackerConfig, focalMeterConfig])! | ||
} | ||
|
||
#endif | ||
} |