Bug 1845134 - Part 4: Update existing ui-icons to use the latest source from acorn...
[gecko.git] / services / common / uptake-telemetry.sys.mjs
blob9b456b137f9209b1b1a75262ce13b668aa16ad69
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   ClientID: "resource://gre/modules/ClientID.sys.mjs",
11 });
13 ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
14   return Components.Constructor(
15     "@mozilla.org/security/hash;1",
16     "nsICryptoHash",
17     "initWithString"
18   );
19 });
21 XPCOMUtils.defineLazyPreferenceGetter(
22   lazy,
23   "gSampleRate",
24   "services.common.uptake.sampleRate"
27 // Telemetry events id (see Events.yaml).
28 const TELEMETRY_EVENTS_ID = "uptake.remotecontent.result";
30 /**
31  * A wrapper around certain low-level operations that can be substituted for testing.
32  */
33 export var Policy = {
34   _clientIDHash: null,
36   getClientID() {
37     return lazy.ClientID.getClientID();
38   },
40   /**
41    * Compute an integer in the range [0, 100) using a hash of the
42    * client ID.
43    *
44    * This is useful for sampling clients when trying to report
45    * telemetry only for a sample of clients.
46    */
47   async getClientIDHash() {
48     if (this._clientIDHash === null) {
49       this._clientIDHash = this._doComputeClientIDHash();
50     }
51     return this._clientIDHash;
52   },
54   async _doComputeClientIDHash() {
55     const clientID = await this.getClientID();
56     let byteArr = new TextEncoder().encode(clientID);
57     let hash = new lazy.CryptoHash("sha256");
58     hash.update(byteArr, byteArr.length);
59     const bytes = hash.finish(false);
60     let rem = 0;
61     for (let i = 0, len = bytes.length; i < len; i++) {
62       rem = ((rem << 8) + (bytes[i].charCodeAt(0) & 0xff)) % 100;
63     }
64     return rem;
65   },
67   getChannel() {
68     return AppConstants.MOZ_UPDATE_CHANNEL;
69   },
72 /**
73  * A Telemetry helper to report uptake of remote content.
74  */
75 export class UptakeTelemetry {
76   /**
77    * Supported uptake statuses:
78    *
79    * - `UP_TO_DATE`: Local content was already up-to-date with remote content.
80    * - `SUCCESS`: Local content was updated successfully.
81    * - `BACKOFF`: Remote server asked clients to backoff.
82    * - `PARSE_ERROR`: Parsing server response has failed.
83    * - `CONTENT_ERROR`: Server response has unexpected content.
84    * - `PREF_DISABLED`: Update is disabled in user preferences.
85    * - `SIGNATURE_ERROR`: Signature verification after diff-based sync has failed.
86    * - `SIGNATURE_RETRY_ERROR`: Signature verification after full fetch has failed.
87    * - `CONFLICT_ERROR`: Some remote changes are in conflict with local changes.
88    * - `CORRUPTION_ERROR`: Error related to corrupted local data.
89    * - `SYNC_ERROR`: Synchronization of remote changes has failed.
90    * - `APPLY_ERROR`: Application of changes locally has failed.
91    * - `SERVER_ERROR`: Server failed to respond.
92    * - `CERTIFICATE_ERROR`: Server certificate verification has failed.
93    * - `DOWNLOAD_ERROR`: Data could not be fully retrieved.
94    * - `TIMEOUT_ERROR`: Server response has timed out.
95    * - `NETWORK_ERROR`: Communication with server has failed.
96    * - `NETWORK_OFFLINE_ERROR`: Network not available.
97    * - `SHUTDOWN_ERROR`: Error occuring during shutdown.
98    * - `UNKNOWN_ERROR`: Uncategorized error.
99    * - `CLEANUP_ERROR`: Clean-up of temporary files has failed.
100    * - `SYNC_BROKEN_ERROR`: Synchronization is broken.
101    * - `CUSTOM_1_ERROR`: Update source specific error #1.
102    * - `CUSTOM_2_ERROR`: Update source specific error #2.
103    * - `CUSTOM_3_ERROR`: Update source specific error #3.
104    * - `CUSTOM_4_ERROR`: Update source specific error #4.
105    * - `CUSTOM_5_ERROR`: Update source specific error #5.
106    *
107    * @type {Object}
108    */
109   static get STATUS() {
110     return {
111       UP_TO_DATE: "up_to_date",
112       SUCCESS: "success",
113       BACKOFF: "backoff",
114       PARSE_ERROR: "parse_error",
115       CONTENT_ERROR: "content_error",
116       PREF_DISABLED: "pref_disabled",
117       SIGNATURE_ERROR: "sign_error",
118       SIGNATURE_RETRY_ERROR: "sign_retry_error",
119       CONFLICT_ERROR: "conflict_error",
120       CORRUPTION_ERROR: "corruption_error",
121       SYNC_ERROR: "sync_error",
122       APPLY_ERROR: "apply_error",
123       SERVER_ERROR: "server_error",
124       CERTIFICATE_ERROR: "certificate_error",
125       DOWNLOAD_ERROR: "download_error",
126       TIMEOUT_ERROR: "timeout_error",
127       NETWORK_ERROR: "network_error",
128       NETWORK_OFFLINE_ERROR: "offline_error",
129       SHUTDOWN_ERROR: "shutdown_error",
130       UNKNOWN_ERROR: "unknown_error",
131       CLEANUP_ERROR: "cleanup_error",
132       SYNC_BROKEN_ERROR: "sync_broken_error",
133       CUSTOM_1_ERROR: "custom_1_error",
134       CUSTOM_2_ERROR: "custom_2_error",
135       CUSTOM_3_ERROR: "custom_3_error",
136       CUSTOM_4_ERROR: "custom_4_error",
137       CUSTOM_5_ERROR: "custom_5_error",
138     };
139   }
141   static get Policy() {
142     return Policy;
143   }
145   /**
146    * Reports the uptake status for the specified source.
147    *
148    * @param {string} component     the component reporting the uptake (eg. "normandy").
149    * @param {string} status        the uptake status (eg. "network_error")
150    * @param {Object} extra         extra values to report
151    * @param {string} extra.source  the update source (eg. "recipe-42").
152    * @param {string} extra.trigger what triggered the polling/fetching (eg. "broadcast", "timer").
153    * @param {int}    extra.age     age of pulled data in seconds
154    */
155   static async report(component, status, extra = {}) {
156     const { source } = extra;
158     if (!source) {
159       throw new Error("`source` value is mandatory.");
160     }
162     if (!Object.values(UptakeTelemetry.STATUS).includes(status)) {
163       throw new Error(`Unknown status '${status}'`);
164     }
166     // Report event for real-time monitoring. See Events.yaml for registration.
167     // Contrary to histograms, Telemetry Events are not enabled by default.
168     // Enable them on first call to `report()`.
169     if (!this._eventsEnabled) {
170       Services.telemetry.setEventRecordingEnabled(TELEMETRY_EVENTS_ID, true);
171       this._eventsEnabled = true;
172     }
174     const hash = await UptakeTelemetry.Policy.getClientIDHash();
175     const channel = UptakeTelemetry.Policy.getChannel();
176     const shouldSendEvent =
177       !["release", "esr"].includes(channel) || hash < lazy.gSampleRate;
178     if (shouldSendEvent) {
179       // The Event API requires `extra` values to be of type string. Force it!
180       const extraStr = Object.keys(extra).reduce((acc, k) => {
181         acc[k] = extra[k].toString();
182         return acc;
183       }, {});
184       Services.telemetry.recordEvent(
185         TELEMETRY_EVENTS_ID,
186         "uptake",
187         component,
188         status,
189         extraStr
190       );
191     }
192   }