Skip to content

Commit 755441c

Browse files
committed
Integrate 7 day confirmation pixel
1 parent d270135 commit 755441c

File tree

7 files changed

+169
-3
lines changed

7 files changed

+169
-3
lines changed

pir/pir-impl/schemas/com.duckduckgo.pir.impl.store.PirDatabase/15.json

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"formatVersion": 1,
33
"database": {
44
"version": 15,
5-
"identityHash": "584648b8b3065521786fb33f44214f2e",
5+
"identityHash": "a5782d19654dee4cad932b458037bb37",
66
"entities": [
77
{
88
"tableName": "pir_broker_json_etag",
@@ -696,7 +696,7 @@
696696
},
697697
{
698698
"tableName": "pir_optout_job_record",
699-
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`extractedProfileId` INTEGER NOT NULL, `brokerName` TEXT NOT NULL, `userProfileId` INTEGER NOT NULL, `status` TEXT NOT NULL, `attemptCount` INTEGER NOT NULL, `lastOptOutAttemptDate` INTEGER, `optOutRequestedDate` INTEGER NOT NULL, `optOutRemovedDate` INTEGER NOT NULL, `deprecated` INTEGER NOT NULL, `dateCreatedInMillis` INTEGER NOT NULL, PRIMARY KEY(`extractedProfileId`))",
699+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`extractedProfileId` INTEGER NOT NULL, `brokerName` TEXT NOT NULL, `userProfileId` INTEGER NOT NULL, `status` TEXT NOT NULL, `attemptCount` INTEGER NOT NULL, `lastOptOutAttemptDate` INTEGER, `optOutRequestedDate` INTEGER NOT NULL, `optOutRemovedDate` INTEGER NOT NULL, `deprecated` INTEGER NOT NULL, `dateCreatedInMillis` INTEGER NOT NULL, `reporting_sevenDayConfirmationReportSentDateMs` INTEGER NOT NULL, `reporting_fourteenDayConfirmationReportSentDateMs` INTEGER NOT NULL, `reporting_twentyOneDayConfirmationReportSentDateMs` INTEGER NOT NULL, `reporting_fortyTwoDayConfirmationReportSentDateMs` INTEGER NOT NULL, PRIMARY KEY(`extractedProfileId`))",
700700
"fields": [
701701
{
702702
"fieldPath": "extractedProfileId",
@@ -757,6 +757,30 @@
757757
"columnName": "dateCreatedInMillis",
758758
"affinity": "INTEGER",
759759
"notNull": true
760+
},
761+
{
762+
"fieldPath": "reporting.sevenDayConfirmationReportSentDateMs",
763+
"columnName": "reporting_sevenDayConfirmationReportSentDateMs",
764+
"affinity": "INTEGER",
765+
"notNull": true
766+
},
767+
{
768+
"fieldPath": "reporting.fourteenDayConfirmationReportSentDateMs",
769+
"columnName": "reporting_fourteenDayConfirmationReportSentDateMs",
770+
"affinity": "INTEGER",
771+
"notNull": true
772+
},
773+
{
774+
"fieldPath": "reporting.twentyOneDayConfirmationReportSentDateMs",
775+
"columnName": "reporting_twentyOneDayConfirmationReportSentDateMs",
776+
"affinity": "INTEGER",
777+
"notNull": true
778+
},
779+
{
780+
"fieldPath": "reporting.fortyTwoDayConfirmationReportSentDateMs",
781+
"columnName": "reporting_fortyTwoDayConfirmationReportSentDateMs",
782+
"affinity": "INTEGER",
783+
"notNull": true
760784
}
761785
],
762786
"primaryKey": {
@@ -946,7 +970,7 @@
946970
"views": [],
947971
"setupQueries": [
948972
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
949-
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '584648b8b3065521786fb33f44214f2e')"
973+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a5782d19654dee4cad932b458037bb37')"
950974
]
951975
}
952976
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/models/scheduling/JobRecord.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ sealed class JobRecord(
5656
val optOutRemovedDateInMillis: Long = 0L,
5757
val deprecated: Boolean = false,
5858
val dateCreatedInMillis: Long = 0L,
59+
val confirmation7dayReportSentDateMs: Long = 0L,
60+
val confirmation14dayReportSentDateMs: Long = 0L,
61+
val confirmation21dayReportSentDateMs: Long = 0L,
62+
val confirmation42dayReportSentDateMs: Long = 0L,
5963
) : JobRecord(brokerName, userProfileId) {
6064
enum class OptOutJobStatus {
6165
/** Opt-out has not been executed yet and should be executed when possible */
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.common.utils.DispatcherProvider
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.pir.impl.models.Broker
23+
import com.duckduckgo.pir.impl.models.scheduling.JobRecord.OptOutJobRecord
24+
import com.duckduckgo.pir.impl.models.scheduling.JobRecord.OptOutJobRecord.OptOutJobStatus.REMOVED
25+
import com.duckduckgo.pir.impl.models.scheduling.JobRecord.OptOutJobRecord.OptOutJobStatus.REQUESTED
26+
import com.duckduckgo.pir.impl.store.PirRepository
27+
import com.duckduckgo.pir.impl.store.PirSchedulingRepository
28+
import com.squareup.anvil.annotations.ContributesBinding
29+
import kotlinx.coroutines.withContext
30+
import java.util.concurrent.TimeUnit
31+
import javax.inject.Inject
32+
33+
interface OptOutConfirmationReporter {
34+
suspend fun attemptFirePixel()
35+
}
36+
37+
@ContributesBinding(AppScope::class)
38+
class RealOptOutConfirmationReporter @Inject constructor(
39+
private val dispatcherProvider: DispatcherProvider,
40+
private val pirSchedulingRepository: PirSchedulingRepository,
41+
private val pirRepository: PirRepository,
42+
private val currentTimeProvider: CurrentTimeProvider,
43+
private val pixelSender: PirPixelSender,
44+
) : OptOutConfirmationReporter {
45+
override suspend fun attemptFirePixel() {
46+
withContext(dispatcherProvider.io()) {
47+
val activeBrokers = pirRepository.getAllActiveBrokerObjects().associateBy { it.name }
48+
val allValidRequestedOptOutJobs = pirSchedulingRepository.getAllValidOptOutJobRecords().filter {
49+
it.status == REQUESTED || it.status == REMOVED // TODO: Filter out removed by user
50+
}
51+
52+
if (activeBrokers.isEmpty() || allValidRequestedOptOutJobs.isEmpty()) return@withContext
53+
54+
attemptFire7dayPixel(allValidRequestedOptOutJobs, activeBrokers)
55+
}
56+
}
57+
58+
private suspend fun attemptFire7dayPixel(
59+
allValidRequestedOptOutJobs: List<OptOutJobRecord>,
60+
activeBrokers: Map<String, Broker>,
61+
) {
62+
val now = currentTimeProvider.currentTimeMillis()
63+
val optOutsForSevenDayPixel = allValidRequestedOptOutJobs.filter {
64+
it.daysPassedSinceSubmission(now, 7) && it.confirmation7dayReportSentDateMs == 0L
65+
}
66+
67+
optOutsForSevenDayPixel.forEach { optOutJobRecord ->
68+
val brokerUrl = activeBrokers[optOutJobRecord.brokerName]?.url ?: return@forEach
69+
70+
if (optOutJobRecord.status == REMOVED) {
71+
pixelSender.reportBrokerOptOutConfirmed7Days(brokerUrl)
72+
} else {
73+
pixelSender.reportBrokerOptOutUnconfirmed7Days(brokerUrl)
74+
}
75+
76+
pirSchedulingRepository.markOptOutDay7ConfirmationPixelSent(optOutJobRecord.extractedProfileId, now)
77+
}
78+
}
79+
80+
private fun OptOutJobRecord.daysPassedSinceSubmission(
81+
now: Long,
82+
interval: Long,
83+
): Boolean {
84+
return now >= this.optOutRequestedDateInMillis + TimeUnit.DAYS.toMillis(interval)
85+
}
86+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ class PirCustomStatsWorker(
3131
@Inject
3232
lateinit var optOutSubmissionSuccessRateReporter: OptOut24HourSubmissionSuccessRateReporter
3333

34+
@Inject
35+
lateinit var optOutConfirmationReporter: OptOutConfirmationReporter
36+
3437
override suspend fun doWork(): Result {
3538
optOutSubmissionSuccessRateReporter.attemptFirePixel()
39+
optOutConfirmationReporter.attemptFirePixel()
40+
3641
return Result.success()
3742
}
3843

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/store/PirSchedulingRepository.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.duckduckgo.pir.impl.models.scheduling.JobRecord.ScanJobRecord.ScanJob
2828
import com.duckduckgo.pir.impl.store.db.EmailConfirmationJobRecordEntity
2929
import com.duckduckgo.pir.impl.store.db.JobSchedulingDao
3030
import com.duckduckgo.pir.impl.store.db.OptOutJobRecordEntity
31+
import com.duckduckgo.pir.impl.store.db.ReportingRecord
3132
import com.duckduckgo.pir.impl.store.db.ScanJobRecordEntity
3233
import com.duckduckgo.pir.impl.store.secure.PirSecureStorageDatabaseFactory
3334
import com.squareup.anvil.annotations.ContributesBinding
@@ -124,6 +125,11 @@ interface PirSchedulingRepository {
124125
suspend fun deleteEmailConfirmationJobRecord(extractedProfileId: Long)
125126

126127
suspend fun deleteAllEmailConfirmationJobRecords()
128+
129+
suspend fun markOptOutDay7ConfirmationPixelSent(
130+
extractedProfileId: Long,
131+
timestampMs: Long,
132+
)
127133
}
128134

129135
@ContributesBinding(
@@ -319,6 +325,15 @@ class RealPirSchedulingRepository @Inject constructor(
319325
}
320326
}
321327

328+
override suspend fun markOptOutDay7ConfirmationPixelSent(
329+
extractedProfileId: Long,
330+
timestampMs: Long,
331+
) {
332+
withContext(dispatcherProvider.io()) {
333+
jobSchedulingDao()?.updateSevenDayConfirmationReportSentDate(extractedProfileId, timestampMs)
334+
}
335+
}
336+
322337
private fun ScanJobRecordEntity.toRecord(): ScanJobRecord =
323338
ScanJobRecord(
324339
brokerName = this.brokerName,
@@ -355,6 +370,10 @@ class RealPirSchedulingRepository @Inject constructor(
355370
optOutRemovedDateInMillis = this.optOutRemovedDate,
356371
deprecated = this.deprecated,
357372
dateCreatedInMillis = this.dateCreatedInMillis,
373+
confirmation7dayReportSentDateMs = this.reporting.sevenDayConfirmationReportSentDateMs,
374+
confirmation14dayReportSentDateMs = this.reporting.fourteenDayConfirmationReportSentDateMs,
375+
confirmation21dayReportSentDateMs = this.reporting.twentyOneDayConfirmationReportSentDateMs,
376+
confirmation42dayReportSentDateMs = this.reporting.fortyTwoDayConfirmationReportSentDateMs,
358377
)
359378

360379
private fun OptOutJobRecord.toEntity(): OptOutJobRecordEntity =
@@ -373,6 +392,12 @@ class RealPirSchedulingRepository @Inject constructor(
373392
} else {
374393
currentTimeProvider.currentTimeMillis()
375394
},
395+
reporting = ReportingRecord(
396+
sevenDayConfirmationReportSentDateMs = this.confirmation7dayReportSentDateMs,
397+
fourteenDayConfirmationReportSentDateMs = this.confirmation14dayReportSentDateMs,
398+
twentyOneDayConfirmationReportSentDateMs = this.confirmation21dayReportSentDateMs,
399+
fortyTwoDayConfirmationReportSentDateMs = this.confirmation42dayReportSentDateMs,
400+
),
376401
)
377402

378403
private fun EmailConfirmationJobRecord.toEntity(): EmailConfirmationJobRecordEntity =

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/store/db/JobSchedulingDao.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,16 @@ interface JobSchedulingDao {
118118
deleteOptOutJobRecordsForProfiles(profileQueryIds)
119119
deleteEmailConfirmationJobRecordsForProfiles(profileQueryIds)
120120
}
121+
122+
@Query(
123+
"""
124+
UPDATE pir_optout_job_record
125+
SET reporting_sevenDayConfirmationReportSentDateMs = :newDate
126+
WHERE extractedProfileId = :extractedProfileId
127+
""",
128+
)
129+
fun updateSevenDayConfirmationReportSentDate(
130+
extractedProfileId: Long,
131+
newDate: Long,
132+
)
121133
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/store/db/JobSchedulingEntities.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.duckduckgo.pir.impl.store.db
1818

19+
import androidx.room.Embedded
1920
import androidx.room.Entity
2021
import androidx.room.PrimaryKey
2122

@@ -44,6 +45,15 @@ data class OptOutJobRecordEntity(
4445
val optOutRemovedDate: Long = 0L,
4546
val deprecated: Boolean = false,
4647
val dateCreatedInMillis: Long,
48+
@Embedded(prefix = "reporting_")
49+
val reporting: ReportingRecord,
50+
)
51+
52+
data class ReportingRecord(
53+
val sevenDayConfirmationReportSentDateMs: Long = 0L,
54+
val fourteenDayConfirmationReportSentDateMs: Long = 0L,
55+
val twentyOneDayConfirmationReportSentDateMs: Long = 0L,
56+
val fortyTwoDayConfirmationReportSentDateMs: Long = 0L,
4757
)
4858

4959
@Entity(tableName = "pir_email_confirmation_job_record")

0 commit comments

Comments
 (0)