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;