Skip to content

Commit e764ff6

Browse files
authored
Релиз 1.7 (#3)
* Доработки аналитики * Обновил зависимости * Документация и версия релиза
1 parent de6ab75 commit e764ff6

27 files changed

Lines changed: 855 additions & 267 deletions

app/src/androidTest/java/com/dayscounter/ui/screens/createedit/TestViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.dayscounter.ui.screens.createedit
22

33
import androidx.lifecycle.SavedStateHandle
4+
import com.dayscounter.analytics.AnalyticsService
5+
import com.dayscounter.analytics.NoopAnalyticsProvider
46
import com.dayscounter.data.provider.ResourceProvider
57
import com.dayscounter.domain.model.Item
68
import com.dayscounter.domain.model.SortOrder
@@ -18,7 +20,8 @@ fun createTestViewModel(): CreateEditScreenViewModel =
1820
CreateEditScreenViewModel(
1921
repository = createTestItemRepository(),
2022
resourceProvider = createTestResourceProvider(),
21-
savedStateHandle = SavedStateHandle()
23+
savedStateHandle = SavedStateHandle(),
24+
analyticsService = AnalyticsService(listOf(NoopAnalyticsProvider()))
2225
)
2326

2427
/**

app/src/main/java/com/dayscounter/MainActivity.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import androidx.compose.runtime.collectAsState
1111
import androidx.compose.runtime.getValue
1212
import androidx.compose.ui.Modifier
1313
import androidx.lifecycle.viewmodel.compose.viewModel
14+
import com.dayscounter.analytics.AnalyticsService
1415
import com.dayscounter.data.preferences.createAppSettingsDataStore
16+
import com.dayscounter.di.AppModule
1517
import com.dayscounter.ui.screens.root.RootScreen
1618
import com.dayscounter.ui.theme.JetpackDaysTheme
1719
import com.dayscounter.ui.viewmodel.MainActivityViewModel
@@ -28,6 +30,9 @@ class MainActivity : ComponentActivity() {
2830
// Создаём DataStore для настроек приложения
2931
val dataStore = createAppSettingsDataStore(applicationContext)
3032

33+
// Создаём AnalyticsService
34+
val analyticsService = AppModule.createAnalyticsService(applicationContext)
35+
3136
setContent {
3237
// Создаём ViewModel для MainActivity
3338
val viewModel: MainActivityViewModel =
@@ -42,7 +47,8 @@ class MainActivity : ComponentActivity() {
4247
// Применяем тему приложения
4348
AppContent(
4449
theme = theme,
45-
useDynamicColors = useDynamicColors
50+
useDynamicColors = useDynamicColors,
51+
analyticsService = analyticsService
4652
)
4753
}
4854
}
@@ -52,11 +58,13 @@ class MainActivity : ComponentActivity() {
5258
* Основной контент Activity с применённой темой.
5359
*
5460
* @param theme Тема приложения из DataStore
61+
* @param analyticsService Сервис аналитики
5562
*/
5663
@Composable
5764
private fun AppContent(
5865
theme: com.dayscounter.domain.model.AppTheme,
59-
useDynamicColors: Boolean
66+
useDynamicColors: Boolean,
67+
analyticsService: AnalyticsService
6068
) {
6169
JetpackDaysTheme(
6270
appTheme = theme,
@@ -67,7 +75,7 @@ private fun AppContent(
6775
modifier = Modifier.fillMaxSize(),
6876
color = MaterialTheme.colorScheme.background
6977
) {
70-
RootScreen()
78+
RootScreen(analyticsService = analyticsService)
7179
}
7280
}
7381
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.dayscounter.analytics
2+
3+
/**
4+
* Имена экранов для логирования screen_view событий.
5+
*/
6+
enum class AppScreen(
7+
val screenName: String
8+
) {
9+
EVENTS("EventsScreen"),
10+
DETAIL("DetailScreen"),
11+
CREATE_EDIT("CreateEditScreen"),
12+
MORE("MoreScreen"),
13+
THEME_ICON("ThemeIconScreen"),
14+
APP_DATA("AppDataScreen")
15+
}
16+
17+
/**
18+
* Типы пользовательских действий для логирования.
19+
*/
20+
enum class UserActionType(
21+
val value: String
22+
) {
23+
CREATE("create"),
24+
EDIT("edit"),
25+
DELETE("delete"),
26+
SORT("sort"),
27+
ITEM_SAVED("item_saved"),
28+
ICON_SELECTED("icon_selected"),
29+
CREATE_BACKUP("create_backup"),
30+
RESTORE_BACKUP("restore_backup"),
31+
DELETE_ALL_DATA("delete_all_data")
32+
}
33+
34+
/**
35+
* Типы операций для логирования ошибок.
36+
*/
37+
enum class AppErrorOperation(
38+
val value: String
39+
) {
40+
SET_ICON("set_icon"),
41+
CREATE_BACKUP("create_backup"),
42+
RESTORE_BACKUP("restore_backup"),
43+
DELETE_ALL_DATA("delete_all_data"),
44+
CREATE_ITEM("create_item"),
45+
UPDATE_ITEM("update_item")
46+
}
47+
48+
/**
49+
* События аналитики.
50+
*/
51+
sealed interface AnalyticsEvent {
52+
/**
53+
* Событие просмотра экрана.
54+
*
55+
* @param screen Экран, который просматривается
56+
* @param screenClass Полное имя класса экрана (опционально)
57+
*/
58+
data class ScreenView(
59+
val screen: AppScreen,
60+
val screenClass: String? = null
61+
) : AnalyticsEvent
62+
63+
/**
64+
* Событие пользовательского действия.
65+
*
66+
* @param action Тип действия
67+
* @param iconName Название иконки (только для ICON_SELECTED)
68+
*/
69+
data class UserAction(
70+
val action: UserActionType,
71+
val iconName: String? = null
72+
) : AnalyticsEvent
73+
74+
/**
75+
* Событие ошибки приложения.
76+
*
77+
* @param operation Операция, в которой произошла ошибка
78+
* @param throwable Исключение
79+
*/
80+
data class AppError(
81+
val operation: AppErrorOperation,
82+
val throwable: Throwable
83+
) : AnalyticsEvent
84+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.dayscounter.analytics
2+
3+
/**
4+
* Интерфейс провайдера аналитики.
5+
*
6+
* Позволяет подключать различные реализации (Firebase, Amplitude, Mixpanel и т.д.)
7+
* без изменения кода, использующего аналитику.
8+
*/
9+
interface AnalyticsProvider {
10+
/**
11+
* Логирует событие аналитики.
12+
*
13+
* @param event Событие для логирования
14+
*/
15+
fun log(event: AnalyticsEvent)
16+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.dayscounter.analytics
2+
3+
import com.dayscounter.util.AndroidLogger
4+
import com.dayscounter.util.Logger
5+
6+
/**
7+
* Сервис аналитики, который делегирует события всем зарегистрированным провайдерам.
8+
*
9+
* @param providers Список провайдеров для отправки событий
10+
*/
11+
class AnalyticsService(
12+
private val providers: List<AnalyticsProvider>,
13+
private val logger: Logger = AndroidLogger()
14+
) {
15+
private companion object {
16+
private const val TAG = "AnalyticsService"
17+
}
18+
19+
/**
20+
* Логирует событие, отправляя его всем зарегистрированным провайдерам.
21+
*
22+
* Ошибки отдельных провайдеров не влияют на работу остальных.
23+
*
24+
* @param event Событие для логирования
25+
*/
26+
@Suppress("TooGenericExceptionCaught")
27+
fun log(event: AnalyticsEvent) {
28+
providers.forEach { provider ->
29+
try {
30+
provider.log(event)
31+
} catch (e: Exception) {
32+
logger.e(
33+
tag = TAG,
34+
message = "Ошибка в провайдере ${provider::class.simpleName}: ${e.message}",
35+
throwable = e
36+
)
37+
}
38+
}
39+
}
40+
}

app/src/main/java/com/dayscounter/analytics/FirebaseAnalyticsHelper.kt

Lines changed: 0 additions & 74 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.dayscounter.analytics
2+
3+
import android.content.Context
4+
import android.os.Bundle
5+
import com.dayscounter.util.AndroidLogger
6+
import com.dayscounter.util.Logger
7+
import com.google.firebase.analytics.FirebaseAnalytics
8+
9+
/**
10+
* Провайдер аналитики Firebase.
11+
*
12+
* @param context Контекст приложения
13+
*/
14+
class FirebaseAnalyticsProvider(
15+
context: Context,
16+
private val logger: Logger = AndroidLogger()
17+
) : AnalyticsProvider {
18+
private val firebaseAnalytics = FirebaseAnalytics.getInstance(context.applicationContext)
19+
20+
private companion object {
21+
private const val TAG = "FirebaseAnalyticsProvider"
22+
private const val PARAM_OPERATION = "operation"
23+
private const val PARAM_ERROR_DOMAIN = "error_domain"
24+
private const val PARAM_ERROR_CODE = "error_code"
25+
private const val PARAM_ACTION = "action"
26+
private const val PARAM_ICON_NAME = "icon_name"
27+
private const val USER_ACTION_EVENT = "user_action"
28+
private const val APP_ERROR_EVENT = "app_error"
29+
}
30+
31+
override fun log(event: AnalyticsEvent) {
32+
when (event) {
33+
is AnalyticsEvent.ScreenView -> logScreenView(event)
34+
is AnalyticsEvent.UserAction -> logUserAction(event)
35+
is AnalyticsEvent.AppError -> logAppError(event)
36+
}
37+
}
38+
39+
private fun logScreenView(event: AnalyticsEvent.ScreenView) {
40+
val params =
41+
Bundle().apply {
42+
putString(FirebaseAnalytics.Param.SCREEN_NAME, event.screen.screenName)
43+
event.screenClass?.let { putString(FirebaseAnalytics.Param.SCREEN_CLASS, it) }
44+
}
45+
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, params)
46+
logger.d(TAG, "screen_view: ${event.screen.screenName}")
47+
}
48+
49+
private fun logUserAction(event: AnalyticsEvent.UserAction) {
50+
val params =
51+
Bundle().apply {
52+
putString(PARAM_ACTION, event.action.value)
53+
if (event.action == UserActionType.ICON_SELECTED) {
54+
event.iconName?.let { putString(PARAM_ICON_NAME, it) }
55+
}
56+
}
57+
firebaseAnalytics.logEvent(USER_ACTION_EVENT, params)
58+
logger.d(TAG, "user_action: ${event.action.value}")
59+
}
60+
61+
private fun logAppError(event: AnalyticsEvent.AppError) {
62+
val params =
63+
Bundle().apply {
64+
putString(PARAM_OPERATION, event.operation.value)
65+
putString(PARAM_ERROR_DOMAIN, event.throwable::class.java.name)
66+
putLong(PARAM_ERROR_CODE, event.throwable.hashCode().toLong())
67+
}
68+
firebaseAnalytics.logEvent(APP_ERROR_EVENT, params)
69+
logger.d(TAG, "app_error: ${event.operation.value}")
70+
}
71+
}

0 commit comments

Comments
 (0)