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/. */
4
5/*
6 * This module sends Origin Telemetry periodically:
7 * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/prio-ping.html
8 */
9
10"use strict";
11
12var EXPORTED_SYMBOLS = ["TelemetryPrioPing", "Policy"];
13
14const { XPCOMUtils } = ChromeUtils.import(
15  "resource://gre/modules/XPCOMUtils.jsm"
16);
17
18XPCOMUtils.defineLazyModuleGetters(this, {
19  TelemetryController: "resource://gre/modules/TelemetryController.jsm",
20  Log: "resource://gre/modules/Log.jsm",
21});
22
23const { TelemetryUtils } = ChromeUtils.import(
24  "resource://gre/modules/TelemetryUtils.jsm"
25);
26const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
27
28const Utils = TelemetryUtils;
29
30const LOGGER_NAME = "Toolkit.Telemetry";
31const LOGGER_PREFIX = "TelemetryPrioPing";
32
33// Triggered from native Origin Telemetry storage.
34const PRIO_LIMIT_REACHED_TOPIC = "origin-telemetry-storage-limit-reached";
35
36const PRIO_PING_VERSION = "1";
37
38var Policy = {
39  sendPing: (type, payload, options) =>
40    TelemetryController.submitExternalPing(type, payload, options),
41  getEncodedOriginSnapshot: async aClear =>
42    Services.telemetry.getEncodedOriginSnapshot(aClear),
43};
44
45var TelemetryPrioPing = {
46  Reason: Object.freeze({
47    PERIODIC: "periodic", // Sent the ping containing Origin Telemetry from the past periodic interval (default 24h).
48    MAX: "max", // Sent the ping containing at least the maximum number (default 10) of prioData elements, earlier than the periodic interval.
49    SHUTDOWN: "shutdown", // Recorded data was sent on shutdown.
50  }),
51
52  PRIO_PING_TYPE: "prio",
53
54  _logger: null,
55  _testing: false,
56  _timeoutId: null,
57
58  startup() {
59    if (!this._testing && !Services.telemetry.canRecordPrereleaseData) {
60      this._log.trace("Extended collection disabled. Prio ping disabled.");
61      return;
62    }
63
64    if (
65      !this._testing &&
66      !Services.prefs.getBoolPref(Utils.Preferences.PrioPingEnabled, true)
67    ) {
68      this._log.trace("Prio ping disabled by pref.");
69      return;
70    }
71    this._log.trace("Starting up.");
72
73    Services.obs.addObserver(this, PRIO_LIMIT_REACHED_TOPIC);
74  },
75
76  async shutdown() {
77    this._log.trace("Shutting down.");
78    // removeObserver may throw, which could interrupt shutdown.
79    try {
80      Services.obs.removeObserver(this, PRIO_LIMIT_REACHED_TOPIC);
81    } catch (ex) {}
82
83    await this._submitPing(this.Reason.SHUTDOWN);
84  },
85
86  observe(aSubject, aTopic, aData) {
87    switch (aTopic) {
88      case PRIO_LIMIT_REACHED_TOPIC:
89        this._log.trace("prio limit reached");
90        this._submitPing(this.Reason.MAX);
91        break;
92    }
93  },
94
95  periodicPing() {
96    this._log.trace("periodic ping triggered");
97    this._submitPing(this.Reason.PERIODIC);
98  },
99
100  /**
101   * Submits an "prio" ping and restarts the timer for the next interval.
102   *
103   * @param {String} reason The reason we're sending the ping. One of TelemetryPrioPing.Reason.
104   */
105  async _submitPing(reason) {
106    this._log.trace("_submitPing");
107
108    let snapshot = await Policy.getEncodedOriginSnapshot(true /* clear */);
109
110    if (!this._testing) {
111      snapshot = snapshot.filter(
112        ({ encoding }) => !encoding.startsWith("telemetry.test")
113      );
114    }
115
116    if (snapshot.length === 0) {
117      // Don't send a ping if we haven't anything to send
118      this._log.trace("nothing to send");
119      return;
120    }
121
122    let payload = {
123      version: PRIO_PING_VERSION,
124      reason,
125      prioData: snapshot,
126    };
127
128    const options = {
129      addClientId: false,
130      addEnvironment: false,
131      usePingSender: reason === this.Reason.SHUTDOWN,
132    };
133
134    Policy.sendPing(this.PRIO_PING_TYPE, payload, options);
135  },
136
137  /**
138   * Test-only, restore to initial state.
139   */
140  testReset() {
141    this._testing = true;
142  },
143
144  get _log() {
145    if (!this._logger) {
146      this._logger = Log.repository.getLoggerWithMessagePrefix(
147        LOGGER_NAME,
148        LOGGER_PREFIX + "::"
149      );
150    }
151
152    return this._logger;
153  },
154};
155