From 5e3063dac01c65eeca3eec09693909ae428e691b Mon Sep 17 00:00:00 2001 From: Aitor Viana Date: Fri, 22 Nov 2024 15:38:01 +0000 Subject: [PATCH] Add AppTP WAU pixel (#5292) Task/Issue URL: https://app.asana.com/0/69071770703008/1208808769852413/f ### Description Add a pixel for AppTP MAU. The pixel fires at most once every 28 days ### Steps to test this PR You may want to change the polici to `REPLACE` in `scheduleDeviceShieldStatusReporting()` to trigger the pixel on every app `onCreate` _Test_ - [x] clean install from this branch and launch the app - [x] enable AppTP - [x] force close the app and re-open - [x] verify the `m_atp_ev_enabled_monthly` is sent - [x] force close the app and re-open - [x] verify the `m_atp_ev_enabled_monthly` is NOT sent - [x] move the device date no more that 28d from now - [x] force close the app and re-open - [x] verify the `m_atp_ev_enabled_monthly` is NOT sent - [x] move the device date no more that 29d from now - [x] force close the app and re-open - [x] verify the `m_atp_ev_enabled_monthly` is sent --- .../vpn/pixels/DeviceShieldPixelNames.kt | 1 + .../android/vpn/pixels/DeviceShieldPixels.kt | 43 +++++++++++++++++++ .../vpn/pixels/RealDeviceShieldPixelsTest.kt | 42 +++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt index b071d578ecec..009c6dbe80de 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt @@ -39,6 +39,7 @@ enum class DeviceShieldPixelNames(override val pixelName: String, val enqueue: B ATP_DISABLE_DAILY("m_atp_ev_disabled_d"), ATP_ENABLE_UNIQUE("m_atp_ev_enabled_u"), + ATP_ENABLE_MONTHLY("m_atp_ev_enabled_monthly"), ATP_ENABLE_FROM_REMINDER_NOTIFICATION_UNIQUE("m_atp_ev_enabled_reminder_notification_u"), ATP_ENABLE_FROM_REMINDER_NOTIFICATION_DAILY("m_atp_ev_enabled_reminder_notification_d"), ATP_ENABLE_FROM_REMINDER_NOTIFICATION("m_atp_ev_enabled_reminder_notification_c"), diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt index 09a213d58733..73429a6738c5 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt @@ -24,10 +24,13 @@ import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn import java.time.Instant +import java.time.LocalDate import java.time.ZoneOffset import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit import java.util.* import javax.inject.Inject +import kotlin.math.absoluteValue interface DeviceShieldPixels { /** This pixel will be unique on a given day, no matter how many times we call this fun */ @@ -414,6 +417,7 @@ class RealDeviceShieldPixels @Inject constructor( override fun reportEnabled() { tryToFireUniquePixel(DeviceShieldPixelNames.ATP_ENABLE_UNIQUE) tryToFireDailyPixel(DeviceShieldPixelNames.ATP_ENABLE_DAILY) + tryToFireMonthlyPixel(DeviceShieldPixelNames.ATP_ENABLE_MONTHLY) } override fun reportDisabled() { @@ -934,6 +938,45 @@ class RealDeviceShieldPixels @Inject constructor( } } + private fun tryToFireMonthlyPixel( + pixel: DeviceShieldPixelNames, + payload: Map = emptyMap(), + ) { + tryToFireMonthlyPixel(pixel.pixelName, payload, pixel.enqueue) + } + + private fun tryToFireMonthlyPixel( + pixelName: String, + payload: Map = emptyMap(), + enqueue: Boolean = false, + ) { + fun isMoreThan28DaysApart(date1: String, date2: String): Boolean { + // Parse the strings into LocalDate objects + val firstDate = LocalDate.parse(date1) + val secondDate = LocalDate.parse(date2) + + // Calculate the difference in days + val daysBetween = ChronoUnit.DAYS.between(firstDate, secondDate).absoluteValue + + // Check if the difference is more than 28 days + return daysBetween > 28 + } + + val now = getUtcIsoLocalDate() + val timestamp = preferences.getString(pixelName.appendTimestampSuffix(), null) + + // check if pixel was already sent in the current day + if (timestamp == null || isMoreThan28DaysApart(now, timestamp)) { + if (enqueue) { + this.pixel.enqueueFire(pixelName, payload) + .also { preferences.edit { putString(pixelName.appendTimestampSuffix(), now) } } + } else { + this.pixel.fire(pixelName, payload) + .also { preferences.edit { putString(pixelName.appendTimestampSuffix(), now) } } + } + } + } + private fun tryToFireUniquePixel( pixel: DeviceShieldPixelNames, tag: String? = null, diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/RealDeviceShieldPixelsTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/RealDeviceShieldPixelsTest.kt index 0a093e0c4860..c71a7b48ff32 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/RealDeviceShieldPixelsTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/RealDeviceShieldPixelsTest.kt @@ -16,9 +16,14 @@ package com.duckduckgo.mobile.android.vpn.pixels +import androidx.core.content.edit import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.api.InMemorySharedPreferences import com.duckduckgo.data.store.api.SharedPreferencesProvider +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit import java.util.* import org.junit.Before import org.junit.Test @@ -28,12 +33,12 @@ class RealDeviceShieldPixelsTest { private val pixel = mock() private val sharedPreferencesProvider = mock() + private val prefs = InMemorySharedPreferences() lateinit var deviceShieldPixels: DeviceShieldPixels @Before fun setup() { - val prefs = InMemorySharedPreferences() whenever( sharedPreferencesProvider.getSharedPreferences(eq("com.duckduckgo.mobile.android.device.shield.pixels"), eq(true), eq(true)), ).thenReturn(prefs) @@ -60,15 +65,48 @@ class RealDeviceShieldPixelsTest { } @Test - fun whenReportEnableThenFireUniqueAndDailyPixel() { + fun whenReportEnableThenFireUniqueAndMonthlyAndDailyPixel() { deviceShieldPixels.reportEnabled() deviceShieldPixels.reportEnabled() verify(pixel).fire(DeviceShieldPixelNames.ATP_ENABLE_UNIQUE) verify(pixel).fire(DeviceShieldPixelNames.ATP_ENABLE_DAILY.pixelName) + verify(pixel).fire(DeviceShieldPixelNames.ATP_ENABLE_MONTHLY.pixelName) verifyNoMoreInteractions(pixel) } + @Test + fun whenReportExactly28DaysApartThenDoNotFireMonthlyPixel() { + val pixelName = DeviceShieldPixelNames.ATP_ENABLE_MONTHLY.pixelName + + deviceShieldPixels.reportEnabled() + + val pastDate = Instant.now() + .minus(28, ChronoUnit.DAYS) + .atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE) + prefs.edit(true) { putString("${pixelName}_timestamp", pastDate) } + + deviceShieldPixels.reportEnabled() + + verify(pixel).fire(pixelName) + } + + @Test + fun whenReportEnableMoreThan28DaysApartReportMonthlyPixel() { + val pixelName = DeviceShieldPixelNames.ATP_ENABLE_MONTHLY.pixelName + + deviceShieldPixels.reportEnabled() + + val pastDate = Instant.now() + .minus(29, ChronoUnit.DAYS) + .atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE) + prefs.edit(true) { putString("${pixelName}_timestamp", pastDate) } + + deviceShieldPixels.reportEnabled() + + verify(pixel, times(2)).fire(pixelName) + } + @Test fun whenReportDisableThenFireDailyPixel() { deviceShieldPixels.reportDisabled()