Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e8bff17
[ADD/#386] 믹스패널 의존성 추가
Hyobeen-Park Jul 5, 2025
9bd5d74
[FEAT/#386] 믹스패널 트래커 구현
Hyobeen-Park Jul 5, 2025
0a22c86
[FEAT/#386] 로컬 트래커 세팅
Hyobeen-Park Jul 5, 2025
c9adc9a
[FEAT/#386] 전환 분석용 이벤트 심기
Hyobeen-Park Jul 5, 2025
c2cf3c8
[CHORE/#386] 실수 바로잡기...
Hyobeen-Park Jul 5, 2025
04d051f
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Jul 8, 2025
320c149
[FEAT/#386] 탭 진입 이벤트 추가
Hyobeen-Park Jul 8, 2025
3d05a8a
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Oct 2, 2025
efcb99f
[FEAT/#386] 상세리뷰 진입 이벤트 추가
Hyobeen-Park Oct 2, 2025
f4d6ec8
[FEAT/#386] 내 리뷰 수정 완료 이벤트 추가
Hyobeen-Park Oct 2, 2025
5781b8a
[FEAT/#386] 유저/마이페이지 진입 이벤트 추가
Hyobeen-Park Oct 2, 2025
1364f73
[FEAT/#386] 온보딩 이벤트 추가
Hyobeen-Park Oct 2, 2025
f3a852f
[FEAT/#386] 스푼뽑기 이벤트 추가
Hyobeen-Park Oct 2, 2025
75e710f
[FEAT/#386] 등록 이벤트 추가
Hyobeen-Park Oct 3, 2025
cade0fc
[FEAT/#386] 유저 프로필 저장
Hyobeen-Park Oct 3, 2025
7c8919f
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Oct 3, 2025
9839722
[MOD/#386] 구조 변경
Hyobeen-Park Oct 3, 2025
eb0e9c7
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Oct 4, 2025
8077f5a
[FEAT/#386] common events 추가
Hyobeen-Park Oct 4, 2025
bcfa1c8
[FEAT/#386] onboarding events 추가
Hyobeen-Park Oct 4, 2025
122a69c
[FEAT/#386] spoon draw & register events
Hyobeen-Park Oct 4, 2025
f6dc277
[FEAT/#386] map events
Hyobeen-Park Oct 4, 2025
4ad657e
[FEAT/#386] explore events
Hyobeen-Park Oct 4, 2025
03b7a03
[FEAT/#386] mypage events
Hyobeen-Park Oct 4, 2025
67682d7
[FEAT/#386] review detail events
Hyobeen-Park Oct 4, 2025
80d50b2
[CHORE/#386] mixpanel version upgrade
Hyobeen-Park Oct 4, 2025
c6de6e6
[CHORE/#386] update pr-checker
Hyobeen-Park Oct 4, 2025
9c22fa8
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Oct 5, 2025
ae0ad49
[MOD/#386] explore events 수정
Hyobeen-Park Oct 5, 2025
1ba8be0
[MOD/#386] category 속성 추가
Hyobeen-Park Oct 5, 2025
0056ac3
[MOD/#386] 리뷰 수정 시 빠진 속성 추가
Hyobeen-Park Oct 5, 2025
a54d41b
[FEAT/#386] 로그아웃, 회원탈퇴 시 userProfile 초기화
Hyobeen-Park Oct 7, 2025
71131fa
[MOD/#386] place_viewed 트래킹 위치 수정
Hyobeen-Park Oct 7, 2025
a27aefd
[REFACTOR/#386] raw string -> jsonObject로 수정
Hyobeen-Park Oct 7, 2025
fbe4eca
[CHORE/#386] mypage tabEntered 트래킹 위치 수정
Hyobeen-Park Oct 7, 2025
3842fbc
Merge remote-tracking branch 'origin/develop' into feat/#386-add-mixp…
Hyobeen-Park Oct 9, 2025
ec840e5
[MOD/#386] ReviewTrackingModel 생성
Hyobeen-Park Oct 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr_builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
NATIVE_APP_KEY: ${{ secrets.NATIVE_APP_KEY }}
MIXPANEL_KEY: ${{ secrets.MIXPANEL_KEY }}
run: |
echo prod.base.url=\"$PROD_BASE_URL\" >> local.properties
echo dev.base.url=\"$DEV_BASE_URL\" >> local.properties
Expand All @@ -57,6 +58,8 @@ jobs:
echo storePassword=$RELEASE_STORE_PASSWORD >> local.properties
echo native.app.key=\"$NATIVE_APP_KEY\" >> local.properties
echo nativeAppKey=$NATIVE_APP_KEY >> local.properties
echo mixpanelDevKey=\"$MIXPANEL_KEY\" >> local.properties
echo mixpanelProdKey=\"$MIXPANEL_KEY\" >> local.properties

- name: Create Google Services JSON
env:
Expand Down
14 changes: 14 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ android {
"BASE_URL",
properties.getProperty("dev.base.url")
)

buildConfigField(
"String",
"MIXPANEL_KEY",
properties["mixpanelDevKey"] as? String ?: ""
)
Comment on lines +61 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

빈 키 fallback이 BuildConfig 생성을 깨뜨립니다.

buildConfigField의 세 번째 인자는 리터럴이어야 하는데, fallback으로 전달한 ""는 따옴표가 없어 public static final String MIXPANEL_KEY = ; 형태로 생성돼 바로 컴파일 에러가 납니다. 최소한 "" 대신 "\"\""을 넘기거나, 값이 없을 때는 명시적으로 에러를 던지도록 처리해 주세요.

             buildConfigField(
                 "String",
                 "MIXPANEL_KEY",
-                properties["mixpanelDevKey"] as? String ?: ""
+                (properties["mixpanelDevKey"] as? String) ?: "\"\""
             )
@@
             buildConfigField(
                 "String",
                 "MIXPANEL_KEY",
-                properties["mixpanelProdKey"] as? String ?: ""
+                (properties["mixpanelProdKey"] as? String) ?: "\"\""
             )

Also applies to: 72-75

🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 58-61 (and similarly lines 72-75), the
third argument to buildConfigField must be a literal Java expression; passing an
unquoted empty string from the Kotlin expression produces invalid generated code
(e.g. public static final String MIXPANEL_KEY = ;). Change the call to provide a
properly quoted string literal when the property is absent (e.g. return "\"\""
for empty) or explicitly throw a Gradle exception when the required property is
missing so buildConfigField always receives a valid literal; apply the same fix
to the other occurrence at lines 72-75.

}

release {
Expand All @@ -65,6 +71,12 @@ android {
properties.getProperty("prod.base.url")
)

buildConfigField(
"String",
"MIXPANEL_KEY",
properties["mixpanelProdKey"] as? String ?: ""
)

isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
Expand Down Expand Up @@ -146,6 +158,8 @@ dependencies {
implementation(libs.pebble)
implementation(libs.jakewharton.process.phoenix)
implementation(libs.play.services.oss.licenses)

implementation(libs.mixpanel)
}

ktlint {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.spoony.spoony.core.analytics

import android.content.Context
import com.mixpanel.android.mpmetrics.MixpanelAPI
import com.spoony.spoony.BuildConfig.MIXPANEL_KEY
import dagger.hilt.android.qualifiers.ApplicationContext
import jakarta.inject.Inject
import org.json.JSONObject
import timber.log.Timber

class MixPanelTracker @Inject constructor(
@ApplicationContext private val context: Context
) {
private val mixpanel = MixpanelAPI.getInstance(
context,
MIXPANEL_KEY,
false
)

fun setUserProfile(userId: String, properties: Map<String, Any>) {
mixpanel.identify(userId)
properties.forEach { (key, value) ->
mixpanel.people.set(key, value)
}
}

fun resetUserProfile() {
mixpanel.reset()
}

fun track(eventName: String) {
Timber.tag("mixpanel").d(eventName)
mixpanel.track(eventName)
}

fun track(eventName: String, properties: JSONObject) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties)
}
Comment on lines +31 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Release 로그에 PII 노출 위험: Timber에 이벤트/프로퍼티 출력

review_id, author_user_id, place_name 등 민감 정보가 릴리스에서도 Logcat으로 노출될 수 있습니다. 디버그에서만 로깅하거나 완전히 제거해 주세요.

아래처럼 디버그 빌드에서만 로깅하도록 가드하는 것을 권장합니다:

 import org.json.JSONObject
 import timber.log.Timber
+import com.spoony.spoony.BuildConfig

 ...
     fun track(eventName: String) {
-        Timber.tag("mixpanel").d(eventName)
+        if (BuildConfig.DEBUG) {
+            Timber.tag("mixpanel").d(eventName)
+        }
         mixpanel.track(eventName)
     }

     fun track(eventName: String, properties: JSONObject) {
-        Timber.tag("mixpanel").d("$eventName $properties")
+        if (BuildConfig.DEBUG) {
+            Timber.tag("mixpanel").d("$eventName $properties")
+        }
         mixpanel.track(eventName, properties)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun track(eventName: String) {
Timber.tag("mixpanel").d(eventName)
mixpanel.track(eventName)
}
fun track(eventName: String, properties: JSONObject) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties)
}
import org.json.JSONObject
import timber.log.Timber
import com.spoony.spoony.BuildConfig
// … other imports and class boilerplate …
fun track(eventName: String) {
if (BuildConfig.DEBUG) {
Timber.tag("mixpanel").d(eventName)
}
mixpanel.track(eventName)
}
fun track(eventName: String, properties: JSONObject) {
if (BuildConfig.DEBUG) {
Timber.tag("mixpanel").d("$eventName $properties")
}
mixpanel.track(eventName, properties)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/spoony/spoony/core/analytics/MixPanelTracker.kt around
lines 31 to 39, the current Timber.debug calls output full event names and
JSONObject properties (which can contain PII) to Logcat; wrap or remove these
logs so they run only in debug builds (e.g., guard with BuildConfig.DEBUG) and
avoid logging raw properties — log only non-sensitive metadata or the event name
(or a sanitized/hashed representation) inside the debug-only guard; apply this
change to both track(eventName: String) and track(eventName: String, properties:
JSONObject) overloads.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import jakarta.inject.Inject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javax.inject.Inject 이거랑 혼용이 있어요! 하나로 통일해주세요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 관련 파일들만 javax -> jakarta로 수정할게요~~

import org.json.JSONObject

class AnalyticsEvents @Inject constructor(
private val tracker: MixPanelTracker
) {
fun appOpen() {
tracker.track("app_open")
}

fun signupCompleted(signupMethod: String) {
tracker.track(
eventName = "signup_completed",
properties = JSONObject().apply {
put("signup_method", signupMethod)
}
)
}

fun loginSuccess() {
tracker.track("login_success")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import com.spoony.spoony.core.analytics.model.ReviewTrackingModel
import jakarta.inject.Inject
import org.json.JSONArray
import org.json.JSONObject

class CommonEvents @Inject constructor(
private val tracker: MixPanelTracker
) {
fun tabEntered(tabName: String) {
tracker.track(
eventName = "tab_entered",
properties = JSONObject().apply {
put("tab_name", tabName)
}
)
}

fun reviewViewed(
reviewTrackingModel: ReviewTrackingModel,
isSelfReview: Boolean,
isFollowedUserReview: Boolean,
isSavedReview: Boolean
) {
tracker.track(
eventName = "review_viewed",
properties = JSONObject().apply {
put("review_id", reviewTrackingModel.reviewId)
put("author_user_id", reviewTrackingModel.authorUserId)
put("place_name", reviewTrackingModel.placeName)
put("category", reviewTrackingModel.category)
put("menu_count", reviewTrackingModel.menuCount)
put("satisfaction_score", reviewTrackingModel.satisfactionScore)
put("review_length", reviewTrackingModel.reviewLength)
put("photo_count", reviewTrackingModel.photoCount)
put("has_disappointment", reviewTrackingModel.hasDisappointment)
put("saved_count", reviewTrackingModel.savedCount)
put("is_self_review", isSelfReview)
put("is_followed_user_review", isFollowedUserReview)
put("is_saved_review", isSavedReview)
}
)
}

fun reviewEdited(
reviewTrackingModel: ReviewTrackingModel
) {
tracker.track(
eventName = "review_edited",
properties = JSONObject().apply {
put("review_id", reviewTrackingModel.reviewId)
put("author_user_id", reviewTrackingModel.authorUserId)
put("place_name", reviewTrackingModel.placeName)
put("category", reviewTrackingModel.category)
put("menu_count", reviewTrackingModel.menuCount)
put("satisfaction_score", reviewTrackingModel.satisfactionScore)
put("review_length", reviewTrackingModel.reviewLength)
put("photo_count", reviewTrackingModel.photoCount)
put("has_disappointment", reviewTrackingModel.hasDisappointment)
put("saved_count", reviewTrackingModel.savedCount)
}
)
}

fun profileViewed(
profileUserId: Int,
isSelfProfile: Boolean,
isFollowingProfileUser: Boolean
// entryPoint: String
) {
tracker.track(
eventName = "profile_viewed",
properties = JSONObject().apply {
put("profile_user_id", profileUserId)
put("is_self_profile", isSelfProfile)
put("is_following_profile_user", isFollowingProfileUser)
}
)
}

fun followUser(
followedUserId: Int,
entryPoint: String
) {
tracker.track(
eventName = "follow_user",
properties = JSONObject().apply {
put("followed_user_id", followedUserId)
put("entry_point", entryPoint)
}
)
}

fun unfollowUser(
unfollowedUserId: Int,
entryPoint: String
) {
tracker.track(
eventName = "unfollow_user",
properties = JSONObject().apply {
put("unfollowed_user_id", unfollowedUserId)
put("entry_point", entryPoint)
}
)
}

fun followUserFromReview(
reviewTrackingModel: ReviewTrackingModel
) {
tracker.track(
eventName = "follow_user_from_review",
properties = JSONObject().apply {
put("review_id", reviewTrackingModel.reviewId)
put("author_user_id", reviewTrackingModel.authorUserId)
put("place_name", reviewTrackingModel.placeName)
put("category", reviewTrackingModel.category)
put("menu_count", reviewTrackingModel.menuCount)
put("satisfaction_score", reviewTrackingModel.satisfactionScore)
put("review_length", reviewTrackingModel.reviewLength)
put("photo_count", reviewTrackingModel.photoCount)
put("has_disappointment", reviewTrackingModel.hasDisappointment)
put("saved_count", reviewTrackingModel.savedCount)
put("entry_point", "review")
}
)
}

fun unfollowUserFromReview(
reviewTrackingModel: ReviewTrackingModel
) {
tracker.track(
eventName = "unfollow_user_from_review",
properties = JSONObject().apply {
put("review_id", reviewTrackingModel.reviewId)
put("author_user_id", reviewTrackingModel.authorUserId)
put("place_name", reviewTrackingModel.placeName)
put("category", reviewTrackingModel.category)
put("menu_count", reviewTrackingModel.menuCount)
put("satisfaction_score", reviewTrackingModel.satisfactionScore)
put("review_length", reviewTrackingModel.reviewLength)
put("photo_count", reviewTrackingModel.photoCount)
put("has_disappointment", reviewTrackingModel.hasDisappointment)
put("saved_count", reviewTrackingModel.savedCount)
put("entry_point", "review")
}
)
}

fun filterApplied(
pageApplied: String,
localReviewFilter: Boolean? = null,
regionFilters: List<String> = listOf(),
categoryFilters: List<String> = listOf(),
ageGroupFilters: List<String> = listOf()
) {
tracker.track(
eventName = "filter_applied",
properties = JSONObject().apply {
put("page_applied", pageApplied)
put("local_review_filter", localReviewFilter)
put("region_filters", JSONArray(regionFilters))
put("category_filters", JSONArray(categoryFilters))
put("age_group_filters", JSONArray(ageGroupFilters))
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import jakarta.inject.Inject
import org.json.JSONObject

class ExploreEvents @Inject constructor(
private val tracker: MixPanelTracker
) {
fun sortSelected(sortType: String) {
tracker.track(
eventName = "sort_selected",
properties = JSONObject().apply {
put("sort_type", sortType)
}
)
}

fun exploreSearched(
searchTargetType: String,
searchTerm: String
) {
tracker.track(
eventName = "explore_searched",
properties = JSONObject().apply {
put("search_target_type", searchTargetType)
put("search_term", searchTerm)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import jakarta.inject.Inject
import org.json.JSONObject

class MapEvents @Inject constructor(
private val tracker: MixPanelTracker
) {
fun mapSearched(
locationType: String,
searchTerm: String
) {
tracker.track(
eventName = "map_searched",
properties = JSONObject().apply {
put("location_type", locationType)
put("search_term", searchTerm)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.spoony.spoony.core.analytics.events

import androidx.compose.runtime.staticCompositionLocalOf
import jakarta.inject.Inject

val LocalTracker = staticCompositionLocalOf<MixPanelEvents> {
error("No MixPanelEvents provided")
}

class MixPanelEvents @Inject constructor(
val userProperties: MixPanelUserProperties,
val analyticsEvents: AnalyticsEvents,
val commonEvents: CommonEvents,
val onboardingEvents: OnboardingEvents,
val spoonDrawEvents: SpoonDrawEvents,
val mapEvents: MapEvents,
val exploreEvents: ExploreEvents,
val registerEvents: RegisterEvents,
val mypageEvents: MypageEvents,
val reviewDetailEvents: ReviewDetailEvents
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
import jakarta.inject.Inject

class MixPanelUserProperties @Inject constructor(
private val tracker: MixPanelTracker
) {
fun setUserProfile(userId: String, properties: Map<String, Any>) {
tracker.setUserProfile(userId = userId, properties = properties)
}

fun resetUserProfile() {
tracker.resetUserProfile()
}
}
Loading