Skip to content

Commit

Permalink
Send app version on search pixel (#5287)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1174433894299346/1208776505075744/f

### Description
We want to add a new temporary triggered on searches where we’ll send
the app version.
There are few conditions, please refer to the task for more info.

Testing scenarios ->
https://app.asana.com/0/1174433894299346/1208801300909712/f
  • Loading branch information
malmstein authored Nov 26, 2024
1 parent bb302de commit 8fd697f
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release_nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
- name: Assemble the bundle
if: steps.check_for_changes.outputs.has_changes == 'true'
run: gradle bundleInternalRelease -PversionNameSuffix=-nightly -PuseUploadSigning -PlatestTag=${{ steps.get_latest_tag.outputs.latest_tag }}
run: gradle bundleInternalRelease -PversionNameSuffix=-nightly -PuseUploadSigning -PlatestTag=${{ steps.get_latest_tag.outputs.latest_tag }} -Pbuild-date-time

- name: Generate nightly version name
if: steps.check_for_changes.outputs.has_changes == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release_upload_play_store.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
destination-path: $HOME/jenkins_static/com.duckduckgo.mobile.android/

- name: Assemble the bundle
run: ./gradlew bundleRelease -PuseUploadSigning
run: ./gradlew bundleRelease -PuseUploadSigning -Pbuild-date-time

- name: Capture App Bundle Path
id: capture_output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface AppBuildConfig {
val model: String
val deviceLocale: Locale
val isDefaultVariantForced: Boolean
val buildDateTimeMillis: Long

/**
* You should call [variantName] in a background thread
Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ android {
} else {
buildConfigField "boolean", "FORCE_DEFAULT_VARIANT", "false"
}
if (project.hasProperty('build-date-time')) {
buildConfigField "long", "BUILD_DATE_MILLIS", "${System.currentTimeMillis()}"
} else {
buildConfigField "long", "BUILD_DATE_MILLIS", "0"
}

namespace 'com.duckduckgo.app.browser'
}
buildFeatures {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 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.trafficquality

import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@ContributesMultibinding(AppScope::class)
class AndroidAppVersionPixelSender @Inject constructor(
private val appVersionProvider: QualityAppVersionProvider,
private val pixel: Pixel,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) : AtbLifecyclePlugin {

override fun onSearchRetentionAtbRefreshed(oldAtb: String, newAtb: String) {
coroutineScope.launch(dispatcherProvider.io()) {
val params = mutableMapOf<String, String>()
params[PARAM_APP_VERSION] = appVersionProvider.provide()
pixel.fire(AppPixelName.APP_VERSION_AT_SEARCH_TIME, params)
}
}

companion object {
internal const val PARAM_APP_VERSION = "app_version"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2024 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.trafficquality

import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
import javax.inject.Inject

interface QualityAppVersionProvider {
fun provide(): String
}

@ContributesBinding(AppScope::class)
class RealQualityAppVersionProvider @Inject constructor(private val appBuildConfig: AppBuildConfig) : QualityAppVersionProvider {
override fun provide(): String {
val appBuildDateMillis = appBuildConfig.buildDateTimeMillis

if (appBuildDateMillis == 0L) {
return APP_VERSION_QUALITY_DEFAULT_VALUE
}

val appBuildDate = LocalDateTime.ofEpochSecond(appBuildDateMillis / 1000, 0, ZoneOffset.UTC)
val now = LocalDateTime.now(ZoneOffset.UTC)
val daysSinceBuild = ChronoUnit.DAYS.between(appBuildDate, now)

if (daysSinceBuild < DAYS_AFTER_APP_BUILD_WITH_DEFAULT_VALUE) {
return APP_VERSION_QUALITY_DEFAULT_VALUE
}

if (daysSinceBuild > DAYS_FOR_APP_VERSION_LOGGING) {
return APP_VERSION_QUALITY_DEFAULT_VALUE
}

return appBuildConfig.versionName
}

companion object {
const val APP_VERSION_QUALITY_DEFAULT_VALUE = "other_versions"
const val DAYS_AFTER_APP_BUILD_WITH_DEFAULT_VALUE = 6
const val DAYS_FOR_APP_VERSION_LOGGING = DAYS_AFTER_APP_BUILD_WITH_DEFAULT_VALUE + 10
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ class RealAppBuildConfig @Inject constructor(
override val isPerformanceTest: Boolean = BuildConfig.IS_PERFORMANCE_TEST

override val isDefaultVariantForced: Boolean = BuildConfig.FORCE_DEFAULT_VARIANT

override val deviceLocale: Locale
get() = Locale.getDefault()

override val variantName: String?
get() = variantManager.get().getVariantKey()

override val buildDateTimeMillis: Long
get() = BuildConfig.BUILD_DATE_MILLIS
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
SitePermissionsPixelName.PERMISSION_DIALOG_IMPRESSION.pixelName to PixelParameter.removeAtb(),
SITE_NOT_WORKING_SHOWN.pixelName to PixelParameter.removeAtb(),
SITE_NOT_WORKING_WEBSITE_BROKEN.pixelName to PixelParameter.removeAtb(),
AppPixelName.APP_VERSION_AT_SEARCH_TIME.pixelName to PixelParameter.removeAll(),
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
URI_LOADED("m_uri_loaded"),

ERROR_PAGE_SHOWN("m_errorpageshown"),

APP_VERSION_AT_SEARCH_TIME("app_version_at_search_time"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.duckduckgo.app.browser.trafficquality

import com.duckduckgo.app.browser.trafficquality.RealQualityAppVersionProvider.Companion.APP_VERSION_QUALITY_DEFAULT_VALUE
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.test.CoroutineTestRule
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

class AndroidAppVersionPixelSenderTest {
@get:Rule
var coroutineRule = CoroutineTestRule()

private val mockAppVersionProvider = mock<QualityAppVersionProvider>()
private val mockPixel = mock<Pixel>()

private lateinit var pixelSender: AndroidAppVersionPixelSender

@Before
fun setup() {
pixelSender = AndroidAppVersionPixelSender(
mockAppVersionProvider,
mockPixel,
coroutineRule.testScope,
coroutineRule.testDispatcherProvider,
)
}

@Test
fun reportFeaturesEnabledOrDisabledWhenEnabledOrDisabled() = runTest {
whenever(mockAppVersionProvider.provide()).thenReturn(APP_VERSION_QUALITY_DEFAULT_VALUE)

pixelSender.onSearchRetentionAtbRefreshed("v123-1", "v123-2")

verify(mockPixel).fire(
AppPixelName.APP_VERSION_AT_SEARCH_TIME,
mapOf(
AndroidAppVersionPixelSender.PARAM_APP_VERSION to APP_VERSION_QUALITY_DEFAULT_VALUE,
),
)
verifyNoMoreInteractions(mockPixel)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.duckduckgo.app.browser.trafficquality

import com.duckduckgo.app.browser.trafficquality.RealQualityAppVersionProvider.Companion.APP_VERSION_QUALITY_DEFAULT_VALUE
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.appbuildconfig.api.BuildFlavor
import java.time.LocalDateTime
import java.time.ZoneId
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class RealQualityAppVersionProviderTest {

private val appBuildConfig = mock<AppBuildConfig>().apply {
whenever(this.flavor).thenReturn(BuildFlavor.PLAY)
}

private lateinit var testee: RealQualityAppVersionProvider

@Before
fun setup() {
testee = RealQualityAppVersionProvider(appBuildConfig)
}

@Test
fun whenBuildDateEmptyThenReturnDefault() {
whenever(appBuildConfig.buildDateTimeMillis).thenReturn(0L)
val appVersion = testee.provide()
assertTrue(appVersion == APP_VERSION_QUALITY_DEFAULT_VALUE)
}

@Test
fun whenBuildDateTodayThenReturnDefault() {
givenBuildDateDaysAgo(0)
val appVersion = testee.provide()
assertTrue(appVersion == APP_VERSION_QUALITY_DEFAULT_VALUE)
}

@Test
fun whenNotYetTimeToLogThenReturnDefault() {
givenBuildDateDaysAgo(2)
val appVersion = testee.provide()
assertTrue(appVersion == APP_VERSION_QUALITY_DEFAULT_VALUE)
}

@Test
fun whenTimeToLogThenReturnAppVersion() {
givenBuildDateDaysAgo(6)
val versionName = "5.212.0"
givenVersionName(versionName)
val appVersion = testee.provide()
assertTrue(appVersion == versionName)
}

@Test
fun whenTimeToLogAndNotOverLoggingPeriodThenReturnAppVersion() {
val versionName = "5.212.0"
givenVersionName(versionName)
givenBuildDateDaysAgo(8)
val appVersion = testee.provide()
assertTrue(appVersion == versionName)
}

@Test
fun whenTimeToLogAndOverLoggingPeriodThenReturnDefault() {
val versionName = "5.212.0"
givenVersionName(versionName)
givenBuildDateDaysAgo(20)
val appVersion = testee.provide()
assertTrue(appVersion == APP_VERSION_QUALITY_DEFAULT_VALUE)
}

@Test
fun whenTimeToLogAndJustOnLoggingPeriodThenReturnVersionName() {
val versionName = "5.212.0"
givenVersionName(versionName)
givenBuildDateDaysAgo(16)
val appVersion = testee.provide()
assertTrue(appVersion == versionName)
}

private fun givenBuildDateDaysAgo(days: Long) {
val daysAgo = LocalDateTime.now().minusDays(days).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
whenever(appBuildConfig.buildDateTimeMillis).thenReturn(daysAgo)
}

private fun givenVersionName(versionName: String) {
whenever(appBuildConfig.versionName).thenReturn(versionName)
}
}

0 comments on commit 8fd697f

Please sign in to comment.