summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/pings/CoveragePing.sys.mjs
blob: a41d7dcc7bfd6fedf059619c04c2ffbbdde02a5a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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;
  },
});