Skip to content

Commit 82c7dfe

Browse files
committed
adds internal event repository and database
1 parent cdf842a commit 82c7dfe

File tree

9 files changed

+464
-10
lines changed

9 files changed

+464
-10
lines changed

attributed-metrics/attributed-metrics-api/src/main/java/com/duckduckgo/app/attributed/metrics/api/AttributedMetricsClient.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ interface AttributedMetricClient {
2525
fun collectEvent(eventName: String)
2626

2727
// return events stored in the last days, and precalculated stats
28-
suspend fun getEventStats(eventName: String, days: Int): EventStats
28+
suspend fun getEventStats(
29+
eventName: String,
30+
days: Int,
31+
): EventStats
2932

3033
// if in monitoring window will emit the metric
3134
// this part owns adding common params to all metrics (e.g. origin, removing default params)
@@ -39,13 +42,14 @@ data class EventStats(
3942
// rolling average of events based on days timeframe
4043
val rollingAverage: Double,
4144
// total number of events in the timeframe
42-
val totalEvents: Int
45+
val totalEvents: Int,
4346
)
4447

4548
// interface for each metric
4649
interface AttributedMetric {
4750
// Metric owns the pixel name value
4851
fun getPixelName(): String
52+
4953
// Metric owns adding metric specific parameters
5054
suspend fun getMetricParameters(): Map<String, String>
5155
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.di
18+
19+
import android.content.Context
20+
import androidx.room.Room
21+
import com.duckduckgo.app.attributed.metrics.store.AttributedMetricsDatabase
22+
import com.duckduckgo.app.attributed.metrics.store.EventDao
23+
import com.duckduckgo.di.scopes.AppScope
24+
import com.squareup.anvil.annotations.ContributesTo
25+
import dagger.Module
26+
import dagger.Provides
27+
import dagger.SingleInstanceIn
28+
29+
@Module
30+
@ContributesTo(AppScope::class)
31+
class AttributedMetricsDatabaseModule {
32+
@Provides
33+
@SingleInstanceIn(AppScope::class)
34+
fun provideAttributedMetricsDatabase(context: Context): AttributedMetricsDatabase =
35+
Room
36+
.databaseBuilder(
37+
context = context,
38+
klass = AttributedMetricsDatabase::class.java,
39+
name = "attributed_metrics.db",
40+
).fallbackToDestructiveMigration()
41+
.build()
42+
43+
@Provides
44+
@SingleInstanceIn(AppScope::class)
45+
fun provideEventDao(db: AttributedMetricsDatabase): EventDao = db.eventDao()
46+
}

attributed-metrics/attributed-metrics-impl/src/main/java/com/duckduckgo/app/attributed/metrics/impl/RealAttributedMetricClient.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,31 @@ package com.duckduckgo.app.attributed.metrics.impl
1818

1919
import com.duckduckgo.app.attributed.metrics.api.AttributedMetric
2020
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
21-
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
2221
import com.duckduckgo.app.attributed.metrics.api.EventStats
22+
import com.duckduckgo.app.attributed.metrics.store.EventRepository
2323
import com.duckduckgo.app.di.AppCoroutineScope
24+
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
2425
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
26+
import com.duckduckgo.common.utils.DispatcherProvider
2527
import com.duckduckgo.di.scopes.AppScope
2628
import com.squareup.anvil.annotations.ContributesBinding
2729
import com.squareup.anvil.annotations.ContributesMultibinding
2830
import dagger.SingleInstanceIn
2931
import kotlinx.coroutines.CoroutineScope
3032
import kotlinx.coroutines.launch
33+
import kotlinx.coroutines.withContext
3134
import javax.inject.Inject
3235

3336
@ContributesMultibinding(AppScope::class, AtbLifecyclePlugin::class)
3437
@ContributesBinding(AppScope::class, AttributedMetricClient::class)
3538
@SingleInstanceIn(AppScope::class)
3639
class RealAttributedMetricClient @Inject constructor(
3740
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
41+
private val dispatcherProvider: DispatcherProvider,
3842
private val appBuildConfig: AppBuildConfig,
39-
): AttributedMetricClient, AtbLifecyclePlugin {
40-
43+
private val eventRepository: EventRepository,
44+
) : AttributedMetricClient,
45+
AtbLifecyclePlugin {
4146
override fun onAppAtbInitialized() {
4247
appCoroutineScope.launch {
4348
if (appBuildConfig.isAppReinstall()) {
@@ -50,15 +55,18 @@ class RealAttributedMetricClient @Inject constructor(
5055
}
5156

5257
override fun collectEvent(eventName: String) {
53-
TODO("Not yet implemented")
58+
appCoroutineScope.launch(dispatcherProvider.io()) {
59+
eventRepository.collectEvent(eventName)
60+
}
5461
}
5562

5663
override suspend fun getEventStats(
5764
eventName: String,
58-
days: Int
59-
): EventStats {
60-
TODO("Not yet implemented")
61-
}
65+
days: Int,
66+
): EventStats =
67+
withContext(dispatcherProvider.io()) {
68+
eventRepository.getEventStats(eventName, days)
69+
}
6270

6371
override fun emitMetric(metric: AttributedMetric) {
6472
TODO("Not yet implemented")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.store
18+
19+
import androidx.room.Database
20+
import androidx.room.RoomDatabase
21+
22+
@Database(
23+
version = 1,
24+
entities = [
25+
EventEntity::class,
26+
],
27+
exportSchema = true,
28+
)
29+
abstract class AttributedMetricsDatabase : RoomDatabase() {
30+
abstract fun eventDao(): EventDao
31+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.store
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.squareup.anvil.annotations.ContributesBinding
21+
import java.time.LocalDate
22+
import java.time.format.DateTimeFormatter
23+
import javax.inject.Inject
24+
25+
interface DateProvider {
26+
fun getCurrentDate(): String
27+
28+
fun getDateMinusDays(days: Int): String
29+
}
30+
31+
@ContributesBinding(AppScope::class)
32+
class RealDateProvider @Inject constructor() : DateProvider {
33+
override fun getCurrentDate(): String = LocalDate.now().format(DATE_FORMATTER)
34+
35+
override fun getDateMinusDays(days: Int): String = LocalDate.now().minusDays(days.toLong()).format(DATE_FORMATTER)
36+
37+
companion object {
38+
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd")
39+
}
40+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.store
18+
19+
import androidx.room.*
20+
21+
@Dao
22+
interface EventDao {
23+
@Transaction
24+
@Query("SELECT * FROM event_metrics WHERE eventName = :eventName AND day >= :startDay ORDER BY day DESC")
25+
suspend fun getEventsByNameAndTimeframe(
26+
eventName: String,
27+
startDay: String,
28+
): List<EventEntity>
29+
30+
@Query("SELECT COUNT(DISTINCT day) FROM event_metrics WHERE eventName = :eventName AND day >= :startDay")
31+
suspend fun getDaysWithEvents(
32+
eventName: String,
33+
startDay: String,
34+
): Int
35+
36+
@Query("SELECT SUM(count) FROM event_metrics WHERE eventName = :eventName AND day >= :startDay")
37+
suspend fun getTotalEvents(
38+
eventName: String,
39+
startDay: String,
40+
): Int
41+
42+
@Insert(onConflict = OnConflictStrategy.REPLACE)
43+
suspend fun insertEvent(event: EventEntity)
44+
45+
@Query(
46+
"""
47+
UPDATE event_metrics
48+
SET count = count + 1
49+
WHERE eventName = :eventName AND day = :day
50+
""",
51+
)
52+
suspend fun incrementEventCount(
53+
eventName: String,
54+
day: String,
55+
)
56+
57+
@Query("SELECT count FROM event_metrics WHERE eventName = :eventName AND day = :day")
58+
suspend fun getEventCount(
59+
eventName: String,
60+
day: String,
61+
): Int?
62+
63+
@Query("DELETE FROM event_metrics WHERE day < :day")
64+
suspend fun deleteEventsOlderThan(day: String)
65+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.store
18+
19+
import androidx.room.Entity
20+
import androidx.room.Index
21+
22+
@Entity(
23+
tableName = "event_metrics",
24+
primaryKeys = ["eventName", "day"],
25+
indices = [Index("eventName"), Index("day")],
26+
)
27+
data class EventEntity(
28+
val eventName: String,
29+
val count: Int,
30+
// Format: YYYY-MM-DD
31+
val day: String,
32+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2024 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.app.attributed.metrics.store
18+
19+
import com.duckduckgo.app.attributed.metrics.api.EventStats
20+
import com.duckduckgo.app.di.AppCoroutineScope
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import kotlinx.coroutines.CoroutineScope
24+
import kotlinx.coroutines.launch
25+
import javax.inject.Inject
26+
27+
interface EventRepository {
28+
suspend fun collectEvent(eventName: String)
29+
30+
suspend fun getEventStats(
31+
eventName: String,
32+
days: Int,
33+
): EventStats
34+
35+
suspend fun deleteOldEvents(olderThanDays: Int)
36+
}
37+
38+
@ContributesBinding(AppScope::class)
39+
class RealEventRepository @Inject constructor(
40+
private val eventDao: EventDao,
41+
private val dateProvider: DateProvider,
42+
@AppCoroutineScope private val coroutineScope: CoroutineScope,
43+
) : EventRepository {
44+
override suspend fun collectEvent(eventName: String) {
45+
val today = dateProvider.getCurrentDate()
46+
val currentCount = eventDao.getEventCount(eventName, today)
47+
48+
if (currentCount == null) {
49+
eventDao.insertEvent(EventEntity(eventName = eventName, count = 1, day = today))
50+
} else {
51+
eventDao.incrementEventCount(eventName, today)
52+
}
53+
}
54+
55+
override suspend fun getEventStats(
56+
eventName: String,
57+
days: Int,
58+
): EventStats {
59+
val startDay = dateProvider.getDateMinusDays(days)
60+
61+
val daysWithEvents = eventDao.getDaysWithEvents(eventName, startDay)
62+
val totalEvents = eventDao.getTotalEvents(eventName, startDay) ?: 0
63+
val rollingAverage = if (days > 0) totalEvents.toDouble() / days else 0.0
64+
65+
return EventStats(
66+
daysWithEvents = daysWithEvents,
67+
rollingAverage = rollingAverage,
68+
totalEvents = totalEvents,
69+
)
70+
}
71+
72+
override suspend fun deleteOldEvents(olderThanDays: Int) {
73+
coroutineScope.launch {
74+
val cutoffDay = dateProvider.getDateMinusDays(olderThanDays)
75+
eventDao.deleteEventsOlderThan(cutoffDay)
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)