|
| 1 | +# Adding a new metric type |
| 2 | + |
| 3 | +This document covers how to add new metric types to Glean.js. New metric types should only be implemented |
| 4 | +after they were proposed and approved through Glean's metric type request process. If Glean is missing a |
| 5 | +metric type you need, start by filing a request as described in the [process documentation](https://wiki.mozilla.org/Glean/Adding_or_changing_Glean_metric_types). |
| 6 | + |
| 7 | +## Glossary |
| 8 | + |
| 9 | +For the purposes of this document, when we say: |
| 10 | + |
| 11 | +- **"Metric Type"** we mean an object that represents one of the metrics a user defined in their |
| 12 | + `metric.yaml` file. This object holds all the metadata related to a user defined metric as |
| 13 | + well as exposes the testing and recording APIs of the metric type. Example: |
| 14 | + |
| 15 | +```ts |
| 16 | +export const enlightening = new StringMetricType({ |
| 17 | + category: "example", |
| 18 | + name: "enlightening", |
| 19 | + sendInPings: ["custom"], |
| 20 | + lifetime: "ping", |
| 21 | + disabled: false, |
| 22 | +}); |
| 23 | +``` |
| 24 | + |
| 25 | +- **"Metric"** a wrapper object around a concrete value to record for a metric type. Example: |
| 26 | + |
| 27 | +```ts |
| 28 | +// Under the hood, Glean.js will use the value passed to `set` to instantiate a new StringMetric. |
| 29 | +enlightening.set("a-ha"); |
| 30 | +``` |
| 31 | + |
| 32 | +## Implementation guide |
| 33 | + |
| 34 | +Glean.js' metric type code lives on the `glean/src/core/metrics/types` folder. Once your metric type |
| 35 | +request is approved, create a new file on that folder to accomodate your new metric type. |
| 36 | + |
| 37 | +### The `Metric` class |
| 38 | + |
| 39 | +Inside the file you just created, define a class that extends the |
| 40 | +[`Metric`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/index.ts#L20) |
| 41 | +abstract class. |
| 42 | + |
| 43 | +This class will be used for instantiating metrics of your new type. Whenever a user |
| 44 | +uses a metric type's recording APIs, a metric instance is created using the given value and |
| 45 | +that is what Glean.js will record. |
| 46 | + |
| 47 | +It expects two type arguments: |
| 48 | + |
| 49 | +- `InternalRepresentation`: the representation in which this metric will be stored in memory. |
| 50 | +- `PayloadRepresentation`: the representation in which this metric will be added to the ping payload. |
| 51 | + |
| 52 | +For most metrics, both representations will be same, but some metrics may need extra information |
| 53 | +on their internal representation in order to assist when deserializing for testing purposes |
| 54 | +(e.g. the [`DatetimeMetric`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/types/datetime.ts#L27-L156)). |
| 55 | + |
| 56 | +The `PayloadRepresentation` must match _exactly_ the representation of this metric on |
| 57 | +[Glean's ping payload schema](https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/schemas/glean/glean/glean.1.schema.json). |
| 58 | +However you define this representation will be the representation used by Glean.js' |
| 59 | +when building ping payloads. |
| 60 | + |
| 61 | +This subclass requires the you implement these two methods: |
| 62 | + |
| 63 | +- [`validate`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/index.ts#L67): |
| 64 | +Which validates that an `unknown` value is in the correct internal representation for the current metric. |
| 65 | +This method will be used to validate values retrieved from the metrics database. |
| 66 | + |
| 67 | +- [`payload`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/index.ts#L74): |
| 68 | +Which returns the metric in it's `PayloadRepresentation`. This method will be used for building |
| 69 | +ping payloads. |
| 70 | + |
| 71 | +Let's look at an example: |
| 72 | + |
| 73 | +```ts |
| 74 | +// The string metric will be stored as a string and included in the ping payload as a string, |
| 75 | +// so its internal and payload representation are of the same type. |
| 76 | +class StringMetric extends Metric<string, string> { |
| 77 | + constructor(v: unknown) { |
| 78 | + // Calling `super` will call `validate` to check that `v` is in the correct type. |
| 79 | + // In case it is not, this function will throw and no metric will be created. |
| 80 | + super(v); |
| 81 | + } |
| 82 | + |
| 83 | + validate(v: unknown): v is string { |
| 84 | + return typeof v === "string"; |
| 85 | + } |
| 86 | + |
| 87 | + payload(): number { |
| 88 | + return this._inner; |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Oce you have your `Metric` subclass, include it in Glean.js' |
| 94 | +[`METRIC_MAP`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/utils.ts#L17). |
| 95 | +This map will be used as a template for creating metric instances from the metrics database. |
| 96 | + |
| 97 | +### The `MetricType` class |
| 98 | + |
| 99 | +Now you are ready to implement the `MetricType` class for your new metric type. |
| 100 | +This class will hold all the metadata related to a specific user defined metric and |
| 101 | +expose the recording and testing APIs of your new metric type. |
| 102 | + |
| 103 | +This class extends the [`MetricType`](https://github.com/mozilla/glean.js/blob/main/glean/src/core/metrics/index.ts#L110) abstract class. Different from the `Metric` class, this class does not have |
| 104 | +any required methods to implement. Each metric type class will have a different API. |
| 105 | +This API's design should have been discussed and decided upon during the metric type request process. |
| 106 | + |
| 107 | +Still, metric type classes will always have at least one recording function and one testing function. |
| 108 | + |
| 109 | +> **Note** The `type` property on the `MetricType` subclass is a constant. It will be used |
| 110 | +> to determine in which section of the ping the recorded metrics for this type should be placed. |
| 111 | +> It's value is the name of the section for this metric type on the ping payload. |
| 112 | +> Make sure that, when you included your `Metric` class on the `METRIC_MAP` the property has the |
| 113 | +> same value as the `type` property on the corresponding `MetricType`. |
| 114 | +
|
| 115 | +#### Recording functions |
| 116 | + |
| 117 | +_Functions that call Glean.js' database and store concrete values of a metric type._ |
| 118 | + |
| 119 | +Database calls are all asynchronous, but Glean.js' external API must **never** return promises. |
| 120 | +Therefore, Glean.js has an internal dispatcher. Asynchronous tasks are dispatched and the dispatcher |
| 121 | +will guarantee that they are executed in order without the user having to worry about |
| 122 | +awaiting or callbacks. |
| 123 | + |
| 124 | +This is to say: any recording action must be wrapped in a `Glean.dispatcher.launch` block. |
| 125 | + |
| 126 | +Continuing on the String metric type example, |
| 127 | +let's look at how a simple string recording function will look like. |
| 128 | + |
| 129 | +```ts |
| 130 | +function set(value: string): void { |
| 131 | + Glean.dispatcher.launch(async () => { |
| 132 | + // !IMPORTANT! Always check whether or not metrics should be recorded before recording. |
| 133 | + // |
| 134 | + // Metrics must not be recorded in case: upload is disabled or the metric is expired. |
| 135 | + if (!this.shouldRecord()) { |
| 136 | + return; |
| 137 | + } |
| 138 | + |
| 139 | + await Glean.metricsDatabase.record(this, value); |
| 140 | + }); |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +#### Testing functions |
| 145 | + |
| 146 | +_Functions that allow users to check what was recorded for the current metric type instance._ |
| 147 | + |
| 148 | +Because all recording actions are dispatched, testing actions must also be dispatched so that they |
| 149 | +are guaranteed to run _after_ recording is finished. We cannot use the usual `Glean.dispatcher.launch` |
| 150 | +function in this case though, because we cannot await on our actions completion when we use it. |
| 151 | + |
| 152 | +Instead we will use the `Glean.dispatcher.testLaunch` API which let's us await on the launched function. |
| 153 | + |
| 154 | +Again on the String metric type example: |
| 155 | + |
| 156 | +```ts |
| 157 | +async function testGetValue(ping: string = this.sendInPings[0]): Promise<string | undefined> { |
| 158 | + let metric: string | undefined; |
| 159 | + await Glean.dispatcher.testLaunch(async () => { |
| 160 | + metric = await Glean.metricsDatabase.getMetric<string>(ping, this); |
| 161 | + }); |
| 162 | + return metric; |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +> **Note**: All testing functions must start with the prefix `test`. |
| 167 | +
|
| 168 | +## Testing |
| 169 | + |
| 170 | +Tests for metric type implementations live under the `glean/tests/core/metrics/types` folder. Create a new |
| 171 | +file with the same name as the one you created in `glean/src/core/metrics/types` to accomodate your |
| 172 | +metric type tests. |
| 173 | + |
| 174 | +Make sure your tests cover at least your metric types basic functionality: |
| 175 | + |
| 176 | +- The metric returns the correct value when it has no value; |
| 177 | +- The metric correctly reports errors; |
| 178 | +- The metric returns the correct value when it has value. |
| 179 | + |
| 180 | +## Documentation |
| 181 | + |
| 182 | +Glean.js' has linter rules that enforce [JSDoc](https://jsdoc.app/) strings on every public function. |
| 183 | + |
| 184 | +Moreover, once a new metric type is added to Glean.js, a new documentation page must be added to the user |
| 185 | +facing documentation on [the Glean book](https://mozilla.github.io/glean/book/index.html). |
| 186 | + |
| 187 | +Source code for the Glean book lives on the [`mozilla/glean`](https://github.com/mozilla/glean) repository. |
| 188 | + |
| 189 | +Once you are on that repository: |
| 190 | + |
| 191 | +- Add a new file for your new metric in `docs/user/user/metrics/`. |
| 192 | + Its contents should follow the form and content of the other examples in that folder. |
| 193 | +- Reference that file in `docs/user/SUMMARY.md` so it will be included in the build. |
| 194 | +- Follow the [Documentation Contribution Guide](https://mozilla.github.io/glean/dev/docs.html). |
| 195 | + |
| 196 | +## Other |
| 197 | + |
| 198 | +Even after your are done with all the above steps, you still need to prepare other parts of the Glean |
| 199 | +ecosystem in order for you to be done implementing your new metric type. |
| 200 | + |
| 201 | +### glean_parser |
| 202 | + |
| 203 | +New metric types need to be added to `glean_parser` so that they can be generated |
| 204 | +from users `.yaml` files. |
| 205 | + |
| 206 | +Please refer to the[`glean_parser` documentation](https://mozilla.github.io/glean_parser/contributing.html) |
| 207 | +on how to do that. |
| 208 | + |
| 209 | +### mozilla-pipeline-schemas |
| 210 | + |
| 211 | +New metrics types must also be added to the Glean schema on the [`mozilla-pipeline-schemas`](https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/schemas/glean/glean/glean.1.schema.json). |
| 212 | +This step makes the Glean pipeline aware of the new metric type, before it is completed all ping |
| 213 | +payloads containing the new metric type will be rejected as a schema error. |
| 214 | + |
| 215 | +Please refer to the [mozilla-pipeline-schemas documentation](https://github.com/mozilla-services/mozilla-pipeline-schemas#contributions) on how to do that. |
| 216 | + |
| 217 | +### The Glean SDK |
| 218 | + |
| 219 | +When adding a new metric type to Glean.js you may also want to add it to the Glean SDK. |
| 220 | + |
| 221 | +If that is the case, please refer to |
| 222 | +[the Glean SDK's developer documentation](https://mozilla.github.io/glean/dev/core/new-metric-type.html) |
| 223 | +on adding new metric types. |
| 224 | + |
| 225 | +> **Note**: This step is not mandatory. If a metric type is only implemented in Glean.js you may |
| 226 | +> start to use it in production given that all the other above steps were completed. |
0 commit comments