diff --git a/CHANGELOG b/CHANGELOG index 30aad5a28..dbd5d4e9a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Version 5.2.0 (2023-06-02) +-------------------------- +Track install referrer details entity along with the application install event if available (#249) +Add a filter API to plugins to decide whether to track an event or not (#608) +Add version to default remote configuration and don't update unless remote configuration is newer (#603) + Version 5.1.0 (2023-05-11) -------------------------- Track new properties in platform context version 1-0-3 and make it configurable which properties to track (#598) diff --git a/VERSION b/VERSION index 831446cbd..91ff57278 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.1.0 +5.2.0 diff --git a/build.gradle b/build.gradle index e83597d6b..52de0308b 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { subprojects { group = 'com.snowplowanalytics' - version = '5.1.0' + version = '5.2.0' repositories { google() maven { diff --git a/gradle.properties b/gradle.properties index 20b3f3daa..d63651f6b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ systemProp.org.gradle.internal.http.socketTimeout=120000 SONATYPE_STAGING_PROFILE=comsnowplowanalytics GROUP=com.snowplowanalytics POM_ARTIFACT_ID=snowplow-android-tracker -VERSION_NAME=5.1.0 +VERSION_NAME=5.2.0 POM_NAME=snowplow-android-tracker POM_PACKAGING=aar diff --git a/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java b/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java index 30daa7cdc..71aa47e99 100644 --- a/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java +++ b/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java @@ -261,6 +261,8 @@ private boolean setupWithRemoteConfig(@NonNull Consumer callbackTracker updateLogger("Configuration retrieved from cache"); case FETCHED: updateLogger("Configuration fetched from remote endpoint"); + case DEFAULT: + updateLogger("Default configuration used"); } Snowplow.getDefaultTracker().getEmitter().setRequestCallback(getRequestCallback()); callbackTrackerReady.accept(true); diff --git a/snowplow-demo-kotlin/build.gradle b/snowplow-demo-kotlin/build.gradle index 8ce33d7cb..b41083dcc 100644 --- a/snowplow-demo-kotlin/build.gradle +++ b/snowplow-demo-kotlin/build.gradle @@ -41,4 +41,5 @@ dependencies { implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.browser:browser:1.5.0' implementation 'com.google.android.gms:play-services-appset:16.0.2' + implementation "com.android.installreferrer:installreferrer:2.2" } diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt index d3a34e4a4..875e809b8 100644 --- a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt +++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt @@ -216,6 +216,7 @@ class Demo : Activity(), LoggerDelegate { updateLogger("Configuration fetched from remote endpoint") } ConfigurationState.FETCHED -> updateLogger("Configuration fetched from remote endpoint") + ConfigurationState.DEFAULT -> updateLogger("Default configuration used") else -> updateLogger("Configuration was not found") } defaultTracker!!.emitter.requestCallback = requestCallback diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt new file mode 100644 index 000000000..1f76e686f --- /dev/null +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt @@ -0,0 +1,102 @@ +/* + * 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.event + +import android.content.Context +import androidx.preference.PreferenceManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.snowplowanalytics.snowplow.Snowplow.createTracker +import com.snowplowanalytics.snowplow.configuration.* +import com.snowplowanalytics.snowplow.controller.TrackerController +import com.snowplowanalytics.snowplow.network.HttpMethod +import com.snowplowanalytics.snowplow.tracker.MockNetworkConnection +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ApplicationInstallEventTest { + + @Before + fun setUp() { + cleanSharedPreferences() + } + + // Tests + @Test + fun testTracksInstallEventOnFirstLaunch() { + // plugin to check if event was tracked + var eventTracked = false + val plugin = PluginConfiguration("testPlugin") + plugin.afterTrack { eventTracked = true } + + // create tracker with install autotracking + val trackerConfiguration = TrackerConfiguration("appId") + .installAutotracking(true) + createTracker(listOf(trackerConfiguration, plugin)) + + Thread.sleep(500) + + // check if event was tracked + Assert.assertTrue(eventTracked) + } + + @Test + fun testDoesntTrackInstallEventIfPreviouslyTracked() { + // plugin to check if event was tracked + var eventTracked = false + val plugin = PluginConfiguration("testPlugin") + plugin.afterTrack { eventTracked = true } + + // create tracker with install autotracking + val trackerConfiguration = TrackerConfiguration("appId") + .installAutotracking(true) + createTracker(listOf(trackerConfiguration, plugin)) + + Thread.sleep(500) + + // check if event was tracked + Assert.assertTrue(eventTracked) + + // reset flag + eventTracked = false + + // create tracker again + createTracker(listOf(trackerConfiguration, plugin)) + + Thread.sleep(500) + + // check if event was tracked + Assert.assertFalse(eventTracked) + } + + private fun cleanSharedPreferences() { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + sharedPreferences.edit().clear().commit() + } + + private fun createTracker(configurations: List): TrackerController { + val networkConfig = NetworkConfiguration(MockNetworkConnection(HttpMethod.POST, 200)) + return createTracker( + context, + namespace = "ns" + Math.random().toString(), + network = networkConfig, + configurations = configurations.toTypedArray() + ) + } + + private val context: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext +} diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallTest.kt deleted file mode 100644 index bbbc6b240..000000000 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.event - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.snowplowanalytics.core.constants.TrackerConstants -import com.snowplowanalytics.core.emitter.Executor.shutdown -import com.snowplowanalytics.snowplow.Snowplow.createTracker -import com.snowplowanalytics.snowplow.configuration.EmitterConfiguration -import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration -import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration -import com.snowplowanalytics.snowplow.network.HttpMethod -import com.snowplowanalytics.snowplow.payload.SelfDescribingJson -import com.snowplowanalytics.snowplow.tracker.MockEventStore -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import java.util.concurrent.TimeUnit - -@RunWith(AndroidJUnit4::class) -class ApplicationInstallTest { - @Before - @Throws(Exception::class) - fun setUp() { - val es = shutdown() - es?.awaitTermination(60, TimeUnit.SECONDS) - } - - // Tests - @Test - @Throws(InterruptedException::class) - fun testApplicationInstall() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - - // Prepare application install event - val installEvent = SelfDescribingJson(TrackerConstants.SCHEMA_APPLICATION_INSTALL) - val event = SelfDescribing(installEvent) - val currentTimestamp = 12345L - event.trueTimestamp = currentTimestamp - - // Setup tracker - val trackerConfiguration = TrackerConfiguration("appId") - .base64encoding(false) - .installAutotracking(false) - val eventStore = MockEventStore() - val networkConfiguration = NetworkConfiguration("fake-url", HttpMethod.POST) - val emitterConfiguration = EmitterConfiguration() - .eventStore(eventStore) - .threadPoolSize(10) - val trackerController = createTracker( - context, - "namespace", - networkConfiguration, - trackerConfiguration, - emitterConfiguration - ) - - // Track event - trackerController.track(event) - var i = 0 - while (eventStore.size() < 1 && i < 10) { - Thread.sleep(1000) - i++ - } - val events = eventStore.getEmittableEvents(10) - eventStore.removeAllEvents() - Assert.assertEquals(1, events.size.toLong()) - val payload = events[0]!!.payload - - // Check timestamp field - val deviceTimestamp = payload.map["dtm"] as String? - val expected = currentTimestamp.toString() - Assert.assertEquals(expected, deviceTimestamp) - } -} diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt index 025c0159d..431e9b3a2 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt @@ -14,7 +14,6 @@ package com.snowplowanalytics.snowplow.internal.remoteconfiguration import android.annotation.SuppressLint import androidx.core.util.Pair -import androidx.test.espresso.core.internal.deps.guava.collect.Lists import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.snowplowanalytics.core.remoteconfiguration.ConfigurationCache @@ -93,24 +92,23 @@ class RemoteConfigurationTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val body = "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0\",\"configurationVersion\":12,\"configurationBundle\":[]}" - val mockWebServer = getMockServer(200, body) - val endpoint = getMockServerURI(mockWebServer) - - val expectation = Any() as Object - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - ConfigurationFetcher( - context, - remoteConfig - ) { fetchedConfigurationBundle: FetchedConfigurationBundle -> - Assert.assertNotNull(fetchedConfigurationBundle) - Assert.assertEquals( - "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0", - fetchedConfigurationBundle.schema - ) - synchronized(expectation) { expectation.notify() } + withMockServer(200, body) { _, endpoint -> + + val expectation = Any() as Object + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + ConfigurationFetcher( + context, + remoteConfig + ) { fetchedConfigurationBundle: FetchedConfigurationBundle -> + Assert.assertNotNull(fetchedConfigurationBundle) + Assert.assertEquals( + "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0", + fetchedConfigurationBundle.schema + ) + synchronized(expectation) { expectation.notify() } + } + synchronized(expectation) { expectation.wait(10000) } } - synchronized(expectation) { expectation.wait(10000) } - mockWebServer.shutdown() } @Test @@ -151,26 +149,25 @@ class RemoteConfigurationTest { fun testConfigurationFetcher_downloads() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer( + withMockServer( 200, "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/2-0-0\",\"configurationVersion\":12,\"configurationBundle\":[]}" - ) - val endpoint = getMockServerURI(mockWebServer) - - // test - val expectation = Any() as Object - val expectationNotified = AtomicBoolean(false) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - ConfigurationFetcher( - context, - remoteConfig - ) { - expectationNotified.set(true) - synchronized(expectation) { expectation.notify() } + ) { _, endpoint -> + + // test + val expectation = Any() as Object + val expectationNotified = AtomicBoolean(false) + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + ConfigurationFetcher( + context, + remoteConfig + ) { + expectationNotified.set(true) + synchronized(expectation) { expectation.notify() } + } + synchronized(expectation) { expectation.wait(1000) } + Assert.assertTrue(expectationNotified.get()) } - synchronized(expectation) { expectation.wait(5000) } - Assert.assertTrue(expectationNotified.get()) - mockWebServer.shutdown() } @Test @@ -178,21 +175,16 @@ class RemoteConfigurationTest { fun testConfigurationProvider_notDownloading_fails() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer(500, "{}") - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) + withMockServer(500, "{}") { _, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) - // test - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - provider.retrieveConfiguration( - context, - false - ) { Assert.fail() } - synchronized(expectation) { expectation.wait(5000) } - mockWebServer.shutdown() + // test + val provider = ConfigurationProvider(remoteConfig) + provider.retrieveConfiguration(context, false) { Assert.fail() } + Thread.sleep(1000) + } } @Test @@ -200,24 +192,19 @@ class RemoteConfigurationTest { fun testConfigurationProvider_downloadOfWrongSchema_fails() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer( + withMockServer( 200, "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0\",\"configurationVersion\":12,\"configurationBundle\":[]}" - ) - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) + ) { _, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) - // test - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - provider.retrieveConfiguration( - context, - false - ) { Assert.fail() } - synchronized(expectation) { expectation.wait(5000) } - mockWebServer.shutdown() + // test + val provider = ConfigurationProvider(remoteConfig) + provider.retrieveConfiguration(context, false) { Assert.fail() } + Thread.sleep(1000) + } } @Test @@ -225,42 +212,40 @@ class RemoteConfigurationTest { fun testConfigurationProvider_downloadSameConfigVersionThanCached_dontUpdate() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer( + withMockServer( 200, "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}" - ) - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) - val bundle = ConfigurationBundle("namespace") - bundle.networkConfiguration = NetworkConfiguration("endpoint") - val cached = - FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") - cached.configurationVersion = 1 - cached.configurationBundle = listOf(bundle) - cache.writeCache(context, cached) + ) { _, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) + val bundle = ConfigurationBundle("namespace") + bundle.networkConfiguration = NetworkConfiguration("endpoint") + val cached = + FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") + cached.configurationVersion = 1 + cached.configurationBundle = listOf(bundle) + cache.writeCache(context, cached) - // test - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - val i = intArrayOf(0) // Needed to make it accessible inside the closure. - provider.retrieveConfiguration( - context, - false - ) { pair: Pair -> - val fetchedConfigurationBundle = pair.first - Assert.assertEquals(ConfigurationState.CACHED, pair.second) - if (i[0] == 1 || fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { - Assert.fail() - } - if (i[0] == 0 && fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") { - i[0]++ + // test + val provider = ConfigurationProvider(remoteConfig) + var numCalls = 0 + provider.retrieveConfiguration( + context, + false + ) { pair: Pair -> + val fetchedConfigurationBundle = pair.first + Assert.assertEquals(ConfigurationState.CACHED, pair.second) + if (numCalls == 1 || fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { + Assert.fail() + } + if (numCalls == 0 && fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") { + numCalls++ + } } + Thread.sleep(1000) + Assert.assertEquals(1, numCalls) } - synchronized(expectation) { expectation.wait(5000) } - Assert.assertEquals(1, i[0]) - mockWebServer.shutdown() } @Test @@ -268,45 +253,43 @@ class RemoteConfigurationTest { fun testConfigurationProvider_downloadHigherConfigVersionThanCached_doUpdate() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer( + withMockServer( 200, "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":2,\"configurationBundle\":[]}" - ) - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) - val bundle = ConfigurationBundle("namespace") - bundle.networkConfiguration = NetworkConfiguration("endpoint") - val cached = - FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") - cached.configurationVersion = 1 - cached.configurationBundle = listOf(bundle) - cache.writeCache(context, cached) + ) { _, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) + val bundle = ConfigurationBundle("namespace") + bundle.networkConfiguration = NetworkConfiguration("endpoint") + val cached = + FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") + cached.configurationVersion = 1 + cached.configurationBundle = listOf(bundle) + cache.writeCache(context, cached) - // test - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - val i = intArrayOf(0) // Needed to make it accessible inside the closure. - provider.retrieveConfiguration( - context, - false - ) { pair: Pair -> - val fetchedConfigurationBundle = pair.first - Assert.assertEquals( - if (i[0] == 0) ConfigurationState.CACHED else ConfigurationState.FETCHED, - pair.second - ) - if (i[0] == 1 || fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { - i[0]++ - } - if (i[0] == 0 && fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") { - i[0]++ + // test + val provider = ConfigurationProvider(remoteConfig) + var numCalls = 0 + provider.retrieveConfiguration( + context, + false + ) { pair: Pair -> + val fetchedConfigurationBundle = pair.first + Assert.assertEquals( + if (numCalls == 0) ConfigurationState.CACHED else ConfigurationState.FETCHED, + pair.second + ) + if (numCalls == 1 || fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { + numCalls++ + } + if (numCalls == 0 && fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") { + numCalls++ + } } + Thread.sleep(1000) + Assert.assertEquals(2, numCalls) } - synchronized(expectation) { expectation.wait(10000) } - Assert.assertEquals(2, i[0]) - mockWebServer.shutdown() } @Test @@ -314,43 +297,37 @@ class RemoteConfigurationTest { fun testConfigurationProvider_justRefresh_downloadSameConfigVersionThanCached_dontUpdate() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer(404, "{}") - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) - val bundle = ConfigurationBundle("namespace") - bundle.networkConfiguration = NetworkConfiguration("endpoint") - val cached = - FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") - cached.configurationVersion = 1 - cached.configurationBundle = listOf(bundle) - cache.writeCache(context, cached) - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - val i = intArrayOf(0) // Needed to make it accessible inside the closure. - provider.retrieveConfiguration( - context, - false - ) { pair: Pair -> - Assert.assertEquals(ConfigurationState.CACHED, pair.second) - synchronized(expectation) { expectation.notify() } + withMockServer(404, "{}") { mockWebServer, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) + val bundle = ConfigurationBundle("namespace") + bundle.networkConfiguration = NetworkConfiguration("endpoint") + val cached = + FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") + cached.configurationVersion = 1 + cached.configurationBundle = listOf(bundle) + cache.writeCache(context, cached) + val expectation = Any() as Object + val provider = ConfigurationProvider(remoteConfig) + provider.retrieveConfiguration( + context, + false + ) { pair: Pair -> + Assert.assertEquals(ConfigurationState.CACHED, pair.second) + synchronized(expectation) { expectation.notify() } + } + synchronized(expectation) { expectation.wait(1000) } + val mockResponse = MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody("{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}") + mockWebServer.enqueue(mockResponse) + + // test + provider.retrieveConfiguration(context, true) { Assert.fail() } + Thread.sleep(1000) } - synchronized(expectation) { expectation.wait(5000) } - val mockResponse = MockResponse() - .setResponseCode(200) - .setHeader("Content-Type", "application/json") - .setBody("{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}") - mockWebServer.enqueue(mockResponse) - - // test - val expectation2 = Any() as Object - provider.retrieveConfiguration( - context, - true - ) { Assert.fail() } - synchronized(expectation2) { expectation2.wait(5000) } - mockWebServer.shutdown() } @Test @@ -358,51 +335,49 @@ class RemoteConfigurationTest { fun testConfigurationProvider_justRefresh_downloadHigherConfigVersionThanCached_doUpdate() { // prepare test val context = InstrumentationRegistry.getInstrumentation().targetContext - val mockWebServer = getMockServer(404, "{}") - val endpoint = getMockServerURI(mockWebServer) - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val cache = ConfigurationCache(remoteConfig) - cache.clearCache(context) - val bundle = ConfigurationBundle("namespace") - bundle.networkConfiguration = NetworkConfiguration("endpoint") - val cached = - FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") - cached.configurationVersion = 1 - cached.configurationBundle = listOf(bundle) - cache.writeCache(context, cached) - val expectation = Any() as Object - val provider = ConfigurationProvider(remoteConfig) - val i = intArrayOf(0) // Needed to make it accessible inside the closure. - provider.retrieveConfiguration( - context, - false - ) { - synchronized(expectation) { expectation.notify() } - } - synchronized(expectation) { expectation.wait(5000) } - val mockResponse = MockResponse() - .setResponseCode(200) - .setHeader("Content-Type", "application/json") - .setBody("{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":2,\"configurationBundle\":[]}") - mockWebServer.enqueue(mockResponse) - - // test - val expectation2 = Any() as Object - val j = intArrayOf(0) // Needed to make it accessible inside the closure. - provider.retrieveConfiguration( - context, - true - ) { pair: Pair -> - val fetchedConfigurationBundle = pair.first - if (fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { - j[0]++ - Assert.assertEquals(ConfigurationState.FETCHED, pair.second) - synchronized(expectation2) { expectation2.notify() } + withMockServer(404, "{}") { mockWebServer, endpoint -> + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val cache = ConfigurationCache(remoteConfig) + cache.clearCache(context) + val bundle = ConfigurationBundle("namespace") + bundle.networkConfiguration = NetworkConfiguration("endpoint") + val cached = + FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0") + cached.configurationVersion = 1 + cached.configurationBundle = listOf(bundle) + cache.writeCache(context, cached) + val expectation = Any() as Object + val provider = ConfigurationProvider(remoteConfig) + provider.retrieveConfiguration( + context, + false + ) { + synchronized(expectation) { expectation.notify() } } + synchronized(expectation) { expectation.wait(1000) } + val mockResponse = MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody("{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":2,\"configurationBundle\":[]}") + mockWebServer.enqueue(mockResponse) + + // test + val expectation2 = Any() as Object + var numCallbackCalls = 0 + provider.retrieveConfiguration( + context, + true + ) { pair: Pair -> + val fetchedConfigurationBundle = pair.first + if (fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") { + numCallbackCalls++ + Assert.assertEquals(ConfigurationState.FETCHED, pair.second) + synchronized(expectation2) { expectation2.notify() } + } + } + synchronized(expectation2) { expectation2.wait(1000) } + Assert.assertEquals(1, numCallbackCalls) } - synchronized(expectation2) { expectation2.wait(5000) } - Assert.assertEquals(1, j[0]) - mockWebServer.shutdown() } @Test @@ -424,38 +399,108 @@ class RemoteConfigurationTest { cache.writeCache(context, cached) // stub request for configuration (return version 1) - val mockWebServer = getMockServer( + withMockServer( 200, "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}" - ) - val endpoint = getMockServerURI(mockWebServer) - - // retrieve remote configuration - val remoteConfig = RemoteConfiguration(endpoint!!, HttpMethod.GET) - val provider = ConfigurationProvider(remoteConfig) - val numCallbackCalls = intArrayOf(0) - val expectation = Any() as Object - provider.retrieveConfiguration( - context, - true - ) { pair: Pair -> - val fetchedConfigurationBundle = pair.first - numCallbackCalls[0]++ - // should be the non-cache configuration (version 1) - Assert.assertEquals( - "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0", - fetchedConfigurationBundle.schema + ) { _, endpoint -> + + // retrieve remote configuration + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val provider = ConfigurationProvider(remoteConfig) + var numCallbackCalls = 0 + provider.retrieveConfiguration( + context, + true + ) { pair: Pair -> + val fetchedConfigurationBundle = pair.first + numCallbackCalls++ + // should be the non-cache configuration (version 1) + Assert.assertEquals( + "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0", + fetchedConfigurationBundle.schema + ) + Assert.assertEquals(1, fetchedConfigurationBundle.configurationVersion.toLong()) + } + Thread.sleep(1000) + Assert.assertEquals(1, numCallbackCalls) + } + } + + @Test + fun testUsesDefaultConfigurationIfTheSameConfigurationVersionAsFetched() { + // prepare test + val context = InstrumentationRegistry.getInstrumentation().targetContext + val cachedRemoteConfig = RemoteConfiguration("http://cache.example.com", HttpMethod.GET) + ConfigurationCache(cachedRemoteConfig).clearCache(context) + + // stub request for configuration (return version 1) + withMockServer( + 200, + "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}" + ) { _, endpoint -> + + // retrieve remote configuration + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val provider = ConfigurationProvider( + remoteConfiguration = remoteConfig, + defaultBundles = listOf( + ConfigurationBundle("namespace", NetworkConfiguration("http://localhost")) + ), + defaultBundleVersion = 1 + ) + var numCallbackCalls = 0 + provider.retrieveConfiguration( + context, + false + ) { pair: Pair -> + numCallbackCalls++ + Assert.assertEquals(ConfigurationState.DEFAULT, pair.second) + } + Thread.sleep(1000) + Assert.assertEquals(1, numCallbackCalls) + } + } + + @Test + fun testReplacesDefaultConfigurationIfFetchedHasNewerVersion() { + // prepare test + val context = InstrumentationRegistry.getInstrumentation().targetContext + val cachedRemoteConfig = RemoteConfiguration("http://cache.example.com", HttpMethod.GET) + ConfigurationCache(cachedRemoteConfig).clearCache(context) + + // stub request for configuration (return version 2) + withMockServer( + 200, + "{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":2,\"configurationBundle\":[]}" + ) { _, endpoint -> + + // retrieve remote configuration + val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET) + val provider = ConfigurationProvider( + remoteConfiguration = remoteConfig, + defaultBundles = listOf( + ConfigurationBundle("namespace", NetworkConfiguration("http://localhost")) + ), + defaultBundleVersion = 1 ) - Assert.assertEquals(1, fetchedConfigurationBundle.configurationVersion.toLong()) + var numCallbackCalls = 0 + var lastConfigurationState: ConfigurationState? = null + provider.retrieveConfiguration( + context, + false + ) { pair: Pair -> + numCallbackCalls++ + lastConfigurationState = pair.second + } + Thread.sleep(1000) + Assert.assertEquals(2, numCallbackCalls) + Assert.assertEquals(ConfigurationState.FETCHED, lastConfigurationState) } - synchronized(expectation) { expectation.wait(5000) } - Assert.assertEquals(1, numCallbackCalls[0]) - mockWebServer.shutdown() } // Private methods @Throws(IOException::class) - fun getMockServer(responseCode: Int, body: String?): MockWebServer { + private fun withMockServer(responseCode: Int, body: String?, callback: (MockWebServer, String) -> Unit) { val mockServer = MockWebServer() mockServer.start() val mockResponse = MockResponse() @@ -463,13 +508,12 @@ class RemoteConfigurationTest { .setHeader("Content-Type", "application/json") .setBody(body!!) mockServer.enqueue(mockResponse) - return mockServer + callback(mockServer, getMockServerURI(mockServer)) + mockServer.shutdown() } @SuppressLint("DefaultLocale") - fun getMockServerURI(mockServer: MockWebServer?): String? { - return if (mockServer != null) { - String.format("http://%s:%d", mockServer.hostName, mockServer.port) - } else null + private fun getMockServerURI(mockServer: MockWebServer): String { + return String.format("http://%s:%d", mockServer.hostName, mockServer.port) } } diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt index 454dd7a8e..c159d0459 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt @@ -437,6 +437,15 @@ class StateManagerTest { Assert.assertEquals(1, stateMachine.afterTrackEvents.size) Assert.assertEquals("cat", stateMachine.afterTrackEvents.first().payload["se_ca"]) } + + @Test + fun testFilterReturnsSettingOfStateMachine() { + val stateManager = StateManager() + stateManager.addOrReplaceStateMachine(MockStateMachine()) + + Assert.assertFalse(stateManager.filter(TrackerEvent(SelfDescribing("s1", emptyMap()), TrackerState()))) + Assert.assertTrue(stateManager.filter(TrackerEvent(SelfDescribing("s2", emptyMap()), TrackerState()))) + } } // Mock classes internal class MockState(var value: Int) : State @@ -457,6 +466,9 @@ internal open class MockStateMachine( override val subscribedEventSchemasForAfterTrackCallback: List get() = Collections.singletonList("*") + override val subscribedEventSchemasForFiltering: List + get() = Collections.singletonList("s1") + override fun transition(event: Event, state: State?): State? { val e = event as SelfDescribing var currentState = state as MockState? @@ -497,4 +509,8 @@ internal open class MockStateMachine( override fun afterTrack(event: InspectableEvent) { afterTrackEvents.add(event) } + + override fun filter(event: InspectableEvent, state: State?): Boolean? { + return false + } } diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/PluginsTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/PluginsTest.kt index 5600471a0..6b1ffad6b 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/PluginsTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/PluginsTest.kt @@ -189,6 +189,28 @@ class PluginsTest { Assert.assertFalse(pluginCalled) } + @Test + fun filtersEvents() { + val filterPlugin = PluginConfiguration("filter") + .filter(listOf("s1")) { false } + + var afterTrackCalled = false + val afterTrackPlugin = PluginConfiguration("afterTrack") + .afterTrack { afterTrackCalled = true } + + val tracker = createTracker(listOf(filterPlugin, afterTrackPlugin)) + + tracker.track(SelfDescribing("s1", emptyMap())) + Thread.sleep(100) + + Assert.assertFalse(afterTrackCalled) + + tracker.track(SelfDescribing("s2", emptyMap())) + Thread.sleep(100) + + Assert.assertTrue(afterTrackCalled) + } + // --- PRIVATE private val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/globalcontexts/GlobalContextPluginConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/globalcontexts/GlobalContextPluginConfiguration.kt index c0d86a030..c2ad47a7d 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/globalcontexts/GlobalContextPluginConfiguration.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/globalcontexts/GlobalContextPluginConfiguration.kt @@ -12,17 +12,13 @@ */ package com.snowplowanalytics.core.globalcontexts -import com.snowplowanalytics.snowplow.configuration.PluginAfterTrackConfiguration -import com.snowplowanalytics.snowplow.configuration.PluginConfigurationInterface -import com.snowplowanalytics.snowplow.configuration.PluginEntitiesConfiguration +import com.snowplowanalytics.snowplow.configuration.* import com.snowplowanalytics.snowplow.globalcontexts.GlobalContext class GlobalContextPluginConfiguration( override val identifier: String, val globalContext: GlobalContext -) : PluginConfigurationInterface { - - override val afterTrackConfiguration: PluginAfterTrackConfiguration? = null +) : PluginIdentifiable, PluginEntitiesCallable { override val entitiesConfiguration: PluginEntitiesConfiguration get() = PluginEntitiesConfiguration(closure = globalContext::generateContexts) diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt index 76846bfc5..fb0902b01 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt @@ -26,7 +26,8 @@ import com.snowplowanalytics.snowplow.configuration.RemoteConfiguration */ class ConfigurationProvider @JvmOverloads constructor( private val remoteConfiguration: RemoteConfiguration, - defaultBundles: List? = null + defaultBundles: List? = null, + defaultBundleVersion: Int = Int.MIN_VALUE ) { private val cache: ConfigurationCache = ConfigurationCache(remoteConfiguration) private var fetcher: ConfigurationFetcher? = null @@ -36,7 +37,7 @@ class ConfigurationProvider @JvmOverloads constructor( init { if (defaultBundles != null) { val bundle = FetchedConfigurationBundle("1.0") - bundle.configurationVersion = Int.MIN_VALUE + bundle.configurationVersion = defaultBundleVersion bundle.configurationBundle = defaultBundles defaultBundle = bundle } @@ -67,17 +68,17 @@ class ConfigurationProvider @JvmOverloads constructor( return } synchronized(this) { - if (cacheBundle != null && cacheBundle!!.configurationVersion >= fetchedConfigurationBundle.configurationVersion) { - return - } - cache.writeCache(context, fetchedConfigurationBundle) - cacheBundle = fetchedConfigurationBundle - onFetchCallback.accept( - Pair( - fetchedConfigurationBundle, - ConfigurationState.FETCHED + val isNewer = (cacheBundle ?: defaultBundle)?.let { it.configurationVersion < fetchedConfigurationBundle.configurationVersion } ?: true + if (isNewer) { + cache.writeCache(context, fetchedConfigurationBundle) + cacheBundle = fetchedConfigurationBundle + onFetchCallback.accept( + Pair( + fetchedConfigurationBundle, + ConfigurationState.FETCHED + ) ) - ) + } } } }) diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/DeepLinkStateMachine.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/DeepLinkStateMachine.kt index f4c5d813a..cde4ce299 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/DeepLinkStateMachine.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/DeepLinkStateMachine.kt @@ -48,6 +48,9 @@ class DeepLinkStateMachine : StateMachineInterface { override val subscribedEventSchemasForAfterTrackCallback: List get() = emptyList() + override val subscribedEventSchemasForFiltering: List + get() = emptyList() + override fun transition(event: Event, state: State?): State? { // - Init (DL) DeepLinkReceived // - ReadyForOutput (DL) DeepLinkReceived @@ -88,6 +91,10 @@ class DeepLinkStateMachine : StateMachineInterface { override fun afterTrack(event: InspectableEvent) { } + override fun filter(event: InspectableEvent, state: State?): Boolean? { + return null + } + companion object { val ID: String get() = "DeepLinkContext" diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/LifecycleStateMachine.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/LifecycleStateMachine.kt index 58a99d5ff..404c4d570 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/LifecycleStateMachine.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/LifecycleStateMachine.kt @@ -45,6 +45,9 @@ class LifecycleStateMachine : StateMachineInterface { override val subscribedEventSchemasForAfterTrackCallback: List get() = emptyList() + override val subscribedEventSchemasForFiltering: List + get() = emptyList() + override fun transition(event: Event, currentState: State?): State? { if (event is Foreground) { return LifecycleState(true, event.foregroundIndex) @@ -69,6 +72,10 @@ class LifecycleStateMachine : StateMachineInterface { override fun afterTrack(event: InspectableEvent) { } + override fun filter(event: InspectableEvent, state: State?): Boolean? { + return null + } + companion object { val ID: String get() = "Lifecycle" diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/PluginStateMachine.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/PluginStateMachine.kt index ef92c2828..ce3de029e 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/PluginStateMachine.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/PluginStateMachine.kt @@ -14,6 +14,7 @@ package com.snowplowanalytics.core.statemachine import com.snowplowanalytics.snowplow.configuration.PluginAfterTrackConfiguration import com.snowplowanalytics.snowplow.configuration.PluginEntitiesConfiguration +import com.snowplowanalytics.snowplow.configuration.PluginFilterConfiguration import com.snowplowanalytics.snowplow.event.Event import com.snowplowanalytics.snowplow.payload.SelfDescribingJson import com.snowplowanalytics.snowplow.tracker.InspectableEvent @@ -22,7 +23,8 @@ import java.util.* class PluginStateMachine( override val identifier: String, val entitiesConfiguration: PluginEntitiesConfiguration?, - val afterTrackConfiguration: PluginAfterTrackConfiguration? + val afterTrackConfiguration: PluginAfterTrackConfiguration?, + val filterConfiguration: PluginFilterConfiguration? ) : StateMachineInterface { override val subscribedEventSchemasForTransitions: List @@ -43,6 +45,12 @@ class PluginStateMachine( return config.schemas ?: Collections.singletonList("*") } + override val subscribedEventSchemasForFiltering: List + get() { + val config = filterConfiguration ?: return emptyList() + return config.schemas ?: Collections.singletonList("*") + } + override fun transition(event: Event, state: State?): State? { return null } @@ -58,4 +66,8 @@ class PluginStateMachine( override fun afterTrack(event: InspectableEvent) { afterTrackConfiguration?.closure?.accept(event) } + + override fun filter(event: InspectableEvent, state: State?): Boolean? { + return filterConfiguration?.closure?.apply(event) + } } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateMachineInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateMachineInterface.kt index ad450b049..f6f718f07 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateMachineInterface.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateMachineInterface.kt @@ -22,8 +22,11 @@ interface StateMachineInterface { val subscribedEventSchemasForEntitiesGeneration: List val subscribedEventSchemasForPayloadUpdating: List val subscribedEventSchemasForAfterTrackCallback: List + val subscribedEventSchemasForFiltering: List + fun transition(event: Event, state: State?): State? fun entities(event: InspectableEvent, state: State?): List? fun payloadValues(event: InspectableEvent, state: State?): Map? fun afterTrack(event: InspectableEvent) + fun filter(event: InspectableEvent, state: State?): Boolean? } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateManager.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateManager.kt index 8c86929c7..5cf4e76fb 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateManager.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/statemachine/StateManager.kt @@ -28,6 +28,7 @@ class StateManager { HashMap>() private val eventSchemaToPayloadUpdater = HashMap>() private val eventSchemaToAfterTrackCallback = HashMap>() + private val eventSchemaToFilter = HashMap>() val trackerState = TrackerState() @@ -63,6 +64,11 @@ class StateManager { stateMachine.subscribedEventSchemasForAfterTrackCallback, stateMachine ) + addToSchemaRegistry( + eventSchemaToFilter, + stateMachine.subscribedEventSchemasForFiltering, + stateMachine + ) } @Synchronized @@ -91,6 +97,11 @@ class StateManager { stateMachine.subscribedEventSchemasForAfterTrackCallback, stateMachine ) + removeFromSchemaRegistry( + eventSchemaToFilter, + stateMachine.subscribedEventSchemasForFiltering, + stateMachine + ) return true } @@ -134,11 +145,9 @@ class StateManager { eventSchemaToEntitiesGenerator["*"]?.let { stateMachines.addAll(it) } for (stateMachine in stateMachines) { - val stateIdentifier = stateMachineToIdentifier[stateMachine] - if (stateIdentifier != null) { + stateMachineToIdentifier[stateMachine]?.let { stateIdentifier -> val state = event.state.getState(stateIdentifier) - val entities = stateMachine.entities(event, state) - if (entities != null) { + stateMachine.entities(event, state)?.let { entities -> result.addAll(entities) } } @@ -154,12 +163,12 @@ class StateManager { eventSchemaToPayloadUpdater["*"]?.let { stateMachines.addAll(it) } for (stateMachine in stateMachines) { - val stateIdentifier = stateMachineToIdentifier[stateMachine] - if (stateIdentifier != null) { + stateMachineToIdentifier[stateMachine]?.let { stateIdentifier -> val state = event.state.getState(stateIdentifier) - val payloadValues = stateMachine.payloadValues(event, state) - if (payloadValues != null && !event.addPayloadValues(payloadValues)) { - failures++ + stateMachine.payloadValues(event, state)?.let { payloadValues -> + if (!event.addPayloadValues(payloadValues)) { + failures++ + } } } } @@ -183,6 +192,25 @@ class StateManager { } } + @Synchronized + fun filter(event: StateMachineEvent): Boolean { + val schema = event.schema ?: event.name + + val stateMachines: MutableList = LinkedList() + eventSchemaToFilter[schema]?.let { stateMachines.addAll(it) } + eventSchemaToFilter["*"]?.let { stateMachines.addAll(it) } + + for (stateMachine in stateMachines) { + stateMachineToIdentifier[stateMachine]?.let { stateIdentifier -> + val state = event.state.getState(stateIdentifier) + if (stateMachine.filter(event, state) == false) { + return false + } + } + } + return true + } + // Private methods private fun addToSchemaRegistry( schemaRegistry: MutableMap>, diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ApplicationInstallEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ApplicationInstallEvent.kt new file mode 100644 index 000000000..c2a54807b --- /dev/null +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ApplicationInstallEvent.kt @@ -0,0 +1,81 @@ +/* + * 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.core.tracker + +import android.content.Context +import androidx.preference.PreferenceManager +import com.snowplowanalytics.core.constants.TrackerConstants +import com.snowplowanalytics.core.emitter.Executor +import com.snowplowanalytics.core.utils.NotificationCenter.postNotification +import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing +import java.util.* + +/** + * An event tracked on the first launch of the app in case install autotracking is enabled. + * It is accompanied by an install referrer entity (`InstallReferrerDetails`) if available. + */ +class ApplicationInstallEvent : AbstractSelfDescribing() { + + override val schema: String + get() = TrackerConstants.SCHEMA_APPLICATION_INSTALL + + override val dataPayload: Map + get() = emptyMap() + + companion object { + private val TAG = ApplicationInstallEvent::class.java.simpleName + + /** + * Asynchronous function that tracks an `application_install` event if it wasn't tracked yet. + * @param context the Android context + */ + fun trackIfFirstLaunch(context: Context) { + Executor.execute(TAG) { + if (isNewInstall(context)) { + sendInstallEvent(context) + } + } + } + + private fun isNewInstall(context: Context): Boolean { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + // if the value was missing in sharedPreferences, we're assuming this is a new install + return sharedPreferences.getString(TrackerConstants.INSTALLED_BEFORE, null) == null; + } + + private fun sendInstallEvent(context: Context) { + val event = ApplicationInstallEvent() + + // add install referrer entity if available + InstallReferrerDetails.fetch(context) { referrer -> + referrer?.let { event.entities.add(it) } + + val notificationData: MutableMap = HashMap() + notificationData["event"] = event + postNotification("SnowplowInstallTracking", notificationData) + + saveInstallTrackedInfo(context) + } + } + + /** + * Save install event tracked info to shared preferences + */ + private fun saveInstallTrackedInfo(context: Context) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val editor = sharedPreferences.edit() + editor?.putString(TrackerConstants.INSTALLED_BEFORE, "YES") + editor?.apply() + } + } +} diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallReferrerDetails.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallReferrerDetails.kt new file mode 100644 index 000000000..7ff985bfc --- /dev/null +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallReferrerDetails.kt @@ -0,0 +1,111 @@ +/* + * 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.core.tracker + +import android.content.Context +import android.os.RemoteException +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse +import com.android.installreferrer.api.ReferrerDetails +import com.snowplowanalytics.core.utils.Util +import com.snowplowanalytics.snowplow.payload.SelfDescribingJson + +/** + * Entity tracked along with the `application_install` event to give information about the Play Store referrer. + * Only tracked in case `com.android.installreferrer` package is added to app dependencies. + * + * Schema: iglu:com.android.installreferrer.api/referrer_details/jsonschema/1-0-0 + * + * @param installReferrer The referrer URL of the installed package + * @param referrerClickTimestamp The timestamp when referrer click happens + * @param installBeginTimestamp The timestamp when the app installation began + * @param googlePlayInstantParam Boolean indicating if the user has interacted with the app's instant experience in the past 7 days + */ +class InstallReferrerDetails( + installReferrer: String, + referrerClickTimestamp: Long, + installBeginTimestamp: Long, + googlePlayInstantParam: Boolean +) : SelfDescribingJson( + "iglu:com.android.installreferrer.api/referrer_details/jsonschema/1-0-0", + mapOf( + "installReferrer" to installReferrer, + "referrerClickTimestamp" to if (referrerClickTimestamp > 0) { Util.getDateTimeFromTimestamp(referrerClickTimestamp) } else { null }, + "installBeginTimestamp" to if (installBeginTimestamp > 0) { Util.getDateTimeFromTimestamp(installBeginTimestamp) } else { null }, + "googlePlayInstantParam" to googlePlayInstantParam, + ) +) { + companion object { + private val TAG = InstallReferrerDetails::class.java.simpleName + + fun fetch(context: Context, callback: (installReferrer: InstallReferrerDetails?) -> Unit) { + if (!isInstallReferrerPackageAvailable()) { + callback(null) + return + } + + val referrerClient = InstallReferrerClient.newBuilder(context).build() + referrerClient.startConnection(object : InstallReferrerStateListener { + + override fun onInstallReferrerSetupFinished(responseCode: Int) { + when (responseCode) { + InstallReferrerResponse.OK -> { + // Connection established. + try { + val response: ReferrerDetails = referrerClient.installReferrer + val referrer = InstallReferrerDetails( + installReferrer = response.installReferrer, + referrerClickTimestamp = response.referrerClickTimestampSeconds, + installBeginTimestamp = response.installBeginTimestampSeconds, + googlePlayInstantParam = response.googlePlayInstantParam + ) + callback(referrer) + } catch (_: RemoteException) { + Logger.d(TAG, "Install referrer API remote exception.") + callback(null) + } + } + InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> { + Logger.d( + TAG, + "Install referrer API not available on the current Play Store app." + ) + callback(null) + } + InstallReferrerResponse.SERVICE_UNAVAILABLE -> { + Logger.d( + TAG, + "Install referrer API connection couldn't be established." + ) + callback(null) + } + } + } + + override fun onInstallReferrerServiceDisconnected() { + } + }) + } + + fun isInstallReferrerPackageAvailable(): Boolean { + try { + Class.forName("com.android.installreferrer.api.InstallReferrerStateListener") + return true + } catch (_: Exception) { + } + return false + } + } +} diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallTracker.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallTracker.kt deleted file mode 100644 index 8e8a372c5..000000000 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/InstallTracker.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.core.tracker - -import android.content.Context -import android.content.SharedPreferences -import android.os.AsyncTask -import androidx.preference.PreferenceManager -import androidx.annotation.RestrictTo -import com.snowplowanalytics.core.constants.TrackerConstants -import com.snowplowanalytics.core.utils.NotificationCenter.postNotification -import com.snowplowanalytics.snowplow.event.SelfDescribing -import com.snowplowanalytics.snowplow.payload.SelfDescribingJson -import java.util.* - -/** - * Class used to keep track of install state of app. - * If a file does not exist, the tracker will send an `application_install` event. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY) -class InstallTracker private constructor(context: Context) { - private var isNewInstall: Boolean? = null - private var sharedPreferences: SharedPreferences? = null - - init { - SharedPreferencesTask().execute(context) - } - - private inner class SharedPreferencesTask : AsyncTask() { - - override fun doInBackground(vararg params: Context?): Boolean? { - sharedPreferences = params[0]?.let { PreferenceManager.getDefaultSharedPreferences(it) } - isNewInstall = if (sharedPreferences?.getString(TrackerConstants.INSTALLED_BEFORE, null) == null) { - // mark the install if there's no value - val editor = sharedPreferences?.edit() - editor?.putString(TrackerConstants.INSTALLED_BEFORE, "YES") - editor?.putLong(TrackerConstants.INSTALL_TIMESTAMP, Calendar.getInstance().timeInMillis) - editor?.apply() - // since the value was missing in sharedPreferences, we're assuming this is a new install - true - } else { - // if there's an INSTALLED_BEFORE record in sharedPreferences - someone has been there! - false - } - return isNewInstall - } - - override fun onPostExecute(isNewInstall: Boolean) { - val installTimestamp = sharedPreferences?.getLong(TrackerConstants.INSTALL_TIMESTAMP, 0) - // We send the installEvent if it's a new installed app but in case the tracker hasn't been able - // to send the event before we can retry checking if INSTALL_TIMESTAMP was already removed. - installTimestamp?.let { - if (!isNewInstall && installTimestamp <= 0) { - return - } - sendInstallEvent(it) - } - // clear install timestamp - val editor = sharedPreferences?.edit() - editor?.remove(TrackerConstants.INSTALL_TIMESTAMP) - editor?.commit() - } - } - - private fun sendInstallEvent(installTimestamp: Long) { - val event = SelfDescribing(SelfDescribingJson(TrackerConstants.SCHEMA_APPLICATION_INSTALL)) - if (installTimestamp > 0) { - event.trueTimestamp(installTimestamp) - } - val notificationData: MutableMap = HashMap() - notificationData["event"] = event - postNotification("SnowplowInstallTracking", notificationData) - } - - companion object { - private val TAG = InstallTracker::class.java.simpleName - private var sharedInstance: InstallTracker? = null - - @JvmStatic - @Synchronized - fun getInstance(context: Context): InstallTracker { - if (sharedInstance == null) { - sharedInstance = InstallTracker(context) - } - return sharedInstance!! - } - } -} diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PluginsControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PluginsControllerImpl.kt index 776058f1d..a312e1eca 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PluginsControllerImpl.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PluginsControllerImpl.kt @@ -13,7 +13,7 @@ package com.snowplowanalytics.core.tracker import com.snowplowanalytics.core.Controller -import com.snowplowanalytics.snowplow.configuration.PluginConfigurationInterface +import com.snowplowanalytics.snowplow.configuration.PluginIdentifiable import com.snowplowanalytics.snowplow.controller.PluginsController class PluginsControllerImpl @@ -24,7 +24,7 @@ class PluginsControllerImpl return serviceProvider.pluginConfigurations.map { it.identifier } } - override fun addPlugin(plugin: PluginConfigurationInterface) { + override fun addPlugin(plugin: PluginIdentifiable) { serviceProvider.addPlugin(plugin) } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ScreenStateMachine.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ScreenStateMachine.kt index 0cc7c07d8..2e3fa5436 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ScreenStateMachine.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ScreenStateMachine.kt @@ -47,6 +47,9 @@ class ScreenStateMachine : StateMachineInterface { override val subscribedEventSchemasForAfterTrackCallback: List get() = emptyList() + override val subscribedEventSchemasForFiltering: List + get() = emptyList() + override fun transition(event: Event, state: State?): State? { val screenView = event as? ScreenView val screenState: ScreenState? = if (state != null) { @@ -101,6 +104,10 @@ class ScreenStateMachine : StateMachineInterface { override fun afterTrack(event: InspectableEvent) { } + override fun filter(event: InspectableEvent, state: State?): Boolean? { + return null + } + companion object { val ID: String get() = "ScreenContext" diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt index 689d7b655..5f9295056 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt @@ -53,7 +53,7 @@ class ServiceProvider( } // Original configurations - override var pluginConfigurations: MutableList = ArrayList() + override var pluginConfigurations: MutableList = ArrayList() private set // Configuration updates @@ -129,7 +129,7 @@ class ServiceProvider( pluginConfigurations.add(plugin) } } - else if (configuration is PluginConfigurationInterface) { + else if (configuration is PluginIdentifiable) { pluginConfigurations.add(configuration) } } @@ -355,7 +355,7 @@ class ServiceProvider( // Plugins - override fun addPlugin(plugin: PluginConfigurationInterface) { + override fun addPlugin(plugin: PluginIdentifiable) { removePlugin(plugin.identifier) pluginConfigurations.add(plugin) tracker?.addOrReplaceStateMachine(plugin.toStateMachine()) diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt index 55b935a48..03ce4d945 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt @@ -18,7 +18,7 @@ import com.snowplowanalytics.core.gdpr.GdprControllerImpl import com.snowplowanalytics.core.globalcontexts.GlobalContextsControllerImpl import com.snowplowanalytics.core.session.SessionConfigurationUpdate import com.snowplowanalytics.core.session.SessionControllerImpl -import com.snowplowanalytics.snowplow.configuration.PluginConfigurationInterface +import com.snowplowanalytics.snowplow.configuration.PluginIdentifiable interface ServiceProviderInterface { val namespace: String @@ -48,7 +48,7 @@ interface ServiceProviderInterface { val gdprConfigurationUpdate: GdprConfigurationUpdate // Plugins - val pluginConfigurations: List - fun addPlugin(plugin: PluginConfigurationInterface) + val pluginConfigurations: List + fun addPlugin(plugin: PluginIdentifiable) fun removePlugin(identifier: String) } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt index 6c2901146..0f4ceea6f 100755 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt @@ -430,7 +430,7 @@ class Tracker( private fun initializeInstallTracking() { if (installAutotracking) { - InstallTracker.getInstance(context) + ApplicationInstallEvent.trackIfFirstLaunch(context) } } @@ -483,16 +483,20 @@ class Tracker( } val reportsOnDiagnostic = event !is TrackerError execute(reportsOnDiagnostic, TAG) { - val payload = payloadWithEvent(trackerEvent) - v(TAG, "Adding new payload to event storage: %s", payload) - emitter.add(payload) - event.endProcessing(this) - stateManager.afterTrack(trackerEvent) + payloadWithEvent(trackerEvent)?.let { payload -> + v(TAG, "Adding new payload to event storage: %s", payload) + emitter.add(payload) + event.endProcessing(this) + stateManager.afterTrack(trackerEvent) + } ?: run { + d(TAG, "Event not tracked due to filtering: %s", trackerEvent.eventId) + event.endProcessing(this) + } } return trackerEvent.eventId } - private fun payloadWithEvent(event: TrackerEvent): Payload { + private fun payloadWithEvent(event: TrackerEvent): Payload? { val payload = TrackerPayload() // Payload properties @@ -506,6 +510,11 @@ class Tracker( addStateMachineEntities(event) event.wrapEntitiesToPayload(payload, base64Encoded=base64Encoded) + // Decide whether to track the event or not + if (!stateManager.filter(event)) { + return null + } + // Workaround for campaign attribution if (!event.isPrimitive) { // TODO: To remove when Atomic table refactoring is finished diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationInterface.kt index ef7a7d2b3..caddc3809 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationInterface.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationInterface.kt @@ -88,7 +88,9 @@ interface TrackerConfigurationInterface { var lifecycleAutotracking: Boolean /** - * Whether enable automatic tracking of install event. + * Whether to enable automatic tracking of install event. + * In case com.android.installreferrer:installreferrer library is present, + * an entity with the referrer details will be attached to the install event. */ var installAutotracking: Boolean diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt index 8396f0021..49db2aec2 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt @@ -87,6 +87,7 @@ object Snowplow { * @param context The Android app context. * @param remoteConfiguration The remote configuration used to indicate where to download the configuration from. * @param defaultBundles The default configuration passed by default in case there isn't a cached version and it's not able to download a new one. + * @param defaultBundleVersion Version of the default configuration that will be used to compare with the fetched remote config to decide whether to replace it. * @param onSuccess The callback called when a configuration (cached or downloaded) is set. * It passes a pair object with the list of the namespaces associated * to the created trackers and the state of the configuration – whether it was @@ -97,9 +98,10 @@ object Snowplow { context: Context, remoteConfiguration: RemoteConfiguration, defaultBundles: List?, + defaultBundleVersion: Int, onSuccess: Consumer, ConfigurationState?>?> ) { - configurationProvider = ConfigurationProvider(remoteConfiguration, defaultBundles) + configurationProvider = ConfigurationProvider(remoteConfiguration, defaultBundles, defaultBundleVersion) configurationProvider?.retrieveConfiguration( context, false @@ -112,6 +114,45 @@ object Snowplow { } } + /** + * Set up a single or a set of tracker instances which will be used inside the app to track events. + * The app can run multiple tracker instances which will be identified by string `namespaces`. + * The trackers configuration is automatically downloaded from the endpoint indicated in the [RemoteConfiguration] + * passed as argument. For more details see [RemoteConfiguration]. + * + * The method is asynchronous and you can receive the list of the created trackers in the callbacks once the trackers are created. + * The callback can be called multiple times in case a cached configuration is ready and later a fetched configuration is available. + * You can also pass as argument a default configuration in case there isn't a cached configuration and it's not able to download + * a new one. The downloaded configuration updates the cached one only if the configuration version is greater than the cached one. + * Otherwise the cached one is kept and the callback is not called. + * + * IMPORTANT: The [SQLiteEventStore](com.snowplowanalytics.core.emitter.storage.SQLiteEventStore) will persist all the events that have been tracked but not yet sent. + * Those events are attached to the namespace. + * If the tracker is removed or the app relaunched with a different namespace, those events can't + * be sent to the collector and they remain in a zombie state inside the EventStore. + * To remove all the zombie events you can use the internal method + * [.removeUnsentEventsExceptForNamespaces](com.snowplowanalytics.core.emitter.storage.SQLiteEventStore.removeUnsentEventsExceptForNamespaces) + * in [SQLiteEventStore](com.snowplowanalytics.core.emitter.storage.SQLiteEventStore) + * which will delete all the EventStores instanced with namespaces not listed in the passed list. + * + * @param context The Android app context. + * @param remoteConfiguration The remote configuration used to indicate where to download the configuration from. + * @param defaultBundles The default configuration passed by default in case there isn't a cached version and it's not able to download a new one. + * @param onSuccess The callback called when a configuration (cached or downloaded) is set. + * It passes a pair object with the list of the namespaces associated + * to the created trackers and the state of the configuration – whether it was + * retrieved from cache or fetched over the network. + */ + @JvmStatic + fun setup( + context: Context, + remoteConfiguration: RemoteConfiguration, + defaultBundles: List?, + onSuccess: Consumer, ConfigurationState?>?> + ) { + setup(context, remoteConfiguration, defaultBundles, Int.MIN_VALUE, onSuccess) + } + /** * Reconfigure, create or delete the trackers based on the configuration downloaded remotely. * The trackers configuration is automatically downloaded from the endpoint indicated in the [RemoteConfiguration] diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/PluginConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/PluginConfiguration.kt index 54c648a70..bfc61e78a 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/PluginConfiguration.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/PluginConfiguration.kt @@ -30,6 +30,17 @@ class PluginAfterTrackConfiguration( val closure: Consumer ) +/** + * Provides a closure that is called to decide whether to track a given event or not. + * + * @property schemas Optional list of event schemas to call the block for. If null, the block is called for all events. + * @property closure Block that returns true if the event should be tracked, false otherwise. + */ +class PluginFilterConfiguration( + val schemas: List? = null, + val closure: Function +) + /** * Provides a block closure that returns a list of context entities and is called when events are tracked. * Optionally, you can specify the event schemas for which the block should be called. @@ -43,26 +54,69 @@ class PluginEntitiesConfiguration( ) /** - * Interface for tracker plugin definition. - * Specifies configurations for the closures called when and after events are tracked. + * Identifies a tracker plugin with a unique identifier. Required for all plugins. * * @property identifier Unique identifier of the plugin within the tracker. - * @property entitiesConfiguration Closure configuration that is called when events are tracked to generate context entities to enrich the events. - * @property afterTrackConfiguration Closure configuration that is called after events are tracked. */ -interface PluginConfigurationInterface { +interface PluginIdentifiable { val identifier: String - val entitiesConfiguration: PluginEntitiesConfiguration? - val afterTrackConfiguration: PluginAfterTrackConfiguration? } -internal fun PluginConfigurationInterface.toStateMachine(): PluginStateMachine { +internal fun PluginIdentifiable.toStateMachine(): PluginStateMachine { + var entitiesConfiguration: PluginEntitiesConfiguration? = null + (this as? PluginEntitiesCallable)?.let { entitiesConfiguration = it.entitiesConfiguration } + + var afterTrackConfiguration: PluginAfterTrackConfiguration? = null + (this as? PluginAfterTrackCallable)?.let { afterTrackConfiguration = it.afterTrackConfiguration } + + var filterConfiguration: PluginFilterConfiguration? = null + (this as? PluginFilterCallable)?.let { filterConfiguration = it.filterConfiguration } + return PluginStateMachine( identifier = identifier, entitiesConfiguration = entitiesConfiguration, - afterTrackConfiguration = afterTrackConfiguration + afterTrackConfiguration = afterTrackConfiguration, + filterConfiguration = filterConfiguration ) } +/** + * Protocol for a plugin that provides a closure to generate context entities to enrich events. + * + * @property entitiesConfiguration Closure configuration that is called when events are tracked to generate context entities to enrich the events. + */ +interface PluginEntitiesCallable { + val entitiesConfiguration: PluginEntitiesConfiguration? +} + +/** + * Protocol for a plugin that provides a closure to call after events are tracked. + * + * @property afterTrackConfiguration Closure configuration that is called after events are tracked. + */ +interface PluginAfterTrackCallable { + val afterTrackConfiguration: PluginAfterTrackConfiguration? +} + +/** + * Protocol for a plugin that provides a closure to decide whether to track events or not. + * + * @property filterConfiguration Closure configuration that is called to decide whether to track a given event or not. + */ +interface PluginFilterCallable { + val filterConfiguration: PluginFilterConfiguration? +} + +/** + * Interface for tracker plugin definition. + * Specifies configurations for the closures called when and after events are tracked. + * + * @property identifier Unique identifier of the plugin within the tracker. + * @property entitiesConfiguration Closure configuration that is called when events are tracked to generate context entities to enrich the events. + * @property afterTrackConfiguration Closure configuration that is called after events are tracked. + */ +@Deprecated("Use PluginIdentifiable, PluginEntitiesCallable and PluginAfterTrackCallable instead") +interface PluginConfigurationInterface : PluginIdentifiable, PluginEntitiesCallable, PluginAfterTrackCallable { +} /** * Configuration for a custom tracker plugin. @@ -72,9 +126,10 @@ internal fun PluginConfigurationInterface.toStateMachine(): PluginStateMachine { */ class PluginConfiguration( override val identifier: String -) : Configuration, PluginConfigurationInterface { +) : Configuration, PluginIdentifiable, PluginEntitiesCallable, PluginAfterTrackCallable, PluginFilterCallable { override var entitiesConfiguration: PluginEntitiesConfiguration? = null override var afterTrackConfiguration: PluginAfterTrackConfiguration? = null + override var filterConfiguration: PluginFilterConfiguration? = null /** * Add a closure that generates entities for a given tracked event. @@ -102,11 +157,29 @@ class PluginConfiguration( fun afterTrack( schemas: List? = null, closure: Consumer - ) { + ): PluginConfiguration { afterTrackConfiguration = PluginAfterTrackConfiguration( schemas = schemas, closure = closure ) + return this + } + + /** + * Add a closure that is called to decide whether to track a given event or not. + * + * @property schemas Optional list of event schemas to call the closure for. If null, the closure is called for all events. + * @property closure Closure block that returns true if the event should be tracked, false otherwise. + */ + fun filter( + schemas: List? = null, + closure: Function + ): PluginConfiguration { + filterConfiguration = PluginFilterConfiguration( + schemas = schemas, + closure = closure + ) + return this } override fun copy(): Configuration { @@ -115,6 +188,7 @@ class PluginConfiguration( ) entitiesConfiguration?.let { copy.entities(schemas = it.schemas, closure = it.closure) } afterTrackConfiguration?.let { copy.afterTrack(schemas = it.schemas, closure = it.closure) } + filterConfiguration?.let { copy.filter(schemas = it.schemas, closure = it.closure) } return copy } } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt index ae4f21ebe..1fb385bbe 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt @@ -192,6 +192,8 @@ open class TrackerConfiguration( /** * Whether to enable automatic tracking of install event. + * In case com.android.installreferrer:installreferrer library is present, + * an entity with the referrer details will be attached to the install event. */ fun installAutotracking(installAutotracking: Boolean): TrackerConfiguration { this.installAutotracking = installAutotracking diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/PluginsController.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/PluginsController.kt index 7201bc089..752b79485 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/PluginsController.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/PluginsController.kt @@ -12,7 +12,7 @@ */ package com.snowplowanalytics.snowplow.controller -import com.snowplowanalytics.snowplow.configuration.PluginConfigurationInterface +import com.snowplowanalytics.snowplow.configuration.PluginIdentifiable /** * Controller for managing plugins initialized in the tracker. @@ -26,7 +26,7 @@ interface PluginsController { /** * Add a new plugin. */ - fun addPlugin(plugin: PluginConfigurationInterface) + fun addPlugin(plugin: PluginIdentifiable) /** * Remove plugin with the identifier.