diff --git a/app/metrics.yaml b/app/metrics.yaml index 13b301405..907747b21 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -1789,3 +1789,70 @@ pocket: notification_emails: - fenix-core@mozilla.com expires: "2020-09-01" + +installation: + campaign: + type: string + send_in_pings: + - installation + description: > + Campaign + bugs: + - https://github.com/mozilla-mobile/fenix/issues/7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/8074 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-09-01" + network: + type: string + send_in_pings: + - installation + description: > + Network + bugs: + - https://github.com/mozilla-mobile/fenix/issues/7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/8074 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-09-01" + adgroup: + type: string + send_in_pings: + - installation + description: > + AdGroup + bugs: + - https://github.com/mozilla-mobile/fenix/issues/7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/8074 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-09-01" + creative: + send_in_pings: + - installation + type: string + description: > + Creative + bugs: + - https://github.com/mozilla-mobile/fenix/issues/7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/8074 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-09-01" + timestamp: + send_in_pings: + - installation + type: string + description: > + Timestamp + bugs: + - https://github.com/mozilla-mobile/fenix/issues/7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/8074 + notification_emails: + - fenix-core@mozilla.com + expires: "2020-09-01" \ No newline at end of file diff --git a/app/pings.yaml b/app/pings.yaml index f8d3605a2..83439e2a5 100644 --- a/app/pings.yaml +++ b/app/pings.yaml @@ -18,3 +18,14 @@ activation: - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 notification_emails: - fenix-core@mozilla.com + +installation: + description: > + Intended for counting user installs. + include_client_id: false + bugs: + - 7295 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 + notification_emails: + - fenix-core@mozilla.com diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt index 4cf7bb352..28e8afd09 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt @@ -38,9 +38,27 @@ class AdjustMetricsService(private val application: Application) : MetricsServic ) config.setOnAttributionChangedListener { - it.campaign?.let { campaign -> - application.applicationContext.settings().adjustCampaignId = campaign + if (!it.network.isNullOrEmpty()) { + application.applicationContext.settings().adjustNetwork = + it.network } + if (!it.adgroup.isNullOrEmpty()) { + application.applicationContext.settings().adjustAdGroup = + it.adgroup + } + if (!it.creative.isNullOrEmpty()) { + application.applicationContext.settings().adjustCreative = + it.creative + } + if (!it.campaign.isNullOrEmpty()) { + application.applicationContext.settings().adjustCampaignId = + it.campaign + } + if (application.applicationContext.settings().adjustInstallTimestamp.isEmpty()) { + application.applicationContext.settings().adjustInstallTimestamp = + System.currentTimeMillis().toString() + } + InstallationPing(application).checkAndSend() } config.setLogLevel(LogLevel.SUPRESS) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index 4a0b76932..927b765ad 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -571,6 +571,7 @@ class GleanMetricsService(private val context: Context) : MetricsService { } activationPing.checkAndSend() + InstallationPing(context).checkAndSend() } override fun stop() { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/InstallationPing.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallationPing.kt new file mode 100644 index 000000000..4d444123e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallationPing.kt @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.metrics + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.GleanMetrics.Installation +import org.mozilla.fenix.GleanMetrics.Pings +import org.mozilla.fenix.ext.settings + +class InstallationPing(private val context: Context) { + + private val prefs: SharedPreferences by lazy { + context.getSharedPreferences( + "${this.javaClass.canonicalName}.prefs", Context.MODE_PRIVATE + ) + } + + /** + * Checks whether or not the installation ping was already + * triggered by the application. + * + * Note that this only tells us that Fenix triggered the + * ping and then delegated the transmission to Glean. We + * have no way to tell if it was actually sent or not. + * + * @return true if it was already triggered, false otherwise. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun wasAlreadyTriggered(): Boolean { + return prefs.getBoolean("ping_sent", false) + } + + /** + * Marks the "installation" ping as triggered by the application. + * This ensures the ping is not triggered again at the next app + * start. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun markAsTriggered() { + prefs.edit().putBoolean("ping_sent", true).apply() + } + + /** + * Fills the metrics and triggers the 'installation' ping. + * This is a separate function to simplify unit-testing. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun triggerPing() { + if (checkMetricsNotEmpty() + ) { + Installation.campaign.set(context.settings().adjustCampaignId) + Installation.adgroup.set(context.settings().adjustAdGroup) + Installation.creative.set(context.settings().adjustCreative) + Installation.network.set(context.settings().adjustNetwork) + Installation.timestamp.set(context.settings().adjustInstallTimestamp) + CoroutineScope(Dispatchers.IO).launch { + Pings.installation.submit() + markAsTriggered() + } + } + } + + /** + * Check that required metrics are not empty before attempting to send ping. + * */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun checkMetricsNotEmpty(): Boolean = listOf( + context.settings().adjustAdGroup, + context.settings().adjustCreative, + context.settings().adjustNetwork + ).all { it.isNotEmpty() } + + /** + * Trigger sending the `installation` ping if it wasn't sent already. + * Then, mark it so that it doesn't get triggered next time Fenix + * starts. + */ + fun checkAndSend() { + if (wasAlreadyTriggered()) { + Logger.debug("InstallationPing - already generated") + return + } + triggerPing() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 9667b179b..61a9f5827 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -95,6 +95,26 @@ class Settings private constructor( default = "" ) + var adjustNetwork by stringPreference( + appContext.getPreferenceKey(R.string.pref_key_adjust_network), + default = "" + ) + + var adjustAdGroup by stringPreference( + appContext.getPreferenceKey(R.string.pref_key_adjust_adgroup), + default = "" + ) + + var adjustCreative by stringPreference( + appContext.getPreferenceKey(R.string.pref_key_adjust_creative), + default = "" + ) + + var adjustInstallTimestamp by stringPreference( + appContext.getPreferenceKey(R.string.pref_key_adjust_install_timestamp), + default = "" + ) + var openLinksInAPrivateTab by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_open_links_in_a_private_tab), default = false diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 6349daabe..d56ebd301 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -133,6 +133,11 @@ pref_key_bounce_quick_action pref_key_reader_mode_notification pref_key_adjust_campaign + pref_key_adjust_network + pref_key_adjust_adgroup + pref_key_adjust_creative + pref_key_adjust_install_timestamp + pref_key_testing_stage pref_key_encryption_key_generated diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/InstallationPingTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/InstallationPingTest.kt new file mode 100644 index 000000000..dce61eba6 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/InstallationPingTest.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.metrics + +import android.content.Context +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Test +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.utils.Settings +import org.robolectric.annotation.Config + +@ExperimentalCoroutinesApi +@Config(application = TestApplication::class) +internal class InstallationPingTest { + + @Test + fun `checkAndSend() triggers the ping if it wasn't marked as triggered`() = runBlockingTest { + val mockedContext: Context = mockk(relaxed = true) + val mockedSettings: Settings = mockk(relaxed = true) + mockkStatic("org.mozilla.fenix.ext.ContextKt") + every { mockedContext.settings() } returns mockedSettings + val mockAp = spyk(InstallationPing(mockedContext), recordPrivateCalls = true) + every { mockAp.checkMetricsNotEmpty() } returns true + every { mockAp.wasAlreadyTriggered() } returns false + every { mockAp.markAsTriggered() } just Runs + + mockAp.checkAndSend() + + verify(exactly = 1) { mockAp.triggerPing() } + // Marking the ping as triggered happens in a co-routine off the main thread, + // so wait a bit for it. + verify(exactly = 1) { mockAp.markAsTriggered() } + } + + @Test + fun `checkAndSend() doesn't trigger the ping again if it was marked as triggered`() { + val mockAp = spyk(InstallationPing(mockk()), recordPrivateCalls = true) + every { mockAp.wasAlreadyTriggered() } returns true + + mockAp.checkAndSend() + + verify(exactly = 0) { mockAp.triggerPing() } + } +} diff --git a/docs/metrics.md b/docs/metrics.md index 6be577f00..1b3feacac 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -10,6 +10,7 @@ This means you might have to go searching through the dependency tree to get a f - [activation](#activation) - [baseline](#baseline) - [events](#events) + - [installation](#installation) - [metrics](#metrics) @@ -169,6 +170,19 @@ The following metrics are added to the ping: | user_specified_search_engines.custom_engine_added |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user added a new custom search engine |[1](https://github.com/mozilla-mobile/fenix/pull/6918)||2020-09-01 | | user_specified_search_engines.custom_engine_deleted |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user deleted a custom search engine |[1](https://github.com/mozilla-mobile/fenix/pull/6918)||2020-09-01 | +## installation +Intended for counting user installs. + +The following metrics are added to the ping: + +| Name | Type | Description | Data reviews | Extras | Expiration | +| --- | --- | --- | --- | --- | --- | +| installation.adgroup |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |AdGroup |[1](https://github.com/mozilla-mobile/fenix/pull/8074)||2020-09-01 | +| installation.campaign |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |Campaign |[1](https://github.com/mozilla-mobile/fenix/pull/8074)||2020-09-01 | +| installation.creative |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |Creative |[1](https://github.com/mozilla-mobile/fenix/pull/8074)||2020-09-01 | +| installation.network |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |Network |[1](https://github.com/mozilla-mobile/fenix/pull/8074)||2020-09-01 | +| installation.timestamp |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |Timestamp |[1](https://github.com/mozilla-mobile/fenix/pull/8074)||2020-09-01 | + ## metrics This is a built-in ping that is assembled out of the box by the Glean SDK. See the Glean SDK documentation for the [`metrics` ping](https://mozilla.github.io/glean/book/user/pings/metrics.html).