-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
328 additions
and
4 deletions.
There are no files selected for viewing
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
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 +1 @@ | ||
5.5.0 | ||
5.6.0 |
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
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
196 changes: 196 additions & 0 deletions
196
...rc/androidTest/java/com/snowplowanalytics/snowplow/tracker/FocalMeterConfigurationTest.kt
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,196 @@ | ||
/* | ||
* Copyright (c) 2015-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. | ||
*/ | ||
|
||
package com.snowplowanalytics.snowplow.tracker | ||
|
||
import android.content.Context | ||
import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
import androidx.test.platform.app.InstrumentationRegistry | ||
import com.snowplowanalytics.snowplow.Snowplow | ||
import com.snowplowanalytics.snowplow.Snowplow.removeAllTrackers | ||
import com.snowplowanalytics.snowplow.configuration.* | ||
import com.snowplowanalytics.snowplow.controller.TrackerController | ||
import com.snowplowanalytics.snowplow.event.Structured | ||
import com.snowplowanalytics.snowplow.network.HttpMethod | ||
import okhttp3.mockwebserver.MockResponse | ||
import okhttp3.mockwebserver.MockWebServer | ||
import org.junit.After | ||
import org.junit.Assert | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import java.util.* | ||
|
||
@RunWith(AndroidJUnit4::class) | ||
class FocalMeterConfigurationTest { | ||
|
||
@After | ||
fun tearDown() { | ||
removeAllTrackers() | ||
} | ||
|
||
// --- TESTS | ||
@Test | ||
fun logsSuccessfulRequest() { | ||
withMockServer(200) { mockServer, endpoint -> | ||
val focalMeter = FocalMeterConfiguration(endpoint) | ||
val debugs = mutableListOf<String>() | ||
val loggerDelegate = createLoggerDelegate(debugs = debugs) | ||
val trackerConfig = TrackerConfiguration(appId = "app-id") | ||
trackerConfig.logLevel(LogLevel.DEBUG) | ||
trackerConfig.loggerDelegate(loggerDelegate) | ||
|
||
val tracker = createTracker(listOf(focalMeter, trackerConfig)) | ||
tracker.track(Structured("cat", "act")) | ||
tracker.track(Structured("cat", "act")) | ||
tracker.track(Structured("cat", "act")) | ||
|
||
Thread.sleep(500) | ||
Assert.assertEquals( | ||
1, | ||
debugs.filter { | ||
it.contains("Request to Kantar endpoint sent with user ID: ${tracker.session?.userId}") | ||
}.size | ||
) | ||
} | ||
} | ||
|
||
@Test | ||
fun logsSuccessfulRequestWithProcessedUserId() { | ||
withMockServer(200) { mockServer, endpoint -> | ||
val focalMeter = FocalMeterConfiguration( | ||
kantarEndpoint = endpoint, | ||
processUserId = { userId -> "processed-" + userId } | ||
) | ||
val debugs = mutableListOf<String>() | ||
val loggerDelegate = createLoggerDelegate(debugs = debugs) | ||
val trackerConfig = TrackerConfiguration(appId = "app-id") | ||
trackerConfig.logLevel(LogLevel.DEBUG) | ||
trackerConfig.loggerDelegate(loggerDelegate) | ||
|
||
val tracker = createTracker(listOf(focalMeter, trackerConfig)) | ||
tracker.track(Structured("cat", "act")) | ||
|
||
Thread.sleep(500) | ||
Assert.assertEquals( | ||
1, | ||
debugs.filter { | ||
it.contains("Request to Kantar endpoint sent with user ID: processed-${tracker.session?.userId}") | ||
}.size | ||
) | ||
} | ||
} | ||
|
||
@Test | ||
fun makesAnotherRequestWhenUserIdChanges() { | ||
withMockServer(200) { mockServer, endpoint -> | ||
val focalMeter = FocalMeterConfiguration(endpoint) | ||
val debugs = mutableListOf<String>() | ||
val loggerDelegate = createLoggerDelegate(debugs = debugs) | ||
val trackerConfig = TrackerConfiguration(appId = "app-id") | ||
trackerConfig.logLevel(LogLevel.DEBUG) | ||
trackerConfig.loggerDelegate(loggerDelegate) | ||
|
||
val tracker = createTracker(listOf(focalMeter, trackerConfig)) | ||
tracker.track(Structured("cat", "act")) | ||
val firstUserId = tracker.session?.userId | ||
tracker.session?.startNewSession() | ||
tracker.track(Structured("cat", "act")) | ||
val secondUserId = tracker.session?.userId | ||
|
||
Thread.sleep(500) | ||
Assert.assertEquals( | ||
1, | ||
debugs.filter { | ||
it.contains("Request to Kantar endpoint sent with user ID: ${firstUserId}") | ||
}.size | ||
) | ||
Assert.assertEquals( | ||
1, | ||
debugs.filter { | ||
it.contains("Request to Kantar endpoint sent with user ID: ${secondUserId}") | ||
}.size | ||
) | ||
} | ||
} | ||
|
||
@Test | ||
fun logsFailedRequest() { | ||
withMockServer(500) { mockServer, endpoint -> | ||
val focalMeter = FocalMeterConfiguration(endpoint) | ||
val errors = mutableListOf<String>() | ||
val loggerDelegate = createLoggerDelegate(errors = errors) | ||
val trackerConfig = TrackerConfiguration(appId = "app-id") | ||
trackerConfig.logLevel(LogLevel.DEBUG) | ||
trackerConfig.loggerDelegate(loggerDelegate) | ||
|
||
val tracker = createTracker(listOf(focalMeter, trackerConfig)) | ||
tracker.track(Structured("cat", "act")) | ||
|
||
Thread.sleep(500) | ||
Assert.assertEquals( | ||
1, | ||
errors.filter { | ||
it.contains("Request to Kantar endpoint failed with code: 500") | ||
}.size | ||
) | ||
} | ||
} | ||
|
||
// --- PRIVATE | ||
private val context: Context | ||
get() = InstrumentationRegistry.getInstrumentation().targetContext | ||
|
||
private fun createTracker(configurations: List<Configuration>): TrackerController { | ||
val networkConfig = NetworkConfiguration(MockNetworkConnection(HttpMethod.POST, 200)) | ||
return Snowplow.createTracker( | ||
context, | ||
namespace = "ns" + Math.random().toString(), | ||
network = networkConfig, | ||
configurations = configurations.toTypedArray() | ||
) | ||
} | ||
|
||
private fun withMockServer(responseCode: Int, callback: (MockWebServer, String) -> Unit) { | ||
val mockServer = MockWebServer() | ||
mockServer.start() | ||
val mockResponse = MockResponse() | ||
.setResponseCode(responseCode) | ||
.setHeader("Content-Type", "application/json") | ||
.setBody("") | ||
mockServer.enqueue(mockResponse) | ||
val endpoint = String.format("http://%s:%d", mockServer.hostName, mockServer.port) | ||
callback(mockServer, endpoint) | ||
mockServer.shutdown() | ||
} | ||
|
||
private fun createLoggerDelegate( | ||
errors: MutableList<String> = mutableListOf(), | ||
debugs: MutableList<String> = mutableListOf(), | ||
verboses: MutableList<String> = mutableListOf() | ||
): LoggerDelegate { | ||
return object : LoggerDelegate { | ||
|
||
override fun error(tag: String, msg: String) { | ||
errors.add(msg) | ||
} | ||
|
||
override fun debug(tag: String, msg: String) { | ||
debugs.add(msg) | ||
} | ||
|
||
override fun verbose(tag: String, msg: String) { | ||
verboses.add(msg) | ||
} | ||
} | ||
} | ||
} |
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
95 changes: 95 additions & 0 deletions
95
...ker/src/main/java/com/snowplowanalytics/snowplow/configuration/FocalMeterConfiguration.kt
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,95 @@ | ||
/* | ||
* Copyright (c) 2015-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. | ||
*/ | ||
|
||
package com.snowplowanalytics.snowplow.configuration | ||
|
||
import android.net.Uri | ||
import com.snowplowanalytics.core.tracker.Logger | ||
import com.snowplowanalytics.snowplow.entity.ClientSessionEntity | ||
import okhttp3.OkHttpClient | ||
import okhttp3.Request | ||
import java.io.IOException | ||
import java.util.function.Function | ||
|
||
/** | ||
* 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). | ||
* @param kantarEndpoint The Kantar URI endpoint including the HTTP protocol to send the requests to. | ||
* @param processUserId Callback to process user ID before sending it in a request. This may be used to apply hashing to the value. | ||
*/ | ||
class FocalMeterConfiguration( | ||
val kantarEndpoint: String, | ||
val processUserId: Function<String, String>? = null, | ||
) : Configuration, PluginAfterTrackCallable, PluginIdentifiable { | ||
private val TAG = FocalMeterConfiguration::class.java.simpleName | ||
|
||
private var lastUserId: String? = null | ||
|
||
override val identifier: String | ||
get() = "KantarFocalMeter" | ||
|
||
override val afterTrackConfiguration: PluginAfterTrackConfiguration? | ||
get() = PluginAfterTrackConfiguration { event -> | ||
val session = event.entities.find { it is ClientSessionEntity } as? ClientSessionEntity | ||
session?.userId?.let { newUserId -> | ||
if (shouldUpdate(newUserId)) { | ||
val processedUserId = processUserId?.apply(newUserId) ?: newUserId | ||
makeRequest(processedUserId) | ||
} | ||
} | ||
} | ||
|
||
private fun shouldUpdate(userId: String): Boolean { | ||
synchronized(this) { | ||
if (lastUserId == null || lastUserId != userId) { | ||
lastUserId = userId | ||
return true | ||
} | ||
return false | ||
} | ||
} | ||
|
||
private fun makeRequest(userId: String) { | ||
val uriBuilder = Uri.parse(kantarEndpoint).buildUpon() | ||
uriBuilder.appendQueryParameter("vendor", "snowplow") | ||
uriBuilder.appendQueryParameter("cs_fpid", userId) | ||
uriBuilder.appendQueryParameter("c12", "not_set") | ||
|
||
val client = OkHttpClient.Builder() | ||
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS) | ||
.readTimeout(15, java.util.concurrent.TimeUnit.SECONDS) | ||
.build() | ||
|
||
val request = Request.Builder() | ||
.url(uriBuilder.build().toString()) | ||
.build() | ||
|
||
try { | ||
val response = client.newCall(request).execute() | ||
if (response.isSuccessful) { | ||
Logger.d(TAG, "Request to Kantar endpoint sent with user ID: $userId") | ||
} else { | ||
Logger.e(TAG, "Request to Kantar endpoint failed with code: ${response.code}") | ||
} | ||
} catch (e: IOException) { | ||
Logger.e(TAG, "Request to Kantar endpoint failed with exception: ${e.message}") | ||
} | ||
} | ||
|
||
override fun copy(): Configuration { | ||
return FocalMeterConfiguration(kantarEndpoint = kantarEndpoint) | ||
} | ||
|
||
} |
28 changes: 28 additions & 0 deletions
28
snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/entity/ClientSessionEntity.kt
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,28 @@ | ||
/* | ||
* Copyright (c) 2015-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. | ||
*/ | ||
|
||
package com.snowplowanalytics.snowplow.entity | ||
|
||
import com.snowplowanalytics.core.constants.Parameters | ||
import com.snowplowanalytics.core.constants.TrackerConstants | ||
import com.snowplowanalytics.snowplow.payload.SelfDescribingJson | ||
|
||
/** | ||
* Used to represent session information. | ||
*/ | ||
class ClientSessionEntity(private val values: Map<String, Any?>) : | ||
SelfDescribingJson(TrackerConstants.SESSION_SCHEMA, values) { | ||
|
||
val userId: String? | ||
get() = values[Parameters.SESSION_USER_ID] as String? | ||
} |