Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ class StatisticsInternalInfoView @JvmOverloads constructor(
}

binding.searchAtbSave.setOnClickListener {
store.searchRetentionAtb?.let {
store.searchRetentionAtb = binding.searchAtb.text
}
store.searchRetentionAtb = binding.searchAtb.text
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal screen bug fix: before making a search, there's no search retention atb, and override was not working

Toast.makeText(this.context, "Search Atb updated", Toast.LENGTH_SHORT).show()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2025 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.
*/

package com.duckduckgo.app.browser.search

import com.duckduckgo.app.attributed.metrics.api.AttributedMetric
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
import com.duckduckgo.app.attributed.metrics.api.EventStats
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import logcat.logcat
import javax.inject.Inject

@ContributesMultibinding(AppScope::class, AtbLifecyclePlugin::class)
@ContributesMultibinding(AppScope::class, AttributedMetric::class)
@SingleInstanceIn(AppScope::class)
class RealSearchAttributedMetric @Inject constructor(
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val attributedMetricClient: AttributedMetricClient,
private val userBrowserProperties: UserBrowserProperties,
) : AttributedMetric, AtbLifecyclePlugin {

companion object {
private const val EVENT_NAME = "ddg_search"
private const val FIRST_MONTH_PIXEL = "user_average_searches_past_week_first_month"
private const val PAST_WEEK_PIXEL_NAME = "user_average_searches_past_week"
private const val DAYS_WINDOW = 7
private const val FIRST_MONTH_DAY_THRESHOLD = 28 // we consider 1 month after 4 weeks
private val SEARCH_BUCKETS = arrayOf(5, 9) // TODO: default bucket, remote bucket implementation will happen in future PRs
}

override fun onSearchRetentionAtbRefreshed(
oldAtb: String,
newAtb: String,
) {
appCoroutineScope.launch(dispatcherProvider.io()) {
attributedMetricClient.collectEvent(EVENT_NAME)

if (oldAtb == newAtb) {
logcat(tag = "AttributedMetrics") {
"SearchCount7d: Skip emitting, atb not changed"
}
return@launch
}
if (shouldSendPixel().not()) {
logcat(tag = "AttributedMetrics") {
"SearchCount7d: Skip emitting, not enough data or no events"
}
return@launch
}
attributedMetricClient.emitMetric(this@RealSearchAttributedMetric)
}
}

override fun getPixelName(): String = when (userBrowserProperties.daysSinceInstalled()) {
in 0..FIRST_MONTH_DAY_THRESHOLD -> FIRST_MONTH_PIXEL
else -> PAST_WEEK_PIXEL_NAME
}

override suspend fun getMetricParameters(): Map<String, String> {
val stats = getEventStats()
val params = mutableMapOf(
"count" to getBucketValue(stats.rollingAverage.toInt()).toString(),
)
if (!hasCompleteDataWindow()) {
params["dayAverage"] = userBrowserProperties.daysSinceInstalled().toString()
}
return params
}

private fun getBucketValue(searches: Int): Int {
return SEARCH_BUCKETS.indexOfFirst { bucket -> searches <= bucket }.let { index ->
if (index == -1) SEARCH_BUCKETS.size else index
}
}

private suspend fun shouldSendPixel(): Boolean {
if (userBrowserProperties.daysSinceInstalled() == 0L) {
// installation day, we don't emit
return false
}

val eventStats = getEventStats()
if (eventStats.daysWithEvents == 0 || eventStats.rollingAverage == 0.0) {
// no events, nothing to emit
return false
}

return true
}

private suspend fun getEventStats(): EventStats {
val stats = if (hasCompleteDataWindow()) {
attributedMetricClient.getEventStats(EVENT_NAME, DAYS_WINDOW)
} else {
attributedMetricClient.getEventStats(EVENT_NAME, userBrowserProperties.daysSinceInstalled().toInt())
}

return stats
}

private fun hasCompleteDataWindow(): Boolean {
val daysSinceInstalled = userBrowserProperties.daysSinceInstalled().toInt()
return daysSinceInstalled >= DAYS_WINDOW
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2025 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.
*/

package com.duckduckgo.app.browser.search

import com.duckduckgo.app.attributed.metrics.api.AttributedMetric
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import logcat.logcat
import javax.inject.Inject

@ContributesMultibinding(AppScope::class, AtbLifecyclePlugin::class)
@ContributesMultibinding(AppScope::class, AttributedMetric::class)
@SingleInstanceIn(AppScope::class)
class RealSearchDaysAttributedMetric @Inject constructor(
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val attributedMetricClient: AttributedMetricClient,
private val userBrowserProperties: UserBrowserProperties,
) : AttributedMetric, AtbLifecyclePlugin {

companion object {
private const val EVENT_NAME = "ddg_search_days"
private const val PIXEL_NAME = "user_active_past_week"
private const val DAYS_WINDOW = 7
private val DAYS_BUCKETS = arrayOf(2, 4) // TODO: default bucket, remote bucket implementation will happen in future PRs
}

override fun onSearchRetentionAtbRefreshed(
oldAtb: String,
newAtb: String,
) {
appCoroutineScope.launch(dispatcherProvider.io()) {
attributedMetricClient.collectEvent(EVENT_NAME)
if (oldAtb == newAtb) {
logcat(tag = "AttributedMetrics") {
"SearchDays: Skip emitting atb not changed"
}
return@launch
}
if (shouldSendPixel().not()) {
logcat(tag = "AttributedMetrics") {
"SearchDays: Skip emitting, not enough data or no events"
}
return@launch
}
attributedMetricClient.emitMetric(this@RealSearchDaysAttributedMetric)
}
}

override fun getPixelName(): String = PIXEL_NAME

override suspend fun getMetricParameters(): Map<String, String> {
val daysSinceInstalled = userBrowserProperties.daysSinceInstalled().toInt()
val hasCompleteDataWindow = daysSinceInstalled >= DAYS_WINDOW
val stats = attributedMetricClient.getEventStats(EVENT_NAME, DAYS_WINDOW)
val params = mutableMapOf(
"days" to getBucketValue(stats.daysWithEvents).toString(),
)
if (!hasCompleteDataWindow) {
params["daysSinceInstalled"] = daysSinceInstalled.toString()
}
return params
}

private fun getBucketValue(days: Int): Int {
return DAYS_BUCKETS.indexOfFirst { bucket -> days <= bucket }.let { index ->
if (index == -1) DAYS_BUCKETS.size else index
}
}

private suspend fun shouldSendPixel(): Boolean {
if (userBrowserProperties.daysSinceInstalled() == 0L) {
// installation day, we don't emit
return false
}

val eventStats = attributedMetricClient.getEventStats(EVENT_NAME, DAYS_WINDOW)
if (eventStats.daysWithEvents == 0) {
// no events, nothing to emit
return false
}

return true
}
}
Loading
Loading