diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index fd875b9320..96b248ddfc 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -39,6 +39,7 @@ import com.wire.android.feature.analytics.globalAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.feature.analytics.model.AnalyticsSettings import com.wire.android.util.AppNameUtil +import com.wire.android.util.AppPerformanceTracker import com.wire.android.util.CurrentScreenManager import com.wire.android.util.DataDogLogger import com.wire.android.util.logging.LogFileWriter @@ -115,6 +116,7 @@ class WireApplication : BaseApp() { override fun onCreate() { super.onCreate() + AppPerformanceTracker.markAppStart() enableStrictMode() diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 562bbfa66a..a4ab48df43 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -117,6 +117,7 @@ import com.wire.android.ui.theme.ThemeOption import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState +import com.wire.android.util.AppPerformanceTracker import com.wire.android.util.CurrentScreenManager import com.wire.android.util.LocalSyncStateObserver import com.wire.android.util.ShakeDetector @@ -208,6 +209,11 @@ class WireActivity : AppCompatActivity() { InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination() InitialAppState.LOGGED_IN -> HomeScreenDestination() } + + if (viewModel.initialAppState() != InitialAppState.LOGGED_IN) { + AppPerformanceTracker.cancelAppStartTracking() + } + appLogger.i("$TAG composable content") setComposableContent(startDestination) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 9ae569c790..679a9d5d31 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -49,6 +49,7 @@ import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.Accent import com.wire.android.ui.theme.ThemeOption +import com.wire.android.util.AppPerformanceTracker import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.android.util.deeplink.DeepLinkProcessor @@ -353,11 +354,15 @@ class WireActivityViewModel @Inject constructor( result.key, result.domain ) { conversationId -> + AppPerformanceTracker.markNotificationOpen() sendAction(OpenConversation(DeepLinkResult.OpenConversation(conversationId, result.switchedAccount))) } is DeepLinkResult.MigrationLogin -> sendAction(OnMigrationLogin(result)) - is DeepLinkResult.OpenConversation -> sendAction(OpenConversation(result)) + is DeepLinkResult.OpenConversation -> { + AppPerformanceTracker.markNotificationOpen() + sendAction(OpenConversation(result)) + } is DeepLinkResult.OpenOtherUserProfile -> onOpenUserProfileDeepLink(result) DeepLinkResult.SharingIntent -> sendAction(OnShowImportMediaScreen) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index 1f1d1eddc2..bd517fca6d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.util.AppPerformanceTracker import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer import com.wire.android.model.SnackBarMessage import com.wire.android.ui.common.visbility.VisibilityState @@ -216,6 +217,7 @@ class ConversationMessagesViewModel @Inject constructor( messages = paginatedMessagesFlow, firstUnreadEventIndex = max(lastReadIndex - 1, 0) ) + AppPerformanceTracker.logConversationMessagesReady() handleSelectedSearchedMessageHighlighting() } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index f880049a59..dbbd9f5036 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -28,6 +28,7 @@ import androidx.paging.cachedIn import androidx.paging.insertSeparators import androidx.paging.map import com.wire.android.BuildConfig +import com.wire.android.util.AppPerformanceTracker import com.wire.android.di.CurrentAccount import com.wire.android.mapper.UserTypeMapper import com.wire.android.mapper.toConversationItem @@ -74,6 +75,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch @Suppress("TooManyFunctions") @@ -195,7 +197,13 @@ class ConversationListViewModelImpl @AssistedInject constructor( init { observeSelfUserLegalHoldState() - if (!usePagination) { + if (usePagination) { + viewModelScope.launch { + conversationsPaginatedFlow.take(1).collect { + AppPerformanceTracker.logConversationsReady() + } + } + } else { observeNonPaginatedSearchConversationList() } } @@ -247,6 +255,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( } .flowOn(dispatcher.io()) .collect { + AppPerformanceTracker.logConversationsReady() conversationListState = ConversationListState.NotPaginated( isLoading = false, conversations = it, diff --git a/app/src/main/kotlin/com/wire/android/util/AppPerformanceTracker.kt b/app/src/main/kotlin/com/wire/android/util/AppPerformanceTracker.kt new file mode 100644 index 0000000000..0499b24d06 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/AppPerformanceTracker.kt @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.util + +import android.os.SystemClock +import com.wire.android.appLogger + +/** + * Captures timestamps at key app entry points and logs elapsed durations for DataDog Logs. + */ +object AppPerformanceTracker { + + @Volatile private var appStartTime: Long? = null + + @Volatile private var notificationOpenTime: Long? = null + + fun markAppStart() { + appStartTime = SystemClock.elapsedRealtime() + } + + // Called when the startup path will NOT lead to the conversations list (e.g. user not logged in). + fun cancelAppStartTracking() { + appStartTime = null + } + + fun logConversationsReady() { + appStartTime?.let { + val durationMs = SystemClock.elapsedRealtime() - it + appLogger.i("Perf | event=conversations_list_ready | duration_ms=$durationMs") + appStartTime = null + } + } + + fun markNotificationOpen() { + notificationOpenTime = SystemClock.elapsedRealtime() + } + + fun logConversationMessagesReady() { + notificationOpenTime?.let { + val durationMs = SystemClock.elapsedRealtime() - it + appLogger.i("Perf | event=conversation_messages_ready | duration_ms=$durationMs") + notificationOpenTime = null + } + } +} diff --git a/kalium b/kalium index 1f00fc2113..5f5ef69da5 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 1f00fc211385f20da1e354009f82def7c9da9fe9 +Subproject commit 5f5ef69da51b01c93b0eed7fd92b410210eb54eb