Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 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_checker.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,54 @@
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 javax.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: String) {
Timber.tag("mixpanel").d("$eventName $properties")
mixpanel.track(eventName, properties.toJsonObject())
}

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

Choose a reason for hiding this comment

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

현재 Events 클래스들에서 모두 JSONObject를 직접 만들어 track 함수를 호출하고 있어요. String 타입의 properties를 받는 오버로딩은 내부에서 바로 JSONObject로 변환하는데 track(eventName: String, properties: String) 오버로딩을 제거해서 MixPanelTracker의 인터페이스를 더 단순하게 만드는 게 지금 기획의 요구에서 가능할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

요건 구현 방식을 바꿔서 2개가 생긴건데요 혹시 나중에 쓸일이 있을까 싶어서 놔두긴 했는데 뭐.. 필요한 사람이 다시 만드는걸로 하죠ㅋㅋ

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.


private fun String.toJsonObject(): JSONObject {
return try {
JSONObject(this)
} catch (e: Exception) {
Timber.e(e)
JSONObject()
}
}
}
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,206 @@
package com.spoony.spoony.core.analytics.events

import com.spoony.spoony.core.analytics.MixPanelTracker
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(
reviewId: Int,
authorUserId: Int,
placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Double,
reviewLength: Int,
photoCount: Int,
hasDisappointment: Boolean,
savedCount: Int,
isSelfReview: Boolean,
isFollowedUserReview: Boolean,
isSavedReview: Boolean
Comment on lines 21 to 25
Copy link
Member

Choose a reason for hiding this comment

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

이런식으로 파라미터가 10개가 넘어가는 경우가 꽤 있어요. 어떤 값이 어떤 파라미터에 해당하는지 한번에 파악하기 어렵다고 생각이 들어요.
data class로 만들어서 객체로 전달하는건 어떻게 생각하십니까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

흠냐냐 사실 이벤트를 위한 모델을 만드는 것에 대해 조금 많이 회의적이긴 한데요ㅎㅎ 겹치는 파라미터가 너무 많기도 하고 해서 일단 가장 많이 겹치는 파라미터만 data class로 만들었습니다!!

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

fun reviewEdited(
reviewId: Int,
authorUserId: Int,
placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Float,
reviewLength: Int,
photoCount: Int,
hasDisappointment: Boolean,
savedCount: Int
// entryPoint: String
) {
tracker.track(
eventName = "review_edited",
properties = JSONObject().apply {
put("review_id", reviewId)
put("author_user_id", authorUserId)
put("place_name", placeName)
put("category", category)
put("menu_count", menuCount)
put("satisfaction_score", satisfactionScore)
put("review_length", reviewLength)
put("photo_count", photoCount)
put("has_disappointment", hasDisappointment)
put("saved_count", 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(
reviewId: Int,
authorUserId: Int,
placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Double,
Copy link
Member

Choose a reason for hiding this comment

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

이거 Float 타입인것도 있더라구요 (e.g. satisfactionScore: Double) 통일하면 곤란한거일까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아 이거 왜 이렇게 됐는지 알아버렸어요... 지금 reviewEdited()만 Float이고 나머지는 Double인데요! registerState에 있는 userSatisfactionValue가 Float타입이더라구요😅 명세서 확인해보니 dto도 Double이라 아요에서도 Double로 전달할 것 같아서 요건 형변환해서 보내는걸로 하겠습니다!

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

fun unfollowUserFromReview(
reviewId: Int,
authorUserId: Int,
placeName: String,
category: String,
menuCount: Int,
satisfactionScore: Double,
reviewLength: Int,
photoCount: Int,
hasDisappointment: Boolean,
savedCount: Int
) {
tracker.track(
eventName = "unfollow_user_from_review",
properties = JSONObject().apply {
put("review_id", reviewId)
put("author_user_id", authorUserId)
put("place_name", placeName)
put("category", category)
put("menu_count", menuCount)
put("satisfaction_score", satisfactionScore)
put("review_length", reviewLength)
put("photo_count", photoCount)
put("has_disappointment", hasDisappointment)
put("saved_count", 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)
}
)
}
}
Loading