summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/service/glean
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/service/glean')
-rw-r--r--mobile/android/android-components/components/service/glean/README.md21
-rw-r--r--mobile/android/android-components/components/service/glean/build.gradle58
-rw-r--r--mobile/android/android-components/components/service/glean/gradle.properties0
-rw-r--r--mobile/android/android-components/components/service/glean/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt145
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt50
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt109
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt119
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt10
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt31
-rw-r--r--mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt7
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java68
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt44
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt340
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties1
17 files changed, 1029 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/service/glean/README.md b/mobile/android/android-components/components/service/glean/README.md
new file mode 100644
index 0000000000..c08c0d17f1
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/README.md
@@ -0,0 +1,21 @@
+# [Android Components](../../../README.md) > Service > Glean
+
+A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service.
+
+Visit the [complete Glean SDK documentation](https://mozilla.github.io/glean/).
+
+## Contact
+
+To contact us you can:
+- Find us in the [#glean channel on chat.mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org).
+* To report issues or request changes, file a bug in [Bugzilla in Data Platform & Tools :: Glean: SDK][newbugzilla].
+- Send an email to *glean-team@mozilla.com*.
+* The Glean Android team is: *:dexter*, *:travis*, *:mdroettboom*, *:janerik*, *:brizental*.
+
+## License
+
+ 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/
+
+[newbugzilla]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D
diff --git a/mobile/android/android-components/components/service/glean/build.gradle b/mobile/android/android-components/components/service/glean/build.gradle
new file mode 100644
index 0000000000..aaed4bac50
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/build.gradle
@@ -0,0 +1,58 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.service.glean'
+}
+
+// Define library names and version constants.
+String GLEAN_LIBRARY = "org.mozilla.telemetry:glean:${Versions.mozilla_glean}"
+String GLEAN_LIBRARY_FORUNITTESTS = "org.mozilla.telemetry:glean-native-forUnitTests:${Versions.mozilla_glean}"
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_work_runtime
+
+ api GLEAN_LIBRARY
+
+ // So consumers can set a HTTP client.
+ api project(':concept-fetch')
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':support-utils')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockwebserver
+ testImplementation ComponentsDependencies.androidx_work_testing
+
+ testImplementation project(':support-test')
+ testImplementation project(':lib-fetch-httpurlconnection')
+ testImplementation project(':lib-fetch-okhttp')
+
+ testImplementation GLEAN_LIBRARY_FORUNITTESTS
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/service/glean/gradle.properties b/mobile/android/android-components/components/service/glean/gradle.properties
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/gradle.properties
diff --git a/mobile/android/android-components/components/service/glean/proguard-rules.pro b/mobile/android/android-components/components/service/glean/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
new file mode 100644
index 0000000000..232ac1b83f
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/Glean.kt
@@ -0,0 +1,145 @@
+/* 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 androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.glean.config.Configuration
+import mozilla.components.service.glean.private.RecordedExperiment
+import org.json.JSONObject
+import mozilla.telemetry.glean.Glean as GleanCore
+
+typealias BuildInfo = mozilla.telemetry.glean.BuildInfo
+
+/**
+ * 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.
+ *
+ * This should only be initialized once by the application, and not by
+ * libraries using Glean. A message is logged to error and no changes are made
+ * to the state if initialize is called a more than once.
+ *
+ * A LifecycleObserver will be added to send pings when the application goes
+ * into the background.
+ *
+ * @param applicationContext [Context] to access application features, such
+ * as shared preferences
+ * @param uploadEnabled A [Boolean] that determines the initial state of the uploader
+ * @param configuration A Glean [Configuration] object with global settings.
+ * @param buildInfo A Glean [BuildInfo] object with build-time metadata. This
+ * object is generated at build time by glean_parser at the import path
+ * ${YOUR_PACKAGE_ROOT}.GleanMetrics.GleanBuildInfo.buildInfo
+ */
+ @MainThread
+ fun initialize(
+ applicationContext: Context,
+ uploadEnabled: Boolean,
+ configuration: Configuration,
+ buildInfo: BuildInfo,
+ ) {
+ GleanCore.initialize(
+ applicationContext = applicationContext,
+ uploadEnabled = uploadEnabled,
+ configuration = configuration.toWrappedConfiguration(),
+ buildInfo = buildInfo,
+ )
+ }
+
+ /**
+ * Register the pings generated from `pings.yaml` with Glean.
+ *
+ * @param pings The `Pings` object generated for your library or application
+ * by Glean.
+ */
+ fun registerPings(pings: Any) {
+ GleanCore.registerPings(pings)
+ }
+
+ /**
+ * Enable or disable Glean collection and upload.
+ *
+ * Metric collection is enabled by default.
+ *
+ * When disabled, metrics aren't recorded at all and no data
+ * is uploaded.
+ *
+ * @param enabled When true, enable metric collection.
+ */
+ fun setUploadEnabled(enabled: Boolean) {
+ GleanCore.setUploadEnabled(enabled)
+ }
+
+ /**
+ * Indicate that an experiment is running. Glean will then add an
+ * experiment annotation to the environment which is sent with pings. This
+ * information is not persisted between runs.
+ *
+ * @param experimentId The id of the active experiment (maximum
+ * 30 bytes)
+ * @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<String, String>? = null,
+ ) {
+ GleanCore.setExperimentActive(
+ experimentId = experimentId,
+ branch = branch,
+ extra = extra,
+ )
+ }
+
+ /**
+ * Indicate that an experiment is no longer running.
+ *
+ * @param experimentId The id of the experiment to deactivate.
+ */
+ fun setExperimentInactive(experimentId: String) {
+ GleanCore.setExperimentInactive(experimentId = experimentId)
+ }
+
+ /**
+ * Set configuration to override metrics' enabled state, typically from a remote_settings
+ * experiment or rollout.
+ *
+ * @param enabled Map of metrics' enabled state.
+ */
+ fun setMetricsEnabledConfig(enabled: Map<String, Boolean>) {
+ GleanCore.setMetricsEnabledConfig(JSONObject(enabled).toString())
+ }
+
+ /**
+ * Tests whether an experiment is active, for testing purposes only.
+ *
+ * @param experimentId the id of the experiment to look for.
+ * @return true if the experiment is active and reported in pings, otherwise false
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testIsExperimentActive(experimentId: String): Boolean {
+ return GleanCore.testIsExperimentActive(experimentId)
+ }
+
+ /**
+ * Returns the stored data for the requested active experiment, for testing purposes only.
+ *
+ * @param experimentId the id of the experiment to look for.
+ * @return the [RecordedExperiment] for the experiment
+ * @throws [NullPointerException] if the requested experiment is not active or data is corrupt.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testGetExperimentData(experimentId: String): RecordedExperiment {
+ return GleanCore.testGetExperimentData(experimentId)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
new file mode 100644
index 0000000000..11911c7046
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/config/Configuration.kt
@@ -0,0 +1,50 @@
+/* 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.config
+
+import mozilla.telemetry.glean.net.PingUploader
+import mozilla.telemetry.glean.config.Configuration as GleanCoreConfiguration
+
+/**
+ * The Configuration class describes how to configure the Glean.
+ *
+ * @property httpClient The HTTP client implementation to use for uploading pings.
+ * If you don't provide your own networking stack with an HTTP client to use,
+ * you can fall back to a simple implementation on top of `java.net` using
+ * `ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() as Client })`
+ * @property serverEndpoint (optional) the server pings are sent to. Please note that this is
+ * is only meant to be changed for tests.
+ * @property channel (optional )the release channel the application is on, if known. This will be
+ * sent along with all the pings, in the `client_info` section.
+ * @property maxEvents (optional) the number of events to store before the events ping is sent
+ */
+data class Configuration @JvmOverloads constructor(
+ val httpClient: PingUploader,
+ val serverEndpoint: String = DEFAULT_TELEMETRY_ENDPOINT,
+ val channel: String? = null,
+ val maxEvents: Int? = null,
+ val enableEventTimestamps: Boolean = false,
+) {
+ // The following is required to support calling our API from Java.
+ companion object {
+ const val DEFAULT_TELEMETRY_ENDPOINT = GleanCoreConfiguration.DEFAULT_TELEMETRY_ENDPOINT
+ }
+
+ /**
+ * 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 = serverEndpoint,
+ channel = channel,
+ maxEvents = maxEvents,
+ httpClient = httpClient,
+ enableEventTimestamps = enableEventTimestamps,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt
new file mode 100644
index 0000000000..06947e6d5b
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/net/ConceptFetchHttpUploader.kt
@@ -0,0 +1,109 @@
+/* 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 androidx.annotation.VisibleForTesting.Companion.PRIVATE
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Header
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.toMutableHeaders
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.telemetry.glean.net.HeadersList
+import mozilla.telemetry.glean.net.HttpStatus
+import mozilla.telemetry.glean.net.RecoverableFailure
+import mozilla.telemetry.glean.net.UploadResult
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+import mozilla.telemetry.glean.net.PingUploader as CorePingUploader
+
+typealias PingUploader = CorePingUploader
+
+/**
+ * A simple ping Uploader, which implements a "send once" policy, never
+ * storing or attempting to send the ping again. This uses Android Component's
+ * `concept-fetch`.
+ *
+ * @param usePrivateRequest Sets the [Request.private] flag in all requests using this uploader.
+ */
+class ConceptFetchHttpUploader(
+ internal val client: Lazy<Client>,
+ private val usePrivateRequest: Boolean = false,
+) : PingUploader {
+ private val logger = Logger("glean/ConceptFetchHttpUploader")
+
+ companion object {
+ // The timeout, in milliseconds, to use when connecting to the server.
+ const val DEFAULT_CONNECTION_TIMEOUT = 10000L
+
+ // The timeout, in milliseconds, to use when reading from the server.
+ const val DEFAULT_READ_TIMEOUT = 30000L
+
+ /**
+ * Export a constructor that is usable from Java.
+ *
+ * This looses the lazyness of creating the `client`.
+ */
+ @JvmStatic
+ fun fromClient(client: Client): ConceptFetchHttpUploader {
+ return ConceptFetchHttpUploader(lazy { client })
+ }
+ }
+
+ /**
+ * 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.
+ */
+ override fun upload(url: String, data: ByteArray, headers: HeadersList): UploadResult {
+ val request = buildRequest(url, data, headers)
+
+ return try {
+ performUpload(client.value, request)
+ } catch (e: IOException) {
+ logger.warn("IOException while uploading ping", e)
+ RecoverableFailure(0)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal fun buildRequest(
+ url: String,
+ data: ByteArray,
+ headers: HeadersList,
+ ): Request {
+ val conceptHeaders = headers.map { (name, value) -> Header(name, value) }.toMutableHeaders()
+
+ return Request(
+ url = url,
+ method = Request.Method.POST,
+ connectTimeout = Pair(DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS),
+ readTimeout = Pair(DEFAULT_READ_TIMEOUT, TimeUnit.MILLISECONDS),
+ headers = conceptHeaders,
+ // Make sure we are not sending cookies. Unfortunately, HttpURLConnection doesn't
+ // offer a better API to do that, so we nuke all cookies going to our telemetry
+ // endpoint.
+ cookiePolicy = Request.CookiePolicy.OMIT,
+ body = Request.Body(data.inputStream()),
+ private = usePrivateRequest,
+ conservative = true,
+ )
+ }
+
+ @Throws(IOException::class)
+ internal fun performUpload(client: Client, request: Request): UploadResult {
+ logger.debug("Submitting ping to: ${request.url}")
+ client.fetch(request).use { response ->
+ return HttpStatus(response.status)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
new file mode 100644
index 0000000000..e6e0be9424
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/private/MetricAliases.kt
@@ -0,0 +1,119 @@
+/* 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
+
+typealias CommonMetricData = mozilla.telemetry.glean.private.CommonMetricData
+typealias EventExtras = mozilla.telemetry.glean.private.EventExtras
+typealias Lifetime = mozilla.telemetry.glean.private.Lifetime
+typealias NoExtras = mozilla.telemetry.glean.private.NoExtras
+typealias NoReasonCodes = mozilla.telemetry.glean.private.NoReasonCodes
+typealias ReasonCode = mozilla.telemetry.glean.private.ReasonCode
+
+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 DenominatorMetricType = mozilla.telemetry.glean.private.DenominatorMetricType
+typealias HistogramMetricBase = mozilla.telemetry.glean.private.HistogramBase
+typealias HistogramType = mozilla.telemetry.glean.private.HistogramType
+typealias LabeledMetricType<T> = mozilla.telemetry.glean.private.LabeledMetricType<T>
+typealias MemoryDistributionMetricType = mozilla.telemetry.glean.private.MemoryDistributionMetricType
+typealias MemoryUnit = mozilla.telemetry.glean.private.MemoryUnit
+typealias NumeratorMetricType = mozilla.telemetry.glean.private.NumeratorMetricType
+typealias PingType<T> = mozilla.telemetry.glean.private.PingType<T>
+typealias QuantityMetricType = mozilla.telemetry.glean.private.QuantityMetricType
+typealias RateMetricType = mozilla.telemetry.glean.private.RateMetricType
+typealias RecordedExperiment = mozilla.telemetry.glean.private.RecordedExperiment
+typealias StringListMetricType = mozilla.telemetry.glean.private.StringListMetricType
+typealias StringMetricType = mozilla.telemetry.glean.private.StringMetricType
+typealias TextMetricType = mozilla.telemetry.glean.private.TextMetricType
+typealias TimeUnit = mozilla.telemetry.glean.private.TimeUnit
+typealias TimespanMetricType = mozilla.telemetry.glean.private.TimespanMetricType
+typealias TimingDistributionMetricType = mozilla.telemetry.glean.private.TimingDistributionMetricType
+typealias UrlMetricType = mozilla.telemetry.glean.private.UrlMetricType
+typealias UuidMetricType = mozilla.telemetry.glean.private.UuidMetricType
+
+// FIXME(bug 1885170): Wrap the Glean SDK `EventMetricType` to overwrite the `testGetValue` function.
+/**
+ * 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.
+ */
+class EventMetricType<ExtraObject> internal constructor(
+ private var inner: mozilla.telemetry.glean.private.EventMetricType<ExtraObject>,
+) where ExtraObject : EventExtras {
+ /**
+ * The public constructor used by automatically generated metrics.
+ */
+ constructor(meta: CommonMetricData, allowedExtraKeys: List<String>) :
+ this(inner = mozilla.telemetry.glean.private.EventMetricType(meta, allowedExtraKeys))
+
+ /**
+ * Record an event by using the information provided by the instance of this class.
+ *
+ * @param extra The event extra properties.
+ * Values are converted to strings automatically
+ * This is used for events where additional richer context is needed.
+ * The maximum length for values is 100 bytes.
+ *
+ * Note: `extra` is not optional here to avoid overlapping with the above definition of `record`.
+ * If no `extra` data is passed the above function will be invoked correctly.
+ */
+ fun record(extra: ExtraObject? = null) {
+ inner.record(extra)
+ }
+
+ /**
+ * 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 first value in `sendInPings`.
+ * @return value of the stored events
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @JvmOverloads
+ fun testGetValue(pingName: String? = null): List<mozilla.telemetry.glean.private.RecordedEvent>? {
+ var events = inner.testGetValue(pingName)
+ if (events == null) {
+ return events
+ }
+
+ // Remove the `glean_timestamp` extra.
+ // This is added by Glean and does not need to be exposed to testing.
+ for (event in events) {
+ if (event.extra == null) {
+ continue
+ }
+
+ // We know it's not null
+ var map = event.extra!!.toMutableMap()
+ map.remove("glean_timestamp")
+ if (map.isEmpty()) {
+ event.extra = null
+ } else {
+ event.extra = map
+ }
+ }
+
+ return events
+ }
+
+ /**
+ * Returns the number of errors recorded for the given metric.
+ *
+ * @param errorType The type of the error recorded.
+ * @return the number of errors recorded for the metric.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testGetNumRecordedErrors(errorType: mozilla.components.service.glean.testing.ErrorType) =
+ inner.testGetNumRecordedErrors(errorType)
+}
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt
new file mode 100644
index 0000000000..8f4e53d501
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/ErrorType.kt
@@ -0,0 +1,10 @@
+/* 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.testing
+
+/**
+ * Different types of errors that can be reported through Glean's error reporting metrics.
+ */
+typealias ErrorType = mozilla.telemetry.glean.testing.ErrorType
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt
new file mode 100644
index 0000000000..0a02b953bb
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestLocalServer.kt
@@ -0,0 +1,31 @@
+/* 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.testing
+
+/**
+ * This implements a JUnit rule for writing tests for Glean SDK metrics.
+ *
+ * The rule takes care of sending Glean SDK pings to a local server, at the
+ * address: "http://localhost:<port>".
+ *
+ * This is useful for Android instrumented tests, where we don't want to
+ * initialize Glean more than once but still want to send pings to a local
+ * server for validation.
+ *
+ * FIXME(bug 1787234): State of the local server can persist across multiple test classes,
+ * leading to hard-to-diagnose intermittent test failures.
+ * It might be necessary to limit use of `GleanTestLocalServer` to a single test class for now.
+ *
+ * Example usage:
+ *
+ * ```
+ * // Add the following lines to you test class.
+ * @get:Rule
+ * val gleanRule = GleanTestLocalServer(3785)
+ * ```
+ *
+ * @param localPort the port of the local ping server
+ */
+typealias GleanTestLocalServer = mozilla.telemetry.glean.testing.GleanTestLocalServer
diff --git a/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt
new file mode 100644
index 0000000000..91d20fad70
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/main/java/mozilla/components/service/glean/testing/GleanTestRule.kt
@@ -0,0 +1,7 @@
+/* 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.testing
+
+typealias GleanTestRule = mozilla.telemetry.glean.testing.GleanTestRule
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java
new file mode 100644
index 0000000000..c495e68bbc
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanFromJavaTest.java
@@ -0,0 +1,68 @@
+/* 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.test.core.app.ApplicationProvider;
+import androidx.work.testing.WorkManagerTestInitHelper;
+
+import android.content.Context;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient;
+import mozilla.components.service.glean.config.Configuration;
+import mozilla.components.service.glean.net.ConceptFetchHttpUploader;
+import mozilla.telemetry.glean.BuildInfo;
+
+@RunWith(RobolectricTestRunner.class)
+public class GleanFromJavaTest {
+ // The only purpose of these tests is to make sure the Glean API is
+ // callable from Java. If something goes wrong, it should complain about missing
+ // methods at build-time.
+
+ @Test
+ public void testInitGleanWithDefaults() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WorkManagerTestInitHelper.initializeTestWorkManager(context);
+ ConceptFetchHttpUploader httpClient = ConceptFetchHttpUploader.fromClient(new HttpURLConnectionClient());
+ Configuration config = new Configuration(httpClient);
+ BuildInfo buildInfo = new BuildInfo("test", "test", Calendar.getInstance());
+ Glean.INSTANCE.initialize(context, true, config, buildInfo);
+ }
+
+ @Test
+ public void testInitGleanWithConfiguration() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WorkManagerTestInitHelper.initializeTestWorkManager(context);
+ ConceptFetchHttpUploader httpClient = ConceptFetchHttpUploader.fromClient(new HttpURLConnectionClient());
+ Configuration config =
+ new Configuration(httpClient, Configuration.DEFAULT_TELEMETRY_ENDPOINT, "test-channel");
+ BuildInfo buildInfo = new BuildInfo("test", "test", Calendar.getInstance());
+ Glean.INSTANCE.initialize(context, true, config, buildInfo);
+ }
+
+ @Test
+ public void testGleanExperimentsAPIWithDefaults() {
+ Glean.INSTANCE.setExperimentActive("test-exp-id-1", "test-branch-1");
+ }
+
+ @Test
+ public void testGleanExperimentsAPIWithOptional() {
+ Map<String, String> experimentProperties = new HashMap<>();
+ experimentProperties.put("test-prop1", "test-prop-result1");
+
+ Glean.INSTANCE.setExperimentActive(
+ "test-exp-id-1",
+ "test-branch-1",
+ experimentProperties
+ );
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt
new file mode 100644
index 0000000000..7eaae408f2
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/GleanTest.kt
@@ -0,0 +1,44 @@
+/* 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 androidx.test.core.app.ApplicationProvider
+import mozilla.components.service.glean.private.BooleanMetricType
+import mozilla.components.service.glean.private.CommonMetricData
+import mozilla.components.service.glean.private.Lifetime
+import mozilla.components.service.glean.testing.GleanTestRule
+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 GleanTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @get:Rule
+ val gleanRule = GleanTestRule(context)
+
+ @Test
+ fun `Glean correctly initializes and records a metric`() {
+ // Define a 'booleanMetric' boolean metric, which will be stored in "store1"
+ val booleanMetric = BooleanMetricType(
+ CommonMetricData(
+ disabled = false,
+ category = "telemetry",
+ lifetime = Lifetime.APPLICATION,
+ name = "boolean_metric",
+ sendInPings = listOf("store1"),
+ ),
+ )
+
+ booleanMetric.set(true)
+
+ assertTrue(booleanMetric.testGetValue()!!)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt
new file mode 100644
index 0000000000..48d0d6c3a4
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/java/mozilla/components/service/glean/net/ConceptFetchHttpUploaderTest.kt
@@ -0,0 +1,340 @@
+/* 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.concept.fetch.Client
+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.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.telemetry.glean.config.Configuration
+import mozilla.telemetry.glean.net.HttpStatus
+import mozilla.telemetry.glean.net.RecoverableFailure
+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
+import org.junit.Test
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import java.io.IOException
+import java.net.CookieHandler
+import java.net.CookieManager
+import java.net.HttpCookie
+import java.net.URI
+import java.util.concurrent.TimeUnit
+
+class ConceptFetchHttpUploaderTest {
+ private val testPath: String = "/some/random/path/not/important"
+ private val testPing: String = "{ 'ping': 'test' }"
+ private val testDefaultConfig = Configuration()
+
+ /**
+ * Create a mock webserver that accepts all requests.
+ * @return a [MockWebServer] instance
+ */
+ private fun getMockWebServer(): MockWebServer {
+ val server = MockWebServer()
+ server.dispatcher =
+ object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ return MockResponse().setBody("OK")
+ }
+ }
+
+ return server
+ }
+
+ @Test
+ fun `connection timeouts must be properly set`() {
+ val uploader =
+ spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }))
+
+ val request = uploader.buildRequest(testPath, testPing.toByteArray(), emptyMap())
+
+ assertEquals(
+ Pair(ConceptFetchHttpUploader.DEFAULT_READ_TIMEOUT, TimeUnit.MILLISECONDS),
+ request.readTimeout,
+ )
+ assertEquals(
+ Pair(ConceptFetchHttpUploader.DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS),
+ request.connectTimeout,
+ )
+ }
+
+ @Test
+ fun `Glean headers are correctly dispatched`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+
+ val expectedHeaders = mapOf(
+ "Content-Type" to "application/json; charset=utf-8",
+ "Test-header" to "SomeValue",
+ "OtherHeader" to "Glean/Test 25.0.2",
+ )
+
+ val uploader = ConceptFetchHttpUploader(lazy { mockClient })
+ uploader.upload(testPath, testPing.toByteArray(), expectedHeaders)
+ val requestCaptor = argumentCaptor<Request>()
+ verify(mockClient).fetch(requestCaptor.capture())
+
+ expectedHeaders.forEach { (headerName, headerValue) ->
+ assertEquals(
+ headerValue,
+ requestCaptor.value.headers!![headerName],
+ )
+ }
+ }
+
+ @Test
+ fun `Cookie policy must be properly set`() {
+ val uploader =
+ spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() }))
+
+ val request = uploader.buildRequest(testPath, testPing.toByteArray(), emptyMap())
+
+ assertEquals(request.cookiePolicy, Request.CookiePolicy.OMIT)
+ }
+
+ @Test
+ fun `upload() returns true for successful submissions (200)`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ 200,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(200), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+
+ @Test
+ fun `upload() returns false for server errors (5xx)`() {
+ for (responseCode in 500..527) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() returns true for successful submissions (2xx)`() {
+ for (responseCode in 200..226) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() returns true for failing submissions with broken requests (4xx)`() {
+ for (responseCode in 400..451) {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response(
+ "URL",
+ responseCode,
+ mock(),
+ mock(),
+ ),
+ )
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ assertEquals(HttpStatus(responseCode), uploader.upload(testPath, testPing.toByteArray(), emptyMap()))
+ }
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with default configuration`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with httpurlconnection client`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() correctly uploads the ping data with OkHttp client`() {
+ val server = getMockWebServer()
+
+ val client = ConceptFetchHttpUploader(lazy { OkHttpClient() })
+
+ val submissionUrl = "http://" + server.hostName + ":" + server.port + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), mapOf("test" to "header")))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertEquals("header", request.getHeader("test"))
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() must not transmit any cookie`() {
+ val server = getMockWebServer()
+
+ val testConfig = testDefaultConfig.copy(
+ serverEndpoint = "http://localhost:" + server.port,
+ )
+
+ // Set the default cookie manager/handler to be used for the http upload.
+ val cookieManager = CookieManager()
+ CookieHandler.setDefault(cookieManager)
+
+ // Store a sample cookie.
+ val cookie = HttpCookie("cookie-time", "yes")
+ cookie.domain = testConfig.serverEndpoint
+ cookie.path = testPath
+ cookie.version = 0
+ cookieManager.cookieStore.add(URI(testConfig.serverEndpoint), cookie)
+
+ // Store a cookie for a subdomain of the same domain's as the server endpoint,
+ // to make sure we don't accidentally remove it.
+ val cookie2 = HttpCookie("cookie-time2", "yes")
+ cookie2.domain = "sub.localhost"
+ cookie2.path = testPath
+ cookie2.version = 0
+ cookieManager.cookieStore.add(URI("http://sub.localhost:${server.port}/test"), cookie2)
+
+ // Add another cookie for the same domain. This one should be removed as well.
+ val cookie3 = HttpCookie("cookie-time3", "yes")
+ cookie3.domain = "localhost"
+ cookie3.path = testPath
+ cookie3.version = 0
+ cookieManager.cookieStore.add(URI("http://localhost:${server.port}/test"), cookie3)
+
+ // Trigger the connection.
+ val client = ConceptFetchHttpUploader(lazy { HttpURLConnectionClient() })
+ val submissionUrl = testConfig.serverEndpoint + testPath
+ assertEquals(HttpStatus(200), client.upload(submissionUrl, testPing.toByteArray(), emptyMap()))
+
+ val request = server.takeRequest()
+ assertEquals(testPath, request.path)
+ assertEquals("POST", request.method)
+ assertEquals(testPing, request.body.readUtf8())
+ assertTrue(request.headers.values("Cookie").isEmpty())
+
+ // Check that we still have a cookie.
+ assertEquals(1, cookieManager.cookieStore.cookies.size)
+ assertEquals("cookie-time2", cookieManager.cookieStore.cookies[0].name)
+
+ server.shutdown()
+ }
+
+ @Test
+ fun `upload() should return false when upload fails`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenThrow(IOException())
+
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+
+ // And IOException during upload is a failed upload that we should retry. The client should
+ // return false in this case.
+ assertEquals(RecoverableFailure(0), uploader.upload("path", "ping".toByteArray(), emptyMap()))
+ }
+
+ @Test
+ fun `the lazy client should only be instantiated after the first upload`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+ val uploader = spy<ConceptFetchHttpUploader>(ConceptFetchHttpUploader(lazy { mockClient }))
+ assertFalse(uploader.client.isInitialized())
+
+ // After calling upload, the client must get instantiated.
+ uploader.upload("path", "ping".toByteArray(), emptyMap())
+ assertTrue(uploader.client.isInitialized())
+ }
+
+ @Test
+ fun `usePrivateRequest sends all requests with private flag`() {
+ val mockClient: Client = mock()
+ `when`(mockClient.fetch(any())).thenReturn(
+ Response("URL", 200, mock(), mock()),
+ )
+
+ val expectedHeaders = mapOf(
+ "Content-Type" to "application/json; charset=utf-8",
+ "Test-header" to "SomeValue",
+ "OtherHeader" to "Glean/Test 25.0.2",
+ )
+
+ val uploader = ConceptFetchHttpUploader(lazy { mockClient }, true)
+ uploader.upload(testPath, testPing.toByteArray(), expectedHeaders)
+
+ val captor = argumentCaptor<Request>()
+
+ verify(mockClient).fetch(captor.capture())
+
+ assertTrue(captor.value.private)
+ }
+}
diff --git a/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1f0955d450
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/glean/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28