tor-browser

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

NewTabGleanUtils.sys.mjs (11132B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
      8  return console.createInstance({
      9    prefix: "NewTabGleanUtils",
     10    maxLogLevel: Services.prefs.getBoolPref(
     11      "browser.newtabpage.glean-utils.log",
     12      false
     13    )
     14      ? "Debug"
     15      : "Warn",
     16  });
     17 });
     18 
     19 const EXTRA_ARGS_TYPES_ALLOWLIST = [
     20  "event",
     21  "memory_distribution",
     22  "timing_distribution",
     23 ];
     24 
     25 /**
     26 * Module for managing Glean telemetry metrics and pings in the New Tab page.
     27 * This object provides functionality to:
     28 * - Read and parse JSON configuration files containing metrics and ping definitions
     29 * - Register metrics and pings at runtime
     30 * - Convert between different naming conventions (dotted snake case, kebab case, camel case)
     31 * - Handle metric and ping registration with proper error handling and logging
     32 */
     33 export const NewTabGleanUtils = {
     34  /**
     35   * Internal Promise.withResolvers() object for tracking metrics and pings
     36   * registration completion. Contains resolve, reject functions and a promise
     37   * that resolves when registerMetricsAndPings completes.
     38   *
     39   * @type {{promise: Promise<void>, resolve: Function, reject: Function}}
     40   * @private
     41   */
     42  _registrationDone: Promise.withResolvers(),
     43 
     44  /**
     45   * Gets the promise that resolves when metrics and pings registration is
     46   * complete. This allows external code to wait for registration to finish
     47   * before using registered metrics.
     48   *
     49   * @returns {Promise<void>} A promise that resolves when
     50   *   registerMetricsAndPings completes
     51   */
     52  get registrationDone() {
     53    return this._registrationDone.promise;
     54  },
     55 
     56  /**
     57   * Fetches and parses a JSON file from a given resource URI.
     58   *
     59   * @param {string} resourceURI - The URI of the JSON file to fetch and parse
     60   * @returns {Promise<object>} A promise that resolves to the parsed JSON object
     61   */
     62  async readJSON(resourceURI) {
     63    let result = await fetch(resourceURI);
     64    return result.json();
     65  },
     66  /**
     67   * Processes and registers Glean metrics and pings from a JSON configuration file.
     68   * This method performs two main operations:
     69   * 1. Registers all pings defined in the configuration
     70   * 2. Registers all metrics under their respective categories
     71   * Example: await NewTabGleanUtils.registerMetricsAndPings("resource://path/to/metrics.json");
     72   *
     73   * @param {string} resourceURI - The URI of the JSON file containing metrics and pings definitions
     74   * @returns {Promise<boolean>} A promise that resolves when all metrics and pings are registered
     75   * If a metric or ping registration fails, all further registration halts and this Promise
     76   * will still resolve (errors will be logged to the console).
     77   */
     78  async registerMetricsAndPings(resourceURI) {
     79    try {
     80      const data = await this.readJSON(resourceURI);
     81 
     82      // Check if data exists and has either metrics or pings to register
     83      if (!data || (!data.metrics && !data.pings)) {
     84        lazy.logConsole.log("No metrics or pings found in the JSON file");
     85        return false;
     86      }
     87 
     88      // First register all pings from the JSON file
     89      if (data.pings) {
     90        for (const [pingName, pingConfig] of Object.entries(data.pings)) {
     91          await this.registerPingIfNeeded({
     92            name: pingName,
     93            ...this.convertToCamelCase(pingConfig),
     94          });
     95        }
     96      }
     97 
     98      // Then register all metrics under their respective categories
     99      if (data.metrics) {
    100        for (const [category, metrics] of Object.entries(data.metrics)) {
    101          for (const [name, config] of Object.entries(metrics)) {
    102            await this.registerMetricIfNeeded({
    103              ...config,
    104              category,
    105              name,
    106            });
    107          }
    108        }
    109      }
    110      lazy.logConsole.debug(
    111        "Successfully registered metrics and pings found in the JSON file"
    112      );
    113      this._registrationDone.resolve();
    114      return true;
    115    } catch (e) {
    116      lazy.logConsole.error(
    117        "Failed to complete registration of metrics and pings found in runtime metrics JSON:",
    118        e
    119      );
    120      this._registrationDone.resolve();
    121      return false;
    122    }
    123  },
    124 
    125  /**
    126   * Registers a metric in Glean if it doesn't already exist.
    127   *
    128   * @param {object} options - The metric configuration options
    129   * @param {string} options.type - The type of metric (e.g., "text", "counter")
    130   * @param {string} options.category - The category the metric belongs to
    131   * @param {string} options.name - The name of the metric
    132   * @param {string[]} options.pings - Array of ping names this metric belongs to
    133   * @param {string} options.lifetime - The lifetime of the metric
    134   * @param {boolean} [options.disabled] - Whether the metric is disabled
    135   * @param {object} [options.extraArgs] - Additional arguments for the metric
    136   * @throws {Error} If a new metrics registration fails and error will be logged in console
    137   */
    138  registerMetricIfNeeded(options) {
    139    const { type, category, name, pings, lifetime, disabled, extraArgs } =
    140      options;
    141 
    142    // Glean metric to record the success of metric registration for telemetry purposes.
    143    let gleanSuccessMetric = Glean.newtab.metricRegistered[name];
    144 
    145    try {
    146      let categoryName = this.dottedSnakeToCamel(category);
    147      let metricName = this.dottedSnakeToCamel(name);
    148 
    149      if (categoryName in Glean && metricName in Glean[categoryName]) {
    150        lazy.logConsole.warn(
    151          `Fail to register metric ${name} in category ${category} as it already exists`
    152        );
    153        return;
    154      }
    155 
    156      // Convert extraArgs to JSON string for metrics types in allowlist
    157      let extraArgsJson = null;
    158      if (
    159        EXTRA_ARGS_TYPES_ALLOWLIST.includes(type) &&
    160        extraArgs &&
    161        Object.keys(extraArgs).length
    162      ) {
    163        extraArgsJson = JSON.stringify(extraArgs);
    164      }
    165 
    166      // Metric doesn't exist, register it
    167      lazy.logConsole.debug(`Registering metric ${name} at runtime`);
    168 
    169      // Register the metric
    170      Services.fog.registerRuntimeMetric(
    171        type,
    172        category,
    173        name,
    174        pings,
    175        `"${lifetime}"`,
    176        disabled,
    177        extraArgsJson
    178      );
    179      gleanSuccessMetric.set(true);
    180    } catch (e) {
    181      gleanSuccessMetric.set(false);
    182      lazy.logConsole.error(`Error registering metric ${name}: ${e}`);
    183      throw new Error(`Failure while registering metrics ${name} `);
    184    }
    185  },
    186 
    187  /**
    188   * Registers a ping in Glean if it doesn't already exist.
    189   *
    190   * @param {object} options - The ping configuration options
    191   * @param {string} options.name - The name of the ping
    192   * @param {boolean} [options.includeClientId] - Whether to include client ID
    193   * @param {boolean} [options.sendIfEmpty] - Whether to send ping if empty
    194   * @param {boolean} [options.preciseTimestamps] - Whether to use precise timestamps
    195   * @param {boolean} [options.includeInfoSections] - Whether to include info sections
    196   * @param {boolean} [options.enabled] - Whether the ping is enabled
    197   * @param {string[]} [options.schedulesPings] - Array of scheduled ping times
    198   * @param {string[]} [options.reasonCodes] - Array of valid reason codes
    199   * @param {boolean} [options.followsCollectionEnabled] - Whether ping follows collection enabled state
    200   * @param {string[]} [options.uploaderCapabilities] - Array of uploader capabilities for this ping
    201   * @throws {Error} If a new ping registration fails and error will be logged in console
    202   */
    203  registerPingIfNeeded(options) {
    204    const {
    205      name,
    206      includeClientId,
    207      sendIfEmpty,
    208      preciseTimestamps,
    209      includeInfoSections,
    210      enabled,
    211      schedulesPings,
    212      reasonCodes,
    213      followsCollectionEnabled,
    214      uploaderCapabilities,
    215    } = options;
    216 
    217    // Glean metric to record the success of ping registration for telemetry purposes.
    218    let gleanSuccessPing = Glean.newtab.pingRegistered[name];
    219    try {
    220      let pingName = this.kebabToCamel(name);
    221      if (pingName in GleanPings) {
    222        lazy.logConsole.warn(
    223          `Fail to register ping ${name} as it already exists`
    224        );
    225        return;
    226      }
    227 
    228      // Ping doesn't exist, register it
    229      lazy.logConsole.debug(`Registering ping ${name} at runtime`);
    230 
    231      Services.fog.registerRuntimePing(
    232        name,
    233        includeClientId,
    234        sendIfEmpty,
    235        preciseTimestamps,
    236        includeInfoSections,
    237        enabled,
    238        schedulesPings,
    239        reasonCodes,
    240        followsCollectionEnabled,
    241        uploaderCapabilities
    242      );
    243      gleanSuccessPing.set(true);
    244    } catch (e) {
    245      gleanSuccessPing.set(false);
    246      lazy.logConsole.error(`Error registering ping ${name}: ${e}`);
    247      throw new Error(`Failure while registering ping ${name} `);
    248    }
    249  },
    250 
    251  /**
    252   * Converts a dotted snake case string to camel case.
    253   * Example: "foo.bar_baz" becomes "fooBarBaz"
    254   *
    255   * @param {string} metricNameOrCategory - The string in dotted snake case format
    256   * @returns {string} The converted camel case string
    257   */
    258  dottedSnakeToCamel(metricNameOrCategory) {
    259    if (!metricNameOrCategory) {
    260      return "";
    261    }
    262 
    263    let camel = "";
    264    // Split by underscore and then by dots
    265    const segments = metricNameOrCategory.split("_");
    266    for (const segment of segments) {
    267      const parts = segment.split(".");
    268      for (const part of parts) {
    269        if (!camel) {
    270          camel += part;
    271        } else if (part.length) {
    272          const firstChar = part.charAt(0);
    273          if (firstChar >= "a" && firstChar <= "z") {
    274            // Capitalize first letter and append rest of the string
    275            camel += firstChar.toUpperCase() + part.slice(1);
    276          } else {
    277            // If first char is not a-z, append as is
    278            camel += part;
    279          }
    280        }
    281      }
    282    }
    283    return camel;
    284  },
    285 
    286  /**
    287   * Converts a kebab case string to camel case.
    288   * Example: "foo-bar-baz" becomes "fooBarBaz"
    289   *
    290   * @param {string} pingName - The string in kebab case format
    291   * @returns {string} The converted camel case string
    292   */
    293  kebabToCamel(pingName) {
    294    if (!pingName) {
    295      return "";
    296    }
    297 
    298    let camel = "";
    299    // Split by hyphens
    300    const segments = pingName.split("-");
    301    for (const segment of segments) {
    302      if (!camel) {
    303        camel += segment;
    304      } else if (segment.length) {
    305        const firstChar = segment.charAt(0);
    306        if (firstChar >= "a" && firstChar <= "z") {
    307          // Capitalize first letter and append rest of the string
    308          camel += firstChar.toUpperCase() + segment.slice(1);
    309        } else {
    310          // If first char is not a-z, append as is
    311          camel += segment;
    312        }
    313      }
    314    }
    315    return camel;
    316  },
    317 
    318  // Convert all properties in an object from snake_case to camelCase
    319  convertToCamelCase(obj) {
    320    const result = {};
    321    for (const [key, value] of Object.entries(obj)) {
    322      result[this.dottedSnakeToCamel(key)] = value;
    323    }
    324    return result;
    325  },
    326 };