Bug 1870926 [wpt PR 43734] - Remove experimental ::details-summary pseudo-element...
[gecko.git] / toolkit / components / extensions / ExtensionTelemetry.sys.mjs
blob06137b9a23fa2fde8d9c46b283090224bbd2f566
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
9 const { DefaultWeakMap } = ExtensionUtils;
11 // Map of the base histogram ids for the metrics recorded for the extensions.
12 const HISTOGRAMS_IDS = {
13   backgroundPageLoad: "WEBEXT_BACKGROUND_PAGE_LOAD_MS",
14   browserActionPopupOpen: "WEBEXT_BROWSERACTION_POPUP_OPEN_MS",
15   browserActionPreloadResult: "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT",
16   contentScriptInjection: "WEBEXT_CONTENT_SCRIPT_INJECTION_MS",
17   eventPageRunningTime: "WEBEXT_EVENTPAGE_RUNNING_TIME_MS",
18   eventPageIdleResult: "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT",
19   extensionStartup: "WEBEXT_EXTENSION_STARTUP_MS",
20   pageActionPopupOpen: "WEBEXT_PAGEACTION_POPUP_OPEN_MS",
21   storageLocalGetJson: "WEBEXT_STORAGE_LOCAL_GET_MS",
22   storageLocalSetJson: "WEBEXT_STORAGE_LOCAL_SET_MS",
23   storageLocalGetIdb: "WEBEXT_STORAGE_LOCAL_IDB_GET_MS",
24   storageLocalSetIdb: "WEBEXT_STORAGE_LOCAL_IDB_SET_MS",
27 const GLEAN_METRICS_TYPES = {
28   backgroundPageLoad: "timing_distribution",
29   browserActionPopupOpen: "timing_distribution",
30   browserActionPreloadResult: "labeled_counter",
31   contentScriptInjection: "timing_distribution",
32   eventPageRunningTime: "custom_distribution",
33   eventPageIdleResult: "labeled_counter",
34   extensionStartup: "timing_distribution",
35   pageActionPopupOpen: "timing_distribution",
36   storageLocalGetJson: "timing_distribution",
37   storageLocalSetJson: "timing_distribution",
38   storageLocalGetIdb: "timing_distribution",
39   storageLocalSetIdb: "timing_distribution",
42 /**
43  * Get a trimmed version of the given string if it is longer than 80 chars (used in telemetry
44  * when a string may be longer than allowed).
45  *
46  * @param {string} str
47  *        The original string content.
48  *
49  * @returns {string}
50  *          The trimmed version of the string when longer than 80 chars, or the given string
51  *          unmodified otherwise.
52  */
53 export function getTrimmedString(str) {
54   if (str.length <= 80) {
55     return str;
56   }
58   const length = str.length;
60   // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
61   // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
62   // that joins the two parts, to visually indicate that the string has been trimmed.
63   return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
66 /**
67  * Get a string representing the error which can be included in telemetry data.
68  * If the resulting string is longer than 80 characters it is going to be
69  * trimmed using the `getTrimmedString` helper function.
70  *
71  * @param {Error | DOMException | Components.Exception} error
72  *        The error object to convert into a string representation.
73  *
74  * @returns {string}
75  *          - The `error.name` string on DOMException or Components.Exception
76  *            (trimmed to 80 chars).
77  *          - "NoError" if error is falsey.
78  *          - "UnkownError" as a fallback.
79  */
80 export function getErrorNameForTelemetry(error) {
81   let text = "UnknownError";
82   if (!error) {
83     text = "NoError";
84   } else if (
85     DOMException.isInstance(error) ||
86     error instanceof Components.Exception
87   ) {
88     text = error.name;
89     if (text.length > 80) {
90       text = getTrimmedString(text);
91     }
92   }
93   return text;
96 /**
97  * This is a internal helper object which contains a collection of helpers used to make it easier
98  * to collect extension telemetry (in both the general histogram and in the one keyed by addon id).
99  *
100  * This helper object is not exported from ExtensionUtils, it is used by the ExtensionTelemetry
101  * Proxy which is exported and used by the callers to record telemetry data for one of the
102  * supported metrics.
103  */
104 class ExtensionTelemetryMetric {
105   constructor(metric) {
106     this.metric = metric;
107     this.gleanTimerIdsMap = new DefaultWeakMap(ext => new WeakMap());
108   }
110   // Stopwatch methods.
111   stopwatchStart(extension, obj = extension) {
112     this._wrappedStopwatchMethod("start", this.metric, extension, obj);
113     this._wrappedTimingDistributionMethod("start", this.metric, extension, obj);
114   }
116   stopwatchFinish(extension, obj = extension) {
117     this._wrappedStopwatchMethod("finish", this.metric, extension, obj);
118     this._wrappedTimingDistributionMethod(
119       "stopAndAccumulate",
120       this.metric,
121       extension,
122       obj
123     );
124   }
126   stopwatchCancel(extension, obj = extension) {
127     this._wrappedStopwatchMethod("cancel", this.metric, extension, obj);
128     this._wrappedTimingDistributionMethod(
129       "cancel",
130       this.metric,
131       extension,
132       obj
133     );
134   }
136   // Histogram counters methods.
137   histogramAdd(opts) {
138     this._histogramAdd(this.metric, opts);
139   }
141   /**
142    * Wraps a call to Glean timing_distribution methods for a given metric and extension.
143    *
144    * @param {string} method
145    *        The Glean timing_distribution method to call ("start", "stopAndAccumulate" or "cancel").
146    * @param {string} metric
147    *        The Glean timing_distribution metric to record (used to retrieve the Glean metric type from the
148    *        GLEAN_METRICS_TYPES map).
149    * @param {Extension | BrowserExtensionContent} extension
150    *        The extension to record the telemetry for.
151    * @param {any | undefined} [obj = extension]
152    *        An optional object the timing_distribution method call should be related to
153    *        (defaults to the extension parameter when missing).
154    */
155   _wrappedTimingDistributionMethod(method, metric, extension, obj = extension) {
156     if (!extension) {
157       Cu.reportError(`Mandatory extension parameter is undefined`);
158       return;
159     }
161     const gleanMetricType = GLEAN_METRICS_TYPES[metric];
162     if (!gleanMetricType) {
163       Cu.reportError(`Unknown metric ${metric}`);
164       return;
165     }
167     if (gleanMetricType !== "timing_distribution") {
168       Cu.reportError(
169         `Glean metric ${metric} is of type ${gleanMetricType}, expected timing_distribution`
170       );
171       return;
172     }
174     switch (method) {
175       case "start": {
176         const timerId = Glean.extensionsTiming[metric].start();
177         this.gleanTimerIdsMap.get(extension).set(obj, timerId);
178         break;
179       }
180       case "stopAndAccumulate": // Intentional fall-through.
181       case "cancel": {
182         if (
183           !this.gleanTimerIdsMap.has(extension) ||
184           !this.gleanTimerIdsMap.get(extension).has(obj)
185         ) {
186           Cu.reportError(
187             `timerId not found for Glean timing_distribution ${metric}`
188           );
189           return;
190         }
191         const timerId = this.gleanTimerIdsMap.get(extension).get(obj);
192         this.gleanTimerIdsMap.get(extension).delete(obj);
193         Glean.extensionsTiming[metric][method](timerId);
194         break;
195       }
196       default:
197         Cu.reportError(
198           `Unknown method ${method} call for Glean metric ${metric}`
199         );
200     }
201   }
203   /**
204    * Wraps a call to a TelemetryStopwatch method for a given metric and extension.
205    *
206    * @param {string} method
207    *        The stopwatch method to call ("start", "finish" or "cancel").
208    * @param {string} metric
209    *        The stopwatch metric to record (used to retrieve the base histogram id from the HISTOGRAMS_IDS object).
210    * @param {Extension | BrowserExtensionContent} extension
211    *        The extension to record the telemetry for.
212    * @param {any | undefined} [obj = extension]
213    *        An optional telemetry stopwatch object (which defaults to the extension parameter when missing).
214    */
215   _wrappedStopwatchMethod(method, metric, extension, obj = extension) {
216     if (!extension) {
217       Cu.reportError(`Mandatory extension parameter is undefined`);
218       return;
219     }
221     const baseId = HISTOGRAMS_IDS[metric];
222     if (!baseId) {
223       Cu.reportError(`Unknown metric ${metric}`);
224       return;
225     }
227     // Record metric in the general histogram.
228     TelemetryStopwatch[method](baseId, obj);
230     // Record metric in the histogram keyed by addon id.
231     let extensionId = getTrimmedString(extension.id);
232     TelemetryStopwatch[`${method}Keyed`](
233       `${baseId}_BY_ADDONID`,
234       extensionId,
235       obj
236     );
237   }
239   /**
240    * Record a telemetry category and/or value for a given metric.
241    *
242    * @param {string} metric
243    *        The metric to record (used to retrieve the base histogram id from the _histogram object).
244    * @param {object}                              options
245    * @param {Extension | BrowserExtensionContent} options.extension
246    *        The extension to record the telemetry for.
247    * @param {string | undefined}                  [options.category]
248    *        An optional histogram category.
249    * @param {number | undefined}                  [options.value]
250    *        An optional value to record.
251    */
252   _histogramAdd(metric, { category, extension, value }) {
253     if (!extension) {
254       Cu.reportError(`Mandatory extension parameter is undefined`);
255       return;
256     }
258     const baseId = HISTOGRAMS_IDS[metric];
259     if (!baseId) {
260       Cu.reportError(`Unknown metric ${metric}`);
261       return;
262     }
264     const histogram = Services.telemetry.getHistogramById(baseId);
265     if (typeof category === "string") {
266       histogram.add(category, value);
267     } else {
268       histogram.add(value);
269     }
271     const keyedHistogram = Services.telemetry.getKeyedHistogramById(
272       `${baseId}_BY_ADDONID`
273     );
274     const extensionId = getTrimmedString(extension.id);
276     if (typeof category === "string") {
277       keyedHistogram.add(extensionId, category, value);
278     } else {
279       keyedHistogram.add(extensionId, value);
280     }
282     switch (GLEAN_METRICS_TYPES[metric]) {
283       case "custom_distribution": {
284         if (typeof category === "string") {
285           Cu.reportError(
286             `Unexpected unsupported category parameter set on Glean metric ${metric}`
287           );
288           return;
289         }
290         // NOTE: extensionsTiming may become a property of the GLEAN_METRICS_TYPES
291         // map once we may introduce new histograms that are not part of the
292         // extensionsTiming Glean metrics category.
293         Glean.extensionsTiming[metric].accumulateSamples([value]);
294         break;
295       }
296       case "labeled_counter": {
297         if (typeof category !== "string") {
298           Cu.reportError(
299             `Missing mandatory category on adding data to labeled Glean metric ${metric}`
300           );
301           return;
302         }
303         Glean.extensionsCounters[metric][category].add(value ?? 1);
304         break;
305       }
306       default:
307         Cu.reportError(
308           `Unexpected unsupported Glean metric type "${GLEAN_METRICS_TYPES[metric]}" for metric ${metric}`
309         );
310     }
311   }
314 // Cache of the ExtensionTelemetryMetric instances that has been lazily created by the
315 // Extension Telemetry Proxy.
316 /** @type {Map<string|symbol, ExtensionTelemetryMetric>} */
317 const metricsCache = new Map();
320  * This proxy object provides the telemetry helpers for the currently supported metrics (the ones listed in
321  * HISTOGRAMS_IDS), the telemetry helpers for a particular metric are lazily created
322  * when the related property is being accessed on this object for the first time, e.g.:
324  *      ExtensionTelemetry.extensionStartup.stopwatchStart(extension);
325  *      ExtensionTelemetry.browserActionPreloadResult.histogramAdd({category: "Shown", extension});
326  */
327 export var ExtensionTelemetry = new Proxy(metricsCache, {
328   get(target, prop, receiver) {
329     // NOTE: if we would be start adding glean probes that do not have a unified
330     // telemetry histogram counterpart, we would need to change this check
331     // accordingly.
332     if (!(prop in HISTOGRAMS_IDS)) {
333       throw new Error(`Unknown metric ${String(prop)}`);
334     }
336     // Lazily create and cache the metric result object.
337     if (!target.has(prop)) {
338       target.set(prop, new ExtensionTelemetryMetric(prop));
339     }
341     return target.get(prop);
342   },