Skip to content

Commit 205e592

Browse files
authored
VPN: Add params to latency pixels (#6607)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211020399411579?focus=true ### Description Adds a few params to the VPN latency pixels that could help us investigate / verify the cause of latency increase ### Steps to test this PR - [x] Filter `Pixel url request:` in your logcat - [x] Enable VPN - [x] Wait for around 5 seconds - [x] Verify you see a `_latency_` pixel. - [x] Verify that that pixel contains the location and os15Above params. - [x] Verify that other pixels don’t have the additional params
1 parent c0018f2 commit 205e592

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.networkprotection.impl.pixels
18+
19+
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
20+
import com.duckduckgo.common.utils.plugins.pixel.PixelInterceptorPlugin
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_EXCELLENT_LATENCY
23+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_GOOD_LATENCY
24+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_MODERATE_LATENCY
25+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_POOR_LATENCY
26+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_TERRIBLE_LATENCY
27+
import com.duckduckgo.networkprotection.store.NetPGeoswitchingRepository
28+
import com.squareup.anvil.annotations.ContributesMultibinding
29+
import javax.inject.Inject
30+
import kotlinx.coroutines.runBlocking
31+
import okhttp3.Interceptor
32+
import okhttp3.Interceptor.Chain
33+
import okhttp3.Response
34+
35+
@ContributesMultibinding(
36+
scope = AppScope::class,
37+
boundType = PixelInterceptorPlugin::class,
38+
)
39+
class VpnLatencyPixelInterceptor @Inject constructor(
40+
private val netPGeoswitchingRepository: NetPGeoswitchingRepository,
41+
private val appBuildConfig: AppBuildConfig,
42+
) : PixelInterceptorPlugin, Interceptor {
43+
override fun getInterceptor(): Interceptor = this
44+
45+
override fun intercept(chain: Chain): Response {
46+
val request = chain.request().newBuilder()
47+
val pixel = chain.request().url.pathSegments.last()
48+
val url = if (LATENCY_PIXELS.any { pixel.startsWith(it) }) {
49+
chain.request().url.newBuilder()
50+
.addQueryParameter(PARAM_LOCATION, getLocationParamValue())
51+
.addQueryParameter(PARAM_OSABOVE15, isOsAbove15().toString())
52+
.build()
53+
} else {
54+
chain.request().url
55+
}
56+
57+
return chain.proceed(request.url(url).build())
58+
}
59+
60+
private fun getLocationParamValue(): String {
61+
return runBlocking {
62+
if (netPGeoswitchingRepository.getUserPreferredLocation().countryCode != null) {
63+
VALUE_LOCATION_CUSTOM
64+
} else {
65+
VALUE_LOCATION_NEAREST
66+
}
67+
}
68+
}
69+
70+
private fun isOsAbove15(): Boolean {
71+
return appBuildConfig.sdkInt >= 35 // API 35 (Android 15 / VANILLA_ICE_CREAM)
72+
}
73+
74+
companion object {
75+
private val LATENCY_PIXELS = listOf(
76+
NETP_REPORT_TERRIBLE_LATENCY.pixelName,
77+
NETP_REPORT_POOR_LATENCY.pixelName,
78+
NETP_REPORT_MODERATE_LATENCY.pixelName,
79+
NETP_REPORT_GOOD_LATENCY.pixelName,
80+
NETP_REPORT_EXCELLENT_LATENCY.pixelName,
81+
)
82+
private const val PARAM_OSABOVE15 = "os15Above"
83+
private const val PARAM_LOCATION = "location"
84+
private const val VALUE_LOCATION_CUSTOM = "custom"
85+
private const val VALUE_LOCATION_NEAREST = "nearest"
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.networkprotection.impl.pixels
18+
19+
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
20+
import com.duckduckgo.common.test.api.FakeChain
21+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_EXCELLENT_LATENCY
22+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_GOOD_LATENCY
23+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_MODERATE_LATENCY
24+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_POOR_LATENCY
25+
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_REPORT_TERRIBLE_LATENCY
26+
import com.duckduckgo.networkprotection.store.NetPGeoswitchingRepository
27+
import com.duckduckgo.networkprotection.store.NetPGeoswitchingRepository.UserPreferredLocation
28+
import kotlinx.coroutines.test.runTest
29+
import org.junit.Assert.assertEquals
30+
import org.junit.Assert.assertTrue
31+
import org.junit.Before
32+
import org.junit.Test
33+
import org.mockito.Mock
34+
import org.mockito.MockitoAnnotations
35+
import org.mockito.kotlin.whenever
36+
37+
class VpnLatencyPixelInterceptorTest {
38+
39+
@Mock
40+
private lateinit var netPGeoswitchingRepository: NetPGeoswitchingRepository
41+
42+
@Mock
43+
private lateinit var appBuildConfig: AppBuildConfig
44+
45+
private lateinit var testee: VpnLatencyPixelInterceptor
46+
47+
@Before
48+
fun setUp() {
49+
MockitoAnnotations.openMocks(this)
50+
testee = VpnLatencyPixelInterceptor(netPGeoswitchingRepository, appBuildConfig)
51+
}
52+
53+
@Test
54+
fun whenLatencyPixelWithCustomLocationAndOsAbove15ThenAddBothParameters() = runTest {
55+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
56+
UserPreferredLocation(countryCode = "US"),
57+
)
58+
whenever(appBuildConfig.sdkInt).thenReturn(35) // API 35 (Android 15)
59+
val pixelUrl = String.format(PIXEL_TEMPLATE, NETP_REPORT_TERRIBLE_LATENCY.pixelName)
60+
61+
val result = testee.intercept(FakeChain(pixelUrl))
62+
63+
val resultUrl = result.request.url
64+
assertEquals("custom", resultUrl.queryParameter("location"))
65+
assertEquals("true", resultUrl.queryParameter("os15Above"))
66+
}
67+
68+
@Test
69+
fun whenLatencyPixelWithNearestLocationAndOsBelow15ThenAddBothParameters() = runTest {
70+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
71+
UserPreferredLocation(),
72+
)
73+
whenever(appBuildConfig.sdkInt).thenReturn(34) // API 34 (Android 14)
74+
val pixelUrl = String.format(PIXEL_TEMPLATE, NETP_REPORT_POOR_LATENCY.pixelName)
75+
76+
val result = testee.intercept(FakeChain(pixelUrl))
77+
78+
val resultUrl = result.request.url
79+
assertEquals("nearest", resultUrl.queryParameter("location"))
80+
assertEquals("false", resultUrl.queryParameter("os15Above"))
81+
}
82+
83+
@Test
84+
fun whenLatencyPixelWithNullCountryCodeThenLocationIsNearest() = runTest {
85+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
86+
UserPreferredLocation(countryCode = null),
87+
)
88+
whenever(appBuildConfig.sdkInt).thenReturn(35) // API 35 (Android 15)
89+
val pixelUrl = String.format(PIXEL_TEMPLATE, NETP_REPORT_MODERATE_LATENCY.pixelName)
90+
91+
val result = testee.intercept(FakeChain(pixelUrl))
92+
93+
val resultUrl = result.request.url
94+
assertEquals("nearest", resultUrl.queryParameter("location"))
95+
assertEquals("true", resultUrl.queryParameter("os15Above"))
96+
}
97+
98+
@Test
99+
fun whenGoodLatencyPixelThenParametersAreAdded() = runTest {
100+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
101+
UserPreferredLocation(countryCode = "CA"),
102+
)
103+
whenever(appBuildConfig.sdkInt).thenReturn(35) // API 35 (Android 15)
104+
val pixelUrl = String.format(PIXEL_TEMPLATE, NETP_REPORT_GOOD_LATENCY.pixelName)
105+
106+
val result = testee.intercept(FakeChain(pixelUrl))
107+
108+
val resultUrl = result.request.url
109+
assertEquals("custom", resultUrl.queryParameter("location"))
110+
assertEquals("true", resultUrl.queryParameter("os15Above"))
111+
}
112+
113+
@Test
114+
fun whenExcellentLatencyPixelThenParametersAreAdded() = runTest {
115+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
116+
UserPreferredLocation(countryCode = "FR"),
117+
)
118+
whenever(appBuildConfig.sdkInt).thenReturn(34) // API 34 (Android 14)
119+
val pixelUrl = String.format(PIXEL_TEMPLATE, NETP_REPORT_EXCELLENT_LATENCY.pixelName)
120+
121+
val result = testee.intercept(FakeChain(pixelUrl))
122+
123+
val resultUrl = result.request.url
124+
assertEquals("custom", resultUrl.queryParameter("location"))
125+
assertEquals("false", resultUrl.queryParameter("os15Above"))
126+
}
127+
128+
@Test
129+
fun whenNonLatencyPixelThenNoParametersAdded() = runTest {
130+
val pixelUrl = String.format(PIXEL_TEMPLATE, "m_netp_ev_enabled_d")
131+
132+
val result = testee.intercept(FakeChain(pixelUrl))
133+
134+
val resultUrl = result.request.url
135+
assertEquals(null, resultUrl.queryParameter("location"))
136+
assertEquals(null, resultUrl.queryParameter("os15Above"))
137+
}
138+
139+
@Test
140+
fun verifyOriginalUrlIsPreservedWithAdditionalParameters() = runTest {
141+
whenever(netPGeoswitchingRepository.getUserPreferredLocation()).thenReturn(
142+
UserPreferredLocation(),
143+
)
144+
whenever(appBuildConfig.sdkInt).thenReturn(35) // API 35 (Android 15)
145+
val originalUrl = "https://improving.duckduckgo.com/t/${NETP_REPORT_TERRIBLE_LATENCY.pixelName}_android_phone?appVersion=5.135.0&test=1"
146+
147+
val result = testee.intercept(FakeChain(originalUrl))
148+
149+
val resultUrl = result.request.url
150+
// Check that original parameters are preserved
151+
assertEquals("5.135.0", resultUrl.queryParameter("appVersion"))
152+
assertEquals("1", resultUrl.queryParameter("test"))
153+
// Check that new parameters are added
154+
assertEquals("nearest", resultUrl.queryParameter("location"))
155+
assertEquals("true", resultUrl.queryParameter("os15Above"))
156+
// Check that the base URL is preserved
157+
assertTrue(resultUrl.toString().contains("improving.duckduckgo.com"))
158+
assertTrue(resultUrl.toString().contains(NETP_REPORT_TERRIBLE_LATENCY.pixelName))
159+
}
160+
161+
companion object {
162+
private const val PIXEL_TEMPLATE = "https://improving.duckduckgo.com/t/%s_android_phone?appVersion=5.135.0&test=1"
163+
}
164+
}

0 commit comments

Comments
 (0)