tor-browser

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

event-recording.js (8821B)


      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 "use strict";
      6 
      7 const Telemetry = require("resource://devtools/client/shared/telemetry.js");
      8 loader.lazyGetter(
      9  this,
     10  "telemetry",
     11  () => new Telemetry({ useSessionId: true })
     12 );
     13 
     14 const {
     15  CONNECT_RUNTIME_CANCEL,
     16  CONNECT_RUNTIME_FAILURE,
     17  CONNECT_RUNTIME_NOT_RESPONDING,
     18  CONNECT_RUNTIME_START,
     19  CONNECT_RUNTIME_SUCCESS,
     20  DISCONNECT_RUNTIME_SUCCESS,
     21  REMOTE_RUNTIMES_UPDATED,
     22  RUNTIMES,
     23  SELECT_PAGE_SUCCESS,
     24  SHOW_PROFILER_DIALOG,
     25  TELEMETRY_RECORD,
     26  UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
     27 } = require("resource://devtools/client/aboutdebugging/src/constants.js");
     28 
     29 const {
     30  findRuntimeById,
     31  getAllRuntimes,
     32  getCurrentRuntime,
     33 } = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
     34 
     35 function recordEvent(method, details) {
     36  telemetry.recordEvent(method, "aboutdebugging", null, details);
     37 
     38  // For close and open events, also ping the regular telemetry helpers used
     39  // for all DevTools UIs.
     40  if (method === "open_adbg") {
     41    telemetry.toolOpened("aboutdebugging", window.AboutDebugging);
     42  } else if (method === "close_adbg") {
     43    // XXX: Note that aboutdebugging has no histogram created for
     44    // TIME_ACTIVE_SECOND, so calling toolClosed will not actually
     45    // record anything.
     46    telemetry.toolClosed("aboutdebugging", window.AboutDebugging);
     47  }
     48 }
     49 
     50 const telemetryRuntimeIds = new Map();
     51 // Create an anonymous id that will allow to track all events related to a runtime without
     52 // leaking personal data related to this runtime.
     53 function getTelemetryRuntimeId(id) {
     54  if (!telemetryRuntimeIds.has(id)) {
     55    const randomId = (Math.random() * 100000) | 0;
     56    telemetryRuntimeIds.set(id, "runtime-" + randomId);
     57  }
     58  return telemetryRuntimeIds.get(id);
     59 }
     60 
     61 function getCurrentRuntimeIdForTelemetry(store) {
     62  const id = getCurrentRuntime(store.getState().runtimes).id;
     63  return getTelemetryRuntimeId(id);
     64 }
     65 
     66 function getRuntimeEventExtras(runtime) {
     67  const { extra, runtimeDetails } = runtime;
     68 
     69  // deviceName can be undefined for non-usb devices, but we should not log "undefined".
     70  const deviceName = extra?.deviceName || "";
     71  const runtimeShortName = runtime.type === RUNTIMES.USB ? runtime.name : "";
     72  const runtimeName = runtimeDetails?.info.name || "";
     73  return {
     74    connection_type: runtime.type,
     75    device_name: deviceName,
     76    runtime_id: getTelemetryRuntimeId(runtime.id),
     77    runtime_name: runtimeName || runtimeShortName,
     78  };
     79 }
     80 
     81 function onConnectRuntimeSuccess(action, store) {
     82  if (action.runtime.type === RUNTIMES.THIS_FIREFOX) {
     83    // Only record connection and disconnection events for remote runtimes.
     84    return;
     85  }
     86  // When we just connected to a runtime, the runtimeDetails are not in the store yet,
     87  // so we merge it here to retrieve the expected telemetry data.
     88  const storeRuntime = findRuntimeById(
     89    action.runtime.id,
     90    store.getState().runtimes
     91  );
     92  const runtime = Object.assign({}, storeRuntime, {
     93    runtimeDetails: action.runtime.runtimeDetails,
     94  });
     95  const extras = Object.assign({}, getRuntimeEventExtras(runtime), {
     96    runtime_os: action.runtime.runtimeDetails.info.os,
     97    runtime_version: action.runtime.runtimeDetails.info.version,
     98  });
     99  recordEvent("runtime_connected", extras);
    100 }
    101 
    102 function onDisconnectRuntimeSuccess(action, store) {
    103  const runtime = findRuntimeById(action.runtime.id, store.getState().runtimes);
    104  if (runtime.type === RUNTIMES.THIS_FIREFOX) {
    105    // Only record connection and disconnection events for remote runtimes.
    106    return;
    107  }
    108 
    109  recordEvent("runtime_disconnected", getRuntimeEventExtras(runtime));
    110 }
    111 
    112 function onRemoteRuntimesUpdated(action, store) {
    113  // Compare new runtimes with the existing runtimes to detect if runtimes, devices
    114  // have been added or removed.
    115  const newRuntimes = action.runtimes;
    116  const allRuntimes = getAllRuntimes(store.getState().runtimes);
    117  const oldRuntimes = allRuntimes.filter(r => r.type === action.runtimeType);
    118 
    119  // Check if all the old runtimes and devices are still available in the updated
    120  // array.
    121  for (const oldRuntime of oldRuntimes) {
    122    const runtimeRemoved = newRuntimes.every(r => r.id !== oldRuntime.id);
    123    if (runtimeRemoved && !oldRuntime.isUnplugged) {
    124      recordEvent("runtime_removed", getRuntimeEventExtras(oldRuntime));
    125    }
    126  }
    127 
    128  // Using device names as unique IDs is inaccurate. See Bug 1544582.
    129  const oldDeviceNames = new Set(oldRuntimes.map(r => r.extra.deviceName));
    130  for (const oldDeviceName of oldDeviceNames) {
    131    const newRuntime = newRuntimes.find(
    132      r => r.extra.deviceName === oldDeviceName
    133    );
    134    const oldRuntime = oldRuntimes.find(
    135      r => r.extra.deviceName === oldDeviceName
    136    );
    137    const isUnplugged = newRuntime?.isUnplugged && !oldRuntime.isUnplugged;
    138    if (oldDeviceName && (!newRuntime || isUnplugged)) {
    139      recordEvent("device_removed", {
    140        connection_type: action.runtimeType,
    141        device_name: oldDeviceName,
    142      });
    143    }
    144  }
    145 
    146  // Check if the new runtimes and devices were already available in the existing
    147  // array.
    148  for (const newRuntime of newRuntimes) {
    149    const runtimeAdded = oldRuntimes.every(r => r.id !== newRuntime.id);
    150    if (runtimeAdded && !newRuntime.isUnplugged) {
    151      recordEvent("runtime_added", getRuntimeEventExtras(newRuntime));
    152    }
    153  }
    154 
    155  // Using device names as unique IDs is inaccurate. See Bug 1544582.
    156  const newDeviceNames = new Set(newRuntimes.map(r => r.extra.deviceName));
    157  for (const newDeviceName of newDeviceNames) {
    158    const newRuntime = newRuntimes.find(
    159      r => r.extra.deviceName === newDeviceName
    160    );
    161    const oldRuntime = oldRuntimes.find(
    162      r => r.extra.deviceName === newDeviceName
    163    );
    164    const isPlugged = oldRuntime?.isUnplugged && !newRuntime.isUnplugged;
    165 
    166    if (newDeviceName && (!oldRuntime || isPlugged)) {
    167      recordEvent("device_added", {
    168        connection_type: action.runtimeType,
    169        device_name: newDeviceName,
    170      });
    171    }
    172  }
    173 }
    174 
    175 function recordConnectionAttempt(connectionId, runtimeId, status, store) {
    176  const runtime = findRuntimeById(runtimeId, store.getState().runtimes);
    177  if (runtime.type === RUNTIMES.THIS_FIREFOX) {
    178    // Only record connection_attempt events for remote runtimes.
    179    return;
    180  }
    181 
    182  recordEvent("connection_attempt", {
    183    connection_id: connectionId,
    184    connection_type: runtime.type,
    185    runtime_id: getTelemetryRuntimeId(runtimeId),
    186    status,
    187  });
    188 }
    189 
    190 /**
    191 * This middleware will record events to telemetry for some specific actions.
    192 */
    193 function eventRecordingMiddleware(store) {
    194  return next => action => {
    195    switch (action.type) {
    196      case CONNECT_RUNTIME_CANCEL:
    197        recordConnectionAttempt(
    198          action.connectionId,
    199          action.id,
    200          "cancelled",
    201          store
    202        );
    203        break;
    204      case CONNECT_RUNTIME_FAILURE:
    205        recordConnectionAttempt(
    206          action.connectionId,
    207          action.id,
    208          "failed",
    209          store
    210        );
    211        break;
    212      case CONNECT_RUNTIME_NOT_RESPONDING:
    213        recordConnectionAttempt(
    214          action.connectionId,
    215          action.id,
    216          "not responding",
    217          store
    218        );
    219        break;
    220      case CONNECT_RUNTIME_START:
    221        recordConnectionAttempt(action.connectionId, action.id, "start", store);
    222        break;
    223      case CONNECT_RUNTIME_SUCCESS:
    224        recordConnectionAttempt(
    225          action.connectionId,
    226          action.runtime.id,
    227          "success",
    228          store
    229        );
    230        onConnectRuntimeSuccess(action, store);
    231        break;
    232      case DISCONNECT_RUNTIME_SUCCESS:
    233        onDisconnectRuntimeSuccess(action, store);
    234        break;
    235      case REMOTE_RUNTIMES_UPDATED:
    236        onRemoteRuntimesUpdated(action, store);
    237        break;
    238      case SELECT_PAGE_SUCCESS:
    239        recordEvent("select_page", { page_type: action.page });
    240        break;
    241      case SHOW_PROFILER_DIALOG:
    242        recordEvent("show_profiler", {
    243          runtime_id: getCurrentRuntimeIdForTelemetry(store),
    244        });
    245        break;
    246      case TELEMETRY_RECORD: {
    247        const { method, details } = action;
    248        if (method) {
    249          recordEvent(method, details);
    250        } else {
    251          console.error(
    252            `[RECORD EVENT FAILED] ${action.type}: no "method" property`
    253          );
    254        }
    255        break;
    256      }
    257      case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
    258        recordEvent("update_conn_prompt", {
    259          prompt_enabled: `${action.connectionPromptEnabled}`,
    260          runtime_id: getCurrentRuntimeIdForTelemetry(store),
    261        });
    262        break;
    263    }
    264 
    265    return next(action);
    266  };
    267 }
    268 
    269 module.exports = eventRecordingMiddleware;