Skip to content

Commit

Permalink
1557048: Add reason codes to the metrics ping
Browse files Browse the repository at this point in the history
This adds support for sending reason codes along with pings.  The reason codes
are defined as an enumeration in the pings.yaml file, and only these values
are allowed on specific pings.

Additionally, this builds on that to add reason codes to the metrics ping.
  • Loading branch information
mdboom committed Feb 5, 2020
1 parent 98f3dd9 commit af91250
Show file tree
Hide file tree
Showing 53 changed files with 673 additions and 365 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
in the baseline ping ([`glean.baseline.locale`](https://github.com/mozilla/glean/blob/c261205d6e84d2ab39c50003a8ffc3bd2b763768/glean-core/metrics.yaml#L28-L42))
is redundant and will be removed by the end of the quarter.
* Drop the Glean handle and move state into glean-core ([#664](https://github.com/mozilla/glean/pull/664))
* If an experiment includes no `extra` fields, it will no longer include `{"extra": null}` in the JSON paylod.
* If an experiment includes no `extra` fields, it will no longer include `{"extra": null}` in the JSON payload.
* Support for ping `reason` codes was added.
* The metrics ping will now include `reason` codes that indicate why it was
submitted.
* The version of `glean_parser` has been upgraded to 1.17.2
* Android:
* Collections performed before initialization (preinit tasks) are now dispatched off
the main thread during initialization.
Expand Down
2 changes: 1 addition & 1 deletion docs/user/pings/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Optional fields are marked accordingly.
| `experiments` | Object | *Optional*. A dictionary of [active experiments](#the-experiments-object) |
| `start_time` | Datetime | The time of the start of collection of the data in the ping, in local time and with minute precision, including timezone information. |
| `end_time` | Datetime | The time of the end of collection of the data in the ping, in local time and with minute precision, including timezone information. This is also the time this ping was generated and is likely well before ping transmission time. |
| `reason` | String | The optional reason the ping was submitted. The specific set of values and their meanings are defined for each metric type in the `reasons` field in the `pings.yaml` file. |

All the metrics surviving application restarts (e.g. `seq`, ...) are removed once the application using the Glean SDK is uninstalled.

Expand Down Expand Up @@ -104,4 +105,3 @@ These docs refer to application 'background' state in several places.
This specifically means when the activity is no longer visible to the user, it has entered the Stopped state, and the system invokes the [`onStop()`](https://developer.android.com/reference/android/app/Activity.html#onStop()) callback.
This may occur, for example, when a newly launched activity covers the entire screen.
The system may also call `onStop()` when the activity has finished running, and is about to be terminated.

80 changes: 21 additions & 59 deletions docs/user/pings/metrics.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
# The `metrics` ping

## 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`](index.md#the-ping_info-section) section of the ping can be used to infer the length of this window.
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.
However, the metrics ping is only submitted while the application is actually running, so in practice, it may not meet the 4AM target very frequently.
Data in the [`ping_info`](index.md#the-ping_info-section) section of the ping can be used to infer the length of this window and the reason that triggered the ping to be submitted.
If the application crashes, unsent recorded metrics are sent along with the next `metrics` ping.

Additionally, it is undesirable to mix metric recording from different versions of the application. Therefore, if a version upgrade is detected, the `metrics` ping is collected immediately before further metrics from the new version are recorded.

> **Note:** As the `metrics` ping was specifically designed for mobile operating systems, it is not sent when using the Glean Python bindings.
## Scheduling
The desired behavior is to collect the ping at the first available opportunity after 4AM local time on a new calendar day.
The desired behavior is to collect the ping at the first available opportunity after 4AM local time on a new calendar day, but given constraints of the platform, it can only be submitted while the application is running.
This breaks down into three scenarios:

1. the application was just installed;
2. the application was just started (after a crash or a long inactivity period);
3. the application was open and the 4AM due time was hit.
2. the application was just upgraded (the version of the app is different from the last time the app was run);
3. the application was just started (after a crash or a long inactivity period);
4. the application was running at 4AM.

In the first case, since the application was just installed, if the due time for the current calendar day has passed, a `metrics` ping is immediately generated and scheduled for sending (reason code `overdue`). Otherwise, if the due time for the current calendar day has not passed, a ping collection is scheduled for that time (reason code `today`).

In the first case, since the application was just installed, if the due time for the current calendar day has passed, a `metrics` ping is immediately generated and scheduled for sending. Otherwise, if the due time for the current calendar day has not passed, a ping collection is scheduled for that time.
In the second case, if a version change is detected at startup, the metrics ping is immediately submitted so that metrics from one version are not aggregated with metrics from another version (reason code `upgrade`).

In the second case, if the `metrics` ping was already collected on the current calendar day, a new collection will be scheduled for the next calendar day, at 4AM.
If no collection happened yet, and the due time for the current calendar day has passed, a `metrics` ping is immediately generated and scheduled for sending.
In the third case, if the `metrics` ping was not already collected on the current calendar day, and it is before 4AM, a collection is scheduled for 4AM on the current calendar day (reason code `today`).
If it is after 4AM, a new collection is scheduled immediately (reason code `overdue`).
Lastly, if a ping was already collected on the current calendar day, the next one is scheduled for collecting at 4AM on the next calendar day (reason code `tomorrow`).

In the third case, similarly to the previous case, if the `metrics` ping was already collected on the current calendar day when we hit 4AM, then a new collection is scheduled for the next calendar day.
Otherwise, the `metrics` is immediately collected and scheduled for sending.
In the fourth and last case, the application is running during a scheduled ping collection time.
The next ping is scheduled for 4AM the next calendar day (reason code `reschedule`).

More [scheduling examples](#scheduling-examples) are included below.

Expand All @@ -35,53 +42,8 @@ Additionally, error metrics in the `glean.error` category are included in the `m
The `metrics` ping shall also include the common [`ping_info`](index.md#the-ping_info-section) and ['client_info'](index.md#the-client_info-section) sections.

### Querying ping contents
A quick note about querying ping contents (i.e. for [sql.telemetry.mozilla.org](https://sql.telemetry.mozilla.org)): Each metric in the metrics ping is organized by its metric type, and uses a namespace of 'glean.metrics'.
For instance, in order to select a String field called `test` you would use `metrics.string['glean.metrics.test']`.

### Example metrics ping

```json
{
"ping_info": {
"ping_type": "metrics",
"experiments": {
"third_party_library": {
"branch": "enabled"
}
},
"seq": 0,
"start_time": "2019-03-29T09:50-04:00",
"end_time": "2019-03-29T10:02-04:00"
},
"client_info": {
"telemetry_sdk_build": "0.49.0",
"first_run_date": "2019-03-29-04:00",
"os": "Android",
"android_sdk_version": "27",
"os_version": "8.1.0",
"device_manufacturer": "Google",
"device_model": "Android SDK built for x86",
"architecture": "x86",
"app_build": "1",
"app_display_version": "1.0",
"client_id": "35dab852-74db-43f4-8aa0-88884211e545"
},
"metrics": {
"counter": {
"sample_metrics.test": 1
},
"string": {
"basic.os": "Android"
},
"timespan": {
"test.test_timespan": {
"time_unit": "microsecond",
"value": 181908
}
}
}
}
```

Information about query ping contents is available in [Accessing Glean data](https://docs.telemetry.mozilla.org/concepts/glean/accessing_glean_data.html) in the Firefox data docs.

## Scheduling Examples

Expand Down
4 changes: 2 additions & 2 deletions glean-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let cfg = Configuration {
max_events: None,
};
let mut glean = Glean::new(cfg).unwrap();
let ping = PingType::new("sample", true);
let ping = PingType::new("sample", true, true, vec![]);
glean.register_ping_type(&ping);

let call_counter: CounterMetric = CounterMetric::new(CommonMetricData {
Expand All @@ -44,7 +44,7 @@ let call_counter: CounterMetric = CounterMetric::new(CommonMetricData {

call_counter.add(&glean, 1);

glean.submit_ping(&ping).unwrap();
glean.submit_ping(&ping, None).unwrap();
```

## License
Expand Down
2 changes: 1 addition & 1 deletion glean-core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ apply plugin: 'jacoco'
* Everytime this hash is changed it should also be changed in
* glean-core/python/tests/conftest.py
*/
String GLEAN_PING_SCHEMA_GIT_HASH = "f2a7ce4"
String GLEAN_PING_SCHEMA_GIT_HASH = "ee08e7c"
String GLEAN_PING_SCHEMA_URL = "https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas/$GLEAN_PING_SCHEMA_GIT_HASH/schemas/glean/glean/glean.1.schema.json"

// Set configuration for the glean_parser
Expand Down
49 changes: 25 additions & 24 deletions glean-core/android/src/main/java/mozilla/telemetry/glean/Glean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import mozilla.telemetry.glean.GleanMetrics.GleanBaseline
import mozilla.telemetry.glean.GleanMetrics.GleanInternalMetrics
import mozilla.telemetry.glean.GleanMetrics.Pings
import mozilla.telemetry.glean.net.BaseUploader
import mozilla.telemetry.glean.private.PingType
import mozilla.telemetry.glean.private.PingTypeBase
import mozilla.telemetry.glean.private.RecordedExperimentData
import mozilla.telemetry.glean.scheduler.GleanLifecycleObserver
import mozilla.telemetry.glean.scheduler.DeletionPingUploadWorker
Expand Down Expand Up @@ -78,7 +78,7 @@ open class GleanInternalAPI internal constructor () {
internal lateinit var metricsPingScheduler: MetricsPingScheduler

// Keep track of ping types that have been registered before Glean is initialized.
private val pingTypeQueue: MutableSet<PingType> = mutableSetOf()
private val pingTypeQueue: MutableSet<PingTypeBase> = mutableSetOf()

// This is used to cache the process state and is used by the function `isMainProcess()`
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
Expand Down Expand Up @@ -468,19 +468,20 @@ open class GleanInternalAPI internal constructor () {
* Collect a ping and return a string
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
internal fun testCollect(ping: PingType): String? {
internal fun testCollect(ping: PingTypeBase): String? {
return LibGleanFFI.INSTANCE.glean_ping_collect(ping.handle)?.getAndConsumeRustString()
}

/**
* Handle the background event and send the appropriate pings.
*/
internal fun handleBackgroundEvent() {
submitPings(listOf(Pings.baseline, Pings.events))
Pings.baseline.submit()
Pings.events.submit()
}

/**
* Collect and submit a list of pings for eventual upload.
* Collect and submit a ping for eventual upload.
*
* The ping content is assembled as soon as possible, but upload is not
* guaranteed to happen immediately, as that depends on the upload
Expand All @@ -489,18 +490,18 @@ open class GleanInternalAPI internal constructor () {
* If the ping currently contains no content, it will not be assembled and
* queued for sending.
*
* @param pings List of pings to submit.
* @param ping Ping to submit.
* @param reason The reason the ping is being submitted.
* @return The async [Job] performing the work of assembling the ping
*/
internal fun submitPings(pings: List<PingType>): Job? {
val pingNames = pings.map { it.name }
return submitPingsByName(pingNames)
internal fun submitPing(ping: PingTypeBase, reason: String? = null): Job? {
return submitPingByName(ping.name, reason)
}

/**
* Collect and submit a list of pings for eventual upload by name.
* Collect and submit a ping for eventual upload by name.
*
* Each ping will be looked up in the known instances of [PingType]. If the
* The 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 as soon as possible, but upload is not
Expand All @@ -511,18 +512,19 @@ open class GleanInternalAPI internal constructor () {
* queued for sending, unless explicitly specified otherwise in the registry
* file.
*
* @param pingNames List of ping names to submit.
* @param pingName Name of the ping to submit.
* @param reason The reason the ping is being submitted.
* @return The async [Job] performing the work of assembling the ping
*/
@Suppress("EXPERIMENTAL_API_USAGE")
internal fun submitPingsByName(pingNames: List<String>) = Dispatchers.API.launch {
submitPingsByNameSync(pingNames)
internal fun submitPingByName(pingName: String, reason: String? = null) = Dispatchers.API.launch {
submitPingByNameSync(pingName, reason)
}

/**
* Collect and submit a list of pings for eventual upload by name, synchronously.
* Collect and submit a ping (by its name) for eventual upload, synchronously.
*
* Each ping will be looked up in the known instances of [PingType]. If the
* The 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 as soon as possible, but upload is not
Expand All @@ -533,9 +535,10 @@ open class GleanInternalAPI internal constructor () {
* queued for sending, unless explicitly specified otherwise in the registry
* file.
*
* @param pingNames List of ping names to submit.
* @param pingName Name of the ping to submit.
* @param reason The reason the ping is being submitted.
*/
internal fun submitPingsByNameSync(pingNames: List<String>) {
internal fun submitPingByNameSync(pingName: String, reason: String? = null) {
if (!isInitialized()) {
Log.e(LOG_TAG, "Glean must be initialized before submitting pings.")
return
Expand All @@ -546,11 +549,9 @@ open class GleanInternalAPI internal constructor () {
return
}

val pingArray = StringArray(pingNames.toTypedArray(), "utf-8")
val pingArrayLen = pingNames.size
val submittedPing = LibGleanFFI.INSTANCE.glean_submit_pings_by_name(
pingArray,
pingArrayLen
val submittedPing = LibGleanFFI.INSTANCE.glean_submit_ping_by_name(
pingName,
reason
).toBoolean()

if (submittedPing) {
Expand Down Expand Up @@ -640,7 +641,7 @@ open class GleanInternalAPI internal constructor () {
* Register a [PingType] in the registry associated with this [Glean] object.
*/
@Synchronized
internal fun registerPingType(pingType: PingType) {
internal fun registerPingType(pingType: PingTypeBase) {
if (this.isInitialized()) {
LibGleanFFI.INSTANCE.glean_register_ping_type(
pingType.handle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class GleanDebugActivity : Activity() {
Glean.configuration = debugConfig

intent.getStringExtra(SEND_PING_EXTRA_KEY)?.let {
Glean.submitPingsByName(listOf(it))
Glean.submitPingByName(it)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,57 @@

package mozilla.telemetry.glean.private

import com.sun.jna.StringArray
import mozilla.telemetry.glean.Glean
import mozilla.telemetry.glean.rust.LibGleanFFI
import mozilla.telemetry.glean.rust.toByte

/**
* An enum with no values for convenient use as the default set of reason codes.
*/
@Suppress("EmptyClassBlock")
enum class NoReasonCodes(
/**
* @suppress
*/
val value: Int
) {
// deliberately empty
}

/**
* The base class of all PingTypes with just enough to track their registration, so
* we can create a heterogeneous collection of ping types.
*/
open class PingTypeBase(
internal val name: String
) {
internal var handle: Long = 0L
}

/**
* 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.
*/
class PingType(
internal val name: String,
class PingType<ReasonCodesEnum : Enum<ReasonCodesEnum>> (
name: String,
includeClientId: Boolean,
sendIfEmpty: Boolean
) {
internal var handle: Long
sendIfEmpty: Boolean,
val reasonCodes: List<String>
) : PingTypeBase(name) {

init {
val ffiReasonList = StringArray(reasonCodes.toTypedArray(), "utf-8")
val ffiReasonListLen = reasonCodes.size
this.handle = LibGleanFFI.INSTANCE.glean_new_ping_type(
name = name,
include_client_id = includeClientId.toByte(),
send_if_empty = sendIfEmpty.toByte()
send_if_empty = sendIfEmpty.toByte(),
reason_codes = ffiReasonList,
reason_codes_len = ffiReasonListLen
)
Glean.registerPingType(this)
}
Expand All @@ -48,9 +76,12 @@ class PingType(
* There are no guarantees that this will happen immediately.
*
* If the ping currently contains no content, it will not be queued.
*
* @param reason The reason the ping is being submitted.
*/
fun submit() {
Glean.submitPings(listOf(this))
fun submit(reason: ReasonCodesEnum? = null) {
val reasonString = reason?.let { this.reasonCodes[it.ordinal] }
Glean.submitPing(this, reasonString)
}

/**
Expand Down
Loading

0 comments on commit af91250

Please sign in to comment.