diff --git a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt index c2535356d..fb8f01a4f 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainActivity.kt @@ -25,6 +25,8 @@ import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity import com.eatssu.android.presentation.util.showInfoToast import com.eatssu.android.presentation.util.showToast import com.eatssu.android.presentation.util.startActivity +import com.eatssu.common.analytics.MapAnalyticsEvent +import com.eatssu.common.analytics.MyPageAnalyticsEvent import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.enums.ScreenId @@ -85,6 +87,7 @@ class MainActivity : BaseActivity( } R.id.map_menu -> { + analyticsTracker.track(MapAnalyticsEvent.EntryClicked) navController.navigate(R.id.mapFragment) true } @@ -95,6 +98,7 @@ class MainActivity : BaseActivity( } R.id.mypage_menu -> { + analyticsTracker.track(MyPageAnalyticsEvent.MenuClicked) navController.navigate(R.id.myPageFragment) true } diff --git a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt index 497be8f69..af89b8c6d 100644 --- a/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt +++ b/app/src/main/java/com/eatssu/android/presentation/event/AnyoneButMeEventPopupController.kt @@ -8,8 +8,12 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.LifecycleCoroutineScope import com.eatssu.android.R import com.eatssu.android.data.local.AppFeatureDataStore +import com.eatssu.android.domain.usecase.user.GetUserCollegeDepartmentUseCase import com.eatssu.android.presentation.mypage.terms.WebViewActivity import com.eatssu.android.presentation.util.openInBrowser +import com.eatssu.common.analytics.AnalyticsTracker +import com.eatssu.common.analytics.AnyoneButMeAnalyticsEvent +import com.eatssu.common.analytics.PopupAnalyticsEvent import com.eatssu.common.enums.ScreenId import com.eatssu.design_system.theme.EatssuTheme import dagger.hilt.android.qualifiers.ActivityContext @@ -22,6 +26,8 @@ import javax.inject.Inject class AnyoneButMeEventPopupController @Inject constructor( @ActivityContext private val context: Context, private val appFeatureDataStore: AppFeatureDataStore, + private val getUserCollegeDepartmentUseCase: GetUserCollegeDepartmentUseCase, + private val analyticsTracker: AnalyticsTracker, ) { private lateinit var composeView: ComposeView private lateinit var lifecycleScope: LifecycleCoroutineScope @@ -49,10 +55,10 @@ class AnyoneButMeEventPopupController @Inject constructor( EatssuTheme { if (isPopupVisible.value) { AnyoneButMeEventDialog( - onDismiss = ::hide, + onDismiss = ::closeByUser, onDismissForever = ::dismissForever, onInstagramClick = ::openInstagram, - onAnyoneButMeClick = ::openAnyoneButMePage + onAnyoneButMeClick = { openAnyoneButMePage(fromPopup = true) } ) } } @@ -71,13 +77,18 @@ class AnyoneButMeEventPopupController @Inject constructor( } private fun dismissForever() { + trackPopupAction(PopupAnalyticsEvent.Action.NOT_SHOW_AGAIN) hide() lifecycleScope.launch { appFeatureDataStore.setAnyoneButMeEventPopupDismissed(true) } } - fun openAnyoneButMePage() { + fun openAnyoneButMePage(fromPopup: Boolean = false) { + if (fromPopup) { + trackPopupAction(PopupAnalyticsEvent.Action.CLICK_POPUP_IMAGE) + } + trackAnyoneButMeClicked() hide() context.startActivity( Intent(context, WebViewActivity::class.java).apply { @@ -93,10 +104,32 @@ class AnyoneButMeEventPopupController @Inject constructor( } private fun openInstagram() { + trackPopupAction(PopupAnalyticsEvent.Action.GO_INSTA) hide() context.openInBrowser(context.getString(R.string.eatssu_event_instagram_url)) } + private fun closeByUser() { + trackPopupAction(PopupAnalyticsEvent.Action.CLOSE) + hide() + } + + private fun trackPopupAction(action: PopupAnalyticsEvent.Action) { + analyticsTracker.track(PopupAnalyticsEvent.AnyoneButMe(action)) + } + + private fun trackAnyoneButMeClicked() { + lifecycleScope.launch { + val userInfo = getUserCollegeDepartmentUseCase() + analyticsTracker.track( + AnyoneButMeAnalyticsEvent.Clicked( + college = userInfo.userCollege.collegeId.toLong(), + major = userInfo.userDepartment.departmentId.toLong(), + ), + ) + } + } + private fun hide() { canAutoShowOnLaunch = false isPopupVisible.value = false diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt index de593dfa0..d4a9fd7c7 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt @@ -13,6 +13,7 @@ import com.eatssu.android.presentation.base.BaseActivity import com.eatssu.android.presentation.util.showErrorToast import com.eatssu.android.presentation.util.showToast import com.eatssu.android.presentation.util.startActivity +import com.eatssu.common.analytics.LoginAnalyticsEvent import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.enums.ScreenId @@ -52,6 +53,7 @@ class LoginActivity : } binding.ibKakaoLogin.setOnClickListener { + analyticsTracker.track(LoginAnalyticsEvent.Clicked(LoginAnalyticsEvent.Method.KAKAO)) handleKakaoLogin() } } diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt index 103c60be3..1bc632a0d 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt @@ -8,6 +8,8 @@ import com.eatssu.android.domain.usecase.auth.LoginUseCase import com.eatssu.android.domain.usecase.auth.SetAccessTokenUseCase import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase import com.eatssu.android.domain.usecase.user.SetUserEmailUseCase +import com.eatssu.common.analytics.AnalyticsTracker +import com.eatssu.common.analytics.LoginAnalyticsEvent import com.eatssu.common.UiEvent import com.eatssu.common.UiState import com.eatssu.common.UiText @@ -30,6 +32,7 @@ class LoginViewModel @Inject constructor( private val setAccessTokenUseCase: SetAccessTokenUseCase, private val setRefreshTokenUseCase: SetRefreshTokenUseCase, private val setUserEmailUseCase: SetUserEmailUseCase, + private val analyticsTracker: AnalyticsTracker, private val analyticsIdentityManager: AnalyticsIdentityManager, ) : ViewModel() { @@ -58,6 +61,7 @@ class LoginViewModel @Inject constructor( setRefreshTokenUseCase(token.refreshToken) setUserEmailUseCase(email) analyticsIdentityManager.identifyUser(email = email) + analyticsTracker.track(LoginAnalyticsEvent.Completed(LoginAnalyticsEvent.Method.KAKAO)) _uiState.value = UiState.Success(LoginState.LoginSuccess) } diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt index 1c93a58a3..d66d788b9 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt @@ -127,7 +127,6 @@ class MapViewModel @Inject constructor( when (filter) { FilterType.All -> { loadPartnerships() - analyticsTracker.track(MapAnalyticsEvent.AllClicked) } FilterType.Mine -> { diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt b/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt index a7d513c74..d7ee2ed48 100644 --- a/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt +++ b/app/src/main/java/com/eatssu/android/presentation/widget/MealWidgetReceiver.kt @@ -3,6 +3,7 @@ package com.eatssu.android.presentation.widget import android.content.Context import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver +import com.eatssu.android.domain.usecase.widget.LoadRestaurantByFileKeyUseCase import com.eatssu.android.presentation.widget.ui.MealWidget import com.eatssu.common.analytics.AnalyticsTracker import com.eatssu.common.analytics.WidgetAnalyticsEvent @@ -17,6 +18,9 @@ class MealWidgetReceiver : GlanceAppWidgetReceiver() { @Inject lateinit var analyticsTracker: AnalyticsTracker + @Inject + lateinit var loadRestaurantByFileKeyUseCase: LoadRestaurantByFileKeyUseCase + override val glanceAppWidget: GlanceAppWidget get() = MealWidget() @@ -32,15 +36,17 @@ class MealWidgetReceiver : GlanceAppWidgetReceiver() { private fun cleanupWidgetDataStore(context: Context, appWidgetId: Int) { try { runBlocking { + val widgetFileKey = "appWidget-$appWidgetId" val filename = "appWidgetLayout-${appWidgetId}" val dataStoreFile = File(context.filesDir, "datastore/$filename") + val restaurant = loadRestaurantByFileKeyUseCase(widgetFileKey) if (dataStoreFile.exists()) { dataStoreFile.delete() Timber.d("Deleted DataStore file for widget $appWidgetId") } - analyticsTracker.track(WidgetAnalyticsEvent.Removed()) + analyticsTracker.track(WidgetAnalyticsEvent.Removed(restaurant)) } } catch (e: Exception) { diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt index 4ef999897..34af19d5f 100644 --- a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingActivity.kt @@ -16,9 +16,11 @@ import androidx.glance.GlanceId import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.lifecycle.lifecycleScope import com.eatssu.android.analytics.ProvideAnalyticsTracker +import com.eatssu.android.domain.usecase.widget.LoadRestaurantByFileKeyUseCase import com.eatssu.android.domain.usecase.widget.SaveRestaurantByFileKeyUseCase import com.eatssu.android.presentation.widget.MealWorker import com.eatssu.common.analytics.AnalyticsTracker +import com.eatssu.common.analytics.WidgetAnalyticsEvent import com.eatssu.common.enums.Restaurant import com.eatssu.design_system.theme.EatssuTheme import dagger.hilt.android.AndroidEntryPoint @@ -32,6 +34,9 @@ class WidgetSettingActivity : ComponentActivity() { @Inject lateinit var saveRestaurantByFileKeyUseCase: SaveRestaurantByFileKeyUseCase + @Inject + lateinit var loadRestaurantByFileKeyUseCase: LoadRestaurantByFileKeyUseCase + @Inject lateinit var analyticsTracker: AnalyticsTracker @@ -46,6 +51,7 @@ class WidgetSettingActivity : ComponentActivity() { } // 변동 식당만 불러옵니다. 하드코딩 x var selectedRestaurant by rememberSaveable { mutableStateOf(restaurantOptions[0]) } + var previousRestaurant by remember { mutableStateOf(null) } val appWidgetId = intent?.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, @@ -54,12 +60,22 @@ class WidgetSettingActivity : ComponentActivity() { var glanceId by remember { mutableStateOf(null) } val context = LocalContext.current LaunchedEffect(appWidgetId) { - glanceId = - if (appWidgetId != null && appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { - GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId) + glanceId = if (appWidgetId != null && appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId) } else { null } + + previousRestaurant = + if (appWidgetId != null && appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + loadRestaurantByFileKeyUseCase("appWidget-$appWidgetId") + } else { + null + } + + previousRestaurant?.let { savedRestaurant -> + selectedRestaurant = getString(savedRestaurant.displayNameResId) + } } WidgetSettingScreen( @@ -75,12 +91,27 @@ class WidgetSettingActivity : ComponentActivity() { } lifecycleScope.launch { + val widgetFileKey = "appWidget-$appWidgetId" saveRestaurantByFileKeyUseCase( - "appWidget-${appWidgetId}", + widgetFileKey, selectedRestaurantValue ) + when (val before = previousRestaurant) { + null -> analyticsTracker.track(WidgetAnalyticsEvent.Added(selectedRestaurantValue)) + selectedRestaurantValue -> + Unit + + else -> + analyticsTracker.track( + WidgetAnalyticsEvent.Changed( + restaurantBefore = before, + restaurantAfter = selectedRestaurantValue, + ), + ) + } + // 위젯 업데이트 glanceId?.let { MealWidget().update(this@WidgetSettingActivity, it) @@ -90,17 +121,16 @@ class WidgetSettingActivity : ComponentActivity() { MealWorker.enqueue(this@WidgetSettingActivity) Timber.d("선택하기 버튼으로 저장: $selectedRestaurantValue for glanceId: $glanceId") - } - // 결과 설정 - val resultIntent = Intent().apply { - putExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, - appWidgetId ?: AppWidgetManager.INVALID_APPWIDGET_ID - ) + val resultIntent = Intent().apply { + putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetId ?: AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + setResult(RESULT_OK, resultIntent) + finish() } - setResult(RESULT_OK, resultIntent) - finish() }, onBack = { finish() } ) diff --git a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt index 10681885b..a6e4e55b0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt +++ b/app/src/main/java/com/eatssu/android/presentation/widget/ui/WidgetSettingScreen.kt @@ -16,8 +16,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.eatssu.android.R -import com.eatssu.android.analytics.LocalAnalyticsTracker -import com.eatssu.common.analytics.WidgetAnalyticsEvent import com.eatssu.android.presentation.util.asString import com.eatssu.common.enums.Restaurant import com.eatssu.design_system.component.EatSsuButton @@ -34,8 +32,6 @@ fun WidgetSettingScreen( onConfirm: (Restaurant) -> Unit = {}, onBack: () -> Unit = {} // 뒤로가기 동작을 위한 람다 추가 ) { - val analyticsTracker = LocalAnalyticsTracker.current - // onClick 람다에서 LocalContext 접근이 불가하므로 Composable 레벨에서 미리 매핑 생성 val restaurantDisplayNameMap = Restaurant.getVariableRestaurantList() .associateBy { it.toUiText().asString() } @@ -77,7 +73,6 @@ fun WidgetSettingScreen( ?: Restaurant.HAKSIK onConfirm(selectedRestaurantEnum) - analyticsTracker.track(WidgetAnalyticsEvent.Added(selectedRestaurantEnum)) } ) } diff --git a/app/src/test/java/com/eatssu/android/analytics/DefaultAnalyticsTrackerTest.kt b/app/src/test/java/com/eatssu/android/analytics/DefaultAnalyticsTrackerTest.kt index b3ee49a27..8d10dfd86 100644 --- a/app/src/test/java/com/eatssu/android/analytics/DefaultAnalyticsTrackerTest.kt +++ b/app/src/test/java/com/eatssu/android/analytics/DefaultAnalyticsTrackerTest.kt @@ -30,7 +30,7 @@ class DefaultAnalyticsTrackerTest { val second = FakeAnalyticsTracker(id = "duplicate") val analyticsTracker = DefaultAnalyticsTracker(setOf(first, second)) - analyticsTracker.track(MapAnalyticsEvent.AllClicked) + analyticsTracker.track(MapAnalyticsEvent.EntryClicked) assertEquals(1, first.events.size + second.events.size) } @@ -57,7 +57,7 @@ class DefaultAnalyticsTrackerTest { val failingTracker = FakeAnalyticsTracker(id = "firebase", failOnTrack = true) val healthyTracker = FakeAnalyticsTracker(id = "posthog") val analyticsTracker = DefaultAnalyticsTracker(setOf(failingTracker, healthyTracker)) - val event = MapAnalyticsEvent.AllClicked + val event = MapAnalyticsEvent.EntryClicked analyticsTracker.track(event) diff --git a/app/src/test/java/com/eatssu/android/analytics/FirebaseAnalyticsTrackerTest.kt b/app/src/test/java/com/eatssu/android/analytics/FirebaseAnalyticsTrackerTest.kt index 460638f93..e5f54b1c9 100644 --- a/app/src/test/java/com/eatssu/android/analytics/FirebaseAnalyticsTrackerTest.kt +++ b/app/src/test/java/com/eatssu/android/analytics/FirebaseAnalyticsTrackerTest.kt @@ -38,7 +38,7 @@ class FirebaseAnalyticsTrackerTest { mapOf( "rating" to 5L, "likes" to 2L, - "photo_attached" to true, + "photo_attached" to 1L, ), payload.properties, ) diff --git a/app/src/test/java/com/eatssu/android/analytics/PostHogAnalyticsTrackerTest.kt b/app/src/test/java/com/eatssu/android/analytics/PostHogAnalyticsTrackerTest.kt index 973216223..98cbd514e 100644 --- a/app/src/test/java/com/eatssu/android/analytics/PostHogAnalyticsTrackerTest.kt +++ b/app/src/test/java/com/eatssu/android/analytics/PostHogAnalyticsTrackerTest.kt @@ -2,6 +2,7 @@ package com.eatssu.android.analytics import com.eatssu.common.analytics.AnalyticsIdentity import com.eatssu.common.analytics.AppAnalyticsEvent +import com.eatssu.common.analytics.LoginAnalyticsEvent import com.eatssu.common.analytics.WidgetAnalyticsEvent import com.eatssu.common.enums.LaunchPath import com.eatssu.common.enums.Restaurant @@ -19,6 +20,14 @@ class PostHogAnalyticsTrackerTest { assertEquals(mapOf("launch_path" to "widget"), payload.properties) } + @Test + fun `remote notification launch payload keeps distinct path`() { + val payload = AppAnalyticsEvent.Launch(LaunchPath.REMOTE_NOTIFICATION).toPayload() + + assertEquals("app_launch", payload.eventName) + assertEquals(mapOf("launch_path" to "remote_notification"), payload.properties) + } + @Test fun `widget removal without restaurant omits properties`() { val payload = WidgetAnalyticsEvent.Removed().toPayload() @@ -57,4 +66,29 @@ class PostHogAnalyticsTrackerTest { assertEquals("add_widget", payload.eventName) assertEquals(mapOf("restaurants" to "haksik"), payload.properties) } + + @Test + fun `widget change payload keeps before and after keys`() { + val payload = WidgetAnalyticsEvent.Changed( + restaurantBefore = Restaurant.DODAM, + restaurantAfter = Restaurant.HAKSIK, + ).toPayload() + + assertEquals("change_widget", payload.eventName) + assertEquals( + mapOf( + "restaurant_before" to "dodam", + "restaurant_after" to "haksik", + ), + payload.properties, + ) + } + + @Test + fun `login payload keeps method schema`() { + val payload = LoginAnalyticsEvent.Clicked(LoginAnalyticsEvent.Method.KAKAO).toPayload() + + assertEquals("click_login", payload.eventName) + assertEquals(mapOf("method" to "kakao"), payload.properties) + } } diff --git a/app/src/test/java/com/eatssu/android/presentation/login/LoginViewModelBehaviorSpec.kt b/app/src/test/java/com/eatssu/android/presentation/login/LoginViewModelBehaviorSpec.kt index 9181c6562..db6973a84 100644 --- a/app/src/test/java/com/eatssu/android/presentation/login/LoginViewModelBehaviorSpec.kt +++ b/app/src/test/java/com/eatssu/android/presentation/login/LoginViewModelBehaviorSpec.kt @@ -11,6 +11,8 @@ import com.eatssu.android.test.AppBehaviorSpec import com.eatssu.android.test.assertToast import com.eatssu.android.test.awaitToastEvent import com.eatssu.android.test.sampleToken +import com.eatssu.common.analytics.AnalyticsTracker +import com.eatssu.common.analytics.LoginAnalyticsEvent import com.eatssu.common.UiState import com.eatssu.common.enums.DeviceType import com.eatssu.common.enums.ToastType @@ -36,6 +38,7 @@ class LoginViewModelBehaviorSpec : AppBehaviorSpec({ val setAccessTokenUseCase = mockk() val setRefreshTokenUseCase = mockk() val setUserEmailUseCase = mockk() + val analyticsTracker = mockk(relaxed = true) val analyticsIdentityManager = mockk(relaxed = true) every { setAccessTokenUseCase(any()) } just Runs @@ -48,6 +51,7 @@ class LoginViewModelBehaviorSpec : AppBehaviorSpec({ setAccessTokenUseCase = setAccessTokenUseCase, setRefreshTokenUseCase = setRefreshTokenUseCase, setUserEmailUseCase = setUserEmailUseCase, + analyticsTracker = analyticsTracker, analyticsIdentityManager = analyticsIdentityManager, ) coEvery { loginUseCase("a@b.com", "pid", DeviceType.ANDROID) } returns null @@ -72,6 +76,7 @@ class LoginViewModelBehaviorSpec : AppBehaviorSpec({ setAccessTokenUseCase = setAccessTokenUseCase, setRefreshTokenUseCase = setRefreshTokenUseCase, setUserEmailUseCase = setUserEmailUseCase, + analyticsTracker = analyticsTracker, analyticsIdentityManager = analyticsIdentityManager, ) val token = sampleToken("acc", "ref") @@ -86,6 +91,11 @@ class LoginViewModelBehaviorSpec : AppBehaviorSpec({ verify { setRefreshTokenUseCase("ref") } coVerify { setUserEmailUseCase("a@b.com") } verify { analyticsIdentityManager.identifyUser(email = "a@b.com") } + verify { + analyticsTracker.track( + LoginAnalyticsEvent.Completed(LoginAnalyticsEvent.Method.KAKAO), + ) + } viewModel.uiState.value shouldBe UiState.Success(LoginState.LoginSuccess) } } @@ -99,6 +109,7 @@ class LoginViewModelBehaviorSpec : AppBehaviorSpec({ setAccessTokenUseCase = mockk(relaxed = true), setRefreshTokenUseCase = mockk(relaxed = true), setUserEmailUseCase = mockk(relaxed = true), + analyticsTracker = mockk(relaxed = true), analyticsIdentityManager = mockk(relaxed = true), ) diff --git a/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt b/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt index a1aacf867..1fa4958d9 100644 --- a/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt +++ b/app/src/test/java/com/eatssu/android/presentation/map/MapViewModelBehaviorSpec.kt @@ -127,7 +127,7 @@ class MapViewModelBehaviorSpec : AppBehaviorSpec({ analyticsTracker = analyticsTracker, ) - then("필터에 맞는 목록을 로드하고 이벤트 로깅을 수행한다") { + then("필터에 맞는 목록을 로드하고 Mine 필터 이벤트를 로깅한다") { runTest { eventually(2.seconds) { val initial = viewModel.uiState.value as UiState.Success @@ -141,7 +141,6 @@ class MapViewModelBehaviorSpec : AppBehaviorSpec({ allState.data.selectedFilter shouldBe FilterType.All allState.data.partnerships shouldBe allPartnerships } - verify(atLeast = 1) { analyticsTracker.track(MapAnalyticsEvent.AllClicked) } viewModel.setFilter(FilterType.Mine) eventually(2.seconds) { diff --git a/core/common/src/main/java/com/eatssu/common/analytics/AnalyticsEvent.kt b/core/common/src/main/java/com/eatssu/common/analytics/AnalyticsEvent.kt index 75b969b12..c1254bf07 100644 --- a/core/common/src/main/java/com/eatssu/common/analytics/AnalyticsEvent.kt +++ b/core/common/src/main/java/com/eatssu/common/analytics/AnalyticsEvent.kt @@ -26,6 +26,32 @@ sealed interface AppAnalyticsEvent : AnalyticsEvent { } } +sealed interface LoginAnalyticsEvent : AnalyticsEvent { + enum class Method(val value: String) { + KAKAO("kakao"), + APPLE("apple"), + GUEST("guest"), + } + + data class Clicked( + val method: Method, + ) : LoginAnalyticsEvent { + override val eventName = "click_login" + override val properties = buildMap { + put("method", method.value) + } + } + + data class Completed( + val method: Method, + ) : LoginAnalyticsEvent { + override val eventName = "complete_login" + override val properties = buildMap { + put("method", method.value) + } + } +} + sealed interface CafeteriaAnalyticsEvent : AnalyticsEvent { data class RestaurantInfoClicked( val restaurant: Restaurant, @@ -48,7 +74,7 @@ sealed interface CafeteriaAnalyticsEvent : AnalyticsEvent { data class DaySelected( val day: String, ) : CafeteriaAnalyticsEvent { - override val eventName = "click_day" + override val eventName = "select_day" override val properties = buildMap { put("day", day.toWeekdayCode()) } @@ -79,13 +105,13 @@ sealed interface ReviewAnalyticsEvent : AnalyticsEvent { override val properties = buildMap { put("rating", rating) put("likes", likes) - put("photo_attached", photoAttached) + put("photo_attached", if (photoAttached) 1L else 0L) } } } sealed interface MapAnalyticsEvent : AnalyticsEvent { - object AllClicked : MapAnalyticsEvent { + object EntryClicked : MapAnalyticsEvent { override val eventName = "click_map" override val properties = emptyMap() } @@ -115,6 +141,45 @@ sealed interface MapAnalyticsEvent : AnalyticsEvent { } } +sealed interface AnyoneButMeAnalyticsEvent : AnalyticsEvent { + data class Clicked( + val college: Long, + val major: Long, + ) : AnyoneButMeAnalyticsEvent { + override val eventName = "click_plz_not_me" + override val properties = buildMap { + put("college", college) + put("major", major) + } + } +} + +sealed interface MyPageAnalyticsEvent : AnalyticsEvent { + object MenuClicked : MyPageAnalyticsEvent { + override val eventName = "click_mypage_menu" + override val properties = emptyMap() + } +} + +sealed interface PopupAnalyticsEvent : AnalyticsEvent { + enum class Action(val value: String) { + CLICK_POPUP_IMAGE("click_popup_image"), + GO_INSTA("go_insta"), + NOT_SHOW_AGAIN("not_show_again"), + CLOSE("close"), + } + + data class AnyoneButMe( + val action: Action, + ) : PopupAnalyticsEvent { + override val eventName = "popup_event" + override val properties = buildMap { + put("popup_name", "plz_not_me") + put("popup_action", action.value) + } + } +} + sealed interface WidgetAnalyticsEvent : AnalyticsEvent { data class Added( val restaurant: Restaurant, @@ -135,11 +200,13 @@ sealed interface WidgetAnalyticsEvent : AnalyticsEvent { } data class Changed( - val restaurant: Restaurant, + val restaurantBefore: Restaurant, + val restaurantAfter: Restaurant, ) : WidgetAnalyticsEvent { override val eventName = "change_widget" override val properties = buildMap { - put("restaurants", restaurant.value) + put("restaurant_before", restaurantBefore.value) + put("restaurant_after", restaurantAfter.value) } } } diff --git a/core/common/src/test/java/com/eatssu/common/enums/EnumsBehaviorSpec.kt b/core/common/src/test/java/com/eatssu/common/enums/EnumsBehaviorSpec.kt index b9e196be3..c162b3260 100644 --- a/core/common/src/test/java/com/eatssu/common/enums/EnumsBehaviorSpec.kt +++ b/core/common/src/test/java/com/eatssu/common/enums/EnumsBehaviorSpec.kt @@ -55,6 +55,12 @@ class EnumsBehaviorSpec : BehaviorSpec({ ) } } + + `when`("analytics value를 확인하면") { + then("기숙사 식당은 설계 문서 기준 값으로 노출된다") { + Restaurant.DORMITORY.value shouldBe "dormitory" + } + } } given("ReportType") {