Skip to content
This repository was archived by the owner on Nov 1, 2022. It is now read-only.

Commit e4d377a

Browse files
MozLandomhammond
andcommitted
Merge #7618
7618: Record send-tab telemetry for Fenix. r=grigoryk a=mhammond application-services needs to be closely involved in creating the data to be recorded in telemetry - however, we are trying to avoid exposing this telemetry directly to the android-components "concepts" or "features" - for example, the part of android-components that sends a tab ideally wouldn't have to deal with any interface changes just because telemetry is being sent or changed - otherwise new/changed telemetry is always a "breaking change" In the future, we expect the rust components to interact directly with Glean, and consumers like android-components wouldn't need to get involved at all. However, until then... The idea with this PR is that there's one new public account function, `processPendingTelemetry()`, which should be called whenever something is done which might record telemetry. However, it doesn't need to know what is actually recorded - that knowledge is only in the telemetry code - the code that is tightly bound to glean. This hasn't been tested yet - but it builds :) I'm soliciting feedback on the general shape of this before I invest too much more in tests etc. I also considered trying to do with Observers(), but that didn't seem any easier. I'm obviously open to alternative approaches! See also mozilla/application-services#3308 Co-authored-by: Mark Hammond <[email protected]>
2 parents fb5d419 + 427c720 commit e4d377a

File tree

10 files changed

+206
-7
lines changed

10 files changed

+206
-7
lines changed

buildSrc/src/main/java/Dependencies.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ object Versions {
2727
const val disklrucache = "2.0.2"
2828
const val leakcanary = "2.4"
2929

30-
const val mozilla_appservices = "61.0.10"
30+
const val mozilla_appservices = "61.0.12"
3131

3232
const val mozilla_glean = "32.1.0"
3333

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import mozilla.components.concept.sync.AuthFlowUrl
1717
import mozilla.components.concept.sync.DeviceConstellation
1818
import mozilla.components.concept.sync.OAuthAccount
1919
import mozilla.components.concept.sync.StatePersistenceCallback
20+
import mozilla.components.support.base.crash.CrashReporting
2021
import mozilla.components.support.base.log.logger.Logger
2122
import org.json.JSONObject
2223

@@ -27,7 +28,8 @@ typealias PersistCallback = mozilla.appservices.fxaclient.FirefoxAccount.Persist
2728
*/
2829
@Suppress("TooManyFunctions")
2930
class FirefoxAccount internal constructor(
30-
private val inner: InternalFxAcct
31+
private val inner: InternalFxAcct,
32+
private val crashReporter: CrashReporting? = null
3133
) : OAuthAccount {
3234
private val job = SupervisorJob()
3335
private val scope = CoroutineScope(Dispatchers.IO) + job
@@ -65,7 +67,7 @@ class FirefoxAccount internal constructor(
6567
}
6668

6769
private var persistCallback = WrappingPersistenceCallback()
68-
private val deviceConstellation = FxaDeviceConstellation(inner, scope)
70+
private val deviceConstellation = FxaDeviceConstellation(inner, scope, crashReporter)
6971

7072
init {
7173
inner.registerPersistCallback(persistCallback)
@@ -82,13 +84,16 @@ class FirefoxAccount internal constructor(
8284
* is saved in a secure location, as it can contain Sync Keys and
8385
* OAuth tokens.
8486
*
87+
* @param crashReporter A crash reporter instance.
88+
*
8589
* Note that it is not necessary to `close` the Config if this constructor is used (however
8690
* doing so will not cause an error).
8791
*/
8892
constructor(
8993
config: ServerConfig,
90-
persistCallback: PersistCallback? = null
91-
) : this(InternalFxAcct(config, persistCallback))
94+
persistCallback: PersistCallback? = null,
95+
crashReporter: CrashReporting? = null
96+
) : this(InternalFxAcct(config, persistCallback), crashReporter)
9297

9398
override fun close() {
9499
job.cancel()

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@ import mozilla.components.concept.sync.AccountEventsObserver
2222
import mozilla.components.concept.sync.DeviceCommandOutgoing
2323
import mozilla.components.concept.sync.DevicePushSubscription
2424
import mozilla.components.concept.sync.DeviceType
25+
import mozilla.components.support.base.crash.CrashReporting
2526
import mozilla.components.support.base.log.logger.Logger
2627
import mozilla.components.support.base.observer.Observable
2728
import mozilla.components.support.base.observer.ObserverRegistry
29+
import mozilla.components.support.sync.telemetry.SyncTelemetry
2830

2931
/**
3032
* Provides an implementation of [DeviceConstellation] backed by a [FirefoxAccount].
3133
*/
3234
@SuppressWarnings("TooManyFunctions")
3335
class FxaDeviceConstellation(
3436
private val account: FirefoxAccount,
35-
private val scope: CoroutineScope
37+
private val scope: CoroutineScope,
38+
private val crashReporter: CrashReporting? = null
3639
) : DeviceConstellation, Observable<AccountEventsObserver> by ObserverRegistry() {
3740
private val logger = Logger("FxaDeviceConstellation")
3841

@@ -111,6 +114,7 @@ class FxaDeviceConstellation(
111114
when (outgoingCommand) {
112115
is DeviceCommandOutgoing.SendTab -> {
113116
account.sendSingleTab(targetDeviceId, outgoingCommand.title, outgoingCommand.url)
117+
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
114118
}
115119
else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
116120
}
@@ -130,6 +134,7 @@ class FxaDeviceConstellation(
130134
false
131135
} else {
132136
processEvents(events)
137+
SyncTelemetry.processFxaTelemetry(account.gatherTelemetry(), crashReporter)
133138
true
134139
}
135140
}

components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ open class FxaAccountManager(
10151015

10161016
@VisibleForTesting
10171017
open fun createAccount(config: ServerConfig): OAuthAccount {
1018-
return FirefoxAccount(config)
1018+
return FirefoxAccount(config, null, crashReporter)
10191019
}
10201020

10211021
@VisibleForTesting

components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ class FxaDeviceConstellationTest {
167167

168168
@Test
169169
fun `send command to device`() = runBlocking(coroutinesTestRule.testDispatcher) {
170+
`when`(account.gatherTelemetry()).thenReturn("{}")
170171
assertTrue(constellation.sendCommandToDeviceAsync(
171172
"targetID", DeviceCommandOutgoing.SendTab("Mozilla", "https://www.mozilla.org")
172173
).await())
@@ -236,6 +237,7 @@ class FxaDeviceConstellationTest {
236237
@ExperimentalCoroutinesApi
237238
fun `polling for commands triggers observers`() = runBlocking(coroutinesTestRule.testDispatcher) {
238239
// No commands, no observers.
240+
`when`(account.gatherTelemetry()).thenReturn("{}")
239241
`when`(account.pollDeviceCommands()).thenReturn(emptyArray())
240242
assertTrue(constellation.pollForCommandsAsync().await())
241243

components/support/sync-telemetry/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies {
4444

4545
implementation project(':service-glean')
4646
implementation Dependencies.kotlin_stdlib
47+
implementation Dependencies.kotlin_coroutines
4748
implementation Dependencies.mozilla_sync15
4849

4950
testImplementation Dependencies.testing_robolectric

components/support/sync-telemetry/docs/metrics.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ The following metrics are added to the ping:
109109

110110
| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) |
111111
| --- | --- | --- | --- | --- | --- | --- |
112+
| fxa_tab.received |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Recorded when a tab is received. Also sent by desktop - see also the docs at https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/sync-ping.html |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1652902)|<ul><li>flow_id: A guid, unique for the URL being sent but common for all target devices. </li><li>reason: The reason we discovered the tab. Will be one of 'push', 'push-missed' or 'poll'. </li><li>stream_id: A guid, unique for both the URL being sent and the target device. </li></ul>|never | |
113+
| fxa_tab.sent |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Recorded when a tab is sent. Also sent by desktop - see also the docs at https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/sync-ping.html |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1652902)|<ul><li>flow_id: A guid, unique for the URL being sent but common for all target devices. The value is randomly generated so can not identify details about the user or tab. </li><li>stream_id: A guid, unique for both the URL being sent and the target device. The value is randomly generated so can not identify details about the user or tab. </li></ul>|never | |
112114
| sync.failure_reason |[labeled_string](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html) |Records a global sync failure: either due to an authentication error, unexpected exception, or other error that caused the sync to fail. Error strings are truncated and sanitized to omit PII, like URLs and file system paths. |[1](https://github.com/mozilla-mobile/android-components/pull/3092)|<ul><li>other</li><li>unexpected</li><li>auth</li></ul>|never |2 |
113115
| sync.sync_uuid |[uuid](https://mozilla.github.io/glean/book/user/metrics/uuid.html) |Unique identifier for this sync, used to correlate together individual pings for data types that were synchronized together (history, bookmarks, logins). If a data type is synchronized by itself via the legacy 'sync' API (as opposed to the Sync Manager), then this field will not be set on the corresponding ping. |[1](https://github.com/mozilla-mobile/android-components/pull/5386#pullrequestreview-392363687)||never |1 |
114116

components/support/sync-telemetry/metrics.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,58 @@ bookmarks_sync:
491491
492492
expires: never
493493
lifetime: ping
494+
fxa_tab:
495+
sent:
496+
type: event
497+
description: >
498+
Recorded when a tab is sent. Also sent by desktop - see also the docs at
499+
https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/sync-ping.html
500+
send_in_pings:
501+
- sync
502+
extra_keys:
503+
flow_id:
504+
description: >
505+
A guid, unique for the URL being sent but common for all target
506+
devices. The value is randomly generated so can not identify details
507+
about the user or tab.
508+
stream_id:
509+
description: >
510+
A guid, unique for both the URL being sent and the target device. The
511+
value is randomly generated so can not identify details about the
512+
user or tab.
513+
bugs:
514+
- https://github.com/mozilla/application-services/pull/3308
515+
- https://github.com/mozilla-mobile/android-components/pull/7618
516+
data_reviews:
517+
- https://bugzilla.mozilla.org/show_bug.cgi?id=1652902
518+
notification_emails:
519+
520+
expires: never
521+
received:
522+
type: event
523+
description: >
524+
Recorded when a tab is received. Also sent by desktop - see also the
525+
docs at
526+
https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/sync-ping.html
527+
send_in_pings:
528+
- sync
529+
extra_keys:
530+
flow_id:
531+
description: >
532+
A guid, unique for the URL being sent but common for all target
533+
devices.
534+
stream_id:
535+
description: >
536+
A guid, unique for both the URL being sent and the target device.
537+
reason:
538+
description: >
539+
The reason we discovered the tab. Will be one of 'push', 'push-missed'
540+
or 'poll'.
541+
bugs:
542+
- https://github.com/mozilla/application-services/pull/3308
543+
- https://github.com/mozilla-mobile/android-components/pull/7618
544+
data_reviews:
545+
- https://bugzilla.mozilla.org/show_bug.cgi?id=1652902
546+
notification_emails:
547+
548+
expires: never

components/support/sync-telemetry/src/main/java/mozilla/components/support/sync/telemetry/SyncTelemetry.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,33 @@
44

55
package mozilla.components.support.sync.telemetry
66

7+
import org.json.JSONException
8+
import org.json.JSONObject
79
import mozilla.appservices.sync15.EngineInfo
810
import mozilla.appservices.sync15.FailureName
911
import mozilla.appservices.sync15.FailureReason
1012
import mozilla.appservices.sync15.SyncTelemetryPing
1113
import mozilla.components.service.glean.private.LabeledMetricType
1214
import mozilla.components.service.glean.private.StringMetricType
15+
import mozilla.components.support.base.crash.CrashReporting
1316
import mozilla.components.support.base.log.logger.Logger
1417
import mozilla.components.support.sync.telemetry.GleanMetrics.BookmarksSync
18+
import mozilla.components.support.sync.telemetry.GleanMetrics.FxaTab
1519
import mozilla.components.support.sync.telemetry.GleanMetrics.HistorySync
1620
import mozilla.components.support.sync.telemetry.GleanMetrics.LoginsSync
1721
import mozilla.components.support.sync.telemetry.GleanMetrics.Sync
1822
import mozilla.components.support.sync.telemetry.GleanMetrics.Pings
1923

2024
const val MAX_FAILURE_REASON_LENGTH = 100
2125

26+
// The exceptions we report to the crash reporter, but otherwise don't escape this module.
27+
internal sealed class InvalidTelemetryException(cause: Exception) : Exception(cause) {
28+
// The top-level data passed in is invalid.
29+
class InvalidData(cause: JSONException) : InvalidTelemetryException(cause)
30+
// The sent or received tabs data is invalid.
31+
class InvalidEvents(cause: JSONException) : InvalidTelemetryException(cause)
32+
}
33+
2234
/**
2335
* Contains functionality necessary to process instances of [SyncTelemetryPing].
2436
*/
@@ -284,4 +296,45 @@ object SyncTelemetry {
284296
val message = reason.message ?: "Unexpected error: ${reason.code}"
285297
metric.set(message.take(MAX_FAILURE_REASON_LENGTH))
286298
}
299+
300+
fun processFxaTelemetry(jsonStr: String, crashReporter: CrashReporting? = null) {
301+
val json = try {
302+
JSONObject(jsonStr)
303+
} catch (e: JSONException) {
304+
crashReporter?.submitCaughtException(InvalidTelemetryException.InvalidData(e))
305+
logger.error("Invalid JSON in FxA telemetry", e)
306+
return
307+
}
308+
try {
309+
val sent = json.getJSONArray("commands_sent")
310+
for (i in 0..sent.length() - 1) {
311+
val one = sent.getJSONObject(i)
312+
val extras = mapOf(
313+
FxaTab.sentKeys.flowId to one.getString("flow_id"),
314+
FxaTab.sentKeys.streamId to one.getString("stream_id")
315+
)
316+
FxaTab.sent.record(extras)
317+
}
318+
logger.info("Reported telemetry for ${sent.length()} sent commands")
319+
} catch (e: JSONException) {
320+
crashReporter?.submitCaughtException(InvalidTelemetryException.InvalidEvents(e))
321+
logger.error("Failed to report sent commands", e)
322+
}
323+
try {
324+
val recd = json.getJSONArray("commands_received")
325+
for (i in 0..recd.length() - 1) {
326+
val one = recd.getJSONObject(i)
327+
val extras = mapOf(
328+
FxaTab.receivedKeys.flowId to one.getString("flow_id"),
329+
FxaTab.receivedKeys.streamId to one.getString("stream_id"),
330+
FxaTab.receivedKeys.reason to one.getString("reason")
331+
)
332+
FxaTab.received.record(extras)
333+
}
334+
logger.info("Reported telemetry for ${recd.length()} received commands")
335+
} catch (e: JSONException) {
336+
crashReporter?.submitCaughtException(InvalidTelemetryException.InvalidEvents(e))
337+
logger.error("Failed to report received commands", e)
338+
}
339+
}
287340
}

components/support/sync-telemetry/src/test/java/mozilla/components/support/sync/telemetry/SyncTelemetryTest.kt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ import mozilla.appservices.sync15.SyncInfo
1515
import mozilla.appservices.sync15.SyncTelemetryPing
1616
import mozilla.appservices.sync15.ValidationInfo
1717
import mozilla.components.service.glean.testing.GleanTestRule
18+
import mozilla.components.support.base.crash.CrashReporting
1819
import mozilla.components.support.sync.telemetry.GleanMetrics.BookmarksSync
20+
import mozilla.components.support.sync.telemetry.GleanMetrics.FxaTab
1921
import mozilla.components.support.sync.telemetry.GleanMetrics.LoginsSync
2022
import mozilla.components.support.sync.telemetry.GleanMetrics.HistorySync
2123
import mozilla.components.support.sync.telemetry.GleanMetrics.Pings
2224
import mozilla.components.support.sync.telemetry.GleanMetrics.Sync
25+
import mozilla.components.support.test.any
26+
import mozilla.components.support.test.mock
2327
import mozilla.components.support.test.robolectric.testContext
28+
import org.json.JSONException
2429
import org.junit.Assert.assertEquals
2530
import org.junit.Assert.assertTrue
2631
import org.junit.Assert.assertFalse
@@ -30,6 +35,8 @@ import org.junit.Before
3035
import org.junit.Rule
3136
import org.junit.Test
3237
import org.junit.runner.RunWith
38+
import org.mockito.Mockito.verify
39+
import org.mockito.Mockito.times
3340
import java.util.Date
3441
import java.util.UUID
3542

@@ -1116,6 +1123,75 @@ class SyncTelemetryTest {
11161123
)
11171124
}
11181125

1126+
@Test
1127+
fun `checks sent tab telemetry records what it should`() {
1128+
val json = """
1129+
{
1130+
"commands_sent":[{
1131+
"flow_id":"test-flow-id",
1132+
"stream_id":"test-stream-id"
1133+
}],
1134+
"commands_received":[]
1135+
}
1136+
"""
1137+
SyncTelemetry.processFxaTelemetry(json)
1138+
assertTrue(FxaTab.sent.testHasValue())
1139+
val events = FxaTab.sent.testGetValue()
1140+
assertEquals(1, events.size)
1141+
assertEquals("test-flow-id", events.elementAt(0).extra!!["flow_id"])
1142+
assertEquals("test-stream-id", events.elementAt(0).extra!!["stream_id"])
1143+
1144+
assertFalse(FxaTab.received.testHasValue())
1145+
}
1146+
1147+
@Test
1148+
fun `checks received tab telemetry records what it should`() {
1149+
val json = """
1150+
{
1151+
"commands_received":[{
1152+
"flow_id":"test-flow-id",
1153+
"stream_id":"test-stream-id",
1154+
"reason":"test-reason"
1155+
}]
1156+
}
1157+
"""
1158+
SyncTelemetry.processFxaTelemetry(json)
1159+
assertTrue(FxaTab.received.testHasValue())
1160+
val events = FxaTab.received.testGetValue()
1161+
assertEquals(1, events.size)
1162+
assertEquals("test-flow-id", events.elementAt(0).extra!!["flow_id"])
1163+
assertEquals("test-stream-id", events.elementAt(0).extra!!["stream_id"])
1164+
assertEquals("test-reason", events.elementAt(0).extra!!["reason"])
1165+
1166+
assertFalse(FxaTab.sent.testHasValue())
1167+
}
1168+
1169+
@Test
1170+
fun `checks invalid tab telemetry doesn't record anything and doesn't crash`() {
1171+
val crashReporter: CrashReporting = mock()
1172+
// commands_sent is missing the stream_id, command_received is missing a reason
1173+
val json = """
1174+
{
1175+
"commands_sent":[{
1176+
"flow_id":"test-flow-id"
1177+
}],
1178+
"commands_received":[{
1179+
"flow_id":"test-flow-id",
1180+
"stream_id":"test-stream-id"
1181+
}]
1182+
}
1183+
"""
1184+
SyncTelemetry.processFxaTelemetry(json, crashReporter)
1185+
// one exception for each of 'send' and 'received'
1186+
verify(crashReporter, times(2)).submitCaughtException(any<JSONException>())
1187+
// completely invalid json
1188+
SyncTelemetry.processFxaTelemetry(""" foo bar """, crashReporter)
1189+
assertFalse(FxaTab.sent.testHasValue())
1190+
assertFalse(FxaTab.received.testHasValue())
1191+
// One more exception, making it the 3rd time
1192+
verify(crashReporter, times(3)).submitCaughtException(any<JSONException>())
1193+
}
1194+
11191195
private fun MutableMap<String, Int>.incrementForKey(key: String) {
11201196
this[key] = 1 + this.getOrElse(key, { 0 })
11211197
}

0 commit comments

Comments
 (0)