From 12163f1f313e0ecbde9ca2abae1cd7b4a4ce9704 Mon Sep 17 00:00:00 2001 From: Alessio Placitelli Date: Thu, 3 Oct 2019 12:44:12 +0200 Subject: [PATCH] Use the Rust implementation of the Glean SDK This removes the Kotlin implementation from this repository and introduces type aliases that point to the Rust implementation, which is now a new dependency. --- buildSrc/src/main/java/Dependencies.kt | 4 + components/lib/crash/build.gradle | 1 + components/service/experiments/build.gradle | 1 + .../service/experiments/ExperimentsTest.kt | 5 + components/service/glean/build.gradle | 51 +- components/service/glean/metrics.yaml | 243 ------- components/service/glean/pings.yaml | 54 -- .../glean/src/main/AndroidManifest.xml | 7 +- .../components/service/glean/Dispatchers.kt | 174 ----- .../mozilla/components/service/glean/Glean.kt | 589 +-------------- .../service/glean/config/Configuration.kt | 57 +- .../service/glean/debug/GleanDebugActivity.kt | 107 --- .../service/glean/error/ErrorRecording.kt | 131 ---- .../glean/histogram/FunctionalHistogram.kt | 194 ----- .../glean/histogram/PrecomputedHistogram.kt | 276 ------- .../service/glean/net/BaseUploader.kt | 110 --- .../glean/net/ConceptFetchHttpUploader.kt | 2 + .../service/glean/net/PingUploader.kt | 26 - .../service/glean/ping/PingMaker.kt | 199 ------ .../glean/private/BooleanMetricType.kt | 87 --- .../service/glean/private/CommonMetricData.kt | 49 -- .../glean/private/CounterMetricType.kt | 90 --- .../private/CustomDistributionMetricType.kt | 106 --- .../glean/private/DatetimeMetricType.kt | 139 ---- .../service/glean/private/EventMetricType.kt | 131 ---- .../glean/private/HistogramMetricBase.kt | 18 - .../service/glean/private/HistogramType.kt | 13 - .../glean/private/LabeledMetricType.kt | 237 ------ .../private/MemoryDistributionMetricType.kt | 114 --- .../service/glean/private/MemoryUnit.kt | 17 - .../service/glean/private/MetricAliases.kt | 28 + .../service/glean/private/PingType.kt | 49 -- .../glean/private/QuantityMetricType.kt | 85 --- .../glean/private/StringListMetricType.kt | 111 --- .../service/glean/private/StringMetricType.kt | 89 --- .../service/glean/private/TimeUnit.kt | 19 - .../glean/private/TimespanMetricType.kt | 173 ----- .../private/TimingDistributionMetricType.kt | 157 ---- .../service/glean/private/UuidMetricType.kt | 107 --- .../glean/scheduler/GleanLifecycleObserver.kt | 44 -- .../glean/scheduler/MetricsPingScheduler.kt | 361 ---------- .../glean/scheduler/PingUploadWorker.kt | 97 --- .../glean/storages/BooleansStorageEngine.kt | 52 -- .../glean/storages/CountersStorageEngine.kt | 70 -- .../CustomDistributionsStorageEngine.kt | 120 ---- .../glean/storages/DatetimesStorageEngine.kt | 83 --- .../glean/storages/EventsStorageEngine.kt | 355 --------- .../storages/ExperimentsStorageEngine.kt | 142 ---- .../glean/storages/GenericStorageEngine.kt | 295 -------- .../MemoryDistributionsStorageEngine.kt | 167 ----- .../glean/storages/PingStorageEngine.kt | 202 ------ .../glean/storages/QuantitiesStorageEngine.kt | 66 -- .../service/glean/storages/StorageEngine.kt | 49 -- .../glean/storages/StorageEngineManager.kt | 165 ----- .../storages/StringListsStorageEngine.kt | 147 ---- .../glean/storages/StringsStorageEngine.kt | 71 -- .../glean/storages/TimespansStorageEngine.kt | 196 ----- .../TimingDistributionsStorageEngine.kt | 165 ----- .../glean/storages/UuidsStorageEngine.kt | 57 -- .../glean/testing/GleanTestLocalServer.kt | 14 +- .../service/glean/testing/GleanTestRule.kt | 43 +- .../service/glean/timing/TimingManager.kt | 134 ---- .../service/glean/utils/DateUtils.kt | 102 --- .../service/glean/utils/FileUtils.kt | 26 - .../service/glean/utils/LocaleUtils.kt | 49 -- .../service/glean/utils/MemoryUtils.kt | 25 - .../service/glean/utils/TimeUtils.kt | 48 -- .../service/glean/DispatchersTest.kt | 63 -- .../components/service/glean/GleanTest.kt | 672 +----------------- .../components/service/glean/TestUtil.kt | 254 ------- .../glean/debug/GleanDebugActivityTest.kt | 249 ------- .../service/glean/error/ErrorRecordingTest.kt | 67 -- .../histogram/FunctionalHistogramTest.kt | 70 -- .../histogram/PrecomputedHistogramTest.kt | 137 ---- .../service/glean/net/BaseUploaderTest.kt | 98 --- .../glean/net/ConceptFetchHttpUploaderTest.kt | 21 +- .../service/glean/ping/PingMakerTest.kt | 265 ------- .../glean/private/BooleanMetricTypeTest.kt | 103 --- .../glean/private/CounterMetricTypeTest.kt | 129 ---- .../CustomDistributionMetricTypeTest.kt | 175 ----- .../glean/private/DatetimeMetricTypeTest.kt | 91 --- .../glean/private/EventMetricTypeTest.kt | 209 ------ .../glean/private/LabeledMetricTypeTest.kt | 602 ---------------- .../MemoryDistributionMetricTypeTest.kt | 200 ------ .../service/glean/private/PingTypeTest.kt | 148 ---- .../glean/private/QuantityMetricTypeTest.kt | 120 ---- .../glean/private/StringListMetricTypeTest.kt | 192 ----- .../glean/private/StringMetricTypeTest.kt | 124 ---- .../glean/private/TimespanMetricTypeTest.kt | 288 -------- .../TimingDistributionMetricTypeTest.kt | 192 ----- .../glean/private/UuidMetricTypeTest.kt | 129 ---- .../scheduler/MetricsPingSchedulerTest.kt | 579 --------------- .../glean/scheduler/PingUploadWorkerTest.kt | 76 -- .../storages/BooleansStorageEngineTest.kt | 194 ----- .../storages/CountersStorageEngineTest.kt | 219 ------ .../CustomDistributionsStorageEngineTest.kt | 457 ------------ .../storages/DatetimesStorageEngineTest.kt | 283 -------- .../glean/storages/EventsStorageEngineTest.kt | 562 --------------- .../storages/ExperimentsStorageEngineTest.kt | 125 ---- .../storages/GenericStorageEngineTest.kt | 506 ------------- .../MemoryDistributionsStorageEngineTest.kt | 298 -------- .../storages/MockGenericStorageEngine.kt | 37 - .../glean/storages/MockStorageEngine.kt | 30 - .../glean/storages/PingStorageEngineTest.kt | 174 ----- .../storages/QuantitiesStorageEngineTest.kt | 236 ------ .../storages/StorageEngineManagerTest.kt | 37 - .../storages/StringListsStorageEngineTest.kt | 370 ---------- .../storages/StringsStorageEngineTest.kt | 220 ------ .../storages/TimespansStorageEngineTest.kt | 173 ----- .../TimingDistributionsStorageEngineTest.kt | 315 -------- .../glean/storages/UuidsStorageEngineTest.kt | 233 ------ .../support/sync-telemetry/build.gradle | 1 + docs/changelog.md | 4 + 113 files changed, 141 insertions(+), 16981 deletions(-) delete mode 100644 components/service/glean/metrics.yaml delete mode 100644 components/service/glean/pings.yaml delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/Dispatchers.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/debug/GleanDebugActivity.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/error/ErrorRecording.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/histogram/FunctionalHistogram.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/histogram/PrecomputedHistogram.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/net/BaseUploader.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/net/PingUploader.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/ping/PingMaker.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/BooleanMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/CommonMetricData.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/CounterMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/CustomDistributionMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/DatetimeMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/EventMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramMetricBase.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/LabeledMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryDistributionMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryUnit.kt create mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/PingType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/QuantityMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/StringListMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/StringMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/TimeUnit.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/TimespanMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/TimingDistributionMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/private/UuidMetricType.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/GleanLifecycleObserver.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/MetricsPingScheduler.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/PingUploadWorker.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/BooleansStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/CountersStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/DatetimesStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/EventsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/ExperimentsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/GenericStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/PingStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/QuantitiesStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngineManager.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringListsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimespansStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/storages/UuidsStorageEngine.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/timing/TimingManager.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/utils/DateUtils.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/utils/FileUtils.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/utils/LocaleUtils.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/utils/MemoryUtils.kt delete mode 100644 components/service/glean/src/main/java/mozilla/components/service/glean/utils/TimeUtils.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/DispatchersTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/TestUtil.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/debug/GleanDebugActivityTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/error/ErrorRecordingTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/histogram/FunctionalHistogramTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/histogram/PrecomputedHistogramTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/net/BaseUploaderTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/ping/PingMakerTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/BooleanMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/CounterMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/CustomDistributionMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/DatetimeMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/EventMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/LabeledMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/MemoryDistributionMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/PingTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/QuantityMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/StringListMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/StringMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/TimespanMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/TimingDistributionMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/private/UuidMetricTypeTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/MetricsPingSchedulerTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/PingUploadWorkerTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/BooleansStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/CountersStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/DatetimesStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/EventsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/ExperimentsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/GenericStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockGenericStorageEngine.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockStorageEngine.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/PingStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/QuantitiesStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/StorageEngineManagerTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringListsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimespansStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngineTest.kt delete mode 100644 components/service/glean/src/test/java/mozilla/components/service/glean/storages/UuidsStorageEngineTest.kt diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index da47669c8f7..d1232f61d98 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -29,6 +29,8 @@ object Versions { const val mozilla_appservices = "0.42.0" + const val mozilla_glean = "19.0.0" + const val material = "1.0.0" object AndroidX { @@ -120,6 +122,8 @@ object Dependencies { const val mozilla_fxa = "org.mozilla.appservices:fxaclient:${Versions.mozilla_appservices}" + const val mozilla_glean_forUnitTests = "org.mozilla.telemetry:glean-forUnitTests:${Versions.mozilla_glean}" + const val mozilla_sync_logins = "org.mozilla.appservices:logins:${Versions.mozilla_appservices}" const val mozilla_places = "org.mozilla.appservices:places:${Versions.mozilla_appservices}" const val mozilla_sync_manager = "org.mozilla.appservices:syncmanager:${Versions.mozilla_appservices}" diff --git a/components/lib/crash/build.gradle b/components/lib/crash/build.gradle index 54f45f74be7..1e5c398cb31 100644 --- a/components/lib/crash/build.gradle +++ b/components/lib/crash/build.gradle @@ -55,6 +55,7 @@ dependencies { testImplementation Dependencies.testing_mockito testImplementation Dependencies.testing_coroutines testImplementation Dependencies.testing_mockwebserver + testImplementation Dependencies.mozilla_glean_forUnitTests } ext.gleanGenerateMarkdownDocs = true diff --git a/components/service/experiments/build.gradle b/components/service/experiments/build.gradle index 5cf24fabe92..252019e7cc0 100644 --- a/components/service/experiments/build.gradle +++ b/components/service/experiments/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation Dependencies.kotlin_reflect testImplementation Dependencies.androidx_work_testing testImplementation Dependencies.androidx_espresso_core + testImplementation Dependencies.mozilla_glean_forUnitTests testImplementation project(':support-test') testImplementation project(':lib-fetch-httpurlconnection') diff --git a/components/service/experiments/src/test/java/mozilla/components/service/experiments/ExperimentsTest.kt b/components/service/experiments/src/test/java/mozilla/components/service/experiments/ExperimentsTest.kt index c7ee26a2a66..18a092ee5ea 100644 --- a/components/service/experiments/src/test/java/mozilla/components/service/experiments/ExperimentsTest.kt +++ b/components/service/experiments/src/test/java/mozilla/components/service/experiments/ExperimentsTest.kt @@ -14,6 +14,7 @@ import androidx.work.testing.WorkManagerTestInitHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import mozilla.components.service.glean.Glean +import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.support.test.any import mozilla.components.support.test.eq import mozilla.components.support.test.mock @@ -25,6 +26,7 @@ import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.assertNotNull +import org.junit.Rule import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString @@ -49,6 +51,9 @@ class ExperimentsTest { private val EXAMPLE_CLIENT_ID = "c641eacf-c30c-4171-b403-f077724e848a" + @get:Rule + val gleanRule = GleanTestRule(context) + private val experimentsList = listOf( createDefaultExperiment( id = "first-id", diff --git a/components/service/glean/build.gradle b/components/service/glean/build.gradle index 3b0500de9aa..147ee04c3f2 100644 --- a/components/service/glean/build.gradle +++ b/components/service/glean/build.gradle @@ -2,30 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -plugins { - id "com.jetbrains.python.envs" version "0.0.26" -} - apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -/* - * This defines the location of the JSON schema used to validate the pings - * created during unit testing. - * This uses a specific version of the schema identified by a git commit hash. - */ -String GLEAN_PING_SCHEMA_GIT_HASH = "c566160" -String GLEAN_PING_SCHEMA_URL = "https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas/$GLEAN_PING_SCHEMA_GIT_HASH/schemas/glean/baseline/baseline.1.schema.json" - android { compileSdkVersion config.compileSdkVersion defaultConfig { minSdkVersion config.minSdkVersion targetSdkVersion config.targetSdkVersion - - buildConfigField("String", "LIBRARY_VERSION", "\"" + config.componentsVersion + "\"") - buildConfigField("String", "GLEAN_PING_SCHEMA_URL", "\"" + GLEAN_PING_SCHEMA_URL + "\"") } buildTypes { @@ -36,11 +21,34 @@ android { } } +configurations { + // There's an interaction between Gradle's resolution of dependencies with different types + // (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in + // JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the + // dependency from the `implementation`, which is type @aar, and therefore the JNA dependency + // doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think + // what's happening is that @aar type in `implementation` resolves to the @jar type in + // `testImplementation`, and that it wins the dependency resolution battle. + // + // A workaround is to add a new configuration which depends on the @jar type and to reference + // the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to + // the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the + // correct runtime classpath when invoked with Android Studio's built-in JUnit test runner. + // Success! + jnaForTest +} + +// Define library names and version constants. +String GLEAN_LIBRARY = "org.mozilla.telemetry:glean:${Versions.mozilla_glean}" +String GLEAN_LIBRARY_FORUNITTESTS = "org.mozilla.telemetry:glean-forUnitTests:${Versions.mozilla_glean}" + dependencies { implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines implementation Dependencies.androidx_lifecycle_extensions implementation Dependencies.androidx_work_runtime + + api GLEAN_LIBRARY implementation project(':support-ktx') implementation project(':support-base') @@ -48,15 +56,6 @@ dependencies { implementation project(':lib-fetch-httpurlconnection') implementation project(':support-utils') - // We need a compileOnly dependency on the following block of testing - // libraries in order to expose the GleanTestRule to applications/libraries - // using the Glean SDK. - // We can't simply create a separate package otherwise we would need - // to provide a public API for the testing package to access the - // Glean internals, which is something we would not want to do. - compileOnly Dependencies.testing_junit - compileOnly Dependencies.androidx_work_testing - testImplementation Dependencies.androidx_test_core testImplementation Dependencies.testing_junit @@ -67,9 +66,9 @@ dependencies { testImplementation project(':support-test') testImplementation project(':lib-fetch-okhttp') + + testImplementation GLEAN_LIBRARY_FORUNITTESTS } apply from: '../../../publish.gradle' ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) - -apply from: './scripts/sdk_generator.gradle' diff --git a/components/service/glean/metrics.yaml b/components/service/glean/metrics.yaml deleted file mode 100644 index df13d29424d..00000000000 --- a/components/service/glean/metrics.yaml +++ /dev/null @@ -1,243 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -# This file defines the metrics that are recorded by Glean telemetry. They are -# automatically converted to Kotlin code at build time using the `glean_parser` -# PyPI package. - -$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 - -glean.baseline: - duration: - type: timespan - description: | - The duration of the last foreground session. - time_unit: second - send_in_pings: - - baseline - bugs: - - https://bugzilla.mozilla.org/1497894 - - https://bugzilla.mozilla.org/1519120 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - locale: - type: string - lifetime: application - send_in_pings: - - baseline - description: | - The locale of the application (e.g. "es-ES"). - bugs: - - https://bugzilla.mozilla.org/1512938 - - https://bugzilla.mozilla.org/1525540 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - -glean.internal.metrics: - os: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The name of the operating system. - bugs: - - https://bugzilla.mozilla.org/1497894 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - os_version: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The user-visible version of the operating system (e.g. "1.2.3"). - bugs: - - https://bugzilla.mozilla.org/1497894 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - android_sdk_version: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The optional Android specific SDK version of the software running on this hardware device. - bugs: - - https://bugzilla.mozilla.org/1525606 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1525606#c14 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - device_manufacturer: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The manufacturer of the device the application is running on. - bugs: - - https://bugzilla.mozilla.org/1522552 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - device_model: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The model of the device the application is running on. - bugs: - - https://bugzilla.mozilla.org/1522552 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - architecture: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The architecture of the device, (e.g. "arm", "x86"). - bugs: - - https://bugzilla.mozilla.org/1497894 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - client_id: - type: uuid - description: - A UUID uniquely identifying the client. - send_in_pings: - - glean_client_info - lifetime: user - bugs: - - https://bugzilla.mozilla.org/1497894 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - app_build: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The build identifier generated by the CI system (e.g. "1234/A"). - bugs: - - https://bugzilla.mozilla.org/1508305 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - app_display_version: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The user visible version string (e.g. "1.0.3"). - bugs: - - https://bugzilla.mozilla.org/1508305 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1508305#c9 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - app_channel: - type: string - lifetime: application - send_in_pings: - - glean_client_info - description: | - The channel the application is being distributed on. - bugs: - - https://bugzilla.mozilla.org/1520741 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1520741#c18 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - - first_run_date: - type: datetime - lifetime: user - send_in_pings: - - glean_client_info - time_unit: day - description: | - The date of the first run of the application. - bugs: - - https://bugzilla.mozilla.org/1525045 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1525045#c18 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - -glean.error: - invalid_value: - type: labeled_counter - send_in_pings: - - all_pings - description: - Counts the number of times a metric was set to an invalid value. - The labels are the `category.name` identifier of the metric. - bugs: - - https://bugzilla.mozilla.org/1499761 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1499761#c5 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - no_lint: - - COMMON_PREFIX - - invalid_label: - type: labeled_counter - send_in_pings: - - all_pings - description: - Counts the number of times a metric was set with an invalid label. - The labels are the `category.name` identifier of the metric. - bugs: - - https://bugzilla.mozilla.org/1499761 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1499761#c5 - notification_emails: - - telemetry-client-dev@mozilla.com - expires: never - no_lint: - - COMMON_PREFIX diff --git a/components/service/glean/pings.yaml b/components/service/glean/pings.yaml deleted file mode 100644 index e1459bfefff..00000000000 --- a/components/service/glean/pings.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -# This file defines the built-in pings that are recorded by glean telemetry. They are -# automatically converted to Kotlin code at build time using the `glean_parser` -# PyPI package. - -$schema: moz://mozilla.org/schemas/glean/pings/1-0-0 - -baseline: - description: > - This ping is intended to provide metrics that are managed by the library - itself, and not explicitly set by the application or included in the - application's `metrics.yaml` file. - The `baseline` ping is automatically sent when the application is moved to - the background. - include_client_id: true - bugs: - - https://bugzilla.mozilla.org/1512938 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com - -metrics: - description: > - The `metrics` ping is intended for all of the metrics that are explicitly - set by the application or are included in the application's `metrics.yaml` - file (except events). - The reported data is tied to the ping's *measurement window*, which is the - time between the collection of two `metrics` ping. Ideally, this window is - expected to be about 24 hours, given that the collection is scheduled daily - at 4AM. Data in the `ping_info` section of the ping can be used to infer the - length of this window. - include_client_id: true - bugs: - - https://bugzilla.mozilla.org/1512938 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com -events: - description: > - The events ping's purpose is to transport all of the event metric information. - The `events` ping is automatically sent when the application is moved to - the background. - include_client_id: true - bugs: - - https://bugzilla.mozilla.org/1512938 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1512938#c3 - notification_emails: - - telemetry-client-dev@mozilla.com diff --git a/components/service/glean/src/main/AndroidManifest.xml b/components/service/glean/src/main/AndroidManifest.xml index 6f83f1ea482..eb75de9d951 100644 --- a/components/service/glean/src/main/AndroidManifest.xml +++ b/components/service/glean/src/main/AndroidManifest.xml @@ -2,9 +2,4 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - - + package="mozilla.components.service.glean" /> diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/Dispatchers.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/Dispatchers.kt deleted file mode 100644 index b9ef86b039c..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/Dispatchers.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean - -import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.ObsoleteCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.newSingleThreadContext -import kotlinx.coroutines.runBlocking -import mozilla.components.support.base.log.logger.Logger -import java.util.concurrent.ConcurrentLinkedQueue - -@ObsoleteCoroutinesApi -internal object Dispatchers { - class WaitableCoroutineScope(val coroutineScope: CoroutineScope) { - // When true, jobs will be run synchronously - internal var testingMode = false - - // When true, jobs will be queued and not ran until triggered by calling - // flushQueuedInitialTasks() - @Volatile - private var queueInitialTasks = true - - // Use a [ConcurrentLinkedQueue] to take advantage of it's thread safety and no locking - internal val taskQueue: ConcurrentLinkedQueue<() -> Unit> = ConcurrentLinkedQueue() - - private val logger = Logger("glean/Dispatchers") - - companion object { - // This value was chosen in order to allow several tasks to be queued for execution but - // still be conservative of memory. This queue size is important for cases where - // setUploadEnabled(false) is not called so that we don't continue to queue tasks and - // waste memory. - const val MAX_QUEUE_SIZE = 100 - } - - /** - * Launch a block of work asynchronously. - * - * If [queueInitialTasks] is true, then the work will be queued and executed when - * [flushQueuedInitialTasks] is called. - * - * If [setTestingMode] has enabled testing mode, the work will run synchronously. This is - * true regardless of whether [queueInitialTasks] is true or not. - * - * @return [Job], or null if queued or run synchronously. - */ - fun launch( - block: suspend CoroutineScope.() -> Unit - ): Job? { - return when { - queueInitialTasks -> { - addTaskToQueue(block) - null - } - else -> executeTask(block) - } - } - - /** - * Helper function to ensure Glean is being used in testing mode and async - * jobs are being run synchronously. This should be called from every method - * in the testing API to make sure that the results of the main API can be - * tested as expected. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun assertInTestingMode() { - assert( - testingMode - ) { - "To use the testing API, Glean must be in testing mode by calling " + - "Glean.enableTestingMode() (for example, in a @Before method)." - } - } - - /** - * Enable testing mode, which makes all of the Glean public API synchronous. - * - * @param enabled whether or not to enable the testing mode - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun setTestingMode(enabled: Boolean) { - testingMode = enabled - } - - /** - * Enable queueing mode, which causes tasks to be queued until launched by calling - * [flushQueuedInitialTasks]. - * - * @param enabled whether or not to enable the testing mode - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun setTaskQueueing(enabled: Boolean) { - queueInitialTasks = enabled - } - - /** - * Stops queueing tasks and processes any tasks in the queue. Since [queueInitialTasks] is - * set to false prior to processing the queue, newly launched tasks should be executed - * on the couroutine scope rather than added to the queue. - */ - internal fun flushQueuedInitialTasks() { - // Setting this to false first should cause any new tasks to just be executed (see - // launch() above) making it safer to process the queue. - // - // NOTE: This has the potential for causing a task to execute out of order in certain - // situations. If a library or thread that runs before init happens to record - // between when the queueInitialTasks is set to false and the taskQueue finishing - // launching, then that task could be executed out of the queued order. See the bugzilla - // bug tracking this for more info: https://bugzilla.mozilla.org/show_bug.cgi?id=1568503 - queueInitialTasks = false - taskQueue.forEach { task -> - task.invoke() - } - taskQueue.clear() - } - - /** - * Helper function to add task to queue as either a synchronous or asynchronous operation, - * depending on whether [testingMode] is true. - */ - private fun addTaskToQueue(block: suspend CoroutineScope.() -> Unit) { - if (taskQueue.size >= MAX_QUEUE_SIZE) { - logger.error("Exceeded maximum queue size, discarding task") - return - } - - if (testingMode) { - logger.info("Task queued for execution in test mode") - taskQueue.add { - runBlocking { - block() - } - } - } else { - logger.info("Task queued for execution and delayed until flushed") - taskQueue.add { - coroutineScope.launch(block = block) - } - } - } - - /** - * Helper function to execute the task as either an synchronous or asynchronous operation, - * depending on whether [testingMode] is true. - * - * WARNING: THIS SHOULD ALMOST NEVER BE USED. IF IN DOUBT, USE [launch] INSTEAD. - * - * This has internal visibility only so that it can be called directly to - * send queued events immediately at startup before any metric recording. - */ - internal fun executeTask(block: suspend CoroutineScope.() -> Unit): Job? { - return when { - testingMode -> { - runBlocking { - block() - } - null - } - else -> coroutineScope.launch(block = block) - } - } - } - - /** - * A coroutine scope to make it easy to dispatch API calls off the main thread. - * This needs to be a `var` so that our tests can override this. - */ - var API = WaitableCoroutineScope(CoroutineScope(newSingleThreadContext("GleanAPIPool"))) -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt index 148b5813c92..cb9b2a60f70 100644 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt @@ -4,85 +4,19 @@ package mozilla.components.service.glean -import android.annotation.SuppressLint import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting -import androidx.lifecycle.ProcessLifecycleOwner -import kotlinx.coroutines.Job -import kotlinx.coroutines.joinAll -import mozilla.components.service.glean.GleanMetrics.GleanBaseline -import java.io.File -import java.util.UUID - import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.GleanMetrics.GleanInternalMetrics -import mozilla.components.service.glean.GleanMetrics.Pings -import mozilla.components.service.glean.net.BaseUploader -import mozilla.components.service.glean.ping.PingMaker -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.scheduler.GleanLifecycleObserver -import mozilla.components.service.glean.scheduler.MetricsPingScheduler -import mozilla.components.service.glean.scheduler.PingUploadWorker -import mozilla.components.service.glean.storages.StorageEngineManager -import mozilla.components.service.glean.storages.PingStorageEngine -import mozilla.components.service.glean.storages.ExperimentsStorageEngine -import mozilla.components.service.glean.storages.UuidsStorageEngine -import mozilla.components.service.glean.storages.DatetimesStorageEngine -import mozilla.components.service.glean.storages.EventsStorageEngine -import mozilla.components.service.glean.storages.RecordedExperimentData -import mozilla.components.service.glean.storages.StringsStorageEngine -import mozilla.components.service.glean.utils.ensureDirectoryExists -import mozilla.components.service.glean.utils.getLocaleTag -import mozilla.components.service.glean.utils.parseISOTimeString -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.android.content.isMainProcess -import mozilla.components.support.utils.ThreadUtils - -@Suppress("TooManyFunctions", "LargeClass") -open class GleanInternalAPI internal constructor () { - private val logger = Logger("glean/Glean") - - private var applicationContext: Context? = null - - // Include our singletons of StorageEngineManager and PingMaker - private lateinit var storageEngineManager: StorageEngineManager - internal lateinit var pingMaker: PingMaker - internal lateinit var configuration: Configuration - - // This is the wrapped http uploading mechanism: provides base functionalities - // for logging and delegates the actual upload to the implementation in - // the `Configuration`. - internal val httpClient by lazy { BaseUploader(configuration.httpClient) } - - private val gleanLifecycleObserver by lazy { GleanLifecycleObserver() } - - // `internal` so this can be modified for testing - internal var initialized = false - internal var uploadEnabled = true - - // The application id detected by Glean to be used as part of the submission - // endpoint. - internal lateinit var applicationId: String - - // This object holds data related to any persistent information about the metrics ping, - // such as the last time it was sent and the store name - internal lateinit var metricsPingScheduler: MetricsPingScheduler - - internal lateinit var pingStorageEngine: PingStorageEngine - - companion object { - internal val KNOWN_CLIENT_ID = UUID.fromString( - "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0" - ) - - // The SharedPreferences file containing migration settings used for upgrading - // from glean-ac to glean-core. - internal const val MIGRATION_PREFS_FILE = "mozilla.components.service.glean.GleanACDataMigrator" - } +import mozilla.components.service.glean.private.RecordedExperimentData +import mozilla.telemetry.glean.Glean as GleanCore +/** + * In contrast with other glean-ac classes (i.e. Configuration), we can't + * use typealias to export mozilla.telemetry.glean.Glean, as we need to provide + * a different default [Configuration]. Moreover, we can't simply delegate other + * methods or inherit, since that doesn't work for `object` in Kotlin. + */ +object Glean { /** * Initialize Glean. * @@ -93,88 +27,18 @@ open class GleanInternalAPI internal constructor () { * A LifecycleObserver will be added to send pings when the application goes * into the background. * - * This method must be called from the main thread. - * * @param applicationContext [Context] to access application features, such * as shared preferences * @param configuration A Glean [Configuration] object with global settings. */ - @JvmOverloads - @MainThread fun initialize( applicationContext: Context, configuration: Configuration = Configuration() ) { - // Glean initialization must be called on the main thread, or lifecycle - // registration may fail. This is also enforced at build time by the - // @MainThread decorator, but this run time check is also performed to - // be extra certain. - ThreadUtils.assertOnUiThread() - - // In certain situations Glean.initialize may be called from a process other than the main - // process. In this case we want initialize to be a no-op and just return. - if (!applicationContext.isMainProcess()) { - logger.error("Attempted to initialize Glean on a process other than the main process") - return - } - - if (isInitialized()) { - logger.error("Glean should not be initialized multiple times") - return - } - - registerPings(Pings) - - this.applicationContext = applicationContext - - storageEngineManager = StorageEngineManager(applicationContext = applicationContext) - pingMaker = PingMaker(storageEngineManager, applicationContext) - this.configuration = configuration - applicationId = sanitizeApplicationId(applicationContext.packageName) - pingStorageEngine = PingStorageEngine(applicationContext) - - // Core metrics are initialized using the engines, without calling the async - // API. For this reason we're safe to set `initialized = true` right after it. - onChangeUploadEnabled(uploadEnabled) - - // This must be set before anything that might trigger the sending of pings. - initialized = true - - // Deal with any pending events so we can start recording new ones - EventsStorageEngine.onReadyToSendPings(applicationContext) - - // Set up information and scheduling for Glean owned pings. Ideally, the "metrics" - // ping startup check should be performed before any other ping, since it relies - // on being dispatched to the API context before any other metric. - metricsPingScheduler = MetricsPingScheduler(applicationContext) - - // If Glean is being initialized from a test, then we want to skip running the - // startupCheck() as it can cause some intermittent test failures due to multi- - // threaded race conditions. - @Suppress("EXPERIMENTAL_API_USAGE") - if (!Dispatchers.API.testingMode) { - metricsPingScheduler.schedule() - } - - // Flush any tasks that were queued prior to initalization. - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.flushQueuedInitialTasks() - - // At this point, all metrics and events can be recorded. - // This should only be called from the main thread. This is enforced by - // the @MainThread decorator on this method. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1581556 - ProcessLifecycleOwner.get().lifecycle.addObserver(gleanLifecycleObserver) - - // Reset the migration status. - resetMigration() - } - - /** - * Returns true if the Glean library has been initialized. - */ - fun isInitialized(): Boolean { - return initialized + GleanCore.initialize( + applicationContext = applicationContext, + configuration = configuration.toWrappedConfiguration() + ) } /** @@ -184,9 +48,7 @@ open class GleanInternalAPI internal constructor () { * by Glean. */ fun registerPings(pings: Any) { - // Instantiating the Pings object to send this function is enough to - // call the constructor and have it registered in [PingType.pingRegistry]. - logger.info("Registering pings for ${pings.javaClass.canonicalName}") + GleanCore.registerPings(pings) } /** @@ -194,110 +56,20 @@ open class GleanInternalAPI internal constructor () { * * Metric collection is enabled by default. * - * When uploading is disabled, metrics aren't recorded at all and no data + * When disabled, metrics aren't recorded at all and no data * is uploaded. * - * When disabling, all pending metrics, events and queued pings are cleared. - * - * When enabling, the core Glean metrics are recreated. - * - * If the value of this flag is not actually changed, this is a no-op. - * * @param enabled When true, enable metric collection. */ fun setUploadEnabled(enabled: Boolean) { - logger.info("Metrics enabled: $enabled") - val origUploadEnabled = uploadEnabled - uploadEnabled = enabled - if (isInitialized() && origUploadEnabled != enabled) { - onChangeUploadEnabled(enabled) - } + GleanCore.setUploadEnabled(enabled) } /** * Get whether or not Glean is allowed to record and upload data. */ fun getUploadEnabled(): Boolean { - return uploadEnabled - } - - /** - * Set the data migration flag to 'was not migrated'. - * - * This is only useful as a safety measure in case the data migration plan goes - * wrong and we're forced to downgrade the version Glean SDK consumers are using - * from glean-core to glean-ac. In that case, this code will run and will reset - * the status of the migration. When attempting the upgrade again, glean-core - * will perform the migration one more time. - */ - private fun resetMigration() { - val migrationPrefs = applicationContext?.getSharedPreferences( - MIGRATION_PREFS_FILE, - Context.MODE_PRIVATE - ) - - // Just set this to 'false'. Once substituted, the glean-ac implementation - // will read the same file and perform the migration. - migrationPrefs?.edit()?.putBoolean("wasMigrated", false)?.apply() - } - - /** - * Handles the changing of state when uploadEnabled changes. - * - * When disabling, all pending metrics, events and queued pings are cleared. - * - * When enabling, the core Glean metrics are recreated. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun onChangeUploadEnabled(enabled: Boolean) { - if (enabled) { - initializeCoreMetrics(applicationContext!!) - } else { - cancelPingWorkers() - clearMetrics() - } - } - - /** - * Cancel any pending [PingUploadWorker] objects that have been enqueued so that we don't - * accidentally upload or collect data after the upload has been disabled. - */ - private fun cancelPingWorkers() { - MetricsPingScheduler.cancel(applicationContext!!) - PingUploadWorker.cancel(applicationContext!!) - } - - /** - * Clear any pending metrics when telemetry is disabled. - */ - @Suppress("EXPERIMENTAL_API_USAGE") - private fun clearMetrics() = Dispatchers.API.launch { - // There is only one metric that we want to survive after clearing all metrics: - // firstRunDate. Here, we store its value so we can restore it after clearing - // the metrics. - val firstRunDateMetric = GleanInternalMetrics.firstRunDate - val existingFirstRunDate = DatetimesStorageEngine.getSnapshot( - firstRunDateMetric.sendInPings[0], false)?.get(firstRunDateMetric.identifier)?.let { - parseISOTimeString(it) - } - - pingStorageEngine.clearPendingPings() - storageEngineManager.clearAllStores() - pingMaker.resetPingMakerStorage() - - // This does not clear the experiments store (which isn't managed by the - // StorageEngineManager), since doing so would mean we would have to have the - // application tell us again which experiments are active if telemetry is - // re-enabled. - - // Store a "dummy" KNOWN_CLIENT_ID in clientId. This will make it easier to detect if - // pings were unintentionally sent after uploading is disabled. - UuidsStorageEngine.record(GleanInternalMetrics.clientId, KNOWN_CLIENT_ID) - - // Restore the firstRunDate - existingFirstRunDate?.let { - DatetimesStorageEngine.set(firstRunDateMetric, it) - } + return GleanCore.getUploadEnabled() } /** @@ -310,13 +82,16 @@ open class GleanInternalAPI internal constructor () { * @param branch The experiment branch (maximum 30 bytes) * @param extra Optional metadata to output with the ping */ - @JvmOverloads fun setExperimentActive( experimentId: String, branch: String, extra: Map? = null ) { - ExperimentsStorageEngine.setExperimentActive(experimentId, branch, extra) + GleanCore.setExperimentActive( + experimentId = experimentId, + branch = branch, + extra = extra + ) } /** @@ -325,7 +100,7 @@ open class GleanInternalAPI internal constructor () { * @param experimentId The id of the experiment to deactivate. */ fun setExperimentInactive(experimentId: String) { - ExperimentsStorageEngine.setExperimentInactive(experimentId) + GleanCore.setExperimentInactive(experimentId = experimentId) } /** @@ -336,7 +111,7 @@ open class GleanInternalAPI internal constructor () { */ @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun testIsExperimentActive(experimentId: String): Boolean { - return ExperimentsStorageEngine.getSnapshot()[experimentId] != null + return GleanCore.testIsExperimentActive(experimentId) } /** @@ -344,322 +119,10 @@ open class GleanInternalAPI internal constructor () { * * @param experimentId the id of the experiment to look for. * @return the [RecordedExperimentData] for the experiment - * @throws [NullPointerException] if the requested experiment is not active + * @throws [NullPointerException] if the requested experiment is not active or data is corrupt. */ @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun testGetExperimentData(experimentId: String): RecordedExperimentData { - return ExperimentsStorageEngine.getSnapshot().getValue(experimentId) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun makePath(docType: String, uuid: UUID): String { - return "/submit/$applicationId/$docType/${Glean.SCHEMA_VERSION}/$uuid" - } - - /** - * Fixes some legacy metrics. - * - * This is a BACKWARD COMPATIBILITY HACK. - * See 1539480: The implementation of 1528787 moved the client_id and - * first_run_date metrics from the ping_info to the client_info - * sections. This introduced a bug that the client_id would not be - * picked up from the old location on devices that had already run - * the application. Missing a client_id is particularly problematic - * because these pings will be rejected by the pipeline. This fix - * looks for these metrics at their old locations, and if found, - * copies them to the new location. If they already exist in the - * new location, this shouldn't override them. - */ - private fun fixLegacyPingInfoMetrics() { - val uuidClientInfoSnapshot = UuidsStorageEngine.getSnapshot("glean_client_info", false) - val newClientId = uuidClientInfoSnapshot?.get("client_id") - if (newClientId == null) { - val uuidPingInfoSnapshot = UuidsStorageEngine.getSnapshot("glean_ping_info", false) - val legacyClientId = uuidPingInfoSnapshot?.get("client_id") - if (legacyClientId != null) { - UuidsStorageEngine.record(GleanInternalMetrics.clientId, legacyClientId) - } else { - // Apparently, this is a very old build that was already run - // at least once and has a different name for the underlying - // UUID storage engine persistence (i.e. UuidStorageEngine.xml - // vs using the full class name with the namespace). - // This should be mostly devs and super early adopters, so just regenerate - // the data. - UuidsStorageEngine.record(GleanInternalMetrics.clientId, UUID.randomUUID()) - } - } - - val datetimeClientInfoSnapshot = DatetimesStorageEngine.getSnapshot("glean_client_info", false) - val newFirstRunDate = datetimeClientInfoSnapshot?.get("first_run_date") - if (newFirstRunDate == null) { - val datetimePingInfoSnapshot = DatetimesStorageEngine.getSnapshot("glean_ping_info", false) - var firstRunDateSet = false - datetimePingInfoSnapshot?.get("first_run_date")?.let { - parseISOTimeString(it)?.let { parsedDate -> - DatetimesStorageEngine.set(GleanInternalMetrics.firstRunDate, parsedDate) - firstRunDateSet = true - } - } - - if (!firstRunDateSet) { - // Apparently, this is a very old build that was already run - // at least once and has a different name for the underlying - // Datetime storage engine persistence (i.e. DatetimeStorageEngine.xml - // vs using the full class name with the namespace). - // This should be mostly devs and super early adopters, so just regenerate - // the data. - DatetimesStorageEngine.set(GleanInternalMetrics.firstRunDate) - } - } - } - - /** - * Initialize the core metrics internally managed by Glean (e.g. client id). - */ - private fun initializeCoreMetrics(applicationContext: Context) { - // Since all of the ping_info properties are required, we can't - // use the normal metrics API to set them, since those work - // asynchronously, and there is a race condition between when they - // are set and the possible sending of the first ping upon startup. - // Therefore, this uses the lower-level internal storage engine API - // to set these metrics, which is synchronous. - - val gleanDataDir = File(applicationContext.applicationInfo.dataDir, Glean.GLEAN_DATA_DIR) - - ensureDirectoryExists(gleanDataDir) - - // The first time Glean runs, we set the client id and other internal - // one-time only metrics. - fixLegacyPingInfoMetrics() - - val clientIdMetric = GleanInternalMetrics.clientId - val existingClientId = UuidsStorageEngine.getSnapshot( - clientIdMetric.sendInPings[0], false)?.get(clientIdMetric.identifier) - if (existingClientId == null || existingClientId == KNOWN_CLIENT_ID) { - val uuid = UUID.randomUUID() - UuidsStorageEngine.record(clientIdMetric, uuid) - } - - val firstRunDateMetric = GleanInternalMetrics.firstRunDate - val existingFirstRunDate = DatetimesStorageEngine.getSnapshot( - firstRunDateMetric.sendInPings[0], false)?.get(firstRunDateMetric.identifier) - if (existingFirstRunDate == null) { - DatetimesStorageEngine.set(firstRunDateMetric) - } - - // Set a few more metrics that will be sent as part of every ping. - StringsStorageEngine.record(GleanBaseline.locale, getLocaleTag()) - StringsStorageEngine.record(GleanInternalMetrics.os, "Android") - // https://developer.android.com/reference/android/os/Build.VERSION - StringsStorageEngine.record(GleanInternalMetrics.androidSdkVersion, Build.VERSION.SDK_INT.toString()) - StringsStorageEngine.record(GleanInternalMetrics.osVersion, Build.VERSION.RELEASE) - // https://developer.android.com/reference/android/os/Build - StringsStorageEngine.record(GleanInternalMetrics.deviceManufacturer, Build.MANUFACTURER) - StringsStorageEngine.record(GleanInternalMetrics.deviceModel, Build.MODEL) - StringsStorageEngine.record(GleanInternalMetrics.architecture, Build.SUPPORTED_ABIS[0]) - - configuration.channel?.let { - StringsStorageEngine.record(GleanInternalMetrics.appChannel, it) - } - - try { - val packageInfo = applicationContext.packageManager.getPackageInfo( - applicationContext.packageName, 0 - ) - @Suppress("DEPRECATION") - StringsStorageEngine.record( - GleanInternalMetrics.appBuild, - packageInfo.versionCode.toString() - ) - StringsStorageEngine.record( - GleanInternalMetrics.appDisplayVersion, - packageInfo.versionName?.let { it } ?: "Unknown" - ) - } catch (e: PackageManager.NameNotFoundException) { - logger.error("Could not get own package info, unable to report build id and display version") - throw AssertionError("Could not get own package info, aborting init") - } + return GleanCore.testGetExperimentData(experimentId) } - - /** - * Sanitizes the application id, generating a pipeline-friendly string that replaces - * non alphanumeric characters with dashes. - * - * @param applicationId the string representing the application id - * - * @return the sanitized version of the application id - */ - internal fun sanitizeApplicationId(applicationId: String): String { - return applicationId.replace("[^a-zA-Z0-9]+".toRegex(), "-") - } - - /** - * Handle the background event and send the appropriate pings. - */ - internal fun handleBackgroundEvent() { - // Schedule the baseline and event pings - sendPings(listOf(Pings.baseline, Pings.events)) - } - - /** - * Send a list of pings. - * - * The ping content is assembled synchronously, but serialization to disk - * happens in a deferred coroutine, and upload happens later as that depends - * on the upload policies. - * - * If the ping currently contains no content, it will not be sent. - * - * @param pings List of pings to send. - * @return The async Job performing the work of assembling the ping - */ - @Suppress("EXPERIMENTAL_API_USAGE") - internal fun sendPings(pings: List) { - if (!isInitialized()) { - logger.error("Glean must be initialized before sending pings.") - return - } - - if (!uploadEnabled) { - logger.error("Glean must be enabled before sending pings.") - return - } - - val pingSerializationTasks = mutableListOf() - for (ping in pings) { - assembleAndSerializePing(ping)?.let { - pingSerializationTasks.add(it) - } ?: logger.debug("No content for ping '$ping.name', therefore no ping queued.") - } - - // If any ping is being serialized to disk, wait for the to finish before spinning up - // the WorkManager upload job. - if (pingSerializationTasks.any()) { - Dispatchers.API.launch { - // Await the serialization tasks. Once the serialization tasks have all completed, - // we can then safely enqueue the PingUploadWorker. - pingSerializationTasks.joinAll() - PingUploadWorker.enqueueWorker(applicationContext!!) - } - } - } - - /** - * Send a list of pings by name. - * - * Each ping will be looked up in the known instances of [PingType]. If the - * ping isn't known, an error is logged and the ping isn't queued for uploading. - * - * The ping content is assembled synchronously, but serialization to disk - * happens in a deferred coroutine, and upload happens later as that depends - * on the upload policies. - * - * If the ping currently contains no content, it will not be sent. - * - * @param pingNames List of ping names to send. - * @return The async Job performing the work of assembling the ping - */ - internal fun sendPingsByName(pingNames: List) { - val pings = pingNames.mapNotNull { pingName -> - PingType.pingRegistry.get(pingName)?.let { - it - } ?: run { - logger.error("Attempted to send unknown ping '$pingName'") - null - } - } - return sendPings(pings) - } - - /** - * Collect and assemble the ping and serialize the ping to be read when uploaded, but only if - * Glean is initialized, upload is enabled, and there is ping data to send. - * - * @param ping This is the object describing the ping - */ - internal fun assembleAndSerializePing(ping: PingType): Job? { - // Since the pingMaker.collect() function returns null if there is nothing to send we can - // use this to avoid sending an empty ping - return pingMaker.collect(ping)?.let { pingContent -> - // Store the serialized ping to file for PingUploadWorker to read and upload when the - // schedule is triggered - val pingId = UUID.randomUUID() - pingStorageEngine.store(pingId, makePath(ping.name, pingId), pingContent) - } - } - - /** - * TEST ONLY FUNCTION. - * This is called by the GleanTestRule, to enable test mode. - * - * This makes all asynchronous work synchronous so we can test the results of the - * API synchronously. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun enableTestingMode() { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.setTestingMode(enabled = true) - } - - /** - * TEST ONLY FUNCTION. - * Resets the Glean state and trigger init again. - * - * @param context the application context to init Glean with - * @param config the [Configuration] to init Glean with - * @param clearStores if true, clear the contents of all stores - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun resetGlean( - context: Context, - config: Configuration, - clearStores: Boolean - ) { - Glean.enableTestingMode() - - if (clearStores) { - // Clear all the stored data. - val storageManager = StorageEngineManager(applicationContext = context) - storageManager.clearAllStores() - // The experiments storage engine needs to be cleared manually as it's not listed - // in the `StorageEngineManager`. - ExperimentsStorageEngine.clearAllStores() - } - - // Init Glean. - Glean.initialized = false - Glean.setUploadEnabled(true) - Glean.initialize(context, config) - } - - /** - * TEST ONLY FUNCTION. - * Sets the server endpoint to a local address for ingesting test pings. - * - * The endpoint will be set as "http://localhost:". - * - * @param port the local address to send pings to - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun testSetLocalEndpoint(port: Int) { - Glean.enableTestingMode() - - // We can't set the configuration unless we're initialized. - assert(isInitialized()) - - val endpointUrl = "http://localhost:$port" - - Glean.configuration = configuration.copy(serverEndpoint = endpointUrl) - } -} - -@SuppressLint("StaticFieldLeak") -object Glean : GleanInternalAPI() { - internal const val SCHEMA_VERSION = 1 - - /** - * The name of the directory, inside the application's directory, - * in which Glean files are stored. - */ - internal const val GLEAN_DATA_DIR = "glean_data" } diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt index 55054efbb6b..6dcb9824d01 100644 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt @@ -5,9 +5,9 @@ package mozilla.components.service.glean.config import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient -import mozilla.components.service.glean.BuildConfig import mozilla.components.service.glean.net.ConceptFetchHttpUploader -import mozilla.components.service.glean.net.PingUploader +import mozilla.telemetry.glean.net.PingUploader +import mozilla.telemetry.glean.config.Configuration as GleanCoreConfiguration /** * The Configuration class describes how to configure the Glean. @@ -16,51 +16,22 @@ import mozilla.components.service.glean.net.PingUploader * is only meant to be changed for tests. * @property channel the release channel the application is on, if known. This will be * sent along with all the pings, in the `client_info` section. - * @property userAgent the user agent used when sending pings, only to be used internally. * @property maxEvents the number of events to store before the events ping is sent - * @property logPings whether to log ping contents to the console. This is only meant to be used - * internally by the `GleanDebugActivity`. * @property httpClient The HTTP client implementation to use for uploading pings. - * @property pingTag String tag to be applied to headers when uploading pings for debug view. - * This is only meant to be used internally by the `GleanDebugActivity`. */ -data class Configuration internal constructor( - val serverEndpoint: String, +data class Configuration( + val serverEndpoint: String = GleanCoreConfiguration.DEFAULT_TELEMETRY_ENDPOINT, val channel: String? = null, - val userAgent: String = DEFAULT_USER_AGENT, - val maxEvents: Int = DEFAULT_MAX_EVENTS, - val logPings: Boolean = DEFAULT_LOG_PINGS, - // NOTE: since only simple object or strings can be made `const val`s, if the - // default values for the lines below are ever changed, they are required - // to change in the public constructor below. - val httpClient: PingUploader = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }), - val pingTag: String? = null + val maxEvents: Int? = null, + val httpClient: PingUploader = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }) ) { - // This is the only public constructor this class should have. It should only - // expose things we want to allow external applications to change. Every test - // only or internal configuration option should be added to the above primary internal - // constructor and only initialized with a proper default when calling the primary - // constructor from the secondary, public one, below. - @JvmOverloads - constructor( - serverEndpoint: String = DEFAULT_TELEMETRY_ENDPOINT, - channel: String? = null, - maxEvents: Int = DEFAULT_MAX_EVENTS, - httpClient: PingUploader = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }) - ) : this ( - serverEndpoint = serverEndpoint, - channel = channel, - userAgent = DEFAULT_USER_AGENT, - maxEvents = maxEvents, - logPings = DEFAULT_LOG_PINGS, - httpClient = httpClient, - pingTag = null - ) - - companion object { - const val DEFAULT_TELEMETRY_ENDPOINT = "https://incoming.telemetry.mozilla.org" - const val DEFAULT_USER_AGENT = "Glean/${BuildConfig.LIBRARY_VERSION} (Android)" - const val DEFAULT_MAX_EVENTS = 500 - const val DEFAULT_LOG_PINGS = false + /** + * Convert the Android Components configuration object to the Glean SDK + * configuration object. + * + * @return a [mozilla.telemetry.glean.config.Configuration] instance. + */ + fun toWrappedConfiguration(): GleanCoreConfiguration { + return GleanCoreConfiguration(serverEndpoint, channel, maxEvents, httpClient) } } diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/debug/GleanDebugActivity.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/debug/GleanDebugActivity.kt deleted file mode 100644 index 19a4f1d190e..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/debug/GleanDebugActivity.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.debug - -import android.app.Activity -import android.os.Bundle -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.support.base.log.logger.Logger - -/** - * Debugging activity exported by Glean to allow easier debugging. - * For example, invoking debug mode in the Glean sample application - * can be done via adb using the following command: - * - * adb shell am start -n org.mozilla.samples.glean/mozilla.components.service.glean.debug.GleanDebugActivity - * - * See the adb developer docs for more info: - * https://developer.android.com/studio/command-line/adb#am - */ -class GleanDebugActivity : Activity() { - private val logger = Logger("glean/GleanDebugActivity") - - companion object { - // This is a list of the currently accepted commands - const val SEND_PING_EXTRA_KEY = "sendPing" - const val LOG_PINGS_EXTRA_KEY = "logPings" - const val TAG_DEBUG_VIEW_EXTRA_KEY = "tagPings" - - // Regular expression filter for debugId - val pingTagPattern = "[a-zA-Z0-9-]{1,20}".toRegex() - } - - // IMPORTANT: These activities are unsecured, and may be triggered by - // any other application on the device, including in release builds. - // Therefore, care should be taken in selecting what features are - // exposed this way. For example, it would be dangerous to change the - // submission URL. - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (!Glean.isInitialized()) { - logger.error( - "Glean is not initialized. " + - "It may be disabled by the application." - ) - finish() - return - } - - if (intent.extras == null) { - logger.error("No debugging option was provided, doing nothing.") - finish() - return - } - - // Make sure that at least one of the supported commands was used. - val supportedCommands = - listOf(SEND_PING_EXTRA_KEY, LOG_PINGS_EXTRA_KEY, TAG_DEBUG_VIEW_EXTRA_KEY) - - // Enable debugging options and start the application. - intent.extras?.let { - it.keySet().forEach { cmd -> - if (!supportedCommands.contains(cmd)) { - logger.error("Unknown command '$cmd'.") - } - } - - // Check for ping debug view tag to apply to the X-Debug-ID header when uploading the - // ping to the endpoint - var pingTag: String? = intent.getStringExtra(TAG_DEBUG_VIEW_EXTRA_KEY) - - // Validate the ping tag against the regex pattern - pingTag?.let { - if (!pingTagPattern.matches(it)) { - logger.error("tagPings value $it does not match accepted pattern $pingTagPattern") - pingTag = null - } - } - - val debugConfig = Glean.configuration.copy( - logPings = intent.getBooleanExtra(LOG_PINGS_EXTRA_KEY, Glean.configuration.logPings), - pingTag = pingTag - ) - - // Finally set the default configuration before starting - // the real product's activity. - logger.info("Setting debug config $debugConfig") - Glean.configuration = debugConfig - - intent.getStringExtra(SEND_PING_EXTRA_KEY)?.let { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - Glean.sendPingsByName(listOf(it)) - } - } - } - - val intent = packageManager.getLaunchIntentForPackage(packageName) - startActivity(intent) - - finish() - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/error/ErrorRecording.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/error/ErrorRecording.kt deleted file mode 100644 index 2c1a3104fb1..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/error/ErrorRecording.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@file:Suppress("MatchingDeclarationName") - -package mozilla.components.service.glean.error - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.CounterMetricType -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.storages.CountersStorageEngine -import mozilla.components.support.base.log.logger.Logger - -// The labeled counter metrics that store the errors are defined in the -// `metrics.yaml` for documentation purposes, but are not actually used -// directly, since the `sendInPings` value needs to match the pings of the -// metric that is erroring (plus the "metrics" ping), not some constant value -// that we could define in `metrics.yaml`. - -object ErrorRecording { - internal enum class ErrorType { - InvalidValue, - InvalidLabel - } - - @Suppress("TopLevelPropertyNaming") - internal val GLEAN_ERROR_NAMES = mapOf( - ErrorType.InvalidValue to "invalid_value", - ErrorType.InvalidLabel to "invalid_label" - ) - - /** - * Record an error that will be sent as a labeled counter in the `glean.error` category - * in pings. - * - * @param metricData The [CommonMetricData] instance associated with the error. - * @param errorType The [ErrorType] type of error. - * @param message The message to send to `logger.warn`. This message is not sent with the - * ping. It does not need to include the metric name, as that is automatically - * prepended to the message. - * @param logger The [Logger] instance to display the warning. - * @param numErrors The optional number of errors to report for this [ErrorType]. - */ - internal fun recordError( - metricData: CommonMetricData, - errorType: ErrorType, - message: String, - logger: Logger, - numErrors: Int? = null - ) { - val errorName = GLEAN_ERROR_NAMES[errorType]!! - - val identifier = metricData.identifier.split("/", limit = 2)[0] - - // Record errors in the pings the metric is in, as well as the metrics ping. - var sendInPings = metricData.sendInPings - if (!sendInPings.contains("metrics")) { - sendInPings = sendInPings + listOf("metrics") - } - - val errorMetric = CounterMetricType( - disabled = false, - category = "glean.error", - lifetime = Lifetime.Ping, - name = "$errorName/$identifier", - sendInPings = sendInPings - ) - - logger.warn("${metricData.identifier}: $message") - - // There are two reasons for using `CountersStorageEngine.record` below - // and not just using the public `CounterMetricType` API. - - // 1) The labeled counter metrics that store the errors are defined in the - // `metrics.yaml` for documentation purposes, but are not actually used - // directly, since the `sendInPings` value needs to match the pings of the - // metric that is erroring (plus the "metrics" ping), not some constant value - // that we could define in `metrics.yaml`. - - // 2) We want to bybass the restriction that there are only N values in a - // dynamically labeled metric. Error reporting should never report errors - // in the __other__ category. - CountersStorageEngine.record( - errorMetric, - amount = numErrors ?: 1 - ) - } - - /** - * Get the number of recorded errors for the given metric and error type. - * - * @param metricData The metric the errors were reported about - * @param errorType The type of error - * @param pingName The name of the ping (optional). If null, will use - * metricData.sendInPings.first() - * @return The number of errors reported - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun testGetNumRecordedErrors( - metricData: CommonMetricData, - errorType: ErrorType, - pingName: String? = null - ): Int { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - val usePingName = pingName?.let { - pingName - } ?: run { - metricData.sendInPings.first() - } - - val errorName = GLEAN_ERROR_NAMES[errorType]!! - - val errorMetric = CounterMetricType( - disabled = false, - category = "glean.error", - lifetime = Lifetime.Ping, - name = "$errorName/${metricData.identifier}", - sendInPings = metricData.sendInPings - ) - - if (!errorMetric.testHasValue(usePingName)) { - return 0 - } else { - return errorMetric.testGetValue(usePingName) - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/FunctionalHistogram.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/FunctionalHistogram.kt deleted file mode 100644 index 5e716e4a58f..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/FunctionalHistogram.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.histogram - -import mozilla.components.support.ktx.android.org.json.tryGetLong -import org.json.JSONObject -import java.lang.Math.pow -import kotlin.math.log - -/** - * This class represents a histogram where the bucketing is performed by a - * function, rather than pre-computed buckets. It is meant to help serialize - * and deserialize data to the correct format for transport and storage, as well - * as performing the calculations to determine the correct bucket for each sample. - * - * The bucket index of a given sample is determined with the following function: - * - * i = ⌊n log₂(𝑥)⌋ - * - * In other words, there are n buckets for each power of 2 magnitude. - * - * @param values a map containing the minimum bucket value mapped to the accumulated count - * @param sum the accumulated sum of all the samples in the histogram - */ -data class FunctionalHistogram( - val logBase: Double, - val bucketsPerMagnitude: Double, - // map from bucket limits to accumulated values - val values: MutableMap = mutableMapOf(), - var sum: Long = 0 -) { - private val exponent = pow(logBase, 1.0 / bucketsPerMagnitude) - - companion object { - /** - * Factory function that takes stringified JSON and converts it back into a - * [FunctionalHistogram]. - * - * @param json Stringified JSON value representing a [FunctionalHistogram] object - * @return A [FunctionalHistogram] or null if unable to rebuild from the string. - */ - @Suppress("ReturnCount", "ComplexMethod", "NestedBlockDepth") - internal fun fromJsonString(json: String): FunctionalHistogram? { - val jsonObject: JSONObject - try { - jsonObject = JSONObject(json) - } catch (e: org.json.JSONException) { - return null - } - - val logBase = try { - jsonObject.getDouble("log_base") - } catch (e: org.json.JSONException) { - return null - } - val bucketsPerMagnitude = try { - jsonObject.getDouble("buckets_per_magnitude") - } catch (e: org.json.JSONException) { - return null - } - - // Attempt to parse the values map, if it fails then something is wrong and we need to - // return null. - val values = try { - val mapData = jsonObject.getJSONObject("values") - val valueMap: MutableMap = mutableMapOf() - mapData.keys().forEach { key -> - mapData.tryGetLong(key)?.let { - valueMap[key.toLong()] = it - } - } - valueMap - } catch (e: org.json.JSONException) { - // This should only occur if there isn't a key/value pair stored for "values" - return null - } - val sum = jsonObject.tryGetLong("sum") ?: return null - - return FunctionalHistogram( - logBase = logBase, - bucketsPerMagnitude = bucketsPerMagnitude, - values = values, - sum = sum - ) - } - } - - /** - * Maps a sample to a "bucket index" that it belongs in. - * A "bucket index" is the consecutive integer index of each bucket, useful as a - * mathematical concept, even though the internal representation is stored and - * sent using the minimum value in each bucket. - * - * @param sample The data sample - * @return The bucket index the sample belongs in - */ - internal fun sampleToBucketIndex(sample: Long): Long { - return log(sample.toDouble() + 1, exponent).toLong() - } - - /** - * Determines the minimum value of a bucket, given a bucket index. - * - * @param bucketIndex The ordinal index of a bucket - * @return The minimum value of the bucket - */ - internal fun bucketIndexToBucketMinimum(bucketIndex: Long): Long { - return pow(exponent, bucketIndex.toDouble()).toLong() - } - - /** - * Maps a sample to the minimum value of the bucket it belongs in. - * - * @param sample The sample value - * @return the minimum value of the bucket the sample belongs in - */ - internal fun sampleToBucketMinimum(sample: Long): Long { - return if (sample == 0L) { - 0L - } else { - bucketIndexToBucketMinimum(sampleToBucketIndex(sample)) - } - } - - // This is a calculated read-only property that returns the total count of accumulated values - val count: Long - get() = values.map { it.value }.sum() - - /** - * Accumulates a sample to the correct bucket. - * If a value doesn't exist for this bucket yet, one is created. - * - * @param sample Long value representing the sample that is being accumulated - */ - internal fun accumulate(sample: Long) { - var bucketMinimum = sampleToBucketMinimum(sample) - values[bucketMinimum] = (values[bucketMinimum] ?: 0) + 1 - sum += sample - } - - /** - * Helper function to build the [FunctionalHistogram] into a JSONObject for serialization - * purposes. - * - * @return The histogram as [JSONObject] for persistence - */ - internal fun toJsonObject(): JSONObject { - return JSONObject(mapOf( - "log_base" to logBase, - "buckets_per_magnitude" to bucketsPerMagnitude, - "values" to values.mapKeys { "${it.key}" }, - "sum" to sum - )) - } - - /** - * Helper function to build the [FunctionalHistogram] into a JSONObject for sending in the - * ping payload. - * - * All buckets [min, max + 1] are included in the histogram, even if the have zero values. - * - * @return The histogram as [JSONObject] for a ping payload - */ - internal fun toJsonPayloadObject(): JSONObject { - val completeValues = if (values.size != 0) { - // A bucket range is defined by its own key, and the key of the next - // highest bucket. This explicitly adds any empty buckets (even if they have values - // of 0) between the lowest and highest bucket so that the backend knows the - // bucket ranges even without needing to know that function that was used to - // create the buckets. - val minBucket = sampleToBucketIndex(values.keys.min()!!) - val maxBucket = sampleToBucketIndex(values.keys.max()!!) + 1 - - var completeValues: MutableMap = mutableMapOf() - - for (i in minBucket..maxBucket) { - val bucketMinimum = bucketIndexToBucketMinimum(i) - val bucketSum = values.get(bucketMinimum)?.let { it } ?: 0 - completeValues[bucketMinimum.toString()] = bucketSum - } - - completeValues - } else { - values - } - - return JSONObject(mapOf( - "values" to completeValues, - "sum" to sum - )) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/PrecomputedHistogram.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/PrecomputedHistogram.kt deleted file mode 100644 index 3438d2b36d8..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/histogram/PrecomputedHistogram.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.histogram - -import mozilla.components.service.glean.private.HistogramType -import mozilla.components.support.ktx.android.org.json.tryGetInt -import mozilla.components.support.ktx.android.org.json.tryGetLong -import mozilla.components.support.ktx.android.org.json.tryGetString -import org.json.JSONArray -import org.json.JSONObject - -/** - * This class represents the structure of a custom distribution. It is meant - * to help serialize and deserialize data to the correct format for transport and - * storage, as well as including helper functions to calculate the bucket sizes. - * - * @param rangeMin the minimum value that can be represented - * @param rangeMax the maximum value that can be represented - * @param bucketCount total number of buckets - * @param histogramType the [HistogramType] representing the bucket layout - * @param values a map containing the bucket index mapped to the accumulated count - * @param sum the accumulated sum of all the samples in the custom distribution - */ -data class PrecomputedHistogram( - val rangeMin: Long, - val rangeMax: Long, - val bucketCount: Int, - val histogramType: HistogramType, - // map from bucket limits to accumulated values - val values: MutableMap = mutableMapOf(), - var sum: Long = 0 -) { - companion object { - /** - * Factory function that takes stringified JSON and converts it back into a - * [PrecomputedHistogram]. - * - * @param json Stringified JSON value representing a [PrecomputedHistogram] object - * @return A [PrecomputedHistogram] or null if unable to rebuild from the string. - */ - @Suppress("ReturnCount", "ComplexMethod") - internal fun fromJsonString(json: String): PrecomputedHistogram? { - val jsonObject: JSONObject - try { - jsonObject = JSONObject(json) - } catch (e: org.json.JSONException) { - return null - } - - val bucketCount = jsonObject.tryGetInt("bucket_count") ?: return null - // If 'range' isn't present, JSONException is thrown - val range = try { - val array = jsonObject.getJSONArray("range") - // Range must have exactly 2 values - if (array.length() == 2) { - // The getLong() function throws JSONException if we can't convert to a Long, so - // the catch should return null if either value isn't a valid Long - array.getLong(0) - array.getLong(1) - // This returns the JSONArray to the assignment if everything checks out - array - } else { - return null - } - } catch (e: org.json.JSONException) { - return null - } - val rawHistogramType = jsonObject.tryGetString("histogram_type") ?: return null - val histogramType = try { - HistogramType.valueOf(rawHistogramType.capitalize()) - } catch (e: IllegalArgumentException) { - return null - } - // Attempt to parse the values map, if it fails then something is wrong and we need to - // return null. - val values = try { - val mapData = jsonObject.getJSONObject("values") - val valueMap: MutableMap = mutableMapOf() - mapData.keys().forEach { key -> - valueMap[key.toLong()] = mapData.tryGetLong(key) ?: 0L - } - valueMap - } catch (e: org.json.JSONException) { - // This should only occur if there isn't a key/value pair stored for "values" - return null - } - val sum = jsonObject.tryGetLong("sum") ?: return null - - return PrecomputedHistogram( - bucketCount = bucketCount, - rangeMin = range.getLong(0), - rangeMax = range.getLong(1), - histogramType = histogramType, - values = values, - sum = sum - ) - } - } - - // This is a calculated read-only property that returns the total count of accumulated values - val count: Long - get() = values.map { it.value }.sum() - - // This is a list of limits for the buckets. Instantiated lazily to ensure that the range and - // bucket counts are set first. - internal val buckets: List by lazy { getBuckets() } - - /** - * Finds the correct bucket, using a binary search to locate the index of the - * bucket where the sample is bigger than or equal to the bucket limit. - * - * @param sample Long value representing the sample that is being accumulated - */ - internal fun findBucket(sample: Long): Long { - var under = 0 - var over = bucketCount - var mid: Int - - do { - mid = under + (over - under) / 2 - if (mid == under) { - break - } - - if (buckets[mid] <= sample) { - under = mid - } else { - over = mid - } - } while (true) - - return buckets[mid] - } - - /** - * Accumulates a sample to the correct bucket. - * If a value doesn't exist for this bucket yet, one is created. - * - * @param sample Long value representing the sample that is being accumulated - */ - internal fun accumulate(sample: Long) { - val limit = findBucket(sample) - values[limit] = (values[limit] ?: 0) + 1 - sum += sample - } - - /** - * Helper function to build the [PrecomputedHistogram] into a JSONObject for serialization - * purposes. - * - * @return The histogram as JSON for persistence - */ - internal fun toJsonObject(): JSONObject { - return JSONObject(mapOf( - "bucket_count" to bucketCount, - "range" to JSONArray(arrayOf(rangeMin, rangeMax)), - "histogram_type" to histogramType.toString().toLowerCase(), - "values" to values.mapKeys { "${it.key}" }, - "sum" to sum - )) - } - - /** - * Helper function to build the [PrecomputedHistogram] into a JSONObject for sending in the - * ping payload. Compared to [toJsonObject] which is designed for lossless roundtripping: - * - * - this does not include the bucketing parameters - * - all buckets [min, max + 1] are inserted into values - * - * @return The histogram as JSON to send in a ping payload - */ - internal fun toJsonPayloadObject(): JSONObject { - // Include all buckets [min, max + 1], where max is the maximum bucket with - // any value recorded. - val contiguousValues = if (!values.isEmpty()) { - val bucketMax = values.keys.max()!! - val contiguousValues = mutableMapOf() - for (bucketMin in buckets) { - contiguousValues["$bucketMin"] = values.getOrElse(bucketMin) { 0L } - if (bucketMin > bucketMax) { - break - } - } - contiguousValues - } else { - values - } - - return JSONObject(mapOf( - "values" to contiguousValues, - "sum" to sum - )) - } - - /** - * Helper function to generate the list of linear bucket min values used when accumulating - * to the correct buckets. - * - * @return List containing the bucket limits - */ - @Suppress("MagicNumber") - private fun getBucketsLinear(): List { - // Written to match the bucket generation on legacy desktop telemetry: - // https://searchfox.org/mozilla-central/rev/e0b0c38ee83f99d3cf868bad525ace4a395039f1/toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py#65 - - val result: MutableList = mutableListOf(0L) - - val dmin = rangeMin.toDouble() - val dmax = rangeMax.toDouble() - - for (i in (1 until bucketCount)) { - val linearRange = (dmin * (bucketCount - 1 - i) + dmax * (i - 1)) / (bucketCount - 2) - result.add((linearRange + 0.5).toLong()) - } - - return result - } - - /** - * Helper function to generate the list of exponential bucket min values used when accumulating - * to the correct buckets. - * - * @return List containing the bucket limits - */ - private fun getBucketsExponential(): List { - // Written to match the bucket generation on legacy desktop telemetry: - // https://searchfox.org/mozilla-central/rev/e0b0c38ee83f99d3cf868bad525ace4a395039f1/toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py#75 - - // This algorithm calculates the bucket sizes using a natural log approach to get - // `bucketCount` number of buckets, exponentially spaced between `range[MIN]` and - // `range[MAX]`. - // - // Bucket limits are the minimal bucket value. - // That means values in a bucket `i` are `range[i] <= value < range[i+1]`. - // It will always contain an underflow bucket (`< 1`). - val logMax = Math.log(rangeMax.toDouble()) - val result: MutableList = mutableListOf() - var current = rangeMin - if (current == 0L) { - current = 1L - } - - // underflow bucket - result.add(0) - result.add(current) - - for (i in 2 until bucketCount) { - val logCurrent = Math.log(current.toDouble()) - val logRatio = (logMax - logCurrent) / (bucketCount - i) - val logNext = logCurrent + logRatio - val nextValue = Math.round(Math.exp(logNext)) - if (nextValue > current) { - current = nextValue - } else { - ++current - } - result.add(current) - } - return result.sorted() - } - - /** - * Helper function to generate the list of bucket min values used when accumulating - * to the correct buckets. - * - * @return List containing the bucket limits - */ - private fun getBuckets(): List { - return when (histogramType) { - HistogramType.Linear -> getBucketsLinear() - HistogramType.Exponential -> getBucketsExponential() - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/net/BaseUploader.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/net/BaseUploader.kt deleted file mode 100644 index 456fa9fa7a1..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/net/BaseUploader.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.net - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.BuildConfig -import mozilla.components.service.glean.config.Configuration -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONException -import org.json.JSONObject -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import java.util.TimeZone - -/** - * The logic for uploading pings: this leaves the actual upload implementation - * to the user-provided delegate. - */ -class BaseUploader(d: PingUploader) : PingUploader by d { - private val logger = Logger("glean/BaseUploader") - - /** - * Log the contents of a ping to the console, if configured to do so in - * [Configuration.logPings]. - * - * @param path the URL path to append to the server address - * @param data the serialized text data to send - * @param config the Glean configuration object - */ - private fun logPing(path: String, data: String, config: Configuration) { - if (config.logPings) { - // Parse and reserialize the JSON so it has indentation and is human-readable. - try { - val json = JSONObject(data) - val indented = json.toString(2) - - logger.debug("Glean ping to URL: $path\n$indented") - } catch (e: JSONException) { - logger.debug("Exception parsing ping as JSON: $e") // $COVERAGE-IGNORE$ - } - } - } - - /** - * TEST-ONLY. Allows to set specific dates for testing. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun getCalendarInstance(): Calendar { return Calendar.getInstance() } - - /** - * Generate a date string to be used in the Date header. - */ - private fun createDateHeaderValue(): String { - val calendar = getCalendarInstance() - val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US) - dateFormat.timeZone = TimeZone.getTimeZone("GMT") - return dateFormat.format(calendar.time) - } - - /** - * Generate a list of headers to send with the request. - * - * @param config the Glean configuration object - * @return a [HeadersList] containing String to String [Pair] with the first - * entry being the header name and the second its value. - */ - private fun getHeadersToSend(config: Configuration): HeadersList { - val headers = mutableListOf( - Pair("Content-Type", "application/json; charset=utf-8"), - Pair("User-Agent", config.userAgent), - Pair("Date", createDateHeaderValue()), - // Add headers for supporting the legacy pipeline. - Pair("X-Client-Type", "Glean"), - Pair("X-Client-Version", BuildConfig.LIBRARY_VERSION) - ) - - // If there is a pingTag set, then this header needs to be added in order to flag pings - // for "debug view" use. - config.pingTag?.let { - headers.add(Pair("X-Debug-ID", it)) - } - - return headers - } - - /** - * This function triggers the actual upload: logs the ping and calls the implementation - * specific upload function. - * - * @param path the URL path to append to the server address - * @param data the serialized text data to send - * @param config the Glean configuration object - * - * @return true if the ping was correctly dealt with (sent successfully - * or faced an unrecoverable error), false if there was a recoverable - * error callers can deal with. - */ - internal fun doUpload(path: String, data: String, config: Configuration): Boolean { - logPing(path, data, config) - - return upload( - url = config.serverEndpoint + path, - data = data, - headers = getHeadersToSend(config) - ) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt index 619a7d06133..01e119e5d66 100644 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt @@ -13,6 +13,8 @@ import mozilla.components.concept.fetch.Request import mozilla.components.concept.fetch.isClientError import mozilla.components.concept.fetch.isSuccess import mozilla.components.support.base.log.logger.Logger +import mozilla.telemetry.glean.net.HeadersList +import mozilla.telemetry.glean.net.PingUploader import java.io.IOException import java.util.concurrent.TimeUnit diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/net/PingUploader.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/net/PingUploader.kt deleted file mode 100644 index 615f290f59e..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/net/PingUploader.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.net - -typealias HeadersList = List> - -/** - * The interface defining how to send pings. - */ -interface PingUploader { - /** - * Synchronously upload a ping to a server. - * - * @param url the URL path to upload the data to - * @param data the serialized text data to send - * @param headers a [HeadersList] containing String to String [Pair] with - * the first entry being the header name and the second its value. - * - * @return true if the ping was correctly dealt with (sent successfully - * or faced an unrecoverable error), false if there was a recoverable - * error callers can deal with. - */ - fun upload(url: String, data: String, headers: HeadersList): Boolean -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/ping/PingMaker.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/ping/PingMaker.kt deleted file mode 100644 index 53a97a03b5f..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/ping/PingMaker.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.ping - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.BuildConfig -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.storages.StorageEngineManager -import mozilla.components.service.glean.storages.ExperimentsStorageEngine -import mozilla.components.service.glean.utils.getISOTimeString -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.android.org.json.mergeWith -import org.json.JSONException -import org.json.JSONObject - -internal class PingMaker( - private val storageManager: StorageEngineManager, - private val applicationContext: Context -) { - private val logger = Logger("glean/PingMaker") - private val objectStartTime = getISOTimeString() - internal val sharedPreferences: SharedPreferences? by lazy { - applicationContext.getSharedPreferences( - this.javaClass.canonicalName, - Context.MODE_PRIVATE - ) - } - - /** - * Get the ping sequence number for a given ping. This is a - * monotonically-increasing value that is updated every time a particular ping - * type is sent. - * - * @param pingName The name of the ping - * @return sequence number - */ - internal fun getPingSeq(pingName: String): Int { - sharedPreferences?.let { - val key = "${pingName}_seq" - val currentValue = it.getInt(key, 0) - val editor = it.edit() - editor.putInt(key, currentValue + 1) - editor.apply() - return currentValue - } - - // This clause should happen in testing only, where a sharedPreferences object - // isn't guaranteed to exist if using a mocked ApplicationContext - logger.error("Couldn't get SharedPreferences object for ping sequence number") - return 0 - } - - /** - * Reset all ping sequence numbers and start times. - */ - internal fun resetPingMakerStorage() { - sharedPreferences?.let { - it.edit().clear().apply() - } - } - - /** - * Get the start time for a given ping. - * This is always equal to the end time of the last time the ping was sent. - * - * @param pingName The name of the ping - * @return start time - */ - internal fun getPingStartTime(pingName: String): String { - sharedPreferences?.let { - val key = "${pingName}_start_time" - val currentValue = it.getString(key, objectStartTime)!! - return currentValue - } - - // This clause should happen in testing only, where a sharedPreferences object - // isn't guaranteed to exist if using a mocked ApplicationContext - logger.error("Couldn't get SharedPreferences object for ping start times") - return objectStartTime - } - - /** - * Set the start time for a given ping. - * - * @param pingName The name of the ping - * @param startTime The start time to set for the ping - */ - internal fun setPingStartTime(pingName: String, startTime: String) { - sharedPreferences?.let { - val key = "${pingName}_start_time" - val editor = it.edit() - editor.putString(key, startTime) - editor.apply() - } - - // This clause should happen in testing only, where a sharedPreferences object - // isn't guaranteed to exist if using a mocked ApplicationContext - logger.error("Couldn't get SharedPreferences object for ping start times") - } - - /** - * Return the object containing the "ping_info" section of a ping. - * - * @param pingName the name of the ping to be sent - * @return a [JSONObject] containing the "ping_info" data - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun getPingInfo(pingName: String): JSONObject { - val pingInfo = JSONObject() - pingInfo.put("ping_type", pingName) - - // Experiments belong in ping_info, because they must appear in every ping - pingInfo.put("experiments", ExperimentsStorageEngine.getSnapshotAsJSON("", false)) - - pingInfo.put("seq", getPingSeq(pingName)) - - // This needs to be a bit more involved for start-end times. "start_time" is - // the time the ping was generated the last time. If not available, we use the - // date the object was initialized. - val startTime = getPingStartTime(pingName) - pingInfo.put("start_time", startTime) - val endTime = getISOTimeString() - pingInfo.put("end_time", endTime) - // Update the start time with the current time. - setPingStartTime(pingName, endTime) - return pingInfo - } - - /** - * Return the object containing the "client_info" section of a ping. - * - * @param includeClientId When `true`, include the "client_id" metric. - * @return a [JSONObject] containing the "client_info" data - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun getClientInfo(includeClientId: Boolean): JSONObject { - val clientInfo = JSONObject() - clientInfo.put("telemetry_sdk_build", BuildConfig.LIBRARY_VERSION) - clientInfo.mergeWith(getClientInfoMetrics(includeClientId)) - return clientInfo - } - - /** - * Collect the metrics stored in the "glean_client_info" bucket. - * - * @param includeClientId When `true`, include the "client_id" metric. - * @return a [JSONObject] containing the metrics belonging to the "client_info" - * section of the ping. - */ - private fun getClientInfoMetrics(includeClientId: Boolean): JSONObject { - val pingInfoData = storageManager.collect("glean_client_info") - - // The data returned by the manager is keyed by the storage engine name. - // For example, the client id will live in the "uuid" object, within - // `clientInfoData`. Remove the first level of indirection and return - // the flattened data to the caller. - val flattenedData = JSONObject() - try { - val metricsData = pingInfoData.getJSONObject("metrics") - for (key in metricsData.keys()) { - flattenedData.mergeWith(metricsData.getJSONObject(key)) - } - } catch (e: JSONException) { - logger.warn("Empty client info data.") - } - - if (!includeClientId) { - flattenedData.remove("client_id") - } - - return flattenedData - } - - /** - * Collects the relevant data and assembles the requested ping. - * - * @param ping The metadata describing the ping to collect. - * @return a string holding the data for the ping, or null if there is no data to send. - */ - fun collect(ping: PingType): String? { - logger.debug("Collecting ${ping.name}") - val jsonPing = storageManager.collect(ping.name) - - // Return null if there is nothing in the jsonPing object so that this can be used by - // consuming functions (i.e. sendPing()) to indicate no ping data is available to send. - if (jsonPing.length() == 0) { - return null - } - - jsonPing.put("ping_info", getPingInfo(ping.name)) - jsonPing.put("client_info", getClientInfo(ping.includeClientId)) - - return jsonPing.toString() - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/BooleanMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/BooleanMetricType.kt deleted file mode 100644 index ab142ec1d2e..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/BooleanMetricType.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.BooleansStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording boolean metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The boolean API only exposes the [set] method. - */ -data class BooleanMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/BooleanMetricType") - - /** - * Set a boolean value. - * - * @param value This is a user defined boolean value. - */ - fun set(value: Boolean) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the boolean to the storage engine. - BooleansStorageEngine.record( - this@BooleanMetricType, - value = value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return BooleansStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return BooleansStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CommonMetricData.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/CommonMetricData.kt deleted file mode 100644 index 6e3bd6fb5fe..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CommonMetricData.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import mozilla.components.service.glean.Glean -import mozilla.components.support.base.log.logger.Logger - -/** - * Enumeration of different metric lifetimes. - */ -enum class Lifetime { - /** - * The metric is reset with each sent ping - */ - Ping, - /** - * The metric is reset on application restart - */ - Application, - /** - * The metric is reset with each user profile - */ - User -} - -/** - * This defines the common set of data shared across all the different - * metric types. - */ -interface CommonMetricData { - val disabled: Boolean - val category: String - val lifetime: Lifetime - val name: String - val sendInPings: List - - val identifier: String get() = if (category.isEmpty()) { name } else { "$category.$name" } - - fun shouldRecord(logger: Logger): Boolean { - // Silently drop if metrics are turned off globally or locally - if (!Glean.getUploadEnabled() || disabled) { - return false - } - - return true - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CounterMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/CounterMetricType.kt deleted file mode 100644 index 1060e394f8e..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CounterMetricType.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.CountersStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording counter metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The counter API only exposes the [add] method, which takes care of validating the input - * data and making sure that limits are enforced. - */ -data class CounterMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/CounterMetricType") - - /** - * Add to counter value. - * - * @param amount This is the amount to increment the counter by, defaulting to 1 if called - * without parameters. - */ - @JvmOverloads - fun add(amount: Int = 1) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the new counter value to the storage engine. - CountersStorageEngine.record( - this@CounterMetricType, - amount = amount - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return CountersStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): Int { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return CountersStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CustomDistributionMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/CustomDistributionMetricType.kt deleted file mode 100644 index a4091201356..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/CustomDistributionMetricType.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.histogram.PrecomputedHistogram -import mozilla.components.service.glean.storages.CustomDistributionsStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording custom distribution metrics. - * - * Custom distributions are histograms with the following parameters that are settable on a - * per-metric basis: - * - * - `rangeMin`/`rangeMax`: The minimum and maximum values - * - `bucketCount`: The number of histogram buckets - * - `histogramType`: Whether the bucketing is linear or exponential - * - * This metric exists primarily for backward compatibility with histograms in - * legacy (pre-Glean) telemetry, and its use is not recommended for newly-created - * metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - */ -data class CustomDistributionMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val rangeMin: Long, - val rangeMax: Long, - val bucketCount: Int, - val histogramType: HistogramType -) : CommonMetricData, HistogramMetricBase { - - private val logger = Logger("glean/CustomDistributionMetricType") - - /** - * Accumulates the provided samples in the metric. - * - * The unit of the samples is entirely defined by the user. We encourage the author of the - * metric to provide a `unit` parameter in the `metrics.yaml` file, but that has no effect - * in the client and there is no unit conversion performed here. - * - * @param samples the [LongArray] holding the samples to be recorded by the metric. - */ - override fun accumulateSamples(samples: LongArray) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - CustomDistributionsStorageEngine.accumulateSamples( - metricData = this@CustomDistributionMetricType, - samples = samples, - rangeMin = rangeMin, - rangeMax = rangeMax, - bucketCount = bucketCount, - histogramType = histogramType - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return CustomDistributionsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun testGetValue(pingName: String = sendInPings.first()): PrecomputedHistogram { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return CustomDistributionsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/DatetimeMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/DatetimeMetricType.kt deleted file mode 100644 index 5466a1dab27..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/DatetimeMetricType.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.DatetimesStorageEngine -import mozilla.components.service.glean.utils.parseISOTimeString -import mozilla.components.support.base.log.logger.Logger -import java.util.Calendar -import java.util.Date - -/** - * This implements the developer facing API for recording datetime metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - */ -data class DatetimeMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val timeUnit: TimeUnit = TimeUnit.Minute -) : CommonMetricData { - - private val logger = Logger("glean/DatetimeMetricType") - - /** - * Set a datetime value, truncating it to the metric's resolution. - * - * @param value The [Date] value to set. If not provided, will record the current time. - */ - @JvmOverloads - fun set(value: Date = Date()) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the datetime to the storage engine. - DatetimesStorageEngine.set( - this@DatetimeMetricType, - value - ) - } - } - - /** - * Set a datetime value, truncating it to the metric's resolution. - * - * This is provided as an internal-only function so that we can test that timezones - * are passed through correctly. The normal public interface uses [Date] objects which - * are always in the local timezone. - * - * @param value The [Calendar] value to set. If not provided, will record the current time. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun set(value: Calendar) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the datetime to the storage engine. - DatetimesStorageEngine.set( - this@DatetimeMetricType, - value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return DatetimesStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the string representation of the stored value for testing purposes only. This - * function will attempt to await the last task (if any) writing to the the metric's storage - * engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValueAsString(pingName: String = sendInPings.first()): String { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return DatetimesStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * [Date] objects are always in the user's local timezone offset. If you - * care about checking that the timezone offset was set and sent correctly, use - * [testGetValueAsString] and inspect the offset. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): Date { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return parseISOTimeString(DatetimesStorageEngine.getSnapshot(pingName, false)!![identifier]!!)!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/EventMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/EventMetricType.kt deleted file mode 100644 index 2ca40c96b47..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/EventMetricType.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import android.os.SystemClock -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.EventsStorageEngine -import mozilla.components.service.glean.storages.RecordedEventData -import mozilla.components.support.base.log.logger.Logger - -/** - * An enum with no values for convenient use as the default set of extra keys - * that an [EventMetricType] can accept. - */ -@Suppress("EmptyClassBlock") -enum class NoExtraKeys(val value: Int) { - // deliberately empty -} - -/** - * This implements the developer facing API for recording events. - * - * Instances of this class type are automatically generated by the parsers at built time, - * allowing developers to record events that were previously registered in the metrics.yaml file. - * - * The Events API only exposes the [record] method, which takes care of validating the input - * data and making sure that limits are enforced. - */ -data class EventMetricType>( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val allowedExtraKeys: List = listOf() -) : CommonMetricData { - - private val logger = Logger("glean/EventMetricType") - - /** - * Record an event by using the information provided by the instance of this class. - * - * @param extra optional. This is map, both keys and values need to be strings, keys are - * identifiers. This is used for events where additional richer context is needed. - * The maximum length for values is defined by [MAX_LENGTH_EXTRA_KEY_VALUE] - */ - @JvmOverloads - fun record(extra: Map? = null) { - if (!shouldRecord(logger)) { - return - } - - // We capture the event time now, since we don't know when the async code below - // might get executed. - val monotonicElapsed = SystemClock.elapsedRealtime() - - val extraStrings = extra?.convertAllowedToStrings(allowedExtraKeys) - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the event to the storage engine. - EventsStorageEngine.record( - metricData = this@EventMetricType, - monotonicElapsedMs = monotonicElapsed, - extra = extraStrings - ) - } - } - - // Convert the extra key enums to strings before passing to the storage engine - // There are two extra "keys" in play here: - // 1. The Kotlin enumeration names, in CamelCase - // 2. The keys sent in the ping, in snake_case - // Here we need to get (2) to send in the ping. - private fun Map.convertAllowedToStrings(allowedKeys: List): Map? = - mapNotNull { (k, v) -> - val stringKey = allowedKeys.getOrNull(k.ordinal) - if (stringKey != null) { - stringKey to v - } else run { - logger.debug("No string value for enum ${k.ordinal}") - null - } - }.toMap() - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - val snapshot = EventsStorageEngine.getSnapshot(pingName, false) ?: return false - return snapshot.any { event -> - event.identifier == identifier - } - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): List { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return EventsStorageEngine.getSnapshot(pingName, false)!!.filter { event -> - event.identifier == identifier - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramMetricBase.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramMetricBase.kt deleted file mode 100644 index 49b9dd04f44..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramMetricBase.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -/** - * A common interface to be implemented by all the histogram-like metric types - * supported by the Glean SDK. - */ -interface HistogramMetricBase { - /** - * Accumulates the provided samples in the metric. - * - * @param samples the [LongArray] holding the samples to be recorded by the metric. - */ - fun accumulateSamples(samples: LongArray) -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramType.kt deleted file mode 100644 index 7387fa22ed6..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/HistogramType.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -/** - * Enumeration of the different kinds of histograms supported by metrics based on histograms. - */ -enum class HistogramType { - Linear, - Exponential -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/LabeledMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/LabeledMetricType.kt deleted file mode 100644 index 765a060f8c1..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/LabeledMetricType.kt +++ /dev/null @@ -1,237 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import mozilla.components.service.glean.storages.StorageEngine -import mozilla.components.service.glean.storages.StorageEngineManager -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for labeled metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * Unlike most metric types, LabeledMetricType does not have its own corresponding storage engine, - * but records metrics for the underlying metric type T in the storage engine for that type. The - * only difference is that labeled metrics are stored with the special key `$category.$name/$label`. - * The |StorageEngineManager.collect| method knows how to pull these special values back out of the - * individual storage engines and rearrange them correctly in the ping. - */ -data class LabeledMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val subMetric: T, - val labels: Set? = null -) : CommonMetricData { - - private val logger = Logger("glean/LabeledMetricType") - - companion object { - private const val MAX_LABELS = 16 - private const val OTHER_LABEL = "__other__" - - // This regex is used for matching against labels and should allow for dots, underscores, - // and/or hyphens. Labels are also limited to starting with either a letter or an - // underscore character. - private val labelRegex = Regex("^[a-z_][a-z0-9_-]{0,29}(\\.[a-z0-9_-]{0,29})*$") - // Some examples of good and bad labels: - // - // Good: - // this.is.fine - // this_is_fine_too - // this.is_still_fine - // thisisfine - // this.is_fine.2 - // _.is_fine - // this.is-fine - // this-is-fine - // Bad: - // this.is.not_fine_due_tu_the_length_being_too_long_i_thing.i.guess - // 1.not_fine - // this.$isnotfine - // -.not_fine - - private const val MAX_LABEL_LENGTH = 61 - } - - private val seenLabels: MutableSet = mutableSetOf() - - // TimespanMetricType holds a state now (the private `timerId`), which must be preserved - // when looking up the labels. Each time we request a label for a timespan, we cache the - // generated type in this map. - private val seenTimespans: MutableMap = mutableMapOf() - - /** - * Handles the label in the case where labels are predefined. - * - * If the given label is not in the predefined set of labels, returns [OTHER_LABEL], otherwise - * returns the label verbatim. - * - * @param label The label, as specified by the user - * @return adjusted label, possibly set to [OTHER_LABEL] - */ - private fun getFinalStaticLabel(label: String): String { - return if (labels!!.contains(label)) label else OTHER_LABEL - } - - /** - * Handles the label in the case where labels aren't predefined. - * - * If we've already seen more than [MAX_LABELS] unique labels, returns [OTHER_LABEL]. - * - * Also validates any unseen labels to make sure they are snake_case and under 30 characters. - * If not, returns [OTHER_LABEL]. - * - * @param label The label, as specified by the user - * @return adjusted label, possibly set to [OTHER_LABEL] - */ - @Suppress("ReturnCount") - private fun getFinalDynamicLabel(label: String): String { - if (lifetime != Lifetime.Application && seenLabels.size == 0) { - // TODO 1530733: This might cause I/O on the main thread if this is the - // first thing being stored to the given storage engine after app restart. - getStorageEngineForMetric()?.let { - val identifier = (subMetric as CommonMetricData).identifier - val prefix = "$identifier/" - seenLabels.addAll( - it.getIdentifiersInStores((subMetric as CommonMetricData).sendInPings) - .filter { it.startsWith(prefix) } - .map { it.substring(prefix.length) } - ) - } - } - - if (!seenLabels.contains(label)) { - if (seenLabels.size >= MAX_LABELS) { - return OTHER_LABEL - } else { - if (label.length > MAX_LABEL_LENGTH) { - recordError( - this, - ErrorType.InvalidLabel, - "label length ${label.length} exceeds maximum of $MAX_LABEL_LENGTH", - logger - ) - return OTHER_LABEL - } - - // Labels must be snake_case. - if (!labelRegex.matches(label)) { - recordError( - this, - ErrorType.InvalidLabel, - "label must be dotted snake_case, got '$label'", - logger - ) - return OTHER_LABEL - } - seenLabels.add(label) - } - } - return label - } - - /** - * Get a copy of the subMetric with the name changed to the given `newName`. - * - * @param newName The new name for the metric. - * @return A copy of subMetric with the new name. - * @throws IllegalStateException If this metric type does not support labels. - */ - @Suppress("UNCHECKED_CAST") - internal fun getMetricWithNewName(newName: String): T { - // function is "internal" so we can mock it in testing - - // Every metric that supports labels needs an entry here - return when (subMetric) { - is BooleanMetricType -> subMetric.copy(name = newName) as T - is CounterMetricType -> subMetric.copy(name = newName) as T - is DatetimeMetricType -> subMetric.copy(name = newName) as T - is StringListMetricType -> subMetric.copy(name = newName) as T - is StringMetricType -> subMetric.copy(name = newName) as T - is TimespanMetricType -> subMetric.copy(name = newName) as T - is UuidMetricType -> subMetric.copy(name = newName) as T - else -> throw IllegalStateException( - "Can not create a labeled version of this metric type" - ) - } - } - - /** - * Delegates to [StorageEngineManager.getStorageEngineForMetric]. - * Provided here so it can be mocked for testing. - */ - internal fun getStorageEngineForMetric(): StorageEngine? { - return StorageEngineManager.getStorageEngineForMetric(subMetric) - } - - /** - * Get the specific metric for a given label. - * - * If a set of acceptable labels were specified in the metrics.yaml file, - * and the given label is not in the set, it will be recorded under the - * special [OTHER_LABEL]. - * - * If a set of acceptable labels was not specified in the metrics.yaml file, - * only the first 16 unique labels will be used. After that, any additional - * labels will be recorded under the special [OTHER_LABEL] label. - * - * Labels must be snake_case and less than 30 characters. If an invalid label - * is used, the metric will be recorded in the special [OTHER_LABEL] label. - * - * @param label The label - * @return The specific metric for that label - */ - operator fun get(label: String): T { - val actualLabel = labels?.let { - getFinalStaticLabel(label) - } ?: run { - getFinalDynamicLabel(label) - } - - val newMetricName = "$name/$actualLabel" - if (subMetric is TimespanMetricType) { - // If this is a timespan, try to look it up from the cache. - // If it's there, return it. If it isn't, create a new TimespanMetricType - // and add it to the cache then return it. - // This needs to be synchronized as access to the map is not thread safe. - synchronized(this) { - @Suppress("UNCHECKED_CAST") - return seenTimespans.getOrPut(newMetricName) { - getMetricWithNewName(newMetricName) as TimespanMetricType - } as T - } - } - - return getMetricWithNewName(newMetricName) - } - - /** - * Get the specific metric for a given label index. - * - * This only works if a set of acceptable labels were specified in the - * metrics.yaml file. If static labels were not defined in that file or - * the index of the given label is not in the set, it will be recorded under - * the special [OTHER_LABEL]. - * - * @param labelIndex The label - * @return The specific metric for that label - */ - operator fun get(labelIndex: Int): T { - val actualLabel = if (labels != null && labelIndex < labels.size) { - labels.elementAt(labelIndex) - } else { - OTHER_LABEL - } - - return this[actualLabel] - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryDistributionMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryDistributionMetricType.kt deleted file mode 100644 index 30b80a2321a..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryDistributionMetricType.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.storages.MemoryDistributionsStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording memory distribution metrics. - * - * To prevent the number of buckets from being unbounded, values larger than 1 TB - * are truncated to 1 TB. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - */ -data class MemoryDistributionMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val memoryUnit: MemoryUnit -) : CommonMetricData, HistogramMetricBase { - - private val logger = Logger("glean/MemoryDistributionMetricType") - - /** - * Record a single value, in the unit specified by `memoryUnit`, to the distribution. - * - * @param sample the value - */ - fun accumulate(sample: Long) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the value to the storage engine. - MemoryDistributionsStorageEngine.accumulate( - metricData = this@MemoryDistributionMetricType, - sample = sample, - memoryUnit = memoryUnit - ) - } - } - - /** - * Accumulates the provided samples, in the unit specified by `memoryUnit`, - * to the distribution. - * - * This function is intended for GeckoView use only. - * - * @param samples the [LongArray] holding the samples to be recorded by the metric. - */ - override fun accumulateSamples(samples: LongArray) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - MemoryDistributionsStorageEngine.accumulateSamples( - metricData = this@MemoryDistributionMetricType, - samples = samples, - memoryUnit = memoryUnit - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return MemoryDistributionsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): FunctionalHistogram { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return MemoryDistributionsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryUnit.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryUnit.kt deleted file mode 100644 index 9e2bcea839e..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/MemoryUnit.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -/** - * Enumeration of different resolutions supported by the MemoryDistribution metric type. - * - * These use the power-of-2 values of these units, that is, Kilobyte is pedantically a Kibibyte. - */ -enum class MemoryUnit { - Byte, // 1 - Kilobyte, // 2^10 - Megabyte, // 2^20 - Gigabyte, // 2^30 -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt new file mode 100644 index 00000000000..5c04a39082b --- /dev/null +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.glean.private + +typealias Lifetime = mozilla.telemetry.glean.private.Lifetime +typealias NoExtraKeys = mozilla.telemetry.glean.private.NoExtraKeys + +typealias BooleanMetricType = mozilla.telemetry.glean.private.BooleanMetricType +typealias CounterMetricType = mozilla.telemetry.glean.private.CounterMetricType +typealias CustomDistributionMetricType = mozilla.telemetry.glean.private.CustomDistributionMetricType +typealias DatetimeMetricType = mozilla.telemetry.glean.private.DatetimeMetricType +typealias EventMetricType = mozilla.telemetry.glean.private.EventMetricType +typealias HistogramMetricBase = mozilla.telemetry.glean.private.HistogramBase +typealias HistogramType = mozilla.telemetry.glean.private.HistogramType +typealias LabeledMetricType = mozilla.telemetry.glean.private.LabeledMetricType +typealias MemoryDistributionMetricType = mozilla.telemetry.glean.private.MemoryDistributionMetricType +typealias MemoryUnit = mozilla.telemetry.glean.private.MemoryUnit +typealias PingType = mozilla.telemetry.glean.private.PingType +typealias QuantityMetricType = mozilla.telemetry.glean.private.QuantityMetricType +typealias RecordedExperimentData = mozilla.telemetry.glean.private.RecordedExperimentData +typealias StringListMetricType = mozilla.telemetry.glean.private.StringListMetricType +typealias StringMetricType = mozilla.telemetry.glean.private.StringMetricType +typealias TimespanMetricType = mozilla.telemetry.glean.private.TimespanMetricType +typealias TimeUnit = mozilla.telemetry.glean.private.TimeUnit +typealias TimingDistributionMetricType = mozilla.telemetry.glean.private.TimingDistributionMetricType +typealias UuidMetricType = mozilla.telemetry.glean.private.UuidMetricType diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/PingType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/PingType.kt deleted file mode 100644 index 0755d53b87b..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/PingType.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for custom pings. - * - * Instances of this class type are automatically generated by the parsers at build time. - * - * The Ping API only exposes the [send] method, which schedules a ping for sending. - */ -data class PingType( - val name: String, - val includeClientId: Boolean -) { - init { - if (pingRegistry.containsKey(name)) { - logger.error("Duplicate ping named $name") - } - pingRegistry[name] = this - } - - companion object { - private val logger = Logger("glean/PingType") - internal val pingRegistry: MutableMap = mutableMapOf() - } - - /** - * Send the ping. - * - * While the collection of metrics into pings happens synchronously, the - * ping queuing and ping uploading happens asyncronously. - * There are no guarantees that this will happen immediately. - * - * If the ping currently contains no content, it will not be queued. - */ - fun send() { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - Glean.sendPings(listOf(this@PingType)) - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/QuantityMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/QuantityMetricType.kt deleted file mode 100644 index 41e822982b2..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/QuantityMetricType.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.QuantitiesStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording quantity metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The quantity API only exposes the [set] method. - */ -data class QuantityMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/QuantityMetricType") - - /** - * Set a quantity value. - * - * @param value The value to set. Must be non-negative. - */ - fun set(value: Long) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the new quantity value to the storage engine. - QuantitiesStorageEngine.record( - this@QuantityMetricType, - value = value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return QuantitiesStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun testGetValue(pingName: String = sendInPings.first()): Long { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return QuantitiesStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringListMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringListMetricType.kt deleted file mode 100644 index ae8f0e5ad7b..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringListMetricType.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.StringListsStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording string list metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The string list API exposes the [add] and [set] methods, which take care of validating the input - * data and making sure that limits are enforced. - */ -data class StringListMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/StringListMetricType") - - /** - * Appends a string value to one or more string list metric stores. If the string exceeds the - * maximum string length or if the list exceeds the maximum length it will be truncated. - * - * @param value This is a user defined string value. The maximum length of - * this string is [MAX_STRING_LENGTH]. - */ - fun add(value: String) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the string to the storage engine. - StringListsStorageEngine.add( - metricData = this@StringListMetricType, - value = value - ) - } - } - - /** - * Sets a string list to one or more metric stores. If any string exceeds the maximum string - * length or if the list exceeds the maximum length it will be truncated. - * - * @param value This is a user defined string list. - */ - fun set(value: List) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the string list to the storage engine. - StringListsStorageEngine.set( - metricData = this@StringListMetricType, - value = value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return StringListsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): List { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return StringListsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringMetricType.kt deleted file mode 100644 index da3dd8fbb73..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/StringMetricType.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.storages.StringsStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording string metrics. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The string API only exposes the [set] method, which takes care of validating the input - * data and making sure that limits are enforced. - */ -data class StringMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/StringMetricType") - - /** - * Set a string value. - * - * @param value This is a user defined string value. If the length of the string exceeds - * the maximum length, it will be truncated. - */ - fun set(value: String) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the string to the storage engine. - StringsStorageEngine.record( - this@StringMetricType, - value = value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return StringsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): String { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return StringsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimeUnit.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimeUnit.kt deleted file mode 100644 index fa4f6131ab5..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimeUnit.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -/** - * Enumeration of different resolutions supported by - * the Timespan and TimingDistribution metric types. - */ -enum class TimeUnit { - Nanosecond, - Microsecond, - Millisecond, - Second, - Minute, - Hour, - Day -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimespanMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimespanMetricType.kt deleted file mode 100644 index 6f949b8bfb2..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimespanMetricType.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.storages.TimespansStorageEngine -import mozilla.components.service.glean.timing.GleanTimerId -import mozilla.components.service.glean.timing.TimingManager -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording timespans. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The timespan API exposes the [start], [stop] and [cancel] methods. - */ -data class TimespanMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val timeUnit: TimeUnit -) : CommonMetricData { - - private val logger = Logger("glean/TimespanMetricType") - - // The identifier for the timer managed by this metric. - private var timerId: GleanTimerId? = null - - /** - * Start tracking time for the provided metric. - * This records an error if it’s already tracking time (i.e. start was already - * called with no corresponding [stop]): in that case the original - * start time will be preserved. - */ - fun start() { - if (!shouldRecord(logger)) { - return - } - - if (timerId != null) { - ErrorRecording.recordError( - this, - ErrorRecording.ErrorType.InvalidValue, - "Timespan already started", - logger - ) - return - } - - timerId = TimingManager.start(this) - } - - /** - * Stop tracking time for the provided metric. - * Sets the metric to the elapsed time.This will record - * an error if no [start] was called. - */ - fun stop() { - if (!shouldRecord(logger)) { - return - } - - if (timerId == null) { - ErrorRecording.recordError( - this, - ErrorRecording.ErrorType.InvalidValue, - "Timespan not running", - logger - ) - return - } - - timerId?.let { id -> - TimingManager.stop(this, id)?.let { elapsedNanos -> - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - TimespansStorageEngine.set(this@TimespanMetricType, timeUnit, elapsedNanos) - } - } - - // Reset the timerId. - timerId = null - } - } - - /** - * Abort a previous [start] call. No error is recorded if no [start] was called. - */ - fun cancel() { - if (!shouldRecord(logger)) { - return - } - - timerId?.let { - TimingManager.cancel(this, it) - } - - // Reset the timerId. - timerId = null - } - - /** - * Explicitly set the timespan value, in nanoseconds. - * - * This API should only be used if your library or application requires recording - * times in a way that can not make use of [start]/[stop]/[cancel]. - * - * [setRawNanos] does not overwrite a running timer or an already existing value. - * - * @param elapsedNanos The elapsed time to record, in nanoseconds. - */ - fun setRawNanos(elapsedNanos: Long) { - if (!shouldRecord(logger)) { - return - } - - if (timerId != null) { - ErrorRecording.recordError( - this, - ErrorRecording.ErrorType.InvalidValue, - "Timespan already running. Raw value not recorded.", - logger - ) - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - TimespansStorageEngine.set( - this@TimespanMetricType, - timeUnit, - elapsedNanos - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - return TimespansStorageEngine.getSnapshotWithTimeUnit(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only, in the metric's time unit. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): Long { - return TimespansStorageEngine.getSnapshotWithTimeUnit(pingName, false)!![identifier]!!.second - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimingDistributionMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimingDistributionMetricType.kt deleted file mode 100644 index c340c5c089a..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/TimingDistributionMetricType.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.storages.TimingDistributionsStorageEngine -import mozilla.components.service.glean.timing.GleanTimerId -import mozilla.components.service.glean.timing.TimingManager -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording timing distribution metrics. - * - * The `timeUnit` parameter is only used when the values are set directly - * through `accumulateSamples`, which is used for bringing in GeckoView metrics, - * and not for normal use. - * - * To prevent the number of buckets from being unbounded, timings longer than 10 minutes - * are truncated to 10 minutes. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - */ -data class TimingDistributionMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List, - val timeUnit: TimeUnit -) : CommonMetricData, HistogramMetricBase { - - private val logger = Logger("glean/TimingDistributionMetricType") - - /** - * Start tracking time for the provided metric and [GleanTimerId]. This - * records an error if it’s already tracking time (i.e. start was already - * called with no corresponding [stopAndAccumulate]): in that case the original - * start time will be preserved. - */ - fun start(): GleanTimerId? { - if (!shouldRecord(logger)) { - return null - } - - return TimingManager.start(this) - } - - /** - * Stop tracking time for the provided metric and associated timer id. Add a - * count to the corresponding bucket in the timing distribution. - * This will record an error if no [start] was called. - * - * @param timerId The [GleanTimerId] to associate with this timing. This allows - * for concurrent timing of events associated with different ids to the - * same timespan metric. - */ - fun stopAndAccumulate(timerId: GleanTimerId?) { - if (!shouldRecord(logger) || timerId == null) { - return - } - - TimingManager.stop(this, timerId)?.let { elapsedNanos -> - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the string to the storage engine. - TimingDistributionsStorageEngine.accumulate( - metricData = this@TimingDistributionMetricType, - sample = elapsedNanos - ) - } - } - } - - /** - * Abort a previous [start] call. No error is recorded if no [start] was called. - * - * @param timerId The [GleanTimerId] to associate with this timing. This allows - * for concurrent timing of events associated with different ids to the - * same timing distribution metric. - */ - fun cancel(timerId: GleanTimerId?) { - if (!shouldRecord(logger) || timerId == null) { - return - } - - TimingManager.cancel(this, timerId) - } - - /** - * Accumulates the provided samples in the metric. - * - * Please note that this assumes that the provided samples are already in the - * "unit" declared by the instance of the implementing metric type (e.g. if the - * implementing class is a [TimingDistributionMetricType] and the instance this - * method was called on is using [TimeUnit.Second], then `samples` are assumed - * to be in that unit). - * - * @param samples the [LongArray] holding the samples to be recorded by the metric. - */ - override fun accumulateSamples(samples: LongArray) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - TimingDistributionsStorageEngine.accumulateSamples( - metricData = this@TimingDistributionMetricType, - samples = samples, - timeUnit = timeUnit - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return TimingDistributionsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): FunctionalHistogram { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return TimingDistributionsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/private/UuidMetricType.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/private/UuidMetricType.kt deleted file mode 100644 index 1abb02495a6..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/private/UuidMetricType.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Dispatchers -import java.util.UUID - -import mozilla.components.service.glean.storages.UuidsStorageEngine -import mozilla.components.support.base.log.logger.Logger - -/** - * This implements the developer facing API for recording uuids. - * - * Instances of this class type are automatically generated by the parsers at build time, - * allowing developers to record values that were previously registered in the metrics.yaml file. - * - * The uuid API exposes the [generateAndSet] and [set] methods. - */ -data class UuidMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List -) : CommonMetricData { - - private val logger = Logger("glean/UuidMetricType") - - /** - * Generate a new UUID value and set it in the metric store. - * - * @return a [UUID] or [null] if we're not allowed to record. - */ - fun generateAndSet(): UUID? { - // Even if `set` is already checking if we're allowed to record, - // we need to check here as well otherwise we'd return a `UUID` - // that won't be stored anywhere. - if (!shouldRecord(logger)) { - return null - } - - val uuid = UUID.randomUUID() - set(uuid) - return uuid - } - - /** - * Explicitly set an existing UUID value - * - * @param value a valid [UUID] to set the metric to - */ - fun set(value: UUID) { - if (!shouldRecord(logger)) { - return - } - - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // Delegate storing the event to the storage engine. - UuidsStorageEngine.record( - this@UuidMetricType, - value = value - ) - } - } - - /** - * Tests whether a value is stored for the metric for testing purposes only. This function will - * attempt to await the last task (if any) writing to the the metric's storage engine before - * returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return true if metric value exists, otherwise false - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testHasValue(pingName: String = sendInPings.first()): Boolean { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return UuidsStorageEngine.getSnapshot(pingName, false)?.get(identifier) != null - } - - /** - * Returns the stored value for testing purposes only. This function will attempt to await the - * last task (if any) writing to the the metric's storage engine before returning a value. - * - * @param pingName represents the name of the ping to retrieve the metric for. Defaults - * to the either the first value in [defaultStorageDestinations] or the first - * value in [sendInPings] - * @return value of the stored metric - * @throws [NullPointerException] if no value is stored - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @JvmOverloads - fun testGetValue(pingName: String = sendInPings.first()): UUID { - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.assertInTestingMode() - - return UuidsStorageEngine.getSnapshot(pingName, false)!![identifier]!! - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/GleanLifecycleObserver.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/GleanLifecycleObserver.kt deleted file mode 100644 index df23a481fda..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/GleanLifecycleObserver.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.scheduler - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.GleanMetrics.GleanBaseline - -/** - * Connects process lifecycle events from Android to Glean's handleEvent - * functionality (where the actual work of sending pings is done). - */ -internal class GleanLifecycleObserver : LifecycleObserver { - /** - * Calls the "background" event when entering the background. - */ - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun onEnterBackground() { - // We're going to background, so store how much time we spent - // on foreground. - GleanBaseline.duration.stop() - Glean.handleBackgroundEvent() - } - - /** - * Updates the baseline.duration metric when entering the foreground. - * We use ON_START here because we don't want to incorrectly count metrics in ON_RESUME as - * pause/resume can happen when interacting with things like the navigation shade which could - * lead to incorrectly recording the start of a duration, etc. - * - * https://developer.android.com/reference/android/app/Activity.html#onStart() - */ - @OnLifecycleEvent(Lifecycle.Event.ON_START) - fun onEnterForeground() { - // Note that this is sending the length of the last foreground session - // because it belongs to the baseline ping and that ping is sent every - // time the app goes to background. - GleanBaseline.duration.start() - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/MetricsPingScheduler.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/MetricsPingScheduler.kt deleted file mode 100644 index 65e74da34ea..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/MetricsPingScheduler.kt +++ /dev/null @@ -1,361 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.scheduler - -import android.content.Context -import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting -import android.text.format.DateUtils -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.ProcessLifecycleOwner -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.Worker -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.GleanMetrics.Pings -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.service.glean.utils.getISOTimeString -import mozilla.components.service.glean.utils.parseISOTimeString -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.support.utils.ThreadUtils -import java.util.Calendar -import java.util.Date -import java.util.concurrent.TimeUnit as AndroidTimeUnit - -/** - * MetricsPingScheduler facilitates scheduling the periodic assembling of metrics pings, - * at a given time, trying its best to handle the following cases: - * - * - ping is overdue (due time already passed) for the current calendar day; - * - ping is soon to be sent in the current calendar day; - * - ping was already sent, and must be scheduled for the next calendar day. - * - * The scheduler also makes use of the [LifecycleObserver] in order to correctly schedule - * the [MetricsPingWorker] - */ -@Suppress("TooManyFunctions") -internal class MetricsPingScheduler(val applicationContext: Context) : LifecycleObserver { - private val logger = Logger("glean/MetricsPingScheduler") - internal val sharedPreferences: SharedPreferences by lazy { - applicationContext.getSharedPreferences(this.javaClass.canonicalName, Context.MODE_PRIVATE) - } - - companion object { - const val LAST_METRICS_PING_SENT_DATETIME = "last_metrics_ping_iso_datetime" - const val DUE_HOUR_OF_THE_DAY = 4 - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var isInForeground = false - - /** - * Function to cancel any pending metrics ping workers - */ - internal fun cancel(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(MetricsPingWorker.TAG) - } - } - - init { - // This should only be called from the main thread. - // We can't enforce this at build time here, since the @MainThread - // decorator can not be applied to a contructor. However, in practice - // this is only called from Glean.initialize which does have that - // decorator. For good measure, we also perform this run time check - // here. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1581556 - ThreadUtils.assertOnUiThread() - ProcessLifecycleOwner.get().lifecycle.addObserver(this) - } - - /** - * Schedules the metrics ping collection at the due time. - * - * @param now the current datetime, a [Calendar] instance. - * @param sendTheNextCalendarDay whether to schedule collection for the next calendar day - * or to attempt to schedule it for the current calendar day. If the latter and - * we're overdue for the expected collection time, the task is scheduled for immediate - * execution. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun schedulePingCollection(now: Calendar, sendTheNextCalendarDay: Boolean) { - // Compute how many milliseconds until the next time the metrics ping - // needs to collect data. - val millisUntilNextDueTime = getMillisecondsUntilDueTime(sendTheNextCalendarDay, now) - logger.debug("Scheduling the 'metrics' ping in ${millisUntilNextDueTime}ms") - - // Build a work request: we don't use use a `PeriodicWorkRequest`, which - // is more suitable for this task, because of the inherent drifting. See - // https://developer.android.com/reference/androidx/work/PeriodicWorkRequest.html - // for more details. - val workRequest = OneTimeWorkRequestBuilder() - .addTag(MetricsPingWorker.TAG) - .setInitialDelay(millisUntilNextDueTime, AndroidTimeUnit.MILLISECONDS) - .build() - - // Enqueue the work request: replace older requests if needed. This is to cover - // the odd case in which: - // - Glean is killed, but the work request is still there; - // - Glean restarts; - // - the ping is overdue and is immediately collected at startup; - // - a new work is scheduled for the next calendar day. - WorkManager.getInstance(applicationContext).enqueueUniqueWork( - MetricsPingWorker.TAG, - ExistingWorkPolicy.REPLACE, - workRequest) - } - - /** - * Computes the time in milliseconds until the next metrics ping due time. - * - * @param sendTheNextCalendarDay whether or not to return the delay for today or tomorrow's - * [dueHourOfTheDay] - * @param now the current datetime, a [Calendar] instance. - * @param dueHourOfTheDay the due hour of the day, in the [0, 23] range. - * @return the milliseconds until the due hour: if current time is before the due - * hour, then |dueHour - currentHour| is returned. If it's exactly on that hour, - * then 0 is returned. Same if we're past the due hour. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun getMillisecondsUntilDueTime( - sendTheNextCalendarDay: Boolean, - now: Calendar, - dueHourOfTheDay: Int = DUE_HOUR_OF_THE_DAY - ): Long { - val nowInMillis = now.timeInMillis - val dueTime = getDueTimeForToday(now, dueHourOfTheDay) - val delay = dueTime.timeInMillis - nowInMillis - return when { - sendTheNextCalendarDay -> { - // We're past the `dueHourOfTheDay` in the current calendar day. - dueTime.add(Calendar.DAY_OF_MONTH, 1) - dueTime.timeInMillis - nowInMillis - } - delay >= 0 -> { - // The `dueHourOfTheDay` is in the current calendar day. - // Return the computed delay. - delay - } - else -> { - // We're overdue and don't want to wait until tomorrow. - 0L - } - } - } - - /** - * Check if the provided time is after the ping due time. - * - * @param now a [Calendar] instance representing the current time. - * @param dueHourOfTheDay the due hour of the day, in the [0, 23] range. - * @return true if the current time is after the due hour, false otherwise. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun isAfterDueTime( - now: Calendar, - dueHourOfTheDay: Int = DUE_HOUR_OF_THE_DAY - ): Boolean { - val nowInMillis = now.timeInMillis - val dueTime = getDueTimeForToday(now, dueHourOfTheDay) - return (dueTime.timeInMillis - nowInMillis) < 0 - } - - /** - * Create a [Calendar] object representing the due time for the current - * calendar day. - * - * @param now a [Calendar] instance representing the current time. - * @param dueHourOfTheDay the due hour of the day, in the [0, 23] range. - * @return a new [Calendar] instance representing the due hour for the current calendar day. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun getDueTimeForToday(now: Calendar, dueHourOfTheDay: Int): Calendar { - val dueTime = now.clone() as Calendar - dueTime.set(Calendar.HOUR_OF_DAY, dueHourOfTheDay) - dueTime.set(Calendar.MINUTE, 0) - dueTime.set(Calendar.SECOND, 0) - dueTime.set(Calendar.MILLISECOND, 0) - return dueTime - } - - /** - * Performs startup checks to decide when to schedule the next metrics ping - * collection. - */ - fun schedule() { - val now = getCalendarInstance() - val lastSentDate = getLastCollectedDate() - - if (lastSentDate != null) { - logger.debug("The 'metrics' ping was last sent on $lastSentDate") - } - - // We expect to cover 3 cases here: - // - // (1) - the ping was already collected the current calendar day; only schedule - // one for collecting the next calendar day at the due time; - // (2) - the ping was NOT collected the current calendar day, and we're later than - // the due time; collect the ping immediately; - // (3) - the ping was NOT collected the current calendar day, but we still have - // some time to the due time; schedule for sending the current calendar day. - - val alreadySentToday = (lastSentDate != null) && DateUtils.isToday(lastSentDate.time) - when { - alreadySentToday -> { - // The metrics ping was already sent today. Schedule it for the next - // calendar day. This addresses (1). - logger.info("The 'metrics' ping was already sent today, ${now.time}.") - schedulePingCollection(now, sendTheNextCalendarDay = true) - } - // The ping wasn't already sent today. Are we overdue or just waiting for - // the right time? - isAfterDueTime(now) -> { - logger.info("The 'metrics' ping is scheduled for immediate collection, ${now.time}") - // The reason why we're collecting the "metrics" ping in the `Dispatchers.API` - // context is that we want to make sure no other metric API adds data before - // the ping is collected. All the exposed metrics API dispatch calls to the - // engines through the `Dispatchers.API` context, so this ensures we are enqueued - // before any other recording API call. - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.launch { - // This addresses (2). - collectPingAndReschedule(now) - } - } - else -> { - // This covers (3). - logger.info("The 'metrics' collection is scheduled for today, ${now.time}") - schedulePingCollection(now, sendTheNextCalendarDay = false) - } - } - } - - /** - * Triggers the collection of the "metrics" ping and schedules the - * next collection. - * - * @param now a [Calendar] instance representing the current time. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun collectPingAndReschedule(now: Calendar) { - logger.info("Collecting the 'metrics' ping, now = ${now.time}") - Pings.metrics.send() - // Update the collection date: we don't really care if we have data or not, let's - // always update the sent date. - updateSentDate(getISOTimeString(now, truncateTo = TimeUnit.Day)) - - // Reschedule the collection if we are in the foreground so that any metrics collected after - // this are sent in the next window. If we are in the background, then we may stay there - // until the app is killed so we shouldn't reschedule unless the app is foregrounded again - // (see GleanLifecycleObserver). - if (isInForeground) { - schedulePingCollection(now, sendTheNextCalendarDay = true) - } - } - - /** - * Get the date the metrics ping was last collected. - * - * @return a [Date] object representing the date the metrics ping was last collected, or - * null if no metrics ping was previously collected. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun getLastCollectedDate(): Date? { - val loadedDate = try { - sharedPreferences.getString(LAST_METRICS_PING_SENT_DATETIME, null) - } catch (e: ClassCastException) { - null - } - - if (loadedDate == null) { - logger.error("MetricsPingScheduler last stored ping time was not valid") - } - - return loadedDate?.let { parseISOTimeString(it) } - } - - /** - * Update the persisted date when the metrics ping is sent. - * - * This is called after sending a metrics ping to timestamp when the last ping was - * sent in order to maintain the proper interval between pings. - * - * @param date the datetime string to store - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun updateSentDate(date: String) { - sharedPreferences.edit()?.putString(LAST_METRICS_PING_SENT_DATETIME, date)?.apply() - } - - /** - * Utility function to mock date creation and ease tests. This is intended - * to be used only in tests, by overriding the return value with mockito. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun getCalendarInstance(): Calendar = Calendar.getInstance() - - /** - * Update flag to show we are no longer in the foreground. - */ - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun onEnterBackground() { - isInForeground = false - } - - /** - * Update the flag to indicate we are moving to the foreground, and if Glean is initialized we - * will check to see if the metrics ping needs scheduled for collection. - */ - @OnLifecycleEvent(Lifecycle.Event.ON_START) - fun onEnterForeground() { - isInForeground = true - - // We check for the metrics ping schedule here because the app could have been in the - // background and resumed in which case Glean would already be initialized but we still need - // to perform the check to determine whether or not to collect and schedule the metrics ping. - // If this is the first ON_START event since the app was launched, Glean wouldn't be - // initialized yet. - if (Glean.isInitialized()) { - schedule() - } - } -} - -/** - * The class representing the work to be done by the [WorkManager]. This is used by - * [MetricsPingScheduler.schedulePingCollection] for scheduling the collection of the - * "metrics" ping at the due hour. - */ -internal class MetricsPingWorker(context: Context, params: WorkerParameters) : Worker(context, params) { - private val logger = Logger("glean/MetricsPingWorker") - - companion object { - const val TAG = "mozac_service_glean_metrics_ping_tick" - } - - override fun doWork(): Result { - // This is getting the instance of the MetricsPingScheduler class instantiated by - // the [Glean] singleton. This is ugly. There are a few alternatives to this: - // - // 1. provide a custom WorkerFactory to the WorkManager; however this would require - // us to prevent the application from initializing the WorkManager at startup in - // order to manually init it ourselves and feed in our custom configuration with - // the new factory. - // 2. make most functions of MetricsPingScheduler static and allow for calling them - // from this worker; this makes testing much more complicated, due to the restrictions - // related to static functions when testing. - val metricsScheduler = Glean.metricsPingScheduler - // Perform the actual work. - val now = metricsScheduler.getCalendarInstance() - logger.debug("MetricsPingWorker doWork(), now = ${now.time}") - metricsScheduler.collectPingAndReschedule(now) - // We don't expect to fail at collection: we might fail at upload, but that's handled - // separately by the upload worker. - return Result.success() - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/PingUploadWorker.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/PingUploadWorker.kt deleted file mode 100644 index f6dfa81379c..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/scheduler/PingUploadWorker.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.scheduler - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.Worker -import androidx.work.WorkerParameters -import mozilla.components.service.glean.Glean - -/** - * This class is the worker class used by [WorkManager] to handle uploading the ping to the server. - * @suppress This is internal only, don't show it in the docs. - */ -class PingUploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) { - companion object { - internal const val PING_WORKER_TAG = "mozac_service_glean_ping_upload_worker" - - /** - * Build the constraints around which the worker can be run, such as whether network - * connectivity is required. - * - * @return [Constraints] object containing the required work constraints - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun buildConstraints(): Constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - /** - * Build the [OneTimeWorkRequest] for enqueueing in the [WorkManager]. This also adds a tag - * by which enqueued requests can be identified. - * - * @return [OneTimeWorkRequest] representing the task for the [WorkManager] to enqueue and run - */ - internal fun buildWorkRequest(): OneTimeWorkRequest = OneTimeWorkRequestBuilder() - .addTag(PING_WORKER_TAG) - .setConstraints(buildConstraints()) - .build() - - /** - * Function to aid in properly enqueuing the worker in [WorkManager] - */ - internal fun enqueueWorker(context: Context) { - WorkManager.getInstance(context).enqueueUniqueWork( - PING_WORKER_TAG, - ExistingWorkPolicy.KEEP, - buildWorkRequest()) - } - - /** - * Function to perform the actual ping upload task. This is created here in the - * companion object in order to facilitate testing. - * - * @return true if process was successful - */ - private fun uploadPings(): Boolean { - val httpPingUploader = Glean.httpClient - return Glean.pingStorageEngine.process(httpPingUploader::doUpload) - } - - /** - * Function to cancel any pending ping upload workers - */ - internal fun cancel(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(PING_WORKER_TAG) - } - } - - /** - * This method is called on a background thread - you are required to **synchronously** do your - * work and return the [androidx.work.ListenableWorker.Result] from this method. Once you - * return from this method, the Worker is considered to have finished what its doing and will be - * destroyed. - * - * A Worker is given a maximum of ten minutes to finish its execution and return a - * [androidx.work.ListenableWorker.Result]. After this time has expired, the Worker will - * be signalled to stop. - * - * @return The [androidx.work.ListenableWorker.Result] of the computation - */ - override fun doWork(): Result { - return when { - !Glean.getUploadEnabled() -> Result.failure() - !uploadPings() -> Result.retry() - else -> Result.success() - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/BooleansStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/BooleansStorageEngine.kt deleted file mode 100644 index c176f66b082..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/BooleansStorageEngine.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger - -/** - * This singleton handles the in-memory storage logic for booleans. It is meant to be used by - * the Specific Booleans API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object BooleansStorageEngine : BooleansStorageEngineImplementation() - -internal open class BooleansStorageEngineImplementation( - override val logger: Logger = Logger("glean/BooleansStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): Boolean? { - return value as? Boolean - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: Boolean, - extraSerializationData: Any? - ) { - userPreferences?.putBoolean(storeName, value) - } - - /** - * Record a boolean in the desired stores. - * - * @param metricData object with metric settings - * @param value the boolean value to record - */ - fun record( - metricData: CommonMetricData, - value: Boolean - ) { - super.recordMetric(metricData, value) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CountersStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CountersStorageEngine.kt deleted file mode 100644 index 6ecd7930029..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CountersStorageEngine.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger - -/** - * This singleton handles the in-memory storage logic for counters. It is meant to be used by - * the Specific Counters API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object CountersStorageEngine : CountersStorageEngineImplementation() - -internal open class CountersStorageEngineImplementation( - override val logger: Logger = Logger("glean/CountersStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): Int? { - return (value as? Int)?.let { - return@let if (it < 0) null else it - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: Int, - extraSerializationData: Any? - ) { - userPreferences?.putInt(storeName, value) - } - - /** - * Record a string in the desired stores. - * - * @param metricData object with metric settings - * @param amount the integer amount to add to the currently stored value. If there is - * no current value, then the amount will be stored as the current value. - */ - fun record( - metricData: CommonMetricData, - amount: Int - ) { - if (amount <= 0) { - recordError( - metricData, - ErrorType.InvalidValue, - "Added negative or zero value $amount", - logger - ) - return - } - - // Use a custom combiner to add the amount to the existing counters rather than overwriting - super.recordMetric(metricData, amount, null) { currentValue, newAmount -> - currentValue?.let { it + newAmount } ?: newAmount - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngine.kt deleted file mode 100644 index 31fb8177acd..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngine.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.PrecomputedHistogram -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.HistogramType - -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONObject - -/** - * This singleton handles the in-memory storage logic for custom distributions. It is meant to be - * used by the Custom Distribution API and the ping assembling objects. - */ -internal object CustomDistributionsStorageEngine : CustomDistributionsStorageEngineImplementation() - -internal open class CustomDistributionsStorageEngineImplementation( - override val logger: Logger = Logger("glean/CustomDistributionsStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): PrecomputedHistogram? { - return try { - (value as? String)?.let { - PrecomputedHistogram.fromJsonString(it) - } - } catch (e: org.json.JSONException) { - null - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: PrecomputedHistogram, - extraSerializationData: Any? - ) { - val json = value.toJsonObject() - userPreferences?.putString(storeName, json.toString()) - } - - /** - * Accumulate an array of samples for the provided metric. - * - * @param metricData the metric information for the custom distribution - * @param samples the values to accumulate - */ - @Suppress("LongParameterList") - @Synchronized - fun accumulateSamples( - metricData: CommonMetricData, - samples: LongArray, - rangeMin: Long, - rangeMax: Long, - bucketCount: Int, - histogramType: HistogramType - ) { - val validSamples = samples.filter { sample -> sample >= 0 } - val numNegativeSamples = samples.size - validSamples.size - if (numNegativeSamples > 0) { - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Accumulate $numNegativeSamples negative samples", - logger, - numNegativeSamples - ) - return - } - - // Since the custom combiner closure captures this value, we need to just create a dummy - // value here that won't be used by the combine function, and create a fresh - // PrecomputedHistogram for each value that doesn't have an existing current value. - val dummy = PrecomputedHistogram( - rangeMin = rangeMin, - rangeMax = rangeMax, - bucketCount = bucketCount, - histogramType = histogramType - ) - validSamples.forEach { sample -> - super.recordMetric(metricData, dummy, null) { currentValue, _ -> - currentValue?.let { - it.accumulate(sample) - it - } ?: let { - val newTD = PrecomputedHistogram( - rangeMin = rangeMin, - rangeMax = rangeMax, - bucketCount = bucketCount, - histogramType = histogramType - ) - newTD.accumulate(sample) - return@let newTD - } - } - } - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the [JSONObject] containing the recorded data. - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshot(storeName, clearStore)?.let { dataMap -> - val jsonObj = JSONObject() - dataMap.forEach { - jsonObj.put(it.key, it.value.toJsonPayloadObject()) - } - return jsonObj - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/DatetimesStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/DatetimesStorageEngine.kt deleted file mode 100644 index b044e660bf9..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/DatetimesStorageEngine.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.DatetimeMetricType -import mozilla.components.service.glean.utils.getISOTimeString -import mozilla.components.service.glean.utils.parseISOTimeString -import mozilla.components.support.base.log.logger.Logger -import java.util.Calendar -import java.util.Date - -/** - * This singleton handles the in-memory storage logic for datetimes. It is meant to be used by - * the Specific Datetime API and the ping assembling objects. - * - * This stores dates both in-memory and on-disk as Strings, not Date objects. We do - * this because we need to preserve the timezone offset value at the time the value - * was set. The [Date]/[Calendar] API in pre-Java 8 unfortunately does not allow - * round-tripping the timezone offset when parsing a datetime string. Since we don't - * actually ever need to operate on the datetime's in a meaningful way, it's easiest - * to just store the strings and treat them as opaque for everything but the testing API. - */ -@SuppressLint("StaticFieldLeak") -internal object DatetimesStorageEngine : DatetimesStorageEngineImplementation() - -internal open class DatetimesStorageEngineImplementation( - override val logger: Logger = Logger("glean/DatetimesStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): String? { - // This parses the date strings on ingestion as a sanity check, but we - // don't actually need their results, and that would throw away the - // timezone offset information. - (value as? String)?.let { - stringValue -> parseISOTimeString(stringValue)?.let { - return stringValue - } - } - return null - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: String, - extraSerializationData: Any? - ) { - userPreferences?.putString(storeName, value) - } - - /** - * Set the metric to the provided date/time, truncating it to the - * metric's resolution. - * - * @param metricData the metric information for the datetime - * @param date the date value to set this metric to - */ - fun set(metricData: DatetimeMetricType, date: Date = Date()) { - super.recordMetric( - metricData as CommonMetricData, - getISOTimeString(date, metricData.timeUnit) - ) - } - - /** - * Set the metric to the provided date/time, truncating it to the - * metric's resolution. - * - * @param metricData the metric information for the datetime - * @param date the date value to set this metric to - */ - fun set(metricData: DatetimeMetricType, calendar: Calendar) { - super.recordMetric( - metricData as CommonMetricData, - getISOTimeString(calendar, metricData.timeUnit) - ) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/EventsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/EventsStorageEngine.kt deleted file mode 100644 index d149527751a..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/EventsStorageEngine.kt +++ /dev/null @@ -1,355 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.Context -import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.private.EventMetricType -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.utils.ensureDirectoryExists -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import java.io.File -import java.io.IOException -import kotlinx.coroutines.Dispatchers as KotlinDispatchers - -/** - * This singleton handles the in-memory storage logic for events. It is meant to be used by - * the Specific Events API and the ping assembling objects. - * - * So that the data survives shutting down of the application, events are stored - * in an append-only file on disk, in addition to the store in memory. Each line - * of this file records a single event in JSON, exactly as it will be sent in the - * ping. There is one file per store. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak, TooManyFunctions") -internal object EventsStorageEngine : StorageEngine { - override lateinit var applicationContext: Context - - // Events recorded within the list should be reasonably sorted by timestamp, assuming - // the sequence of calls to [record] has not been messed with. However, please only - // trust the recorded timestamp values. - internal val eventStores: MutableMap> = mutableMapOf() - - // The storage directory where the append-only events files reside. - internal val storageDirectory: File by lazy { - val dir = File( - applicationContext.applicationInfo.dataDir, - "${Glean.GLEAN_DATA_DIR}/events/" - ) - ensureDirectoryExists(dir) - dir - } - - // Maximum length of any string value in the extra dictionary, in characters - internal const val MAX_LENGTH_EXTRA_KEY_VALUE = 100 - - // The position of the fields within the JSON payload for each event - internal const val TIMESTAMP_FIELD = "timestamp" - internal const val CATEGORY_FIELD = "category" - internal const val NAME_FIELD = "name" - internal const val EXTRA_FIELD = "extra" - - private val logger = Logger("glean/EventsStorageEngine") - - // Timeout for waiting on async IO (during testing only) - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal const val JOB_TIMEOUT_MS = 5000L - - // A lock to prevent simultaneous writing of the event files - private val eventFileLock = Any() - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - var ioTask: Job? = null - - /** - * Initialize events storage. This must be called once on application startup, - * e.g. from [Glean.initialize], but after we are ready to send pings, since - * this could potentially collect and send pings. - * - * If there are any events queued on disk, it loads them into memory so - * that the memory and disk representations are in sync. - * - * Secondly, if this is the first time the application has been run since - * rebooting, any pings containing events are assembled into pings and cleared - * immediately, since their timestamps won't be compatible with the timestamps - * we would create during this boot of the device. - * - * @param context The application context - */ - internal fun onReadyToSendPings(@Suppress("UNUSED_PARAMETER") context: Context) { - // We want this to run off of the main thread, because it might perform I/O. - // However, we don't use the built-in KotlinDispatchers.IO since we need - // to make sure this work is done before any other Glean API calls, and this - // will force them to be queued after this work. - @Suppress("EXPERIMENTAL_API_USAGE") - Dispatchers.API.executeTask { - // Load events from disk - storageDirectory.listFiles()?.forEach { file -> - val storeData = eventStores.getOrPut(file.name) { mutableListOf() } - file.forEachLine { line -> - try { - val jsonContent = JSONObject(line) - val event = deserializeEvent(jsonContent) - storeData.add(event) - } catch (e: JSONException) { - // pass - } - } - } - - // TODO Technically, we only need to send out all pending events - // if they came from a previous boot (since their timestamps won't match). - // However, there are various challenges in detecting when the device has - // been booted, so for now we just pay the penalty of a few more - // unnecessary pings. - if (eventStores.isNotEmpty()) { - Glean.sendPingsByName(eventStores.keys.toList()) - } - } - } - - /** - * Record an event in the desired stores. - * - * @param metricData the [EventMetricType] instance being recorded - * @param monotonicElapsedMs the monotonic elapsed time since boot, in milliseconds - * @param extra an optional, user defined String to String map used to provide richer event - * context if needed - */ - fun > record( - metricData: EventMetricType, - monotonicElapsedMs: Long, - extra: Map? = null - ) { - if (metricData.lifetime != Lifetime.Ping) { - recordError( - metricData, - ErrorType.InvalidValue, - "Must have `Ping` lifetime.", - logger - ) - return - } - - // Check that the extra content has sane values. - val truncatedExtraKeys = extra?.toMutableMap()?.let { eventKeys -> - for ((key, extraValue) in eventKeys) { - if (extraValue.length > MAX_LENGTH_EXTRA_KEY_VALUE) { - recordError( - metricData, - ErrorType.InvalidValue, - "Extra key length ${extraValue.length} exceeds maximum of $MAX_LENGTH_EXTRA_KEY_VALUE", - logger - ) - eventKeys[key] = extraValue.substring(0, MAX_LENGTH_EXTRA_KEY_VALUE) - } - } - eventKeys - } - - val event = RecordedEventData( - metricData.category, - metricData.name, - monotonicElapsedMs, - truncatedExtraKeys - ) - val jsonEvent = serializeEvent(event).toString() - - // Record a copy of the event in all the needed stores. - synchronized(this) { - val eventStoresToUpload: MutableList = mutableListOf() - for (storeName in metricData.sendInPings) { - val storeData = eventStores.getOrPut(storeName) { mutableListOf() } - storeData.add(event.copy()) - writeEventToDisk(storeName, jsonEvent) - if (storeData.size == Glean.configuration.maxEvents) { - // The ping contains enough events to send now, add it to the list of pings to - // send - eventStoresToUpload.add(storeName) - } - } - Glean.sendPingsByName(eventStoresToUpload) - } - } - - /** - * Retrieves the [recorded event data][RecordedEventData] for the provided - * store name. - * - * @param storeName the name of the desired event store - * @param clearStore whether or not to clearStore the requested event store - * - * @return the list of events recorded in the requested store - */ - @Synchronized - fun getSnapshot(storeName: String, clearStore: Boolean): List? { - // Rewrite all of the timestamps to they are relative to the first - // timestamp - val events = eventStores[storeName]?.let { store -> - if (store.size == 0) { - logger.error("Unexpectedly got empty event store") - null - } else { - val firstTimestamp = store[0].timestamp - store.map { event -> - event.copy(timestamp = event.timestamp - firstTimestamp) - } - } - } - - if (clearStore) { - ioTask = GlobalScope.launch(KotlinDispatchers.IO) { - synchronized(eventFileLock) { - getFile(storeName).delete() - } - } - eventStores.remove(storeName) - } - - return events - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the JSONArray containing the recorded data - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshot(storeName, clearStore)?.let { pingEvents -> - val eventsArray = JSONArray() - pingEvents.forEach { - eventsArray.put(serializeEvent(it)) - } - eventsArray - } - } - - /** - * Get the File object in which to store events for the given store. - * - * @param storeName The name of the store - * @return File object to store events - */ - private fun getFile(storeName: String): File { - return File(storageDirectory, storeName) - } - - /** - * Serializes an event to its JSON representation. - * - * @param event Event data to serialize - * @return [JSONObject] representing the event - */ - private fun serializeEvent(event: RecordedEventData): JSONObject { - val eventData = JSONObject(mapOf( - TIMESTAMP_FIELD to event.timestamp, - CATEGORY_FIELD to event.category, - NAME_FIELD to event.name - )) - if (event.extra != null) { - eventData.put(EXTRA_FIELD, JSONObject(event.extra)) - } - return eventData - } - - /** - * Deserializes an event in JSON into a RecordedEventData object. - * - * @param jsonContent The JSONObject containing the data for the event. - * @return [RecordedEventData] representing the event data - */ - private fun deserializeEvent(jsonContent: JSONObject): RecordedEventData { - val extra: Map? = jsonContent.optJSONObject(EXTRA_FIELD)?.let { - val extraValues: MutableMap = mutableMapOf() - it.names()?.let { names -> - for (i in 0 until names.length()) { - extraValues[names.getString(i)] = it.getString(names.getString(i)) - } - } - extraValues - } - - return RecordedEventData( - jsonContent.getString(CATEGORY_FIELD), - jsonContent.getString(NAME_FIELD), - jsonContent.getLong(TIMESTAMP_FIELD), - extra - ) - } - - /** - * Writes an event to a single store on disk. - * - * @param storeName The name of the store to add the event to. - * @param eventContent A string of JSON as returned by [serializeEvent] - */ - internal fun writeEventToDisk(storeName: String, eventContent: String) { - ioTask = GlobalScope.launch(KotlinDispatchers.IO) { - synchronized(eventFileLock) { - try { - getFile(storeName).appendText("${eventContent}\n") - } catch (e: IOException) { - logger.warn("IOException while writing event to disk: $e") - } - } - } - } - - override val sendAsTopLevelField: Boolean - get() = true - - override fun clearAllStores() { - // Wait until any writes have cleared until deleting all the files. - // This is not a performance problem since this function is for use - // in testing only. - testWaitForWrites() - synchronized(eventFileLock) { - storageDirectory.listFiles()?.forEach { - it.delete() - } - } - eventStores.clear() - } - - internal fun testWaitForWrites(timeout: Long = JOB_TIMEOUT_MS) { - ioTask?.let { - runBlocking { - withTimeout(timeout) { - it.join() - } - } - } - } -} - -@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -data class RecordedEventData( - val category: String, - val name: String, - var timestamp: Long, - val extra: Map? = null, - - internal val identifier: String = if (category.isEmpty()) { name } else { "$category.$name" } -) diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/ExperimentsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/ExperimentsStorageEngine.kt deleted file mode 100644 index 908c766508d..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/ExperimentsStorageEngine.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.android.org.json.toJSON - -import android.content.Context -import androidx.annotation.VisibleForTesting -import org.json.JSONObject - -/** - * This singleton handles the in-memory storage logic for keeping track of - * the active experiments. It is meant to be used by through the methods in - * the Glean class (General API). - */ -@SuppressLint("StaticFieldLeak") -internal object ExperimentsStorageEngine : StorageEngine { - override lateinit var applicationContext: Context - - private val logger = Logger("glean/ExperimentsStorageEngine") - - // Maximum length of the experiment and branch names, in characters. - private const val MAX_ID_LENGTH = 30 - - private val experiments: MutableMap = mutableMapOf() - - /** - * Record an experiment as active. - * - * @param experimentId the id of the experiment - * @param branch the active branch of the experiment - * @param extra an optional, user defined String to String map used to provide richer experiment - * context if needed - */ - fun setExperimentActive( - experimentId: String, - branch: String, - extra: Map? = null - ) { - val truncatedExperimentId = experimentId.let { - if (it.length > MAX_ID_LENGTH) { - logger.warn("experimentId ${it.length} > $MAX_ID_LENGTH") - return@let it.substring(0, MAX_ID_LENGTH) - } - it - } - - val truncatedBranch = branch.let { - if (it.length > MAX_ID_LENGTH) { - logger.warn("branch ${it.length} > $MAX_ID_LENGTH") - return@let it.substring(0, MAX_ID_LENGTH) - } - it - } - - val experimentData = RecordedExperimentData(truncatedBranch, extra) - experiments.put(truncatedExperimentId, experimentData) - } - - /** - * Set an active experiment as inactive. - * - * Warns to the logger (but does not raise) if the experiment is not already active. - * - * @param experimentId the id of the experiment to set as inactive - */ - fun setExperimentInactive(experimentId: String) { - val truncatedExperimentId = experimentId.let { - if (it.length > MAX_ID_LENGTH) { - logger.warn("experimentId ${it.length} > $MAX_ID_LENGTH") - return@let it.substring(0, MAX_ID_LENGTH) - } - it - } - - if (!experiments.containsKey(truncatedExperimentId)) { - logger.warn("experiment $experimentId} not already active") - } else { - experiments.remove(truncatedExperimentId) - } - } - - /** - * Retrieves the data about active experiments - * - * @return the map of experiment ids to [RecordedExperimentData] - */ - @Synchronized - fun getSnapshot(): Map { - return experiments - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store. Since experiments are - * included in all stores, this parameter is effectively ignored, but - * must be provided to comply with the StorageEngine interface. - * @param clearStore whether or not to clearStore the requested store. - * This parameter is ignored. - * - * @return the JSONObject containing information about the active experiments - */ - override fun getSnapshotAsJSON( - @Suppress("UNUSED_PARAMETER") storeName: String, - @Suppress("UNUSED_PARAMETER") clearStore: Boolean - ): Any? { - val pingExperiments = getSnapshot() - - if (pingExperiments.count() == 0) { - return null - } - - val experimentsMap = JSONObject() - for ((key, value) in pingExperiments) { - val experimentData = JSONObject() - experimentData.put("branch", value.branch) - if (value.extra != null) { - experimentData.put("extra", value.extra.toJSON()) - } - experimentsMap.put(key, experimentData) - } - return experimentsMap - } - - override val sendAsTopLevelField: Boolean - get() = true - - override fun clearAllStores() { - experiments.clear() - } -} - -@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -data class RecordedExperimentData( - val branch: String, - val extra: Map? = null -) diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/GenericStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/GenericStorageEngine.kt deleted file mode 100644 index e7973626171..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/GenericStorageEngine.kt +++ /dev/null @@ -1,295 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONObject - -/** - * Defines an alias for a generic data storage to be used by - * [GenericStorageEngine]. This maps a metric name to - * its data. - */ -internal typealias GenericDataStorage = MutableMap - -/** - * Defines an alias for a generic storage map to be used by - * [GenericStorageEngine]. This maps a store name to - * the [GenericDataStorage] it holds. - */ -internal typealias GenericStorageMap = MutableMap> - -/** - * Defines the typealias for the combiner function to be used by - * [GenericStorageEngine.recordMetric] when recording new values. - */ -internal typealias MetricsCombiner = (currentValue: T?, newValue: T) -> T - -/** - * A base class for common metric storage functionality. This allows sharing the common - * store managing and lifetime behaviours. - */ -internal abstract class GenericStorageEngine : StorageEngine { - override lateinit var applicationContext: Context - - // Let derived class define a logger so that they can provide a proper name, - // useful when debugging weird behaviours. - abstract val logger: Logger - - protected val userLifetimeStorage: SharedPreferences by lazy { - deserializeLifetime(Lifetime.User) - } - protected val pingLifetimeStorage: SharedPreferences by lazy { - deserializeLifetime(Lifetime.Ping) - } - - // Store a map for each lifetime as an array element: - // Array[Lifetime] = Map[StorageName, MetricType]. - protected val dataStores: Array> = - Array(Lifetime.values().size) { mutableMapOf>() } - - /** - * Implementor's provided function to convert deserialized 'user' lifetime - * data to the destination [MetricType]. - * - * @param metricName the name of the metric being deserialized - * @param value loaded from the storage as [Any] - * - * @return data as [MetricType] or null if deserialization failed - */ - protected abstract fun deserializeSingleMetric(metricName: String, value: Any?): MetricType? - - /** - * Implementor's provided function to serialize 'user' lifetime data as needed by the data type. - * - * @param userPreferences [SharedPreferences.Editor] for writing preferences as needed by type. - * @param storeName The metric store name where the data is stored in [SharedPreferences]. - * @param value The value to be stored, passed as a [MetricType] to be handled correctly by the - * implementor. - * @param extraSerializationData extra data to be serialized to disk for "User" persisted values - */ - protected abstract fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: MetricType, - extraSerializationData: Any? - ) - - /** - * Deserialize the metrics with a particular lifetime that are on disk. - * This will be called the first time a metric is used or before a snapshot is - * taken. - * - * @param lifetime a [Lifetime] to deserialize - * @return A [SharedPreferences] reference that will be used to initialize [pingLifetimeStorage] - * or [userLifetimeStorage] or null if an invalid lifetime is used. - */ - @Suppress("TooGenericExceptionCaught", "ComplexMethod") - open fun deserializeLifetime(lifetime: Lifetime): SharedPreferences { - require(lifetime == Lifetime.Ping || lifetime == Lifetime.User) { - "deserializeLifetime does not support Lifetime.Application" - } - - val prefsName = if (lifetime == Lifetime.Ping) { - "${this.javaClass.canonicalName}.PingLifetime" - } else { - this.javaClass.canonicalName - } - val prefs = applicationContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE) - - val metrics = try { - prefs.all.entries - } catch (e: NullPointerException) { - // If we fail to deserialize, we can log the problem but keep on going. - logger.error("Failed to deserialize metric with ${lifetime.name} lifetime") - return prefs - } - - for ((metricStoragePath, metricValue) in metrics) { - if (!metricStoragePath.contains('#')) { - continue - } - - // Split the stored name in 2: we expect it to be in the format - // store#metric.name - val (storeName, metricName) = - metricStoragePath.split('#', limit = 2) - if (storeName.isEmpty()) { - continue - } - - val storeData = dataStores[lifetime.ordinal].getOrPut(storeName) { mutableMapOf() } - // Only set the stored value if we're able to deserialize the persisted data. - deserializeSingleMetric(metricName, metricValue)?.let { value -> - storeData[metricName] = value - } ?: logger.warn("Failed to deserialize $metricStoragePath") - } - - return prefs - } - - /** - * Ensures that the lifetime metrics in [pingLifetimeStorage] and [userLifetimeStorage] is - * loaded. This is a no-op if they are already loaded. - */ - private fun ensureAllLifetimesLoaded() { - // Make sure data with the provided lifetime is loaded. - // We still need to catch exceptions here, as `getAll()` might throw. - @Suppress("TooGenericExceptionCaught") - try { - pingLifetimeStorage.all - userLifetimeStorage.all - } catch (e: NullPointerException) { - // Intentionally left blank. We just want to fall through. - } - } - - /** - * Retrieves the [recorded metric data][MetricType] for the provided - * store name. - * - * Please note that the [Lifetime.Application] lifetime is handled implicitly - * by never clearing its data. It will naturally clear out when restarting the - * application. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clear the requested store. Not that only - * metrics stored with a lifetime of [Lifetime.Ping] will be cleared. - * - * @return the [MetricType] recorded in the requested store - */ - @Synchronized - fun getSnapshot(storeName: String, clearStore: Boolean): GenericDataStorage? { - val allLifetimes: GenericDataStorage = mutableMapOf() - - ensureAllLifetimesLoaded() - - // Get the metrics for all the supported lifetimes. - for (store in dataStores) { - store[storeName]?.let { - allLifetimes.putAll(it) - } - } - - if (clearStore) { - // We only allow clearing metrics with the "ping" lifetime. - val editor = pingLifetimeStorage.edit() - dataStores[Lifetime.Ping.ordinal][storeName]?.keys?.forEach { key -> - editor.remove("$storeName#$key") - } - editor.apply() - dataStores[Lifetime.Ping.ordinal].remove(storeName) - } - - return if (allLifetimes.isNotEmpty()) allLifetimes else null - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the [JSONObject] containing the recorded data. - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshot(storeName, clearStore)?.let { dataMap -> - return JSONObject(dataMap as MutableMap<*, *>) - } - } - - /** - * Return all of the metric identifiers currently holding data for the given - * stores. - * - * @param stores The stores to look in. - * @return a sequence of identifiers (including labels, if any) found in - * those stores. - */ - override fun getIdentifiersInStores(stores: List) = sequence { - // Make sure data with "user" and "ping" lifetimes are loaded before getting the snapshot. - ensureAllLifetimesLoaded() - - dataStores.forEach { lifetime -> - stores.forEach { - lifetime[it]?.let { store -> - store.forEach { - yield(it.key) - } - } - } - } - } - - /** - * Helper function for the derived classes. It can be used to record - * simple metrics to the internal storage. Internally, this calls [recordMetric] - * with a custom `combine` function that only sets the new value. - * - * @param metricData the information about the metric - * @param value the new value - */ - protected fun recordMetric( - metricData: CommonMetricData, - value: MetricType - ) = recordMetric(metricData, value) { _, v -> v } - - /** - * Helper function for the derived classes. It can be used to record - * simple metrics to the internal storage. - * - * @param metricData the information about the metric - * @param value the new value - * @param extraSerializationData extra data to be serialized to disk for "User" persisted values - * @param combine a lambda function to combine the currently stored value and - * the new one; this allows to implement new behaviours such as adding. - */ - @Synchronized - protected fun recordMetric( - metricData: CommonMetricData, - value: MetricType, - extraSerializationData: Any? = null, - combine: MetricsCombiner - ) { - checkNotNull(applicationContext) { "No recording can take place without an application context" } - - // Record a copy of the metric in all the needed stores. - @SuppressLint("CommitPrefEdits") - val editor: SharedPreferences.Editor? = when (metricData.lifetime) { - Lifetime.User -> userLifetimeStorage.edit() - Lifetime.Ping -> pingLifetimeStorage.edit() - else -> null - } - metricData.sendInPings.forEach { store -> - val storeData = dataStores[metricData.lifetime.ordinal].getOrPut(store) { mutableMapOf() } - // We support empty categories for enabling the internal use of metrics - // when assembling pings in [PingMaker]. - val entryName = metricData.identifier - val combinedValue = combine(storeData[entryName], value) - storeData[entryName] = combinedValue - // Persist data with "user" or "ping" lifetimes - editor?.let { - serializeSingleMetric( - it, - "$store#$entryName", - combinedValue, - extraSerializationData - ) - } - } - editor?.apply() - } - - override fun clearAllStores() { - userLifetimeStorage.edit().clear().apply() - pingLifetimeStorage.edit().clear().apply() - dataStores.forEach { it.clear() } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngine.kt deleted file mode 100644 index 918f36b28fc..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngine.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.MemoryUnit -import mozilla.components.service.glean.utils.memoryToBytes - -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONObject - -/** - * This singleton handles the in-memory storage logic for memory distributions. It is meant to be - * used by the Memory Distribution API and the ping assembling objects. - */ -internal object MemoryDistributionsStorageEngine : MemoryDistributionsStorageEngineImplementation() - -internal open class MemoryDistributionsStorageEngineImplementation( - override val logger: Logger = Logger("glean/MemoryDistributionsStorageEngine") -) : GenericStorageEngine() { - - companion object { - // The base of the logarithm used to determine bucketing - internal const val LOG_BASE = 2.0 - - // The buckets per each order of magnitude of the logarithm. - internal const val BUCKETS_PER_MAGNITUDE = 16.0 - - // Set a maximum recordable value of 1 terabyte so the buckets aren't - // completely unbounded. - internal const val MAX_BYTES: Long = 1L shl 40 - } - - override fun deserializeSingleMetric(metricName: String, value: Any?): FunctionalHistogram? { - return try { - (value as? String)?.let { - FunctionalHistogram.fromJsonString(it) - } - } catch (e: org.json.JSONException) { - null - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: FunctionalHistogram, - extraSerializationData: Any? - ) { - val json = value.toJsonObject() - userPreferences?.putString(storeName, json.toString()) - } - - /** - * Accumulate value for the provided metric. - * - * Samples greater than 1TB are truncated to 1TB. - * - * @param metricData the metric information for the memory distribution - * @param sample the value to accumulate - * @param memoryUnit the unit of the sample - */ - @Synchronized - fun accumulate( - metricData: CommonMetricData, - sample: Long, - memoryUnit: MemoryUnit - ) { - accumulateSamples(metricData, longArrayOf(sample), memoryUnit) - } - - /** - * Accumulate an array of samples for the provided metric. - * - * Samples greater than 1TB are truncated to 1TB. - * - * @param metricData the metric information for the memory distribution - * @param samples the values to accumulate, in the given `memoryUnit` - * @param memoryUnit the unit that the given samples are in - */ - @Suppress("ComplexMethod") - @Synchronized - fun accumulateSamples( - metricData: CommonMetricData, - samples: LongArray, - memoryUnit: MemoryUnit - ) { - // Remove invalid samples, and convert to bytes - var numTooLongSamples = 0 - var numNegativeSamples = 0 - var factor = memoryToBytes(memoryUnit, 1) - val validSamples = samples.map { sample -> - if (sample < 0) { - numNegativeSamples += 1 - 0 - } else { - val sampleInBytes = sample * factor - if (sampleInBytes > MAX_BYTES) { - numTooLongSamples += 1 - MAX_BYTES - } else { - sampleInBytes - } - } - } - - if (numNegativeSamples > 0) { - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Accumulate $numNegativeSamples negative samples", - logger, - numNegativeSamples - ) - // Negative samples indicate a serious and unexpected error, so don't record anything - return - } - - if (numTooLongSamples > 0) { - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Accumulate $numTooLongSamples samples longer than 1 terabyte", - logger, - numTooLongSamples - ) - // Too large samples should just be truncated, but otherwise we record and handle them - } - - val dummy = FunctionalHistogram(LOG_BASE, BUCKETS_PER_MAGNITUDE) - validSamples.forEach { sample -> - super.recordMetric(metricData, dummy, null) { currentValue, _ -> - currentValue?.let { - it.accumulate(sample) - it - } ?: let { - val newMD = FunctionalHistogram(LOG_BASE, BUCKETS_PER_MAGNITUDE) - newMD.accumulate(sample) - return@let newMD - } - } - } - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the [JSONObject] containing the recorded data. - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshot(storeName, clearStore)?.let { dataMap -> - val jsonObj = JSONObject() - dataMap.forEach { - jsonObj.put(it.key, it.value.toJsonPayloadObject()) - } - return jsonObj - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/PingStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/PingStorageEngine.kt deleted file mode 100644 index 0c725d9257d..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/PingStorageEngine.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.Dispatchers as KotlinDispatchers -import kotlinx.coroutines.launch -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.utils.ensureDirectoryExists -import mozilla.components.support.base.log.logger.Logger -import java.io.BufferedReader -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.FileReader -import java.io.IOException -import java.util.UUID - -/** - * This class implementation stores pings as files on disk by serializing the upload path and the - * JSON ping payload data. Data is stored in the application's data directory under the - * [Glean.GLEAN_DATA_DIR] in a folder called pending_pings, which is consistent with desktop. - * - * The pings are further separated into folders based on the ping name in order to make it easier to - * upload different pings on different schedules. - */ -internal class PingStorageEngine(context: Context) { - - private val logger: Logger = Logger("glean/PingStorageEngine") - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val storageDirectory: File = File(context.applicationInfo.dataDir, PING_STORAGE_DIRECTORY) - - // The cache directory is used to temporarily write ping files before they are moved - // to the final [PING_STORAGE_DIRECTORY]. This directory is still app-private, so - // saving pings there it's still safe. - private val cacheDirectory: File = context.cacheDir - - companion object { - // Since ping file names are UUIDs, this matches UUIDs for filtering purposes - private const val FILE_PATTERN = - "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - // Base directory for storing serialized pings - private const val PING_STORAGE_DIRECTORY = "${Glean.GLEAN_DATA_DIR}/pending_pings" - } - - /** - * Function to store the ping to file to the application's storage directory - * - * @param uuidFileName UUID that will represent the file named used to store the ping - * @param uploadPath String representing the upload path used for uploading the ping - * @param pingData Serialized JSON string representing the ping payload - */ - fun store(uuidFileName: UUID, uploadPath: String, pingData: String): Job { - return GlobalScope.launch(KotlinDispatchers.IO) { - // Check that the director exists and create it if needed - ensureDirectoryExists(storageDirectory) - - // Build the file path for the ping, using the UUID from the path for the file name - val pingFile = File(storageDirectory, uuidFileName.toString()) - logger.debug("Storing ping $uuidFileName at ${pingFile.absolutePath}") - - // Write ping to file - writePingToFile(pingFile, uploadPath, pingData) - } - } - - /** - * Function to deserialize and process all serialized ping files. This function will ignore - * files that don't match the UUID regex and just delete them to prevent files from polluting - * the ping storage directory. - * - * @param processingCallback Callback function to do the actual process action on the ping. - * Typically this will be a [PingUploader.upload] function. - * @return Boolean representing the success of the upload task. This may be the value bubbled up - * from the callback, or if there was an error reading the files. - */ - @Synchronized - fun process(processingCallback: (String, String, Configuration) -> Boolean): Boolean { - // Marked as @Synchronized so that this doesn't run at the same time as - // clearPendingPings. - logger.debug("Processing persisted pings at ${storageDirectory.absolutePath}") - - var success = true - - storageDirectory.listFiles()?.forEach { file -> - if (file.name.matches(Regex(FILE_PATTERN))) { - logger.debug("Processing ping: ${file.name}") - if (!processFile(file, processingCallback)) { - logger.error("Error processing ping file: ${file.name}") - success = false - } - } else { - // Delete files that don't match the UUID FILE_PATTERN regex - logger.debug("Pattern mismatch. Deleting ${file.name}") - file.delete() - } - } - - return success - } - - /** - * This function encapsulates processing of a single ping file. - * - * @param file The [File] to process - * @param processingCallback the callback that actually processes the file - * - */ - private fun processFile( - file: File, - processingCallback: (String, String, Configuration) -> Boolean - ): Boolean { - var processed = false - BufferedReader(FileReader(file)).use { - try { - val path = it.readLine() - val serializedPing = it.readLine() - - processed = serializedPing == null || - processingCallback(path, serializedPing, Glean.configuration) - } catch (e: FileNotFoundException) { - // This shouldn't happen after we queried the directory. - logger.error("Could not find ping file ${file.name}") - return false - } catch (e: IOException) { - // Something is not right. - logger.error("IO Exception when reading file ${file.name}") - return false - } - } - - return if (processed) { - val fileWasDeleted = file.delete() - logger.debug("${file.name} was deleted: $fileWasDeleted") - true - } else { - // The callback couldn't process this file. - false - } - } - - /** - * Serializes the upload path and data to a file to persist data for the upload worker to use. - * - * @param pingFile The file to write the path and data to - * @param uploadPath The upload path to serialize - * @param pingData The ping data to serialize - */ - private fun writePingToFile(pingFile: File, uploadPath: String, pingData: String) { - // Store data to a temporary file. - val temporaryFile = File.createTempFile("glean-ping", ".tmp", cacheDirectory) - - // Write the ping content data to the temp file. - FileOutputStream(temporaryFile, true).bufferedWriter().use { - try { - it.write(uploadPath) - it.newLine() - it.write(pingData) - it.newLine() - it.flush() - } catch (e: IOException) { - logger.warn("IOException while writing ping to file", e) - return - } - } - - // Nothing weird happened, move the file to the final location. This could still - // fail, but there's really no way for us to do something about it. - if (!temporaryFile.renameTo(pingFile)) { - logger.warn("Unable to move ${temporaryFile.absolutePath} to ${pingFile.absolutePath}") - } - } - - /** - * Deletes any pending pings on disk. - */ - @Synchronized - internal fun clearPendingPings() { - // Marked as @Synchronized so that this doesn't run at the same time as - // process. - storageDirectory.listFiles()?.forEach { file -> - val fileWasDeleted = file.delete() - logger.debug("${file.name} was deleted: $fileWasDeleted") - } - } - - /** - * Returns the number of pending pings on disk, for testing purposes. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @Synchronized - internal fun testGetNumPendingPings(): Int { - return storageDirectory.listFiles()!!.size - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/QuantitiesStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/QuantitiesStorageEngine.kt deleted file mode 100644 index 62089eff5a1..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/QuantitiesStorageEngine.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger - -/** - * This singleton handles the in-memory storage logic for quantities. It is meant to be used by - * the Specific Quantities API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object QuantitiesStorageEngine : QuantitiesStorageEngineImplementation() - -internal open class QuantitiesStorageEngineImplementation( - override val logger: Logger = Logger("glean/QuantitiesStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): Long? { - return (value as? Long)?.let { - return@let if (it < 0) null else it - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: Long, - extraSerializationData: Any? - ) { - userPreferences?.putLong(storeName, value) - } - - /** - * Record a string in the desired stores. - * - * @param metricData object with metric settings - * @param value the value to set. Must be non-negative. - */ - fun record( - metricData: CommonMetricData, - value: Long - ) { - if (value < 0) { - recordError( - metricData, - ErrorType.InvalidValue, - "Set negative value $value", - logger - ) - return - } - - super.recordMetric(metricData, value) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngine.kt deleted file mode 100644 index 2171d97e569..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngine.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context - -/** - * Base interface intended to be implemented by the different - * storage engines - */ -internal interface StorageEngine { - var applicationContext: Context - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the JSON object containing the recorded data. This could be either - * a [JSONObject] or a [JSONArray]. Unfortunately, the only common - * ancestor is [Object], so we need to return [Any]. - */ - fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? - - /** - * Return all of the metric identifiers currently holding data for the given - * stores. - * - * @param stores The stores to look in. - * @return a sequence of identifiers (including labels, if any) found in - * those stores. - */ - fun getIdentifiersInStores(stores: List): Sequence = sequence {} - - /** - * Clear all stored data in the storage engine - */ - fun clearAllStores() - - /** - * Indicate whether this storage engine is sent at the top level of the ping - * (rather than in the metrics section). - */ - val sendAsTopLevelField: Boolean - get() = false -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngineManager.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngineManager.kt deleted file mode 100644 index 6efa34319ae..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StorageEngineManager.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import mozilla.components.service.glean.private.BooleanMetricType -import mozilla.components.service.glean.private.CounterMetricType -import mozilla.components.service.glean.private.CustomDistributionMetricType -import mozilla.components.service.glean.private.DatetimeMetricType -import mozilla.components.service.glean.private.MemoryDistributionMetricType -import mozilla.components.service.glean.private.QuantityMetricType -import mozilla.components.service.glean.private.StringListMetricType -import mozilla.components.service.glean.private.StringMetricType -import mozilla.components.service.glean.private.TimespanMetricType -import mozilla.components.service.glean.private.TimingDistributionMetricType -import mozilla.components.service.glean.private.UuidMetricType -import mozilla.components.support.ktx.android.org.json.getOrPutJSONObject -import org.json.JSONArray -import org.json.JSONObject - -/** - * This singleton is the one interface to all the available storage engines: - * it provides a convenient way to collect the data stored in a particular store - * and serialize it - */ -internal class StorageEngineManager( - private val storageEngines: Map = mapOf( - "boolean" to BooleansStorageEngine, - "counter" to CountersStorageEngine, - "custom_distribution" to CustomDistributionsStorageEngine, - "datetime" to DatetimesStorageEngine, - "events" to EventsStorageEngine, - "memory_distribution" to MemoryDistributionsStorageEngine, - "quantity" to QuantitiesStorageEngine, - "string" to StringsStorageEngine, - "string_list" to StringListsStorageEngine, - "timespan" to TimespansStorageEngine, - "timing_distribution" to TimingDistributionsStorageEngine, - "uuid" to UuidsStorageEngine - ), - applicationContext: Context -) { - init { - for ((_, engine) in storageEngines) { - engine.applicationContext = applicationContext - } - } - - /** - * Splits a labeled metric back into its name/label parts. - * - * If not a labeled metric, the second part of the Pair will be null. - * - * @param key The key for the metric value in the flattened storage engine - * @return A pair (metricName, label). label is null if key is not - * from a labeled metric. - */ - private fun parseLabeledMetric(key: String): Pair { - val divider = key.indexOf('/', 1) - if (divider >= 0) { - return Pair( - key.substring(0, divider), - key.substring(divider + 1) - ) - } else { - return Pair(key, null) - } - } - - /** - * Reorganizes the flat storage of metrics into labeled and unlabeled categories as - * they appear in the ping. - * - * The unlabeled metrics go under the `sectionName` key, and the labeled metrics go - * under the `labeled_$sectionName` key. - * - * @param sectionName The name of the metric section (the name of the metric type) - * @param dst The destination JSONObject in the ping - * @param engineData The flat object of metrics from the storage engine - */ - private fun separateLabeledAndUnlabeledMetrics( - sectionName: String, - dst: JSONObject, - engineData: JSONObject - ) { - for (key in engineData.keys()) { - val parts = parseLabeledMetric(key) - parts.second?.let { - val labeledSection = dst.getOrPutJSONObject("labeled_$sectionName") { JSONObject() } - val labeledMetric = labeledSection.getOrPutJSONObject(parts.first) { JSONObject() } - labeledMetric.put(it, engineData.get(key)) - } ?: run { - val section = dst.getOrPutJSONObject(sectionName) { JSONObject() } - section.put(key, engineData.get(key)) - } - } - } - - /** - * Collect the recorded data for the requested storage. - * - * @param storeName the name of the storage of interest - * @return a [JSONObject] containing the data collected from all the - * storage engines or returns a completely empty, zero-length [JSONObject] - * if the store is empty and there are no real metrics to send. - */ - fun collect(storeName: String): JSONObject { - val jsonPing = JSONObject() - val metricsSection = JSONObject() - for ((sectionName, engine) in storageEngines) { - val engineData = engine.getSnapshotAsJSON(storeName, clearStore = true) - val dst = if (engine.sendAsTopLevelField) jsonPing else metricsSection - // Most storage engines return a JSONObject mapping metric names - // to metric values, and these can include labeled metrics that we - // need to separate out. The EventsStorageEngine just returns an - // array of events, which are never "labeled". - if (engineData is JSONObject) { - separateLabeledAndUnlabeledMetrics(sectionName, dst, engineData) - } else if (engineData is JSONArray) { - dst.put(sectionName, engineData) - } - } - if (metricsSection.length() != 0) { - jsonPing.put("metrics", metricsSection) - } - - return jsonPing - } - - /** - * Clear all recorded metrics in all stores. - */ - internal fun clearAllStores() { - for (storageEngine in storageEngines) { - storageEngine.value.clearAllStores() - } - } - - companion object { - /** - * Get the storage engine associated with a given metric type. - * - * @return A storage engine, or null if none exists - */ - internal fun getStorageEngineForMetric(subMetric: T): StorageEngine? { - // Every metric that supports labels needs an entry here - return when (subMetric) { - is BooleanMetricType -> BooleansStorageEngine - is CounterMetricType -> CountersStorageEngine - is CustomDistributionMetricType -> CustomDistributionsStorageEngine - is DatetimeMetricType -> DatetimesStorageEngine - is MemoryDistributionMetricType -> MemoryDistributionsStorageEngine - is QuantityMetricType -> QuantitiesStorageEngine - is StringListMetricType -> StringListsStorageEngine - is StringMetricType -> StringsStorageEngine - is TimingDistributionMetricType -> TimingDistributionsStorageEngine - is TimespanMetricType -> TimespansStorageEngine - is UuidMetricType -> UuidsStorageEngine - else -> null - } - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringListsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringListsStorageEngine.kt deleted file mode 100644 index 743703b95c2..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringListsStorageEngine.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.android.org.json.toList -import org.json.JSONArray - -/** - * This singleton handles the in-memory storage logic for string lists. It is meant to be used by - * the Specific String List API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object StringListsStorageEngine : StringListsStorageEngineImplementation() - -internal open class StringListsStorageEngineImplementation( - override val logger: Logger = Logger("glean/StringsListsStorageEngine") -) : GenericStorageEngine>() { - companion object { - // Maximum length of any list - const val MAX_LIST_LENGTH_VALUE = 20 - // Maximum length of any string in the list - const val MAX_STRING_LENGTH = 50 - } - - override fun deserializeSingleMetric(metricName: String, value: Any?): List? { - /* - Since SharedPreferences doesn't directly support storing of List<> types, we must use - an intermediate JSONArray which can be deserialized and converted back to List. - Using JSONArray introduces a possible issue in that it's constructor will still properly - convert a stringified JSONArray into an array of Strings regardless of whether the values - have been properly quoted or not. For example, [1,2,3] is as valid just like - ["a","b","c"] is valid. - - The try/catch is necessary as JSONArray can throw a JSONException if it cannot parse the - string into an array. - */ - return (value as? String)?.let { - try { - return@let JSONArray(it).toList() - } catch (e: org.json.JSONException) { - return@let null - } - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: List, - extraSerializationData: Any? - ) { - // Since SharedPreferences doesn't directly support storing of List<> types, we must use - // an intermediate JSONArray which can be serialized to a String type and stored. - val jsonArray = JSONArray(value) - userPreferences?.putString(storeName, jsonArray.toString()) - } - - /** - * Appends to an existing string list in the desired stores. If the store or list doesn't exist - * then it is created and added to the desired stores. - * - * @param metricData object with metric settings - * @param value the string list value to add - */ - fun add( - metricData: CommonMetricData, - value: String - ) { - val truncatedValue = value.let { - if (it.length > MAX_STRING_LENGTH) { - recordError( - metricData, - ErrorType.InvalidValue, - "Individual value length ${it.length} exceeds maximum of $MAX_STRING_LENGTH", - logger - ) - return@let it.substring(0, MAX_STRING_LENGTH) - } - it - } - - // Use a custom combiner to add the string to the existing list rather than overwriting - super.recordMetric(metricData, listOf(truncatedValue), null) { currentValue, newValue -> - currentValue?.let { - if (it.count() + 1 > MAX_LIST_LENGTH_VALUE) { - recordError( - metricData, - ErrorType.InvalidValue, - "String list length of ${it.count() + 1} exceeds maximum of $MAX_LIST_LENGTH_VALUE", - logger - ) - } - - it + newValue.take(MAX_LIST_LENGTH_VALUE - it.count()) - } ?: newValue - } - } - - /** - * Sets a string list in the desired stores. This function will replace the existing list or - * create a new list if it doesn't already exist. To add or append to an existing list, use - * [add] function. If an empty list is passed in, then an [ErrorType.InvalidValue] will be - * generated and the method will return without recording. - * - * @param metricData object with metric settings - * @param value the string list value to record - */ - fun set( - metricData: CommonMetricData, - value: List - ) { - val stringList = value.map { - if (it.length > MAX_STRING_LENGTH) { - recordError( - metricData, - ErrorType.InvalidValue, - "String too long ${it.length} > $MAX_STRING_LENGTH", - logger - ) - } - it.take(MAX_STRING_LENGTH) - } - - if (stringList.count() > MAX_LIST_LENGTH_VALUE) { - recordError( - metricData, - ErrorType.InvalidValue, - "String list length of ${value.count()} exceeds maximum of $MAX_LIST_LENGTH_VALUE", - logger - ) - } - - super.recordMetric(metricData, stringList.take(MAX_LIST_LENGTH_VALUE)) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringsStorageEngine.kt deleted file mode 100644 index aae9622680f..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/StringsStorageEngine.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger - -/** - * This singleton handles the in-memory storage logic for strings. It is meant to be used by - * the Specific Strings API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object StringsStorageEngine : StringsStorageEngineImplementation() - -internal open class StringsStorageEngineImplementation( - override val logger: Logger = Logger("glean/StringsStorageEngine") -) : GenericStorageEngine() { - companion object { - // Maximum length of any passed value string, in characters. - internal const val MAX_LENGTH_VALUE = 100 - } - - override fun deserializeSingleMetric(metricName: String, value: Any?): String? { - return value as? String - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: String, - extraSerializationData: Any? - ) { - userPreferences?.putString(storeName, value) - } - - /** - * Record a string in the desired stores. - * - * @param metricData object with metric settings - * @param value the string value to record - */ - fun record( - metricData: CommonMetricData, - value: String - ) { - val truncatedValue = value.let { - if (it.length > MAX_LENGTH_VALUE) { - recordError( - metricData, - ErrorType.InvalidValue, - "Value length ${it.length} exceeds maximum of $MAX_LENGTH_VALUE", - logger - ) - return@let it.substring(0, MAX_LENGTH_VALUE) - } - it - } - - super.recordMetric(metricData, truncatedValue) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimespansStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimespansStorageEngine.kt deleted file mode 100644 index f9d5b8e0831..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimespansStorageEngine.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.utils.getAdjustedTime - -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONArray -import org.json.JSONObject - -/** - * This singleton handles the in-memory storage logic for timespans. It is meant to be used by - * the Specific Timespan API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object TimespansStorageEngine : TimespansStorageEngineImplementation() - -internal open class TimespansStorageEngineImplementation( - override val logger: Logger = Logger("glean/TimespansStorageEngine") -) : GenericStorageEngine() { - - /** - * An internal map to keep track of the desired time units for the recorded timespans. - * We need this in order to get a snapshot of the data, with the right time unit, - * later on. - */ - private val timeUnitsMap = mutableMapOf() - - override fun deserializeSingleMetric(metricName: String, value: Any?): Long? { - val jsonArray = (value as? String)?.let { - return@let try { - JSONArray(it) - } catch (e: org.json.JSONException) { - null - } - } - - // In order to perform timeunit conversion when taking a snapshot, we persisted - // the desired time unit together with the raw values. We unpersist the first element - // in the array as the time unit, the second as the raw Long value. - if (jsonArray == null || jsonArray.length() != 2) { - logger.error("Unexpected format found when deserializing $metricName") - return null - } - - return try { - val timeUnit = jsonArray.getInt(0) - val rawValue = jsonArray.getLong(1) - // If nothing threw, make sure our time unit is within the enum's range - // and finally set/return the values. - TimeUnit.values().getOrNull(timeUnit)?.let { - timeUnitsMap[metricName] = it - rawValue - } - } catch (e: org.json.JSONException) { - null - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: Long, - extraSerializationData: Any? - ) { - // To support converting to the desired time unit when taking a snapshot, we need a way - // to know the time unit for timespans that are loaded off the disk, for user lifetime. - // To do that, instead of simply persisting a Long, we instead persist a JSONArray. The - // first item in this array is the the time unit, the second is the long value. - - // We expect to have received the time unit as extraSerializationData. There's - // no point in persisting if we didn't. - if (extraSerializationData == null || - extraSerializationData !is TimeUnit) { - logger.error("Unexpected or missing extra data for time unit serialization") - return - } - - val tuple = JSONArray() - tuple.put(extraSerializationData.ordinal) - tuple.put(value) - userPreferences?.putString(storeName, tuple.toString()) - } - - /** - * Set the elapsed time explicitly. - * - * @param metricData the metric information for the timespan - * @param timeUnit the time unit we want the data in when snapshotting - * @param elapsedNanos the time to record, in nanoseconds - */ - @Synchronized - fun set( - metricData: CommonMetricData, - timeUnit: TimeUnit, - elapsedNanos: Long - ) { - // Look for the start time: if it's there, commit the timespan. - val timespanName = metricData.identifier - - // Store the time unit: we'll need it when snapshotting. - timeUnitsMap[timespanName] = timeUnit - - super.recordMetric(metricData, elapsedNanos, timeUnit) { oldValue, newValue -> - oldValue?.let { - // Report an error if we attempt to set a value and we already - // have one. - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Timespan value already recorded. New value discarded.", - logger - ) - // Do not overwrite the old value. - it - } ?: newValue - } - } - - /** - * Get a snapshot of the stored timespans and adjust it to the desired time units. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clear the requested store. Not that only - * metrics stored with a lifetime of [Lifetime.Ping] will be cleared. - * - * @return the [Long] recorded in the requested store - */ - @Synchronized - internal fun getSnapshotWithTimeUnit(storeName: String, clearStore: Boolean): Map>? { - val adjustedData = super.getSnapshot(storeName, clearStore) - ?.mapValuesTo(mutableMapOf>()) { - // Convert to the expected time unit. - if (it.key !in timeUnitsMap) { - logger.error("Can't find the time unit for ${it.key}. Reporting raw value.") - } - - timeUnitsMap[it.key]?.let { timeUnit -> - Pair(timeUnit.name.toLowerCase(), getAdjustedTime(timeUnit, it.value)) - } ?: Pair("unknown", it.value) - } - - // Clear the time unit map if needed: we need to check all the stores - // for all the lifetimes. - if (clearStore) { - // Get a list of the metrics that are still stored. We'll drop the time units for all the - // metrics that are not in this set. - val unclearedMetricNames = - dataStores.flatMap { lifetime -> lifetime.entries }.flatMap { it -> it.value.keys }.toSet() - - timeUnitsMap.keys.retainAll { it in unclearedMetricNames } - } - - return adjustedData - } - - /** - * Get a snapshot of the stored data as a JSON object, including - * the time_unit for each field. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the [JSONObject] containing the recorded data. - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshotWithTimeUnit(storeName, clearStore)?.let { dataMap -> - val data = dataMap.mapValuesTo(mutableMapOf()) { - JSONObject(mapOf( - "time_unit" to it.value.first, - "value" to it.value.second - )) - } - return JSONObject(data as MutableMap<*, *>) - } - } - - /** - * Test-only method used to clear the timespans stores. - */ - override fun clearAllStores() { - super.clearAllStores() - timeUnitsMap.clear() - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngine.kt deleted file mode 100644 index 73aa8c2979a..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngine.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.SharedPreferences -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.utils.timeToNanos - -import mozilla.components.support.base.log.logger.Logger -import org.json.JSONObject - -/** - * This singleton handles the in-memory storage logic for timing distributions. It is meant to be - * used by the Timing Distribution API and the ping assembling objects. - */ -internal object TimingDistributionsStorageEngine : TimingDistributionsStorageEngineImplementation() - -internal open class TimingDistributionsStorageEngineImplementation( - override val logger: Logger = Logger("glean/TimingDistributionsStorageEngine") -) : GenericStorageEngine() { - - companion object { - // The base of the logarithm used to determine bucketing - internal const val LOG_BASE = 2.0 - - // The buckets per each order of magnitude of the logarithm. - internal const val BUCKETS_PER_MAGNITUDE = 8.0 - - // Maximum time of 10 minutes in nanoseconds. This maximum means we - // retain a maximum of 313 buckets. - internal const val MAX_SAMPLE_TIME: Long = 1000L * 1000L * 1000L * 60L * 10L - } - - override fun deserializeSingleMetric(metricName: String, value: Any?): FunctionalHistogram? { - return try { - (value as? String)?.let { - FunctionalHistogram.fromJsonString(it) - } - } catch (e: org.json.JSONException) { - null - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: FunctionalHistogram, - extraSerializationData: Any? - ) { - val json = value.toJsonObject() - userPreferences?.putString(storeName, json.toString()) - } - - /** - * Accumulate value for the provided metric. - * - * Samples greater than 10 minutes in length are truncated to 10 minutes. - * - * @param metricData the metric information for the timing distribution - * @param sample the value to accumulate, in nanoseconds - */ - @Synchronized - fun accumulate( - metricData: CommonMetricData, - sample: Long - ) { - accumulateSamples(metricData, longArrayOf(sample)) - } - - /** - * Accumulate an array of samples for the provided metric. - * - * Samples greater than 10 minutes in length are truncated to 10 minutes. - * - * @param metricData the metric information for the timing distribution - * @param samples the values to accumulate, in the given `timeUnit` - * @param timeUnit the unit that the given samples are in, defaults to nanoseconds - */ - @Suppress("ComplexMethod") - @Synchronized - fun accumulateSamples( - metricData: CommonMetricData, - samples: LongArray, - timeUnit: TimeUnit = TimeUnit.Nanosecond - ) { - // Remove invalid samples, and convert to nanos - var numTooLongSamples = 0 - var numNegativeSamples = 0 - var factor = timeToNanos(timeUnit, 1) - val validSamples = samples.map { sample -> - if (sample < 0) { - numNegativeSamples += 1 - 0 - } else { - val sampleInNanos = sample * factor - if (sampleInNanos > MAX_SAMPLE_TIME) { - numTooLongSamples += 1 - MAX_SAMPLE_TIME - } else { - sampleInNanos - } - } - } - - if (numNegativeSamples > 0) { - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Accumulate $numNegativeSamples negative samples", - logger, - numNegativeSamples - ) - // Negative samples indicate a serious and unexpected error, so don't record anything - return - } - - if (numTooLongSamples > 0) { - ErrorRecording.recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Accumulate $numTooLongSamples samples longer than 10 minutes", - logger, - numTooLongSamples - ) - // Too long samples should just be truncated, but otherwise we record and handle them - } - - val dummy = FunctionalHistogram(LOG_BASE, BUCKETS_PER_MAGNITUDE) - validSamples.forEach { sample -> - super.recordMetric(metricData, dummy, null) { currentValue, _ -> - currentValue?.let { - it.accumulate(sample) - it - } ?: let { - val newTD = FunctionalHistogram(LOG_BASE, BUCKETS_PER_MAGNITUDE) - newTD.accumulate(sample) - return@let newTD - } - } - } - } - - /** - * Get a snapshot of the stored data as a JSON object. - * - * @param storeName the name of the desired store - * @param clearStore whether or not to clearStore the requested store - * - * @return the [JSONObject] containing the recorded data. - */ - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - return getSnapshot(storeName, clearStore)?.let { dataMap -> - val jsonObj = JSONObject() - dataMap.forEach { - jsonObj.put(it.key, it.value.toJsonPayloadObject()) - } - return jsonObj - } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/UuidsStorageEngine.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/storages/UuidsStorageEngine.kt deleted file mode 100644 index c667bf5b215..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/storages/UuidsStorageEngine.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger -import java.util.UUID - -/** - * This singleton handles the in-memory storage logic for uuids. It is meant to be used by - * the Specific UUID API and the ping assembling objects. - * - * This class contains a reference to the Android application Context. While the IDE warns - * us that this could leak, the application context lives as long as the application and this - * object. For this reason, we should be safe to suppress the IDE warning. - */ -@SuppressLint("StaticFieldLeak") -internal object UuidsStorageEngine : UuidsStorageEngineImplementation() - -internal open class UuidsStorageEngineImplementation( - override val logger: Logger = Logger("glean/UuidsStorageEngine") -) : GenericStorageEngine() { - - override fun deserializeSingleMetric(metricName: String, value: Any?): UUID? { - return try { - if (value is String) UUID.fromString(value) else null - } catch (e: IllegalArgumentException) { - null - } - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: UUID, - extraSerializationData: Any? - ) { - userPreferences?.putString(storeName, value.toString()) - } - - /** - * Record a uuid in the desired stores. - * - * @param metricData object with metric settings - * @param value the uuid value to record - */ - fun record( - metricData: CommonMetricData, - value: UUID - ) { - super.recordMetric(metricData, value) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt index bdf7f3d61a9..ba09ac94df1 100644 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt @@ -4,11 +4,6 @@ package mozilla.components.service.glean.testing -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.Glean -import org.junit.rules.TestWatcher -import org.junit.runner.Description - /** * This implements a JUnit rule for writing tests for Glean SDK metrics. * @@ -29,11 +24,4 @@ import org.junit.runner.Description * * @param localPort the port of the local ping server */ -@VisibleForTesting(otherwise = VisibleForTesting.NONE) -class GleanTestLocalServer( - private val localPort: Int -) : TestWatcher() { - override fun starting(description: Description?) { - Glean.testSetLocalEndpoint(localPort) - } -} +typealias GleanTestLocalServer = mozilla.telemetry.glean.testing.GleanTestLocalServer diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt index 9e32e06907c..91d20fad701 100644 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt +++ b/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt @@ -4,45 +4,4 @@ package mozilla.components.service.glean.testing -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.work.testing.WorkManagerTestInitHelper -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.config.Configuration -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -/** - * This implements a JUnit rule for writing tests for Glean SDK metrics. - * - * The rule takes care of resetting the Glean SDK between tests and - * initializing all the required dependencies. - * - * Example usage: - * - * ``` - * // Add the following lines to you test class. - * @get:Rule - * val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - * ``` - * - * @param context the application context - * @param configToUse an optional [Configuration] to initialize the Glean SDK with - */ -@VisibleForTesting(otherwise = VisibleForTesting.NONE) -class GleanTestRule( - val context: Context, - val configToUse: Configuration = Configuration() -) : TestWatcher() { - override fun starting(description: Description?) { - // We're using the WorkManager in a bunch of places, and Glean will crash - // in tests without this line. Let's simply put it here. - WorkManagerTestInitHelper.initializeTestWorkManager(context) - - Glean.resetGlean( - context = context, - config = configToUse, - clearStores = true - ) - } -} +typealias GleanTestRule = mozilla.telemetry.glean.testing.GleanTestRule diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/timing/TimingManager.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/timing/TimingManager.kt deleted file mode 100644 index 7773910dd8a..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/timing/TimingManager.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.timing - -import android.os.SystemClock -import androidx.annotation.VisibleForTesting -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.error.ErrorRecording.recordError -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger -import java.util.WeakHashMap - -/** - * An opaque type that represents a Glean timer identifier. - */ -typealias GleanTimerId = Long - -/** - * A class to record timing intervals associated with a given arbitrary [GleanTimerId]. - * - * A timings are recorded and returned in nanoseconds. - */ -internal object TimingManager { - private val logger = Logger("glean/TimingManager") - - /** - * A monotonically increasing counter that represents a timer id. - */ - private var timerIdCounter: GleanTimerId = 0L - - /** - * A map that stores the start times of running timers. - * - * A [WeakHashMap] is used so that we don't unintentionally leak memory - * by keeping references to otherwise destroyed objects around. - */ - private val uncommittedStartTimes = mutableMapOf>() - - /** - * Helper function used for getting the elapsed time, since the process - * started, using a monotonic clock. - * We need to have this as an helper so that we can override it in tests. - * - * @return the time, in nanoseconds, since the process started. - */ - internal var getElapsedNanos = { SystemClock.elapsedRealtimeNanos() } - - /** - * Start tracking time associated with the provided [GleanTimerId]. This records an - * error if it’s already tracking time (i.e. start was already called with - * no corresponding [stop]): in that case, the original start time will be - * preserved. - * - * @param metricData The metric managing the timing. Used for error reporting. - * @return a [GleanTimerId] representing the id to associate with this timing. - */ - fun start(metricData: CommonMetricData): GleanTimerId? { - val startTime = getElapsedNanos() - - var timerId: GleanTimerId - - synchronized(this) { - timerId = timerIdCounter - timerIdCounter += 1 - - val metricName = metricData.identifier - uncommittedStartTimes[metricName]?.let { metricTimings -> - if (timerId in metricTimings) { - recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Timing operation already started", - logger - ) - return null - } - } - - uncommittedStartTimes.getOrPut(metricName, { mutableMapOf() })[timerId] = startTime - } - - return timerId - } - - /** - * Stop tracking time for the associated [GleanTimerId]. This will record - * an error if no [start] was called. - * - * @param metricData The metric managing the timing. Used for error reporting. - * @param timerId The id to associate with this timing. - * @return The length of the timespan, in nanoseconds, or null if called on a stopped timer. - */ - fun stop(metricData: CommonMetricData, timerId: GleanTimerId): Long? { - val stopTime = getElapsedNanos() - - return synchronized(this) { - val metricName = metricData.identifier - uncommittedStartTimes[metricName]?.remove(timerId)?.let { startTime -> - stopTime - startTime - } ?: run { - recordError( - metricData, - ErrorRecording.ErrorType.InvalidValue, - "Timespan not running", - logger - ) - null - } - } - } - - /** - * Abort a previous [start] call. No error is recorded if no [start] was called. - * - * @param timerId The id to associate with this timing. - */ - fun cancel(metricData: CommonMetricData, timerId: GleanTimerId) { - synchronized(this) { - val metricName = metricData.identifier - uncommittedStartTimes[metricName]?.remove(timerId) - } - } - - /** - * Reset the source of timing back to the default after it's been overridden. Use - * for testing only. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - internal fun testResetTimeSource() { - getElapsedNanos = { SystemClock.elapsedRealtimeNanos() } - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/DateUtils.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/utils/DateUtils.kt deleted file mode 100644 index cc36bf058ab..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/DateUtils.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.utils - -import java.lang.StringBuilder -import java.text.SimpleDateFormat -import mozilla.components.service.glean.private.TimeUnit -import java.util.Calendar -import java.util.Date -import java.util.Locale - -@Suppress("TopLevelPropertyNaming") -internal val DATE_FORMAT_PATTERNS = mapOf( - TimeUnit.Nanosecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ", - TimeUnit.Microsecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ", - TimeUnit.Millisecond to "yyyy-MM-dd'T'HH:mm:ss.SSSZ", - TimeUnit.Second to "yyyy-MM-dd'T'HH:mm:ssZ", - TimeUnit.Minute to "yyyy-MM-dd'T'HH:mmZ", - TimeUnit.Hour to "yyyy-MM-dd'T'HHZ", - TimeUnit.Day to "yyyy-MM-ddZ" -) - -@Suppress("TopLevelPropertyNaming") -internal val DATE_FORMAT_PATTERN_VALUES = DATE_FORMAT_PATTERNS.values.toSet() - -/** - * Generate an ISO8601 compliant time string for the given time. - * - * @param date the [Date] object to convert to string - * @param truncateTo The TimeUnit to truncate the value to - * @return a string containing the date, time and timezone offset - */ -internal fun getISOTimeString( - date: Date = Date(), - truncateTo: TimeUnit = TimeUnit.Minute -): String { - val cal = Calendar.getInstance() - cal.setTime(date) - return getISOTimeString(cal, truncateTo) -} - -/** - * Generate an ISO8601 compliant time string for the given time. - * - * @param calendar the [Calendar] object to convert to string - * @param truncateTo The TimeUnit to truncate the value to - * @return a string containing the date, time and timezone offset - */ -internal fun getISOTimeString( - calendar: Calendar, - truncateTo: TimeUnit = TimeUnit.Minute -): String { - val dateFormat = SimpleDateFormat(DATE_FORMAT_PATTERNS[truncateTo], Locale.US) - dateFormat.setTimeZone(calendar.getTimeZone()) - val timeString = StringBuilder(dateFormat.format(calendar.getTime())) - - // Due to limitations of SDK version 21, there isn't a way to properly output the time - // offset with a ':' character: - // 2018-12-19T12:36:00-0600 -- This is what we get - // 2018-12-19T12:36:00-06:00 -- This is what GCP will expect - // - // In order to satisfy time offset requirements of GCP, we manually insert the ":" - timeString.insert(timeString.length - 2, ":") - - return timeString.toString() -} - -/** - * Parses the subset of ISO8601 datetime strings generated by [getISOTimeString]. - * - * Always returns the result in the device's current timezone offset, regardless of the - * timezone offset specified in the string. - * - * @param date a [String] representing an ISO date string generated by [getISOTimeString] - * @return a [Date] object representation of the provided string - */ -@Suppress("MagicNumber") -internal fun parseISOTimeString(date: String): Date? { - // Due to limitations of SDK version 21, there isn't a way to properly parse the time - // offset with a ':' character: - // 2018-12-19T12:36:00-06:00 -- This is what we store - // 2018-12-19T12:36:00-0600 -- This is what SimpleDateFormat will expect - - val correctedDate = if (date.get(date.length - 3) == ':') { - date.substring(0, date.length - 3) + date.substring(date.length - 2) - } else { - date - } - - for (format in DATE_FORMAT_PATTERN_VALUES) { - val dateFormat = SimpleDateFormat(format, Locale.US) - try { - return dateFormat.parse(correctedDate) - } catch (e: java.text.ParseException) { - continue - } - } - - return null -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/FileUtils.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/utils/FileUtils.kt deleted file mode 100644 index 0ad8a9acc0f..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/FileUtils.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.utils - -import mozilla.components.support.base.log.logger.Logger -import java.io.File - -private val logger: Logger = Logger("glean/FileUtils") - -/** - * Helper function to determine if the directory exists and attempts to create it if - * it doesn't - * - * @param directory File representing the directory path - */ -internal fun ensureDirectoryExists(directory: File) { - if (!directory.exists() && !directory.mkdirs()) { - logger.error("Directory doesn't exist and can't be created: " + directory.absolutePath) - } - - if (!directory.isDirectory || !directory.canWrite()) { - logger.error("Directory is not writable directory: " + directory.absolutePath) - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/LocaleUtils.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/utils/LocaleUtils.kt deleted file mode 100644 index 247d8252817..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/LocaleUtils.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.utils - -import java.util.Locale - -/** - * Gets a gecko-compatible locale string (e.g. "es-ES" instead of Java [Locale] - * "es_ES") for the default locale. - * - * This method approximates the API21 method [Locale.toLanguageTag]. - * - * @return a locale string that supports custom injected locale/languages. - */ -internal fun getLocaleTag(): String { - // Thanks to toLanguageTag() being introduced in API21, we could have - // simple returned `locale.toLanguageTag();` from this function. However - // what kind of languages the Android build supports is up to the manufacturer - // and our apps usually support translations for more rare languages, through - // our custom locale injector. For this reason, we can't use `toLanguageTag` - // and must try to replicate its logic ourselves. - val locale = Locale.getDefault() - val language = getLanguageFromLocale(locale) - val country = locale.country // Can be an empty string. - return if (country.isEmpty()) language else "$language-$country" -} - -/** - * Sometimes we want just the language for a locale, not the entire language - * tag. But Java's .getLanguage method is wrong. A reference to the deprecated - * ISO language codes and their mapping can be found in [Locale.toLanguageTag] docs. - * - * @param locale a [Locale] object to be stringified. - * @return a language string, such as "he" for the Hebrew locales. - */ -internal fun getLanguageFromLocale(locale: Locale): String { - // Can, but should never be, an empty string. - val language = locale.language - - // Modernize certain language codes. - return when (language) { - "iw" -> "he" - "in" -> "id" - "ji" -> "yi" - else -> language - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/MemoryUtils.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/utils/MemoryUtils.kt deleted file mode 100644 index 974e0227efe..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/MemoryUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.utils - -import mozilla.components.service.glean.private.MemoryUnit - -/** - * Convenience method to convert a memory size in a different unit to bytes. - * - * @param memoryUnit the [MemoryUnit] the value is in - * @param value a memory size in the given unit - * - * @return the memory size, in bytes - */ -@Suppress("MagicNumber") -internal fun memoryToBytes(memoryUnit: MemoryUnit, value: Long): Long { - return when (memoryUnit) { - MemoryUnit.Byte -> value - MemoryUnit.Kilobyte -> value shl 10 - MemoryUnit.Megabyte -> value shl 20 - MemoryUnit.Gigabyte -> value shl 30 - } -} diff --git a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/TimeUtils.kt b/components/service/glean/src/main/java/mozilla/components/service/glean/utils/TimeUtils.kt deleted file mode 100644 index f67bdaa8d74..00000000000 --- a/components/service/glean/src/main/java/mozilla/components/service/glean/utils/TimeUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.utils - -import mozilla.components.service.glean.private.TimeUnit -import java.util.concurrent.TimeUnit as AndroidTimeUnit - -/** - * Convenience method to get a time in nanoseconds in a different, supported time unit. - * - * @param timeUnit the required time unit, one in [TimeUnit] - * @param elapsedNanos a time in nanoseconds - * - * @return the time in the desired time unit - */ -internal fun getAdjustedTime(timeUnit: TimeUnit, elapsedNanos: Long): Long { - return when (timeUnit) { - TimeUnit.Nanosecond -> elapsedNanos - TimeUnit.Microsecond -> AndroidTimeUnit.NANOSECONDS.toMicros(elapsedNanos) - TimeUnit.Millisecond -> AndroidTimeUnit.NANOSECONDS.toMillis(elapsedNanos) - TimeUnit.Second -> AndroidTimeUnit.NANOSECONDS.toSeconds(elapsedNanos) - TimeUnit.Minute -> AndroidTimeUnit.NANOSECONDS.toMinutes(elapsedNanos) - TimeUnit.Hour -> AndroidTimeUnit.NANOSECONDS.toHours(elapsedNanos) - TimeUnit.Day -> AndroidTimeUnit.NANOSECONDS.toDays(elapsedNanos) - } -} - -/** - * Convenience method to get a time in a different unit to nanoseconds. - * - * @param timeUnit the unit the value is in - * @param value a time in the given unit - * - * @return the time, in nanoseconds - */ -internal fun timeToNanos(timeUnit: TimeUnit, value: Long): Long { - return when (timeUnit) { - TimeUnit.Nanosecond -> value - TimeUnit.Microsecond -> AndroidTimeUnit.MICROSECONDS.toNanos(value) - TimeUnit.Millisecond -> AndroidTimeUnit.MILLISECONDS.toNanos(value) - TimeUnit.Second -> AndroidTimeUnit.SECONDS.toNanos(value) - TimeUnit.Minute -> AndroidTimeUnit.MINUTES.toNanos(value) - TimeUnit.Hour -> AndroidTimeUnit.HOURS.toNanos(value) - TimeUnit.Day -> AndroidTimeUnit.DAYS.toNanos(value) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/DispatchersTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/DispatchersTest.kt deleted file mode 100644 index 3265080f457..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/DispatchersTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean - -import kotlinx.coroutines.runBlocking -import org.junit.Test - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotSame -import org.junit.Assert.assertSame - -@Suppress("EXPERIMENTAL_API_USAGE") -class DispatchersTest { - - @Test - fun `API scope runs off the main thread`() { - val mainThread = Thread.currentThread() - var threadCanary = false - Dispatchers.API.setTestingMode(false) - Dispatchers.API.setTaskQueueing(false) - - runBlocking { - Dispatchers.API.launch { - assertNotSame(mainThread, Thread.currentThread()) - // Use the canary bool to make sure this is getting called before - // the test completes. - assertEquals(false, threadCanary) - threadCanary = true - }!!.join() - } - - Dispatchers.API.setTestingMode(true) - assertEquals(true, threadCanary) - assertSame(mainThread, Thread.currentThread()) - } - - @Test - fun `launch() correctly adds tests to queue if queueTasks is true`() { - var threadCanary = 0 - - Dispatchers.API.setTestingMode(true) - Dispatchers.API.setTaskQueueing(true) - - // Add 3 tasks to queue each one setting threadCanary to true to indicate if any task has ran - repeat(3) { - Dispatchers.API.launch { - threadCanary += 1 - } - } - - assertEquals("Task queue contains the correct number of tasks", - 3, Dispatchers.API.taskQueue.size) - assertEquals("Tasks have not run while in queue", 0, threadCanary) - - // Now trigger execution to ensure the tasks fired - Dispatchers.API.flushQueuedInitialTasks() - - assertEquals("Tasks have executed", 3, threadCanary) - assertEquals("Task queue is cleared", 0, Dispatchers.API.taskQueue.size) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt index 72c4d45711d..4b2583a7ab1 100644 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt +++ b/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt @@ -5,65 +5,18 @@ package mozilla.components.service.glean import android.content.Context -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.Dispatchers as KotlinDispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import kotlinx.coroutines.runBlocking -import mozilla.components.service.glean.GleanMetrics.GleanInternalMetrics -import mozilla.components.service.glean.GleanMetrics.Pings -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.private.CounterMetricType -import mozilla.components.service.glean.private.DatetimeMetricType -import mozilla.components.service.glean.private.EventMetricType +import mozilla.components.service.glean.private.BooleanMetricType import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.NoExtraKeys -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.private.StringMetricType -import mozilla.components.service.glean.private.UuidMetricType -import mozilla.components.service.glean.scheduler.GleanLifecycleObserver -import mozilla.components.service.glean.scheduler.MetricsPingWorker -import mozilla.components.service.glean.scheduler.PingUploadWorker -import mozilla.components.service.glean.storages.StorageEngineManager -import mozilla.components.service.glean.storages.StringsStorageEngine import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.utils.getLanguageFromLocale -import mozilla.components.service.glean.utils.getLocaleTag -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy import org.robolectric.RobolectricTestRunner -import java.io.BufferedReader -import java.io.File -import java.io.FileReader -import java.time.Instant -import java.util.Calendar -import java.util.Date -import java.util.Locale -import java.util.UUID -import java.util.concurrent.TimeUnit -import mozilla.components.service.glean.private.TimeUnit as GleanTimeUnit -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class GleanTest { - private val context: Context get() = ApplicationProvider.getApplicationContext() @@ -71,627 +24,18 @@ class GleanTest { val gleanRule = GleanTestRule(context) @Test - fun `disabling upload should disable metrics recording`() { - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("store1") - ) - Glean.setUploadEnabled(false) - assertEquals(false, Glean.getUploadEnabled()) - stringMetric.set("foo") - assertNull( - "Metrics should not be recorded if Glean is disabled", - StringsStorageEngine.getSnapshot(storeName = "store1", clearStore = false) - ) - } - - @Test - fun `test path generation`() { - val uuid = UUID.randomUUID() - val path = Glean.makePath("test", uuid) - val applicationId = "mozilla-components-service-glean" - // Make sure that the default applicationId matches the package name. - assertEquals(applicationId, Glean.applicationId) - assertEquals(path, "/submit/$applicationId/test/${Glean.SCHEMA_VERSION}/$uuid") - } - - @Test - fun `test experiments recording`() { - Glean.setExperimentActive( - "experiment_test", "branch_a" - ) - Glean.setExperimentActive( - "experiment_api", "branch_b", - mapOf("test_key" to "value") - ) - assertTrue(Glean.testIsExperimentActive("experiment_api")) - assertTrue(Glean.testIsExperimentActive("experiment_test")) - - Glean.setExperimentInactive("experiment_test") - - assertTrue(Glean.testIsExperimentActive("experiment_api")) - assertFalse(Glean.testIsExperimentActive("experiment_test")) - - val storedData = Glean.testGetExperimentData("experiment_api") - assertEquals("branch_b", storedData.branch) - assertEquals(1, storedData.extra?.size) - assertEquals("value", storedData.extra?.getValue("test_key")) - } - - @Test - fun `test sending of background pings`() { - val server = getMockWebServer() - - val click = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("events") - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - // Fake calling the lifecycle observer. - val lifecycleOwner = mock(LifecycleOwner::class.java) - val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) - val gleanLifecycleObserver = GleanLifecycleObserver() - lifecycleRegistry.addObserver(gleanLifecycleObserver) - - try { - // Simulate the first foreground event after the application starts. - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) - click.record() - - // Simulate going to background. - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - val requests = mutableMapOf() - for (i in 0..1) { - val request = server.takeRequest(20L, TimeUnit.SECONDS) - val docType = request.path.split("/")[3] - requests[docType] = request.body.readUtf8() - } - - val eventsJson = JSONObject(requests["events"]!!) - checkPingSchema(eventsJson) - assertEquals("events", eventsJson.getJSONObject("ping_info")["ping_type"]) - assertEquals(1, eventsJson.getJSONArray("events").length()) - - val baselineJson = JSONObject(requests["baseline"]!!) - assertEquals("baseline", baselineJson.getJSONObject("ping_info")["ping_type"]) - checkPingSchema(baselineJson) - - val baselineMetricsObject = baselineJson.getJSONObject("metrics") - val baselineStringMetrics = baselineMetricsObject.getJSONObject("string") - assertEquals(1, baselineStringMetrics.length()) - assertNotNull(baselineStringMetrics.get("glean.baseline.locale")) - - val baselineTimespanMetrics = baselineMetricsObject.getJSONObject("timespan") - assertEquals(1, baselineTimespanMetrics.length()) - assertNotNull(baselineTimespanMetrics.get("glean.baseline.duration")) - } finally { - server.shutdown() - lifecycleRegistry.removeObserver(gleanLifecycleObserver) - } - } - - @Test - fun `initialize() must not crash the app if Glean's data dir is messed up`() { - // Remove the Glean's data directory. - val gleanDir = File( - ApplicationProvider.getApplicationContext().applicationInfo.dataDir, - Glean.GLEAN_DATA_DIR - ) - assertTrue(gleanDir.deleteRecursively()) - - // Create a file in its place. - assertTrue(gleanDir.createNewFile()) - - resetGlean() - - // Clean up after this, so that other tests don't fail. - assertTrue(gleanDir.delete()) - } - - @Test - fun `Don't send metrics if not initialized`() { - val stringMetric = StringMetricType( + fun `Glean correctly initializes and records a metric`() { + // Define a 'booleanMetric' boolean metric, which will be stored in "store1" + val booleanMetric = BooleanMetricType( disabled = false, category = "telemetry", lifetime = Lifetime.Application, - name = "string_metric", + name = "boolean_metric", sendInPings = listOf("store1") ) - Glean.initialized = false - Dispatchers.API.testingMode = false - stringMetric.set("foo") - assertNull( - "Metrics should not be recorded if Glean is not initialized", - StringsStorageEngine.getSnapshot(storeName = "store1", clearStore = false) - ) - - Glean.initialized = true - } - - @Test - fun `queued recorded metrics correctly record during init`() { - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // First set to a pre-init state - Glean.initialized = false - Dispatchers.API.setTaskQueueing(true) - - // This will queue 3 tasks that will add to the metric value once Glean is initialized - for (i in 0..2) { - counterMetric.add() - } - - // Ensure that no value has been stored yet since the tasks have only been queued and not - // executed yet - assertFalse("No value must be stored", counterMetric.testHasValue()) - - // Calling resetGlean here will cause Glean to be initialized and should cause the queued - // tasks recording metrics to execute - resetGlean(clearStores = false) - - // Verify that the callback was executed by testing for the correct value - assertTrue("Value must exist", counterMetric.testHasValue()) - assertEquals("Value must match", 3, counterMetric.testGetValue()) - } - - @Test - fun `Initializing twice is a no-op`() { - val beforeConfig = Glean.configuration - - Glean.initialize(ApplicationProvider.getApplicationContext()) - - val afterConfig = Glean.configuration - - assertSame(beforeConfig, afterConfig) - } - - @Test - fun `Don't handle events when uninitialized`() { - val gleanSpy = spy(GleanInternalAPI::class.java) - - gleanSpy.initialized = false - runBlocking { - gleanSpy.handleBackgroundEvent() - } - assertFalse(getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - } - - @Test - fun `Don't schedule pings if metrics disabled`() { - Glean.setUploadEnabled(false) - - runBlocking { - Glean.handleBackgroundEvent() - } - assertFalse(getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - } - - @Test - fun `Don't schedule pings if there is no ping content`() { - resetGlean(getContextWithMockedInfo()) - - runBlocking { - Glean.handleBackgroundEvent() - } - - // We should only have a baseline ping and no events or metrics pings since nothing was - // recorded - val files = Glean.pingStorageEngine.storageDirectory.listFiles()!! - - // Make sure only the baseline ping is present and no events or metrics pings - assertEquals(1, files.count()) - val file = files.first() - BufferedReader(FileReader(file)).use { - val lines = it.readLines() - assert(lines[0].contains("baseline")) - } - } - - @Test - fun `Application id sanitizer must correctly filter undesired characters`() { - assertEquals( - "org-mozilla-test-app", - Glean.sanitizeApplicationId("org.mozilla.test-app") - ) - - assertEquals( - "org-mozilla-test-app", - Glean.sanitizeApplicationId("org.mozilla..test---app") - ) - - assertEquals( - "org-mozilla-test-app", - Glean.sanitizeApplicationId("org-mozilla-test-app") - ) - } - - @Test - fun `The appChannel must be correctly set, if requested`() { - // No appChannel must be set if nothing was provided through the config - // options. - resetGlean(getContextWithMockedInfo(), Configuration()) - assertFalse(GleanInternalMetrics.appChannel.testHasValue()) - - // The appChannel must be correctly reported if a channel value - // was provided. - val testChannelName = "my-test-channel" - resetGlean(getContextWithMockedInfo(), Configuration(channel = testChannelName)) - assertTrue(GleanInternalMetrics.appChannel.testHasValue()) - assertEquals(testChannelName, GleanInternalMetrics.appChannel.testGetValue()) - } - - @Test - fun `client_id and first_run_date metrics should be copied from the old location`() { - // 1539480 BACKWARD COMPATIBILITY HACK - - // The resetGlean called right before this function will add client_id - // and first_run_date to the new location in glean_client_info. We - // need to clear those out again so we can test what happens when they - // are missing. - StorageEngineManager( - applicationContext = ApplicationProvider.getApplicationContext() - ).clearAllStores() - - val clientIdMetric = UuidMetricType( - disabled = false, - category = "", - name = "client_id", - lifetime = Lifetime.User, - sendInPings = listOf("glean_ping_info") - ) - val clientIdValue = clientIdMetric.generateAndSet() - - val firstRunDateMetric = DatetimeMetricType( - disabled = false, - category = "", - name = "first_run_date", - lifetime = Lifetime.User, - sendInPings = listOf("glean_ping_info"), - timeUnit = GleanTimeUnit.Day - ) - firstRunDateMetric.set() - - assertFalse(GleanInternalMetrics.clientId.testHasValue()) - assertFalse(GleanInternalMetrics.firstRunDate.testHasValue()) - - // This should copy the values to their new locations - Glean.initialized = false - Glean.initialize(ApplicationProvider.getApplicationContext()) - - assertEquals(clientIdValue, GleanInternalMetrics.clientId.testGetValue()) - assertTrue(GleanInternalMetrics.firstRunDate.testHasValue()) - } - - @Test - fun `client_id and first_run_date metrics should not override new location`() { - // 1539480 BACKWARD COMPATIBILITY HACK - - // The resetGlean called right before this function will add client_id - // and first_run_date to the new location in glean_client_info. - // In this case we want to keep those and confirm that any old values - // won't override the new ones. - - val clientIdMetric = UuidMetricType( - disabled = false, - category = "", - name = "client_id", - lifetime = Lifetime.User, - sendInPings = listOf("glean_ping_info") - ) - val clientIdValue = clientIdMetric.generateAndSet() - - val firstRunDateMetric = DatetimeMetricType( - disabled = false, - category = "", - name = "first_run_date", - lifetime = Lifetime.User, - sendInPings = listOf("glean_ping_info"), - timeUnit = GleanTimeUnit.Day - ) - firstRunDateMetric.set(Date.from( - Instant.parse("2200-01-01T00:00:00.00Z"))) - - assertTrue(GleanInternalMetrics.clientId.testHasValue()) - assertTrue(GleanInternalMetrics.firstRunDate.testHasValue()) - - // This should copy the values to their new locations - Glean.initialized = false - Glean.initialize(ApplicationProvider.getApplicationContext()) - - assertNotEquals(clientIdValue, GleanInternalMetrics.clientId.testGetValue()) - assertNotEquals(firstRunDateMetric.testGetValue(), GleanInternalMetrics.firstRunDate.testGetValue()) - } - - @Test - fun `client_id and first_run_date must be generated if not available after the first start`() { - // 1539480 BACKWARD COMPATIBILITY HACK - - // The resetGlean called right before this function will add client_id - // and first_run_date to the new location in glean_client_info. We - // need to clear those out again so we can test what happens when they - // are missing. - StorageEngineManager( - applicationContext = ApplicationProvider.getApplicationContext() - ).clearAllStores() - - assertFalse(GleanInternalMetrics.clientId.testHasValue()) - assertFalse(GleanInternalMetrics.firstRunDate.testHasValue()) - - // This should copy the values to their new locations - Glean.initialized = false - Glean.initialize(ApplicationProvider.getApplicationContext()) - - assertTrue(GleanInternalMetrics.clientId.testHasValue()) - assertTrue(GleanInternalMetrics.firstRunDate.testHasValue()) - } - - @Test - fun `getLanguageTag() reports the tag for the default locale`() { - val defaultLanguageTag = getLocaleTag() - - assertNotNull(defaultLanguageTag) - assertFalse(defaultLanguageTag.isEmpty()) - assertEquals("en-US", defaultLanguageTag) - } - - @Test - fun `getLanguageTag reports the correct tag for a non-default language`() { - val defaultLocale = Locale.getDefault() - - try { - Locale.setDefault(Locale("fy", "NL")) - - val languageTag = getLocaleTag() - - assertNotNull(languageTag) - assertFalse(languageTag.isEmpty()) - assertEquals("fy-NL", languageTag) - } finally { - Locale.setDefault(defaultLocale) - } - } - - @Test - fun `getLanguage reports the modern translation for some languages`() { - assertEquals("he", getLanguageFromLocale(Locale("iw", "IL"))) - assertEquals("id", getLanguageFromLocale(Locale("in", "ID"))) - assertEquals("yi", getLanguageFromLocale(Locale("ji", "ID"))) - } - - @Test - fun `ping collection must happen after currently scheduled metrics recordings`() { - // Given the following block of code: - // - // Metric.A.set("SomeTestValue") - // Glean.sendPings(listOf("custom-ping-1")) - // - // This test ensures that "custom-ping-1" contains "metric.a" with a value of "SomeTestValue" - // when the ping is collected. - - val server = getMockWebServer() - - val pingName = "custom_ping_1" - val ping = PingType( - name = pingName, - includeClientId = true - ) - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = listOf(pingName) - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - // This test relies on testing mode to be disabled, since we need to prove the - // real-world async behaviour of this. We don't need to care about clearing it, - // the test-unit hooks will call `resetGlean` anyway. - Dispatchers.API.setTestingMode(false) - - // This is the important part of the test. Even though both the metrics API and - // ping.send() are async and off the main thread, "SomeTestValue" should be recorded, - // the order of the calls must be preserved. - val testValue = "SomeTestValue" - stringMetric.set(testValue) - ping.send() - - // Trigger worker task to upload the pings in the background. We need - // to wait for the work to be enqueued first, since this test runs - // asynchronously. - waitForEnqueuedWorker(context, PingUploadWorker.PING_WORKER_TAG) - triggerWorkManager(context) - - // Validate the received data. - val request = server.takeRequest(20L, TimeUnit.SECONDS) - val docType = request.path.split("/")[3] - assertEquals(pingName, docType) - - val pingJson = JSONObject(request.body.readUtf8()) - assertEquals(pingName, pingJson.getJSONObject("ping_info")["ping_type"]) - checkPingSchema(pingJson) - - val pingMetricsObject = pingJson.getJSONObject("metrics") - val pingStringMetrics = pingMetricsObject.getJSONObject("string") - assertEquals(1, pingStringMetrics.length()) - assertEquals(testValue, pingStringMetrics.get("telemetry.string_metric")) - } - - @Test - fun `Basic metrics should be cleared when disabling uploading`() { - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = listOf("default") - ) - - stringMetric.set("TEST VALUE") - assertTrue(stringMetric.testHasValue()) - - Glean.setUploadEnabled(false) - assertFalse(stringMetric.testHasValue()) - stringMetric.set("TEST VALUE") - assertFalse(stringMetric.testHasValue()) - - Glean.setUploadEnabled(true) - assertFalse(stringMetric.testHasValue()) - stringMetric.set("TEST VALUE") - assertTrue(stringMetric.testHasValue()) - } - - @Test - fun `Core metrics should be cleared and restored when disabling and enabling uploading`() { - assertTrue(GleanInternalMetrics.os.testHasValue()) - - // Call this a couple of times to make sure it moves to a non-zero value - Glean.pingMaker.getPingSeq("custom") - assertTrue(Glean.pingMaker.getPingSeq("custom") > 0) - - Glean.setUploadEnabled(false) - assertFalse(GleanInternalMetrics.os.testHasValue()) - assertEquals(0, Glean.pingMaker.getPingSeq("custom")) - - Glean.setUploadEnabled(true) - assertTrue(GleanInternalMetrics.os.testHasValue()) - } - - @Test - fun `Workers should be cancelled when disabling uploading`() { - // Force the MetricsPingScheduler to schedule the MetricsPingWorker - Glean.metricsPingScheduler.schedulePingCollection(Calendar.getInstance(), true) - // Enqueue a worker to send the baseline ping - Pings.baseline.send() - - // Verify that the workers are enqueued - assertTrue("PingUploadWorker is enqueued", - getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - assertTrue("MetricsPingWorker is enqueued", - getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - - // Toggle upload enabled to false - Glean.setUploadEnabled(false) - - // Verify workers have been cancelled - assertFalse("PingUploadWorker is not enqueued", - getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - assertFalse("MetricsPingWorker is not enqueued", - getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - } - - @Test - fun `firstRunDate is managed correctly when disabling and enabling metrics`() { - val originalFirstRunDate = GleanInternalMetrics.firstRunDate.testGetValue() - - Glean.setUploadEnabled(false) - assertEquals(originalFirstRunDate, GleanInternalMetrics.firstRunDate.testGetValue()) - - Glean.setUploadEnabled(true) - assertEquals(originalFirstRunDate, GleanInternalMetrics.firstRunDate.testGetValue()) - } - - @Test - fun `disabling upload clears pending pings`() { - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = listOf("default") - ) - - stringMetric.set("TEST VALUE") - - Pings.baseline.send() - assertEquals(1, Glean.pingStorageEngine.testGetNumPendingPings()) - - Glean.setUploadEnabled(false) - assertEquals(0, Glean.pingStorageEngine.testGetNumPendingPings()) - - Glean.setUploadEnabled(true) - assertEquals(0, Glean.pingStorageEngine.testGetNumPendingPings()) - } - - fun `clientId is managed correctly when disabling and enabling metrics`() { - val originalClientId = GleanInternalMetrics.clientId.testGetValue() - assertNotEquals(GleanInternalAPI.KNOWN_CLIENT_ID, GleanInternalMetrics.clientId.testGetValue()) - - Glean.setUploadEnabled(false) - assertEquals(GleanInternalAPI.KNOWN_CLIENT_ID, GleanInternalMetrics.clientId.testGetValue()) - - Glean.setUploadEnabled(true) - assertNotEquals(GleanInternalAPI.KNOWN_CLIENT_ID, GleanInternalMetrics.clientId.testGetValue()) - assertNotEquals(originalClientId, GleanInternalMetrics.clientId.testGetValue()) - } - - @Test - fun `clientId is set to known id when setting disabled from a cold start`() { - // Recreate "cold start" values - Glean.initialized = false - Glean.uploadEnabled = true - - Glean.setUploadEnabled(false) - Glean.initialize(ApplicationProvider.getApplicationContext()) - - assertEquals(GleanInternalAPI.KNOWN_CLIENT_ID, GleanInternalMetrics.clientId.testGetValue()) - } - - @Test - fun `clientId is set to random id when setting enabled from a cold start`() { - // Recreate "cold start" values - Glean.initialized = false - Glean.uploadEnabled = true - - Glean.setUploadEnabled(true) - Glean.initialize(ApplicationProvider.getApplicationContext()) - - assertNotEquals(GleanInternalAPI.KNOWN_CLIENT_ID, GleanInternalMetrics.clientId.testGetValue()) - } - - @Test - fun `calling setUploadEnabled is a no-op`() { - val gleanMock = mock(GleanInternalAPI::class.java) - val context: Context = ApplicationProvider.getApplicationContext() - - gleanMock.initialize(context) - gleanMock.setUploadEnabled(true) - - `when`(gleanMock.onChangeUploadEnabled(anyBoolean())).thenThrow(AssertionError::class.java) - gleanMock.setUploadEnabled(true) - } - @Test(expected = IllegalThreadStateException::class) - fun `Glean initialize must be called on the main thread`() { - runBlocking(KotlinDispatchers.IO) { - val context: Context = ApplicationProvider.getApplicationContext() + booleanMetric.set(true) - Glean.initialize(context) - } + assertTrue(booleanMetric.testGetValue()) } -} +} \ No newline at end of file diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/TestUtil.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/TestUtil.kt deleted file mode 100644 index 5f797d8bfd3..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/TestUtil.kt +++ /dev/null @@ -1,254 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean - -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import androidx.test.core.app.ApplicationProvider -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.testing.WorkManagerTestInitHelper -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import mozilla.components.concept.fetch.Client -import mozilla.components.concept.fetch.Headers -import mozilla.components.concept.fetch.MutableHeaders -import mozilla.components.concept.fetch.Request -import mozilla.components.concept.fetch.Response -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.ping.PingMaker -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.scheduler.PingUploadWorker -import mozilla.components.service.glean.storages.StorageEngineManager -import okhttp3.mockwebserver.Dispatcher -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest -import org.json.JSONObject -import org.junit.Assert -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import java.util.UUID -import java.util.concurrent.ExecutionException - -/** - * Checks ping content against the Glean ping schema. - * - * This uses the Python utility, glean_parser, to perform the actual checking. - * This is installed in its own Miniconda environment as part of the build - * configuration in sdk_generator.gradle. - * - * @param content The JSON content of the ping - * @throws AssertionError If the JSON content is not valid - */ -internal fun checkPingSchema(content: JSONObject) { - val os = System.getProperty("os.name")?.toLowerCase() - val pythonExecutable = - if (os?.indexOf("win")?.compareTo(0) == 0) - "${BuildConfig.GLEAN_MINICONDA_DIR}/python" - else - "${BuildConfig.GLEAN_MINICONDA_DIR}/bin/python" - - val proc = ProcessBuilder( - listOf( - pythonExecutable, - "-m", - "glean_parser", - "check", - "-s", - "${BuildConfig.GLEAN_PING_SCHEMA_URL}" - ) - ).redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - val process = proc.start() - - val jsonString = content.toString() - with(process.outputStream.bufferedWriter()) { - write(jsonString) - newLine() - flush() - close() - } - - val exitCode = process.waitFor() - assert(exitCode == 0) -} - -/** - * Checks ping content against the Glean ping schema. - * - * This uses the Python utility, glean_parser, to perform the actual checking. - * This is installed in its own Miniconda environment as part of the build - * configuration in sdk_generator.gradle. - * - * @param content The JSON content of the ping - * @return the content string, parsed into a JSONObject - * @throws AssertionError If the JSON content is not valid - */ -internal fun checkPingSchema(content: String): JSONObject { - val jsonContent = JSONObject(content) - checkPingSchema(jsonContent) - return jsonContent -} - -/** - * Collects a specified ping type and checks it against the Glean ping schema. - * - * @param ping The ping to check - * @return the ping contents, in a JSONObject - * @throws AssertionError If the JSON content is not valid - */ -internal fun collectAndCheckPingSchema(ping: PingType): JSONObject { - val appContext = ApplicationProvider.getApplicationContext() - val jsonString = PingMaker( - StorageEngineManager(applicationContext = appContext), - appContext - ).collect(ping)!! - return checkPingSchema(jsonString) -} - -/** - * Resets the Glean state and trigger init again. - * - * @param context the application context to init Glean with - * @param config the [Configuration] to init Glean with - * @param clearStores if true, clear the contents of all stores - */ -internal fun resetGlean( - context: Context = ApplicationProvider.getApplicationContext(), - config: Configuration = Configuration(), - clearStores: Boolean = true -) { - // We're using the WorkManager in a bunch of places, and Glean will crash - // in tests without this line. Let's simply put it here. - WorkManagerTestInitHelper.initializeTestWorkManager(context) - Glean.resetGlean(context, config, clearStores) -} - -/** - * Get a context that contains [PackageInfo.versionName] mocked to - * "glean.version.name". - * - * @return an application [Context] that can be used to init Glean - */ -internal fun getContextWithMockedInfo(): Context { - val context = Mockito.spy(ApplicationProvider.getApplicationContext()) - val packageInfo = Mockito.mock(PackageInfo::class.java) - packageInfo.versionName = "glean.version.name" - val packageManager = Mockito.mock(PackageManager::class.java) - Mockito.`when`(packageManager.getPackageInfo(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenReturn(packageInfo) - Mockito.`when`(context.packageManager).thenReturn(packageManager) - return context -} - -/** - * Represents the Worker status returned by [getWorkerStatus] - */ -internal class WorkerStatus(val isEnqueued: Boolean, val workerId: UUID? = null) - -/** - * Helper function to check to see if a worker has been scheduled with the [WorkManager] - * - * @param context the context that will be used to get the [WorkManager] - * @param tag a string representing the worker tag - * @return Pair first contains True if the task found in [WorkManager], False otherwise - * AND second contains the UUID of the running task so its constraints can be met. - */ -internal fun getWorkerStatus(context: Context, tag: String): WorkerStatus { - val instance = WorkManager.getInstance(context) - val statuses = instance.getWorkInfosByTag(tag) - try { - val workInfoList = statuses.get() - for (workInfo in workInfoList) { - val state = workInfo.state - if ((state === WorkInfo.State.RUNNING) || (state === WorkInfo.State.ENQUEUED)) { - return WorkerStatus(true, workInfo.id) - } - } - } catch (e: ExecutionException) { - // Do nothing but will return false - } catch (e: InterruptedException) { - // Do nothing but will return false - } - - return WorkerStatus(false, null) -} - -/** - * Wait for a specifically tagged [WorkManager]'s Worker to be enqueued. - * - * @param context the context that will be used to get the [WorkManager] - * @param workTag the tag of the expected Worker - * @param timeoutMillis how log before stopping the wait. This defaults to 5000ms (5 seconds). - */ -internal fun waitForEnqueuedWorker(context: Context, workTag: String, timeoutMillis: Long = 5000) = runBlocking { - runBlocking { - withTimeout(timeoutMillis) { - do { - if (getWorkerStatus(context, workTag).isEnqueued) { - return@withTimeout - } - } while (true) - } - } -} - -/** - * Helper function to simulate WorkManager being triggered since there appears to be a bug in - * the current WorkManager test utilites that prevent it from being triggered by a test. Once this - * is fixed, the contents of this can be amended to trigger WorkManager directly. - * - * @param context the context that will be used to get the [WorkManager] - */ -internal fun triggerWorkManager(context: Context) { - // Check that the work is scheduled - val status = getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG) - Assert.assertTrue("A scheduled PingUploadWorker must exist", - status.isEnqueued) - - // Trigger WorkManager using TestDriver - val workManagerTestInitHelper = WorkManagerTestInitHelper.getTestDriver(context) - workManagerTestInitHelper?.setAllConstraintsMet(status.workerId!!) -} - -/** - * This is a helper class to facilitate testing of ping tagging - */ -internal class TestPingTagClient( - private val responseUrl: String = Configuration.DEFAULT_TELEMETRY_ENDPOINT, - private val responseStatus: Int = 200, - private val responseHeaders: Headers = MutableHeaders(), - private val responseBody: Response.Body = Response.Body.empty(), - private val debugHeaderValue: String? = null -) : Client() { - override fun fetch(request: Request): Response { - Assert.assertTrue("URL must be redirected for tagged pings", - request.url.startsWith(responseUrl)) - Assert.assertEquals("Debug headers must match what the ping tag was set to", - debugHeaderValue, request.headers!!["X-Debug-ID"]) - - // Have to return a response here. - return Response( - responseUrl, - responseStatus, - request.headers ?: responseHeaders, - responseBody) - } -} - -/** - * Create a mock webserver that accepts all requests. - * @return a [MockWebServer] instance - */ -internal fun getMockWebServer(): MockWebServer { - val server = MockWebServer() - server.setDispatcher(object : Dispatcher() { - override fun dispatch(request: RecordedRequest): MockResponse { - return MockResponse().setBody("OK") - } - }) - return server -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/debug/GleanDebugActivityTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/debug/GleanDebugActivityTest.kt deleted file mode 100644 index 9fcfbf15ab9..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/debug/GleanDebugActivityTest.kt +++ /dev/null @@ -1,249 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.debug - -import android.content.Context -import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.TestPingTagClient -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.getMockWebServer -import mozilla.components.service.glean.net.ConceptFetchHttpUploader -import mozilla.components.service.glean.private.BooleanMetricType -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.resetGlean -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.triggerWorkManager -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.Robolectric -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf -import java.util.concurrent.TimeUnit - -@RunWith(RobolectricTestRunner::class) -class GleanDebugActivityTest { - - private val testPackageName = "mozilla.components.service.glean" - - private val context: Context - get() = ApplicationProvider.getApplicationContext() - - @get:Rule - val gleanRule = GleanTestRule(context) - - @Before - fun setup() { - // This makes sure we have a "launch" intent in our package, otherwise - // it will fail looking for it in `GleanDebugActivityTest`. - val pm = ApplicationProvider.getApplicationContext().packageManager - val launchIntent = Intent(Intent.ACTION_MAIN) - launchIntent.setPackage(testPackageName) - launchIntent.addCategory(Intent.CATEGORY_LAUNCHER) - val resolveInfo = ResolveInfo() - resolveInfo.activityInfo = ActivityInfo() - resolveInfo.activityInfo.packageName = testPackageName - resolveInfo.activityInfo.name = "LauncherActivity" - @Suppress("DEPRECATION") - shadowOf(pm).addResolveInfoForIntent(launchIntent, resolveInfo) - } - - @Test - fun `the default configuration is not changed if no extras are provided`() { - val originalConfig = Configuration() - Glean.configuration = originalConfig - - // Build the intent that will call our debug activity, with no extra. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - assertNull(intent.extras) - - // Start the activity through our intent. - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // Verify that the original configuration and the one after init took place - // are the same. - assertEquals(originalConfig, Glean.configuration) - } - - @Test - fun `command line extra arguments are correctly parsed`() { - // Make sure to set a baseline configuration to check against. - val originalConfig = Configuration() - Glean.configuration = originalConfig - assertFalse(originalConfig.logPings) - - // Set the extra values and start the intent. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - intent.putExtra(GleanDebugActivity.LOG_PINGS_EXTRA_KEY, true) - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // Check that the configuration option was correctly flipped. - assertTrue(Glean.configuration.logPings) - } - - @Test - fun `the main activity is correctly started`() { - // Build the intent that will call our debug activity, with no extra. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - // Add at least an option, otherwise the activity will be removed. - intent.putExtra(GleanDebugActivity.LOG_PINGS_EXTRA_KEY, true) - // Start the activity through our intent. - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // Check that our main activity was launched. - assertEquals(testPackageName, - shadowOf(activity.get()).peekNextStartedActivityForResult().intent.`package`!!) - } - - @Test - fun `pings are sent using sendPing`() { - val server = getMockWebServer() - - resetGlean(config = Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port - )) - - // Put some metric data in the store, otherwise we won't get a ping out - // Define a 'booleanMetric' boolean metric, which will be stored in "store1" - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "boolean_metric", - sendInPings = listOf("metrics") - ) - - booleanMetric.set(true) - assertTrue(booleanMetric.testHasValue()) - - // Set the extra values and start the intent. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - intent.putExtra(GleanDebugActivity.SEND_PING_EXTRA_KEY, "metrics") - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // Since we reset the serverEndpoint back to the default for untagged pings, we need to - // override it here so that the local server we created to intercept the pings will - // be the one that the ping is sent to. - Glean.configuration = Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port - ) - - triggerWorkManager(context) - val request = server.takeRequest(10L, TimeUnit.SECONDS) - - assertTrue( - request.requestUrl.encodedPath().startsWith("/submit/mozilla-components-service-glean/metrics") - ) - - server.shutdown() - } - - @Test - fun `tagPings filters ID's that don't match the pattern`() { - val server = getMockWebServer() - - resetGlean(config = Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port - )) - - // Put some metric data in the store, otherwise we won't get a ping out - // Define a 'booleanMetric' boolean metric, which will be stored in "store1" - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "boolean_metric", - sendInPings = listOf("metrics") - ) - - booleanMetric.set(true) - assertTrue(booleanMetric.testHasValue()) - - // Set the extra values and start the intent. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - intent.putExtra(GleanDebugActivity.SEND_PING_EXTRA_KEY, "metrics") - intent.putExtra(GleanDebugActivity.TAG_DEBUG_VIEW_EXTRA_KEY, "inv@lid_id") - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // Since a bad tag ID results in resetting the endpoint to the default, verify that - // has happened. - assertEquals("Server endpoint must be reset if tag didn't pass regex", - "http://" + server.hostName + ":" + server.port, Glean.configuration.serverEndpoint) - - triggerWorkManager(context) - val request = server.takeRequest(10L, TimeUnit.SECONDS) - - assertTrue( - "Request path must be correct", - request.requestUrl.encodedPath().startsWith("/submit/mozilla-components-service-glean/metrics") - ) - - assertNull( - "Headers must not contain X-Debug-ID if passed a non matching pattern", - request.headers.get("X-Debug-ID") - ) - - server.shutdown() - } - - @Test - fun `pings are correctly tagged using tagPings`() { - val pingTag = "test-debug-ID" - - // The TestClient class found in TestUtil is used to intercept the request in order to check - // that the header has been added correctly for the tagged ping. - val testClient = TestPingTagClient( - debugHeaderValue = pingTag) - - // Use the test client in the Glean configuration - resetGlean(config = Glean.configuration.copy( - httpClient = ConceptFetchHttpUploader(lazy { testClient }) - )) - - // Put some metric data in the store, otherwise we won't get a ping out - // Define a 'booleanMetric' boolean metric, which will be stored in "store1" - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "boolean_metric", - sendInPings = listOf("metrics") - ) - - booleanMetric.set(true) - assertTrue(booleanMetric.testHasValue()) - - // Set the extra values and start the intent. - val intent = Intent(ApplicationProvider.getApplicationContext(), - GleanDebugActivity::class.java) - intent.putExtra(GleanDebugActivity.SEND_PING_EXTRA_KEY, "metrics") - intent.putExtra(GleanDebugActivity.TAG_DEBUG_VIEW_EXTRA_KEY, pingTag) - val activity = Robolectric.buildActivity(GleanDebugActivity::class.java, intent) - activity.create().start().resume() - - // This will trigger the call to `fetch()` in the TestPingTagClient which is where the - // test assertions will occur - triggerWorkManager(context) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/error/ErrorRecordingTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/error/ErrorRecordingTest.kt deleted file mode 100644 index 17e5c034d10..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/error/ErrorRecordingTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.error - -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.StringMetricType -import mozilla.components.service.glean.storages.CountersStorageEngine -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.support.base.log.logger.Logger -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class ErrorRecordingTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `test recording of all error types`() { - CountersStorageEngine.clearAllStores() - val logger = Logger("glean/ErrorRecordingTest") - - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("store1", "store2") - ) - - val expectedErrors = mapOf( - ErrorRecording.ErrorType.InvalidValue to 1, - ErrorRecording.ErrorType.InvalidLabel to 2 - ) - - ErrorRecording.recordError( - stringMetric, - ErrorRecording.ErrorType.InvalidValue, - "Invalid value", - logger - ) - - ErrorRecording.recordError( - stringMetric, - ErrorRecording.ErrorType.InvalidLabel, - "Invalid label", - logger, - numErrors = expectedErrors[ErrorRecording.ErrorType.InvalidLabel] - ) - - for (storeName in listOf("store1", "store2", "metrics")) { - for (errorType in expectedErrors.keys) { - assertEquals( - expectedErrors[errorType], - ErrorRecording.testGetNumRecordedErrors(stringMetric, errorType, storeName) - ) - } - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/FunctionalHistogramTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/FunctionalHistogramTest.kt deleted file mode 100644 index 68fff99dd3e..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/FunctionalHistogramTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.histogram - -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.lang.Math.pow - -@RunWith(RobolectricTestRunner::class) -class FunctionalHistogramTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `sampleToBucketMinimum correctly rounds down`() { - val hist = FunctionalHistogram(2.0, 8.0) - - // Check each of the first 100 integers, where numerical accuracy of the round-tripping - // is most potentially problematic - for (i in (0..100)) { - val value = i.toLong() - val bucketMinimum = hist.sampleToBucketMinimum(value) - assert(bucketMinimum <= value) - - assertEquals(bucketMinimum, hist.sampleToBucketMinimum(bucketMinimum)) - } - - // Do an exponential sampling of higher numbers - for (i in (11..500)) { - val value = pow(1.5, i.toDouble()).toLong() - val bucketMinimum = hist.sampleToBucketMinimum(value) - assert(bucketMinimum <= value) - - assertEquals(bucketMinimum, hist.sampleToBucketMinimum(bucketMinimum)) - } - } - - @Test - fun `toJsonObject correctly converts a FunctionalHistogram object`() { - // Define a FunctionalHistogram object - val tdd = FunctionalHistogram(2.0, 8.0) - - // Accumulate some samples to populate sum and values properties - tdd.accumulate(1L) - tdd.accumulate(2L) - tdd.accumulate(3L) - - // Convert to JSON object using toJsonObject() - val jsonTdd = tdd.toJsonPayloadObject() - - // Verify properties - val jsonValue = jsonTdd.getJSONObject("values") - assertEquals("JSON values must match Timing Distribution values", - tdd.values[1], jsonValue.getLong("1")) - assertEquals("JSON values must match Timing Distribution values", - tdd.values[3], jsonValue.getLong("3")) - assertEquals("JSON values must match Timing Distribution values", - 0, jsonValue.getLong("4")) - assertEquals("JSON sum must match Timing Distribution sum", - tdd.sum, jsonTdd.getLong("sum")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/PrecomputedHistogramTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/PrecomputedHistogramTest.kt deleted file mode 100644 index 9a29b3532de..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/histogram/PrecomputedHistogramTest.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.histogram - -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.HistogramType -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class PrecomputedHistogramTest { - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `samples go in the correct bucket for exponential bucketing`() { - val td = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - for (i in (0..60)) { - val x = i * 1000L - val bucket = td.findBucket(x) - assert(bucket <= x) - } - - val td2 = PrecomputedHistogram( - rangeMin = 10000L, - rangeMax = 50000L, - bucketCount = 50, - histogramType = HistogramType.Exponential - ) - - for (i in (0..60)) { - val x = i * 1000L - val bucket = td2.findBucket(x) - assert(bucket <= x) - } - } - - @Test - fun `validate the generated linear buckets`() { - val td = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 99L, - bucketCount = 100, - histogramType = HistogramType.Linear - ) - assertEquals(100, td.buckets.size) - - for (i in (0L until 100L)) { - val bucket = td.findBucket(i) - assert(bucket <= i) - } - - val td2 = PrecomputedHistogram( - rangeMin = 50L, - rangeMax = 50000L, - bucketCount = 50, - histogramType = HistogramType.Linear - ) - assertEquals(50, td2.buckets.size) - - for (i in (0..60)) { - val x: Long = i * 1000L - val bucket = td2.findBucket(x) - assert(bucket <= x) - } - } - - @Test - fun `toJsonObject and toJsonPayloadObject correctly converts a PrecomputedHistogram object`() { - // Define a PrecomputedHistogram object - val tdd = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Accumulate some samples to populate sum and values properties - tdd.accumulate(1L) - tdd.accumulate(2L) - tdd.accumulate(3L) - - // Convert to JSON object using toJsonObject() - val jsonTdd = tdd.toJsonObject() - - // Verify properties - assertEquals("JSON bucket count must match Precomputed Histogram bucket count", - tdd.bucketCount, jsonTdd.getInt("bucket_count")) - val jsonRange = jsonTdd.getJSONArray("range") - assertEquals("JSON range minimum must match Precomputed Histogram range minimum", - tdd.rangeMin, jsonRange.getLong(0)) - assertEquals("JSON range maximum must match Precomputed Histogram range maximum", - tdd.rangeMax, jsonRange.getLong(1)) - assertEquals("JSON histogram type must match Precomputed Histogram histogram type", - tdd.histogramType.toString().toLowerCase(), jsonTdd.getString("histogram_type")) - val jsonValue = jsonTdd.getJSONObject("values") - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[1], jsonValue.getLong("1")) - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[2], jsonValue.getLong("2")) - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[3], jsonValue.getLong("3")) - assertEquals("JSON sum must match Precomputed Histogram sum", - tdd.sum, jsonTdd.getLong("sum")) - - // Convert to JSON object using toJsonObject() - val jsonPayload = tdd.toJsonPayloadObject() - - // Verify properties - assertEquals(2, jsonPayload.length()) - val jsonPayloadValue = jsonPayload.getJSONObject("values") - assertEquals("JSON values must match Precomputed Histogram values", - 0, jsonPayloadValue.getLong("0")) - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[1], jsonPayloadValue.getLong("1")) - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[2], jsonPayloadValue.getLong("2")) - assertEquals("JSON values must match Precomputed Histogram values", - tdd.values[3], jsonPayloadValue.getLong("3")) - assertEquals("JSON values must match Precomputed Histogram values", - 0, jsonPayloadValue.getLong("4")) - assertEquals("JSON sum must match Precomputed Histogram sum", - tdd.sum, jsonPayload.getLong("sum")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/net/BaseUploaderTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/net/BaseUploaderTest.kt deleted file mode 100644 index bec4d3ba984..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/net/BaseUploaderTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.net - -import mozilla.components.service.glean.BuildConfig -import mozilla.components.service.glean.config.Configuration -import mozilla.components.support.test.any -import mozilla.components.support.test.argumentCaptor -import mozilla.components.support.test.eq -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.`when` -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.robolectric.RobolectricTestRunner -import java.util.Calendar -import java.util.TimeZone - -@RunWith(RobolectricTestRunner::class) -class BaseUploaderTest { - private val testPath: String = "/some/random/path/not/important" - private val testPing: String = "{ 'ping': 'test' }" - private val testDefaultConfig = Configuration().copy( - userAgent = "Glean/Test 25.0.2" - ) - - /** - * A stub uploader class that does not upload anything. - */ - private class TestUploader : PingUploader { - override fun upload(url: String, data: String, headers: HeadersList): Boolean { - return false - } - } - - @Test - fun `upload() must get called with the full submission path`() { - val uploader = spy(BaseUploader(TestUploader())) - - val expectedUrl = testDefaultConfig.serverEndpoint + testPath - uploader.doUpload(testPath, testPing, testDefaultConfig) - verify(uploader).upload(eq(expectedUrl), any(), any()) - } - - @Test - fun `All headers are correctly reported for upload`() { - val uploader = spy(BaseUploader(TestUploader())) - `when`(uploader.getCalendarInstance()).thenAnswer { - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.timeZone = TimeZone.getTimeZone("GMT") - fakeNow.set(2015, 6, 11, 11, 0, 0) - fakeNow - } - - uploader.doUpload(testPath, testPing, testDefaultConfig) - val headersCaptor = argumentCaptor() - - val expectedUrl = testDefaultConfig.serverEndpoint + testPath - verify(uploader).upload(eq(expectedUrl), eq(testPing), headersCaptor.capture()) - - val expectedHeaders = mapOf( - "Content-Type" to "application/json; charset=utf-8", - "Date" to "Sat, 11 Jul 2015 11:00:00 GMT", - "User-Agent" to "Glean/Test 25.0.2", - "X-Client-Type" to "Glean", - "X-Client-Version" to BuildConfig.LIBRARY_VERSION - ) - - expectedHeaders.forEach { (headerName, headerValue) -> - assertEquals( - headerValue, - headersCaptor.value.find { it.first == headerName }!!.second - ) - } - } - - @Test - fun `X-Debug-ID header is correctly added when pingTag is not null`() { - val uploader = spy(BaseUploader(TestUploader())) - - val debugConfig = testDefaultConfig.copy( - pingTag = "this-ping-is-tagged" - ) - - uploader.doUpload(testPath, testPing, debugConfig) - val headersCaptor = argumentCaptor() - verify(uploader).upload(any(), any(), headersCaptor.capture()) - - assertEquals( - "this-ping-is-tagged", - headersCaptor.value.find { it.first == "X-Debug-ID" }!!.second - ) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt index 691396be28a..290584b0849 100644 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt +++ b/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt @@ -9,11 +9,14 @@ import mozilla.components.concept.fetch.Request import mozilla.components.concept.fetch.Response import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient import mozilla.components.lib.fetch.okhttp.OkHttpClient -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.getMockWebServer import mozilla.components.support.test.any import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock +import mozilla.telemetry.glean.config.Configuration +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -38,6 +41,20 @@ class ConceptFetchHttpUploaderTest { userAgent = "Glean/Test 25.0.2" ) + /** + * Create a mock webserver that accepts all requests. + * @return a [MockWebServer] instance + */ + private fun getMockWebServer(): MockWebServer { + val server = MockWebServer() + server.setDispatcher(object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return MockResponse().setBody("OK") + } + }) + return server + } + @Test fun `connection timeouts must be properly set`() { val uploader = diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/ping/PingMakerTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/ping/PingMakerTest.kt deleted file mode 100644 index 5a0a4271066..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/ping/PingMakerTest.kt +++ /dev/null @@ -1,265 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.ping - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.BuildConfig -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.storages.MockStorageEngine -import mozilla.components.service.glean.storages.StorageEngineManager -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter - -@RunWith(RobolectricTestRunner::class) -class PingMakerTest { - private val mockApplicationContext = mock(Context::class.java) - - private val customPing = PingType( - name = "test", - includeClientId = true - ) - - // This test requires us to test against the minSdk of 21 in order to make sure that a date - // related issue is not regressed. We do this using @Config(sdk = [21, 28]) annotation which - // accepts an array of sdk versions to test against. Since loading the sdk versions is time - // consuming, we only test the minSdk to reduce test overhead of glean. - // - // If the minSdk gets changed to >= 24, this can be removed, and DateUtils.getISOTimeString() - // can be updated to remove the workaround. - @Test - @Config(sdk = [ 21, 28 ]) - fun `"ping_info" must contain a non-empty start_time and end_time`() { - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine2" to MockStorageEngine(JSONObject(mapOf("a.b" to "foo"))) - ), - applicationContext = mockApplicationContext - ), - mockApplicationContext - ) - - // Gather the data. We expect an empty ping with the "ping_info" information - val data = maker.collect(customPing).orEmpty() - assertTrue("We expect a non-empty JSON blob", "{}" != data) - - // Parse the data so that we can easily check the other fields - val jsonData = JSONObject(data) - val pingInfo = jsonData["ping_info"] as JSONObject - assertNotNull(pingInfo) - - // "start_time" and "end_time" must be valid ISO8601 dates. DateTimeFormatter would - // throw otherwise. - DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(pingInfo.getString("start_time")) - DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(pingInfo.getString("end_time")) - OffsetDateTime.parse(pingInfo.getString("start_time")) - assertTrue(OffsetDateTime.parse(pingInfo.getString("start_time")) - <= OffsetDateTime.parse(pingInfo.getString("end_time"))) - } - - @Test - fun `ping_info must persist start_time`() { - val applicationContext = ApplicationProvider.getApplicationContext() - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine2" to MockStorageEngine(JSONObject(mapOf("a.b" to "foo"))) - ), - applicationContext = applicationContext - ), - applicationContext - ) - - // Insert a dummy value into the first maker to make sure it's picked up by - // the second maker - maker.setPingStartTime(customPing.name, "2100-01-01") - - val maker2 = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine2" to MockStorageEngine(JSONObject(mapOf("a.b" to "foo"))) - ), - applicationContext = applicationContext - ), - applicationContext - ) - - // Gather the data. We expect an empty ping with the "ping_info" information - val data = maker2.collect(customPing).orEmpty() - assertTrue("We expect a non-empty JSON blob", "{}" != data) - - // Parse the data so that we can easily check the other fields - val jsonData = JSONObject(data) - val pingInfo = jsonData["ping_info"] as JSONObject - assertNotNull(pingInfo) - - // "start_time" and "end_time" must be valid ISO8601 dates. DateTimeFormatter would - // throw otherwise. - assertEquals("2100-01-01", pingInfo.getString("start_time")) - DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(pingInfo.getString("end_time")) - } - - @Test - fun `getPingInfo() must report all the required fields`() { - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine2" to MockStorageEngine(JSONArray(listOf("a", "b", "c"))) - ), - applicationContext = mockApplicationContext - ), - mockApplicationContext - ) - - // Gather the data. We expect an empty ping with the "ping_info" information - val data = maker.collect(customPing).orEmpty() - assertTrue("We expect a non-empty JSON blob", "{}" != data) - - // Parse the data so that we can easily check the other fields - val jsonData = JSONObject(data) - val pingInfo = jsonData["ping_info"] as JSONObject - - assertEquals("test", pingInfo.getString("ping_type")) - assertTrue(pingInfo.has("start_time")) - assertTrue(pingInfo.has("end_time")) - assertTrue(pingInfo.has("seq")) - } - - @Test - fun `getClientInfo() must report all the available data`() { - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine2" to MockStorageEngine(JSONArray(listOf("a", "b", "c"))) - ), - applicationContext = ApplicationProvider.getApplicationContext() - ), - ApplicationProvider.getApplicationContext() - ) - - // Gather the data. We expect an empty ping with the "ping_info" information - val data = maker.collect(customPing).orEmpty() - assertTrue("We expect a non-empty JSON blob", "{}" != data) - - // Parse the data so that we can easily check the other fields - val jsonData = JSONObject(data) - val clientInfo = jsonData["client_info"] as JSONObject - - assertEquals(BuildConfig.LIBRARY_VERSION, clientInfo.getString("telemetry_sdk_build")) - } - - @Test - fun `collect() must report a valid ping with the data from the engines`() { - val engine1Data = JSONArray(listOf("1", "2", "3")) - val engine2Data = JSONArray(listOf("a", "b", "c")) - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine1" to MockStorageEngine(engine1Data), - "engine2" to MockStorageEngine(engine2Data), - "wontCollect" to MockStorageEngine(JSONObject(), "notThisPing") - ), - applicationContext = mockApplicationContext - ), - mockApplicationContext - ) - - // Gather the data, this should have everything in the 'test' ping which is the default - // storex - val data = maker.collect(customPing).orEmpty() - assertNotNull("We expect a non-null JSON blob", data) - - // Parse the data so that we can easily check the other fields - val jsonData = JSONObject(data) - val metricsData = jsonData.getJSONObject("metrics") - assertEquals(engine1Data, metricsData.getJSONArray("engine1")) - assertEquals(engine2Data, metricsData.getJSONArray("engine2")) - assertFalse(metricsData.has("wontCollect")) - } - - @Test - fun `collect() must report an empty string when no data is stored`() { - val engine1Data = JSONArray(listOf("1", "2", "3")) - val engine2Data = JSONArray(listOf("a", "b", "c")) - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine1" to MockStorageEngine(engine1Data), - "engine2" to MockStorageEngine(engine2Data) - ), - applicationContext = mockApplicationContext - ), - mockApplicationContext - ) - - val noSuchPing = PingType( - name = "noSuchPing", - includeClientId = true - ) - - // Gather the data. We expect an empty string - val data = maker.collect(noSuchPing) - assertNull("We expect an empty string", data) - } - - @Test - fun `seq number must be sequential`() { - // NOTE: Using a "real" ApplicationContext here so that it will have - // a working SharedPreferences implementation - val applicationContext = ApplicationProvider.getApplicationContext() - val maker = PingMaker( - StorageEngineManager( - storageEngines = mapOf( - "engine1" to MockStorageEngine(JSONObject(mapOf("a.b" to "foo")), "test1"), - "engine2" to MockStorageEngine(JSONObject(mapOf("c.d" to "foo")), "test2") - ), - applicationContext = applicationContext - ), - applicationContext - ) - - // Clear the sharedPreferences on the PingMaker so we can test that the - // numbers start at zero. - maker.sharedPreferences?.let { - val editor = it.edit() - editor.clear() - editor.apply() - } - // Collect pings, and make sure the seq numbers within each ping type - // are sequential - val results = mutableListOf() - for (i in 1..2) { - for (pingName in arrayOf("test1", "test2")) { - val ping = PingType( - name = pingName, - includeClientId = true - ) - val data = maker.collect(ping).orEmpty() - val jsonData = JSONObject(data) - val pingInfo = jsonData["ping_info"] as JSONObject - val seqNum = pingInfo.getInt("seq") - results.add(seqNum) - } - } - - assertEquals(results[0], 0) - assertEquals(results[1], 0) - assertEquals(results[2], 1) - assertEquals(results[3], 1) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/BooleanMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/BooleanMetricTypeTest.kt deleted file mode 100644 index c99d31ceecc..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/BooleanMetricTypeTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class BooleanMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'booleanMetric' boolean metric, which will be stored in "store1" - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "boolean_metric", - sendInPings = listOf("store1") - ) - - // Record two booleans of the same type, with a little delay. - booleanMetric.set(true) - // Check that data was properly recorded. - assertTrue(booleanMetric.testHasValue()) - assertTrue(booleanMetric.testGetValue()) - - booleanMetric.set(false) - // Check that data was properly recorded. - assertTrue(booleanMetric.testHasValue()) - assertFalse(booleanMetric.testGetValue()) - } - - @Test - fun `disabled booleans must not record data`() { - // Define a 'booleanMetric' boolean metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val booleanMetric = BooleanMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "booleanMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the boolean. - booleanMetric.set(true) - // Check that nothing was recorded. - assertFalse(booleanMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - // Define a 'booleanMetric' boolean metric to have an instance to call - // testGetValue() on - val booleanMetric = BooleanMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "booleanMetric", - sendInPings = listOf("store1") - ) - booleanMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'booleanMetric' boolean metric, which will be stored in "store1" and "store2" - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "boolean_metric", - sendInPings = listOf("store1", "store2") - ) - - // Record two booleans of the same type, with a little delay. - booleanMetric.set(true) - // Check that data was properly recorded in the second ping - assertTrue(booleanMetric.testHasValue("store2")) - assertTrue(booleanMetric.testGetValue("store2")) - - booleanMetric.set(false) - // Check that data was properly recorded in the second ping. - assertTrue(booleanMetric.testHasValue("store2")) - assertFalse(booleanMetric.testGetValue("store2")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/CounterMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/CounterMetricTypeTest.kt deleted file mode 100644 index ea32e46ed99..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/CounterMetricTypeTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.lang.NullPointerException - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class CounterMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'counterMetric' counter metric, which will be stored in "store1" - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Add to the counter a couple of times with a little delay. The first call will check - // calling add() without parameters to test increment by 1. - counterMetric.add() - - // Check that the count was incremented and properly recorded. - assertTrue(counterMetric.testHasValue()) - assertEquals(1, counterMetric.testGetValue()) - - counterMetric.add(10) - // Check that count was incremented and properly recorded. This second call will check - // calling add() with 10 to test increment by other amount - assertTrue(counterMetric.testHasValue()) - assertEquals(11, counterMetric.testGetValue()) - } - - @Test - fun `counters with no lifetime must not record data`() { - // Define a 'counterMetric' counter metric, which will be stored in "store1". - // It's disabled so it should not record anything. - val counterMetric = CounterMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Attempt to increment the counter - counterMetric.add(1) - // Check that nothing was recorded. - assertFalse("Counters must not be recorded if they are disabled", - counterMetric.testHasValue()) - } - - @Test - fun `disabled counters must not record data`() { - // Define a 'counterMetric' counter metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val counterMetric = CounterMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Attempt to store the counter. - counterMetric.add() - // Check that nothing was recorded. - assertFalse("Counters must not be recorded if they are disabled", - counterMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val counterMetric = CounterMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1") - ) - counterMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'counterMetric' counter metric, which will be stored in "store1" and "store2" - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1", "store2") - ) - - // Add to the counter a couple of times with a little delay. The first call will check - // calling add() without parameters to test increment by 1. - counterMetric.add() - - // Check that the count was incremented and properly recorded for the second ping. - assertTrue(counterMetric.testHasValue("store2")) - assertEquals(1, counterMetric.testGetValue("store2")) - - counterMetric.add(10) - // Check that count was incremented and properly recorded for the second ping. - // This second call will check calling add() with 10 to test increment by other amount - assertTrue(counterMetric.testHasValue("store2")) - assertEquals(11, counterMetric.testGetValue("store2")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/CustomDistributionMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/CustomDistributionMetricTypeTest.kt deleted file mode 100644 index dadd1ff4600..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/CustomDistributionMetricTypeTest.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class CustomDistributionMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a custom distribution metric which will be stored in "store1" - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Accumulate a few values - for (i in 1L..3L) { - metric.accumulateSamples(listOf(i).toLongArray()) - } - - // Check that data was properly recorded. - assertTrue(metric.testHasValue()) - val snapshot = metric.testGetValue() - // Check the sum - assertEquals(6L, snapshot.sum) - // Check that the 1L fell into the first value bucket - assertEquals(1L, snapshot.values[1]) - // Check that the 2L fell into the second value bucket - assertEquals(1L, snapshot.values[2]) - // Check that the 3L fell into the third value bucket - assertEquals(1L, snapshot.values[3]) - } - - @Test - fun `disabled custom distributions must not record data`() { - // Define a custom distribution metric which will be stored in "store1" - // It's lifetime is set to Lifetime.Ping so it should not record anything. - val metric = CustomDistributionMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Attempt to store to the distribution - metric.accumulateSamples(listOf(0L).toLongArray()) - - // Check that nothing was recorded. - assertFalse("CustomDistributions without a lifetime should not record data.", - metric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - // Define a custom distribution metric which will be stored in "store1" - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - metric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a custom distribution metric which will be stored in multiple stores - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "custom_distribution", - sendInPings = listOf("store1", "store2", "store3"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Accumulate a few values - metric.accumulateSamples(listOf(1L, 2L, 3L).toLongArray()) - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store2")) - val snapshot = metric.testGetValue("store2") - // Check the sum - assertEquals(6L, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot.values[1]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot.values[2]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot.values[3]) - - // Check that data was properly recorded in the third ping. - assertTrue(metric.testHasValue("store3")) - val snapshot2 = metric.testGetValue("store3") - // Check the sum - assertEquals(6L, snapshot2.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot2.values[1]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot2.values[2]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot2.values[3]) - } - - @Test - fun `The accumulateSamples API correctly stores values`() { - // Define a custom distribution metric which will be stored in multiple stores - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "custom_distribution_samples", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Accumulate a few values - val testSamples = (1L..3L).toList().toLongArray() - metric.accumulateSamples(testSamples) - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store1")) - val snapshot = metric.testGetValue("store1") - // Check the sum - assertEquals(6L, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot.values[1]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot.values[2]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot.values[3]) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/DatetimeMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/DatetimeMetricTypeTest.kt deleted file mode 100644 index 915a8d86750..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/DatetimeMetricTypeTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.util.Calendar -import java.util.TimeZone - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class DatetimeMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'datetimeMetric' datetime metric, which will be stored in "store1" - val datetimeMetric = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "datetime_metric", - sendInPings = listOf("store1") - ) - - val value = Calendar.getInstance() - value.set(2004, 11, 9, 8, 3, 29) - value.timeZone = TimeZone.getTimeZone("America/Los_Angeles") - datetimeMetric.set(value) - assertTrue(datetimeMetric.testHasValue()) - assertEquals("2004-12-09T08:03-08:00", datetimeMetric.testGetValueAsString()) - - val value2 = Calendar.getInstance() - value2.set(1993, 1, 23, 9, 5, 43) - value2.timeZone = TimeZone.getTimeZone("GMT+0") - datetimeMetric.set(value2) - // Check that data was properly recorded. - assertTrue(datetimeMetric.testHasValue()) - assertEquals("1993-02-23T09:05+00:00", datetimeMetric.testGetValueAsString()) - - // A date prior to the UNIX epoch - val value3 = Calendar.getInstance() - value3.set(1969, 7, 20, 20, 17, 3) - value3.timeZone = TimeZone.getTimeZone("GMT-12") - datetimeMetric.set(value3) - // Check that data was properly recorded. - assertTrue(datetimeMetric.testHasValue()) - assertEquals("1969-08-20T20:17-12:00", datetimeMetric.testGetValueAsString()) - - // A date following 2038 (the extent of signed 32-bits after UNIX epoch) - // This fails on some workers on Taskcluster. 32-bit platforms, perhaps? - - // val value4 = Calendar.getInstance() - // value4.set(2039, 7, 20, 20, 17, 3) - // datetimeMetric.set(value4) - // // Check that data was properly recorded. - // assertTrue(datetimeMetric.testHasValue()) - // assertEquals("2039-08-20T20:17:03-04:00", datetimeMetric.testGetValueAsString()) - } - - @Test - fun `disabled datetimes must not record data`() { - // Define a 'datetimeMetric' datetime metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val datetimeMetric = DatetimeMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "datetimeMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the datetime. - datetimeMetric.set() - assertFalse(datetimeMetric.testHasValue()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/EventMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/EventMetricTypeTest.kt deleted file mode 100644 index af87ca0afa1..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/EventMetricTypeTest.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import android.os.SystemClock -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -// Declared here, since Kotlin can not declare nested enum classes. -enum class clickKeys { - objectId -} - -enum class testNameKeys { - testName -} - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class EventMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API records to its storage engine`() { - - // Define a 'click' event, which will be stored in "store1" - val click = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("store1"), - allowedExtraKeys = listOf("object_id") - ) - - // Record two events of the same type, with a little delay. - click.record(extra = mapOf(clickKeys.objectId to "buttonA")) - - val expectedTimeSinceStart: Long = 37 - SystemClock.sleep(expectedTimeSinceStart) - - click.record(extra = mapOf(clickKeys.objectId to "buttonB")) - - // Check that data was properly recorded. - val snapshot = click.testGetValue() - assertTrue(click.testHasValue()) - assertEquals(2, snapshot.size) - - val firstEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonA" } - assertEquals("ui", firstEvent.category) - assertEquals("click", firstEvent.name) - - val secondEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonB" } - assertEquals("ui", secondEvent.category) - assertEquals("click", secondEvent.name) - - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) - } - - @Test - fun `The API records to its storage engine when category is empty`() { - // Define a 'click' event, which will be stored in "store1" - val click = EventMetricType( - disabled = false, - category = "", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("store1"), - allowedExtraKeys = listOf("object_id") - ) - - // Record two events of the same type, with a little delay. - click.record(extra = mapOf(clickKeys.objectId to "buttonA")) - - val expectedTimeSinceStart: Long = 37 - SystemClock.sleep(expectedTimeSinceStart) - - click.record(extra = mapOf(clickKeys.objectId to "buttonB")) - - // Check that data was properly recorded. - val snapshot = click.testGetValue() - assertTrue(click.testHasValue()) - assertEquals(2, snapshot.size) - - val firstEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonA" } - assertEquals("click", firstEvent.name) - - val secondEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonB" } - assertEquals("click", secondEvent.name) - - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) - } - - @Test - fun `disabled events must not record data`() { - // Define a 'click' event, which will be stored in "store1". It's disabled - // so it should not record anything. - val click = EventMetricType( - disabled = true, - category = "ui", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("store1") - ) - - // Attempt to store the event. - click.record() - - // Check that nothing was recorded. - assertFalse("Events must not be recorded if they are disabled", - click.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val testEvent = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "testEvent", - sendInPings = listOf("store1") - ) - testEvent.testGetValue() - } - - @Test - fun `The API records to secondary pings`() { - // Define a 'click' event, which will be stored in "store1" and "store2" - val click = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("store1", "store2"), - allowedExtraKeys = listOf("object_id") - ) - - // Record two events of the same type, with a little delay. - click.record(extra = mapOf(clickKeys.objectId to "buttonA")) - - val expectedTimeSinceStart: Long = 37 - SystemClock.sleep(expectedTimeSinceStart) - - click.record(extra = mapOf(clickKeys.objectId to "buttonB")) - - // Check that data was properly recorded in the second ping. - val snapshot = click.testGetValue("store2") - assertTrue(click.testHasValue("store2")) - assertEquals(2, snapshot.size) - - val firstEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonA" } - assertEquals("ui", firstEvent.category) - assertEquals("click", firstEvent.name) - - val secondEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonB" } - assertEquals("ui", secondEvent.category) - assertEquals("click", secondEvent.name) - - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) - } - - @Test - fun `events should not record when upload is disabled`() { - val eventMetric = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "event_metric", - sendInPings = listOf("store1"), - allowedExtraKeys = listOf("test_name") - ) - assertEquals(true, Glean.getUploadEnabled()) - Glean.setUploadEnabled(true) - eventMetric.record(mapOf(testNameKeys.testName to "event1")) - val snapshot1 = eventMetric.testGetValue() - assertEquals(1, snapshot1.size) - Glean.setUploadEnabled(false) - assertEquals(false, Glean.getUploadEnabled()) - eventMetric.record(mapOf(testNameKeys.testName to "event2")) - try { - eventMetric.testGetValue() - fail("Expected events to be empty") - } catch (e: NullPointerException) { - } - Glean.setUploadEnabled(true) - eventMetric.record(mapOf(testNameKeys.testName to "event3")) - val snapshot3 = eventMetric.testGetValue() - assertEquals(1, snapshot3.size) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/LabeledMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/LabeledMetricTypeTest.kt deleted file mode 100644 index 353304a9b7f..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/LabeledMetricTypeTest.kt +++ /dev/null @@ -1,602 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.GleanMetrics.Pings -import mozilla.components.service.glean.collectAndCheckPingSchema -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.storages.BooleansStorageEngine -import mozilla.components.service.glean.storages.CountersStorageEngine -import mozilla.components.service.glean.storages.MockGenericStorageEngine -import mozilla.components.service.glean.storages.StringListsStorageEngine -import mozilla.components.service.glean.storages.StringsStorageEngine -import mozilla.components.service.glean.storages.TimespansStorageEngine -import mozilla.components.service.glean.storages.UuidsStorageEngine -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.doAnswer -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy -import org.robolectric.RobolectricTestRunner -import java.util.UUID - -@RunWith(RobolectricTestRunner::class) -class LabeledMetricTypeTest { - private data class GenericMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List - ) : CommonMetricData - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `test labeled counter type`() { - CountersStorageEngine.clearAllStores() - - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric - ) - - CountersStorageEngine.record(labeledCounterMetric["label1"], 1) - CountersStorageEngine.record(labeledCounterMetric["label2"], 2) - - // Record a regular non-labeled counter. This isn't normally - // possible with the generated code because the subMetric is private, - // but it's useful to test here that it works. - CountersStorageEngine.record(counterMetric, 3) - - val snapshot = CountersStorageEngine.getSnapshot(storeName = "metrics", clearStore = false) - - assertEquals(3, snapshot!!.size) - assertEquals(1, snapshot["telemetry.labeled_counter_metric/label1"]) - assertEquals(2, snapshot["telemetry.labeled_counter_metric/label2"]) - assertEquals(3, snapshot["telemetry.labeled_counter_metric"]) - - val json = collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - // Do the same checks again on the JSON structure - assertEquals( - 1, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("label1") - ) - assertEquals( - 2, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("label2") - ) - assertEquals( - 3, - json.getJSONObject("counter") - .get("telemetry.labeled_counter_metric") - ) - } - - @Test - fun `test __other__ label with predefined labels`() { - CountersStorageEngine.clearAllStores() - - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric, - labels = setOf("foo", "bar", "baz") - ) - - CountersStorageEngine.record(labeledCounterMetric["foo"], 1) - CountersStorageEngine.record(labeledCounterMetric["foo"], 1) - CountersStorageEngine.record(labeledCounterMetric["bar"], 1) - CountersStorageEngine.record(labeledCounterMetric["not_there"], 1) - CountersStorageEngine.record(labeledCounterMetric["also_not_there"], 1) - CountersStorageEngine.record(labeledCounterMetric["not_me"], 1) - - val snapshot = CountersStorageEngine.getSnapshot(storeName = "metrics", clearStore = false) - - assertEquals(3, snapshot!!.size) - assertEquals(2, snapshot.get("telemetry.labeled_counter_metric/foo")) - assertEquals(1, snapshot.get("telemetry.labeled_counter_metric/bar")) - assertNull(snapshot.get("telemetry.labeled_counter_metric/baz")) - assertEquals(3, snapshot.get("telemetry.labeled_counter_metric/__other__")) - - val json = collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - // Do the same checks again on the JSON structure - assertEquals( - 2, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("foo") - ) - assertEquals( - 1, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("bar") - ) - assertEquals( - 3, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("__other__") - ) - } - - @Test - fun `test __other__ label without predefined labels`() { - CountersStorageEngine.clearAllStores() - - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric - ) - - for (i in 0..20) { - CountersStorageEngine.record(labeledCounterMetric["label_$i"], 1) - } - // Go back and record in one of the real labels again - CountersStorageEngine.record(labeledCounterMetric["label_0"], 1) - - val snapshot = CountersStorageEngine.getSnapshot(storeName = "metrics", clearStore = false) - - assertEquals(17, snapshot!!.size) - assertEquals(2, snapshot.get("telemetry.labeled_counter_metric/label_0")) - for (i in 1..15) { - assertEquals(1, snapshot.get("telemetry.labeled_counter_metric/label_$i")) - } - assertEquals(5, snapshot.get("telemetry.labeled_counter_metric/__other__")) - - val json = collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - // Do the same checks again on the JSON structure - assertEquals( - 2, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("label_0") - ) - for (i in 1..15) { - assertEquals( - 1, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("label_$i") - ) - } - assertEquals( - 5, - json.getJSONObject("labeled_counter") - .getJSONObject("telemetry.labeled_counter_metric") - .get("__other__") - ) - } - - @Test - fun `Ensure invalid labels go to __other__`() { - CountersStorageEngine.clearAllStores() - - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric - ) - - CountersStorageEngine.record(labeledCounterMetric["notSnakeCase"], 1) - CountersStorageEngine.record(labeledCounterMetric[""], 1) - CountersStorageEngine.record(labeledCounterMetric["with/slash"], 1) - CountersStorageEngine.record( - labeledCounterMetric["this_string_has_more_than_thirty_characters"], - 1 - ) - - assertEquals( - 4, - ErrorRecording.testGetNumRecordedErrors( - labeledCounterMetric, - ErrorRecording.ErrorType.InvalidLabel - ) - ) - assertEquals( - 4, - labeledCounterMetric["__other__"].testGetValue() - ) - } - - @Test - fun `Ensure labels pass regex matching`() { - CountersStorageEngine.clearAllStores() - - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric - ) - - // Declare the labels we expect to pass the regex. - val expectedToPass = listOf( - "this.is.fine", - "this.is_still_fine", - "thisisfine", - "this_is_fine_too", - "this.is_fine.2", - "_.is_fine", - "this.is-fine", - "this-is-fine" - ) - - // And the ones we expect to fail the regex. - val expectedToFail = listOf( - "this.is.not_fine_due_to_the_length_being_too_long", - "1.not_fine", - "this.\$isnotfine", - "-.not_fine" - ) - - // Attempt to record both the failing and the passing labels. - expectedToPass.forEach { label -> - CountersStorageEngine.record(labeledCounterMetric[label], 1) - } - - expectedToFail.forEach { label -> - CountersStorageEngine.record(labeledCounterMetric[label], 1) - } - - // Validate the recorded data. - val snapshot = CountersStorageEngine.getSnapshot(storeName = "metrics", clearStore = false) - // This snapshot includes: - // - The expectedToPass.size valid values from above - // - The error counter - // - The invalid (__other__) value - val numExpectedLabels = expectedToPass.size + 2 - assertEquals(numExpectedLabels, snapshot!!.size) - - // Make sure the passing labels were accepted - expectedToPass.forEach { label -> - assertEquals(1, snapshot["telemetry.labeled_counter_metric/$label"]) - } - - // Make sure we see the bad labels went to "__other__" - assertEquals( - expectedToFail.size, - labeledCounterMetric["__other__"].testGetValue() - ) - } - - @Test - fun `Test labeled timespan metric type`() { - TimespansStorageEngine.clearAllStores() - - val timespanMetric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_timespan_metric", - sendInPings = listOf("metrics"), - timeUnit = TimeUnit.Nanosecond - ) - - val labeledTimespanMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_timespan_metric", - sendInPings = listOf("metrics"), - subMetric = timespanMetric - ) - - labeledTimespanMetric["label1"].start() - labeledTimespanMetric["label1"].stop() - labeledTimespanMetric["label2"].start() - labeledTimespanMetric["label2"].stop() - - assertTrue(labeledTimespanMetric["label1"].testHasValue()) - - collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - } - - @Test - fun `Test labeled uuid metric type`() { - val uuidMetric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_uuid_metric", - sendInPings = listOf("metrics") - ) - - val labeledUuidMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_uuid_metric", - sendInPings = listOf("metrics"), - subMetric = uuidMetric - ) - - UuidsStorageEngine.record(labeledUuidMetric["label1"], UUID.randomUUID()) - UuidsStorageEngine.record(labeledUuidMetric["label2"], UUID.randomUUID()) - - collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - } - - @Test - fun `Test labeled string list metric type`() { - StringListsStorageEngine.clearAllStores() - - val stringListMetric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_list_metric", - sendInPings = listOf("metrics") - ) - - val labeledStringListMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_list_metric", - sendInPings = listOf("metrics"), - subMetric = stringListMetric - ) - - StringListsStorageEngine.set(labeledStringListMetric["label1"], listOf("a", "b", "c")) - StringListsStorageEngine.set(labeledStringListMetric["label2"], listOf("a", "b", "c")) - - collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - } - - @Test - fun `Test labeled string metric type`() { - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_metric", - sendInPings = listOf("metrics") - ) - - val labeledStringMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_metric", - sendInPings = listOf("metrics"), - subMetric = stringMetric - ) - - StringsStorageEngine.record(labeledStringMetric["label1"], "foo") - StringsStorageEngine.record(labeledStringMetric["label2"], "bar") - - collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - } - - @Test - fun `Test labeled boolean metric type`() { - BooleansStorageEngine.clearAllStores() - - val booleanMetric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_list_metric", - sendInPings = listOf("metrics") - ) - - val labeledBooleanMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_string_list_metric", - sendInPings = listOf("metrics"), - subMetric = booleanMetric - ) - - BooleansStorageEngine.record(labeledBooleanMetric["label1"], false) - BooleansStorageEngine.record(labeledBooleanMetric["label2"], true) - - collectAndCheckPingSchema(Pings.metrics).getJSONObject("metrics") - } - - @Test(expected = IllegalStateException::class) - fun `Test that we labeled events are an exception`() { - val eventMetric = EventMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_event_metric", - sendInPings = listOf("metrics") - ) - - val labeledEventMetric = LabeledMetricType>( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_event_metric", - sendInPings = listOf("metrics"), - subMetric = eventMetric - ) - - labeledEventMetric["label1"] - } - - @Test - fun `test seen labels get reloaded from disk`() { - val persistedSample = mapOf( - "store1#telemetry.labeled_metric/label1" to 1, - "store1#telemetry.labeled_metric/label2" to 1, - "store1#telemetry.labeled_metric/label3" to 1 - ) - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - val metric = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "labeled_metric", - sendInPings = listOf("store1") - ) - - val labeledMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "labeled_metric", - sendInPings = listOf("store1"), - subMetric = metric - ) - - val labeledMetricSpy = spy(labeledMetric) - doReturn(storageEngine).`when`(labeledMetricSpy).getStorageEngineForMetric() - - doAnswer { - metric.copy(name = it.arguments[0] as String) - }.`when`(labeledMetricSpy).getMetricWithNewName(anyString()) - - for (i in 4..20) { - storageEngine.record(labeledMetricSpy["label$i"], 1) - } - // Go back and record in one of the real labels again - storageEngine.record(labeledMetricSpy["label1"], 2) - - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = false) - - assertEquals(17, snapshot!!.size) - assertEquals(2, snapshot.get("telemetry.labeled_metric/label1")) - for (i in 2..15) { - assertEquals(1, snapshot.get("telemetry.labeled_metric/label$i")) - } - assertEquals(1, snapshot.get("telemetry.labeled_metric/__other__")) - } - - @Test - fun `test recording to static labels by label index`() { - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics") - ) - - val labeledCounterMetric = LabeledMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "labeled_counter_metric", - sendInPings = listOf("metrics"), - subMetric = counterMetric, - labels = setOf("foo", "bar", "baz") - ) - - // Increment using a label name first. - labeledCounterMetric["foo"].add(2) - - // Now only use label indices: "foo" first. - labeledCounterMetric[0].add(1) - // Then "bar". - labeledCounterMetric[1].add(1) - // Then some out of bound index: will go to "__other__". - labeledCounterMetric[100].add(100) - - // Only use snapshotting to make sure there's just 2 labels recorded. - val snapshot = CountersStorageEngine.getSnapshot(storeName = "metrics", clearStore = false) - assertEquals(3, snapshot!!.size) - - // Use the testing API to get the values for the labels. - assertEquals(3, labeledCounterMetric["foo"].testGetValue()) - assertEquals(1, labeledCounterMetric["bar"].testGetValue()) - assertEquals(100, labeledCounterMetric["__other__"].testGetValue()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/MemoryDistributionMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/MemoryDistributionMetricTypeTest.kt deleted file mode 100644 index 8e712097c9b..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/MemoryDistributionMetricTypeTest.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.storages.MemoryDistributionsStorageEngineImplementation -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class MemoryDistributionMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a memory distribution metric which will be stored in "store1" - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Kilobyte - ) - - // Accumulate a few values - for (i in 1L..3L) { - metric.accumulate(i) - } - - val kb = 1024 - - // Check that data was properly recorded. - assertTrue(metric.testHasValue()) - val snapshot = metric.testGetValue() - // Check the sum - assertEquals(1L * kb + 2L * kb + 3L * kb, snapshot.sum) - // Check that the 1L fell into the first value bucket - assertEquals(1L, snapshot.values[1023]) - // Check that the 2L fell into the second value bucket - assertEquals(1L, snapshot.values[2047]) - // Check that the 3L fell into the third value bucket - assertEquals(1L, snapshot.values[3024]) - } - - @Test - fun `values are truncated to 1TB`() { - // Define a memory distribution metric which will be stored in "store1" - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Gigabyte - ) - - metric.accumulate(2048L) - - // Check that data was properly recorded. - assertTrue(metric.testHasValue()) - val snapshot = metric.testGetValue() - // Check the sum - assertEquals(1L shl 40, snapshot.sum) - // Check that the 1L fell into 1TB bucket - assertEquals(1L, snapshot.values[(1L shl 40) - 1]) - } - - @Test - fun `disabled memory distributions must not record data`() { - // Define a memory distribution metric which will be stored in "store1" - // It's lifetime is set to Lifetime.Ping so it should not record anything. - val metric = MemoryDistributionMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Kilobyte - ) - - metric.accumulate(1L) - - // Check that nothing was recorded. - assertFalse("MemoryDistributions without a lifetime should not record data.", - metric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - // Define a memory distribution metric which will be stored in "store1" - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Kilobyte - ) - metric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a memory distribution metric which will be stored in multiple stores - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution", - sendInPings = listOf("store1", "store2", "store3"), - memoryUnit = MemoryUnit.Kilobyte - ) - - // Accumulate a few values - for (i in 1L..3L) { - metric.accumulate(i) - } - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store2")) - val snapshot = metric.testGetValue("store2") - // Check the sum - assertEquals(6144L, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot.values[1023]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot.values[2047]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot.values[3024]) - - // Check that data was properly recorded in the third ping. - assertTrue(metric.testHasValue("store3")) - val snapshot2 = metric.testGetValue("store3") - // Check the sum - assertEquals(6144L, snapshot2.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot2.values[1023]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot2.values[2047]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot2.values[3024]) - } - - @Test - fun `The accumulateSamples API correctly stores memory values`() { - // Define a memory distribution metric which will be stored in multiple stores - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "memory_distribution_samples", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Kilobyte - ) - - // Accumulate a few values - val testSamples = (1L..3L).toList().toLongArray() - metric.accumulateSamples(testSamples) - - val kb = 1024L - val hist = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store1")) - val snapshot = metric.testGetValue("store1") - // Check the sum - assertEquals(6L * kb, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(1 * kb)] - ) - // Check that the 2L fell into the second bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(2 * kb)] - ) - // Check that the 3L fell into the third bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(3 * kb)] - ) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/PingTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/PingTypeTest.kt deleted file mode 100644 index 25bfbf55059..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/PingTypeTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.checkPingSchema -import mozilla.components.service.glean.getContextWithMockedInfo -import mozilla.components.service.glean.getMockWebServer -import mozilla.components.service.glean.getWorkerStatus -import mozilla.components.service.glean.resetGlean -import mozilla.components.service.glean.scheduler.PingUploadWorker -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.triggerWorkManager -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.util.concurrent.TimeUnit - -@RunWith(RobolectricTestRunner::class) -class PingTypeTest { - - private val context: Context - get() = ApplicationProvider.getApplicationContext() - - @get:Rule - val gleanRule = GleanTestRule(context) - - @Test - fun `test sending of custom pings`() { - val server = getMockWebServer() - - val customPing = PingType( - name = "custom", - includeClientId = true - ) - - val counter = CounterMetricType( - disabled = false, - category = "test", - lifetime = Lifetime.Ping, - name = "counter", - sendInPings = listOf("custom") - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - counter.add() - assertTrue(counter.testHasValue()) - - customPing.send() - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - val request = server.takeRequest(20L, TimeUnit.SECONDS) - val docType = request.path.split("/")[3] - assertEquals("custom", docType) - - val pingJson = JSONObject(request.body.readUtf8()) - assertNotNull(pingJson.getJSONObject("client_info")["client_id"]) - checkPingSchema(pingJson) - } - - @Test - fun `test sending of custom pings without client_id`() { - val server = getMockWebServer() - - val customPing = PingType( - name = "custom", - includeClientId = false - ) - - val counter = CounterMetricType( - disabled = false, - category = "test", - lifetime = Lifetime.Ping, - name = "counter", - sendInPings = listOf("custom") - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - counter.add() - assertTrue(counter.testHasValue()) - - customPing.send() - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - val request = server.takeRequest(20L, TimeUnit.SECONDS) - val docType = request.path.split("/")[3] - assertEquals("custom", docType) - - val pingJson = JSONObject(request.body.readUtf8()) - assertNull(pingJson.getJSONObject("client_info").opt("client_id")) - checkPingSchema(pingJson) - } - - @Test - fun `Sending a ping with an unknown name is a no-op`() { - val server = getMockWebServer() - - val counter = CounterMetricType( - disabled = false, - category = "test", - lifetime = Lifetime.Ping, - name = "counter", - sendInPings = listOf("unknown") - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - counter.add() - assertTrue(counter.testHasValue()) - - Glean.sendPingsByName(listOf("unknown")) - - assertFalse("We shouldn't have any pings scheduled", - getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued - ) - } - - @Test - fun `Registry should contain built-in pings`() { - assertTrue(PingType.pingRegistry.containsKey("metrics")) - assertTrue(PingType.pingRegistry.containsKey("events")) - assertTrue(PingType.pingRegistry.containsKey("baseline")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/QuantityMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/QuantityMetricTypeTest.kt deleted file mode 100644 index f8bd606e616..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/QuantityMetricTypeTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.lang.NullPointerException - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class QuantityMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'quantityMetric' quantity metric, which will be stored in "store1" - val quantityMetric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Set the quantity a couple of times. - quantityMetric.set(1L) - - assertTrue(quantityMetric.testHasValue()) - assertEquals(1, quantityMetric.testGetValue()) - - quantityMetric.set(10L) - assertTrue(quantityMetric.testHasValue()) - assertEquals(10, quantityMetric.testGetValue()) - } - - @Test - fun `quantities with no lifetime must not record data`() { - // Define a 'quantityMetric' quantity metric, which will be stored in "store1". - // It's disabled so it should not record anything. - val quantityMetric = QuantityMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - quantityMetric.set(1L) - // Check that nothing was recorded. - assertFalse("Quantities must not be recorded if they are disabled", - quantityMetric.testHasValue()) - } - - @Test - fun `disabled quantities must not record data`() { - // Define a 'quantityMetric' quantity metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val quantityMetric = QuantityMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Attempt to store the quantity. - quantityMetric.set(1L) - // Check that nothing was recorded. - assertFalse("Quantities must not be recorded if they are disabled", - quantityMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val quantityMetric = QuantityMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - quantityMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'quantityMetric' quantity metric, which will be stored in "store1" and "store2" - val quantityMetric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "quantity_metric", - sendInPings = listOf("store1", "store2") - ) - - // Add to the quantity a couple of times. - quantityMetric.set(1L) - - assertTrue(quantityMetric.testHasValue("store2")) - assertEquals(1L, quantityMetric.testGetValue("store2")) - - quantityMetric.set(10L) - assertTrue(quantityMetric.testHasValue("store2")) - assertEquals(10L, quantityMetric.testGetValue("store2")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringListMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringListMetricTypeTest.kt deleted file mode 100644 index b6174cfe7a8..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringListMetricTypeTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class StringListMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine by first adding then setting`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" - val stringListMetric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - // Record two lists using add and set - stringListMetric.add("value1") - stringListMetric.add("value2") - stringListMetric.add("value3") - - // Check that data was properly recorded. - val snapshot = stringListMetric.testGetValue() - assertEquals(3, snapshot.size) - assertTrue(stringListMetric.testHasValue()) - assertEquals("value1", snapshot[0]) - assertEquals("value2", snapshot[1]) - assertEquals("value3", snapshot[2]) - - // Use set() to see that the first list is replaced by the new list - stringListMetric.set(listOf("other1", "other2", "other3")) - // Check that data was properly recorded. - val snapshot2 = stringListMetric.testGetValue() - assertEquals(3, snapshot2.size) - assertTrue(stringListMetric.testHasValue()) - assertEquals("other1", snapshot2[0]) - assertEquals("other2", snapshot2[1]) - assertEquals("other3", snapshot2[2]) - } - - @Test - fun `The API saves to its storage engine by first setting then adding`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" - val stringListMetric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - // Record two lists using set and add - stringListMetric.set(listOf("value1", "value2", "value3")) - - // Check that data was properly recorded. - val snapshot = stringListMetric.testGetValue() - assertEquals(3, snapshot.size) - assertTrue(stringListMetric.testHasValue()) - assertEquals("value1", snapshot[0]) - assertEquals("value2", snapshot[1]) - assertEquals("value3", snapshot[2]) - - // Use set() to see that the first list is replaced by the new list - stringListMetric.add("added1") - // Check that data was properly recorded. - val snapshot2 = stringListMetric.testGetValue() - assertEquals(4, snapshot2.size) - assertTrue(stringListMetric.testHasValue()) - assertEquals("value1", snapshot2[0]) - assertEquals("value2", snapshot2[1]) - assertEquals("value3", snapshot2[2]) - assertEquals("added1", snapshot2[3]) - } - - @Test - fun `lists with no lifetime must not record data`() { - // Define a string list metric which will be stored in "store1". - // It's lifetime is set to Lifetime.Ping so it should not record anything. - val stringListMetric = StringListMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - // Attempt to store the string list using set - stringListMetric.set(listOf("value1", "value2", "value3")) - // Check that nothing was recorded. - assertFalse("StringLists without a lifetime should not record data", - stringListMetric.testHasValue()) - - // Attempt to store the string list using add. - stringListMetric.add("value4") - // Check that nothing was recorded. - assertFalse("StringLists without a lifetime should not record data", - stringListMetric.testHasValue()) - } - - @Test - fun `disabled lists must not record data`() { - // Define a string list metric which will be stored in "store1". - // It's disabled so it should not record anything. - val stringListMetric = StringListMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - // Attempt to store the string list using set. - stringListMetric.set(listOf("value1", "value2", "value3")) - // Check that nothing was recorded. - assertFalse("StringLists must not be recorded if they are disabled", - stringListMetric.testHasValue()) - - // Attempt to store the string list using add. - stringListMetric.add("value4") - // Check that nothing was recorded. - assertFalse("StringLists must not be recorded if they are disabled", - stringListMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val stringListMetric = StringListMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - stringListMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" and "store2" - val stringListMetric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1", "store2") - ) - - // Record two lists using add and set - stringListMetric.add("value1") - stringListMetric.add("value2") - stringListMetric.add("value3") - - // Check that data was properly recorded in the second ping. - assertTrue(stringListMetric.testHasValue("store2")) - val snapshot = stringListMetric.testGetValue("store2") - assertEquals(3, snapshot.size) - assertEquals("value1", snapshot[0]) - assertEquals("value2", snapshot[1]) - assertEquals("value3", snapshot[2]) - - // Use set() to see that the first list is replaced by the new list. - stringListMetric.set(listOf("other1", "other2", "other3")) - // Check that data was properly recorded in the second ping. - assertTrue(stringListMetric.testHasValue("store2")) - val snapshot2 = stringListMetric.testGetValue("store2") - assertEquals(3, snapshot2.size) - assertEquals("other1", snapshot2[0]) - assertEquals("other2", snapshot2[1]) - assertEquals("other3", snapshot2[2]) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringMetricTypeTest.kt deleted file mode 100644 index 80eb5c96080..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/StringMetricTypeTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class StringMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("store1") - ) - - // Record two strings of the same type, with a little delay. - stringMetric.set("value") - - // Check that data was properly recorded. - assertTrue(stringMetric.testHasValue()) - assertEquals("value", stringMetric.testGetValue()) - - stringMetric.set("overriddenValue") - // Check that data was properly recorded. - assertTrue(stringMetric.testHasValue()) - assertEquals("overriddenValue", stringMetric.testGetValue()) - } - - @Test - fun `strings with no lifetime must not record data`() { - // Define a 'stringMetric' string metric, which will be stored in - // "store1". It's disabled so it should not record anything. - val stringMetric = StringMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "stringMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the string. - stringMetric.set("value") - // Check that nothing was recorded. - assertFalse("Strings must not be recorded if they have no lifetime", - stringMetric.testHasValue()) - } - - @Test - fun `disabled strings must not record data`() { - // Define a 'stringMetric' string metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val stringMetric = StringMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "stringMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the string. - stringMetric.set("value") - // Check that nothing was recorded. - assertFalse("Strings must not be recorded if they are disabled", - stringMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val stringMetric = StringMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "stringMetric", - sendInPings = listOf("store1") - ) - stringMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" and "store2" - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("store1", "store2") - ) - - // Record two strings of the same type, with a little delay. - stringMetric.set("value") - - // Check that data was properly recorded in the second ping. - assertTrue(stringMetric.testHasValue("store2")) - assertEquals("value", stringMetric.testGetValue("store2")) - - stringMetric.set("overriddenValue") - // Check that data was properly recorded in the second ping. - assertTrue(stringMetric.testHasValue("store2")) - assertEquals("overriddenValue", stringMetric.testGetValue("store2")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimespanMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimespanMetricTypeTest.kt deleted file mode 100644 index 2e0cc85bb40..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimespanMetricTypeTest.kt +++ /dev/null @@ -1,288 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.timing.TimingManager -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class TimespanMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API must record to its storage engine`() { - // Define a timespan metric, which will be stored in "store1" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - metric.start() - metric.stop() - - // Check that data was properly recorded. - assertTrue(metric.testHasValue()) - assertTrue(metric.testGetValue() >= 0) - } - - @Test - fun `The API should not record if the metric is disabled`() { - // Define a timespan metric, which will be stored in "store1" - val metric = TimespanMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - metric.start() - metric.stop() - - // Let's also call cancel() to make sure it's a no-op. - metric.cancel() - - // Check that data was not recorded. - assertFalse("The API should not record a counter if metric is disabled", - metric.testHasValue()) - } - - @Test - fun `The API must correctly cancel`() { - // Define a timespan metric, which will be stored in "store1" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - metric.start() - metric.cancel() - metric.stop() - - // Check that data was not recorded. - assertFalse("The API should not record a counter if metric is cancelled", - metric.testHasValue()) - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `The API must correctly clear the timer state on stop()`() { - // Define a timespan metric, which will be stored in "store1" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - TimingManager.getElapsedNanos = { 0 } - metric.start() - TimingManager.getElapsedNanos = { 5000000 } - metric.stop() - - // Attempt to record again. - TimingManager.getElapsedNanos = { 0 } - metric.start() - TimingManager.getElapsedNanos = { 10000000 } - metric.stop() - - // Check only the first chunk was recorded. - assertEquals(5, metric.testGetValue()) - // And that an attempt to record the second time was tracked. - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - metric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a timespan metric, which will be stored in "store1" and "store2" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1", "store2"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - metric.start() - metric.stop() - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store2")) - assertTrue(metric.testGetValue("store2") >= 0) - } - - @Test - fun `Records an error if started twice`() { - // Define a timespan metric, which will be stored in "store1" and "store2" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1", "store2"), - timeUnit = TimeUnit.Millisecond - ) - - // Record a timespan. - metric.start() - metric.start() - metric.stop() - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store2")) - assertTrue(metric.testGetValue("store2") >= 0) - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `Value unchanged if stopped twice`() { - // Define a timespan metric, which will be stored in "store1" and "store2" - val metric = TimespanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "timespan_metric", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Nanosecond - ) - - // Record a timespan. - metric.start() - metric.stop() - assertTrue(metric.testHasValue()) - val value = metric.testGetValue() - - metric.stop() - - assertEquals(value, metric.testGetValue()) - } - - @Test - fun `test setRawNanos`() { - val timespanNanos = 6 * 1000000000L - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "explicit_timespan", - listOf("store1"), - timeUnit = TimeUnit.Second - ) - - metric.setRawNanos(timespanNanos) - assertEquals(6, metric.testGetValue()) - } - - @Test - fun `test setRawNanos followed by other API`() { - val timespanNanos = 6 * 1000000000L - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "explicit_timespan_1", - listOf("store1"), - timeUnit = TimeUnit.Second - ) - - metric.setRawNanos(timespanNanos) - assertEquals(6, metric.testGetValue()) - - TimingManager.getElapsedNanos = { 0 } - metric.start() - TimingManager.getElapsedNanos = { 50 } - metric.stop() - val value = metric.testGetValue() - assertEquals(6, value) - } - - @Test - fun `setRawNanos does not overwrite value`() { - val timespanNanos = 6 * 1000000000L - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "explicit_timespan_1", - listOf("store1"), - timeUnit = TimeUnit.Second - ) - - metric.start() - metric.stop() - val value = metric.testGetValue() - - metric.setRawNanos(timespanNanos) - - assertEquals(value, metric.testGetValue()) - } - - @Test - fun `setRawNanos does nothing when timer is running`() { - val timespanNanos = 1000000000L - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "explicit_timespan", - listOf("store1"), - timeUnit = TimeUnit.Second - ) - - metric.start() - metric.setRawNanos(timespanNanos) - metric.stop() - - assertNotEquals(timespanNanos, metric.testGetValue()) - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimingDistributionMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimingDistributionMetricTypeTest.kt deleted file mode 100644 index 54f2ba589db..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/TimingDistributionMetricTypeTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.storages.TimingDistributionsStorageEngineImplementation -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.timing.TimingManager -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class TimingDistributionMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @After - fun reset() { - TimingManager.testResetTimeSource() - } - - @Test - fun `The API saves to its storage engine`() { - // Define a timing distribution metric which will be stored in "store1" - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Accumulate a few values - for (i in 1L..3L) { - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { i } - metric.stopAndAccumulate(timerId) - } - - // Check that data was properly recorded. - assertTrue(metric.testHasValue()) - val snapshot = metric.testGetValue() - // Check the sum - assertEquals(6L, snapshot.sum) - // Check that the 1L fell into the first value bucket - assertEquals(1L, snapshot.values[1]) - // Check that the 2L fell into the second value bucket - assertEquals(1L, snapshot.values[2]) - // Check that the 3L fell into the third value bucket - assertEquals(1L, snapshot.values[3]) - } - - @Test - fun `disabled timing distributions must not record data`() { - // Define a timing distribution metric which will be stored in "store1" - // It's lifetime is set to Lifetime.Ping so it should not record anything. - val metric = TimingDistributionMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Attempt to store the timespan using set - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { 1 } - metric.stopAndAccumulate(timerId) - - // Check that nothing was recorded. - assertFalse("TimingDistributions without a lifetime should not record data.", - metric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - // Define a timing distribution metric which will be stored in "store1" - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - metric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a timing distribution metric which will be stored in multiple stores - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "timing_distribution", - sendInPings = listOf("store1", "store2", "store3"), - timeUnit = TimeUnit.Millisecond - ) - - // Accumulate a few values - for (i in 1L..3L) { - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { i } - metric.stopAndAccumulate(timerId) - } - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store2")) - val snapshot = metric.testGetValue("store2") - // Check the sum - assertEquals(6L, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot.values[1]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot.values[2]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot.values[3]) - - // Check that data was properly recorded in the third ping. - assertTrue(metric.testHasValue("store3")) - val snapshot2 = metric.testGetValue("store3") - // Check the sum - assertEquals(6L, snapshot2.sum) - // Check that the 1L fell into the first bucket - assertEquals(1L, snapshot2.values[1]) - // Check that the 2L fell into the second bucket - assertEquals(1L, snapshot2.values[2]) - // Check that the 3L fell into the third bucket - assertEquals(1L, snapshot2.values[3]) - } - - @Test - fun `The accumulateSamples API correctly stores timing values`() { - // Define a timing distribution metric which will be stored in multiple stores - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "timing_distribution_samples", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Second - ) - - // Accumulate a few values - val testSamples = (1L..3L).toList().toLongArray() - metric.accumulateSamples(testSamples) - - val secondsToNanos = 1000L * 1000L * 1000L - val hist = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - // Check that data was properly recorded in the second ping. - assertTrue(metric.testHasValue("store1")) - val snapshot = metric.testGetValue("store1") - // Check the sum - assertEquals(6L * secondsToNanos, snapshot.sum) - // Check that the 1L fell into the first bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(1 * secondsToNanos)] - ) - // Check that the 2L fell into the second bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(2 * secondsToNanos)] - ) - // Check that the 3L fell into the third bucket - assertEquals( - 1L, snapshot.values[hist.sampleToBucketMinimum(3 * secondsToNanos)] - ) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/private/UuidMetricTypeTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/private/UuidMetricTypeTest.kt deleted file mode 100644 index 478344096ad..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/private/UuidMetricTypeTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.private - -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.util.UUID - -@ObsoleteCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class UuidMetricTypeTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `The API saves to its storage engine`() { - // Define a 'uuidMetric' uuid metric, which will be stored in "store1" - val uuidMetric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "uuid_metric", - sendInPings = listOf("store1") - ) - - // Record two uuids of the same type, with a little delay. - val uuid = uuidMetric.generateAndSet() - - // Check that data was properly recorded. - assertTrue(uuidMetric.testHasValue()) - assertEquals(uuid, uuidMetric.testGetValue()) - - val uuid2 = UUID.fromString("ce2adeb8-843a-4232-87a5-a099ed1e7bb3") - uuidMetric.set(uuid2) - - // Check that data was properly recorded. - assertTrue(uuidMetric.testHasValue()) - assertEquals(uuid2, uuidMetric.testGetValue()) - } - - @Test - fun `uuids with no lifetime must not record data`() { - // Define a 'uuidMetric' uuid metric, which will be stored in - // "store1". It's disabled so it should not record anything. - val uuidMetric = UuidMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "uuidMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the uuid. - uuidMetric.generateAndSet() - // Check that nothing was recorded. - assertFalse("Uuids must not be recorded if they have no lifetime", - uuidMetric.testHasValue()) - } - - @Test - fun `disabled uuids must not record data`() { - // Define a 'uuidMetric' uuid metric, which will be stored in "store1". It's disabled - // so it should not record anything. - val uuidMetric = UuidMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "uuidMetric", - sendInPings = listOf("store1") - ) - - // Attempt to store the uuid. - uuidMetric.generateAndSet() - // Check that nothing was recorded. - assertFalse("Uuids must not be recorded if they are disabled", - uuidMetric.testHasValue()) - } - - @Test(expected = NullPointerException::class) - fun `testGetValue() throws NullPointerException if nothing is stored`() { - val uuidMetric = UuidMetricType( - disabled = true, - category = "telemetry", - lifetime = Lifetime.Application, - name = "uuidMetric", - sendInPings = listOf("store1") - ) - uuidMetric.testGetValue() - } - - @Test - fun `The API saves to secondary pings`() { - // Define a 'uuidMetric' uuid metric, which will be stored in "store1" and "store2" - val uuidMetric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "uuid_metric", - sendInPings = listOf("store1", "store2") - ) - - // Record two uuids of the same type, with a little delay. - val uuid = uuidMetric.generateAndSet() - - // Check that data was properly recorded. - assertTrue(uuidMetric.testHasValue("store2")) - assertEquals(uuid, uuidMetric.testGetValue("store2")) - - val uuid2 = UUID.fromString("ce2adeb8-843a-4232-87a5-a099ed1e7bb3") - uuidMetric.set(uuid2) - - // Check that data was properly recorded. - assertTrue(uuidMetric.testHasValue("store2")) - assertEquals(uuid2, uuidMetric.testGetValue("store2")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/MetricsPingSchedulerTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/MetricsPingSchedulerTest.kt deleted file mode 100644 index 1b9fc680a99..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/MetricsPingSchedulerTest.kt +++ /dev/null @@ -1,579 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.scheduler - -import android.content.Context -import android.os.SystemClock -import androidx.test.core.app.ApplicationProvider -import androidx.work.testing.WorkManagerTestInitHelper -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.checkPingSchema -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.StringMetricType -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.getContextWithMockedInfo -import mozilla.components.service.glean.getMockWebServer -import mozilla.components.service.glean.getWorkerStatus -import mozilla.components.service.glean.resetGlean -import mozilla.components.service.glean.triggerWorkManager -import mozilla.components.service.glean.utils.getISOTimeString -import mozilla.components.service.glean.utils.parseISOTimeString -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.anyString -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.robolectric.RobolectricTestRunner -import java.util.Calendar -import java.util.concurrent.TimeUnit as AndroidTimeUnit - -@RunWith(RobolectricTestRunner::class) -class MetricsPingSchedulerTest { - - private val context: Context - get() = ApplicationProvider.getApplicationContext() - - private fun kotlinFriendlyAny(): T { - // This is required to work around the Kotlin/ArgumentMatchers problem with using - // `verify` on non-nullable arguments (since `any` may return null). See - // https://github.com/nhaarman/mockito-kotlin/issues/241 - ArgumentMatchers.any() - // This weird cast was suggested as part of https://youtrack.jetbrains.com/issue/KT-8135 . - // See the related issue for why this is needed in mocks. - @Suppress("UNCHECKED_CAST") - return null as T - } - - @Before - fun setup() { - WorkManagerTestInitHelper.initializeTestWorkManager( - ApplicationProvider.getApplicationContext()) - - Glean.enableTestingMode() - } - - @Test - fun `milliseconds until the due time must be correctly computed`() { - val metricsPingScheduler = MetricsPingScheduler( - ApplicationProvider.getApplicationContext()) - - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 3, 0, 0) - - // We expect the function to return 1 hour, in milliseconds. - assertEquals(60 * 60 * 1000, - metricsPingScheduler.getMillisecondsUntilDueTime( - sendTheNextCalendarDay = false, now = fakeNow, dueHourOfTheDay = 4) - ) - - // If we're exactly at 4am, there must be no delay. - fakeNow.set(2015, 6, 11, 4, 0, 0) - assertEquals(0, - metricsPingScheduler.getMillisecondsUntilDueTime( - sendTheNextCalendarDay = false, now = fakeNow, dueHourOfTheDay = 4) - ) - - // Set the clock to after 4 of some minutes. - fakeNow.set(2015, 6, 11, 4, 5, 0) - - // Since `sendTheNextCalendarDay` is false, this will be overdue, returning 0. - assertEquals(0, - metricsPingScheduler.getMillisecondsUntilDueTime( - sendTheNextCalendarDay = false, now = fakeNow, dueHourOfTheDay = 4) - ) - - // With `sendTheNextCalendarDay` true, we expect the function to return 23 hours - // and 55 minutes, in milliseconds. - assertEquals(23 * 60 * 60 * 1000 + 55 * 60 * 1000, - metricsPingScheduler.getMillisecondsUntilDueTime( - sendTheNextCalendarDay = true, now = fakeNow, dueHourOfTheDay = 4) - ) - } - - @Test - fun `getDueTimeForToday must correctly return the due time for the current day`() { - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 3, 0, 0) - - val expected = Calendar.getInstance() - expected.time = fakeNow.time - expected.set(Calendar.HOUR_OF_DAY, 4) - - assertEquals(expected, mps.getDueTimeForToday(fakeNow, 4)) - - // Let's check what happens at "midnight". - fakeNow.set(2015, 6, 11, 0, 0, 0) - assertEquals(expected, mps.getDueTimeForToday(fakeNow, 4)) - } - - @Test - fun `isAfterDueTime must report false before the due time on the same calendar day`() { - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - - val fakeNow = Calendar.getInstance() - fakeNow.clear() - - // Shortly before. - fakeNow.set(2015, 6, 11, 3, 0, 0) - assertFalse(mps.isAfterDueTime(fakeNow, 4)) - - // The same hour. - fakeNow.set(2015, 6, 11, 4, 0, 0) - assertFalse(mps.isAfterDueTime(fakeNow, 4)) - - // Midnight. - fakeNow.set(2015, 6, 11, 0, 0, 0) - assertFalse(mps.isAfterDueTime(fakeNow, 4)) - } - - @Test - fun `isAfterDueTime must report true after the due time on the same calendar day`() { - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - - val fakeNow = Calendar.getInstance() - fakeNow.clear() - - // Shortly after. - fakeNow.set(2015, 6, 11, 4, 1, 0) - assertTrue(mps.isAfterDueTime(fakeNow, 4)) - } - - @Test - fun `getLastCollectedDate must report null when no stored date is available`() { - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - mps.sharedPreferences.edit().clear().apply() - - assertNull( - "null must be reported when no date is stored", - mps.getLastCollectedDate() - ) - } - - @Test - fun `getLastCollectedDate must report null when the stored date is corrupted`() { - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - mps.sharedPreferences - .edit() - .putLong(MetricsPingScheduler.LAST_METRICS_PING_SENT_DATETIME, 123L) - .apply() - - // Wrong key type should trigger returning null. - assertNull( - "null must be reported when no date is stored", - mps.getLastCollectedDate() - ) - - // Wrong date format string should trigger returning null. - mps.sharedPreferences - .edit() - .putString(MetricsPingScheduler.LAST_METRICS_PING_SENT_DATETIME, "not-an-ISO-date") - .apply() - - assertNull( - "null must be reported when the date key is of unexpected format", - mps.getLastCollectedDate() - ) - } - - @Test - fun `getLastCollectedDate must report the stored last collected date, if available`() { - val testDate = "2018-12-19T12:36:00-06:00" - val mps = MetricsPingScheduler(ApplicationProvider.getApplicationContext()) - mps.updateSentDate(testDate) - - val expectedDate = parseISOTimeString(testDate)!! - assertEquals( - "The date the ping was collected must be reported", - expectedDate, - mps.getLastCollectedDate() - ) - } - - @Test - fun `collectMetricsPing must update the last sent date and reschedule the collection`() { - val mpsSpy = spy( - MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - - // Ensure we have the right assumptions in place: the methods were not called - // prior to |collectPingAndReschedule|. - verify(mpsSpy, times(0)).updateSentDate(anyString()) - verify(mpsSpy, times(0)).schedulePingCollection( - kotlinFriendlyAny(), - anyBoolean() - ) - - mpsSpy.collectPingAndReschedule(Calendar.getInstance()) - - // Verify that we correctly called in the methods. - verify(mpsSpy, times(1)).updateSentDate(anyString()) - verify(mpsSpy, times(1)).schedulePingCollection( - kotlinFriendlyAny(), - anyBoolean() - ) - } - - @Test - fun `collectMetricsPing must correctly trigger the collection of the metrics ping`() { - // Setup a test server and make Glean point to it. - val server = getMockWebServer() - - resetGlean(getContextWithMockedInfo(), Configuration( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - try { - // Setup a testing metric and set it to some value. - val testMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("metrics") - ) - - val expectedValue = "test-only metric" - testMetric.set(expectedValue) - assertTrue("The initial test data must have been recorded", testMetric.testHasValue()) - - // Manually call the function to trigger the collection. - Glean.metricsPingScheduler.collectPingAndReschedule(Calendar.getInstance()) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - // Fetch the ping from the server and decode its JSON body. - val request = server.takeRequest(20L, AndroidTimeUnit.SECONDS) - val metricsJsonData = request.body.readUtf8() - val metricsJson = JSONObject(metricsJsonData) - - // Validate the received data. - checkPingSchema(metricsJson) - assertEquals("The received ping must be a 'metrics' ping", - "metrics", metricsJson.getJSONObject("ping_info")["ping_type"]) - assertEquals( - "The reported metric must contain the expected value", - expectedValue, - metricsJson.getJSONObject("metrics") - .getJSONObject("string") - .getString("telemetry.string_metric") - ) - } finally { - server.shutdown() - } - } - - @Test - fun `startupCheck must immediately collect if the ping is overdue for today`() { - // Set the current system time to a known datetime. - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 7, 0, 0) - - // Set the last sent date to a previous day, so that today's ping is overdue. - val mpsSpy = - spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - val overdueTestDate = "2015-07-05T12:36:00-06:00" - mpsSpy.updateSentDate(overdueTestDate) - - MetricsPingScheduler.isInForeground = true - - verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny()) - - // Make sure to return the fake date when requested. - doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance() - - // Trigger the startup check. We need to wrap this in `blockDispatchersAPI` since - // the immediate startup collection happens in the Dispatchers.API context. If we - // don't, test will fail due to async weirdness. - mpsSpy.schedule() - - // And that we're storing the current date (this only reports the date, not the time). - fakeNow.set(Calendar.HOUR_OF_DAY, 0) - assertEquals(fakeNow.time, mpsSpy.getLastCollectedDate()) - - // Verify that we're immediately collecting. - verify(mpsSpy, times(1)).collectPingAndReschedule(fakeNow) - } - - @Test - fun `startupCheck must schedule collection for the next calendar day if collection already happened`() { - // Set the current system time to a known datetime. - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 7, 0, 0) - SystemClock.setCurrentTimeMillis(fakeNow.timeInMillis) - - // Set the last sent date to now. - val mpsSpy = - spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - mpsSpy.updateSentDate(getISOTimeString(fakeNow, truncateTo = TimeUnit.Day)) - - verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny(), anyBoolean()) - - // Make sure to return the fake date when requested. - doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance() - - // Trigger the startup check. - mpsSpy.schedule() - - // Verify that we're scheduling for the next day and not collecting immediately. - verify(mpsSpy, times(1)).schedulePingCollection(fakeNow, sendTheNextCalendarDay = true) - verify(mpsSpy, never()).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false) - verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny()) - } - - @Test - fun `startupCheck must schedule collection for later today if it's before the due time`() { - // Set the current system time to a known datetime. - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 2, 0, 0) - SystemClock.setCurrentTimeMillis(fakeNow.timeInMillis) - - // Set the last sent date to yesterday. - val mpsSpy = - spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - - val fakeYesterday = Calendar.getInstance() - fakeYesterday.time = fakeNow.time - fakeYesterday.add(Calendar.DAY_OF_MONTH, -1) - mpsSpy.updateSentDate(getISOTimeString(fakeYesterday, truncateTo = TimeUnit.Day)) - - // Make sure to return the fake date when requested. - doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance() - - verify(mpsSpy, never()).schedulePingCollection(kotlinFriendlyAny(), anyBoolean()) - - // Trigger the startup check. - mpsSpy.schedule() - - // Verify that we're scheduling for today, but not collecting immediately. - verify(mpsSpy, times(1)).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false) - verify(mpsSpy, never()).schedulePingCollection(fakeNow, sendTheNextCalendarDay = true) - verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny()) - } - - @Test - fun `startupCheck must correctly handle fresh installs (before due time)`() { - // Set the current system time to a known datetime: before 4am local. - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 3, 0, 0) - - // Clear the last sent date. - val mpsSpy = - spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - mpsSpy.sharedPreferences.edit().clear().apply() - - verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny()) - - // Make sure to return the fake date when requested. - doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance() - - // Trigger the startup check. - mpsSpy.schedule() - - // Verify that we're immediately collecting. - verify(mpsSpy, never()).collectPingAndReschedule(fakeNow) - verify(mpsSpy, times(1)).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false) - } - - @Test - fun `startupCheck must correctly handle fresh installs (after due time)`() { - // Set the current system time to a known datetime: after 4am local. - val fakeNow = Calendar.getInstance() - fakeNow.clear() - fakeNow.set(2015, 6, 11, 6, 0, 0) - - // Clear the last sent date. - val mpsSpy = - spy(MetricsPingScheduler(ApplicationProvider.getApplicationContext())) - mpsSpy.sharedPreferences.edit().clear().apply() - - verify(mpsSpy, never()).collectPingAndReschedule(kotlinFriendlyAny()) - - // Make sure to return the fake date when requested. - doReturn(fakeNow).`when`(mpsSpy).getCalendarInstance() - - // Trigger the startup check. - mpsSpy.schedule() - - // And that we're storing the current date (this only reports the date, not the time). - fakeNow.set(Calendar.HOUR_OF_DAY, 0) - assertEquals( - "The scheduler must save the date the ping was collected", - fakeNow.time, - mpsSpy.getLastCollectedDate() - ) - - // Verify that we're immediately collecting. - verify(mpsSpy, times(1)).collectPingAndReschedule(fakeNow) - verify(mpsSpy, never()).schedulePingCollection(fakeNow, sendTheNextCalendarDay = false) - } - - @Test - fun `schedulePingCollection must correctly append a work request to the WorkManager`() { - // Replacing the singleton's metricsPingScheduler here since doWork() refers to it when - // the worker runs, otherwise we can get a lateinit property is not initialized error. - Glean.metricsPingScheduler = MetricsPingScheduler(context) - MetricsPingScheduler.isInForeground = true - - // No work should be enqueued at the beginning of the test. - assertFalse(getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - - // Manually schedule a collection task for today. - Glean.metricsPingScheduler.schedulePingCollection(Calendar.getInstance(), sendTheNextCalendarDay = false) - - // We expect the worker to be scheduled. - assertTrue(getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - - resetGlean(clearStores = true) - } - - @Test - fun `schedule() happens when returning from background when Glean is already initialized`() { - // Initialize Glean - resetGlean() - - // We expect the worker to not be scheduled. - assertFalse(getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - - // Simulate returning to the foreground with glean initialized. - Glean.metricsPingScheduler.onEnterForeground() - - // We expect the worker to be scheduled. - assertTrue(getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - } - - @Test - fun `cancel() correctly cancels worker`() { - val mps = MetricsPingScheduler(context) - - mps.schedulePingCollection(Calendar.getInstance(), true) - - // Verify that the worker is enqueued - assertTrue("MetricsPingWorker is enqueued", - getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - - // Cancel the worker - MetricsPingScheduler.cancel(context) - - // Verify worker has been cancelled - assertFalse("MetricsPingWorker is not enqueued", - getWorkerStatus(context, MetricsPingWorker.TAG).isEnqueued) - } - - // @Test - // fun `Glean should close the measurement window for overdue pings before recording new data`() { - // // This test is a bit tricky: we want to make sure that, when our metrics ping is overdue - // // and collected at startup (if there's pending data), we don't mistakenly add new collected - // // data to it. In order to test for this specific edge case, we resort to the following: - // // record some data, then "pretend Glean is disabled" to simulate a crash, start using the - // // recording API off the main thread, init Glean in a separate thread and trigger a metrics - // // ping at startup. We expect the initially written data to be there ("expected_data!"), but - // // not the "should_not_be_recorded", which will be reported in a separate ping. - - // // Create a string metric with a Ping lifetime. - // val stringMetric = StringMetricType( - // disabled = false, - // category = "telemetry", - // lifetime = Lifetime.Ping, - // name = "string_metric", - // sendInPings = listOf("metrics") - // ) - - // // Start Glean in the current thread, clean the local storage. - // resetGlean() - - // // Record the data we expect to be in the final metrics ping. - // val expectedValue = "expected_data!" - // stringMetric.set(expectedValue) - // assertTrue("The initial expected data must be recorded", stringMetric.testHasValue()) - - // // Pretend Glean is disabled. This is used so that any API call will be discarded and - // // Glean will init again. - // Glean.initialized = false - - // // Start the web-server that will receive the metrics ping. - // val server = getMockWebServer() - - // // Set the current system time to a known datetime: this should make the metrics ping - // // overdue and trigger it at startup. - // val fakeNow = Calendar.getInstance() - // fakeNow.clear() - // fakeNow.set(2015, 6, 11, 7, 0, 0) - // SystemClock.setCurrentTimeMillis(fakeNow.timeInMillis) - - // // Start recording to the metric asynchronously, from a separate thread. If something - // // goes wrong with our init, we should see the value set in the loop below in the triggered - // // "metrics" ping. - // var stopWrites = false - // val stringWriter = GlobalScope.async { - // do { - // stringMetric.set("should_not_be_recorded") - // } while (!stopWrites) - // } - - // try { - // // Restart Glean in a separate thread to simulate a crash/restart without - // // clearing the local storage. - // val asyncInit = GlobalScope.async { - // Glean.initialize(getContextWithMockedInfo(), Configuration( - // serverEndpoint = "http://" + server.hostName + ":" + server.port, - // logPings = true - // )) - - // // Trigger worker task to upload the pings in the background. - // triggerWorkManager() - // } - - // // Wait for the metrics ping to be received. - // val request = server.takeRequest(20L, AndroidTimeUnit.SECONDS) - - // // Stop recording to the test metric and wait for the async stuff - // // to complete. - // runBlocking { - // stopWrites = true - // stringWriter.await() - // asyncInit.await() - // } - - // // Parse the received ping payload to a JSON object. - // val metricsJsonData = request.body.readUtf8() - // val metricsJson = JSONObject(metricsJsonData) - - // // Validate the received data. - // checkPingSchema(metricsJson) - // assertEquals("The received ping must be a 'metrics' ping", - // "metrics", metricsJson.getJSONObject("ping_info")["ping_type"]) - // assertEquals( - // "The reported metric must contain the expected value", - // expectedValue, - // metricsJson.getJSONObject("metrics") - // .getJSONObject("string") - // .getString("telemetry.string_metric") - // ) - // } finally { - // server.shutdown() - // } - // } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/PingUploadWorkerTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/PingUploadWorkerTest.kt deleted file mode 100644 index 5bde81bb456..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/scheduler/PingUploadWorkerTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.scheduler - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.work.BackoffPolicy -import androidx.work.NetworkType -import androidx.work.WorkerParameters -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.getWorkerStatus -import mozilla.components.service.glean.resetGlean -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class PingUploadWorkerTest { - - private val context: Context - get() = ApplicationProvider.getApplicationContext() - - @Mock - var workerParams: WorkerParameters? = null - - private var pingUploadWorker: PingUploadWorker? = null - - @Before - @Throws(Exception::class) - fun setUp() { - MockitoAnnotations.initMocks(this) - resetGlean(context, config = Configuration().copy(logPings = true)) - pingUploadWorker = PingUploadWorker(context, workerParams!!) - } - - @Test - fun testPingConfiguration() { - // Set the constraints around which the worker can be run, in this case it - // only requires that any network connection be available. - val workRequest = PingUploadWorker.buildWorkRequest() - val workSpec = workRequest.workSpec - - // verify constraints - Assert.assertEquals(NetworkType.CONNECTED, workSpec.constraints.requiredNetworkType) - Assert.assertEquals(BackoffPolicy.EXPONENTIAL, workSpec.backoffPolicy) - Assert.assertTrue(workRequest.tags.contains(PingUploadWorker.PING_WORKER_TAG)) - } - - @Test - fun testDoWorkSuccess() { - val result = pingUploadWorker!!.doWork() - Assert.assertTrue(result.toString().contains("Success")) - } - - @Test - fun `cancel() correctly cancels worker`() { - PingUploadWorker.enqueueWorker(context) - - // Verify that the worker is enqueued - Assert.assertTrue("PingUploadWorker is enqueued", - getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - - // Cancel the worker - PingUploadWorker.cancel(context) - - // Verify worker has been cancelled - Assert.assertFalse("PingUploadWorker is not enqueued", - getWorkerStatus(context, PingUploadWorker.PING_WORKER_TAG).isEnqueued) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/BooleansStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/BooleansStorageEngineTest.kt deleted file mode 100644 index 964f57d55f6..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/BooleansStorageEngineTest.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.BooleanMetricType -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class BooleansStorageEngineTest { - - @Before - fun setUp() { - BooleansStorageEngine.applicationContext = ApplicationProvider.getApplicationContext() - BooleansStorageEngine.clearAllStores() - } - - @Test - fun `boolean deserializer should correctly parse booleans`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_number" to 1, - "store1#telemetry.bool" to true, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_string" to "test" - ) - - val storageEngine = BooleansStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(true, snapshot["telemetry.bool"]) - } - - @Test - fun `boolean serializer should correctly serialize booleans`() { - run { - val storageEngine = BooleansStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "boolean_metric", - sendInPings = listOf("store1") - ) - - // Record the boolean in the store, without providing optional arguments. - storageEngine.record( - metric, - value = true - ) - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.boolean_metric\":true}", - snapshot.toString()) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = BooleansStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.boolean_metric\":true}", - snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "boolean_metric", - sendInPings = storeNames - ) - - // Record the boolean in the stores, without providing optional arguments. - BooleansStorageEngine.record( - metric, - value = true - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = BooleansStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(true, snapshot.get("telemetry.boolean_metric")) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - BooleansStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val metric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "boolean_metric", - sendInPings = storeNames - ) - - // Record the boolean in the stores, without providing optional arguments. - BooleansStorageEngine.record( - metric, - value = true - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = BooleansStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - BooleansStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = BooleansStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `Booleans are serialized in the correct JSON format`() { - val metric = BooleanMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "boolean_metric", - sendInPings = listOf("store1") - ) - - // Record the boolean in the store, without providing optional arguments. - BooleansStorageEngine.record( - metric, - value = true - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = BooleansStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - BooleansStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.boolean_metric\":true}", - snapshot.toString()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CountersStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CountersStorageEngineTest.kt deleted file mode 100644 index 943777c52bd..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CountersStorageEngineTest.kt +++ /dev/null @@ -1,219 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.CounterMetricType -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertFalse -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class CountersStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `counter deserializer should correctly parse integers`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.valid" to 1 - ) - - val storageEngine = CountersStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(1, snapshot["telemetry.valid"]) - } - - @Test - fun `counter serializer should correctly serialize counters`() { - run { - val storageEngine = CountersStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Record the counter in the store - storageEngine.record( - metric, - amount = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.counter_metric\":1}", snapshot.toString()) - } - - // Re-instantiate storage engine to validate serialization from storage rather than cache - run { - val storageEngine = CountersStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get the snapshot from "store1" and clear it. - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.counter_metric\":1}", snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "counter_metric", - sendInPings = storeNames - ) - - // Record the counter in the stores, without providing optional arguments. - CountersStorageEngine.record( - metric, - amount = 1 - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = CountersStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(1, snapshot.get("telemetry.counter_metric")) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - CountersStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val metric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "counter_metric", - sendInPings = storeNames - ) - - // Record the counter in the stores, without providing optional arguments. - CountersStorageEngine.record( - metric, - amount = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = CountersStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - CountersStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = CountersStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `counters are serialized in the correct JSON format`() { - val metric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Record the counter in the store - CountersStorageEngine.record( - metric, - amount = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = CountersStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - CountersStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.counter_metric\":1}", - snapshot.toString()) - } - - @Test - fun `counters must not increment when passed zero or negative`() { - // Define a 'counterMetric' counter metric, which will be stored in "store1". - val counterMetric = CounterMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "counter_metric", - sendInPings = listOf("store1") - ) - - // Attempt to increment the counter with zero - counterMetric.add(0) - // Check that nothing was recorded. - assertFalse("Counters must not be recorded if incremented with zero", - counterMetric.testHasValue()) - - // Attempt to increment the counter with negative - counterMetric.add(-1) - // Check that nothing was recorded. - assertFalse("Counters must not be recorded if incremented with negative", - counterMetric.testHasValue()) - - // Make sure that the errors have been recorded - assertEquals(2, testGetNumRecordedErrors(counterMetric, ErrorType.InvalidValue)) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngineTest.kt deleted file mode 100644 index a3325fcd50d..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/CustomDistributionsStorageEngineTest.kt +++ /dev/null @@ -1,457 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.runBlocking -import mozilla.components.service.glean.collectAndCheckPingSchema -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.CustomDistributionMetricType -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.PrecomputedHistogram -import mozilla.components.service.glean.private.HistogramType -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class CustomDistributionsStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `accumulate() properly updates the values in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "test_custom_distribution", - sendInPings = storeNames, - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Create a sample that will fall into the underflow bucket (bucket '0') so we can easily - // find it - val sample = 1L - CustomDistributionsStorageEngine.accumulateSamples( - metricData = metric, - samples = listOf(sample).toLongArray(), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = CustomDistributionsStorageEngine.getSnapshot( - storeName = storeName, - clearStore = true - ) - assertEquals(1, snapshot!!.size) - assertEquals(1L, snapshot["telemetry.test_custom_distribution"]?.values!![1]) - } - } - - @Test - fun `deserializer should correctly parse custom distributions`() { - val td = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.invalid_list" to listOf("1", "2", "3"), - "store1#telemetry.invalid_int_list" to "[1,2,3]", - "store1#telemetry.invalid_td_bucket_count" to "{\"bucket_count\":\"not an int!\",\"range\":[0,60000,12],\"histogram_type\":1,\"values\":{},\"sum\":0}", - "store1#telemetry.invalid_td_range" to "{\"bucket_count\":100,\"range\":[0,60000,12],\"histogram_type\":1,\"values\":{},\"sum\":0}", - "store1#telemetry.invalid_td_range2" to "{\"bucket_count\":100,\"range\":[\"not\",\"numeric\"],\"histogram_type\":1,\"values\":{},\"sum\":0}", - "store1#telemetry.invalid_td_histogram_type" to "{\"bucket_count\":100,\"range\":[0,60000,12],\"histogram_type\":-1,\"values\":{},\"sum\":0}", - "store1#telemetry.invalid_td_values" to "{\"bucket_count\":100,\"range\":[0,60000,12],\"histogram_type\":1,\"values\":{\"0\": \"nope\"},\"sum\":0}", - "store1#telemetry.invalid_td_sum" to "{\"bucket_count\":100,\"range\":[0,60000,12],\"histogram_type\":1,\"values\":{},\"sum\":\"nope\"}", - "store1#telemetry.test_custom_distribution" to td.toJsonObject().toString() - ) - - val storageEngine = CustomDistributionsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = Mockito.mock(Context::class.java) - val sharedPreferences = Mockito.mock(SharedPreferences::class.java) - Mockito.`when`(sharedPreferences.all).thenAnswer { persistedSample } - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq(storageEngine::class.java.canonicalName), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(td.toJsonObject().toString(), - snapshot["telemetry.test_custom_distribution"]?.toJsonObject().toString()) - } - - @Test - fun `serializer should serialize custom distribution that matches schema`() { - val ping1 = PingType("store1", true) - - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - metric.accumulateSamples(listOf(1000000L).toLongArray()) - - collectAndCheckPingSchema(ping1) - } - - @Test - fun `serializer should correctly serialize custom distributions`() { - run { - val storageEngine = CustomDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1", "store2"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Using the PrecomputedHistogram object here to easily turn the object into JSON - // for comparison purposes. - val td = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - td.accumulate(100L) - - runBlocking { - storageEngine.accumulateSamples( - metricData = metric, - samples = listOf(100L).toLongArray(), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - } - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"${metric.identifier}\":${td.toJsonPayloadObject()}}", - json.toString() - ) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = CustomDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val td = PrecomputedHistogram( - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - td.accumulate(100L) - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"telemetry.test_custom_distribution\":${td.toJsonPayloadObject()}}", - json.toString() - ) - } - } - - @Test - fun `custom distributions must not accumulate negative values`() { - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Attempt to accumulate a negative sample - metric.accumulateSamples(listOf(-1L).toLongArray()) - // Check that nothing was recorded. - assertFalse("Custom distributions must not accumulate negative values", - metric.testHasValue()) - - // Make sure that the errors have been recorded - assertEquals("Accumulating negative values must generate an error", - 1, - ErrorRecording.testGetNumRecordedErrors(metric, ErrorRecording.ErrorType.InvalidValue)) - } - - @Test - fun `underflow values accumulate in the first bucket`() { - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Attempt to accumulate an overflow sample - metric.accumulateSamples(listOf(0L).toLongArray()) - - // Check that custom distribution was recorded. - assertTrue("Accumulating underflow values records data", - metric.testHasValue()) - - // Make sure that the underflow landed in the correct (first) bucket - val snapshot = metric.testGetValue() - assertEquals("Accumulating overflow values should increment underflow bucket", - 1L, - snapshot.values[0]) - } - - @Test - fun `overflow values accumulate in the last bucket`() { - val rangeMax = 60000L - - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Attempt to accumulate an overflow sample - metric.accumulateSamples(listOf(rangeMax + 100 * 1000000).toLongArray()) - - // Check that custom distribution was recorded. - assertTrue("Accumulating overflow values records data", - metric.testHasValue()) - - // Make sure that the overflow landed in the correct (last) bucket - val snapshot = metric.testGetValue() - assertEquals("Accumulating overflow values should increment last bucket", - 1L, - snapshot.values[rangeMax]) - } - - @Test - fun `getBuckets() correctly populates the buckets properly for exponential distributions`() { - // Hand calculated values using current default range 0 - 60000 and bucket count of 100. - // NOTE: The final bucket, regardless of width, represents the overflow bucket to hold any - // values beyond the maximum (in this case the maximum is 60000) - val testBuckets: List = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, - 19, 21, 23, 25, 28, 31, 34, 38, 42, 46, 51, 56, 62, 68, 75, 83, 92, 101, 111, 122, 135, - 149, 164, 181, 200, 221, 244, 269, 297, 328, 362, 399, 440, 485, 535, 590, 651, 718, - 792, 874, 964, 1064, 1174, 1295, 1429, 1577, 1740, 1920, 2118, 2337, 2579, 2846, 3140, - 3464, 3822, 4217, 4653, 5134, 5665, 6250, 6896, 7609, 8395, 9262, 10219, 11275, 12440, - 13726, 15144, 16709, 18436, 20341, 22443, 24762, 27321, 30144, 33259, 36696, 40488, - 44672, 49288, 54381, 60000) - - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Accumulate a sample to force the lazy loading of `buckets` to occur - metric.accumulateSamples(listOf(0L).toLongArray()) - - // Check that custom distribution was recorded. - assertTrue("Accumulating values records data", metric.testHasValue()) - - // Make sure that the sample in the correct (underflow) bucket - val snapshot = metric.testGetValue() - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[0]) - - // verify buckets lists worked - assertNotNull("Buckets must not be null", snapshot.buckets) - - assertEquals("Bucket calculation failed", testBuckets, snapshot.buckets) - } - - @Test - fun `accumulate finds the correct bucket for exponential distributions`() { - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Exponential - ) - - // Check that a few values correctly fall into the correct buckets (as calculated by hand) - // to validate the linear bucket search algorithm - - // Attempt to accumulate a sample to force metric to be stored - metric.accumulateSamples(listOf(1L, 10L, 100L, 1000L, 10000L).toLongArray()) - - // Check that custom distribution was recorded. - assertTrue("Accumulating values records data", metric.testHasValue()) - - // Make sure that the samples are in the correct buckets - val snapshot = metric.testGetValue() - - // Check sum and count - assertEquals("Accumulating updates the sum", 11111, snapshot.sum) - assertEquals("Accumulating updates the count", 5, snapshot.count) - - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[1]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[10]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[92]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[964]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[9262]) - } - - @Test - fun `accumulate finds the correct bucket for linear distributions`() { - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Linear - ) - - // Check that a few values correctly fall into the correct buckets (as calculated by hand) - // to validate the linear bucket search algorithm - - // Attempt to accumulate a sample to force metric to be stored - metric.accumulateSamples(listOf(1L, 10L, 100L, 1000L, 10000L).toLongArray()) - - // Check that custom distribution was recorded. - assertTrue("Accumulating values records data", metric.testHasValue()) - - // Make sure that the samples are in the correct buckets - val snapshot = metric.testGetValue() - - // Check sum and count - assertEquals("Accumulating updates the sum", 11111, snapshot.sum) - assertEquals("Accumulating updates the count", 5, snapshot.count) - - assertEquals("Accumulating should increment correct bucket", - 3L, snapshot.values[0]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[612]) - assertEquals("Accumulating should increment correct bucket", - 1L, snapshot.values[9796]) - } - - @Test - fun `toJsonPayloadObject correctly inserts zero buckets`() { - // Define a custom distribution metric, which will be stored in "store1". - val metric = CustomDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_custom_distribution", - sendInPings = listOf("store1"), - rangeMin = 0L, - rangeMax = 60000L, - bucketCount = 100, - histogramType = HistogramType.Linear - ) - - metric.accumulateSamples(listOf(10000L).toLongArray()) - - // Make sure that the samples are in the correct buckets - val snapshot = metric.testGetValue() - val payload = snapshot.toJsonPayloadObject() - val payloadValues = payload.getJSONObject("values") - - assertEquals(18, payloadValues.length()) - assertEquals(1L, payloadValues["9796"]) - for (key in payloadValues.keys()) { - if (key != "9796") { - assertEquals(0L, payloadValues[key]) - } - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/DatetimesStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/DatetimesStorageEngineTest.kt deleted file mode 100644 index 700bdd53102..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/DatetimesStorageEngineTest.kt +++ /dev/null @@ -1,283 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.DatetimeMetricType -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner -import java.util.Calendar -import java.util.TimeZone - -@RunWith(RobolectricTestRunner::class) -class DatetimesStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `datetime deserializer should correctly parse datetimes`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_date" to "2019-13-01T00:00:00Z", - "store1#telemetry.null" to null, - "store1#telemetry.invalid_string" to "test", - "store1#telemetry.valid_date1" to "1993-02-23T09:05:23.320-08:00", - "store1#telemetry.valid_date2" to "1993-02-23T09:05:23-08:00", - "store1#telemetry.valid_date3" to "1993-02-23T09:05-08:00", - "store1#telemetry.valid_date4" to "1993-02-23T09-08:00", - "store1#telemetry.valid_date5" to "1993-02-23-08:00" - ) - - val storageEngine = DatetimesStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(5, snapshot!!.size) - assertEquals("1993-02-23T09:05:23.320-08:00", snapshot["telemetry.valid_date1"]) - assertEquals("1993-02-23T09:05:23-08:00", snapshot["telemetry.valid_date2"]) - assertEquals("1993-02-23T09:05-08:00", snapshot["telemetry.valid_date3"]) - assertEquals("1993-02-23T09-08:00", snapshot["telemetry.valid_date4"]) - assertEquals("1993-02-23-08:00", snapshot["telemetry.valid_date5"]) - } - - @Test - fun `datetime serializer should correctly serialize datetimes`() { - val value = Calendar.getInstance() - value.set(1993, 1, 23, 9, 5, 23) - value.set(Calendar.MILLISECOND, 0) - value.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles")) - - run { - val storageEngine = DatetimesStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "datetime_metric", - sendInPings = listOf("store1") - ) - - // Record the datetime in the store, without providing optional arguments. - storageEngine.set(metric, value) - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.datetime_metric\":\"1993-02-23T09:05-08:00\"}", - snapshot.toString()) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = DatetimesStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.datetime_metric\":\"1993-02-23T09:05-08:00\"}", - snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "datetime_metric", - sendInPings = storeNames - ) - - // Record the datetime in the stores, without providing optional arguments. - DatetimesStorageEngine.set(metric) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - metric.testHasValue(storeName) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - DatetimesStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val metric = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "datetime_metric", - sendInPings = storeNames - ) - - // Record the datetime in the stores, without providing optional arguments. - DatetimesStorageEngine.set(metric) - - // Get the snapshot from "store1" and clear it. - val snapshot = DatetimesStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - DatetimesStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = DatetimesStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `test that truncation works`() { - val value = Calendar.getInstance() - value.set(2004, 11, 9, 8, 3, 29) - value.set(Calendar.MILLISECOND, 320) - value.timeZone = TimeZone.getTimeZone("GMT-2") - - val datetimeNanosecondTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "nanosecond", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Nanosecond - ) - datetimeNanosecondTruncation.set(value) - - val datetimeMillisecondTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "millisecond", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - datetimeMillisecondTruncation.set(value) - - val datetimeSecondTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "second", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Second - ) - datetimeSecondTruncation.set(value) - - val datetimeMinuteTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "minute", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Minute - ) - datetimeMinuteTruncation.set(value) - - val datetimeHourTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "hour", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Hour - ) - datetimeHourTruncation.set(value) - - val datetimeDayTruncation = DatetimeMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "day", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Day - ) - datetimeDayTruncation.set(value) - - // Calendar objects don't have nanosecond resolution, so we don't expect any change, - // but test anyway so we're testing all of the TimeUnit enumeration values. - val expectedNanosecond = Calendar.getInstance() - expectedNanosecond.timeZone = TimeZone.getTimeZone("GMT-2") - expectedNanosecond.set(2004, 11, 9, 8, 3, 29) - expectedNanosecond.set(Calendar.MILLISECOND, 320) - assertEquals(expectedNanosecond.getTime(), datetimeNanosecondTruncation.testGetValue()) - assertEquals("2004-12-09T08:03:29.320-02:00", datetimeNanosecondTruncation.testGetValueAsString()) - - val expectedMillisecond = Calendar.getInstance() - expectedMillisecond.timeZone = TimeZone.getTimeZone("GMT-2") - expectedMillisecond.set(2004, 11, 9, 8, 3, 29) - expectedMillisecond.set(Calendar.MILLISECOND, 320) - assertEquals(expectedMillisecond.getTime(), datetimeMillisecondTruncation.testGetValue()) - assertEquals("2004-12-09T08:03:29.320-02:00", datetimeMillisecondTruncation.testGetValueAsString()) - - val expectedSecond = Calendar.getInstance() - expectedSecond.timeZone = TimeZone.getTimeZone("GMT-2") - expectedSecond.set(2004, 11, 9, 8, 3, 29) - expectedSecond.set(Calendar.MILLISECOND, 0) - assertEquals(expectedSecond.getTime(), datetimeSecondTruncation.testGetValue()) - assertEquals("2004-12-09T08:03:29-02:00", datetimeSecondTruncation.testGetValueAsString()) - - val expectedMinute = Calendar.getInstance() - expectedMinute.timeZone = TimeZone.getTimeZone("GMT-2") - expectedMinute.set(2004, 11, 9, 8, 3, 0) - expectedMinute.set(Calendar.MILLISECOND, 0) - assertEquals(expectedMinute.getTime(), datetimeMinuteTruncation.testGetValue()) - assertEquals("2004-12-09T08:03-02:00", datetimeMinuteTruncation.testGetValueAsString()) - - val expectedHour = Calendar.getInstance() - expectedHour.timeZone = TimeZone.getTimeZone("GMT-2") - expectedHour.set(2004, 11, 9, 8, 0, 0) - expectedHour.set(Calendar.MILLISECOND, 0) - assertEquals(expectedHour.getTime(), datetimeHourTruncation.testGetValue()) - assertEquals("2004-12-09T08-02:00", datetimeHourTruncation.testGetValueAsString()) - - val expectedDay = Calendar.getInstance() - expectedDay.timeZone = TimeZone.getTimeZone("GMT-2") - expectedDay.set(2004, 11, 9, 0, 0, 0) - expectedDay.set(Calendar.MILLISECOND, 0) - assertEquals(expectedDay.getTime(), datetimeDayTruncation.testGetValue()) - assertEquals("2004-12-09-02:00", datetimeDayTruncation.testGetValueAsString()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/EventsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/EventsStorageEngineTest.kt deleted file mode 100644 index ca00900d976..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/EventsStorageEngineTest.kt +++ /dev/null @@ -1,562 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.os.SystemClock -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.checkPingSchema -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.getContextWithMockedInfo -import mozilla.components.service.glean.getMockWebServer -import mozilla.components.service.glean.private.EventMetricType -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.NoExtraKeys -import mozilla.components.service.glean.resetGlean -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.triggerWorkManager -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.util.concurrent.TimeUnit - -// Declared here, since Kotlin can not declare nested enum classes -enum class ExtraKeys { - Key1, - Key2 -} - -enum class SomeExtraKeys { - SomeExtra -} - -enum class TestEventNumberKeys { - TestEventNumber -} - -enum class TruncatedKeys { - Extra1, - TruncatedExtra -} - -@RunWith(RobolectricTestRunner::class) -class EventsStorageEngineTest { - - private val context: Context - get() = ApplicationProvider.getApplicationContext() - - @get:Rule - val gleanRule = GleanTestRule(context) - - @Before - fun setUp() { - assert(Glean.initialized) - EventsStorageEngine.clearAllStores() - } - - @Test - fun `record() properly records without optional arguments`() { - val storeNames = listOf("store1", "store2") - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_no_optional", - lifetime = Lifetime.Ping, - sendInPings = storeNames - ) - - // Record the event in the stores, without providing optional arguments. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime() - ) - - // Check that the data was correctly recorded in each store. - for (storeName in storeNames) { - val snapshot = EventsStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals("telemetry", snapshot.first().category) - assertEquals("test_event_no_optional", snapshot.first().name) - assertNull("The 'extra' must be null if not provided", - snapshot.first().extra) - } - } - - @Test - fun `record() properly records with optional arguments`() { - val storeNames = listOf("store1", "store2") - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_with_optional", - lifetime = Lifetime.Ping, - sendInPings = storeNames, - allowedExtraKeys = listOf("key1", "key2") - ) - - // Record the event in the stores, providing optional arguments. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime(), - extra = mapOf("key1" to "value1", "key2" to "value2") - ) - - // Check that the data was correctly recorded in each store. - for (storeName in storeNames) { - val snapshot = EventsStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals("telemetry", snapshot.first().category) - assertEquals("test_event_with_optional", snapshot.first().name) - assertEquals(mapOf("key1" to "value1", "key2" to "value2"), snapshot.first().extra) - } - } - - @Test - fun `record() computes the correct time between events`() { - val delayTime: Long = 37 - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_time", - lifetime = Lifetime.Ping, - sendInPings = listOf("store1") - ) - - // Sleep a bit Record the event in the store. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime() - ) - SystemClock.sleep(delayTime) - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime() - ) - - val snapshot = EventsStorageEngine.getSnapshot(storeName = "store1", clearStore = false) - assertEquals(2, snapshot!!.size) - assertEquals("telemetry", snapshot.first().category) - assertEquals("test_event_time", snapshot.first().name) - assertNull("The 'extra' must be null if not provided", - snapshot.first().extra) - assertEquals(0, snapshot.first().timestamp) - assertEquals(delayTime, snapshot[1].timestamp) - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - EventsStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_clear", - lifetime = Lifetime.Ping, - sendInPings = storeNames - ) - - // Record the event in the stores, without providing optional arguments. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime() - ) - EventsStorageEngine.testWaitForWrites() - - // Get the snapshot from "store1" and clear it. - val snapshot = EventsStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - EventsStorageEngine.testWaitForWrites() - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - EventsStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - val files = EventsStorageEngine.storageDirectory.listFiles()!! - assertEquals(1, files.size) - assertEquals( - "There should be no events on disk for store1, but there are for store2", - "store2", - EventsStorageEngine.storageDirectory.listFiles()?.first()?.name - ) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = EventsStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - assertEquals("telemetry", s.first().category) - assertEquals("test_event_clear", s.first().name) - assertNull("The 'extra' must be null if not provided", - s.first().extra) - } - } - - @Test - fun `Events are serialized in the correct JSON format (no extra)`() { - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_clear", - lifetime = Lifetime.Ping, - sendInPings = listOf("store1") - ) - - // Record the event in the store, without providing optional arguments. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime() - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = EventsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - EventsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("[{\"timestamp\":0,\"category\":\"telemetry\",\"name\":\"test_event_clear\"}]", - snapshot.toString()) - } - - @Test - fun `Events are serialized in the correct JSON format (with extra)`() { - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event_clear", - lifetime = Lifetime.Ping, - sendInPings = listOf("store1"), - allowedExtraKeys = listOf("someExtra") - ) - - // Record the event in the store, without providing optional arguments. - EventsStorageEngine.record( - metricData = event, - monotonicElapsedMs = SystemClock.elapsedRealtime(), - extra = mapOf("someExtra" to "field") - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = EventsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - EventsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("[{\"timestamp\":0,\"category\":\"telemetry\",\"name\":\"test_event_clear\",\"extra\":{\"someExtra\":\"field\"}}]", - snapshot.toString()) - } - - @Test - fun `test sending of event ping when it fills up`() { - val server = getMockWebServer() - - val click = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "click", - sendInPings = listOf("events"), - allowedExtraKeys = listOf("test_event_number") - ) - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - try { - // We send 510 events. We expect to get the first 500 in the ping, and 10 remaining afterward - for (i in 0..509) { - click.record(extra = mapOf(TestEventNumberKeys.TestEventNumber to "$i")) - } - - assertTrue(click.testHasValue()) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - val request = server.takeRequest(20L, TimeUnit.SECONDS) - val applicationId = "mozilla-components-service-glean" - assert(request.path.startsWith("/submit/$applicationId/events/${Glean.SCHEMA_VERSION}/")) - val eventsJsonData = request.body.readUtf8() - val eventsJson = checkPingSchema(eventsJsonData) - val eventsArray = eventsJson.getJSONArray("events") - assertEquals(500, eventsArray.length()) - - for (i in 0..499) { - assertEquals("$i", eventsArray.getJSONObject(i).getJSONObject("extra")["test_event_number"]) - } - } finally { - server.shutdown() - } - - val remaining = EventsStorageEngine.getSnapshot("events", false)!! - assertEquals(10, remaining.size) - for (i in 0..9) { - assertEquals("${i + 500}", remaining[i].extra?.get("test_event_number")) - } - } - - @Test - fun `'extra' keys must be recorded and truncated if needed`() { - val testEvent = EventMetricType( - disabled = false, - category = "ui", - lifetime = Lifetime.Ping, - name = "testEvent", - sendInPings = listOf("store1"), - allowedExtraKeys = listOf("extra1", "truncatedExtra") - ) - - val testValue = "LeanGleanByFrank" - testEvent.record( - extra = mapOf( - TruncatedKeys.Extra1 to testValue, - TruncatedKeys.TruncatedExtra to testValue.repeat(10) - ) - ) - - // Check that nothing was recorded. - val snapshot = testEvent.testGetValue() - assertEquals(1, snapshot.size) - assertEquals("ui", snapshot.first().category) - assertEquals("testEvent", snapshot.first().name) - - assertTrue( - "'extra' keys must be correctly recorded and truncated", - mapOf( - "extra1" to testValue, - "truncatedExtra" to (testValue.repeat(10)).substring(0, EventsStorageEngine.MAX_LENGTH_EXTRA_KEY_VALUE) - ) == snapshot.first().extra) - assertEquals(1, testGetNumRecordedErrors(testEvent, ErrorType.InvalidValue)) - } - - @Test - fun `flush queued events on startup`() { - assertEquals( - "There should be no events on disk to start", - 0, - EventsStorageEngine.storageDirectory.listFiles()?.size - ) - - val server = getMockWebServer() - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event", - lifetime = Lifetime.Ping, - sendInPings = listOf("events"), - allowedExtraKeys = listOf("someExtra") - ) - - event.record(extra = mapOf(SomeExtraKeys.SomeExtra to "bar")) - assertEquals(1, event.testGetValue().size) - - // Clear the in-memory storage only to mock being loaded in a fresh process - EventsStorageEngine.eventStores.clear() - resetGlean( - getContextWithMockedInfo(), - Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - ), - clearStores = false - ) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - val request = server.takeRequest(20L, TimeUnit.SECONDS) - assertEquals("POST", request.method) - val applicationId = "mozilla-components-service-glean" - assert( - request.path.startsWith("/submit/$applicationId/events/${Glean.SCHEMA_VERSION}/") - ) - val pingJsonData = request.body.readUtf8() - val pingJson = JSONObject(pingJsonData) - checkPingSchema(pingJson) - assertNotNull(pingJson.opt("events")) - assertEquals( - 1, - pingJson.getJSONArray("events").length() - ) - - EventsStorageEngine.clearAllStores() - } - - @kotlinx.coroutines.ObsoleteCoroutinesApi - @Test - fun `flush queued events on startup and correctly handle pre-init events`() { - assertEquals( - "There should be no events on disk to start", - 0, - EventsStorageEngine.storageDirectory.listFiles()?.size - ) - - val server = getMockWebServer() - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event", - lifetime = Lifetime.Ping, - sendInPings = listOf("events"), - allowedExtraKeys = listOf("someExtra") - ) - - event.record(extra = mapOf(SomeExtraKeys.SomeExtra to "run1")) - assertEquals(1, event.testGetValue().size) - - // Clear the in-memory storage only to mock being loaded in a fresh process - EventsStorageEngine.eventStores.clear() - - Dispatchers.API.setTaskQueueing(true) - - event.record(extra = mapOf(SomeExtraKeys.SomeExtra to "pre-init")) - - resetGlean( - getContextWithMockedInfo(), - Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - ), - clearStores = false - ) - - event.record(extra = mapOf(SomeExtraKeys.SomeExtra to "post-init")) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - var request = server.takeRequest(20L, TimeUnit.SECONDS) - var pingJsonData = request.body.readUtf8() - var pingJson = JSONObject(pingJsonData) - checkPingSchema(pingJson) - assertNotNull(pingJson.opt("events")) - - // This event comes from disk from the prior "run" - assertEquals( - 1, - pingJson.getJSONArray("events").length() - ) - assertEquals( - "run1", - pingJson.getJSONArray("events").getJSONObject(0).getJSONObject("extra").getString("someExtra") - ) - - Glean.sendPingsByName(listOf("events")) - - // Trigger worker task to upload the pings in the background - triggerWorkManager(context) - - request = server.takeRequest(20L, TimeUnit.SECONDS) - pingJsonData = request.body.readUtf8() - pingJson = JSONObject(pingJsonData) - checkPingSchema(pingJson) - assertNotNull(pingJson.opt("events")) - - // This event comes from the pre-initialization event - assertEquals( - 2, - pingJson.getJSONArray("events").length() - ) - assertEquals( - "pre-init", - pingJson.getJSONArray("events").getJSONObject(0).getJSONObject("extra").getString("someExtra") - ) - assertEquals( - "post-init", - pingJson.getJSONArray("events").getJSONObject(1).getJSONObject("extra").getString("someExtra") - ) - - EventsStorageEngine.clearAllStores() - } - - @Test - fun `handle truncated events on disk`() { - val server = getMockWebServer() - - resetGlean(getContextWithMockedInfo(), Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - )) - - val event = EventMetricType( - disabled = false, - category = "telemetry", - name = "test_event", - lifetime = Lifetime.Ping, - sendInPings = listOf("events"), - allowedExtraKeys = listOf("key1", "key2") - ) - - // Record the event in the store, without providing optional arguments. - event.record(mapOf(ExtraKeys.Key1 to "bar")) - EventsStorageEngine.testWaitForWrites() - // Add a couple of truncated events to disk. One is still valid JSON, the other isn't. - // These event should be skipped, all others intact. - EventsStorageEngine.writeEventToDisk("events", "[500]") - EventsStorageEngine.testWaitForWrites() - EventsStorageEngine.writeEventToDisk("events", "[500, \"foo") - EventsStorageEngine.testWaitForWrites() - event.record(mapOf(ExtraKeys.Key1 to "baz")) - EventsStorageEngine.testWaitForWrites() - assertEquals(2, event.testGetValue().size) - - // Clear the in-memory storage only to mock being loaded in a fresh process - EventsStorageEngine.eventStores.clear() - resetGlean( - getContextWithMockedInfo(), - Glean.configuration.copy( - serverEndpoint = "http://" + server.hostName + ":" + server.port, - logPings = true - ), - clearStores = false - ) - triggerWorkManager(context) - - event.record(mapOf(ExtraKeys.Key1 to "bip")) - - val request = server.takeRequest(20L, TimeUnit.SECONDS) - assertEquals("POST", request.method) - val applicationId = "mozilla-components-service-glean" - assert( - request.path.startsWith("/submit/$applicationId/events/${Glean.SCHEMA_VERSION}/") - ) - val pingJsonData = request.body.readUtf8() - val pingJson = JSONObject(pingJsonData) - checkPingSchema(pingJson) - assertNotNull(pingJson.opt("events")) - val events = pingJson.getJSONArray("events") - assertEquals(2, events.length()) - assertEquals("bar", events.getJSONObject(0).getJSONObject("extra").getString("key1")) - assertEquals("baz", events.getJSONObject(1).getJSONObject("extra").getString("key1")) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/ExperimentsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/ExperimentsStorageEngineTest.kt deleted file mode 100644 index f0a89a8b26c..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/ExperimentsStorageEngineTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import java.util.ArrayList - -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.Assert.assertEquals -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class ExperimentsStorageEngineTest { - @Before - fun setUp() { - ExperimentsStorageEngine.clearAllStores() - } - - @After - fun clear() { - ExperimentsStorageEngine.clearAllStores() - } - - @Test - fun `setExperimentActive() properly sets the value in all stores`() { - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_a", - branch = "branch_a", - extra = mapOf("extra_key_1" to "extra_value_1") - ) - - val snapshot = ExperimentsStorageEngine.getSnapshot() - assertEquals(1, snapshot.size) - assertEquals("branch_a", snapshot.get("experiment_a")!!.branch) - assertEquals( - "extra_value_1", - snapshot.get("experiment_a")!!.extra!!.get("extra_key_1") - ) - } - - @Test - fun `setExperimentActive() properly truncates experiment and branch names`() { - ExperimentsStorageEngine.setExperimentActive( - experimentId = "e0123456789012345678901234567890123456789", - branch = "b0123456789012345678901234567890123456789" - ) - - val snapshot = ExperimentsStorageEngine.getSnapshot() - assertEquals(1, snapshot.size) - assertEquals( - "b01234567890123456789012345678", - snapshot.get("e01234567890123456789012345678")!!.branch - ) - - // Removing a long name should still work, despite truncation - ExperimentsStorageEngine.setExperimentInactive( - experimentId = "e0123456789012345678901234567890123456789" - ) - - val snapshot2 = ExperimentsStorageEngine.getSnapshot() - assertEquals(0, snapshot2.size) - } - - @Test - fun `test behavior of adding and removing experiments`() { - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_1", - branch = "branch_a" - ) - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_2", - branch = "branch_c" - ) - - val snapshot = ExperimentsStorageEngine.getSnapshot() - assertEquals(2, snapshot.size) - assertEquals( - listOf("experiment_1", "experiment_2"), - ArrayList(snapshot.keys) - ) - - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_1", - branch = "branch_b" - ) - val snapshot2 = ExperimentsStorageEngine.getSnapshot() - assertEquals(2, snapshot2.size) - assertEquals("branch_b", snapshot2.get("experiment_1")!!.branch) - - ExperimentsStorageEngine.setExperimentInactive("experiment_2") - val snapshot3 = ExperimentsStorageEngine.getSnapshot() - assertEquals(1, snapshot3.size) - assertEquals("branch_b", snapshot3.get("experiment_1")!!.branch) - - // Remove non-existent experiment - ExperimentsStorageEngine.setExperimentInactive("ridiculous") - val snapshot4 = ExperimentsStorageEngine.getSnapshot() - assertEquals(1, snapshot4.size) - assertEquals("branch_b", snapshot4.get("experiment_1")!!.branch) - } - - @Test - fun `test JSON output`() { - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_1", - branch = "branch_a" - ) - ExperimentsStorageEngine.setExperimentActive( - experimentId = "experiment_2", - branch = "branch_c", - extra = mapOf("extra_key_1" to "extra_val_1") - ) - - val json = ExperimentsStorageEngine.getSnapshotAsJSON( - "test", true - ) - assertEquals( - "{\"experiment_1\":{\"branch\":\"branch_a\"},\"experiment_2\":{\"branch\":\"branch_c\",\"extra\":{\"extra_key_1\":\"extra_val_1\"}}}", - json.toString() - ) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/GenericStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/GenericStorageEngineTest.kt deleted file mode 100644 index c05dcbeec89..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/GenericStorageEngineTest.kt +++ /dev/null @@ -1,506 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.service.glean.private.Lifetime -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class GenericStorageEngineTest { - private data class GenericMetricType( - override val disabled: Boolean, - override val category: String, - override val lifetime: Lifetime, - override val name: String, - override val sendInPings: List - ) : CommonMetricData - - @Test - fun `metrics with 'user' lifetime must not be cleared when snapshotting`() { - val dataUserLifetime = 37 - val dataPingLifetime = 3 - - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric1 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "userLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with User lifetime - storageEngine.record( - metric1, - value = dataUserLifetime - ) - - val metric2 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "pingLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with Ping lifetime - storageEngine.record( - metric2, - value = dataPingLifetime - ) - - // Take a snapshot and clear the store: this snapshot must contain the data for - // both metrics. - val firstSnapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(2, firstSnapshot!!.size) - assertEquals(dataUserLifetime, firstSnapshot["telemetry.userLifetimeData"]) - assertEquals(dataPingLifetime, firstSnapshot["telemetry.pingLifetimeData"]) - - // Take a new snapshot. The ping lifetime data should have been cleared and not be - // available anymore, data with 'user' lifetime must still be around. - val secondSnapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, secondSnapshot!!.size) - assertEquals(dataUserLifetime, secondSnapshot["telemetry.userLifetimeData"]) - assertFalse(secondSnapshot.contains("telemetry.pingLifetimeData")) - } - - @Test - fun `metrics with empty 'category' must be properly recorded`() { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = GenericMetricType( - disabled = false, - category = "", - lifetime = Lifetime.Ping, - name = "noCategoryName", - sendInPings = listOf("store1") - ) - - // Record a value with User lifetime - storageEngine.record( - metric, - value = 37 - ) - - // Take a snapshot and clear the store: this snapshot must contain the data for - // both metrics. - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(37, snapshot["noCategoryName"]) - } - - @Test - fun `metric with 'user' lifetime must be correctly unpersisted when recording 'user' values`() { - // Make up the test data that we pretend to be unserialized. - val persistedSample = mapOf( - "store1#telemetry.value1" to 1, - "store1#telemetry.value2" to 2, - "store2#telemetry.value1" to 1 - ) - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - - // Instantiate our mock engine and check that it correctly unpersists the - // data and makes it available in the snapshot. - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - val metric = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "pingLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with Ping lifetime - storageEngine.record( - metric, - value = 37 - ) - - verify(sharedPreferences, times(1)).all - } - - @Test - fun `unpersisting broken 'user' lifetime data should not break the API`() { - val brokenSample = mapOf( - "store1#telemetry.value1" to "test", - "store1#telemetry.value2" to false, - "store1#telemetry.value1" to null - ) - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { brokenSample } - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - // Instantiate our mock engine and check that it correctly unpersists the - // data and makes it available in the snapshot. - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - val metric = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "pingLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with Ping lifetime - storageEngine.record( - metric, - value = 37 - ) - - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(37, snapshot["telemetry.pingLifetimeData"]) - } - - @Test - fun `unpersisting metrics must skip data with missing storage name`() { - val brokenSample = mapOf( - "store_name#telemetry.value1" to 11, - "telemetry.value2" to 7, - "#telemetry.value3" to 15 - ) - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { brokenSample } - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - // Instantiate our mock engine and check that it correctly unpersists the - // data and makes it available in the snapshot. - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - val snapshot = storageEngine.getSnapshot(storeName = "store_name", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(11, snapshot["telemetry.value1"]) - } - - @Test - fun `unpersisting metrics must not fail if SharedPreferences throws`() { - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenThrow(NullPointerException()) - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - // Instantiate our mock engine and check that it correctly unpersists the - // data and makes it available in the snapshot. - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - // Make sure we attempt to load data to trigger the exception. - storageEngine.getSnapshot(storeName = "store_name", clearStore = true) - // The next call verifies that we're called twice: one directly by the snapshot, the other - // indirectly by the internal lazy loading function. - verify(sharedPreferences, times(2)).all - } - - @Test - fun `metrics with 'user' lifetime must be correctly unpersisted before taking snapshots`() { - // Make up the test data that we pretend to be unserialized. - val persistedSample = mapOf( - "store1#telemetry.value1" to 1, - "store1#telemetry.value2" to 2, - "store2#telemetry.value1" to 1 - ) - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(MockGenericStorageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${MockGenericStorageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - // Instantiate our mock engine and check that it correctly unpersists the - // data and makes it available in the snapshot. - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = context - - val store1Snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(2, store1Snapshot!!.size) - assertEquals(1, store1Snapshot["telemetry.value1"]) - assertEquals(2, store1Snapshot["telemetry.value2"]) - - val store2Snapshot = storageEngine.getSnapshot(storeName = "store2", clearStore = true) - assertEquals(1, store2Snapshot!!.size) - assertEquals(1, store2Snapshot["telemetry.value1"]) - } - - @Test - fun `snapshotting must only clear 'ping' lifetime`() { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val stores = listOf("store1", "store2") - - val metric1 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "userLifetimeData", - sendInPings = stores - ) - - // Record a value with User lifetime - storageEngine.record( - metric1, - value = 11 - ) - - val metric2 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "applicationLifetimeData", - sendInPings = stores - ) - - // Record a value with Application lifetime - storageEngine.record( - metric2, - value = 7 - ) - - val metric3 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "pingLifetimeData", - sendInPings = stores - ) - - // Record a value with Ping lifetime - storageEngine.record( - metric3, - value = 2015 - ) - - for (store in stores) { - // Get a first snapshot: it will clear the "ping" lifetime for the - // requested store. - val snapshot1 = storageEngine.getSnapshot(storeName = store, clearStore = true) - assertEquals(3, snapshot1!!.size) - assertEquals(11, snapshot1["telemetry.userLifetimeData"]) - assertEquals(7, snapshot1["telemetry.applicationLifetimeData"]) - assertEquals(2015, snapshot1["telemetry.pingLifetimeData"]) - - // A new snapshot should not contain data with "ping" lifetime, since it was - // previously cleared. - val snapshot2 = storageEngine.getSnapshot(storeName = store, clearStore = true) - assertEquals(2, snapshot2!!.size) - assertEquals(11, snapshot2["telemetry.userLifetimeData"]) - assertEquals(7, snapshot2["telemetry.applicationLifetimeData"]) - } - } - - @Test - fun `metrics with 'application' lifetime must be cleared when the application is closed`() { - // We use block scopes to simulate restarting the application. We use the same - // context otherwise the test environment will use a different underlying file - // for the SharedPreferences. - run { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric1 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "userLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with User lifetime - storageEngine.record( - metric1, - value = 37 - ) - - val metric2 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "applicationLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with Application lifetime - storageEngine.record( - metric2, - value = 85 - ) - - // Make sure the data was recorded without clearing the storage. - val snapshot = storageEngine.getSnapshot("store1", false) - assertEquals(2, snapshot!!.size) - assertEquals(37, snapshot["telemetry.userLifetimeData"]) - assertEquals(85, snapshot["telemetry.applicationLifetimeData"]) - } - - // Re-instantiate the engine: application lifetime probes should have been cleared. - run { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val snapshot = storageEngine.getSnapshot("store1", true) - assertEquals(1, snapshot!!.size) - assertEquals(37, snapshot["telemetry.userLifetimeData"]) - } - } - - @Test - fun `metrics with 'ping' lifetime must be cleared when the ping is scheduled`() { - // We use block scopes to simulate restarting the application. We use the same - // context otherwise the test environment will use a different underlying file - // for the SharedPreferences. - run { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric1 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "userLifetimeData", - sendInPings = listOf("store1") - ) - - // Record a value with User lifetime - storageEngine.record( - metric1, - value = 37 - ) - - val metric2 = GenericMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "pingLifetimeData", - sendInPings = listOf("store1", "store2") - ) - - // Record a value with Application lifetime - storageEngine.record( - metric2, - value = 85 - ) - - // Make sure the data was recorded without clearing the storage. - val snapshot = storageEngine.getSnapshot("store1", false) - assertEquals(2, snapshot!!.size) - assertEquals(37, snapshot["telemetry.userLifetimeData"]) - assertEquals(85, snapshot["telemetry.pingLifetimeData"]) - - // Verify data was recorded in the second ping "store2" - val snapshot2 = storageEngine.getSnapshot("store2", false) - assertEquals(1, snapshot2!!.size) - assertEquals(85, snapshot2["telemetry.pingLifetimeData"]) - } - - // Re-instantiate the engine: ping lifetime metrics should be persisted. - run { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val snapshot = storageEngine.getSnapshot("store1", true) - assertEquals(2, snapshot!!.size) - assertEquals(37, snapshot["telemetry.userLifetimeData"]) - // Ensure the ping lifetime was persisted - assertEquals(85, snapshot["telemetry.pingLifetimeData"]) - - // Check that "store2" was persisted - val snapshot2 = storageEngine.getSnapshot("store2", false) - assertEquals(1, snapshot2!!.size) - // Ensure the ping lifetime was persisted - assertEquals(85, snapshot2["telemetry.pingLifetimeData"]) - } - - // Now that the store was cleared, ping lifetime should have nothing persisted - run { - val storageEngine = MockGenericStorageEngine() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val snapshot = storageEngine.getSnapshot("store1", true) - assertEquals(1, snapshot!!.size) - assertEquals(37, snapshot["telemetry.userLifetimeData"]) - // Ensure the ping lifetime was not persisted - assertNull(snapshot["telemetry.pingLifetimeData"]) - - // Check that "store2" was persisted, passing true to go ahead and clear store2 out - val snapshot2 = storageEngine.getSnapshot("store2", true) - assertEquals(1, snapshot2!!.size) - // Ensure the ping lifetime was persisted - assertEquals(85, snapshot2["telemetry.pingLifetimeData"]) - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngineTest.kt deleted file mode 100644 index d209d7c16c3..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MemoryDistributionsStorageEngineTest.kt +++ /dev/null @@ -1,298 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.runBlocking -import mozilla.components.service.glean.collectAndCheckPingSchema -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.MemoryDistributionMetricType -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.private.MemoryUnit -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class MemoryDistributionsStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `accumulate() properly updates the values in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "test_memory_distribution", - sendInPings = storeNames, - memoryUnit = MemoryUnit.Kilobyte - ) - - // Create a sample that will fall into the underflow bucket (bucket '0') so we can easily - // find it - val sample = 1L - MemoryDistributionsStorageEngine.accumulate( - metricData = metric, - sample = sample, - memoryUnit = MemoryUnit.Kilobyte - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = MemoryDistributionsStorageEngine.getSnapshot( - storeName = storeName, - clearStore = true - ) - assertEquals(1, snapshot!!.size) - assertEquals(1L, snapshot["telemetry.test_memory_distribution"]?.values!![1023]) - } - } - - @Test - fun `deserializer should correctly parse memory distributions`() { - val md = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.invalid_list" to listOf("1", "2", "3"), - "store1#telemetry.invalid_int_list" to "[1,2,3]", - "store1#telemetry.invalid_md_values" to "{\"log_base\":2.0,\"buckets_per_magnitude\":16.0,\"values\":{\"0\": \"nope\"},\"sum\":0}", - "store1#telemetry.invalid_md_sum" to "{\"log_base\":2.0,\"buckets_per_magnitude\":16.0,\"values\":{},\"sum\":\"nope\"}", - "store1#telemetry.test_memory_distribution" to md.toJsonObject().toString() - ) - - val storageEngine = MemoryDistributionsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = Mockito.mock(Context::class.java) - val sharedPreferences = Mockito.mock(SharedPreferences::class.java) - Mockito.`when`(sharedPreferences.all).thenAnswer { persistedSample } - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq(storageEngine::class.java.canonicalName), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(md.toJsonObject().toString(), - snapshot["telemetry.test_memory_distribution"]?.toJsonObject().toString()) - } - - @Test - fun `serializer should serialize memory distribution that matches schema`() { - val ping1 = PingType("store1", true) - - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Kilobyte - ) - - runBlocking { - metric.accumulate(100000L) - } - - collectAndCheckPingSchema(ping1) - } - - @Test - fun `serializer should correctly serialize memory distributions`() { - run { - val storageEngine = MemoryDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_memory_distribution", - sendInPings = listOf("store1", "store2"), - memoryUnit = MemoryUnit.Kilobyte - ) - - // Using the FunctionalHistogram object here to easily turn the object into JSON - // for comparison purposes. - val md = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - md.accumulate(1000000L * 1024L) - - runBlocking { - storageEngine.accumulate( - metricData = metric, - sample = 1000000L, - memoryUnit = MemoryUnit.Kilobyte - ) - } - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"${metric.identifier}\":${md.toJsonPayloadObject()}}", - json.toString() - ) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = MemoryDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val md = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - md.accumulate(1000000L * 1024) - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"telemetry.test_memory_distribution\":${md.toJsonPayloadObject()}}", - json.toString() - ) - } - } - - @Test - fun `memory distributions must not accumulate negative values`() { - // Define a memory distribution metric, which will be stored in "store1". - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Byte - ) - - metric.accumulate(-1) - // Check that nothing was recorded. - assertFalse("Memory distributions must not accumulate negative values", - metric.testHasValue()) - - // Make sure that the errors have been recorded - assertEquals("Accumulating negative values must generate an error", - 1, - ErrorRecording.testGetNumRecordedErrors(metric, ErrorRecording.ErrorType.InvalidValue)) - } - - @Test - fun `overflow values accumulate in the last bucket`() { - // Define a memory distribution metric, which will be stored in "store1". - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Byte - ) - - // Attempt to accumulate an overflow sample - metric.accumulate(1L shl 41) - - val hist = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - // Check that memory distribution was recorded. - assertTrue("Accumulating overflow values records data", - metric.testHasValue()) - - // Make sure that the overflow landed in the correct (last) bucket - val snapshot = metric.testGetValue() - assertEquals("Accumulating overflow values should increment last bucket", - 1L, - snapshot.values[hist.sampleToBucketMinimum(MemoryDistributionsStorageEngineImplementation.MAX_BYTES)]) - } - - @Test - fun `accumulate finds the correct bucket`() { - // Define a memory distribution metric, which will be stored in "store1". - val metric = MemoryDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_memory_distribution", - sendInPings = listOf("store1"), - memoryUnit = MemoryUnit.Byte - ) - - val samples = listOf(10L, 100L, 1000L, 10000L) - - // Attempt to accumulate a sample to force metric to be stored - for (i in samples) { - metric.accumulate(i) - } - - // Check that memory distribution was recorded. - assertTrue("Accumulating values records data", metric.testHasValue()) - - // Make sure that the samples are in the correct buckets - val snapshot = metric.testGetValue() - - // Check sum and count - assertEquals("Accumulating updates the sum", 11110, snapshot.sum) - assertEquals("Accumulating updates the count", 4, snapshot.count) - - val hist = FunctionalHistogram( - MemoryDistributionsStorageEngineImplementation.LOG_BASE, - MemoryDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - for (i in samples) { - val binEdge = hist.sampleToBucketMinimum(i) - assertEquals("Accumulating should increment correct bucket", 1L, snapshot.values[binEdge]) - } - - val json = snapshot.toJsonPayloadObject() - val values = json.getJSONObject("values") - assertEquals(154, values.length()) - - for (i in samples) { - val binEdge = hist.sampleToBucketMinimum(i) - assertEquals("Accumulating should increment correct bucket", 1L, values.getLong(binEdge.toString())) - values.remove(binEdge.toString()) - } - - for (k in values.keys()) { - assertEquals(0L, values.getLong(k)) - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockGenericStorageEngine.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockGenericStorageEngine.kt deleted file mode 100644 index 3a4793a1140..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockGenericStorageEngine.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.SharedPreferences -import mozilla.components.service.glean.private.CommonMetricData -import mozilla.components.support.base.log.logger.Logger - -internal class MockGenericStorageEngine( - override val logger: Logger = Logger("test") -) : GenericStorageEngine() { - override fun deserializeSingleMetric(metricName: String, value: Any?): Int? { - if (value is String) { - return value.toIntOrNull() - } - - return value as? Int? - } - - override fun serializeSingleMetric( - userPreferences: SharedPreferences.Editor?, - storeName: String, - value: Int, - extraSerializationData: Any? - ) { - userPreferences?.putInt(storeName, value) - } - - fun record( - metricData: CommonMetricData, - value: Int - ) { - super.recordMetric(metricData, value) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockStorageEngine.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockStorageEngine.kt deleted file mode 100644 index f91b480ddf9..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/MockStorageEngine.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import org.junit.Assert - -/** - * A mock storage engine. This merely returns some sample - * data as a JSONObject or JSONArray. - */ -internal class MockStorageEngine( - // The following needs to be Any as we expect to - // return both JSONObject and JSONArray - private val sampleJSON: Any, - private val sampleStore: String = "test" -) : StorageEngine { - override fun clearAllStores() { - // Nothing to do here for a mocked storage engine - } - - override lateinit var applicationContext: Context - - override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? { - Assert.assertTrue(clearStore) - return if (storeName == sampleStore) sampleJSON else null - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/PingStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/PingStorageEngineTest.kt deleted file mode 100644 index 0a424fd2c73..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/PingStorageEngineTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.Dispatchers -import mozilla.components.service.glean.Glean -import mozilla.components.service.glean.config.Configuration -import mozilla.components.service.glean.resetGlean -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.assertFalse -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.io.File -import java.io.FileReader -import java.io.BufferedReader -import java.io.FileOutputStream -import java.util.UUID - -@RunWith(RobolectricTestRunner::class) -class PingStorageEngineTest { - // Filenames and paths to test with, regenerated with each test. - private lateinit var fileName1: UUID - private lateinit var path1: String - private lateinit var fileName2: UUID - private lateinit var path2: String - private lateinit var fileName3: UUID - private lateinit var path3: String - - private lateinit var fileNames: Array - private lateinit var pathNames: Array - - private lateinit var pingStorageEngine: PingStorageEngine - - @Before - fun setup() { - resetGlean() - pingStorageEngine = Glean.pingStorageEngine - - // Clear out pings and assert that there are none in the directory before we start - pingStorageEngine.storageDirectory.listFiles()?.forEach { file -> - file.delete() - } - assertNull("Pending pings directory must be empty before test start", - pingStorageEngine.storageDirectory.listFiles()) - - fileName1 = UUID.randomUUID() - fileName2 = UUID.randomUUID() - fileName3 = UUID.randomUUID() - - path1 = "/test/$fileName1" - path2 = "/test/$fileName2" - path3 = "/test/$fileName3" - - fileNames = arrayOf(fileName1.toString(), fileName2.toString(), fileName3.toString()) - pathNames = arrayOf(path1, path2, path3) - } - - @Test - fun `storage engine correctly stores pings to file`() { - runBlocking(Dispatchers.IO) { - pingStorageEngine.store(fileName1, path1, "dummy data").join() - } - - assertEquals(1, pingStorageEngine.storageDirectory.listFiles()?.count()) - - val file = File(pingStorageEngine.storageDirectory, fileName1.toString()) - BufferedReader(FileReader(file)).use { - val lines = it.readLines() - assertEquals(path1, lines[0]) - assertEquals("dummy data", lines[1]) - } - } - - @Test - fun `process correctly processes files`() { - runBlocking(Dispatchers.IO) { - pingStorageEngine.store(fileName1, path1, "dummy data").join() - } - - assertEquals(1, pingStorageEngine.storageDirectory.listFiles()?.count()) - assertTrue(pingStorageEngine.process(this::testCallback)) - } - - @Test - fun `process correctly handles callback returning false`() { - runBlocking(Dispatchers.IO) { - pingStorageEngine.store(fileName1, path1, "dummy data").join() - } - - assertEquals(1, pingStorageEngine.storageDirectory.listFiles()?.count()) - assertFalse(pingStorageEngine.process(this::testFailedCallback)) - } - - private fun testCallback(path: String, pingData: String, config: Configuration): Boolean { - assertTrue(pathNames.contains(path)) - assertEquals("dummy data", pingData) - assertEquals(Glean.configuration, config) - - return true - } - - private fun testFailedCallback(path: String, pingData: String, config: Configuration): Boolean { - assertTrue(pathNames.contains(path)) - assertEquals("dummy data", pingData) - assertEquals(Glean.configuration, config) - return false - } - - @Test - fun `listPingFiles correctly lists all files`() { - runBlocking(Dispatchers.IO) { - pingStorageEngine.store(fileName1, path1, "dummy data").join() - pingStorageEngine.store(fileName2, path2, "dummy data").join() - pingStorageEngine.store(fileName3, path3, "dummy data").join() - } - - val files = pingStorageEngine.storageDirectory.listFiles() - assertEquals(3, files?.count()) - - files?.forEach { file -> - assertTrue(fileNames.contains(file.name)) - } - } - - @Test - fun `process removes files that do not have UUID names`() { - // Clear out pings and assert that there are none in the directory before we start - pingStorageEngine.storageDirectory.listFiles()?.forEach { file -> - file.delete() - } - assertNull("Pending pings directory must be empty before test start", - pingStorageEngine.storageDirectory.listFiles()) - - // Store a "valid" ping with a UUID file name, using runBlocking to make sure the write is - // complete before continuing. - runBlocking { - pingStorageEngine.store(fileName1, path1, "dummy data").join() - } - - // Store an "invalid" ping without a UUID file name (writing synchronously on purpose as we - // immediately assert the file count after this) - val invalidPingFile = File(pingStorageEngine.storageDirectory, "NotUuid") - FileOutputStream(invalidPingFile, true).bufferedWriter().use { - it.write("/test/NotUuid") - it.newLine() - it.write("dummy data") - it.newLine() - it.flush() - } - - // Check to see that we list both the valid and invalid ping files - assertEquals(2, pingStorageEngine.storageDirectory.listFiles()?.count()) - - var numCalls = 0 - fun testCountingCallback(path: String, pingData: String, config: Configuration): Boolean { - numCalls++ - return testCallback(path, pingData, config) - } - - // Process the directory - assertTrue(pingStorageEngine.process(::testCountingCallback)) - assertEquals("The callback should be called once", 1, numCalls) - - // Check to see that we list no ping files and no invalid files - assertEquals(0, pingStorageEngine.storageDirectory.listFiles()?.count()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/QuantitiesStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/QuantitiesStorageEngineTest.kt deleted file mode 100644 index c6c462f80a0..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/QuantitiesStorageEngineTest.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.collectAndCheckPingSchema -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.QuantityMetricType -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertFalse -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class QuantitiesStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `quantity deserializer should correctly parse integers`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.valid" to 1L - ) - - val storageEngine = QuantitiesStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(1L, snapshot["telemetry.valid"]) - } - - @Test - fun `quantity serializer should correctly serialize quantities`() { - run { - val storageEngine = QuantitiesStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Record the quantity in the store - storageEngine.record( - metric, - value = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.quantity_metric\":1}", snapshot.toString()) - } - - // Re-instantiate storage engine to validate serialization from storage rather than cache - run { - val storageEngine = QuantitiesStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get the snapshot from "store1" and clear it. - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.quantity_metric\":1}", snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "quantity_metric", - sendInPings = storeNames - ) - - // Record the quantity in the stores, without providing optional arguments. - QuantitiesStorageEngine.record( - metric, - value = 1 - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = QuantitiesStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(1L, snapshot.get("telemetry.quantity_metric")) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - QuantitiesStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val metric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "quantity_metric", - sendInPings = storeNames - ) - - // Record the quantity in the stores, without providing optional arguments. - QuantitiesStorageEngine.record( - metric, - value = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = QuantitiesStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - QuantitiesStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = QuantitiesStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `quantities are serialized in the correct JSON format`() { - val metric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Record the quantity in the store - QuantitiesStorageEngine.record( - metric, - value = 1 - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = QuantitiesStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - QuantitiesStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.quantity_metric\":1}", - snapshot.toString()) - } - - @Test - fun `quantities are serialized in a form that validates against the schema`() { - val pingType = PingType("store1", true) - - val metric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Record the quantity in the store - QuantitiesStorageEngine.record( - metric, - value = 1 - ) - - collectAndCheckPingSchema(pingType) - } - - @Test - fun `quantities must not set when passed negative`() { - // Define a 'quantityMetric' quantity metric, which will be stored in "store1". - val quantityMetric = QuantityMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "quantity_metric", - sendInPings = listOf("store1") - ) - - // Attempt to set the quantity to negative - quantityMetric.set(-1) - // Check that nothing was recorded. - assertFalse("Quantities must not be recorded if set with negative", - quantityMetric.testHasValue()) - - // Make sure that the errors have been recorded - assertEquals(1, testGetNumRecordedErrors(quantityMetric, ErrorType.InvalidValue)) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StorageEngineManagerTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StorageEngineManagerTest.kt deleted file mode 100644 index 3988787101b..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StorageEngineManagerTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import androidx.test.core.app.ApplicationProvider -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class StorageEngineManagerTest { - @Test - fun `collect() must return an empty object for empty or unknown stores`() { - val manager = StorageEngineManager(applicationContext = ApplicationProvider.getApplicationContext()) - val data = manager.collect("thisStoreNameDoesNotExist") - assertNotNull(data) - assertEquals("{}", data.toString()) - } - - @Test - fun `collect() must report data from all the stores`() { - val manager = StorageEngineManager(storageEngines = mapOf( - "engine1" to MockStorageEngine(JSONObject(mapOf("test" to "val"))), - "engine2" to MockStorageEngine(JSONArray(listOf("a", "b", "c"))) - ), applicationContext = ApplicationProvider.getApplicationContext()) - - val data = manager.collect("test") - assertEquals("{\"metrics\":{\"engine1\":{\"test\":\"val\"},\"engine2\":[\"a\",\"b\",\"c\"]}}", - data.toString()) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringListsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringListsStorageEngineTest.kt deleted file mode 100644 index 0edfb5c01b3..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringListsStorageEngineTest.kt +++ /dev/null @@ -1,370 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.StringListMetricType -import mozilla.components.service.glean.testing.GleanTestRule -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class StringListsStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `set() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = storeNames - ) - - val list = listOf("First", "Second") - - StringListsStorageEngine.set( - metricData = metric, - value = list - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = StringListsStorageEngine.getSnapshot( - storeName = storeName, - clearStore = false - ) - assertEquals(1, snapshot!!.size) - assertEquals("First", snapshot["telemetry.string_list_metric"]?.get(0)) - assertEquals("Second", snapshot["telemetry.string_list_metric"]?.get(1)) - } - } - - @Test - fun `add() properly adds the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = storeNames - ) - - StringListsStorageEngine.add( - metricData = metric, - value = "First" - ) - - // Check that the data was correctly added in each store. - for (storeName in storeNames) { - val snapshot = StringListsStorageEngine.getSnapshot( - storeName = storeName, - clearStore = false) - assertEquals("First", snapshot!!["telemetry.string_list_metric"]?.get(0)) - } - } - - @Test - fun `add() won't allow adding beyond the max list length in a single accumulation`() { - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - for (i in 1..StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE + 1) { - StringListsStorageEngine.add( - metricData = metric, - value = "value$i" - ) - } - - // Check that list was truncated. - val snapshot = StringListsStorageEngine.getSnapshot( - storeName = "store1", - clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(true, snapshot.containsKey("telemetry.string_list_metric")) - assertEquals( - StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE, - snapshot["telemetry.string_list_metric"]?.count() - ) - - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `add() won't allow adding beyond the max list length over multiple accumulations`() { - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - // Add values up to half capacity - for (i in 1..StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE / 2) { - StringListsStorageEngine.add( - metricData = metric, - value = "value$i" - ) - } - - // Check that list was added - val snapshot = StringListsStorageEngine.getSnapshot( - storeName = "store1", - clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(true, snapshot.containsKey("telemetry.string_list_metric")) - assertEquals( - StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE / 2, - snapshot["telemetry.string_list_metric"]?.count() - ) - - // Add values that would exceed capacity - for (i in 1..StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE) { - StringListsStorageEngine.add( - metricData = metric, - value = "otherValue$i" - ) - } - - // Check that the list was truncated to the list capacity - val snapshot2 = StringListsStorageEngine.getSnapshot( - storeName = "store1", - clearStore = false) - assertEquals(1, snapshot2!!.size) - assertEquals(true, snapshot2.containsKey("telemetry.string_list_metric")) - assertEquals( - StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE, - snapshot2["telemetry.string_list_metric"]?.count() - ) - - assertEquals(StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE / 2, - testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `set() won't allow adding a list longer than the max list length`() { - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - val stringList: MutableList = mutableListOf() - for (i in 1..StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE + 1) { - stringList.add("value$i") - } - - StringListsStorageEngine.set(metricData = metric, value = stringList) - - // Check that list was truncated. - val snapshot = StringListsStorageEngine.getSnapshot( - storeName = "store1", - clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(true, snapshot.containsKey("telemetry.string_list_metric")) - assertEquals( - StringListsStorageEngineImplementation.MAX_LIST_LENGTH_VALUE, - snapshot["telemetry.string_list_metric"]?.count() - ) - - assertEquals(1, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `set() doesn't record an error when passed an empty list`() { - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - StringListsStorageEngine.set(metricData = metric, value = listOf()) - - assertEquals(listOf(), metric.testGetValue()) - - // Verify the error was not recorded - assertEquals(0, testGetNumRecordedErrors(metric, ErrorType.InvalidValue)) - } - - @Test - fun `string list deserializer should correctly parse string lists`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.invalid_list" to listOf("1", "2", "3"), - "store1#telemetry.invalid_int_list" to "[1,2,3]", - "store1#telemetry.valid" to "[\"a\",\"b\",\"c\"]" - ) - - val storageEngine = StringListsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = Mockito.mock(Context::class.java) - val sharedPreferences = Mockito.mock(SharedPreferences::class.java) - Mockito.`when`(sharedPreferences.all).thenAnswer { persistedSample } - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq(storageEngine::class.java.canonicalName), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Because JSONArray constructor will deserialize with or without the escaped quotes, it - // treat the invalid_int_list above the same as the valid list, so we assertEquals 2 - assertEquals(2, snapshot!!.size) - assertEquals(listOf("a", "b", "c"), snapshot["telemetry.valid"]) - } - - @Test - fun `string list serializer should correctly serialize lists`() { - run { - val storageEngine = StringListsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val storeNames = listOf("store1", "store2") - - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "string_list_metric", - sendInPings = storeNames - ) - - val list = listOf("First", "Second") - - storageEngine.set( - metricData = metric, - value = list - ) - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals( - "{\"telemetry.string_list_metric\":[\"First\",\"Second\"]}", - json.toString() - ) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = StringListsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals( - "{\"telemetry.string_list_metric\":[\"First\",\"Second\"]}", - json.toString() - ) - } - } - - @Test - fun `test JSON output`() { - val storeNames = listOf("store1", "store2") - - val metric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_list_metric", - sendInPings = storeNames - ) - - val list = listOf("First", "Second") - - StringListsStorageEngine.set( - metricData = metric, - value = list - ) - - // Get snapshot from store1 and clear it - val json = StringListsStorageEngine.getSnapshotAsJSON("store1", true) - // Check that getting a new snapshot for "store1" returns an empty store. - Assert.assertNull("The engine must report 'null' on empty stores", - StringListsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check for correct JSON serialization - assertEquals( - "{\"telemetry.string_list_metric\":[\"First\",\"Second\"]}", - json.toString() - ) - } - - @Test - fun `The API truncates long string values`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" - val stringListMetric = StringListMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_list_metric", - sendInPings = listOf("store1") - ) - - val longString = "a".repeat(StringListsStorageEngineImplementation.MAX_STRING_LENGTH + 10) - - // Check that data was truncated via add() method. - StringListsStorageEngine.add(stringListMetric, longString) - var snapshot = StringListsStorageEngine.getSnapshotAsJSON("store1", true) as JSONObject - var stringList = snapshot["telemetry.string_list_metric"] as JSONArray - assertEquals(longString.take(StringListsStorageEngineImplementation.MAX_STRING_LENGTH), - stringList[0]) - - // Check that data was truncated via set() method. - StringListsStorageEngine.set(stringListMetric, listOf(longString)) - snapshot = StringListsStorageEngine.getSnapshotAsJSON("store1", true) as JSONObject - stringList = snapshot["telemetry.string_list_metric"] as JSONArray - assertEquals(1, stringList.length()) - assertTrue(stringListMetric.testHasValue()) - assertEquals(longString.take(StringListsStorageEngineImplementation.MAX_STRING_LENGTH), - stringList[0]) - - assertEquals(2, testGetNumRecordedErrors(stringListMetric, ErrorType.InvalidValue)) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringsStorageEngineTest.kt deleted file mode 100644 index 31ef6b4997a..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/StringsStorageEngineTest.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.error.ErrorRecording.ErrorType -import mozilla.components.service.glean.error.ErrorRecording.testGetNumRecordedErrors -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.StringMetricType -import mozilla.components.service.glean.testing.GleanTestRule -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class StringsStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Test - fun `string deserializer should correctly parse strings`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_number" to 1, - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.valid" to "test" - ) - - val storageEngine = StringsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals("test", snapshot["telemetry.valid"]) - } - - @Test - fun `string serializer should correctly serialize strings`() { - run { - val storageEngine = StringsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "string_metric", - sendInPings = listOf("store1") - ) - - // Record the string in the store, without providing optional arguments. - storageEngine.record( - metric, - value = "test_string_value" - ) - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.string_metric\":\"test_string_value\"}", - snapshot.toString()) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = StringsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", - clearStore = true) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.string_metric\":\"test_string_value\"}", - snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = storeNames - ) - - // Record the string in the stores, without providing optional arguments. - StringsStorageEngine.record( - metric, - value = "test_string_object" - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = StringsStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals("test_string_object", snapshot.get("telemetry.string_metric")) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - StringsStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val metric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = storeNames - ) - - // Record the string in the stores, without providing optional arguments. - StringsStorageEngine.record( - metric, - value = "test_string_value" - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = StringsStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - StringsStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = StringsStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `Strings are serialized in the correct JSON format`() { - val metric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "string_metric", - sendInPings = listOf("store1") - ) - - // Record the string in the store, without providing optional arguments. - StringsStorageEngine.record( - metric, - value = "test_string_value" - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = StringsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - StringsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.string_metric\":\"test_string_value\"}", - snapshot.toString()) - } - - @Test - fun `The API truncates long string values`() { - // Define a 'stringMetric' string metric, which will be stored in "store1" - val stringMetric = StringMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Application, - name = "string_metric", - sendInPings = listOf("store1") - ) - - val testString = "012345678901234567890".repeat(20) - val expectedString = testString.take(StringsStorageEngineImplementation.MAX_LENGTH_VALUE) - - stringMetric.set(testString) - // Check that data was truncated. - assertTrue(stringMetric.testHasValue()) - assertEquals( - expectedString, - stringMetric.testGetValue() - ) - - assertEquals(1, testGetNumRecordedErrors(stringMetric, ErrorType.InvalidValue)) - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimespansStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimespansStorageEngineTest.kt deleted file mode 100644 index a3ff22ac243..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimespansStorageEngineTest.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.private.TimespanMetricType -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.timing.TimingManager -import org.junit.After - -import org.junit.Before -import org.junit.Test -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.runner.RunWith -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.RobolectricTestRunner -import java.util.concurrent.TimeUnit as AndroidTimeUnit - -@RunWith(RobolectricTestRunner::class) -class TimespansStorageEngineTest { - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @Before - fun setUp() { - TimespansStorageEngine.applicationContext = ApplicationProvider.getApplicationContext() - TimespansStorageEngine.clearAllStores() - } - - @After - fun reset() { - TimingManager.testResetTimeSource() - } - - @Test - fun `timespan deserializer should correctly parse JSONArray(s)`() { - val expectedValue: Long = 37 - val persistedSample = mapOf( - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.invalid_string" to "c4ff33", - "store1#telemetry.valid" to "[${TimeUnit.Nanosecond.ordinal}, $expectedValue]" - ) - - val storageEngine = TimespansStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshotWithTimeUnit(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(Pair("nanosecond", expectedValue), snapshot["telemetry.valid"]) - } - - @Test - fun `a single elapsed time must be correctly recorded`() { - val expectedTimespanNanos: Long = 37 - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "single_elapsed_test", - listOf("store1"), - timeUnit = TimeUnit.Nanosecond - ) - - // Return 0 the first time we get the time - TimingManager.getElapsedNanos = { 0 } - metric.start() - - // Return the expected time when we stop the timer. - TimingManager.getElapsedNanos = { expectedTimespanNanos } - metric.stop() - - assertTrue(metric.testHasValue()) - - val snapshot = TimespansStorageEngine.getSnapshotWithTimeUnit(storeName = "store1", clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(Pair("nanosecond", expectedTimespanNanos), snapshot["telemetry.single_elapsed_test"]) - } - - @Test - fun `timespan only records a single timespan`() { - val expectedChunkNanos: Long = 37 - - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "single_elapsed_test", - listOf("store1"), - timeUnit = TimeUnit.Nanosecond - ) - - // Record the time for the first chunk. - TimingManager.getElapsedNanos = { 0 } - metric.start() - TimingManager.getElapsedNanos = { expectedChunkNanos } - metric.stop() - - // Record the time for the second chunk of time. - metric.start() - TimingManager.getElapsedNanos = { expectedChunkNanos * 2 } - metric.stop() - - assertTrue(metric.testHasValue()) - - val snapshot = TimespansStorageEngine.getSnapshotWithTimeUnit(storeName = "store1", clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals( - "TimespanMetricType must only record one timespan", - Pair("nanosecond", expectedChunkNanos), snapshot["telemetry.single_elapsed_test"] - ) - } - - @Test - fun `the recorded time must conform to the chosen resolution`() { - val expectedLengthInNanos: Long = AndroidTimeUnit.DAYS.toNanos(3) - val expectedResults = mapOf( - TimeUnit.Nanosecond to expectedLengthInNanos, - TimeUnit.Microsecond to AndroidTimeUnit.NANOSECONDS.toMicros(expectedLengthInNanos), - TimeUnit.Millisecond to AndroidTimeUnit.NANOSECONDS.toMillis(expectedLengthInNanos), - TimeUnit.Second to AndroidTimeUnit.NANOSECONDS.toSeconds(expectedLengthInNanos), - TimeUnit.Minute to AndroidTimeUnit.NANOSECONDS.toMinutes(expectedLengthInNanos), - TimeUnit.Hour to AndroidTimeUnit.NANOSECONDS.toHours(expectedLengthInNanos), - TimeUnit.Day to AndroidTimeUnit.NANOSECONDS.toDays(expectedLengthInNanos) - ) - - expectedResults.forEach { (res, expectedTimespan) -> - val metric = TimespanMetricType( - false, - "telemetry", - Lifetime.Ping, - "resolution_test", - listOf("store1"), - timeUnit = res - ) - - // Record the timespan in the provided resolution. - TimingManager.getElapsedNanos = { 0 } - metric.start() - TimingManager.getElapsedNanos = { expectedLengthInNanos } - metric.stop() - assertTrue(metric.testHasValue()) - - val snapshot = TimespansStorageEngine.getSnapshotWithTimeUnit(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(Pair(res.name.toLowerCase(), expectedTimespan), snapshot["telemetry.resolution_test"]) - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngineTest.kt deleted file mode 100644 index 09ce19fba30..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/TimingDistributionsStorageEngineTest.kt +++ /dev/null @@ -1,315 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.runBlocking -import mozilla.components.service.glean.collectAndCheckPingSchema -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.TimeUnit -import mozilla.components.service.glean.private.TimingDistributionMetricType -import mozilla.components.service.glean.error.ErrorRecording -import mozilla.components.service.glean.histogram.FunctionalHistogram -import mozilla.components.service.glean.private.PingType -import mozilla.components.service.glean.testing.GleanTestRule -import mozilla.components.service.glean.timing.TimingManager -import org.junit.After -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class TimingDistributionsStorageEngineTest { - - @get:Rule - val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) - - @After - fun reset() { - TimingManager.testResetTimeSource() - } - - @Test - fun `accumulate() properly updates the values in all stores`() { - val storeNames = listOf("store1", "store2") - - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "test_timing_distribution", - sendInPings = storeNames, - timeUnit = TimeUnit.Millisecond - ) - - // Create a sample that will fall into the underflow bucket (bucket '0') so we can easily - // find it - val sample = 1L - TimingDistributionsStorageEngine.accumulate( - metricData = metric, - sample = sample - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = TimingDistributionsStorageEngine.getSnapshot( - storeName = storeName, - clearStore = true - ) - assertEquals(1, snapshot!!.size) - assertEquals(1L, snapshot["telemetry.test_timing_distribution"]?.values!![1]) - } - } - - @Test - fun `deserializer should correctly parse timing distributions`() { - val td = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - val persistedSample = mapOf( - "store1#telemetry.invalid_string" to "invalid_string", - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.null" to null, - "store1#telemetry.invalid_int" to -1, - "store1#telemetry.invalid_list" to listOf("1", "2", "3"), - "store1#telemetry.invalid_int_list" to "[1,2,3]", - "store1#telemetry.invalid_td_values" to "{\"log_base\":2.0,\"buckets_per_magnitude\":8.0,\"values\":{\"0\": \"nope\"},\"sum\":0}", - "store1#telemetry.invalid_td_sum" to "{\"log_base\":2.0,\"buckets_per_magnitude\":8.0,\"values\":{},\"sum\":\"nope\"}", - "store1#telemetry.test_timing_distribution" to td.toJsonObject().toString() - ) - - val storageEngine = TimingDistributionsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = Mockito.mock(Context::class.java) - val sharedPreferences = Mockito.mock(SharedPreferences::class.java) - Mockito.`when`(sharedPreferences.all).thenAnswer { persistedSample } - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq(storageEngine::class.java.canonicalName), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - Mockito.`when`(context.getSharedPreferences( - ArgumentMatchers.eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - ArgumentMatchers.eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals(td.toJsonObject().toString(), - snapshot["telemetry.test_timing_distribution"]?.toJsonObject().toString()) - } - - @Test - fun `serializer should serialize timing distribution that matches schema`() { - val ping1 = PingType("store1", true) - - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - runBlocking { - TimingManager.getElapsedNanos = { 0 } - val id = metric.start() - TimingManager.getElapsedNanos = { 1000000 } - metric.stopAndAccumulate(id) - } - - collectAndCheckPingSchema(ping1) - } - - @Test - fun `serializer should correctly serialize timing distributions`() { - run { - val storageEngine = TimingDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_timing_distribution", - sendInPings = listOf("store1", "store2"), - timeUnit = TimeUnit.Millisecond - ) - - // Using the FunctionalHistogram object here to easily turn the object into JSON - // for comparison purposes. - val td = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - td.accumulate(1000000L) - - runBlocking { - storageEngine.accumulate( - metricData = metric, - sample = 1000000L - ) - } - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"${metric.identifier}\":${td.toJsonPayloadObject()}}", - json.toString() - ) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = TimingDistributionsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val td = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - td.accumulate(1000000L) - - // Get snapshot from store1 - val json = storageEngine.getSnapshotAsJSON("store1", true) - // Check for correct JSON serialization - assertEquals("{\"telemetry.test_timing_distribution\":${td.toJsonPayloadObject()}}", - json.toString() - ) - } - } - - @Test - fun `timing distributions must not accumulate negative values`() { - // Define a timing distribution metric, which will be stored in "store1". - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Attempt to accumulate a negative sample - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { -1 } - metric.stopAndAccumulate(timerId) - // Check that nothing was recorded. - assertFalse("Timing distributions must not accumulate negative values", - metric.testHasValue()) - - // Make sure that the errors have been recorded - assertEquals("Accumulating negative values must generate an error", - 1, - ErrorRecording.testGetNumRecordedErrors(metric, ErrorRecording.ErrorType.InvalidValue)) - } - - @Test - fun `overflow values accumulate in the last bucket`() { - // Define a timing distribution metric, which will be stored in "store1". - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - // Attempt to accumulate an overflow sample - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { TimingDistributionsStorageEngineImplementation.MAX_SAMPLE_TIME * 2 } - metric.stopAndAccumulate(timerId) - - // Check that timing distribution was recorded. - assertTrue("Accumulating overflow values records data", - metric.testHasValue()) - - // Make sure that the overflow landed in the correct (last) bucket - val hist = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - val snapshot = metric.testGetValue() - assertEquals("Accumulating overflow values should increment last bucket", - 1L, - snapshot.values[hist.sampleToBucketMinimum(TimingDistributionsStorageEngineImplementation.MAX_SAMPLE_TIME)]) - } - - @Test - fun `accumulate finds the correct bucket`() { - // Define a timing distribution metric, which will be stored in "store1". - val metric = TimingDistributionMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "test_timing_distribution", - sendInPings = listOf("store1"), - timeUnit = TimeUnit.Millisecond - ) - - val samples = listOf(10L, 100L, 1000L, 10000L) - - // Attempt to accumulate a sample to force metric to be stored - for (i in samples) { - TimingManager.getElapsedNanos = { 0 } - val timerId = metric.start() - TimingManager.getElapsedNanos = { i } - metric.stopAndAccumulate(timerId) - } - - // Check that timing distribution was recorded. - assertTrue("Accumulating values records data", metric.testHasValue()) - - // Make sure that the samples are in the correct buckets - val snapshot = metric.testGetValue() - - val hist = FunctionalHistogram( - TimingDistributionsStorageEngineImplementation.LOG_BASE, - TimingDistributionsStorageEngineImplementation.BUCKETS_PER_MAGNITUDE - ) - - // Check sum and count - assertEquals("Accumulating updates the sum", 11110, snapshot.sum) - assertEquals("Accumulating updates the count", 4, snapshot.count) - - for (i in samples) { - val binEdge = hist.sampleToBucketMinimum(i) - assertEquals("Accumulating should increment correct bucket", 1L, snapshot.values[binEdge]) - } - - val json = snapshot.toJsonPayloadObject() - val values = json.getJSONObject("values") - assertEquals(81, values.length()) - - for (i in samples) { - val binEdge = hist.sampleToBucketMinimum(i) - assertEquals("Accumulating should increment correct bucket", 1L, values.getLong(binEdge.toString())) - values.remove(binEdge.toString()) - } - - for (k in values.keys()) { - assertEquals(0L, values.getLong(k)) - } - } -} diff --git a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/UuidsStorageEngineTest.kt b/components/service/glean/src/test/java/mozilla/components/service/glean/storages/UuidsStorageEngineTest.kt deleted file mode 100644 index 97d14a0fdfa..00000000000 --- a/components/service/glean/src/test/java/mozilla/components/service/glean/storages/UuidsStorageEngineTest.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -package mozilla.components.service.glean.storages - -import android.content.Context -import android.content.SharedPreferences -import androidx.test.core.app.ApplicationProvider -import mozilla.components.service.glean.private.Lifetime -import mozilla.components.service.glean.private.UuidMetricType -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.robolectric.RobolectricTestRunner -import java.util.UUID - -@RunWith(RobolectricTestRunner::class) -class UuidsStorageEngineTest { - - @Before - fun setUp() { - UuidsStorageEngine.applicationContext = ApplicationProvider.getApplicationContext() - UuidsStorageEngine.clearAllStores() - } - - @Test - fun `uuid deserializer should correctly parse UUIDs`() { - val persistedSample = mapOf( - "store1#telemetry.invalid_number" to 1, - "store1#telemetry.invalid_bool" to false, - "store1#telemetry.invalid_string" to "c4ff33", - "store1#telemetry.valid" to "ce2adeb8-843a-4232-87a5-a099ed1e7bb3" - ) - - val storageEngine = UuidsStorageEngineImplementation() - - // Create a fake application context that will be used to load our data. - val context = mock(Context::class.java) - val sharedPreferences = mock(SharedPreferences::class.java) - `when`(sharedPreferences.all).thenAnswer { persistedSample } - `when`(context.getSharedPreferences( - eq(storageEngine::class.java.canonicalName), - eq(Context.MODE_PRIVATE) - )).thenReturn(sharedPreferences) - `when`(context.getSharedPreferences( - eq("${storageEngine::class.java.canonicalName}.PingLifetime"), - eq(Context.MODE_PRIVATE) - )).thenReturn(ApplicationProvider.getApplicationContext() - .getSharedPreferences("${storageEngine::class.java.canonicalName}.PingLifetime", - Context.MODE_PRIVATE)) - - storageEngine.applicationContext = context - val snapshot = storageEngine.getSnapshot(storeName = "store1", clearStore = true) - assertEquals(1, snapshot!!.size) - assertEquals("ce2adeb8-843a-4232-87a5-a099ed1e7bb3", snapshot["telemetry.valid"].toString()) - } - - @Test - fun `UUID serializer correctly serializes UUID's`() { - run { - val storageEngine = UuidsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val testUUID = "ce2adeb8-843a-4232-87a5-a099ed1e7bb3" - - val metric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "uuid_metric", - sendInPings = listOf("store1") - ) - - // Record the string in the store, without providing optional arguments. - storageEngine.record( - metric, - value = UUID.fromString(testUUID) - ) - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - assertEquals("{\"telemetry.uuid_metric\":\"$testUUID\"}", - snapshot.toString()) - } - - // Create a new instance of storage engine to verify serialization to storage rather than - // to the cache - run { - val storageEngine = UuidsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val testUUID = "ce2adeb8-843a-4232-87a5-a099ed1e7bb3" - - // Get the snapshot from "store1" - val snapshot = storageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - assertEquals("{\"telemetry.uuid_metric\":\"$testUUID\"}", - snapshot.toString()) - } - } - - @Test - fun `setValue() properly sets the value in all stores`() { - val storeNames = listOf("store1", "store2") - val uuid = UUID.fromString("ce2adeb8-843a-4232-87a5-a099ed1e7bb3") - - val metric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "uuid_metric", - sendInPings = storeNames - ) - - // Record the uuid in the stores, without providing optional arguments. - UuidsStorageEngine.record( - metric, - value = uuid - ) - - // Check that the data was correctly set in each store. - for (storeName in storeNames) { - val snapshot = UuidsStorageEngine.getSnapshot(storeName = storeName, clearStore = false) - assertEquals(1, snapshot!!.size) - assertEquals(uuid, snapshot.get("telemetry.uuid_metric")) - } - } - - @Test - fun `getSnapshot() returns null if nothing is recorded in the store`() { - assertNull("The engine must report 'null' on empty or unknown stores", - UuidsStorageEngine.getSnapshot(storeName = "unknownStore", clearStore = false)) - } - - @Test - fun `getSnapshot() correctly clears the stores`() { - val storeNames = listOf("store1", "store2") - - val uuid = UUID.fromString("ce2adeb8-843a-4232-87a5-a099ed1e7bb3") - - val metric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "uuid_metric", - sendInPings = storeNames - ) - - // Record the uuid in the stores, without providing optional arguments. - UuidsStorageEngine.record( - metric, - value = uuid - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = UuidsStorageEngine.getSnapshot(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - UuidsStorageEngine.getSnapshot(storeName = "store1", clearStore = false)) - - // Check that we get the right data from both the stores. Clearing "store1" must - // not clear "store2" as well. - val snapshot2 = UuidsStorageEngine.getSnapshot(storeName = "store2", clearStore = false) - for (s in listOf(snapshot, snapshot2)) { - assertEquals(1, s!!.size) - } - } - - @Test - fun `Uuids are serialized in the correct JSON format`() { - val testUUID = "ce2adeb8-843a-4232-87a5-a099ed1e7bb3" - - val metric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.Ping, - name = "uuid_metric", - sendInPings = listOf("store1") - ) - - // Record the string in the store, without providing optional arguments. - UuidsStorageEngine.record( - metric, - value = UUID.fromString(testUUID) - ) - - // Get the snapshot from "store1" and clear it. - val snapshot = UuidsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = true) - // Check that getting a new snapshot for "store1" returns an empty store. - assertNull("The engine must report 'null' on empty stores", - UuidsStorageEngine.getSnapshotAsJSON(storeName = "store1", clearStore = false)) - // Check that this serializes to the expected JSON format. - assertEquals("{\"telemetry.uuid_metric\":\"$testUUID\"}", - snapshot.toString()) - } - - @Test - fun `uuids with 'user' lifetime must be correctly persisted`() { - val sampleUUID = "decaffde-caff-d3ca-ffd3-caffd3caffd3" - - val storageEngine = UuidsStorageEngineImplementation() - storageEngine.applicationContext = ApplicationProvider.getApplicationContext() - - val metric = UuidMetricType( - disabled = false, - category = "telemetry", - lifetime = Lifetime.User, - name = "uuidMetric", - sendInPings = listOf("some_store", "other_store") - ) - - storageEngine.record( - metric, - value = UUID.fromString(sampleUUID) - ) - - // Check that the persisted shared prefs contains the expected data. - val storedData = ApplicationProvider.getApplicationContext() - .getSharedPreferences(storageEngine.javaClass.canonicalName, Context.MODE_PRIVATE) - .all - - assertEquals(2, storedData.size) - assertTrue(storedData.containsKey("some_store#telemetry.uuidMetric")) - assertTrue(storedData.containsKey("other_store#telemetry.uuidMetric")) - assertEquals(sampleUUID, storedData.getValue("some_store#telemetry.uuidMetric")) - assertEquals(sampleUUID, storedData.getValue("other_store#telemetry.uuidMetric")) - } -} diff --git a/components/support/sync-telemetry/build.gradle b/components/support/sync-telemetry/build.gradle index ff09c6e68af..19f047ebc64 100644 --- a/components/support/sync-telemetry/build.gradle +++ b/components/support/sync-telemetry/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation Dependencies.androidx_work_testing testImplementation Dependencies.testing_mockito + testImplementation Dependencies.mozilla_glean_forUnitTests } apply from: '../../../publish.gradle' diff --git a/docs/changelog.md b/docs/changelog.md index 9575ebb9d9b..00000cd3be7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,10 @@ permalink: /changelog/ * **browser-state** * Added `externalAppType` to `CustomTabConfig` to indicate how the session is being used. +* **service-glean** + * The Rust implementation of the Glean SDK is now being used. + * ⚠️ **This is a breaking change**: the `GleanDebugActivity` is no longer exposed from service-glean. Users need to use the one in `mozilla.telemetry.glean.debug.GleanDebugActivity` from the `adb` command line. + # 18.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v17.0.0...v18.0.0)