From 383984657ad4979a26ebc11d957fc8cc6ab445d4 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Mon, 5 Sep 2022 21:35:42 +0530 Subject: [PATCH] Fixes #4335: Addition of support for uploading performance metric logs (#4399) * qualifiers and constants for metric record and upload times * comments * dagger provides for flags * rename to enablePerformanceMetricCollection * initial proto * nit fixes * nit fixes * comments. * nits * nit * updates. * updates. * nits. * metric log inclusion. * base setup. * name correction. * nits. * nits. * storage comment * pss comment * network usage comment. * network usage comment - part 2. * metric addition in proto definitions. * metricLog --> loggableMetric * proto definitions in oppiaLogger * wording update for transmission * dependency/api updates. * nits. * metric controller and oppiaLogger * nits * logger-controller pattern, single utils * nits * comments * log uploading support * nits * nits * performance metric event logger nits. * nits. * logger reinstated. * nits * deleting unused interfaces. * log generator to metric log scheduler * nits * nits -- kdoc, code placing, readability * nits -- kdoc, code placing, readability * worker functions to schedule logging, renaming * nits. * clearing up build errors. * nits. * fixing build errors. * removal of dependency from oppiaLogger and basing the entire implementation on Logger-Controller * performanceMetricController tests + fakeLogger * fakeLogger tests * nits * tests for the new logger. * tests for the worker + renaming of package to logScheduler * tests for work manager initializer * initial base setup for utils test * nits * nits * nit * worker and initializer tests * event bundle creator tests * module tests * nits * performanceMetricsUtils tests * nits * renamed logUpload to logReport + nits * nits * nits * review updates. * nit * review updates - 2 * isAppInForeground revamp * isAppInForeground revamp * nits * domain tests repair * app tests repair * nits. * nits. * nits. * nits. * nit * test update. * test update. * nit * todo issue number update -- static check * comments. * nits. * utils test + nits * utils test + nits * nits * test rearrangement * nits * test fixes. * nits * nits * nits * memory and storage tier updates * memory and storage tier updates * updates. * nits * test file exemptions refactor due to file renaming. * additional tests, nit fixes. * activity manager shadow and assessor test * nit * nit * shadow traffic stats + assessor test fix. * custom shadow tests * module deps * tests * nits * metricLogScheduler refactor and test * exemption removal * nits * updates. * variable nits + parameterized test exemption * app component dependencies. * nits * logging module bazel build, config module creation * testing robolectric bazel module update * domain bazel build fixes * working oppia bazel build * nits. * nits. * nit for bazel building * addition of test file for module * bazel tests fixes * more fixes. * reformatting * nits * nits * dep fix * deps fix * deps fix * nit * nits * nits * gradle test fix. * bazel oppia build * test fixes -- renaming. * deps addition * previous merge correction * nits * updates. * nits * bazel update. * nit * nits. --- .../oppialogger/loguploader/BUILD.bazel | 2 + .../LogReportWorkManagerInitializer.kt | 23 ++ .../loguploader/LogUploadWorker.kt | 25 ++ .../loguploader/FakeLogUploader.kt | 11 + .../LogReportWorkManagerInitializerTest.kt | 19 + .../loguploader/LogUploadWorkerTest.kt | 70 +++- .../oppia/android/util/logging/BUILD.bazel | 1 + .../util/logging/EventBundleCreator.kt | 178 +++++++++ .../oppia/android/util/logging/LogUploader.kt | 6 + .../logging/firebase/FirebaseEventLogger.kt | 13 +- .../logging/firebase/FirebaseLogUploader.kt | 12 + .../util/logging/EventBundleCreatorTest.kt | 363 ++++++++++++++++++ .../firebase/LogReportingModuleTest.kt | 150 ++++++++ 13 files changed, 871 insertions(+), 2 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/logging/firebase/LogReportingModuleTest.kt diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel index 5b8f53e4f17..01287999145 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel @@ -27,12 +27,14 @@ kt_android_library( ], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:performance_metrics_controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//third_party:androidx_work_work-runtime-ktx", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", + "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_event_logger", "//utility/src/main/java/org/oppia/android/util/threading:annotations", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt index 3bab2316a2b..e5830fddaa5 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt @@ -50,6 +50,13 @@ class LogReportWorkManagerInitializer @Inject constructor( ) .build() + private val workerCaseForUploadingPerformanceMetrics: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.PERFORMANCE_METRICS_WORKER + ) + .build() + private val workerCaseForSchedulingPeriodicBackgroundMetricLogs: Data = Data.Builder() .putString( MetricLogSchedulingWorker.WORKER_CASE_KEY, @@ -83,6 +90,12 @@ class LogReportWorkManagerInitializer @Inject constructor( .setConstraints(logReportWorkerConstraints) .build() + private val workRequestForUploadingPerformanceMetrics: PeriodicWorkRequest = PeriodicWorkRequest + .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) + .setInputData(workerCaseForUploadingPerformanceMetrics) + .setConstraints(logReportWorkerConstraints) + .build() + private val workRequestForSchedulingPeriodicBackgroundMetricLogs: PeriodicWorkRequest = PeriodicWorkRequest.Builder( MetricLogSchedulingWorker::class.java, @@ -117,6 +130,10 @@ class LogReportWorkManagerInitializer @Inject constructor( val workManager = WorkManager.getInstance(context) logUploader.enqueueWorkRequestForEvents(workManager, workRequestForUploadingEvents) logUploader.enqueueWorkRequestForExceptions(workManager, workRequestForUploadingExceptions) + logUploader.enqueueWorkRequestForPerformanceMetrics( + workManager, + workRequestForUploadingPerformanceMetrics + ) metricLogScheduler.enqueueWorkRequestForPeriodicBackgroundMetrics( workManager, workRequestForSchedulingPeriodicBackgroundMetricLogs @@ -140,6 +157,9 @@ class LogReportWorkManagerInitializer @Inject constructor( /** Returns the [UUID] of the work request that is enqueued for uploading exception logs. */ fun getWorkRequestForExceptionsId(): UUID = workRequestForUploadingExceptions.id + /** Returns the [UUID] of the work request that is enqueued for uploading performance metrics logs. */ + fun getWorkRequestForPerformanceMetricsId(): UUID = workRequestForUploadingPerformanceMetrics.id + /** * Returns the [UUID] of the work request that is enqueued for scheduling memory usage * performance metrics collection. @@ -172,6 +192,9 @@ class LogReportWorkManagerInitializer @Inject constructor( */ fun getWorkRequestDataForExceptions(): Data = workerCaseForUploadingExceptions + /** Returns the [Data] that goes into the work request that is enqueued for uploading performance metric logs. */ + fun getWorkRequestDataForPerformanceMetrics(): Data = workerCaseForUploadingPerformanceMetrics + /** * Returns the [Data] that goes into the work request that is enqueued for scheduling storage * usage performance metrics collection. diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt index 44b59243406..ad7ffe8e878 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import org.oppia.android.domain.oppialogger.analytics.AnalyticsController +import org.oppia.android.domain.oppialogger.analytics.PerformanceMetricsController import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.oppialogger.exceptions.toException import org.oppia.android.domain.util.getStringFromData @@ -17,6 +18,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.logging.EventLogger import org.oppia.android.util.logging.ExceptionLogger import org.oppia.android.util.logging.SyncStatusManager +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsEventLogger import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject @@ -26,8 +28,10 @@ class LogUploadWorker private constructor( params: WorkerParameters, private val analyticsController: AnalyticsController, private val exceptionsController: ExceptionsController, + private val performanceMetricsController: PerformanceMetricsController, private val exceptionLogger: ExceptionLogger, private val eventLogger: EventLogger, + private val performanceMetricsEventLogger: PerformanceMetricsEventLogger, private val consoleLogger: ConsoleLogger, private val syncStatusManager: SyncStatusManager, @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher @@ -38,6 +42,7 @@ class LogUploadWorker private constructor( const val TAG = "LogUploadWorker.tag" const val EVENT_WORKER = "event_worker" const val EXCEPTION_WORKER = "exception_worker" + const val PERFORMANCE_METRICS_WORKER = "performance_metrics_worker" } @ExperimentalCoroutinesApi @@ -47,6 +52,7 @@ class LogUploadWorker private constructor( when (inputData.getStringFromData(WORKER_CASE_KEY)) { EVENT_WORKER -> uploadEvents() EXCEPTION_WORKER -> uploadExceptions() + PERFORMANCE_METRICS_WORKER -> uploadPerformanceMetrics() else -> Result.failure() } } @@ -97,12 +103,29 @@ class LogUploadWorker private constructor( } } + /** Extracts performance metric logs from the cache store and logs them to the remote service. */ + private suspend fun uploadPerformanceMetrics(): Result { + return try { + val performanceMetricsLogs = performanceMetricsController.getMetricLogStoreList() + performanceMetricsLogs.forEach { performanceMetricsLog -> + performanceMetricsEventLogger.logPerformanceMetric(performanceMetricsLog) + performanceMetricsController.removeFirstMetricLogFromStore() + } + Result.success() + } catch (e: Exception) { + consoleLogger.e(TAG, e.toString(), e) + Result.failure() + } + } + /** Creates an instance of [LogUploadWorker] by properly injecting dependencies. */ class Factory @Inject constructor( private val analyticsController: AnalyticsController, private val exceptionsController: ExceptionsController, + private val performanceMetricsController: PerformanceMetricsController, private val exceptionLogger: ExceptionLogger, private val eventLogger: EventLogger, + private val performanceMetricsEventLogger: PerformanceMetricsEventLogger, private val consoleLogger: ConsoleLogger, private val syncStatusManager: SyncStatusManager, @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher @@ -114,8 +137,10 @@ class LogUploadWorker private constructor( params, analyticsController, exceptionsController, + performanceMetricsController, exceptionLogger, eventLogger, + performanceMetricsEventLogger, consoleLogger, syncStatusManager, backgroundDispatcher diff --git a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt index b2a02324827..48783b794d5 100644 --- a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt +++ b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt @@ -12,6 +12,7 @@ import javax.inject.Singleton class FakeLogUploader @Inject constructor() : LogUploader { private val eventRequestIdList = mutableListOf() private val exceptionRequestIdList = mutableListOf() + private val performanceMetricsRequestIdList = mutableListOf() override fun enqueueWorkRequestForEvents( workManager: WorkManager, @@ -27,9 +28,19 @@ class FakeLogUploader @Inject constructor() : LogUploader { exceptionRequestIdList.add(workRequest.id) } + override fun enqueueWorkRequestForPerformanceMetrics( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + performanceMetricsRequestIdList.add(workRequest.id) + } + /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */ fun getMostRecentEventRequestId() = eventRequestIdList.last() /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */ fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last() + + /** Returns the most recent work request id that's stored in the [performanceMetricsRequestIdList]. */ + fun getMostRecentPerformanceMetricsRequestId() = performanceMetricsRequestIdList.last() } diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt index 50b92a0573e..cb31d65425e 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt @@ -122,6 +122,8 @@ class LogReportWorkManagerInitializerTest { val enqueuedEventWorkRequestId = logReportWorkManagerInitializer.getWorkRequestForEventsId() val enqueuedExceptionWorkRequestId = logReportWorkManagerInitializer.getWorkRequestForExceptionsId() + val enqueuedPerformanceMetricsWorkRequestId = + logReportWorkManagerInitializer.getWorkRequestForPerformanceMetricsId() val enqueuedSchedulingStorageUsageMetricWorkRequestId = logReportWorkManagerInitializer.getWorkRequestForSchedulingStorageUsageMetricLogsId() val enqueuedSchedulingPeriodicUiMetricWorkRequestId = @@ -134,6 +136,9 @@ class LogReportWorkManagerInitializerTest { assertThat(fakeLogUploader.getMostRecentExceptionRequestId()).isEqualTo( enqueuedExceptionWorkRequestId ) + assertThat(fakeLogUploader.getMostRecentPerformanceMetricsRequestId()).isEqualTo( + enqueuedPerformanceMetricsWorkRequestId + ) assertThat(fakeLogScheduler.getMostRecentStorageUsageMetricLoggingRequestId()).isEqualTo( enqueuedSchedulingStorageUsageMetricWorkRequestId ) @@ -186,6 +191,20 @@ class LogReportWorkManagerInitializerTest { ).isEqualTo(workerCaseForUploadingExceptions) } + @Test + fun testWorkRequest_verifyWorkRequestDataForPerformanceMetrics() { + val workerCaseForUploadingPerformanceMetrics: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.PERFORMANCE_METRICS_WORKER + ) + .build() + + assertThat(logReportWorkManagerInitializer.getWorkRequestDataForPerformanceMetrics()).isEqualTo( + workerCaseForUploadingPerformanceMetrics + ) + } + @Test fun testWorkRequest_verifyWorkRequestData_forSchedulingStorageUsageMetricLogs() { val workerCaseForSchedulingStorageUsageMetricLogs: Data = Data.Builder() diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt index 3e7881b4525..51ffe407e95 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt @@ -26,18 +26,22 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.reset import org.oppia.android.app.model.EventLog +import org.oppia.android.app.model.OppiaMetricLog import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.domain.oppialogger.LoggingIdentifierModule import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.oppialogger.PerformanceMetricsLogStorageCacheSize import org.oppia.android.domain.oppialogger.analytics.AnalyticsController import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.PerformanceMetricsController import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.testing.oppialogger.loguploader.FakeLogUploader import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.FakeExceptionLogger +import org.oppia.android.testing.FakePerformanceMetricsEventLogger import org.oppia.android.testing.logging.FakeSyncStatusManager import org.oppia.android.testing.logging.SyncStatusTestModule import org.oppia.android.testing.mockito.anyOrNull @@ -58,6 +62,9 @@ import org.oppia.android.util.logging.SyncStatusManager.SyncStatus.DATA_UPLOADED import org.oppia.android.util.logging.SyncStatusManager.SyncStatus.DATA_UPLOADING import org.oppia.android.util.logging.SyncStatusManager.SyncStatus.NETWORK_ERROR import org.oppia.android.util.logging.SyncStatusManager.SyncStatus.NO_CONNECTIVITY +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsEventLogger import org.oppia.android.util.networking.NetworkConnectionDebugUtil import org.oppia.android.util.networking.NetworkConnectionUtil.ProdConnectionStatus.NONE import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule @@ -69,6 +76,7 @@ import javax.inject.Singleton private const val TEST_TIMESTAMP = 1556094120000 private const val TEST_TOPIC_ID = "test_topicId" +private const val TEST_APK_SIZE = Long.MAX_VALUE /** Tests for [LogUploadWorker]. */ // FunctionName: test names are conventionally named with underscores. @@ -80,9 +88,11 @@ class LogUploadWorkerTest { @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil @Inject lateinit var fakeEventLogger: FakeEventLogger @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var fakePerformanceMetricsEventLogger: FakePerformanceMetricsEventLogger @Inject lateinit var oppiaLogger: OppiaLogger @Inject lateinit var analyticsController: AnalyticsController @Inject lateinit var exceptionsController: ExceptionsController + @Inject lateinit var performanceMetricsController: PerformanceMetricsController @Inject lateinit var logUploadWorkerFactory: LogUploadWorkerFactory @Inject lateinit var dataProviders: DataProviders @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @@ -105,6 +115,13 @@ class LogUploadWorkerTest { .setTimestamp(TEST_TIMESTAMP) .build() + private val apkSizeTestLoggableMetric = OppiaMetricLog.LoggableMetric.newBuilder() + .setApkSizeMetric( + OppiaMetricLog.ApkSizeMetric.newBuilder() + .setApkSizeBytes(TEST_APK_SIZE) + .build() + ).build() + private val exception = Exception("TEST") @Before @@ -203,6 +220,46 @@ class LogUploadWorkerTest { assertThat(loggedExceptionStackTraceElems).isEqualTo(expectedExceptionStackTraceElems) } + @Test + fun testWorker_logPerformanceMetric_withoutNetwork_enqueueRequest_verifySuccess() { + networkConnectionUtil.setCurrentConnectionStatus(NONE) + performanceMetricsController.logPerformanceMetricsEvent( + TEST_TIMESTAMP, + OppiaMetricLog.CurrentScreen.SCREEN_UNSPECIFIED, + apkSizeTestLoggableMetric, + OppiaMetricLog.Priority.LOW_PRIORITY + ) + + val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext()) + + val inputData = Data.Builder().putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.PERFORMANCE_METRICS_WORKER + ).build() + + val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + workManager.enqueue(request) + testCoroutineDispatchers.runCurrent() + + val workInfo = workManager.getWorkInfoById(request.id) + val loggedPerformanceMetric = + fakePerformanceMetricsEventLogger.getMostRecentPerformanceMetricsEvent() + assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) + assertThat(loggedPerformanceMetric.loggableMetric.loggableMetricTypeCase).isEqualTo( + OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.APK_SIZE_METRIC + ) + assertThat(loggedPerformanceMetric.currentScreen).isEqualTo( + OppiaMetricLog.CurrentScreen.SCREEN_UNSPECIFIED + ) + assertThat(loggedPerformanceMetric.priority).isEqualTo(OppiaMetricLog.Priority.LOW_PRIORITY) + assertThat(loggedPerformanceMetric.timestampMillis).isEqualTo(TEST_TIMESTAMP) + assertThat(loggedPerformanceMetric.loggableMetric.apkSizeMetric.apkSizeBytes).isEqualTo( + TEST_APK_SIZE + ) + } + @Test fun testWorker_logEvent_withoutNetwork_enqueueRequest_verifyCorrectSyncStatusSequence() { networkConnectionUtil.setCurrentConnectionStatus(NONE) @@ -310,6 +367,11 @@ class LogUploadWorkerTest { @Provides fun bindFakeExceptionLogger(fakeLogger: FakeExceptionLogger): ExceptionLogger = fakeLogger + + @Provides + fun bindFakePerformanceMetricsLogger( + fakePerformanceMetricsEventLogger: FakePerformanceMetricsEventLogger + ): PerformanceMetricsEventLogger = fakePerformanceMetricsEventLogger } @Module @@ -322,6 +384,10 @@ class LogUploadWorkerTest { @Provides @ExceptionLogStorageCacheSize fun provideExceptionLogStorageSize(): Int = 2 + + @Provides + @PerformanceMetricsLogStorageCacheSize + fun providePerformanceMetricsLogStorageCacheSize(): Int = 2 } @Module @@ -340,7 +406,9 @@ class LogUploadWorkerTest { TestFirebaseLogUploaderModule::class, FakeOppiaClockModule::class, NetworkConnectionUtilDebugModule::class, LocaleProdModule::class, LoggerModule::class, AssetModule::class, PlatformParameterModule::class, PlatformParameterSingletonModule::class, - LoggingIdentifierModule::class, SyncStatusTestModule::class, ApplicationLifecycleModule::class + LoggingIdentifierModule::class, SyncStatusTestModule::class, + PerformanceMetricsAssessorModule::class, ApplicationLifecycleModule::class, + PerformanceMetricsConfigurationsModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index a96279e3f68..77c82bb3bf7 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -66,6 +66,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//model/src/main/proto:event_logger_java_proto_lite", + "//model/src/main/proto:performance_metrics_event_logger_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility", ], diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt index 376f5fedb39..3c2747e9ec9 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt @@ -30,6 +30,15 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SUBMIT_ANSWER_CONTEXT +import org.oppia.android.app.model.OppiaMetricLog +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.APK_SIZE_METRIC +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.CPU_USAGE_METRIC +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.LOGGABLEMETRICTYPE_NOT_SET +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.MEMORY_USAGE_METRIC +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.NETWORK_USAGE_METRIC +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.STARTUP_LATENCY_METRIC +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric.LoggableMetricTypeCase.STORAGE_USAGE_METRIC import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.CardContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.ConceptCardContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.EmptyContext @@ -43,6 +52,12 @@ import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.Se import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.StoryContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.SubmitAnswerContext import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.TopicContext +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.ApkSizeLoggableMetric +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.CpuUsageLoggableMetric +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.MemoryUsageLoggableMetric +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.NetworkUsageLoggableMetric +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.StartupLatencyLoggableMetric +import org.oppia.android.util.logging.EventBundleCreator.PerformanceMetricsLoggableMetricType.StorageUsageLoggableMetric import org.oppia.android.util.platformparameter.LearnerStudyAnalytics import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject @@ -57,6 +72,12 @@ import org.oppia.android.app.model.EventLog.RevisionCardContext as RevisionCardE import org.oppia.android.app.model.EventLog.StoryContext as StoryEventContext import org.oppia.android.app.model.EventLog.SubmitAnswerContext as SubmitAnswerEventContext import org.oppia.android.app.model.EventLog.TopicContext as TopicEventContext +import org.oppia.android.app.model.OppiaMetricLog.ApkSizeMetric as ApkSizePerformanceLoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.CpuUsageMetric as CpuUsagePerformanceLoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.MemoryUsageMetric as MemoryUsagePerformanceLoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.NetworkUsageMetric as NetworkUsagePerformanceLoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.StartupLatencyMetric as StartupLatencyPerformanceLoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.StorageUsageMetric as StorageUsagePerformanceLoggableMetric // See https://firebase.google.com/docs/reference/cpp/group/parameter-names for context. private const val MAX_CHARACTERS_IN_PARAMETER_NAME = 40 @@ -84,6 +105,25 @@ class EventBundleCreator @Inject constructor( }?.activityName ?: "unknown_activity_context" } + /** + * Fills the specified [bundle] with a logging-ready representation of [oppiaMetricLog] and + * returns a string representation of the high-level type of event logged (per + * [OppiaMetricLog.LoggableMetric.getLoggableMetricTypeCase]). + */ + fun fillPerformanceMetricsEventBundle(oppiaMetricLog: OppiaMetricLog, bundle: Bundle): String { + bundle.putLong("timestamp", oppiaMetricLog.timestampMillis) + bundle.putString("priority", oppiaMetricLog.priority.toAnalyticsName()) + bundle.putString("is_app_in_foreground", oppiaMetricLog.isAppInForeground.toString()) + bundle.putString("memory_tier", oppiaMetricLog.memoryTier.toAnalyticsName()) + bundle.putString("storage_tier", oppiaMetricLog.storageTier.toAnalyticsName()) + bundle.putString("network_type", oppiaMetricLog.networkType.toAnalyticsName()) + bundle.putString("current_screen", oppiaMetricLog.currentScreen.toAnalyticsName()) + return oppiaMetricLog.loggableMetric.convertToLoggableMetricType()?.also { loggableMetric -> + // No performance metrics need to be tied to user IDs. + loggableMetric.storeValue(PropertyStore(bundle, allowUserIds = false)) + }?.metricName ?: "unknown_loggable_metric" + } + private fun EventLog.Context.convertToActivityContext(): EventActivityContext<*>? { return when (activityContextCase) { OPEN_EXPLORATION_ACTIVITY -> @@ -129,6 +169,28 @@ class EventBundleCreator @Inject constructor( } } + private fun OppiaMetricLog.LoggableMetric.convertToLoggableMetricType(): + PerformanceMetricsLoggableMetricType<*>? { + return when (loggableMetricTypeCase) { + APK_SIZE_METRIC -> ApkSizeLoggableMetric("apk_size_metric", apkSizeMetric) + STORAGE_USAGE_METRIC -> StorageUsageLoggableMetric( + "storage_usage_metric", + storageUsageMetric + ) + STARTUP_LATENCY_METRIC -> StartupLatencyLoggableMetric( + "startup_latency_metric", + startupLatencyMetric + ) + MEMORY_USAGE_METRIC -> MemoryUsageLoggableMetric("memory_usage_metric", memoryUsageMetric) + NETWORK_USAGE_METRIC -> NetworkUsageLoggableMetric( + "network_usage_metric", + networkUsageMetric + ) + CPU_USAGE_METRIC -> CpuUsageLoggableMetric("cpu_usage_metric", cpuUsageMetric) + LOGGABLEMETRICTYPE_NOT_SET, null -> null // No context to create here. + } + } + /** * Utility for storing properties within a [Bundle] (indicated by [bundle]), omitting those which * contain sensitive information (if they should be per [allowUserIds]. @@ -374,10 +436,126 @@ class EventBundleCreator @Inject constructor( } } + /*** Represents an [OppiaMetricLog] loggable metric (denoted by [LoggableMetricTypeCase]).*/ + private sealed class PerformanceMetricsLoggableMetricType( + val metricName: String, + private val value: T + ) { + /** + * Stores the value of this context (i.e. its constituent properties which may correspond to + * other [LoggableMetricTypeCase]s). + */ + fun storeValue(store: PropertyStore) = value.storeValue(store) + + /** Method that should be overridden by base classes to satisfy the contract of [storeValue]. */ + protected abstract fun T.storeValue(store: PropertyStore) + + /** The [LoggableMetricTypeCase] corresponding to [ApkSizePerformanceLoggableMetric]. */ + class ApkSizeLoggableMetric( + metricName: String, + value: OppiaMetricLog.ApkSizeMetric + ) : PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.ApkSizeMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("apk_size_bytes", apkSizeBytes) + } + } + + /** The [LoggableMetricTypeCase] corresponding to [StorageUsagePerformanceLoggableMetric]. */ + class StorageUsageLoggableMetric( + metricName: String, + value: OppiaMetricLog.StorageUsageMetric + ) : PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.StorageUsageMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("storage_usage_bytes", storageUsageBytes) + } + } + + /** The [LoggableMetricTypeCase] corresponding to [StartupLatencyPerformanceLoggableMetric]. */ + class StartupLatencyLoggableMetric( + metricName: String, + value: OppiaMetricLog.StartupLatencyMetric + ) : + PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.StartupLatencyMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("startup_latency_millis", startupLatencyMillis) + } + } + + /** The [LoggableMetricTypeCase] corresponding to [MemoryUsagePerformanceLoggableMetric]. */ + class MemoryUsageLoggableMetric( + metricName: String, + value: OppiaMetricLog.MemoryUsageMetric + ) : PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.MemoryUsageMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("total_pss_bytes", totalPssBytes) + } + } + + /** The [LoggableMetricTypeCase] corresponding to [NetworkUsagePerformanceLoggableMetric]. */ + class NetworkUsageLoggableMetric( + metricName: String, + value: OppiaMetricLog.NetworkUsageMetric + ) : PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.NetworkUsageMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("bytes_received", bytesReceived) + store.putNonSensitiveValue("bytes_sent", bytesSent) + } + } + + /** The [LoggableMetricTypeCase] corresponding to [CpuUsagePerformanceLoggableMetric]. */ + class CpuUsageLoggableMetric( + metricName: String, + value: OppiaMetricLog.CpuUsageMetric + ) : PerformanceMetricsLoggableMetricType(metricName, value) { + override fun OppiaMetricLog.CpuUsageMetric.storeValue(store: PropertyStore) { + store.putNonSensitiveValue("cpu_usage", cpuUsageMetric) + } + } + } + private fun EventLog.Priority.toAnalyticsName() = when (this) { EventLog.Priority.PRIORITY_UNSPECIFIED -> "unspecified_priority" EventLog.Priority.ESSENTIAL -> "essential" EventLog.Priority.OPTIONAL -> "optional" EventLog.Priority.UNRECOGNIZED -> "unknown_priority" } + + private fun OppiaMetricLog.Priority.toAnalyticsName() = when (this) { + OppiaMetricLog.Priority.PRIORITY_UNSPECIFIED -> "unspecified_priority" + OppiaMetricLog.Priority.LOW_PRIORITY -> "low_priority" + OppiaMetricLog.Priority.MEDIUM_PRIORITY -> "medium_priority" + OppiaMetricLog.Priority.HIGH_PRIORITY -> "high_priority" + OppiaMetricLog.Priority.UNRECOGNIZED -> "unknown_priority" + } + + private fun OppiaMetricLog.MemoryTier.toAnalyticsName() = when (this) { + OppiaMetricLog.MemoryTier.MEMORY_TIER_UNSPECIFIED -> "unspecified_memory_tier" + OppiaMetricLog.MemoryTier.LOW_MEMORY_TIER -> "low_memory" + OppiaMetricLog.MemoryTier.MEDIUM_MEMORY_TIER -> "medium_memory" + OppiaMetricLog.MemoryTier.HIGH_MEMORY_TIER -> "high_memory" + OppiaMetricLog.MemoryTier.UNRECOGNIZED -> "unknown_memory_tier" + } + + private fun OppiaMetricLog.StorageTier.toAnalyticsName() = when (this) { + OppiaMetricLog.StorageTier.STORAGE_TIER_UNSPECIFIED -> "unspecified_storage_tier" + OppiaMetricLog.StorageTier.LOW_STORAGE -> "low_storage" + OppiaMetricLog.StorageTier.MEDIUM_STORAGE -> "medium_storage" + OppiaMetricLog.StorageTier.HIGH_STORAGE -> "high_storage" + OppiaMetricLog.StorageTier.UNRECOGNIZED -> "unknown_storage_tier" + } + + private fun OppiaMetricLog.NetworkType.toAnalyticsName() = when (this) { + OppiaMetricLog.NetworkType.NETWORK_UNSPECIFIED -> "unspecified_network_type" + OppiaMetricLog.NetworkType.WIFI -> "wifi" + OppiaMetricLog.NetworkType.CELLULAR -> "cellular" + OppiaMetricLog.NetworkType.NONE -> "none" + OppiaMetricLog.NetworkType.UNRECOGNIZED -> "unknown_network_type" + } + + private fun OppiaMetricLog.CurrentScreen.toAnalyticsName() = when (this) { + OppiaMetricLog.CurrentScreen.SCREEN_UNSPECIFIED -> "unspecified_current_screen" + OppiaMetricLog.CurrentScreen.HOME_SCREEN -> "home_screen" + OppiaMetricLog.CurrentScreen.UNRECOGNIZED -> "unknown_screen_name" + // TODO(#4340): Add support for all screens which are going to be used for metric logging. + } } diff --git a/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt b/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt index fdfa943b5ad..9104e66a24f 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt @@ -11,4 +11,10 @@ interface LogUploader { /** Enqueues a [workRequest] using the [workManager] for uploading exception logs that are stored in the cache store. */ fun enqueueWorkRequestForExceptions(workManager: WorkManager, workRequest: PeriodicWorkRequest) + + /** Enqueues a [workRequest] using the [workManager] for uploading performance metrics logs that are stored in the cache store. */ + fun enqueueWorkRequestForPerformanceMetrics( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) } diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt index b2a1f71d4ef..eb285ed8864 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt @@ -38,7 +38,18 @@ class FirebaseEventLogger private constructor( * Logs a performance metric to Firebase Analytics with [NETWORK_USER_PROPERTY] and [COUNTRY_USER_PROPERTY]. */ override fun logPerformanceMetric(oppiaMetricLog: OppiaMetricLog) { - // TODO(#4335): Add implementation to upload performance metrics logs to firebase. + Bundle().let { + firebaseAnalytics.logEvent( + eventBundleCreator.fillPerformanceMetricsEventBundle( + oppiaMetricLog, + it + ), + it + ) + } + // TODO(#3792): Remove this usage of Locale. + firebaseAnalytics.setUserProperty(COUNTRY_USER_PROPERTY, Locale.getDefault().displayCountry) + firebaseAnalytics.setUserProperty(NETWORK_USER_PROPERTY, getNetworkStatus()) } private fun getNetworkStatus(): String { diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt index fbe0b7c4f57..067c7c4c815 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt @@ -8,6 +8,7 @@ import javax.inject.Inject private const val OPPIA_EVENT_WORK = "OPPIA_EVENT_WORK_REQUEST" private const val OPPIA_EXCEPTION_WORK = "OPPIA_EXCEPTION_WORK_REQUEST" +private const val OPPIA_PERFORMANCE_METRICS_WORK = "OPPIA_PERFORMANCE_METRICS_WORK" /** Enqueues work requests for uploading stored event/exception logs to the remote service. */ class FirebaseLogUploader @Inject constructor() : @@ -34,4 +35,15 @@ class FirebaseLogUploader @Inject constructor() : workRequest ) } + + override fun enqueueWorkRequestForPerformanceMetrics( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + workManager.enqueueUniquePeriodicWork( + OPPIA_PERFORMANCE_METRICS_WORK, + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } } diff --git a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt index 268b08e8a86..f9067c27f4c 100644 --- a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt @@ -28,6 +28,23 @@ import org.oppia.android.app.model.EventLog.RevisionCardContext import org.oppia.android.app.model.EventLog.StoryContext import org.oppia.android.app.model.EventLog.SubmitAnswerContext import org.oppia.android.app.model.EventLog.TopicContext +import org.oppia.android.app.model.OppiaMetricLog +import org.oppia.android.app.model.OppiaMetricLog.CurrentScreen +import org.oppia.android.app.model.OppiaMetricLog.CurrentScreen.HOME_SCREEN +import org.oppia.android.app.model.OppiaMetricLog.CurrentScreen.SCREEN_UNSPECIFIED +import org.oppia.android.app.model.OppiaMetricLog.LoggableMetric +import org.oppia.android.app.model.OppiaMetricLog.MemoryTier +import org.oppia.android.app.model.OppiaMetricLog.MemoryTier.HIGH_MEMORY_TIER +import org.oppia.android.app.model.OppiaMetricLog.MemoryTier.MEDIUM_MEMORY_TIER +import org.oppia.android.app.model.OppiaMetricLog.NetworkType +import org.oppia.android.app.model.OppiaMetricLog.NetworkType.CELLULAR +import org.oppia.android.app.model.OppiaMetricLog.NetworkType.WIFI +import org.oppia.android.app.model.OppiaMetricLog.Priority +import org.oppia.android.app.model.OppiaMetricLog.Priority.HIGH_PRIORITY +import org.oppia.android.app.model.OppiaMetricLog.Priority.MEDIUM_PRIORITY +import org.oppia.android.app.model.OppiaMetricLog.StorageTier +import org.oppia.android.app.model.OppiaMetricLog.StorageTier.HIGH_STORAGE +import org.oppia.android.app.model.OppiaMetricLog.StorageTier.MEDIUM_STORAGE import org.oppia.android.util.platformparameter.LEARNER_STUDY_ANALYTICS_DEFAULT_VALUE import org.oppia.android.util.platformparameter.LearnerStudyAnalytics import org.oppia.android.util.platformparameter.PlatformParameterValue @@ -74,6 +91,12 @@ class EventBundleCreatorTest { private const val TEST_IS_ANSWER_CORRECT = true private const val TEST_IS_ANSWER_CORRECT_STR = "true" private const val TEST_CONTENT_ID = "test_content_id" + private const val TEST_CPU_USAGE = Long.MAX_VALUE + private const val TEST_APK_SIZE = Long.MAX_VALUE + private const val TEST_STORAGE_USAGE = Long.MAX_VALUE + private const val TEST_STARTUP_LATENCY = Long.MAX_VALUE + private const val TEST_NETWORK_USAGE = Long.MAX_VALUE + private const val TEST_MEMORY_USAGE = Long.MAX_VALUE } @Inject @@ -97,6 +120,26 @@ class EventBundleCreatorTest { assertThat(bundle).string("priority").isEqualTo("unspecified_priority") } + @Test + fun testFillPerformanceMetricsBundle_defaultEvent_defaultsBundleAndRetsUnknownActivityContext() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + OppiaMetricLog.getDefaultInstance(), bundle + ) + + assertThat(typeName).isEqualTo("unknown_loggable_metric") + assertThat(bundle).hasSize(7) + assertThat(bundle).longInt("timestamp").isEqualTo(0) + assertThat(bundle).string("priority").isEqualTo("unspecified_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("false") + assertThat(bundle).string("memory_tier").isEqualTo("unspecified_memory_tier") + assertThat(bundle).string("storage_tier").isEqualTo("unspecified_storage_tier") + assertThat(bundle).string("network_type").isEqualTo("unspecified_network_type") + assertThat(bundle).string("current_screen").isEqualTo("unspecified_current_screen") + } + @Test fun testFillEventBundle_eventWithDefaultedContext_fillsPriorityAndTimeAndRetsUnknownContext() { setUpTestApplicationComponent() @@ -133,6 +176,118 @@ class EventBundleCreatorTest { assertThat(bundle).string("priority").isEqualTo("optional") } + @Test + fun testFillMetricsBundle_eventWithDefaultLoggableMetric_fillsDetailsAndRetsUnknownLog() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = createPerformanceMetricLog( + timestamp = TEST_TIMESTAMP_1, + priority = HIGH_PRIORITY, + currentScreen = HOME_SCREEN, + memoryTier = HIGH_MEMORY_TIER, + storageTier = HIGH_STORAGE, + networkType = WIFI, + isAppInForeground = true + ) + + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetricLog, + bundle + ) + + assertThat(typeName).isEqualTo("unknown_loggable_metric") + assertThat(bundle).hasSize(7) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffTimestamp_savesDifferentTimestampInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = createPerformanceMetricLog(timestamp = TEST_TIMESTAMP_2) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_2) + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDifferentPriority_savesDifferentPriorityInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = + createPerformanceMetricLog(priority = MEDIUM_PRIORITY) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("priority").isEqualTo("medium_priority") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffMemoryTier_savesDiffMemoryTierInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = + createPerformanceMetricLog(memoryTier = MEDIUM_MEMORY_TIER) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("memory_tier").isEqualTo("medium_memory") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffStorageTier_savesDiffStorageTierInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = + createPerformanceMetricLog(storageTier = MEDIUM_STORAGE) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("storage_tier").isEqualTo("medium_storage") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffCurrentScreen_savesDiffCurrentScreenInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = + createPerformanceMetricLog(currentScreen = SCREEN_UNSPECIFIED) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("current_screen").isEqualTo("unspecified_current_screen") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffNetworkType_savesDiffNetworkTypeInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = + createPerformanceMetricLog(networkType = CELLULAR) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("network_type").isEqualTo("cellular") + } + + @Test + fun testFillPerformanceMetricBundle_eventWithDiffAppInForeground_savesDiffValueInBundle() { + setUpTestApplicationComponent() + val bundle = Bundle() + val performanceMetricLog = createPerformanceMetricLog(isAppInForeground = false) + + eventBundleCreator.fillPerformanceMetricsEventBundle(performanceMetricLog, bundle) + + assertThat(bundle).string("is_app_in_foreground").isEqualTo("false") + } + @Test fun testFillEventBundle_openExpActivityEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() { setUpTestApplicationComponentWithoutLearnerAnalyticsStudy() @@ -205,6 +360,151 @@ class EventBundleCreatorTest { assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID) } + @Test + fun testFillPerformanceMetricBundle_createApkSizeLoggableMetric_bundlesAllDetailsCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createApkSizeLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("apk_size_metric") + assertThat(bundle).hasSize(8) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("apk_size_bytes").isEqualTo(TEST_APK_SIZE) + } + + @Test + fun testFillPerformanceMetricBundle_createStorageUsageLogMetric_bundlesAllDetailsCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createStorageUsageLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("storage_usage_metric") + assertThat(bundle).hasSize(8) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("storage_usage_bytes").isEqualTo(TEST_STORAGE_USAGE) + } + + @Test + fun testFillPerformanceMetricBundle_createStartupLatencyMetric_bundlesAllDetailsCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createStartupLatencyLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("startup_latency_metric") + assertThat(bundle).hasSize(8) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("startup_latency_millis").isEqualTo(TEST_STARTUP_LATENCY) + } + + @Test + fun testFillPerformanceMetricBundle_createMemoryUsageMetric_fillsAllDetailsInBundleCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createMemoryUsageLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("memory_usage_metric") + assertThat(bundle).hasSize(8) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("total_pss_bytes").isEqualTo(TEST_MEMORY_USAGE) + } + + @Test + fun testFillPerformanceMetricBundle_createNetworkUsageMetric_fillsAllDetailsInBundleCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createNetworkUsageTestLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("network_usage_metric") + assertThat(bundle).hasSize(9) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("bytes_received").isEqualTo(TEST_NETWORK_USAGE) + assertThat(bundle).longInt("bytes_sent").isEqualTo(TEST_NETWORK_USAGE) + } + + @Test + fun testFillPerformanceMetricBundle_createCpuUsageLogMetric_fillsAllDetailsInBundleCorrectly() { + setUpTestApplicationComponent() + val bundle = Bundle() + + val performanceMetric = createPerformanceMetricLog( + loggableMetric = createCpuUsageLoggableMetric() + ) + val typeName = eventBundleCreator.fillPerformanceMetricsEventBundle( + performanceMetric, bundle + ) + + assertThat(typeName).isEqualTo("cpu_usage_metric") + assertThat(bundle).hasSize(8) + assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1) + assertThat(bundle).string("priority").isEqualTo("high_priority") + assertThat(bundle).string("is_app_in_foreground").isEqualTo("true") + assertThat(bundle).string("memory_tier").isEqualTo("high_memory") + assertThat(bundle).string("storage_tier").isEqualTo("high_storage") + assertThat(bundle).string("network_type").isEqualTo("wifi") + assertThat(bundle).string("current_screen").isEqualTo("home_screen") + assertThat(bundle).longInt("cpu_usage").isEqualTo(TEST_CPU_USAGE) + } + @Test fun testFillEventBundle_openPracticeTabContextEvent_fillsAllFieldsInBundleAndReturnsName() { setUpTestApplicationComponent() @@ -949,6 +1249,26 @@ class EventBundleCreatorTest { this.context = context }.build() + private fun createPerformanceMetricLog( + timestamp: Long = TEST_TIMESTAMP_1, + priority: Priority = HIGH_PRIORITY, + currentScreen: CurrentScreen = HOME_SCREEN, + memoryTier: MemoryTier = HIGH_MEMORY_TIER, + storageTier: StorageTier = HIGH_STORAGE, + isAppInForeground: Boolean = true, + networkType: NetworkType = WIFI, + loggableMetric: LoggableMetric = LoggableMetric.getDefaultInstance() + ) = OppiaMetricLog.newBuilder().apply { + this.timestampMillis = timestamp + this.priority = priority + this.currentScreen = currentScreen + this.memoryTier = memoryTier + this.storageTier = storageTier + this.isAppInForeground = isAppInForeground + this.networkType = networkType + this.loggableMetric = loggableMetric + }.build() + private fun createOpenExplorationActivity( explorationContext: ExplorationContext = createExplorationContext() ) = createEventContext(explorationContext, EventContextBuilder::setOpenExplorationActivity) @@ -1138,6 +1458,49 @@ class EventBundleCreatorTest { this.contentId = contentId }.build() + private fun createApkSizeLoggableMetric() = OppiaMetricLog.LoggableMetric.newBuilder() + .setApkSizeMetric( + OppiaMetricLog.ApkSizeMetric.newBuilder() + .setApkSizeBytes(TEST_APK_SIZE) + .build() + ).build() + + private fun createStorageUsageLoggableMetric() = LoggableMetric.newBuilder() + .setStorageUsageMetric( + OppiaMetricLog.StorageUsageMetric.newBuilder() + .setStorageUsageBytes(TEST_STORAGE_USAGE) + .build() + ).build() + + private fun createStartupLatencyLoggableMetric() = LoggableMetric.newBuilder() + .setStartupLatencyMetric( + OppiaMetricLog.StartupLatencyMetric.newBuilder() + .setStartupLatencyMillis(TEST_STARTUP_LATENCY) + .build() + ).build() + + private fun createCpuUsageLoggableMetric() = LoggableMetric.newBuilder() + .setCpuUsageMetric( + OppiaMetricLog.CpuUsageMetric.newBuilder() + .setCpuUsageMetric(TEST_CPU_USAGE) + .build() + ).build() + + private fun createNetworkUsageTestLoggableMetric() = LoggableMetric.newBuilder() + .setNetworkUsageMetric( + OppiaMetricLog.NetworkUsageMetric.newBuilder() + .setBytesReceived(TEST_NETWORK_USAGE) + .setBytesSent(TEST_NETWORK_USAGE) + .build() + ).build() + + private fun createMemoryUsageLoggableMetric() = LoggableMetric.newBuilder() + .setMemoryUsageMetric( + OppiaMetricLog.MemoryUsageMetric.newBuilder() + .setTotalPssBytes(TEST_MEMORY_USAGE) + .build() + ).build() + private fun setUpTestApplicationComponentWithoutLearnerAnalyticsStudy() { setUpTestApplicationComponent() } diff --git a/utility/src/test/java/org/oppia/android/util/logging/firebase/LogReportingModuleTest.kt b/utility/src/test/java/org/oppia/android/util/logging/firebase/LogReportingModuleTest.kt new file mode 100644 index 00000000000..83f50f95c21 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/logging/firebase/LogReportingModuleTest.kt @@ -0,0 +1,150 @@ +package org.oppia.android.util.logging.firebase + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.data.DataProvidersInjector +import org.oppia.android.util.data.DataProvidersInjectorProvider +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLogger +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsEventLogger +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.platformparameter.ENABLE_LANGUAGE_SELECTION_UI_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.EnableLanguageSelectionUi +import org.oppia.android.util.platformparameter.LearnerStudyAnalytics +import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.platformparameter.SPLASH_SCREEN_WELCOME_MSG_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.SYNC_UP_WORKER_TIME_PERIOD_IN_HOURS_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.SplashScreenWelcomeMsg +import org.oppia.android.util.platformparameter.SyncUpWorkerTimePeriodHours +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [LogReportingModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = LogReportingModuleTest.TestApplication::class) +class LogReportingModuleTest { + + @Inject + lateinit var performanceMetricsEventLogger: PerformanceMetricsEventLogger + + @Inject + lateinit var eventLogger: EventLogger + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_injectsProductionImplementationOfEventLogger() { + assertThat(eventLogger).isInstanceOf(FirebaseEventLogger::class.java) + } + + @Test + fun testModule_injectsProductionImplementationOfPerformanceMetricsEventLogger() { + assertThat(performanceMetricsEventLogger).isInstanceOf(FirebaseEventLogger::class.java) + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + interface TestModule { + @Binds + fun provideContext(application: Application): Context + } + + @Module + class TestPlatformParameterModule { + + companion object { + var forceLearnerAnalyticsStudy: Boolean = false + } + + @Provides + @SplashScreenWelcomeMsg + fun provideSplashScreenWelcomeMsgParam(): PlatformParameterValue { + return PlatformParameterValue.createDefaultParameter(SPLASH_SCREEN_WELCOME_MSG_DEFAULT_VALUE) + } + + @Provides + @SyncUpWorkerTimePeriodHours + fun provideSyncUpWorkerTimePeriod(): PlatformParameterValue { + return PlatformParameterValue.createDefaultParameter( + SYNC_UP_WORKER_TIME_PERIOD_IN_HOURS_DEFAULT_VALUE + ) + } + + @Provides + @EnableLanguageSelectionUi + fun provideEnableLanguageSelectionUi(): PlatformParameterValue { + return PlatformParameterValue.createDefaultParameter( + ENABLE_LANGUAGE_SELECTION_UI_DEFAULT_VALUE + ) + } + + @Provides + @LearnerStudyAnalytics + fun provideLearnerStudyAnalytics(): PlatformParameterValue { + return PlatformParameterValue.createDefaultParameter(forceLearnerAnalyticsStudy) + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LogReportingModule::class, TestDispatcherModule::class, + RobolectricModule::class, FakeOppiaClockModule::class, + NetworkConnectionUtilDebugModule::class, LocaleProdModule::class, + TestPlatformParameterModule::class, LoggerModule::class, SyncStatusModule::class + ] + ) + interface TestApplicationComponent : DataProvidersInjector { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + fun build(): TestApplicationComponent + } + + fun inject(logReportingModuleTest: LogReportingModuleTest) + } + + class TestApplication : Application(), DataProvidersInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerLogReportingModuleTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: LogReportingModuleTest) { + component.inject(test) + } + + override fun getDataProvidersInjector(): DataProvidersInjector = component + } +}