Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.navigationSafeArgs)
alias(libs.plugins.googleServices)
alias(libs.plugins.firebaseCrashlytics)
alias(libs.plugins.firebasePerf)
}

val properties = Properties().apply {
Expand All @@ -24,8 +25,8 @@ android {
applicationId = "com.kuit.findu"
minSdk = 28
targetSdk = 35
versionCode = 19
versionName = "1.1.4"
versionCode = 21
versionName = "1.1.6"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "GPT_KEY", properties["GPT_KEY"].toString())
Expand Down Expand Up @@ -171,6 +172,7 @@ dependencies {
implementation(libs.firebase.analytics.ktx)
implementation(libs.firebase.config.ktx)
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.perf)

// AdMob
implementation("com.google.android.gms:play-services-ads:23.1.0")
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- CameraX -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:ignore="AdvertisingIdPolicy" />

<queries>
Expand Down Expand Up @@ -47,6 +48,11 @@
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-7675272869453438~5374193050" />

<!-- Performance -->
<meta-data
android:name="firebase_performance_logcat_enabled"
android:value="true" />

<!-- Kakao Login -->
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import android.os.SystemClock
import android.util.Log
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.firebase.perf.FirebasePerformance
import com.kuit.findu.R
import com.kuit.findu.domain.model.ProtectAnimal
import com.kuit.findu.presentation.type.AnimalStateType
Expand All @@ -48,8 +54,33 @@ fun HomeProtectAnimalCard(
Box(
modifier = Modifier.size(height = 100.dp, width = 120.dp),
) {
val context = LocalContext.current
val imageRequest = remember(animal.thumbnailImageUrl) {
val trace = FirebasePerformance.getInstance().newTrace("home_protect_image_load")
var startTime = 0L
ImageRequest.Builder(context)
.data(animal.thumbnailImageUrl)
.listener(
onStart = {
startTime = SystemClock.elapsedRealtime()
trace.start()
},
onSuccess = { _, _ ->
val duration = SystemClock.elapsedRealtime() - startTime
Log.d("ImagePerf", "보호동물 이미지 로딩 완료: ${duration}ms | ${animal.thumbnailImageUrl}")
trace.stop()
},
onError = { _, _ ->
val duration = SystemClock.elapsedRealtime() - startTime
Log.e("ImagePerf", "보호동물 이미지 로딩 실패: ${duration}ms | ${animal.thumbnailImageUrl}")
trace.putAttribute("status", "error")
trace.stop()
}
)
.build()
}
Comment on lines +57 to +81
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

remember 블록 안에서 trace를 생성하면 재사용 시 문제가 될 수 있습니다.

remember(animal.thumbnailImageUrl) 블록 안에서 trace 객체가 생성됩니다. Coil이 캐시에서 이미지를 로드하면 onStartonSuccess가 빠르게 호출되어 정상 동작하지만, 만약 같은 composable이 LazyList에서 재활용되면서 동일 URL로 재구성될 경우, remember가 캐시된 ImageRequest를 반환하므로 이미 stop()된 trace에 대해 다시 start()가 호출될 수 있습니다.

대부분의 경우 문제가 없겠지만, 안전하게 하려면 trace 생성을 listener 내부로 이동하거나, 매 요청마다 새 trace를 생성하는 구조를 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt`
around lines 57 - 81, The ImageRequest currently creates a Firebase Performance
Trace inside the remember(animal.thumbnailImageUrl) block (see imageRequest and
the Trace created via
FirebasePerformance.getInstance().newTrace("home_protect_image_load")), which
can cause start() to be called on a stopped trace when the composable is
recycled; move trace creation into the listener callbacks so each request gets a
fresh trace (e.g., create the trace in onStart or create a new Trace instance at
the beginning of onStart and call trace.start(), then stop it in
onSuccess/onError), or alternatively make the trace nullable and instantiate a
new Trace inside onStart and only call stop() if that trace was created—update
the listener in ImageRequest.Builder(...) (onStart/onSuccess/onError) and remove
trace construction from the remember block to ensure a new trace per request.

AsyncImage(
model = animal.thumbnailImageUrl,
model = imageRequest,
contentDescription = "Animal Image",
contentScale = ContentScale.Crop,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import android.os.SystemClock
import android.util.Log
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.firebase.perf.FirebasePerformance
import com.kuit.findu.R
import com.kuit.findu.domain.model.ReportAnimal
import com.kuit.findu.presentation.type.AnimalStateType
Expand All @@ -43,8 +49,33 @@ fun HomeReportedAnimalCard(
.background(shape = RoundedCornerShape(10.dp), color = FindUTheme.colors.white)
.noRippleClickable { navigateToReportDetail(animal) }
) {
val context = LocalContext.current
val imageRequest = remember(animal.thumbnailImageUrl) {
val trace = FirebasePerformance.getInstance().newTrace("home_report_image_load")
var startTime = 0L
ImageRequest.Builder(context)
.data(animal.thumbnailImageUrl)
.listener(
onStart = {
startTime = SystemClock.elapsedRealtime()
trace.start()
},
onSuccess = { _, _ ->
val duration = SystemClock.elapsedRealtime() - startTime
Log.d("ImagePerf", "제보동물 이미지 로딩 완료: ${duration}ms | ${animal.thumbnailImageUrl}")
trace.stop()
},
onError = { _, _ ->
val duration = SystemClock.elapsedRealtime() - startTime
Log.e("ImagePerf", "제보동물 이미지 로딩 실패: ${duration}ms | ${animal.thumbnailImageUrl}")
trace.putAttribute("status", "error")
trace.stop()
}
)
.build()
}
AsyncImage(
model = animal.thumbnailImageUrl,
model = imageRequest,
contentDescription = "Animal Image",
contentScale = ContentScale.Crop,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.kuit.findu.presentation.type.HomeReportDurationType
import com.kuit.findu.presentation.type.HomeUserStatusType
import com.kuit.findu.presentation.type.view.LoadState
import com.kuit.findu.presentation.util.Nickname.GUEST_NAME
import com.google.firebase.perf.FirebasePerformance
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -137,8 +138,14 @@ class HomeViewModel @Inject constructor(
private fun loadHomeData() {
viewModelScope.launch {
_uiState.update { it.copy(loadState = LoadState.Loading) }
val trace = FirebasePerformance.getInstance().newTrace("home_data_load")
trace.start()
homeUseCase().fold(
onSuccess = { data ->
trace.putAttribute("status", "success")
trace.putMetric("protect_animal_count", data.protectAnimalCards.size.toLong())
trace.putMetric("report_animal_count", data.reportAnimalCards.size.toLong())
trace.stop()
Comment on lines +141 to +148
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

코루틴 취소 시 trace가 stop되지 않을 수 있습니다.

viewModelScope가 취소되면 (예: ViewModel이 clear될 때) homeUseCase() 호출이 CancellationException으로 중단되면서 trace.stop()이 호출되지 않습니다. try-finally 또는 invokeOnCompletion으로 trace 종료를 보장하면 더 안전합니다.

🛡️ 제안하는 수정
             _uiState.update { it.copy(loadState = LoadState.Loading) }
             val trace = FirebasePerformance.getInstance().newTrace("home_data_load")
             trace.start()
-            homeUseCase().fold(
-                onSuccess = { data ->
-                    trace.putAttribute("status", "success")
-                    trace.putMetric("protect_animal_count", data.protectAnimalCards.size.toLong())
-                    trace.putMetric("report_animal_count", data.reportAnimalCards.size.toLong())
-                    trace.stop()
+            try {
+                homeUseCase().fold(
+                    onSuccess = { data ->
+                        trace.putAttribute("status", "success")
+                        trace.putMetric("protect_animal_count", data.protectAnimalCards.size.toLong())
+                        trace.putMetric("report_animal_count", data.reportAnimalCards.size.toLong())
+                        trace.stop()
                     ...
-                onFailure = { error ->
-                    trace.putAttribute("status", "failure")
-                    trace.stop()
+                    onFailure = { error ->
+                        trace.putAttribute("status", "failure")
+                        trace.stop()
                     ...
-            )
+                )
+            } catch (e: Exception) {
+                trace.putAttribute("status", "cancelled")
+                trace.stop()
+                throw e
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt`
around lines 141 - 148, The Firebase trace started via
FirebasePerformance.getInstance().newTrace("home_data_load") may not be stopped
if the coroutine launched by homeUseCase() is cancelled; ensure trace.stop() is
called in all termination paths by wrapping the homeUseCase() call and
subsequent trace attribute/metric calls in a try { ... } finally { trace.stop()
} or attach trace.stop() in the coroutine's completion handler (e.g.,
invokeOnCompletion) so that the trace is always stopped even on
CancellationException or other failures; update the HomeViewModel code around
the trace creation and the homeUseCase() invocation to guarantee trace.stop() is
executed.

_uiState.update {
it.copy(
loadState = LoadState.Success,
Expand All @@ -148,6 +155,8 @@ class HomeViewModel @Inject constructor(
}
},
onFailure = { error ->
trace.putAttribute("status", "failure")
trace.stop()
Log.e("HomeViewModel", "loadHomeData: $error")
if(error.message?.contains("401") == true) {
_uiEffect.send(HomeUiEffect.NavigateToLogin)
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ plugins {
alias(libs.plugins.navigationSafeArgs) apply false
alias(libs.plugins.googleServices) apply false
alias(libs.plugins.firebaseCrashlytics) apply false
alias(libs.plugins.firebasePerf) apply false
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ coilCompose = "2.5.0"
composeBom = "2024.04.01"
firebaseBom = "34.6.0"
firebaseCrashlytics = "3.0.6"
firebasePerf = "2.0.2"
googleServices = "4.4.2"
kotlin = "2.0.21"
coreKtx = "1.15.0"
Expand Down Expand Up @@ -54,6 +55,7 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir
firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics" }
firebase-config-ktx = { module = "com.google.firebase:firebase-config" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics"}
firebase-perf = { module = "com.google.firebase:firebase-perf" }

junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand Down Expand Up @@ -105,6 +107,7 @@ dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt
navigationSafeArgs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigationFragmentKtx" }
googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" }
firebaseCrashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" }
firebasePerf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerf" }

[bundles]
hilt = [
Expand Down
Loading