Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
"use strict";
const Telemetry = require("resource://devtools/client/shared/telemetry.js");
loader.lazyGetter(
this,
"telemetry",
() => new Telemetry({ useSessionId: true })
);
const {
CONNECT_RUNTIME_CANCEL,
CONNECT_RUNTIME_FAILURE,
CONNECT_RUNTIME_NOT_RESPONDING,
CONNECT_RUNTIME_START,
CONNECT_RUNTIME_SUCCESS,
DISCONNECT_RUNTIME_SUCCESS,
REMOTE_RUNTIMES_UPDATED,
RUNTIMES,
SELECT_PAGE_SUCCESS,
SHOW_PROFILER_DIALOG,
TELEMETRY_RECORD,
UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
} = require("resource://devtools/client/aboutdebugging/src/constants.js");
const {
findRuntimeById,
getAllRuntimes,
getCurrentRuntime,
} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
function recordEvent(method, details) {
telemetry.recordEvent(method, "aboutdebugging", null, details);
// For close and open events, also ping the regular telemetry helpers used
// for all DevTools UIs.
if (method === "open_adbg") {
telemetry.toolOpened("aboutdebugging", window.AboutDebugging);
} else if (method === "close_adbg") {
// XXX: Note that aboutdebugging has no histogram created for
// TIME_ACTIVE_SECOND, so calling toolClosed will not actually
// record anything.
telemetry.toolClosed("aboutdebugging", window.AboutDebugging);
}
}
const telemetryRuntimeIds = new Map();
// Create an anonymous id that will allow to track all events related to a runtime without
// leaking personal data related to this runtime.
function getTelemetryRuntimeId(id) {
if (!telemetryRuntimeIds.has(id)) {
const randomId = (Math.random() * 100000) | 0;
telemetryRuntimeIds.set(id, "runtime-" + randomId);
}
return telemetryRuntimeIds.get(id);
}
function getCurrentRuntimeIdForTelemetry(store) {
const id = getCurrentRuntime(store.getState().runtimes).id;
return getTelemetryRuntimeId(id);
}
function getRuntimeEventExtras(runtime) {
const { extra, runtimeDetails } = runtime;
// deviceName can be undefined for non-usb devices, but we should not log "undefined".
const deviceName = extra?.deviceName || "";
const runtimeShortName = runtime.type === RUNTIMES.USB ? runtime.name : "";
const runtimeName = runtimeDetails?.info.name || "";
return {
connection_type: runtime.type,
device_name: deviceName,
runtime_id: getTelemetryRuntimeId(runtime.id),
runtime_name: runtimeName || runtimeShortName,
};
}
function onConnectRuntimeSuccess(action, store) {
if (action.runtime.type === RUNTIMES.THIS_FIREFOX) {
// Only record connection and disconnection events for remote runtimes.
return;
}
// When we just connected to a runtime, the runtimeDetails are not in the store yet,
// so we merge it here to retrieve the expected telemetry data.
const storeRuntime = findRuntimeById(
action.runtime.id,
store.getState().runtimes
);
const runtime = Object.assign({}, storeRuntime, {
runtimeDetails: action.runtime.runtimeDetails,
});
const extras = Object.assign({}, getRuntimeEventExtras(runtime), {
runtime_os: action.runtime.runtimeDetails.info.os,
runtime_version: action.runtime.runtimeDetails.info.version,
});
recordEvent("runtime_connected", extras);
}
function onDisconnectRuntimeSuccess(action, store) {
const runtime = findRuntimeById(action.runtime.id, store.getState().runtimes);
if (runtime.type === RUNTIMES.THIS_FIREFOX) {
// Only record connection and disconnection events for remote runtimes.
return;
}
recordEvent("runtime_disconnected", getRuntimeEventExtras(runtime));
}
function onRemoteRuntimesUpdated(action, store) {
// Compare new runtimes with the existing runtimes to detect if runtimes, devices
// have been added or removed.
const newRuntimes = action.runtimes;
const allRuntimes = getAllRuntimes(store.getState().runtimes);
const oldRuntimes = allRuntimes.filter(r => r.type === action.runtimeType);
// Check if all the old runtimes and devices are still available in the updated
// array.
for (const oldRuntime of oldRuntimes) {
const runtimeRemoved = newRuntimes.every(r => r.id !== oldRuntime.id);
if (runtimeRemoved && !oldRuntime.isUnplugged) {
recordEvent("runtime_removed", getRuntimeEventExtras(oldRuntime));
}
}
const oldDeviceNames = new Set(oldRuntimes.map(r => r.extra.deviceName));
for (const oldDeviceName of oldDeviceNames) {
const newRuntime = newRuntimes.find(
r => r.extra.deviceName === oldDeviceName
);
const oldRuntime = oldRuntimes.find(
r => r.extra.deviceName === oldDeviceName
);
const isUnplugged = newRuntime?.isUnplugged && !oldRuntime.isUnplugged;
if (oldDeviceName && (!newRuntime || isUnplugged)) {
recordEvent("device_removed", {
connection_type: action.runtimeType,
device_name: oldDeviceName,
});
}
}
// Check if the new runtimes and devices were already available in the existing
// array.
for (const newRuntime of newRuntimes) {
const runtimeAdded = oldRuntimes.every(r => r.id !== newRuntime.id);
if (runtimeAdded && !newRuntime.isUnplugged) {
recordEvent("runtime_added", getRuntimeEventExtras(newRuntime));
}
}
const newDeviceNames = new Set(newRuntimes.map(r => r.extra.deviceName));
for (const newDeviceName of newDeviceNames) {
const newRuntime = newRuntimes.find(
r => r.extra.deviceName === newDeviceName
);
const oldRuntime = oldRuntimes.find(
r => r.extra.deviceName === newDeviceName
);
const isPlugged = oldRuntime?.isUnplugged && !newRuntime.isUnplugged;
if (newDeviceName && (!oldRuntime || isPlugged)) {
recordEvent("device_added", {
connection_type: action.runtimeType,
device_name: newDeviceName,
});
}
}
}
function recordConnectionAttempt(connectionId, runtimeId, status, store) {
const runtime = findRuntimeById(runtimeId, store.getState().runtimes);
if (runtime.type === RUNTIMES.THIS_FIREFOX) {
// Only record connection_attempt events for remote runtimes.
return;
}
recordEvent("connection_attempt", {
connection_id: connectionId,
connection_type: runtime.type,
runtime_id: getTelemetryRuntimeId(runtimeId),
status,
});
}
/**
* This middleware will record events to telemetry for some specific actions.
*/
function eventRecordingMiddleware(store) {
return next => action => {
switch (action.type) {
case CONNECT_RUNTIME_CANCEL:
recordConnectionAttempt(
action.connectionId,
action.id,
"cancelled",
store
);
break;
case CONNECT_RUNTIME_FAILURE:
recordConnectionAttempt(
action.connectionId,
action.id,
"failed",
store
);
break;
case CONNECT_RUNTIME_NOT_RESPONDING:
recordConnectionAttempt(
action.connectionId,
action.id,
"not responding",
store
);
break;
case CONNECT_RUNTIME_START:
recordConnectionAttempt(action.connectionId, action.id, "start", store);
break;
case CONNECT_RUNTIME_SUCCESS:
recordConnectionAttempt(
action.connectionId,
action.runtime.id,
"success",
store
);
onConnectRuntimeSuccess(action, store);
break;
case DISCONNECT_RUNTIME_SUCCESS:
onDisconnectRuntimeSuccess(action, store);
break;
case REMOTE_RUNTIMES_UPDATED:
onRemoteRuntimesUpdated(action, store);
break;
case SELECT_PAGE_SUCCESS:
recordEvent("select_page", { page_type: action.page });
break;
case SHOW_PROFILER_DIALOG:
recordEvent("show_profiler", {
runtime_id: getCurrentRuntimeIdForTelemetry(store),
});
break;
case TELEMETRY_RECORD:
const { method, details } = action;
if (method) {
recordEvent(method, details);
} else {
console.error(
`[RECORD EVENT FAILED] ${action.type}: no "method" property`
);
}
break;
case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
recordEvent("update_conn_prompt", {
prompt_enabled: `${action.connectionPromptEnabled}`,
runtime_id: getCurrentRuntimeIdForTelemetry(store),
});
break;
}
return next(action);
};
}
module.exports = eventRecordingMiddleware;