Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ dependencies {

implementation project(':breakage-reporting-impl')

implementation project(':web-interference-detection-impl')

implementation project(':web-detection-impl')

implementation project(':dax-prompts-api')
implementation project(':dax-prompts-impl')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class RealBrokenSiteContext @Inject constructor(

override var jsPerformance: DoubleArray? = null

override var breakageData: String? = null

override fun onUserTriggeredRefresh() {
userRefreshCount++
}
Expand Down Expand Up @@ -69,4 +71,9 @@ class RealBrokenSiteContext @Inject constructor(
jsPerformance = recordedJsValues
logcat { "jsPerformance recorded as $performanceMetrics" }
}

override fun recordBreakageData(breakageData: String?) {
this.breakageData = breakageData
logcat { "breakageData recorded as $breakageData" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,12 @@ class BrokenSiteSubmitter @Inject constructor(
params[LOGIN_SITE] = brokenSite.loginSite.orEmpty()
}

val encodedParams = mapOf(
val encodedParams = mutableMapOf(
BLOCKED_TRACKERS_KEY to brokenSite.blockedTrackers,
SURROGATES_KEY to brokenSite.surrogates,
)
// breakageData is pre-encoded by content-scope-scripts, add to encodedParams to avoid re-encoding
brokenSite.breakageData?.let { encodedParams[BREAKAGE_DATA] = it }
runCatching {
if (toggle) {
val unnecessaryKeys = listOf(CATEGORY_KEY, DESCRIPTION_KEY, PROTECTIONS_STATE)
Expand Down Expand Up @@ -242,6 +244,7 @@ class BrokenSiteSubmitter @Inject constructor(
private const val BLOCKLIST_EXPERIMENT = "blockListExperiment"
private const val CONTENT_SCOPE_EXPERIMENTS = "contentScopeExperiments"
private const val DEBUG_FLAGS = "debugFlags"
private const val BREAKAGE_DATA = "breakageData"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4315,9 +4315,12 @@ class BrowserTabViewModel @Inject constructor(
val referrer = data.get("referrer") as? String
val sanitizedReferrer = referrer?.removeSurrounding("\"")
val isExternalLaunch = site?.isExternalLaunch ?: false
// breakageData is pre-encoded by content-scope-scripts, pass as-is without re-encoding
val breakageData = data.optString("breakageData").takeIf { it.isNotEmpty() }

site?.realBrokenSiteContext?.recordJsPerformance(jsPerformanceData)
site?.realBrokenSiteContext?.inferOpenerContext(sanitizedReferrer, isExternalLaunch)
site?.realBrokenSiteContext?.recordBreakageData(breakageData)
}

fun onHomeShown() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,25 @@ class RealBrokenSiteContextTest {

doubleArrayOf(123.45).contentEquals(testee.jsPerformance)
}

@Test
fun whenInitializedThenBreakageDataIsNull() {
assertNull(testee.breakageData)
}

@Test
fun whenBreakageDataIsRecordedThenItIsStored() {
val preEncodedBreakageData = "%7B%22test%22%3A%22value%22%7D"

testee.recordBreakageData(preEncodedBreakageData)

assertEquals(preEncodedBreakageData, testee.breakageData)
}

@Test
fun whenBreakageDataIsNullThenItRemainsNull() {
testee.recordBreakageData(null)

assertNull(testee.breakageData)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,34 @@ class BrokenSiteSubmitterTest {
assertEquals("flag1,flag2", params["debugFlags"])
}

@Test
fun whenBreakageDataIsNullThenEncodedParamsDoNotContainIt() = runTest {
val brokenSite = getBrokenSite()

testee.submitBrokenSiteFeedback(brokenSite, toggle = false)

val encodedParamsCaptor = argumentCaptor<Map<String, String>>()
verify(mockPixel).fire(eq(BROKEN_SITE_REPORT.pixelName), any(), encodedParamsCaptor.capture(), eq(Count))
val encodedParams = encodedParamsCaptor.firstValue

assertFalse(encodedParams.containsKey("breakageData"))
}

@Test
fun whenBreakageDataExistsThenItIsIncludedInEncodedParams() = runTest {
// Pre-encoded breakage data from content-scope-scripts
val preEncodedBreakageData = "%7B%22test%22%3A%22value%22%7D"
val brokenSite = getBrokenSite().copy(breakageData = preEncodedBreakageData)

testee.submitBrokenSiteFeedback(brokenSite, toggle = false)

val encodedParamsCaptor = argumentCaptor<Map<String, String>>()
verify(mockPixel).fire(eq(BROKEN_SITE_REPORT.pixelName), any(), encodedParamsCaptor.capture(), eq(Count))
val encodedParams = encodedParamsCaptor.firstValue

assertEquals(preEncodedBreakageData, encodedParams["breakageData"])
}

private fun assignToExperiment() {
val enrollmentDateET = ZonedDateTime.now(ZoneId.of("America/New_York")).toString()
testBlockListFeature.tdsNextExperimentTest().setRawStoredState(
Expand Down Expand Up @@ -674,6 +702,7 @@ class BrokenSiteSubmitterTest {
jsPerformance = null,
contentScopeExperiments = null,
debugFlags = null,
breakageData = null,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class BrokenSitesMultipleReportReferenceTest(private val testCase: MultipleRepor
jsPerformance = null,
contentScopeExperiments = null,
debugFlags = null,
breakageData = report.breakageData,
)

testee.submitBrokenSiteFeedback(brokenSite, toggle = false)
Expand Down Expand Up @@ -279,6 +280,7 @@ class BrokenSitesMultipleReportReferenceTest(private val testCase: MultipleRepor
val remoteConfigEtag: String?,
val remoteConfigVersion: String?,
val lastSentDay: String?,
val breakageData: String?,
)

data class UrlParam(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class BrokenSitesReferenceTest(private val testCase: TestCase) {
jsPerformance = listOf(123.45),
contentScopeExperiments = null,
debugFlags = null,
breakageData = testCase.breakageData,
)

testee.submitBrokenSiteFeedback(brokenSite, toggle = false)
Expand Down Expand Up @@ -250,6 +251,7 @@ class BrokenSitesReferenceTest(private val testCase: TestCase) {
val consentSelfTestFailed: String,
val remoteConfigEtag: String?,
val remoteConfigVersion: String?,
val breakageData: String?,
)

data class UrlParam(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ data class BrokenSite(
val jsPerformance: List<Double>?,
val contentScopeExperiments: List<Toggle>?,
val debugFlags: List<String>?,
val breakageData: String?,
) {
companion object {
const val SITE_TYPE_DESKTOP = "desktop"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ interface BrokenSiteContext {

var jsPerformance: DoubleArray?

var breakageData: String?

fun onUserTriggeredRefresh()
fun inferOpenerContext(
referrer: String?,
wasLaunchedExternally: Boolean,
)
fun recordJsPerformance(performanceMetrics: JSONArray)
fun recordBreakageData(breakageData: String?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class BrokenSiteData(
val userRefreshCount: Int,
val openerContext: BrokenSiteOpenerContext?,
val jsPerformance: DoubleArray?,
val breakageData: String?,
) {
enum class ReportFlow { MENU, DASHBOARD, TOGGLE_DASHBOARD, TOGGLE_MENU, RELOAD_THREE_TIMES_WITHIN_20_SECONDS }

Expand All @@ -59,6 +60,7 @@ data class BrokenSiteData(
val userRefreshCount = site?.realBrokenSiteContext?.userRefreshCount ?: 0
val openerContext = site?.realBrokenSiteContext?.openerContext
val jsPerformance = site?.realBrokenSiteContext?.jsPerformance
val breakageData = site?.realBrokenSiteContext?.breakageData
return BrokenSiteData(
url = url,
blockedTrackers = blockedTrackers,
Expand All @@ -75,6 +77,7 @@ data class BrokenSiteData(
userRefreshCount = userRefreshCount,
openerContext = openerContext,
jsPerformance = jsPerformance,
breakageData = breakageData,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor(
userRefreshCount = site.realBrokenSiteContext.userRefreshCount,
openerContext = site.realBrokenSiteContext.openerContext?.context,
jsPerformance = site.realBrokenSiteContext.jsPerformance?.toList(),
breakageData = site.realBrokenSiteContext.breakageData,
contentScopeExperiments = site.activeContentScopeExperiments,
debugFlags = site.debugFlags,
)
Expand Down Expand Up @@ -534,6 +535,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor(
userRefreshCount = site.realBrokenSiteContext.userRefreshCount,
openerContext = site.realBrokenSiteContext.openerContext?.context,
jsPerformance = site.realBrokenSiteContext.jsPerformance?.toList(),
breakageData = site.realBrokenSiteContext.breakageData,
contentScopeExperiments = site.activeContentScopeExperiments,
debugFlags = site.debugFlags,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class PrivacyDashboardHybridViewModelTest {
val brokenSiteContext: BrokenSiteContext = mock { brokenSiteContext ->
whenever(brokenSiteContext.userRefreshCount).thenReturn(userRefreshCount)
whenever(brokenSiteContext.jsPerformance).thenReturn(jsPerformance)
whenever(brokenSiteContext.breakageData).thenReturn(null)
}
whenever(site.realBrokenSiteContext).thenReturn(brokenSiteContext)
}
Expand Down Expand Up @@ -236,6 +237,7 @@ class PrivacyDashboardHybridViewModelTest {
jsPerformance = jsPerformance.toList(),
contentScopeExperiments = null,
debugFlags = null,
breakageData = null,
)

val isToggleReport = false
Expand Down Expand Up @@ -265,6 +267,7 @@ class PrivacyDashboardHybridViewModelTest {
val brokenSiteContext: BrokenSiteContext = mock { brokenSiteContext ->
whenever(brokenSiteContext.userRefreshCount).thenReturn(userRefreshCount)
whenever(brokenSiteContext.jsPerformance).thenReturn(jsPerformance)
whenever(brokenSiteContext.breakageData).thenReturn(null)
}
whenever(site.realBrokenSiteContext).thenReturn(brokenSiteContext)
}
Expand Down Expand Up @@ -300,6 +303,7 @@ class PrivacyDashboardHybridViewModelTest {
jsPerformance = jsPerformance.toList(),
contentScopeExperiments = listOf(mockToggle),
debugFlags = null,
breakageData = null,
)

val isToggleReport = false
Expand Down Expand Up @@ -329,6 +333,7 @@ class PrivacyDashboardHybridViewModelTest {
val brokenSiteContext: BrokenSiteContext = mock { brokenSiteContext ->
whenever(brokenSiteContext.userRefreshCount).thenReturn(userRefreshCount)
whenever(brokenSiteContext.jsPerformance).thenReturn(jsPerformance)
whenever(brokenSiteContext.breakageData).thenReturn(null)
}
whenever(site.realBrokenSiteContext).thenReturn(brokenSiteContext)
}
Expand Down Expand Up @@ -364,6 +369,73 @@ class PrivacyDashboardHybridViewModelTest {
jsPerformance = jsPerformance.toList(),
contentScopeExperiments = null,
debugFlags = debugFlags,
breakageData = null,
)

val isToggleReport = false

verify(brokenSiteSender).submitBrokenSiteFeedback(expectedBrokenSite, isToggleReport)
verify(pixel).fire(REPORT_BROKEN_SITE_SENT, mapOf("opener" to "dashboard"), type = Count)
}

@Test
fun whenUserClicksOnSubmitReportWithBreakageDataThenSubmitsReportWithBreakageData() = runTest {
val siteUrl = "https://example.com"
val userRefreshCount = 3
val jsPerformance = doubleArrayOf(1.0, 2.0, 3.0)
val breakageData = "%7B%22test%22%3A%22value%22%7D"

val site: Site = mock { site ->
whenever(site.uri).thenReturn(siteUrl.toUri())
whenever(site.url).thenReturn(siteUrl)
whenever(site.userAllowList).thenReturn(true)
whenever(site.isDesktopMode).thenReturn(false)
whenever(site.upgradedHttps).thenReturn(true)
whenever(site.consentManaged).thenReturn(true)
whenever(site.errorCodeEvents).thenReturn(listOf("401", "401", "500"))
whenever(site.activeContentScopeExperiments).thenReturn(null)
whenever(site.debugFlags).thenReturn(null)

val brokenSiteContext: BrokenSiteContext = mock { brokenSiteContext ->
whenever(brokenSiteContext.userRefreshCount).thenReturn(userRefreshCount)
whenever(brokenSiteContext.jsPerformance).thenReturn(jsPerformance)
whenever(brokenSiteContext.breakageData).thenReturn(breakageData)
}
whenever(site.realBrokenSiteContext).thenReturn(brokenSiteContext)
}

testee.onSiteChanged(site)

val category = "login"
val description = "I can't sign in!"
testee.onSubmitBrokenSiteReport(
payload = """{"category":"$category","description":"$description"}""",
reportFlow = DASHBOARD,
opener = DashboardOpener.DASHBOARD,
)

val expectedBrokenSite = BrokenSite(
category = category,
description = description,
siteUrl = siteUrl,
upgradeHttps = true,
blockedTrackers = "",
surrogates = "",
siteType = "mobile",
urlParametersRemoved = false,
consentManaged = true,
consentOptOutFailed = false,
consentSelfTestFailed = false,
errorCodes = """["401","401","500"]""",
httpErrorCodes = "",
loginSite = null,
reportFlow = DASHBOARD,
userRefreshCount = userRefreshCount,
openerContext = null,
jsPerformance = jsPerformance.toList(),
contentScopeExperiments = null,
debugFlags = null,
breakageData = breakageData,
)

val isToggleReport = false
Expand Down
1 change: 1 addition & 0 deletions web-detection/web-detection-impl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
52 changes: 52 additions & 0 deletions web-detection/web-detection-impl/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'com.squareup.anvil'
}

apply from: "$rootProject.projectDir/gradle/android-library.gradle"

android {
anvil {
generateDaggerFactories = true
}
namespace 'com.duckduckgo.webdetection.impl'
}

dependencies {
anvil project(path: ':anvil-compiler')
implementation project(path: ':anvil-annotations')
implementation project(path: ':di')
implementation project(path: ':common-utils')
implementation project(path: ':privacy-config-api')
implementation project(path: ':content-scope-scripts-api')

api AndroidX.dataStore.preferences

implementation Google.dagger

implementation KotlinX.coroutines.core

testImplementation(KotlinX.coroutines.test) {
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}

testImplementation "org.mockito.kotlin:mockito-kotlin:_"
testImplementation Testing.junit4
}
Loading
Loading