Skip to content

Commit af869e4

Browse files
committed
Extract 24hour optout submission reporter
1 parent fea6536 commit af869e4

File tree

3 files changed

+114
-80
lines changed

3 files changed

+114
-80
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.pir.impl.pixels
18+
19+
import com.duckduckgo.common.utils.CurrentTimeProvider
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.pir.impl.store.PirRepository
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import java.util.concurrent.TimeUnit
24+
import javax.inject.Inject
25+
import kotlin.math.abs
26+
27+
interface OptOut24HourSubmissionSuccessRateReporter {
28+
suspend fun attemptFirePixel()
29+
}
30+
31+
@ContributesBinding(AppScope::class)
32+
class RealOptOut24HourSubmissionSuccessRateReporter @Inject constructor(
33+
private val optOutSubmitRateCalculator: OptOutSubmitRateCalculator,
34+
private val pirRepository: PirRepository,
35+
private val currentTimeProvider: CurrentTimeProvider,
36+
private val pirPixelSender: PirPixelSender,
37+
) : OptOut24HourSubmissionSuccessRateReporter {
38+
override suspend fun attemptFirePixel() {
39+
val startDate = pirRepository.getCustomStatsPixelsLastSentMs()
40+
val now = currentTimeProvider.currentTimeMillis()
41+
42+
if (shouldFirePixel(startDate, now)) {
43+
val endDate = now - TimeUnit.HOURS.toMillis(24)
44+
val activeBrokers = pirRepository.getAllActiveBrokerObjects()
45+
val hasUserProfiles = pirRepository.getAllUserProfileQueries().isNotEmpty()
46+
47+
if (activeBrokers.isNotEmpty() && hasUserProfiles) {
48+
activeBrokers.forEach {
49+
val successRate = optOutSubmitRateCalculator.calculateOptOutSubmitRate(
50+
it.name,
51+
startDate,
52+
endDate,
53+
)
54+
55+
if (successRate != null) {
56+
pirPixelSender.reportBrokerCustomStateOptOutSubmitRate(
57+
brokerUrl = it.url,
58+
optOutSuccessRate = successRate,
59+
)
60+
}
61+
}
62+
63+
pirRepository.setCustomStatsPixelsLastSentMs(endDate)
64+
}
65+
}
66+
}
67+
68+
private fun shouldFirePixel(
69+
startDate: Long,
70+
now: Long,
71+
): Boolean {
72+
return if (startDate == 0L) {
73+
// IF first run, we emit the custom stats pixel
74+
true
75+
} else {
76+
// Else we check if at least 24 hours have passed since last emission
77+
val nowDiffFromStart = abs(now - startDate)
78+
nowDiffFromStart > TimeUnit.HOURS.toMillis(24)
79+
}
80+
}
81+
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/pixels/PirCustomStatsWorker.kt

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,76 +20,22 @@ import android.content.Context
2020
import androidx.work.CoroutineWorker
2121
import androidx.work.WorkerParameters
2222
import com.duckduckgo.anvil.annotations.ContributesWorker
23-
import com.duckduckgo.common.utils.CurrentTimeProvider
2423
import com.duckduckgo.di.scopes.AppScope
25-
import com.duckduckgo.pir.impl.store.PirRepository
26-
import java.util.concurrent.TimeUnit
2724
import javax.inject.Inject
28-
import kotlin.math.abs
2925

3026
@ContributesWorker(AppScope::class)
3127
class PirCustomStatsWorker(
3228
context: Context,
3329
workerParameters: WorkerParameters,
3430
) : CoroutineWorker(context, workerParameters) {
3531
@Inject
36-
lateinit var optOutSubmitRateCalculator: OptOutSubmitRateCalculator
37-
38-
@Inject
39-
lateinit var pirRepository: PirRepository
40-
41-
@Inject
42-
lateinit var currentTimeProvider: CurrentTimeProvider
43-
44-
@Inject
45-
lateinit var pirPixelSender: PirPixelSender
32+
lateinit var optOutSubmissionSuccessRateReporter: OptOut24HourSubmissionSuccessRateReporter
4633

4734
override suspend fun doWork(): Result {
48-
val startDate = pirRepository.getCustomStatsPixelsLastSentMs()
49-
val now = currentTimeProvider.currentTimeMillis()
50-
51-
if (shouldFirePixel(startDate, now)) {
52-
val endDate = now - TimeUnit.HOURS.toMillis(24)
53-
val activeBrokers = pirRepository.getAllActiveBrokerObjects()
54-
val hasUserProfiles = pirRepository.getAllUserProfileQueries().isNotEmpty()
55-
56-
if (activeBrokers.isNotEmpty() && hasUserProfiles) {
57-
activeBrokers.forEach {
58-
val successRate = optOutSubmitRateCalculator.calculateOptOutSubmitRate(
59-
it.name,
60-
startDate,
61-
endDate,
62-
)
63-
64-
if (successRate != null) {
65-
pirPixelSender.reportBrokerCustomStateOptOutSubmitRate(
66-
brokerUrl = it.url,
67-
optOutSuccessRate = successRate,
68-
)
69-
}
70-
}
71-
72-
pirRepository.setCustomStatsPixelsLastSentMs(endDate)
73-
}
74-
}
75-
35+
optOutSubmissionSuccessRateReporter.attemptFirePixel()
7636
return Result.success()
7737
}
7838

79-
private fun shouldFirePixel(
80-
startDate: Long,
81-
now: Long,
82-
): Boolean {
83-
return if (startDate == 0L) {
84-
// IF first run, we emit the custom stats pixel
85-
true
86-
} else {
87-
// Else we check if at least 24 hours have passed since last emission
88-
val nowDiffFromStart = abs(now - startDate)
89-
nowDiffFromStart > TimeUnit.HOURS.toMillis(24)
90-
}
91-
}
92-
9339
companion object {
9440
const val TAG_PIR_RECURRING_CUSTOM_STATS = "TAG_PIR_RECURRING_CUSTOM_STATS"
9541
}

pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/pixels/PirCustomStatsWorkerTest.kt renamed to pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/pixels/RealOptOut24HourSubmissionSuccessRateReporterTest.kt

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.duckduckgo.pir.impl.pixels
1818

1919
import android.content.Context
20-
import androidx.work.testing.TestListenableWorkerBuilder
2120
import com.duckduckgo.common.test.CoroutineTestRule
2221
import com.duckduckgo.common.utils.CurrentTimeProvider
2322
import com.duckduckgo.pir.impl.models.Broker
@@ -35,7 +34,7 @@ import org.mockito.kotlin.verify
3534
import org.mockito.kotlin.whenever
3635
import java.util.concurrent.TimeUnit
3736

38-
class PirCustomStatsWorkerTest {
37+
class RealOptOut24HourSubmissionSuccessRateReporterTest {
3938

4039
@get:Rule
4140
var coroutineRule = CoroutineTestRule()
@@ -46,7 +45,7 @@ class PirCustomStatsWorkerTest {
4645
private val mockPirPixelSender: PirPixelSender = mock()
4746
private val context: Context = mock()
4847

49-
private lateinit var worker: PirCustomStatsWorker
48+
private lateinit var toTest: RealOptOut24HourSubmissionSuccessRateReporter
5049

5150
// Test data
5251
// January 15, 2024 10:00:00 UTC
@@ -89,13 +88,12 @@ class PirCustomStatsWorkerTest {
8988

9089
@Before
9190
fun setUp() {
92-
worker = TestListenableWorkerBuilder
93-
.from(context, PirCustomStatsWorker::class.java)
94-
.build()
95-
worker.pirRepository = mockPirRepository
96-
worker.currentTimeProvider = mockCurrentTimeProvider
97-
worker.optOutSubmitRateCalculator = mockOptOutSubmitRateCalculator
98-
worker.pirPixelSender = mockPirPixelSender
91+
toTest = RealOptOut24HourSubmissionSuccessRateReporter(
92+
optOutSubmitRateCalculator = mockOptOutSubmitRateCalculator,
93+
pirRepository = mockPirRepository,
94+
currentTimeProvider = mockCurrentTimeProvider,
95+
pirPixelSender = mockPirPixelSender,
96+
)
9997
}
10098

10199
@Test
@@ -113,7 +111,7 @@ class PirCustomStatsWorkerTest {
113111
),
114112
).thenReturn(0.5)
115113

116-
worker.doWork()
114+
toTest.attemptFirePixel()
117115

118116
verify(mockPirPixelSender).reportBrokerCustomStateOptOutSubmitRate(
119117
brokerUrl = testBroker1.url,
@@ -130,7 +128,8 @@ class PirCustomStatsWorkerTest {
130128
whenever(mockPirRepository.getCustomStatsPixelsLastSentMs()).thenReturn(startDate)
131129
whenever(mockCurrentTimeProvider.currentTimeMillis()).thenReturn(now)
132130

133-
worker.doWork()
131+
toTest.attemptFirePixel()
132+
134133
verify(mockPirRepository, never()).getAllActiveBrokerObjects()
135134
verify(mockPirPixelSender, never()).reportBrokerCustomStateOptOutSubmitRate(any(), any())
136135
verify(mockPirRepository, never()).setCustomStatsPixelsLastSentMs(any())
@@ -153,7 +152,8 @@ class PirCustomStatsWorkerTest {
153152
),
154153
).thenReturn(0.75)
155154

156-
worker.doWork()
155+
toTest.attemptFirePixel()
156+
157157
verify(mockPirPixelSender).reportBrokerCustomStateOptOutSubmitRate(
158158
brokerUrl = testBroker1.url,
159159
optOutSuccessRate = 0.75,
@@ -169,7 +169,8 @@ class PirCustomStatsWorkerTest {
169169
whenever(mockPirRepository.getCustomStatsPixelsLastSentMs()).thenReturn(startDate)
170170
whenever(mockCurrentTimeProvider.currentTimeMillis()).thenReturn(now)
171171

172-
worker.doWork()
172+
toTest.attemptFirePixel()
173+
173174
verify(mockPirRepository, never()).getAllActiveBrokerObjects()
174175
verify(mockPirPixelSender, never()).reportBrokerCustomStateOptOutSubmitRate(any(), any())
175176
}
@@ -182,7 +183,8 @@ class PirCustomStatsWorkerTest {
182183
whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(emptyList())
183184
whenever(mockPirRepository.getAllUserProfileQueries()).thenReturn(listOf(testProfileQuery))
184185

185-
worker.doWork()
186+
toTest.attemptFirePixel()
187+
186188
verify(mockOptOutSubmitRateCalculator, never()).calculateOptOutSubmitRate(
187189
any(),
188190
any(),
@@ -200,7 +202,8 @@ class PirCustomStatsWorkerTest {
200202
whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(listOf(testBroker1))
201203
whenever(mockPirRepository.getAllUserProfileQueries()).thenReturn(emptyList())
202204

203-
worker.doWork()
205+
toTest.attemptFirePixel()
206+
204207
verify(mockOptOutSubmitRateCalculator, never()).calculateOptOutSubmitRate(
205208
any(),
206209
any(),
@@ -239,7 +242,8 @@ class PirCustomStatsWorkerTest {
239242
)
240243
.thenReturn(0.8)
241244

242-
worker.doWork()
245+
toTest.attemptFirePixel()
246+
243247
verify(mockPirPixelSender).reportBrokerCustomStateOptOutSubmitRate(
244248
brokerUrl = testBroker1.url,
245249
optOutSuccessRate = 0.5,
@@ -280,7 +284,8 @@ class PirCustomStatsWorkerTest {
280284
)
281285
.thenReturn(null)
282286

283-
worker.doWork()
287+
toTest.attemptFirePixel()
288+
284289
verify(mockPirPixelSender).reportBrokerCustomStateOptOutSubmitRate(
285290
brokerUrl = testBroker1.url,
286291
optOutSuccessRate = 0.5,
@@ -307,7 +312,8 @@ class PirCustomStatsWorkerTest {
307312
),
308313
).thenReturn(null)
309314

310-
worker.doWork()
315+
toTest.attemptFirePixel()
316+
311317
verify(mockPirPixelSender, never()).reportBrokerCustomStateOptOutSubmitRate(any(), any())
312318
verify(mockPirRepository).setCustomStatsPixelsLastSentMs(now - twentyFourHours)
313319
}
@@ -330,7 +336,7 @@ class PirCustomStatsWorkerTest {
330336
),
331337
).thenReturn(0.5)
332338

333-
worker.doWork()
339+
toTest.attemptFirePixel()
334340

335341
verify(mockOptOutSubmitRateCalculator).calculateOptOutSubmitRate(
336342
brokerName = testBroker1.name,
@@ -369,7 +375,8 @@ class PirCustomStatsWorkerTest {
369375
)
370376
.thenReturn(0.9)
371377

372-
worker.doWork()
378+
toTest.attemptFirePixel()
379+
373380
verify(mockPirPixelSender, never()).reportBrokerCustomStateOptOutSubmitRate(
374381
brokerUrl = eq(testBroker1.url),
375382
optOutSuccessRate = any(),
@@ -389,7 +396,7 @@ class PirCustomStatsWorkerTest {
389396
whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(emptyList())
390397
whenever(mockPirRepository.getAllUserProfileQueries()).thenReturn(emptyList())
391398

392-
worker.doWork()
399+
toTest.attemptFirePixel()
393400

394401
verify(mockOptOutSubmitRateCalculator, never()).calculateOptOutSubmitRate(
395402
any(),
@@ -408,7 +415,7 @@ class PirCustomStatsWorkerTest {
408415
whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(emptyList())
409416
whenever(mockPirRepository.getAllUserProfileQueries()).thenReturn(listOf(testProfileQuery))
410417

411-
worker.doWork()
418+
toTest.attemptFirePixel()
412419

413420
verify(mockOptOutSubmitRateCalculator, never()).calculateOptOutSubmitRate(
414421
any(),
@@ -427,7 +434,7 @@ class PirCustomStatsWorkerTest {
427434
whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(listOf(testBroker1))
428435
whenever(mockPirRepository.getAllUserProfileQueries()).thenReturn(emptyList())
429436

430-
worker.doWork()
437+
toTest.attemptFirePixel()
431438

432439
verify(mockOptOutSubmitRateCalculator, never()).calculateOptOutSubmitRate(
433440
any(),

0 commit comments

Comments
 (0)