diff options
Diffstat (limited to 'toolkit/components/telemetry/pings/CoveragePing.sys.mjs')
-rw-r--r-- | toolkit/components/telemetry/pings/CoveragePing.sys.mjs | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/pings/CoveragePing.sys.mjs b/toolkit/components/telemetry/pings/CoveragePing.sys.mjs new file mode 100644 index 0000000000..a41d7dcc7b --- /dev/null +++ b/toolkit/components/telemetry/pings/CoveragePing.sys.mjs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CommonUtils: "resource://services-common/utils.sys.mjs", + ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +const COVERAGE_VERSION = "2"; + +const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled"; +const LOG_LEVEL_PREF = "toolkit.coverage.log-level"; +const OPT_OUT_PREF = "toolkit.coverage.opt-out"; +const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`; +const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`; +const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled"; +const REPORTING_ENDPOINT_BASE_PREF = `toolkit.coverage.endpoint.base`; +const REPORTING_ENDPOINT = "submit/coverage/coverage"; +const PING_SUBMISSION_TIMEOUT = 30 * 1000; // 30 seconds + +const log = Log.repository.getLogger("Telemetry::CoveragePing"); +log.level = Services.prefs.getIntPref(LOG_LEVEL_PREF, Log.Level.Error); +log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); + +export var CoveragePing = Object.freeze({ + async startup() { + if (!Services.prefs.getBoolPref(COVERAGE_ENABLED_PREF, false)) { + log.debug("coverage not enabled"); + return; + } + + if (Services.prefs.getBoolPref(OPT_OUT_PREF, false)) { + log.debug("user has set opt-out pref"); + return; + } + + if (Services.prefs.getBoolPref(ALREADY_RUN_PREF, false)) { + log.debug("already run on this profile"); + return; + } + + if (!Services.prefs.getCharPref(REPORTING_ENDPOINT_BASE_PREF, null)) { + log.error("no endpoint base set"); + return; + } + + try { + await this.reportTelemetrySetting(); + } catch (e) { + log.error("unable to upload payload", e); + } + }, + + // NOTE - this does not use existing Telemetry code or honor Telemetry opt-out prefs, + // by design. It also sends no identifying data like the client ID. See the "coverage ping" + // documentation for details. + reportTelemetrySetting() { + const enabled = Services.prefs.getBoolPref(TELEMETRY_ENABLED_PREF, false); + + const payload = { + appVersion: Services.appinfo.version, + appUpdateChannel: lazy.UpdateUtils.getUpdateChannel(false), + osName: Services.sysinfo.getProperty("name"), + osVersion: Services.sysinfo.getProperty("version"), + telemetryEnabled: enabled, + }; + + let cachedUuid = Services.prefs.getCharPref(COVERAGE_UUID_PREF, null); + if (!cachedUuid) { + // Totally random UUID, just for detecting duplicates. + cachedUuid = lazy.CommonUtils.generateUUID(); + Services.prefs.setCharPref(COVERAGE_UUID_PREF, cachedUuid); + } + + let reportingEndpointBase = Services.prefs.getCharPref( + REPORTING_ENDPOINT_BASE_PREF, + null + ); + + let endpoint = `${reportingEndpointBase}/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${cachedUuid}`; + + log.debug(`putting to endpoint ${endpoint} with payload:`, payload); + + let deferred = Promise.withResolvers(); + + let request = new lazy.ServiceRequest({ mozAnon: true }); + request.mozBackgroundRequest = true; + request.timeout = PING_SUBMISSION_TIMEOUT; + + request.open("PUT", endpoint, true); + request.overrideMimeType("text/plain"); + request.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); + request.setRequestHeader("Date", new Date().toUTCString()); + + let errorhandler = event => { + let failure = event.type; + log.error(`error making request to ${endpoint}: ${failure}`); + deferred.reject(event); + }; + + request.onerror = errorhandler; + request.ontimeout = errorhandler; + request.onabort = errorhandler; + + request.onloadend = event => { + let status = request.status; + let statusClass = status - (status % 100); + let success = false; + + if (statusClass === 200) { + // We can treat all 2XX as success. + log.info(`successfully submitted, status: ${status}`); + success = true; + } else if (statusClass === 400) { + // 4XX means that something with the request was broken. + + // TODO: we should handle this better, but for now we should avoid resubmitting + // broken requests by pretending success. + success = true; + log.error( + `error submitting to ${endpoint}, status: ${status} - ping request broken?` + ); + } else if (statusClass === 500) { + // 5XX means there was a server-side error and we should try again later. + log.error( + `error submitting to ${endpoint}, status: ${status} - server error, should retry later` + ); + } else { + // We received an unexpected status code. + log.error( + `error submitting to ${endpoint}, status: ${status}, type: ${event.type}` + ); + } + + if (success) { + Services.prefs.setBoolPref(ALREADY_RUN_PREF, true); + log.debug(`result from PUT: ${request.responseText}`); + deferred.resolve(); + } else { + deferred.reject(event); + } + }; + + request.send(JSON.stringify(payload)); + + return deferred.promise; + }, +}); |