Bug 1832069 - Add Google Play install referrer library

fenix/115.2.0
James Hugman 1 year ago committed by mergify[bot]
parent a71ebd543e
commit b02c292c8f

@ -6702,6 +6702,7 @@ first_session:
metadata:
tags:
- Performance
- Attribution
network:
type: string
send_in_pings:
@ -6726,6 +6727,7 @@ first_session:
metadata:
tags:
- Performance
- Attribution
adgroup:
type: string
send_in_pings:
@ -6750,6 +6752,7 @@ first_session:
metadata:
tags:
- Telemetry
- Attribution
creative:
send_in_pings:
- first-session
@ -6774,6 +6777,7 @@ first_session:
metadata:
tags:
- Performance
- Attribution
distribution_id:
type: string
description: |
@ -6817,11 +6821,13 @@ first_session:
metadata:
tags:
- Performance
- Attribution
adjust_attribution_time:
type: timing_distribution
time_unit: millisecond
send_in_pings:
- first-session
- metrics
description: >
The time that it takes to derive the attribution parameters by
the Adjust SDK.
@ -6834,6 +6840,123 @@ first_session:
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Performance
- Attribution
play_store_attribution:
source:
type: string
send_in_pings:
- first-session
description: |
The name of the utm_source that is responsible for this installation.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Attribution
medium:
type: string
send_in_pings:
- first-session
description: |
The name of the utm_medium that is responsible for this installation.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Attribution
campaign:
type: string
send_in_pings:
- first-session
description: |
The name of the utm_campaign that is responsible for this installation.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Attribution
term:
type: string
send_in_pings:
- first-session
description: |
The name of the utm_term that is responsible for this installation.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Attribution
content:
type: string
send_in_pings:
- first-session
description: |
The name of the utm_content that is responsible for this installation.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Attribution
attribution_time:
type: timing_distribution
time_unit: millisecond
send_in_pings:
- metrics
description: >
The time that it takes to derive the attribution parameters by
the Google Play Install Referrer library.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1832069
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1991#issuecomment-1545842578
data_sensitivity:
- technical
notification_emails:
- android-probes@mozilla.com
expires: 124
metadata:
tags:
- Performance

@ -26,6 +26,7 @@ import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.DefaultMetricsStorage
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.InstallReferrerMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus
@ -136,6 +137,7 @@ class Analytics(
storage = metricsStorage,
crashReporter = crashReporter,
),
InstallReferrerMetricsService(context),
),
isDataTelemetryEnabled = { context.settings().isTelemetryEnabled },
isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled },

@ -0,0 +1,201 @@
/* 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.net.UrlQuerySanitizer
import android.os.RemoteException
import androidx.annotation.VisibleForTesting
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Settings
/**
* A metrics service used to derive the UTM parameters with the Google Play Install Referrer library.
*
* At first startup, the [UTMParams] are derived from the install referrer URL and stored in settings.
*/
class InstallReferrerMetricsService(private val context: Context) : MetricsService {
override val type = MetricServiceType.Marketing
private var referrerClient: InstallReferrerClient? = null
override fun start() {
if (context.settings().utmParamsKnown) {
return
}
val timerId = PlayStoreAttribution.attributionTime.start()
val client = InstallReferrerClient.newBuilder(context).build()
referrerClient = client
client.startConnection(
object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
PlayStoreAttribution.attributionTime.stopAndAccumulate(timerId)
when (responseCode) {
InstallReferrerClient.InstallReferrerResponse.OK -> {
// Connection established.
try {
val response = client.installReferrer
recordInstallReferrer(context.settings(), response.installReferrer)
context.settings().utmParamsKnown = true
} catch (e: RemoteException) {
// NOOP.
// We can't do anything about this.
}
}
InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
// API not available on the current Play Store app.
context.settings().utmParamsKnown = true
}
InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
// Connection couldn't be established.
}
}
// End the connection, and null out the client.
stop()
}
override fun onInstallReferrerServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
referrerClient = null
}
},
)
}
override fun stop() {
referrerClient?.endConnection()
referrerClient = null
}
override fun track(event: Event) = Unit
override fun shouldTrack(event: Event): Boolean = false
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun recordInstallReferrer(settings: Settings, url: String?) {
val params = url?.let(UTMParams::fromUrl)
if (params == null || params.isEmpty()) {
return
}
params.intoSettings(settings)
params.apply {
source?.let {
PlayStoreAttribution.source.set(it)
}
medium?.let {
PlayStoreAttribution.medium.set(it)
}
campaign?.let {
PlayStoreAttribution.campaign.set(it)
}
content?.let {
PlayStoreAttribution.content.set(it)
}
term?.let {
PlayStoreAttribution.term.set(it)
}
}
}
}
/**
* Descriptions of utm parameters comes from
* https://support.google.com/analytics/answer/1033863
* - utm_source
* Identify the advertiser, site, publication, etc.
* that is sending traffic to your property, for example: google, newsletter4, billboard.
* - utm_medium
* The advertising or marketing medium, for example: cpc, banner, email newsletter.
* utm_campaign
* The individual campaign name, slogan, promo code, etc. for a product.
* - utm_term
* Identify paid search keywords.
* If you're manually tagging paid keyword campaigns, you should also use
* utm_term to specify the keyword.
* - utm_content
* Used to differentiate similar content, or links within the same ad.
* For example, if you have two call-to-action links within the same email message,
* you can use utm_content and set different values for each so you can tell
* which version is more effective.
*/
data class UTMParams(
val source: String?,
val medium: String?,
val campaign: String?,
val term: String?,
val content: String?,
) {
companion object {
const val UTM_SOURCE = "utm_source"
const val UTM_MEDIUM = "utm_medium"
const val UTM_CAMPAIGN = "utm_campaign"
const val UTM_TERM = "utm_term"
const val UTM_CONTENT = "utm_content"
/**
* Derive a set of UTM parameters from a string URL.
*/
fun fromUrl(url: String): UTMParams =
with(UrlQuerySanitizer()) {
allowUnregisteredParamaters = true
unregisteredParameterValueSanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal()
parseUrl(url)
UTMParams(
source = getValue(UTM_SOURCE),
medium = getValue(UTM_MEDIUM),
campaign = getValue(UTM_CAMPAIGN),
term = getValue(UTM_TERM),
content = getValue(UTM_CONTENT),
)
}
/**
* Derive the set of UTM parameters stored in Settings.
*/
fun fromSettings(settings: Settings): UTMParams =
with(settings) {
UTMParams(
source = utmSource,
medium = utmMedium,
campaign = utmCampaign,
term = utmTerm,
content = utmContent,
)
}
}
/**
* Persist the UTM params into Settings.
*/
fun intoSettings(settings: Settings) {
with(settings) {
utmSource = source ?: ""
utmMedium = medium ?: ""
utmCampaign = campaign ?: ""
utmTerm = term ?: ""
utmContent = content ?: ""
}
}
/**
* Return [true] if none of the utm params are set.
*/
fun isEmpty(): Boolean {
return source.isNullOrBlank() &&
medium.isNullOrBlank() &&
campaign.isNullOrBlank() &&
term.isNullOrBlank() &&
content.isNullOrBlank()
}
}

@ -8,6 +8,11 @@ import android.content.Context
import androidx.core.app.NotificationManagerCompat
import mozilla.components.service.nimbus.messaging.JexlAttributeProvider
import org.json.JSONObject
import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_CAMPAIGN
import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_CONTENT
import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_MEDIUM
import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_SOURCE
import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_TERM
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.BrowsersCache
@ -63,6 +68,12 @@ object CustomAttributeProvider : JexlAttributeProvider {
"adjust_ad_group" to settings.adjustAdGroup,
"adjust_creative" to settings.adjustCreative,
UTM_SOURCE to settings.utmSource,
UTM_MEDIUM to settings.utmMedium,
UTM_CAMPAIGN to settings.utmCampaign,
UTM_TERM to settings.utmTerm,
UTM_CONTENT to settings.utmContent,
"are_notifications_enabled" to NotificationManagerCompat.from(context).areNotificationsEnabledSafe(),
),
)

@ -185,6 +185,36 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = "",
)
var utmParamsKnown by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_params_known),
default = false,
)
var utmSource by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_source),
default = "",
)
var utmMedium by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_medium),
default = "",
)
var utmCampaign by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_campaign),
default = "",
)
var utmTerm by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_term),
default = "",
)
var utmContent by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_utm_content),
default = "",
)
var contileContextId by stringPreference(
appContext.getPreferenceKey(R.string.pref_key_contile_context_id),
default = "",

@ -213,6 +213,13 @@
<string name="pref_key_adjust_adgroup" translatable="false">pref_key_adjust_adgroup</string>
<string name="pref_key_adjust_creative" translatable="false">pref_key_adjust_creative</string>
<string name="pref_key_utm_params_known" translatable="false">pref_key_utm_params_known</string>
<string name="pref_key_utm_source" translatable="false">pref_key_utm_source</string>
<string name="pref_key_utm_medium" translatable="false">pref_key_utm_medium</string>
<string name="pref_key_utm_campaign" translatable="false">pref_key_utm_campaign</string>
<string name="pref_key_utm_term" translatable="false">pref_key_utm_term</string>
<string name="pref_key_utm_content" translatable="false">pref_key_utm_content</string>
<string name="pref_key_contile_context_id" translatable="false">pref_key_contile_context_id</string>
<!-- Wallpaper Settings -->

@ -0,0 +1,142 @@
/* 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 androidx.test.core.app.ApplicationProvider
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution
import org.mozilla.fenix.utils.Settings
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
internal class InstallReferrerMetricsServiceTest {
val context: Context = ApplicationProvider.getApplicationContext()
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
@Test
fun testUtmParamsFromUrl() {
assertEquals("SOURCE", UTMParams.fromUrl("https://example.com?utm_source=SOURCE").source)
assertEquals("MEDIUM", UTMParams.fromUrl("https://example.com?utm_medium=MEDIUM").medium)
assertEquals("CAMPAIGN", UTMParams.fromUrl("https://example.com?utm_campaign=CAMPAIGN").campaign)
assertEquals("TERM", UTMParams.fromUrl("https://example.com?utm_term=TERM").term)
assertEquals("CONTENT", UTMParams.fromUrl("https://example.com?utm_content=CONTENT").content)
}
@Test
fun testUtmParamsFromUrlWithSpaces() {
assertEquals("WITH SPACE", UTMParams.fromUrl("https://example.com?utm_source=WITH+SPACE").source)
assertEquals("WITH SPACE", UTMParams.fromUrl("https://example.com?utm_medium=WITH%20SPACE").medium)
assertEquals("WITH SPACE", UTMParams.fromUrl("https://example.com?utm_campaign=WITH SPACE").campaign)
}
@Test
fun testUtmParamsFromUrlWithMissingParams() {
assertNull(UTMParams.fromUrl("https://example.com?missing=").source)
assertEquals("", UTMParams.fromUrl("https://example.com?utm_source=").source)
}
@Test
fun testUtmParamsRoundTripThroughSettingsMinimumParams() {
val settings = Settings(context)
val expected = UTMParams(source = "", medium = "", campaign = "", content = "", term = "")
val observed = UTMParams.fromSettings(settings)
assertEquals(observed, expected)
assertTrue(observed.isEmpty())
}
@Test
fun testUtmParamsRoundTripThroughSettingsMaximumParams() {
val expected = UTMParams(source = "source", medium = "medium", campaign = "campaign", content = "content", term = "term")
val settings = Settings(context)
expected.intoSettings(settings)
val observed = UTMParams.fromSettings(settings)
assertEquals(observed, expected)
assertFalse(observed.isEmpty())
}
@Test
fun testInstallReferrerMetricsMinimumParams() {
val service = InstallReferrerMetricsService(context)
val settings = Settings(context)
service.recordInstallReferrer(settings, "https://example.com")
val expected = UTMParams(source = "", medium = "", campaign = "", content = "", term = "")
val observed = UTMParams.fromSettings(settings)
assertEquals(observed, expected)
assertNull(PlayStoreAttribution.source.testGetValue())
assertNull(PlayStoreAttribution.medium.testGetValue())
assertNull(PlayStoreAttribution.campaign.testGetValue())
assertNull(PlayStoreAttribution.content.testGetValue())
assertNull(PlayStoreAttribution.term.testGetValue())
assertTrue(observed.isEmpty())
}
@Test
fun testInstallReferrerMetricsPartial() {
val service = InstallReferrerMetricsService(context)
val settings = Settings(context)
service.recordInstallReferrer(settings, "https://example.com?utm_campaign=CAMPAIGN")
val expected = UTMParams(source = "", medium = "", campaign = "CAMPAIGN", content = "", term = "")
val observed = UTMParams.fromSettings(settings)
assertEquals(observed, expected)
assertNull(PlayStoreAttribution.source.testGetValue())
assertNull(PlayStoreAttribution.medium.testGetValue())
assertEquals("CAMPAIGN", PlayStoreAttribution.campaign.testGetValue())
assertNull(PlayStoreAttribution.content.testGetValue())
assertNull(PlayStoreAttribution.term.testGetValue())
assertFalse(observed.isEmpty())
}
@Test
fun testInstallReferrerMetricsMaximumParams() {
val service = InstallReferrerMetricsService(context)
val settings = Settings(context)
service.recordInstallReferrer(settings, "https://example.com?utm_source=SOURCE&utm_medium=MEDIUM&utm_campaign=CAMPAIGN&utm_content=CONTENT&utm_term=TERM")
val expected = UTMParams(source = "SOURCE", medium = "MEDIUM", campaign = "CAMPAIGN", content = "CONTENT", term = "TERM")
val observed = UTMParams.fromSettings(settings)
assertEquals(observed, expected)
assertEquals("SOURCE", PlayStoreAttribution.source.testGetValue())
assertEquals("MEDIUM", PlayStoreAttribution.medium.testGetValue())
assertEquals("CAMPAIGN", PlayStoreAttribution.campaign.testGetValue())
assertEquals("CONTENT", PlayStoreAttribution.content.testGetValue())
assertEquals("TERM", PlayStoreAttribution.term.testGetValue())
assertFalse(observed.isEmpty())
}
@Test
fun testInstallReferrerMetricsShouldTrack() {
val service = InstallReferrerMetricsService(context)
assertFalse(service.shouldTrack(Event.GrowthData.FirstAppOpenForDay))
}
@Test
fun testInstallReferrerMetricsType() {
val service = InstallReferrerMetricsService(context)
assertEquals(MetricServiceType.Marketing, service.type)
}
}

@ -18,6 +18,8 @@ Accounts:
AndroidIntegration:
description: Corresponds to the [Feature:AndroidIntegration](https://github.com/mozilla-mobile/fenix/issues?q=label%3AFeature%3AAndroidIntegration)
label on GitHub.
Attribution:
description: Corresponds to the attribution of an install derived from Google Play Store Install Referrer and the Adjust SDK.
Autofill:
description: Address and Credit Card autofill. Corresponds to the [Feature:Autofill](https://github.com/mozilla-mobile/fenix/issues?q=label%3AFeature%3AAutofill)
label on GitHub.

Loading…
Cancel
Save