Skip to content
This repository has been archived by the owner on Dec 14, 2021. It is now read-only.

Commit

Permalink
Introduce Glean SDK (#1085)
Browse files Browse the repository at this point in the history
* Add the Glean SDK as a dependency

* Document the Glean SDK data collection

* Add GleanTelemetryStore

* Address Glean reviewer feedback

 * ensure uload enabled setting is respected before the initialization.
 * Include test to demonstrate this.
 * change the emphasis of Feature Flag so we can deprecate the legacy telemetry service.
 * Move telemetry service registeration dispatch events to `injectContext` so as to be switchable with Feature Flag.

* Add link to data-review
  • Loading branch information
jhugman authored and Elise Richards committed Dec 13, 2019
1 parent f16ca3f commit 6f9d36d
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 9 deletions.
7 changes: 7 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ plugins {
id 'kotlin-android'
id 'io.sentry.android.gradle'
id 'jacoco'
id "com.jetbrains.python.envs" version "0.0.26"
}

android {
Expand Down Expand Up @@ -103,6 +104,7 @@ dependencies {
implementation "org.mozilla.components:service-sync-logins:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:service-firefox-accounts:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:service-telemetry:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:service-glean:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:lib-dataprotect:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:lib-fetch-httpurlconnection:${rootProject.ext.android_components_version}"
implementation "org.mozilla.components:lib-publicsuffixlist:${rootProject.ext.android_components_version}"
Expand Down Expand Up @@ -261,5 +263,10 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions.allWarningsAsErrors = true
}

// Generate markdown docs for the collected metrics.
ext.gleanGenerateMarkdownDocs = true
ext.gleanDocsDirectory = "$rootDir/docs/glean"
apply from: 'https://github.com/mozilla-mobile/android-components/raw/v' + rootProject.ext.android_components_version + '/components/service/glean/scripts/sdk_generator.gradle'

// Internal, but stable and convenient.
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
7 changes: 5 additions & 2 deletions app/src/main/java/mozilla/lockbox/LockboxApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import mozilla.lockbox.store.ClipboardStore
import mozilla.lockbox.store.ContextStore
import mozilla.lockbox.store.DataStore
import mozilla.lockbox.store.FingerprintStore
import mozilla.lockbox.store.GleanTelemetryStore
import mozilla.lockbox.store.LockedStore
import mozilla.lockbox.store.NetworkStore
import mozilla.lockbox.store.SentryStore
Expand All @@ -34,6 +35,7 @@ import mozilla.lockbox.store.TelemetryStore
import mozilla.lockbox.support.AdjustSupport
import mozilla.lockbox.support.TimingSupport
import mozilla.lockbox.support.Constant
import mozilla.lockbox.support.FeatureFlags
import mozilla.lockbox.support.FxASyncDataStoreSupport
import mozilla.lockbox.support.PublicSuffixSupport
import mozilla.lockbox.support.SecurePreferences
Expand Down Expand Up @@ -98,15 +100,16 @@ open class LockboxApplication : Application() {
}

private fun injectContext() {
val contextStoreList: List<ContextStore> = listOf(
val contextStoreList: List<ContextStore> = listOfNotNull(
FingerprintStore.shared,
SettingStore.shared,
SecurePreferences.shared,
ClipboardStore.shared,
NetworkStore.shared,
TimingSupport.shared,
AccountStore.shared,
TelemetryStore.shared,
if (FeatureFlags.INCLUDE_DEPRECATED_TELEMETRY) TelemetryStore.shared else null,
GleanTelemetryStore.shared,
SentryStore.shared,
PublicSuffixSupport.shared
)
Expand Down
12 changes: 9 additions & 3 deletions app/src/main/java/mozilla/lockbox/LockboxAutofillService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.store.AccountStore
import mozilla.lockbox.store.AutofillStore
import mozilla.lockbox.store.DataStore
import mozilla.lockbox.store.GleanTelemetryStore
import mozilla.lockbox.store.SettingStore
import mozilla.lockbox.store.TelemetryStore
import mozilla.lockbox.support.FxASyncDataStoreSupport
import mozilla.lockbox.support.Constant
import mozilla.lockbox.support.FeatureFlags
import mozilla.lockbox.support.PublicSuffixSupport
import mozilla.lockbox.support.SecurePreferences
import mozilla.lockbox.support.asOptional
Expand All @@ -44,9 +47,10 @@ import mozilla.lockbox.support.isDebug
class LockboxAutofillService(
private val accountStore: AccountStore = AccountStore.shared,
private val dataStore: DataStore = DataStore.shared,
private val settingStore: SettingStore = SettingStore.shared,
private val securePreferences: SecurePreferences = SecurePreferences.shared,
private val fxaSupport: FxASyncDataStoreSupport = FxASyncDataStoreSupport.shared,
private val telemetryStore: TelemetryStore = TelemetryStore.shared,
private val gleanTelemetryStore: GleanTelemetryStore = GleanTelemetryStore.shared,
private val autofillStore: AutofillStore = AutofillStore.shared,
val dispatcher: Dispatcher = Dispatcher.shared
) : AutofillService() {
Expand Down Expand Up @@ -148,8 +152,10 @@ class LockboxAutofillService(
private fun intializeService() {
isRunning = true

val contextInjectables = arrayOf(
telemetryStore,
val contextInjectables = listOfNotNull(
settingStore,
gleanTelemetryStore,
if (FeatureFlags.INCLUDE_DEPRECATED_TELEMETRY) TelemetryStore.shared else null,
securePreferences,
accountStore,
fxaSupport
Expand Down
47 changes: 47 additions & 0 deletions app/src/main/java/mozilla/lockbox/store/GleanTelemetryStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.lockbox.store

import android.content.Context
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration
import mozilla.lockbox.BuildConfig

open class GleanWrapper {
open var uploadEnabled: Boolean
get() = Glean.getUploadEnabled()
set(value) {
Glean.setUploadEnabled(value)
}

open fun initialize(context: Context, channel: String) {
Glean.initialize(context, Configuration(channel = channel))
}
}

class GleanTelemetryStore(
private val gleanWrapper: GleanWrapper = GleanWrapper(),
private val settingStore: SettingStore = SettingStore.shared
) : ContextStore {

companion object {
val shared by lazy { GleanTelemetryStore() }
}

private val compositeDisposable = CompositeDisposable()

override fun injectContext(context: Context) {
settingStore.sendUsageData
.subscribe {
gleanWrapper.uploadEnabled = it
}
.addTo(compositeDisposable)
gleanWrapper.initialize(context, BuildConfig.BUILD_TYPE)
}
}
5 changes: 4 additions & 1 deletion app/src/main/java/mozilla/lockbox/store/TelemetryStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package mozilla.lockbox.store

import android.content.Context
import androidx.annotation.VisibleForTesting
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
Expand Down Expand Up @@ -91,7 +92,8 @@ open class TelemetryStore(

internal val compositeDisposable = CompositeDisposable()

init {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun register() {
dispatcher.register
.filterByType(TelemetryAction::class.java)
.subscribe {
Expand All @@ -108,6 +110,7 @@ open class TelemetryStore(
}

override fun injectContext(context: Context) {
register()
wrapper.lateinitContext(context)
settingStore
.sendUsageData
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/mozilla/lockbox/support/FeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,20 @@ object FeatureFlags {
isRelease -> true
else -> true
}

/**
* Include the legacy telemetry service. As part of our migration to the Glean, we will want
* to keep the legacy service running in parallel.
*
* If true, the legacy telemetry-service is used.
* If false, the legacy telemetry-service is not.
*
* Either way, the user can opt out of this from the settings.
*/
val INCLUDE_DEPRECATED_TELEMETRY = when {
isDebug -> true
isTesting -> true
isRelease -> true
else -> true
}
}
136 changes: 136 additions & 0 deletions app/src/test/java/mozilla/lockbox/store/GleanTelemetryStoreTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.lockbox.store

import android.content.Context
import android.content.SharedPreferences
import android.view.autofill.AutofillManager
import androidx.test.core.app.ApplicationProvider
import io.reactivex.subjects.ReplaySubject
import mozilla.lockbox.flux.Dispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.powermock.api.mockito.PowerMockito.`when`
import org.powermock.api.mockito.PowerMockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(packageName = "mozilla.lockbox")
class GleanTelemetryStoreTest {
@Mock
private val settingStore = mock(SettingStore::class.java)
private val sendUsageDataStub = ReplaySubject.createWithSize<Boolean>(1)

@Mock
val telemetryWrapper = object : GleanWrapper() {
override var uploadEnabled = false
var channel: String = ""
override fun initialize(context: Context, channel: String) {
this.channel = channel
}
}

val dispatcher = Dispatcher()

val context: Context = ApplicationProvider.getApplicationContext()

lateinit var subject: GleanTelemetryStore

@Before
fun setUp() {
sendUsageDataStub.onNext(true)
`when`(settingStore.sendUsageData).thenReturn(sendUsageDataStub)
subject = GleanTelemetryStore(telemetryWrapper, settingStore)
}

@Test
fun `when context is injected, verify glean is initialized`() {
subject.injectContext(context)
assertTrue(telemetryWrapper.uploadEnabled)
}

@Test
fun `when sendUsageData is toggled, verify glean is turned off`() {
subject.injectContext(context)
sendUsageDataStub.onNext(false)
assertFalse(telemetryWrapper.uploadEnabled)

sendUsageDataStub.onNext(true)
assertTrue(telemetryWrapper.uploadEnabled)
}

@Test
fun `ensure upload enabled is called before initialize`() {
// We spend quite a lot of effort here to convince ourselves that the user's preference
// for sending usage data is respected before initializing glean.
// If this test fails, then either we're losing ping data or we're uploading ping data
// when the user has explicitly said not to.

// mock all this out for the setting store, so we can use the Rx machinery it uses.
val context = mock(Context::class.java)
`when`(context.getSystemService(eq(AutofillManager::class.java))).thenReturn(mock(AutofillManager::class.java))
val prefs = mock(SharedPreferences::class.java)
`when`(prefs.contains(eq(SettingStore.Keys.DEVICE_SECURITY_PRESENT))).thenReturn(true)
`when`(prefs.contains(eq(SettingStore.Keys.SEND_USAGE_DATA))).thenReturn(true)
`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs)

val fingerprintStore = mock(FingerprintStore::class.java)
`when`(fingerprintStore.isDeviceSecure).thenReturn(true)

class DummyGleanWrapper : GleanWrapper() {
var initializationOrder = arrayListOf<String>()

override var uploadEnabled: Boolean = false
set(value) {
initializationOrder.add("uploadEnabled")
field = value
}

override fun initialize(context: Context, channel: String) {
initializationOrder.add("initialize")
}
}

fun testWithPref(enabled: Boolean) {
`when`(
prefs.getBoolean(
eq(SettingStore.Keys.SEND_USAGE_DATA),
anyBoolean()
)
).thenReturn(enabled)

val telemetryWrapper = DummyGleanWrapper()

val settingStore = SettingStore(fingerprintStore = fingerprintStore)
val gleanTelemetryStore = GleanTelemetryStore(telemetryWrapper, settingStore)

// These should appear in the same order as they do in `injectContext` in
// `LockwiseApplication` and `initializeService` in `LockwiseAutofillService`.
settingStore.injectContext(context)
gleanTelemetryStore.injectContext(context)

assertEquals(
"uploadEnabled, initialize",
telemetryWrapper.initializationOrder.joinToString(", ")
)
assertEquals(enabled, telemetryWrapper.uploadEnabled)
}

testWithPref(false)
testWithPref(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class TelemetryStoreTest : DisposingTest() {
val uploadObserver = createTestObserver<Int>()
wrapper.eventsSubject.subscribe(eventsObserver)
wrapper.uploadSubject.subscribe(uploadObserver)
subject.register()

var action: TelemetryAction = LifecycleAction.Foreground
dispatcher.dispatch(action)
Expand Down
18 changes: 15 additions & 3 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ _Last Updated: Feb 4, 2019_
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->

- [Analysis](#analysis)
- [Collection](#collection)
- [Collection](#collection-legacy)
- [List of Implemented Events](#list-of-implemented-events)
- [Mozilla Glean SDK](#mozilla-glean-sdk)
- [Adjust SDK](#adjust-sdk)
- [References](#references)

Expand Down Expand Up @@ -50,7 +51,7 @@ In service to validating the above hypothesis, we plan on answering these specif

In addition to answering the above questions that directly concern actions in the app, we will also analyze telemetry emitted from the password manager that exists in the the Firefox desktop browser. These analyses will primarily examine whether users of Lockwise start active curation of their credentials in the desktop browser (Lockwise users will not be able to edit credentials directly from the app).

## Collection
## Collection (legacy)

*Note: There is currently a new Mozilla mobile telemetry SDK under development, however it will not ship prior to the Lockwise for Android app. Once the new SDK ships we will evaluate whether or not to tear out the old implementation and replace it with the new SDK.*

Expand Down Expand Up @@ -179,14 +180,25 @@ https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/c
* `value`: null
* `extras`: null

## Mozilla Glean SDK

Lockwise for Android uses the [Glean SDK](https://mozilla.github.io/glean/book/index.html) to collect telemetry. The Glean SDK provides a handful of [pings and metrics out of the box](https://mozilla.github.io/glean/book/user/pings/index.html). The data review for using the Glean SDK is available at [this link](https://github.com/mozilla-lockwise/lockwise-android/pull/1085#issuecomment-558767676).

Lockwise for Android also uses the following Glean-enabled components of [Android Components](https://github.com/mozilla-mobile/android-components/), which are sending telemetry:

|Name|Description|Collected metrics|Data review|
|---|---|---|---|
|[Firefox accounts](https://github.com/mozilla-mobile/android-components/tree/master/components/service/firefox-accounts)|A library for integrating with Firefox Accounts.| [docs](https://github.com/mozilla-mobile/android-components/blob/master/components/support/sync-telemetry/docs/metrics.md)| [review](https://github.com/mozilla-lockwise/lockwise-android/pull/1085#issuecomment-558767676) |

## Adjust SDK

The app also includes a version of the [adjust SDK](https://github.com/adjust/android_sdk). Mozilla uses this software to keep track of the number of installations of the Lockwise app, as well the number of new Firefox Accounts registered through the app.

## References

[Library used to collect and send telemetry on Android](https://github.com/mozilla-mobile/android-components/blob/master/components/service/telemetry/README.md)
[Glean SDK repository, used to collect and send telemetry](https://github.com/mozilla/glean/)

[Legacy library used to collect and send telemetry on Android](https://github.com/mozilla-mobile/android-components/blob/master/components/service/telemetry/README.md)

[Description of the "Core" ping](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/core-ping.html)

Expand Down

0 comments on commit 6f9d36d

Please sign in to comment.