From 556142b1ede2452d13cdf3c39bf9377ac70b2026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Ju=C3=A1rez=20L=C3=B3pez?= Date: Tue, 1 Oct 2024 20:07:28 -0400 Subject: [PATCH] [andr][example] Track App Launch TTI (#49) * [andr][example] Track App Launch TTI * okhttp handling cleanup * addApplicationStartInfoCompletionListener --- platform/jvm/gradle-test-app/build.gradle.kts | 7 +- .../bitdrift/gradletestapp/FirstFragment.kt | 8 +- .../bitdrift/gradletestapp/GradleTestApp.kt | 165 +++++++++++++++++- 3 files changed, 173 insertions(+), 7 deletions(-) diff --git a/platform/jvm/gradle-test-app/build.gradle.kts b/platform/jvm/gradle-test-app/build.gradle.kts index 7c90d8f..1399e74 100644 --- a/platform/jvm/gradle-test-app/build.gradle.kts +++ b/platform/jvm/gradle-test-app/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(project(":capture")) implementation(project(":capture-apollo3")) implementation(project(":capture-timber")) + implementation(libs.androidx.material3.android) implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.compose.material:material:1.4.0") implementation("androidx.activity:activity-compose:1.8.0") @@ -18,7 +19,7 @@ dependencies { implementation("com.apollographql.apollo3:apollo-runtime:3.8.3") implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.google.android.material:material:1.8.0") - implementation(libs.androidx.material3.android) + implementation("com.squareup.papa:papa:0.26") debugImplementation("androidx.compose.ui:ui-test-manifest:1.4.0") debugImplementation("androidx.fragment:fragment-testing:1.6.2") @@ -41,7 +42,7 @@ dependencies { android { namespace = "io.bitdrift.gradletestapp" - compileSdk = 34 + compileSdk = 35 buildFeatures { compose = true @@ -56,7 +57,7 @@ android { defaultConfig { applicationId = "io.bitdrift.gradletestapp" minSdk = 21 - targetSdk = 34 + targetSdk = 35 versionCode = 66 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt index be024dd..9379f24 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/FirstFragment.kt @@ -202,12 +202,14 @@ class FirstFragment : Fragment() { call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { - val s = response.body!!.string() - Timber.v("Http request completed with status code=${response.code}") + val body = response.use { + it.body!!.string() + } + Timber.v("Http request completed with status code=${response.code} and body=$body") } override fun onFailure(call: Call, e: IOException) { - Timber.v("Http request failed with exception=${e.javaClass::class.simpleName}") + Timber.v("Http request failed with exception=$e") } }) } diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt index 8bc1916..69a1ce5 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt @@ -8,16 +8,62 @@ package io.bitdrift.gradletestapp import android.app.Activity +import android.app.ActivityManager import android.app.Application +import android.app.ApplicationExitInfo +import android.app.ApplicationStartInfo.LAUNCH_MODE_SINGLE_INSTANCE +import android.app.ApplicationStartInfo.LAUNCH_MODE_SINGLE_INSTANCE_PER_TASK +import android.app.ApplicationStartInfo.LAUNCH_MODE_SINGLE_TASK +import android.app.ApplicationStartInfo.LAUNCH_MODE_SINGLE_TOP +import android.app.ApplicationStartInfo.LAUNCH_MODE_STANDARD +import android.app.ApplicationStartInfo.STARTUP_STATE_ERROR +import android.app.ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN +import android.app.ApplicationStartInfo.STARTUP_STATE_STARTED +import android.app.ApplicationStartInfo.START_REASON_ALARM +import android.app.ApplicationStartInfo.START_REASON_BACKUP +import android.app.ApplicationStartInfo.START_REASON_BOOT_COMPLETE +import android.app.ApplicationStartInfo.START_REASON_BROADCAST +import android.app.ApplicationStartInfo.START_REASON_CONTENT_PROVIDER +import android.app.ApplicationStartInfo.START_REASON_JOB +import android.app.ApplicationStartInfo.START_REASON_LAUNCHER +import android.app.ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS +import android.app.ApplicationStartInfo.START_REASON_OTHER +import android.app.ApplicationStartInfo.START_REASON_PUSH +import android.app.ApplicationStartInfo.START_REASON_SERVICE +import android.app.ApplicationStartInfo.START_REASON_START_ACTIVITY +import android.app.ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE +import android.app.ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION +import android.app.ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME +import android.app.ApplicationStartInfo.START_TIMESTAMP_FORK +import android.app.ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN +import android.app.ApplicationStartInfo.START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME +import android.app.ApplicationStartInfo.START_TIMESTAMP_LAUNCH +import android.app.ApplicationStartInfo.START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE +import android.app.ApplicationStartInfo.START_TYPE_COLD +import android.app.ApplicationStartInfo.START_TYPE_HOT +import android.app.ApplicationStartInfo.START_TYPE_UNSET +import android.app.ApplicationStartInfo.START_TYPE_WARM +import android.content.Context +import android.os.Build import android.os.Bundle +import android.util.Log +import androidx.core.content.ContextCompat import io.bitdrift.capture.Capture import io.bitdrift.capture.Capture.Logger.sessionUrl import io.bitdrift.capture.LogLevel import io.bitdrift.capture.events.span.Span import io.bitdrift.capture.events.span.SpanResult import io.bitdrift.capture.timber.CaptureTree +import papa.AppLaunchType +import papa.PapaEvent +import papa.PapaEventListener +import papa.PapaEventLogger +import papa.PreLaunchState import timber.log.Timber import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration /** * A Java app entry point that initializes the Bitdrift Logger. @@ -30,16 +76,133 @@ class GradleTestApp : Application() { super.onCreate() Timber.i("Hello World!") initLogging() + trackAppLaunch() trackAppLifecycle() } private fun initLogging() { BitdriftInit.initBitdriftCaptureInJava() + // Timber if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } Timber.plant(CaptureTree()) - Timber.i("Bitdrift Logger initialized with session_url=%s", sessionUrl) + Timber.i("Bitdrift Logger initialized with session_url=$sessionUrl") + } + + private fun trackAppLaunch() { + // ApplicationStartInfo + if (Build.VERSION.SDK_INT >= 35) { + val activityManager: ActivityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + activityManager.addApplicationStartInfoCompletionListener(ContextCompat.getMainExecutor(this)) { appStartInfo -> + val appStartInfoFields = mapOf( + "startup_type" to appStartInfo.startType.toStartTypeText(), + "startup_state" to appStartInfo.startupState.toStartupStateText(), + "startup_launch_mode" to appStartInfo.launchMode.toLaunchModeText(), + "startup_was_forced_stopped" to appStartInfo.wasForceStopped().toString(), + "startup_reason" to appStartInfo.reason.toStartReasonText(), + "start_timestamp_launch_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_LAUNCH]?.toString() ?: "null"), + "start_timestamp_fork_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_FORK]?.toString() ?: "null"), + "start_timestamp_oncreate_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_APPLICATION_ONCREATE]?.toString() ?: "null"), + "start_timestamp_bind_application_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_BIND_APPLICATION]?.toString() ?: "null"), + "start_timestamp_first_frame_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_FIRST_FRAME]?.toString() ?: "null"), + "start_timestamp_fully_drawn_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_FULLY_DRAWN]?.toString() ?: "null"), + "start_timestamp_initial_renderthread_frame_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME]?.toString() ?: "null"), + "start_timestamp_surfaceflinger_composition_complete_ns" to (appStartInfo.startupTimestamps[START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE]?.toString() ?: "null") + ) + Timber.d("ApplicationStartInfoCompletion event: $appStartInfoFields") + Capture.Logger.logInfo(appStartInfoFields) { "ApplicationStartInfoCompletion" } + } + } + + // Papa + PapaEventListener.install { event -> + Timber.d("Papa event: $event") + when (event) { + is PapaEvent.AppLaunch -> { + Capture.Logger.logInfo( + mapOf( + "preLaunchState" to event.preLaunchState.toString(), + "durationMs" to event.durationUptimeMillis.toString(), + "isSlowLaunch" to event.isSlowLaunch.toString(), + "trampolined" to event.trampolined.toString(), + "backgroundDurationMs" to event.invisibleDurationRealtimeMillis.toString(), + "startUptimeMs" to event.startUptimeMillis.toString(), + ) + ) { "PapaEvent.AppLaunch" } + if (event.preLaunchState.launchType == AppLaunchType.COLD) { + Capture.Logger.logAppLaunchTTI(event.durationUptimeMillis.toDuration(DurationUnit.MILLISECONDS)) + } + } + is PapaEvent.FrozenFrameOnTouch -> { + Capture.Logger.logInfo( + mapOf( + "activityName" to event.activityName, + "repeatTouchDownCount" to event.repeatTouchDownCount.toString(), + "handledElapsedMs" to event.deliverDurationUptimeMillis.toString(), + "frameElapsedMs" to event.dislayDurationUptimeMillis.toString(), + "pressedView" to event.pressedView.orEmpty(), + ) + ) { "PapaEvent.FrozenFrameOnTouch" } + } + is PapaEvent.UsageError -> { + Capture.Logger.logInfo( + mapOf( + "debugMessage" to event.debugMessage, + ) + ) { "PapaEvent.UsageError" } + + } + } + } + } + + private fun Int.toStartTypeText(): String { + return when (this) { + START_TYPE_UNSET -> "START_TYPE_UNSET" + START_TYPE_COLD -> "START_TYPE_COLD" + START_TYPE_WARM -> "START_TYPE_WARM" + START_TYPE_HOT -> "START_TYPE_HOT" + else -> "UNKNOWN" + } + } + + private fun Int.toStartupStateText(): String { + return when (this) { + STARTUP_STATE_STARTED -> "STARTUP_STATE_STARTED" + STARTUP_STATE_ERROR -> "STARTUP_STATE_ERROR" + STARTUP_STATE_FIRST_FRAME_DRAWN -> "STARTUP_STATE_FIRST_FRAME_DRAWN" + else -> "UNKNOWN" + } + } + + private fun Int.toLaunchModeText(): String { + return when (this) { + LAUNCH_MODE_STANDARD -> "LAUNCH_MODE_STANDARD" + LAUNCH_MODE_SINGLE_TOP -> "LAUNCH_MODE_SINGLE_TOP" + LAUNCH_MODE_SINGLE_INSTANCE -> "LAUNCH_MODE_SINGLE_INSTANCE" + LAUNCH_MODE_SINGLE_TASK -> "LAUNCH_MODE_SINGLE_TASK" + LAUNCH_MODE_SINGLE_INSTANCE_PER_TASK -> "LAUNCH_MODE_SINGLE_INSTANCE_PER_TASK" + else -> "UNKNOWN" + } + } + + private fun Int.toStartReasonText(): String { + return when (this) { + START_REASON_ALARM -> "START_REASON_ALARM" + START_REASON_BACKUP -> "START_REASON_BACKUP" + START_REASON_BOOT_COMPLETE -> "START_REASON_BOOT_COMPLETE" + START_REASON_BROADCAST -> "START_REASON_BROADCAST" + START_REASON_CONTENT_PROVIDER -> "START_REASON_CONTENT_PROVIDER" + START_REASON_JOB -> "START_REASON_JOB" + START_REASON_LAUNCHER -> "START_REASON_LAUNCHER" + START_REASON_LAUNCHER_RECENTS -> "START_REASON_LAUNCHER_RECENTS" + START_REASON_OTHER -> "START_REASON_OTHER" + START_REASON_PUSH -> "START_REASON_PUSH" + START_REASON_SERVICE -> "START_REASON_SERVICE" + START_REASON_START_ACTIVITY -> "START_REASON_START_ACTIVITY" + else -> "UNKNOWN" + } } private fun trackAppLifecycle(){