From 803b31a1793492275a75dac6ab9afaae5589ac48 Mon Sep 17 00:00:00 2001 From: Abhay Sood Date: Fri, 9 Aug 2024 18:48:48 +0530 Subject: [PATCH 1/2] feat(android): configure http urls to collect events for Adds a new configuration `httpUrlAllowlist`. Allows enabling collection of `http` events only for certain URLs. This is useful to setup if you want to collect data only for certain endpoints or third party domains. If this list is empty, `httpUrlBlocklist` is considered. By default, this list is empty. --- android/measure/api/measure.api | 5 +- .../sh/measure/android/MeasureInitializer.kt | 1 + .../java/sh/measure/android/config/Config.kt | 1 + .../measure/android/config/ConfigProvider.kt | 13 +++- .../measure/android/config/DefaultConfig.kt | 1 + .../measure/android/config/MeasureConfig.kt | 25 ++++++- .../android/config/ConfigProviderTest.kt | 66 +++++++++++++++++-- .../android/fakes/FakeConfigProvider.kt | 1 + docs/android/configuration-options.md | 15 +++++ 9 files changed, 120 insertions(+), 8 deletions(-) diff --git a/android/measure/api/measure.api b/android/measure/api/measure.api index e2e5708a7..7154d2dae 100644 --- a/android/measure/api/measure.api +++ b/android/measure/api/measure.api @@ -23,10 +23,11 @@ public final class sh/measure/android/Measure { public final class sh/measure/android/config/MeasureConfig : sh/measure/android/config/IMeasureConfig { public static final field $stable I public fun ()V - public fun (ZZLsh/measure/android/config/ScreenshotMaskLevel;ZZLjava/util/List;Ljava/util/List;ZF)V - public synthetic fun (ZZLsh/measure/android/config/ScreenshotMaskLevel;ZZLjava/util/List;Ljava/util/List;ZFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZLsh/measure/android/config/ScreenshotMaskLevel;ZZLjava/util/List;Ljava/util/List;Ljava/util/List;ZF)V + public synthetic fun (ZZLsh/measure/android/config/ScreenshotMaskLevel;ZZLjava/util/List;Ljava/util/List;Ljava/util/List;ZFILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun getEnableLogging ()Z public fun getHttpHeadersBlocklist ()Ljava/util/List; + public fun getHttpUrlAllowlist ()Ljava/util/List; public fun getHttpUrlBlocklist ()Ljava/util/List; public fun getScreenshotMaskLevel ()Lsh/measure/android/config/ScreenshotMaskLevel; public fun getSessionSamplingRate ()F diff --git a/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt b/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt index af3a4ee05..a36969b37 100644 --- a/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt +++ b/android/measure/src/main/java/sh/measure/android/MeasureInitializer.kt @@ -104,6 +104,7 @@ internal class MeasureInitializerImpl( trackHttpBody = inputConfig.trackHttpBody, httpHeadersBlocklist = inputConfig.httpHeadersBlocklist, httpUrlBlocklist = inputConfig.httpUrlBlocklist, + httpUrlAllowlist = inputConfig.httpUrlAllowlist, trackActivityIntentData = inputConfig.trackActivityIntentData, sessionSamplingRate = inputConfig.sessionSamplingRate, ), diff --git a/android/measure/src/main/java/sh/measure/android/config/Config.kt b/android/measure/src/main/java/sh/measure/android/config/Config.kt index 53bf9e250..6e5293c80 100644 --- a/android/measure/src/main/java/sh/measure/android/config/Config.kt +++ b/android/measure/src/main/java/sh/measure/android/config/Config.kt @@ -10,6 +10,7 @@ internal data class Config( override val trackHttpBody: Boolean = DefaultConfig.TRACK_HTTP_BODY, override val httpHeadersBlocklist: List = DefaultConfig.HTTP_HEADERS_BLOCKLIST, override val httpUrlBlocklist: List = DefaultConfig.HTTP_URL_BLOCKLIST, + override val httpUrlAllowlist: List = DefaultConfig.HTTP_URL_ALLOWLIST, override val trackActivityIntentData: Boolean = DefaultConfig.TRACK_ACTIVITY_INTENT_DATA, override val sessionSamplingRate: Float = DefaultConfig.SESSION_SAMPLING_RATE, ) : InternalConfig, IMeasureConfig { diff --git a/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt b/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt index e58734031..c8da8bf8a 100644 --- a/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt +++ b/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt @@ -68,6 +68,8 @@ internal class ConfigProviderImpl( get() = getMergedConfig { httpHeadersBlocklist } override val httpUrlBlocklist: List get() = getMergedConfig { httpUrlBlocklist } + override val httpUrlAllowlist: List + get() = getMergedConfig { httpUrlAllowlist } override val trackActivityIntentData: Boolean get() = getMergedConfig { trackActivityIntentData } override val sessionSamplingRate: Float @@ -95,13 +97,22 @@ internal class ConfigProviderImpl( if (contentType.isNullOrEmpty()) { return false } - if (combinedHttpUrlBlocklist.any { value -> value?.let { url.contains(it, ignoreCase = true) } == true }) { + + // make sure no body is tracked if the URL is not tracked + if (!shouldTrackHttpUrl(url)) { return false } + return httpContentTypeAllowlist.any { contentType.startsWith(it, ignoreCase = true) } } override fun shouldTrackHttpUrl(url: String): Boolean { + // If the allowlist is not empty, then only allow the URLs that are in the allowlist. + if (httpUrlAllowlist.isNotEmpty()) { + return httpUrlAllowlist.any { url.contains(it, ignoreCase = true) } + } + + // If the allowlist is empty, then block the URLs that are in the blocklist. return !combinedHttpUrlBlocklist.any { value -> value?.let { url.contains(it, ignoreCase = true) } ?: false } diff --git a/android/measure/src/main/java/sh/measure/android/config/DefaultConfig.kt b/android/measure/src/main/java/sh/measure/android/config/DefaultConfig.kt index 074ee2825..1cda23104 100644 --- a/android/measure/src/main/java/sh/measure/android/config/DefaultConfig.kt +++ b/android/measure/src/main/java/sh/measure/android/config/DefaultConfig.kt @@ -8,6 +8,7 @@ internal object DefaultConfig { const val TRACK_HTTP_BODY: Boolean = false val HTTP_HEADERS_BLOCKLIST: List = emptyList() val HTTP_URL_BLOCKLIST: List = emptyList() + val HTTP_URL_ALLOWLIST: List = emptyList() const val TRACK_ACTIVITY_INTENT_DATA: Boolean = false const val SESSION_SAMPLING_RATE: Float = 1.0f } diff --git a/android/measure/src/main/java/sh/measure/android/config/MeasureConfig.kt b/android/measure/src/main/java/sh/measure/android/config/MeasureConfig.kt index d0a0f815f..a7c8d853e 100644 --- a/android/measure/src/main/java/sh/measure/android/config/MeasureConfig.kt +++ b/android/measure/src/main/java/sh/measure/android/config/MeasureConfig.kt @@ -11,6 +11,7 @@ internal interface IMeasureConfig { val trackHttpBody: Boolean val httpHeadersBlocklist: List val httpUrlBlocklist: List + val httpUrlAllowlist: List val trackActivityIntentData: Boolean val sessionSamplingRate: Float } @@ -60,11 +61,13 @@ class MeasureConfig( /** * Allows disabling collection of `http` events for certain URLs. This is useful to setup if you do not - * want to collect data for certain endpoints or third party domains. + * want to collect data for certain endpoints. * * The check is made using [String.contains] to see if the URL contains any of the strings in * the list. * + * Note that this config is ignored if [httpUrlAllowlist] is set. + * * Example: * * ```kotlin @@ -79,6 +82,26 @@ class MeasureConfig( */ override val httpUrlBlocklist: List = DefaultConfig.HTTP_URL_BLOCKLIST, + /** + * Allows enabling collection of `http` events for only certain URLs. This is useful to setup if you do not + * want to collect data for all endpoints except for a few. + * + * The check is made using [String.contains] to see if the URL contains any of the strings in + * the list. + * + * Example: + * + * ```kotlin + * MeasureConfig( + * httpUrlAllowlist = listOf( + * "example.com", // enables a domain + * "api.example.com", // enable a subdomain + * "example.com/order" // enable a particular path + * ) + * ) + */ + override val httpUrlAllowlist: List = DefaultConfig.HTTP_URL_ALLOWLIST, + /** * Whether to capture intent data used to launch an Activity. Defaults to `false`. */ diff --git a/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt b/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt index 3a3b3391b..090f34910 100644 --- a/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt +++ b/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt @@ -72,18 +72,27 @@ class ConfigProviderTest { } @Test - fun `shouldTrackHttpBody returns true for a allowed URL and content type`() { - val url = "example.com" + fun `shouldTrackHttpBody returns true for a URL not blocked and allowed content type`() { val contentType = "application/json" val configProvider = ConfigProviderImpl( defaultConfig = Config(httpUrlBlocklist = listOf("allowed.com")), configLoader = configLoader, ) - Assert.assertTrue(configProvider.shouldTrackHttpBody(url, contentType)) + Assert.assertTrue(configProvider.shouldTrackHttpBody("example.com", contentType)) } @Test - fun `shouldTrackHttpBody returns false for a disallowed URL`() { + fun `shouldTrackHttpBody returns true for a allowed URL and content type`() { + val contentType = "application/json" + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com")), + configLoader = configLoader, + ) + Assert.assertTrue(configProvider.shouldTrackHttpBody("allowed.com", contentType)) + } + + @Test + fun `shouldTrackHttpBody returns false for a blocked URL`() { val url = "example.com" val contentType = "application/json" val configProvider = ConfigProviderImpl( @@ -93,6 +102,16 @@ class ConfigProviderTest { Assert.assertFalse(configProvider.shouldTrackHttpBody(url, contentType)) } + @Test + fun `shouldTrackHttpBody returns false URL not part of allowList`() { + val contentType = "application/json" + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com")), + configLoader = configLoader, + ) + Assert.assertFalse(configProvider.shouldTrackHttpBody("notinallowlist.com", contentType)) + } + @Test fun `shouldTrackHttpBody returns false for a disallowed content type`() { val url = "https://example.com/" @@ -140,4 +159,43 @@ class ConfigProviderTest { ) Assert.assertFalse(configProvider.shouldTrackHttpHeader("key1")) } + + @Test + fun `shouldTrackHttpUrl given urlAllowlist and urlBlocklist are empty, returns true for any url`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlAllowlist = emptyList(), httpUrlBlocklist = emptyList()), + configLoader = configLoader, + ) + Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/")) + } + + @Test + fun `shouldTrackHttpUrl given urlAllowlist is not empty, returns true if url is part of allowlist`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com")), + configLoader = configLoader, + ) + Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/")) + Assert.assertFalse(configProvider.shouldTrackHttpUrl("https://blocked.com/")) + } + + @Test + fun `shouldTrackHttpUrl given urlAllowlist is empty, urlBlocklist is not empty, returns true if url is not part of blocklist`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlBlocklist = listOf("blocked.com"), httpUrlAllowlist = emptyList()), + configLoader = configLoader, + ) + Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/")) + Assert.assertFalse(configProvider.shouldTrackHttpUrl("https://blocked.com/")) + } + + @Test + fun `shouldTrackHttpUrl given both urlAllowlist and urlBlocklist are not empty, considers only the allowlist`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com", "unblocked.com"), httpUrlBlocklist = listOf("unblocked.com")), + configLoader = configLoader, + ) + Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/")) + Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://unblocked.com/")) + } } diff --git a/android/measure/src/test/java/sh/measure/android/fakes/FakeConfigProvider.kt b/android/measure/src/test/java/sh/measure/android/fakes/FakeConfigProvider.kt index f58c71bf7..e77b4e81b 100644 --- a/android/measure/src/test/java/sh/measure/android/fakes/FakeConfigProvider.kt +++ b/android/measure/src/test/java/sh/measure/android/fakes/FakeConfigProvider.kt @@ -19,6 +19,7 @@ internal class FakeConfigProvider : ConfigProvider { override var trackHttpBody: Boolean = false override var httpHeadersBlocklist: List = emptyList() override var httpUrlBlocklist: List = emptyList() + override var httpUrlAllowlist: List = emptyList() override var trackActivityIntentData: Boolean = false override var sessionSamplingRate: Float = 1.0f override var maxAttachmentSizeInEventsBatchInBytes: Int = 3 diff --git a/docs/android/configuration-options.md b/docs/android/configuration-options.md index 8eaba6c79..10a367bb6 100644 --- a/docs/android/configuration-options.md +++ b/docs/android/configuration-options.md @@ -27,6 +27,7 @@ Measure.init( * [**trackScreenshotOnCrash**](#trackScreenshotOnCrash) * [**screenshotMaskLevel**](#screenshotMaskLevel) * [**httpUrlBlocklist**](#httpUrlBlocklist) +* [**httpUrlAllowlist**](#httpUrlAllowlist) * [**trackHttpHeaders**](#trackHttpHeaders) * [**httpHeadersBlocklist**](#httpHeadersBlocklist) * [**trackHttpBody**](#trackHttpBody) @@ -132,6 +133,20 @@ Allows enabling/disabling capturing of HTTP request and response body. Disabled Allows disabling collection of `http` events for certain URLs. This is useful to setup if you do not want to collect data for certain endpoints or third party domains. By default, no URLs are blocked. +Note that this list is used only if `httpUrlAllowlist` is empty. + +The check is made using [String.contains] to see if the URL contains any of the strings in +the list. + +## `httpUrlAllowlist` + +Allows enabling collection of `http` events only for certain URLs. This is useful to setup if you want +to collect data only for certain endpoints or third party domains. If this list is empty, `httpUrlBlocklist` is +considered. By default, this list is empty. + +The check is made using [String.contains] to see if the URL contains any of the strings in +the list. + ## `trackActivityIntentData` Android [Intent](https://developer.android.com/reference/android/content/Intent#standard-extra-data) From 5524b7397378d82a3202af041cf6d8857e3832bf Mon Sep 17 00:00:00 2001 From: Abhay Sood Date: Fri, 9 Aug 2024 19:03:17 +0530 Subject: [PATCH 2/2] fix(android): respect http configs for trackHttpBody and trackHttpHeaders --- .../measure/android/config/ConfigProvider.kt | 7 ++++ .../android/config/ConfigProviderTest.kt | 37 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt b/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt index c8da8bf8a..898fca5dd 100644 --- a/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt +++ b/android/measure/src/main/java/sh/measure/android/config/ConfigProvider.kt @@ -94,6 +94,10 @@ internal class ConfigProviderImpl( get() = getMergedConfig { userDefinedAttributeKeyWithSpaces } override fun shouldTrackHttpBody(url: String, contentType: String?): Boolean { + if (!trackHttpBody) { + return false + } + if (contentType.isNullOrEmpty()) { return false } @@ -119,6 +123,9 @@ internal class ConfigProviderImpl( } override fun shouldTrackHttpHeader(key: String): Boolean { + if (!trackHttpHeaders) { + return false + } return !combinedHttpHeadersBlocklist.any { key.contains(it, ignoreCase = true) } } diff --git a/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt b/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt index 090f34910..cb1856293 100644 --- a/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt +++ b/android/measure/src/test/java/sh/measure/android/config/ConfigProviderTest.kt @@ -71,11 +71,23 @@ class ConfigProviderTest { assertEquals(true, configProvider.trackScreenshotOnCrash) } + @Test + fun `shouldTrackHttpBody returns false if trackHttpBody is set to false`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config( + trackHttpBody = false, + httpUrlAllowlist = listOf("example.com"), + ), + configLoader = configLoader, + ) + Assert.assertFalse(configProvider.shouldTrackHttpBody("example.com", "application/json")) + } + @Test fun `shouldTrackHttpBody returns true for a URL not blocked and allowed content type`() { val contentType = "application/json" val configProvider = ConfigProviderImpl( - defaultConfig = Config(httpUrlBlocklist = listOf("allowed.com")), + defaultConfig = Config(trackHttpBody = true, httpUrlBlocklist = listOf("allowed.com")), configLoader = configLoader, ) Assert.assertTrue(configProvider.shouldTrackHttpBody("example.com", contentType)) @@ -85,7 +97,7 @@ class ConfigProviderTest { fun `shouldTrackHttpBody returns true for a allowed URL and content type`() { val contentType = "application/json" val configProvider = ConfigProviderImpl( - defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com")), + defaultConfig = Config(trackHttpBody = true, httpUrlAllowlist = listOf("allowed.com")), configLoader = configLoader, ) Assert.assertTrue(configProvider.shouldTrackHttpBody("allowed.com", contentType)) @@ -142,10 +154,19 @@ class ConfigProviderTest { Assert.assertFalse(configProvider.shouldTrackHttpBody(url, contentType)) } + @Test + fun `shouldTrackHttpHeader returns false if trackHttpHeaders is set to false`() { + val configProvider = ConfigProviderImpl( + defaultConfig = Config(trackHttpHeaders = false), + configLoader = configLoader, + ) + Assert.assertFalse(configProvider.shouldTrackHttpHeader("key1")) + } + @Test fun `shouldTrackHttpHeader returns true for a allowed header`() { val configProvider = ConfigProviderImpl( - defaultConfig = Config(httpHeadersBlocklist = listOf("key1")), + defaultConfig = Config(trackHttpHeaders = true, httpHeadersBlocklist = listOf("key1")), configLoader = configLoader, ) Assert.assertTrue(configProvider.shouldTrackHttpHeader("key2")) @@ -182,7 +203,10 @@ class ConfigProviderTest { @Test fun `shouldTrackHttpUrl given urlAllowlist is empty, urlBlocklist is not empty, returns true if url is not part of blocklist`() { val configProvider = ConfigProviderImpl( - defaultConfig = Config(httpUrlBlocklist = listOf("blocked.com"), httpUrlAllowlist = emptyList()), + defaultConfig = Config( + httpUrlBlocklist = listOf("blocked.com"), + httpUrlAllowlist = emptyList(), + ), configLoader = configLoader, ) Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/")) @@ -192,7 +216,10 @@ class ConfigProviderTest { @Test fun `shouldTrackHttpUrl given both urlAllowlist and urlBlocklist are not empty, considers only the allowlist`() { val configProvider = ConfigProviderImpl( - defaultConfig = Config(httpUrlAllowlist = listOf("allowed.com", "unblocked.com"), httpUrlBlocklist = listOf("unblocked.com")), + defaultConfig = Config( + httpUrlAllowlist = listOf("allowed.com", "unblocked.com"), + httpUrlBlocklist = listOf("unblocked.com"), + ), configLoader = configLoader, ) Assert.assertTrue(configProvider.shouldTrackHttpUrl("https://allowed.com/"))