summaryrefslogtreecommitdiffstats
path: root/browser/modules/PingCentre.jsm
blob: 7d9bfb18ec9cc6731ea9f9d9d2002138262b2e68 (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/. */

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
  sendStandalonePing: "resource://gre/modules/TelemetrySend.sys.mjs",
});

const PREF_BRANCH = "browser.ping-centre.";

const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
const LOGGING_PREF = `${PREF_BRANCH}log`;

const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";

/**
 * Observe various notifications and send them to a telemetry endpoint.
 *
 * @param {Object} options
 * @param {string} options.topic - a unique ID for users of PingCentre to distinguish
 *                  their data on the server side.
 */
class PingCentre {
  constructor(options) {
    if (!options.topic) {
      throw new Error("Must specify topic.");
    }

    this._topic = options.topic;
    this._prefs = Services.prefs.getBranch("");

    this._enabled = this._prefs.getBoolPref(TELEMETRY_PREF);
    this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
    this._prefs.addObserver(TELEMETRY_PREF, this._onTelemetryPrefChange);

    this._fhrEnabled = this._prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF);
    this._onFhrPrefChange = this._onFhrPrefChange.bind(this);
    this._prefs.addObserver(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);

    this.logging = this._prefs.getBoolPref(LOGGING_PREF);
    this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);
    this._prefs.addObserver(LOGGING_PREF, this._onLoggingPrefChange);
  }

  get enabled() {
    return this._enabled && this._fhrEnabled;
  }

  _onLoggingPrefChange(aSubject, aTopic, prefKey) {
    this.logging = this._prefs.getBoolPref(prefKey);
  }

  _onTelemetryPrefChange(aSubject, aTopic, prefKey) {
    this._enabled = this._prefs.getBoolPref(prefKey);
  }

  _onFhrPrefChange(aSubject, aTopic, prefKey) {
    this._fhrEnabled = this._prefs.getBoolPref(prefKey);
  }

  _createExperimentsPayload() {
    let activeExperiments = lazy.TelemetryEnvironment.getActiveExperiments();
    let experiments = {};
    for (let experimentID in activeExperiments) {
      if (
        activeExperiments[experimentID] &&
        activeExperiments[experimentID].branch
      ) {
        experiments[experimentID] = {
          branch: activeExperiments[experimentID].branch,
        };
      }
    }
    return experiments;
  }

  _createStructuredIngestionPing(data) {
    let experiments = this._createExperimentsPayload();
    let locale = data.locale || Services.locale.appLocaleAsBCP47;
    const payload = {
      experiments,
      locale,
      version: AppConstants.MOZ_APP_VERSION,
      release_channel: lazy.UpdateUtils.getUpdateChannel(false),
      ...data,
    };

    return payload;
  }

  // We route through this helper because it gets hooked in testing.
  static _sendStandalonePing(endpoint, payload) {
    return lazy.sendStandalonePing(endpoint, payload);
  }

  /**
   * Sends a ping to the Structured Ingestion telemetry pipeline.
   *
   * The payload would be compressed using gzip.
   *
   * @param {Object} data     The payload to be sent.
   * @param {String} endpoint The destination endpoint. Note that Structured Ingestion
   *                          requires a different endpoint for each ping. It's up to the
   *                          caller to provide that. See more details at
   *                          https://github.com/mozilla/gcp-ingestion/blob/master/docs/edge.md#postput-request
   */
  sendStructuredIngestionPing(data, endpoint) {
    if (!this.enabled) {
      return Promise.resolve();
    }

    const ping = this._createStructuredIngestionPing(data);
    const payload = JSON.stringify(ping);

    if (this.logging) {
      Services.console.logStringMessage(
        `TELEMETRY PING (${this._topic}): ${payload}\n`
      );
    }

    return PingCentre._sendStandalonePing(endpoint, payload).catch(event => {
      Glean.pingCentre.sendFailures.add(1);
      console.error(
        `Structured Ingestion ping failure with error: ${event.type}`
      );
    });
  }

  uninit() {
    try {
      this._prefs.removeObserver(TELEMETRY_PREF, this._onTelemetryPrefChange);
      this._prefs.removeObserver(LOGGING_PREF, this._onLoggingPrefChange);
      this._prefs.removeObserver(
        FHR_UPLOAD_ENABLED_PREF,
        this._onFhrPrefChange
      );
    } catch (e) {
      console.error(e);
    }
  }
}

const PingCentreConstants = {
  FHR_UPLOAD_ENABLED_PREF,
  TELEMETRY_PREF,
  LOGGING_PREF,
};
const EXPORTED_SYMBOLS = ["PingCentre", "PingCentreConstants"];