From 29aa1399daaa97a74cc8ab94cd7fd03dfda21848 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Thu, 18 May 2023 13:06:30 +0100 Subject: [PATCH] =?UTF-8?q?Bug=201832074=20=E2=80=94=20Parse=20referrer=20?= =?UTF-8?q?URL=20with=20URLDecoder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../metrics/InstallReferrerMetricsService.kt | 36 ++++++++++- .../InstallReferrerMetricsServiceTest.kt | 62 +++++++++++++++---- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt index 36662bf97..2062090bb 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt @@ -13,6 +13,7 @@ import com.android.installreferrer.api.InstallReferrerStateListener import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.Settings +import java.net.URLDecoder /** * A metrics service used to derive the UTM parameters with the Google Play Install Referrer library. @@ -83,7 +84,10 @@ class InstallReferrerMetricsService(private val context: Context) : MetricsServi @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun recordInstallReferrer(settings: Settings, url: String?) { - val params = url?.let(UTMParams::fromUrl) + if (url.isNullOrBlank()) { + return + } + val params = UTMParams.fromURLString(url) if (params == null || params.isEmpty()) { return } @@ -143,14 +147,40 @@ data class UTMParams( const val UTM_TERM = "utm_term" const val UTM_CONTENT = "utm_content" + /** + * Try and unpack the referrer URL by successively URLDecoding the URL. + * + * Once the url ceases to decode anymore, it gives up. + */ + fun fromURLString(string: String): UTMParams? { + // Look for the first time 'utm_' is detected, after the first '?'. + val utmIndex = string.indexOf("utm_", string.indexOf('?')) + if (utmIndex < 0) { + return null + } + var url = string.substring(utmIndex) + while (true) { + val params = fromQueryString(url) + if (!params.isEmpty()) { + return params + } + val newValue = URLDecoder.decode(url, "UTF-8") + if (newValue == url) { + break + } + url = newValue + } + return null + } + /** * Derive a set of UTM parameters from a string URL. */ - fun fromUrl(url: String): UTMParams = + fun fromQueryString(queryString: String): UTMParams = with(UrlQuerySanitizer()) { allowUnregisteredParamaters = true unregisteredParameterValueSanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal() - parseUrl(url) + parseQuery(queryString) UTMParams( source = getValue(UTM_SOURCE), medium = getValue(UTM_MEDIUM), diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsServiceTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsServiceTest.kt index dad154a35..fac3b0df4 100644 --- a/app/src/test/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsServiceTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsServiceTest.kt @@ -10,6 +10,7 @@ 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.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Rule @@ -28,24 +29,24 @@ internal class InstallReferrerMetricsServiceTest { @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) + assertEquals("SOURCE", UTMParams.fromQueryString("utm_source=SOURCE").source) + assertEquals("MEDIUM", UTMParams.fromQueryString("utm_medium=MEDIUM").medium) + assertEquals("CAMPAIGN", UTMParams.fromQueryString("utm_campaign=CAMPAIGN").campaign) + assertEquals("TERM", UTMParams.fromQueryString("utm_term=TERM").term) + assertEquals("CONTENT", UTMParams.fromQueryString("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) + assertEquals("WITH SPACE", UTMParams.fromQueryString("utm_source=WITH+SPACE").source) + assertEquals("WITH SPACE", UTMParams.fromQueryString("utm_medium=WITH%20SPACE").medium) + assertEquals("WITH SPACE", UTMParams.fromQueryString("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) + assertNull(UTMParams.fromQueryString("missing=").source) + assertEquals("", UTMParams.fromQueryString("utm_source=").source) } @Test @@ -117,7 +118,7 @@ internal class InstallReferrerMetricsServiceTest { val expected = UTMParams(source = "SOURCE", medium = "MEDIUM", campaign = "CAMPAIGN", content = "CONTENT", term = "TERM") val observed = UTMParams.fromSettings(settings) - assertEquals(observed, expected) + assertEquals(expected, observed) assertEquals("SOURCE", PlayStoreAttribution.source.testGetValue()) assertEquals("MEDIUM", PlayStoreAttribution.medium.testGetValue()) @@ -139,4 +140,43 @@ internal class InstallReferrerMetricsServiceTest { val service = InstallReferrerMetricsService(context) assertEquals(MetricServiceType.Marketing, service.type) } + + @Test + fun testDecodeReferrerUrl() { + // Example from https://developers.google.com/analytics/devguides/collection/android/v4/campaigns#google-play-url-builder + val params = UTMParams.fromURLString( + "https://play.google.com/store/apps/details?id=com.example.application" + + "&referrer=utm_source%3Dgoogle" + + "%26utm_medium%3Dcpc" + + "%26utm_term%3Drunning%252Bshoes" + + "%26utm_content%3Dlogolink" + + "%26utm_campaign%3Dspring_sale", + ) + assertNotNull(params) + + val expected = UTMParams( + source = "google", + medium = "cpc", + campaign = "spring_sale", + content = "logolink", + term = "running+shoes", + ) + + assertEquals(expected, params) + } + + @Test + fun testDecodeReferrerAdMobUrl() { + val expected = UTMParams(source = "SOURCE", medium = "MEDIUM", campaign = "CAMPAIGN", content = "CONTENT", term = "TERM") + // Generated with https://developers.google.com/analytics/devguides/collection/android/v4/campaigns#google-play-url-builder + val fromUrl = UTMParams.fromURLString( + "https://play.google.com/store/apps/details?id=org.mozilla.fenix&referrer=utm_source%3DSOURCE%26utm_medium%3DMEDIUM%26utm_term%3DTERM%26utm_content%3DCONTENT%26utm_campaign%3DCAMPAIGN%26anid%3Dadmob", + ) + assertNotNull(fromUrl) + assertEquals(expected, fromUrl) + + val fromReferrerAttribute = UTMParams.fromURLString("utm_source%3DSOURCE%26utm_medium%3DMEDIUM%26utm_term%3DTERM%26utm_content%3DCONTENT%26utm_campaign%3DCAMPAIGN%26anid%3Dadmob") + assertNotNull(fromReferrerAttribute) + assertEquals(expected, fromReferrerAttribute) + } }