Skip to content

Commit 0592b00

Browse files
committed
feat: Firebase 이벤트 로그 추가
1 parent 5df070e commit 0592b00

22 files changed

Lines changed: 460 additions & 7 deletions

File tree

build-logic/src/main/java/runcombi.android.feature.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ configureHiltAndroid()
2323
dependencies {
2424
implementation(project(":core:designsystem"))
2525
implementation(project(":core:navigation"))
26+
implementation(project(":core:analytics"))
2627
implementation(project(":core:ui"))
2728
implementation(project(":core:domain:auth"))
2829
implementation(project(":core:domain:common"))

core/analytics/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

core/analytics/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Analytics 모듈
2+
3+
RunCombi_Android 프로젝트의 분석 이벤트 로깅을 위한 모듈입니다.
4+
5+
## 주요 구성 요소
6+
7+
### 1. AnalyticsEvent
8+
분석 이벤트를 정의하는 데이터 클래스입니다.
9+
10+
### 2. AnalyticsHelper
11+
분석 이벤트를 로깅하는 인터페이스입니다.
12+
13+
### 3. StubAnalyticsHelper
14+
개발 및 디버그 빌드에서 사용하는 구현체로, 이벤트를 logcat에 기록합니다.
15+
16+
### 4. FirebaseAnalyticsHelper
17+
프로덕션 빌드에서 사용하는 구현체로, Firebase Analytics에 실제 이벤트를 전송합니다.
18+
19+
### 5. NoOpAnalyticsHelper
20+
테스트와 프리뷰에서 사용하는 구현체로, 아무것도 하지 않습니다.
21+
22+
## 사용법
23+
24+
### 기본 사용법
25+
26+
```kotlin
27+
@Inject
28+
lateinit var analyticsHelper: AnalyticsHelper
29+
30+
analyticsHelper.logEvent(
31+
AnalyticsEvent(
32+
type = "custom_event",
33+
extras = listOf(
34+
AnalyticsEvent.Param(key = "key", value = "value")
35+
)
36+
)
37+
)
38+
```
39+
40+
### 확장 함수 사용
41+
42+
```kotlin
43+
analyticsHelper.logScreenView("LoginActivity")
44+
analyticsHelper.logUserLogin("google")
45+
analyticsHelper.logWalkStarted("outdoor")
46+
analyticsHelper.logSettingChanged("theme", "dark")
47+
analyticsHelper.logFeatureUsed("camera")
48+
analyticsHelper.logButtonClick("login_button", "LoginScreen")
49+
analyticsHelper.logError("network_error", "Connection failed", "MainScreen")
50+
```
51+
52+
### Compose에서 사용
53+
54+
```kotlin
55+
val analyticsHelper = LocalAnalyticsHelper.current
56+
57+
LaunchedEffect(Unit) {
58+
analyticsHelper.logScreenView("HomeScreen")
59+
}
60+
```
61+
62+
## 표준 이벤트 타입
63+
64+
- `screen_view`: 화면 보기
65+
- `user_login`: 사용자 로그인
66+
- `user_signup`: 사용자 회원가입
67+
- `walk_started`: 산책 시작
68+
- `walk_completed`: 산책 완료
69+
- `setting_changed`: 설정 변경
70+
- `feature_used`: 기능 사용
71+
- `button_click`: 버튼 클릭
72+
- `error_occurred`: 오류 발생
73+
- `app_started`: 앱 시작
74+
- `app_backgrounded`: 앱 백그라운드
75+
- `app_foregrounded`: 앱 포그라운드
76+
77+
## 표준 매개변수 키
78+
79+
- `screen_name`: 화면 이름
80+
- `login_method`: 로그인 방법
81+
- `signup_method`: 회원가입 방법
82+
- `walk_type`: 산책 타입
83+
- `walk_duration`: 산책 시간
84+
- `walk_distance`: 산책 거리
85+
- `setting_name`: 설정 이름
86+
- `setting_value`: 설정 값
87+
- `feature_name`: 기능 이름
88+
- `button_name`: 버튼 이름
89+
- `error_type`: 오류 타입
90+
- `error_message`: 오류 메시지
91+
- `is_new_user`: 신규 사용자 여부
92+
- `user_status`: 사용자 상태

core/analytics/build.gradle.kts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
alias(libs.plugins.android.library)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.hilt.android)
5+
alias(libs.plugins.kotlin.ksp)
6+
}
7+
8+
android {
9+
namespace = "com.combo.runcombi.analytics"
10+
compileSdk = 35
11+
12+
defaultConfig {
13+
minSdk = 26
14+
15+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
16+
consumerProguardFiles("consumer-rules.pro")
17+
}
18+
19+
buildTypes {
20+
release {
21+
isMinifyEnabled = false
22+
proguardFiles(
23+
getDefaultProguardFile("proguard-android-optimize.txt"),
24+
"proguard-rules.pro"
25+
)
26+
}
27+
}
28+
compileOptions {
29+
sourceCompatibility = JavaVersion.VERSION_11
30+
targetCompatibility = JavaVersion.VERSION_11
31+
}
32+
kotlinOptions {
33+
jvmTarget = "11"
34+
}
35+
}
36+
37+
dependencies {
38+
// Hilt
39+
implementation(libs.hilt.android)
40+
ksp(libs.hilt.compiler)
41+
42+
// Firebase
43+
implementation(platform(libs.firebase.bom))
44+
implementation(libs.firebase.analytics)
45+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.combo.runcombi.analytics
2+
3+
data class AnalyticsEvent(
4+
val type: String,
5+
val extras: List<Param> = emptyList(),
6+
) {
7+
class Types {
8+
companion object {
9+
const val SCREEN_VIEW = "screen_view"
10+
const val USER_LOGIN = "user_login"
11+
const val USER_SIGNUP = "user_signup"
12+
const val WALK_STARTED = "walk_started"
13+
const val WALK_COMPLETED = "walk_completed"
14+
const val SETTING_CHANGED = "setting_changed"
15+
const val FEATURE_USED = "feature_used"
16+
const val BUTTON_CLICK = "button_click"
17+
const val ERROR_OCCURRED = "error_occurred"
18+
const val APP_STARTED = "app_started"
19+
const val APP_BACKGROUNDED = "app_backgrounded"
20+
const val APP_FOREGROUNDED = "app_foregrounded"
21+
}
22+
}
23+
24+
data class Param(val key: String, val value: String)
25+
26+
class ParamKeys {
27+
companion object {
28+
const val SCREEN_NAME = "screen_name"
29+
const val LOGIN_METHOD = "login_method"
30+
const val SIGNUP_METHOD = "signup_method"
31+
const val WALK_TYPE = "walk_type"
32+
const val WALK_DURATION = "walk_duration"
33+
const val WALK_DISTANCE = "walk_distance"
34+
const val SETTING_NAME = "setting_name"
35+
const val SETTING_VALUE = "setting_value"
36+
const val FEATURE_NAME = "feature_name"
37+
const val BUTTON_NAME = "button_name"
38+
const val ERROR_TYPE = "error_type"
39+
const val ERROR_MESSAGE = "error_message"
40+
const val IS_NEW_USER = "is_new_user"
41+
const val USER_STATUS = "user_status"
42+
}
43+
}
44+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.combo.runcombi.analytics
2+
3+
import com.combo.runcombi.analytics.AnalyticsEvent
4+
import com.combo.runcombi.analytics.AnalyticsEvent.Param
5+
import com.combo.runcombi.analytics.AnalyticsHelper
6+
7+
// 기본 화면 및 사용자 이벤트
8+
fun AnalyticsHelper.logScreenView(screenName: String) =
9+
logEvent(
10+
AnalyticsEvent(
11+
type = AnalyticsEvent.Types.SCREEN_VIEW,
12+
extras = listOf(
13+
Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName),
14+
),
15+
),
16+
)
17+
18+
fun AnalyticsHelper.logUserLogin(loginMethod: String) =
19+
logEvent(
20+
AnalyticsEvent(
21+
type = AnalyticsEvent.Types.USER_LOGIN,
22+
extras = listOf(
23+
Param(key = AnalyticsEvent.ParamKeys.LOGIN_METHOD, value = loginMethod),
24+
),
25+
),
26+
)
27+
28+
fun AnalyticsHelper.logUserSignup(signupMethod: String) =
29+
logEvent(
30+
AnalyticsEvent(
31+
type = AnalyticsEvent.Types.USER_SIGNUP,
32+
extras = listOf(
33+
Param(key = AnalyticsEvent.ParamKeys.SIGNUP_METHOD, value = signupMethod),
34+
),
35+
),
36+
)
37+
38+
// 산책 관련 이벤트
39+
fun AnalyticsHelper.logWalkStarted(walkType: String) =
40+
logEvent(
41+
AnalyticsEvent(
42+
type = AnalyticsEvent.Types.WALK_STARTED,
43+
extras = listOf(
44+
Param(key = AnalyticsEvent.ParamKeys.WALK_TYPE, value = walkType),
45+
),
46+
),
47+
)
48+
49+
fun AnalyticsHelper.logWalkCompleted(duration: String, distance: String) =
50+
logEvent(
51+
AnalyticsEvent(
52+
type = AnalyticsEvent.Types.WALK_COMPLETED,
53+
extras = listOf(
54+
Param(key = AnalyticsEvent.ParamKeys.WALK_DURATION, value = duration),
55+
Param(key = AnalyticsEvent.ParamKeys.WALK_DISTANCE, value = distance),
56+
),
57+
),
58+
)
59+
60+
// 설정 및 기능 이벤트
61+
fun AnalyticsHelper.logSettingChanged(settingName: String, settingValue: String) =
62+
logEvent(
63+
AnalyticsEvent(
64+
type = AnalyticsEvent.Types.SETTING_CHANGED,
65+
extras = listOf(
66+
Param(key = AnalyticsEvent.ParamKeys.SETTING_NAME, value = settingName),
67+
Param(key = AnalyticsEvent.ParamKeys.SETTING_VALUE, value = settingValue),
68+
),
69+
),
70+
)
71+
72+
fun AnalyticsHelper.logFeatureUsed(featureName: String) =
73+
logEvent(
74+
AnalyticsEvent(
75+
type = AnalyticsEvent.Types.FEATURE_USED,
76+
extras = listOf(
77+
Param(key = AnalyticsEvent.ParamKeys.FEATURE_NAME, value = featureName),
78+
),
79+
),
80+
)
81+
82+
// 앱 생명주기 이벤트
83+
fun AnalyticsHelper.logAppStarted(isNewUser: Boolean, userStatus: String) =
84+
logEvent(
85+
AnalyticsEvent(
86+
type = AnalyticsEvent.Types.APP_STARTED,
87+
extras = listOf(
88+
Param(key = AnalyticsEvent.ParamKeys.IS_NEW_USER, value = isNewUser.toString()),
89+
Param(key = AnalyticsEvent.ParamKeys.USER_STATUS, value = userStatus),
90+
),
91+
),
92+
)
93+
94+
fun AnalyticsHelper.logAppBackgrounded() =
95+
logEvent(
96+
AnalyticsEvent(
97+
type = AnalyticsEvent.Types.APP_BACKGROUNDED
98+
)
99+
)
100+
101+
fun AnalyticsHelper.logAppForegrounded() =
102+
logEvent(
103+
AnalyticsEvent(
104+
type = AnalyticsEvent.Types.APP_FOREGROUNDED
105+
)
106+
)
107+
108+
// 사용자 상호작용 이벤트
109+
fun AnalyticsHelper.logButtonClick(buttonName: String, screenName: String? = null) =
110+
logEvent(
111+
AnalyticsEvent(
112+
type = AnalyticsEvent.Types.BUTTON_CLICK,
113+
extras = listOf(
114+
Param(key = AnalyticsEvent.ParamKeys.BUTTON_NAME, value = buttonName),
115+
).let { params ->
116+
if (screenName != null) {
117+
params + Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName)
118+
} else {
119+
params
120+
}
121+
}
122+
)
123+
)
124+
125+
// 오류 및 예외 이벤트
126+
fun AnalyticsHelper.logError(errorType: String, errorMessage: String, screenName: String? = null) =
127+
logEvent(
128+
AnalyticsEvent(
129+
type = AnalyticsEvent.Types.ERROR_OCCURRED,
130+
extras = listOf(
131+
Param(key = AnalyticsEvent.ParamKeys.ERROR_TYPE, value = errorType),
132+
Param(key = AnalyticsEvent.ParamKeys.ERROR_MESSAGE, value = errorMessage),
133+
).let { params ->
134+
if (screenName != null) {
135+
params + Param(key = AnalyticsEvent.ParamKeys.SCREEN_NAME, value = screenName)
136+
} else {
137+
params
138+
}
139+
}
140+
)
141+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.combo.runcombi.analytics
2+
3+
interface AnalyticsHelper {
4+
fun logEvent(event: AnalyticsEvent)
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.combo.runcombi.analytics
2+
3+
import dagger.Binds
4+
import dagger.Module
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
8+
@Module
9+
@InstallIn(SingletonComponent::class)
10+
internal abstract class AnalyticsModule {
11+
12+
@Binds
13+
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper
14+
15+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.combo.runcombi.analytics
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.os.Bundle
6+
import com.google.firebase.analytics.FirebaseAnalytics
7+
import dagger.hilt.android.qualifiers.ApplicationContext
8+
import javax.inject.Inject
9+
import javax.inject.Singleton
10+
11+
@SuppressLint("MissingPermission")
12+
@Singleton
13+
internal class FirebaseAnalyticsHelper @Inject constructor(
14+
@ApplicationContext private val context: Context
15+
) : AnalyticsHelper {
16+
17+
private val firebaseAnalytics: FirebaseAnalytics by lazy {
18+
FirebaseAnalytics.getInstance(context)
19+
}
20+
21+
override fun logEvent(event: AnalyticsEvent) {
22+
val bundle = Bundle().apply {
23+
putString(FirebaseAnalytics.Param.ITEM_ID, event.type)
24+
25+
event.extras.forEach { param ->
26+
putString(param.key, param.value)
27+
}
28+
}
29+
30+
firebaseAnalytics.logEvent(event.type, bundle)
31+
}
32+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.combo.runcombi.analytics
2+
3+
class NoOpAnalyticsHelper : AnalyticsHelper {
4+
override fun logEvent(event: AnalyticsEvent) = Unit
5+
}

0 commit comments

Comments
 (0)