tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AboutWelcomeTelemetry.sys.mjs (8274B)


      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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  AttributionCode:
     11    "moz-src:///browser/components/attribution/AttributionCode.sys.mjs",
     12  ClientID: "resource://gre/modules/ClientID.sys.mjs",
     13  TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () =>
     17  lazy.ClientID.getClientID()
     18 );
     19 ChromeUtils.defineLazyGetter(
     20  lazy,
     21  "browserSessionId",
     22  () => lazy.TelemetrySession.getMetadata("").sessionId
     23 );
     24 
     25 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     26  const { Logger } = ChromeUtils.importESModule(
     27    "resource://messaging-system/lib/Logger.sys.mjs"
     28  );
     29  return new Logger("AboutWelcomeTelemetry");
     30 });
     31 
     32 export class AboutWelcomeTelemetry {
     33  constructor() {
     34    XPCOMUtils.defineLazyPreferenceGetter(
     35      this,
     36      "telemetryEnabled",
     37      "browser.newtabpage.activity-stream.telemetry",
     38      false
     39    );
     40  }
     41 
     42  /**
     43   * Attach browser attribution data to a ping payload.
     44   *
     45   * It intentionally queries the *cached* attribution data other than calling
     46   * `getAttrDataAsync()` in order to minimize the overhead here.
     47   * For the same reason, we are not querying the attribution data from
     48   * `TelemetryEnvironment.currentEnvironment.settings`.
     49   *
     50   * In practice, it's very likely that the attribution data is already read
     51   * and cached at some point by `AboutWelcomeParent`, so it should be able to
     52   * read the cached results for the most if not all of the pings.
     53   */
     54  _maybeAttachAttribution(ping) {
     55    const attribution = lazy.AttributionCode.getCachedAttributionData();
     56    if (attribution && Object.keys(attribution).length) {
     57      ping.attribution = attribution;
     58    }
     59    return ping;
     60  }
     61 
     62  async _createPing(event) {
     63    if (event.event_context && typeof event.event_context === "object") {
     64      event.event_context = JSON.stringify(event.event_context);
     65    }
     66    let ping = {
     67      ...event,
     68      addon_version: Services.appinfo.appBuildID,
     69      locale: Services.locale.appLocaleAsBCP47,
     70      client_id: await lazy.telemetryClientId,
     71      browser_session_id: lazy.browserSessionId,
     72    };
     73 
     74    return this._maybeAttachAttribution(ping);
     75  }
     76 
     77  /**
     78   * Augment the provided event with some metadata and then send it
     79   * to the messaging-system's onboarding endpoint.
     80   *
     81   * Is sometimes used by non-onboarding events.
     82   *
     83   * @param event - an object almost certainly from an onboarding flow (though
     84   *                there is a case where spotlight may use this, too)
     85   *                containing a nested structure of data for reporting as
     86   *                telemetry, as documented in
     87   * https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/data_events.html
     88   *                Does not have all of its data (`_createPing` will augment
     89   *                with ids and attribution if available).
     90   */
     91  async sendTelemetry(event) {
     92    if (!this.telemetryEnabled) {
     93      return;
     94    }
     95 
     96    const ping = await this._createPing(event);
     97 
     98    try {
     99      this.submitGleanPingForPing(ping);
    100    } catch (e) {
    101      // Though Glean APIs are forbidden to throw, it may be possible that a
    102      // mismatch between the shape of `ping` and the defined metrics is not
    103      // adequately handled.
    104      Glean.messagingSystem.gleanPingForPingFailures.add(1);
    105    }
    106  }
    107 
    108  /**
    109   * Tries to infer appropriate Glean metrics on the "messaging-system" ping,
    110   * sets them, and submits a "messaging-system" ping.
    111   *
    112   * Does not check if telemetry is enabled.
    113   * (Though Glean will check the global prefs).
    114   *
    115   * Note: This is a very unusual use of Glean that is specific to the use-
    116   *       cases of Messaging System. Please do not copy this pattern.
    117   */
    118  submitGleanPingForPing(ping) {
    119    lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`);
    120    // event.event_context is an object, but it may have been stringified.
    121    let event_context = ping?.event_context;
    122 
    123    if (typeof event_context === "string") {
    124      try {
    125        event_context = JSON.parse(event_context);
    126      } catch (e) {
    127        // The Empty JSON strings and non-objects often provided by the
    128        // existing telemetry we need to send failing to parse do not fit in
    129        // the spirit of what this error is meant to capture. Instead, we want
    130        // to capture when what we got should have been an object,
    131        // but failed to parse.
    132        if (event_context.length && event_context.includes("{")) {
    133          Glean.messagingSystem.eventContextParseError.add(1);
    134        }
    135      }
    136    }
    137 
    138    // We echo certain properties from event_context into their own metrics
    139    // to aid analysis.
    140    if (event_context?.reason) {
    141      Glean.messagingSystem.eventReason.set(event_context.reason);
    142    }
    143    if (event_context?.page) {
    144      Glean.messagingSystem.eventPage.set(event_context.page);
    145    }
    146    if (event_context?.source) {
    147      Glean.messagingSystem.eventSource.set(event_context.source);
    148    }
    149    if (event_context?.screen_family) {
    150      Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family);
    151    }
    152    // Screen_index was being coerced into a boolean value
    153    // which resulted in 0 (first screen index) being ignored.
    154    if (Number.isInteger(event_context?.screen_index)) {
    155      Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index);
    156    }
    157    if (event_context?.screen_id) {
    158      Glean.messagingSystem.eventScreenId.set(event_context.screen_id);
    159    }
    160    if (event_context?.screen_initials) {
    161      Glean.messagingSystem.eventScreenInitials.set(
    162        event_context.screen_initials
    163      );
    164    }
    165 
    166    // The event_context is also provided as-is as stringified JSON.
    167    if (event_context) {
    168      Glean.messagingSystem.eventContext.set(JSON.stringify(event_context));
    169    }
    170 
    171    if ("attribution" in ping) {
    172      for (const [key, value] of Object.entries(ping.attribution)) {
    173        const camelKey = this._snakeToCamelCase(key);
    174        try {
    175          Glean.messagingSystemAttribution[camelKey].set(value);
    176        } catch (e) {
    177          // We here acknowledge that we don't know the full breadth of data
    178          // being collected. Ideally AttributionCode will later centralize
    179          // definition and reporting of attribution data and we can be rid of
    180          // this fail-safe for collecting the names of unknown keys.
    181          Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1);
    182        }
    183      }
    184    }
    185 
    186    // List of keys handled above.
    187    const handledKeys = ["event_context", "attribution"];
    188 
    189    for (const [key, value] of Object.entries(ping)) {
    190      if (handledKeys.includes(key)) {
    191        continue;
    192      }
    193      const camelKey = this._snakeToCamelCase(key);
    194      try {
    195        // We here acknowledge that even known keys might have non-scalar
    196        // values. We're pretty sure we handled them all with handledKeys,
    197        // but we might not have.
    198        // Ideally this can later be removed after running for a version or two
    199        // with no values seen in messaging_system.invalid_nested_data
    200        if (typeof value === "object") {
    201          Glean.messagingSystem.invalidNestedData[camelKey].add(1);
    202        } else {
    203          Glean.messagingSystem[camelKey].set(value);
    204        }
    205      } catch (e) {
    206        // We here acknowledge that we don't know the full breadth of data being
    207        // collected. Ideally we will later gain that confidence and can remove
    208        // this fail-safe for collecting the names of unknown keys.
    209        Glean.messagingSystem.unknownKeys[camelKey].add(1);
    210        // TODO(bug 1600008): For testing, also record the overall count.
    211        Glean.messagingSystem.unknownKeyCount.add(1);
    212      }
    213    }
    214 
    215    // With all the metrics set, now it's time to submit this ping.
    216    GleanPings.messagingSystem.submit();
    217  }
    218 
    219  _snakeToCamelCase(s) {
    220    return s.toString().replace(/_([a-z])/gi, (_str, group) => {
    221      return group.toUpperCase();
    222    });
    223  }
    224 }