diff options
Diffstat (limited to 'devtools/client/aboutdebugging/src/middleware')
8 files changed, 660 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js new file mode 100644 index 0000000000..ecd2d6d309 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js @@ -0,0 +1,62 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + UNWATCH_RUNTIME_START, + WATCH_RUNTIME_SUCCESS, +} = require("devtools/client/aboutdebugging/src/constants"); +const Actions = require("devtools/client/aboutdebugging/src/actions/index"); + +function debugTargetListenerMiddleware(store) { + const onExtensionsUpdated = () => { + store.dispatch(Actions.requestExtensions()); + }; + + const onTabsUpdated = () => { + store.dispatch(Actions.requestTabs()); + }; + + const onWorkersUpdated = () => { + store.dispatch(Actions.requestWorkers()); + }; + + return next => action => { + switch (action.type) { + case WATCH_RUNTIME_SUCCESS: { + const { runtime } = action; + const { clientWrapper } = runtime.runtimeDetails; + + // Tabs + clientWrapper.on("tabListChanged", onTabsUpdated); + + // Addons + clientWrapper.on("addonListChanged", onExtensionsUpdated); + + // Workers + clientWrapper.on("workersUpdated", onWorkersUpdated); + break; + } + case UNWATCH_RUNTIME_START: { + const { runtime } = action; + const { clientWrapper } = runtime.runtimeDetails; + + // Tabs + clientWrapper.off("tabListChanged", onTabsUpdated); + + // Addons + clientWrapper.off("addonListChanged", onExtensionsUpdated); + + // Workers + clientWrapper.off("workersUpdated", onWorkersUpdated); + break; + } + } + + return next(action); + }; +} + +module.exports = debugTargetListenerMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/error-logging.js b/devtools/client/aboutdebugging/src/middleware/error-logging.js new file mode 100644 index 0000000000..fd04f34b76 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/error-logging.js @@ -0,0 +1,35 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Error logging middleware that will forward all actions that contain an error property + * to the console. + */ +function errorLoggingMiddleware() { + return next => action => { + if (action.error) { + const { error } = action; + if (error.message) { + console.error(`[ACTION FAILED] ${action.type}: ${error.message}`); + } else if (typeof error === "string") { + // All failure actions should dispatch an error object instead of a message. + // We allow some flexibility to still provide some error logging. + console.error(`[ACTION FAILED] ${action.type}: ${error}`); + console.error( + `[ACTION FAILED] ${action.type} should dispatch the error object!` + ); + } + + if (error.stack) { + console.error(error.stack); + } + } + + return next(action); + }; +} + +module.exports = errorLoggingMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/event-recording.js b/devtools/client/aboutdebugging/src/middleware/event-recording.js new file mode 100644 index 0000000000..0070fc0d7d --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/event-recording.js @@ -0,0 +1,270 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Telemetry = require("devtools/client/shared/telemetry"); +loader.lazyGetter(this, "telemetry", () => new Telemetry()); +// This is a unique id that should be submitted with all about:debugging events. +loader.lazyGetter(this, "sessionId", () => + parseInt(telemetry.msSinceProcessStart(), 10) +); + +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("devtools/client/aboutdebugging/src/constants"); + +const { + findRuntimeById, + getAllRuntimes, + getCurrentRuntime, +} = require("devtools/client/aboutdebugging/src/modules/runtimes-state-helper"); + +function recordEvent(method, details) { + // Add the session id to the event details. + const eventDetails = Object.assign({}, details, { session_id: sessionId }); + telemetry.recordEvent(method, "aboutdebugging", null, eventDetails); + + // For close and open events, also ping the regular telemetry helpers used + // for all DevTools UIs. + if (method === "open_adbg") { + telemetry.toolOpened("aboutdebugging", sessionId, 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", sessionId, 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)); + } + } + + // Using device names as unique IDs is inaccurate. See Bug 1544582. + 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)); + } + } + + // Using device names as unique IDs is inaccurate. See Bug 1544582. + 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: 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; diff --git a/devtools/client/aboutdebugging/src/middleware/extension-component-data.js b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js new file mode 100644 index 0000000000..33f3121230 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js @@ -0,0 +1,80 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + DEBUG_TARGETS, + REQUEST_EXTENSIONS_SUCCESS, +} = require("devtools/client/aboutdebugging/src/constants"); + +const { + getExtensionUuid, + parseFileUri, +} = require("devtools/client/aboutdebugging/src/modules/extensions-helper"); + +/** + * This middleware converts extensions object that get from DevToolsClient.listAddons() + * to data which is used in DebugTargetItem. + */ +const extensionComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_EXTENSIONS_SUCCESS: { + action.installedExtensions = toComponentData(action.installedExtensions); + action.temporaryExtensions = toComponentData(action.temporaryExtensions); + break; + } + } + + return next(action); +}; + +function getFilePath(extension) { + // Only show file system paths, and only for temporarily installed add-ons. + if ( + !extension.temporarilyInstalled || + !extension.url || + !extension.url.startsWith("file://") + ) { + return null; + } + + return parseFileUri(extension.url); +} + +function toComponentData(extensions) { + return extensions.map(extension => { + const type = DEBUG_TARGETS.EXTENSION; + const { + actor, + iconDataURL, + iconURL, + id, + manifestURL, + name, + warnings, + } = extension; + const icon = + iconDataURL || + iconURL || + "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + const location = getFilePath(extension); + const uuid = getExtensionUuid(extension); + return { + name, + icon, + id, + type, + details: { + actor, + location, + manifestURL, + uuid, + warnings: warnings || [], + }, + }; + }); +} + +module.exports = extensionComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/moz.build b/devtools/client/aboutdebugging/src/middleware/moz.build new file mode 100644 index 0000000000..f50150f569 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/moz.build @@ -0,0 +1,13 @@ +# 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 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "debug-target-listener.js", + "error-logging.js", + "event-recording.js", + "extension-component-data.js", + "process-component-data.js", + "tab-component-data.js", + "worker-component-data.js", +) diff --git a/devtools/client/aboutdebugging/src/middleware/process-component-data.js b/devtools/client/aboutdebugging/src/middleware/process-component-data.js new file mode 100644 index 0000000000..72c9f67d8c --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/process-component-data.js @@ -0,0 +1,65 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Services = require("Services"); +const { l10n } = require("devtools/client/aboutdebugging/src/modules/l10n"); + +const { + DEBUG_TARGETS, + PREFERENCES, + REQUEST_PROCESSES_SUCCESS, +} = require("devtools/client/aboutdebugging/src/constants"); + +/** + * This middleware converts tabs object that get from DevToolsClient.listProcesses() to + * data which is used in DebugTargetItem. + */ +const processComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_PROCESSES_SUCCESS: { + const mainProcessComponentData = toMainProcessComponentData( + action.mainProcess + ); + action.processes = [mainProcessComponentData]; + break; + } + } + + return next(action); +}; + +function toMainProcessComponentData(process) { + const type = DEBUG_TARGETS.PROCESS; + const id = process.id; + const icon = "chrome://devtools/skin/images/aboutdebugging-process-icon.svg"; + + const isMultiProcessToolboxEnabled = Services.prefs.getBoolPref( + PREFERENCES.FISSION_BROWSER_TOOLBOX, + false + ); + + // For now, we assume there is only one process and this is the main process + // So the name and title are for a remote (multiprocess) browser toolbox. + const name = isMultiProcessToolboxEnabled + ? l10n.getString("about-debugging-multiprocess-toolbox-name") + : l10n.getString("about-debugging-main-process-name"); + + const description = isMultiProcessToolboxEnabled + ? l10n.getString("about-debugging-multiprocess-toolbox-description") + : l10n.getString("about-debugging-main-process-description2"); + + return { + name, + icon, + id, + type, + details: { + description, + }, + }; +} + +module.exports = processComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/tab-component-data.js b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js new file mode 100644 index 0000000000..fe9966f8cb --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js @@ -0,0 +1,51 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + DEBUG_TARGETS, + REQUEST_TABS_SUCCESS, +} = require("devtools/client/aboutdebugging/src/constants"); + +/** + * This middleware converts tabs object that get from DevToolsClient.listTabs() to data + * which is used in DebugTargetItem. + */ +const tabComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_TABS_SUCCESS: { + action.tabs = toComponentData(action.tabs); + break; + } + } + + return next(action); +}; + +function toComponentData(tabs) { + return tabs.map(tab => { + const type = DEBUG_TARGETS.TAB; + const id = tab.outerWindowID; + const icon = tab.favicon + ? `data:image/png;base64,${btoa( + String.fromCharCode.apply(String, tab.favicon) + )}` + : "chrome://devtools/skin/images/globe.svg"; + const name = tab.title || tab.url; + const { url, isZombieTab } = tab; + return { + name, + icon, + id, + type, + details: { + isZombieTab, + url, + }, + }; + }); +} + +module.exports = tabComponentDataMiddleware; diff --git a/devtools/client/aboutdebugging/src/middleware/worker-component-data.js b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js new file mode 100644 index 0000000000..8507c50160 --- /dev/null +++ b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js @@ -0,0 +1,84 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Ci } = require("chrome"); + +const { + DEBUG_TARGETS, + REQUEST_WORKERS_SUCCESS, + SERVICE_WORKER_FETCH_STATES, + SERVICE_WORKER_STATUSES, +} = require("devtools/client/aboutdebugging/src/constants"); + +/** + * This middleware converts workers object that get from DevToolsClient.listAllWorkers() + * to data which is used in DebugTargetItem. + */ +const workerComponentDataMiddleware = store => next => action => { + switch (action.type) { + case REQUEST_WORKERS_SUCCESS: { + action.otherWorkers = toComponentData(action.otherWorkers); + action.serviceWorkers = toComponentData(action.serviceWorkers, true); + action.sharedWorkers = toComponentData(action.sharedWorkers); + break; + } + } + + return next(action); +}; + +function getServiceWorkerStatus(worker) { + const isActive = worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED; + const isRunning = !!worker.workerDescriptorFront; + + if (isActive && isRunning) { + return SERVICE_WORKER_STATUSES.RUNNING; + } else if (isActive) { + return SERVICE_WORKER_STATUSES.STOPPED; + } + + // We cannot get service worker registrations unless the registration is in + // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we + // display a custom state "registering" for now. See Bug 1153292. + return SERVICE_WORKER_STATUSES.REGISTERING; +} + +function toComponentData(workers, isServiceWorker) { + return workers.map(worker => { + // Here `worker` is the worker object created by RootFront.listAllWorkers + const type = DEBUG_TARGETS.WORKER; + const icon = "chrome://devtools/skin/images/debugging-workers.svg"; + let { fetch } = worker; + const { id, name, registrationFront, scope, subscription } = worker; + + let pushServiceEndpoint = null; + let status = null; + + if (isServiceWorker) { + fetch = fetch + ? SERVICE_WORKER_FETCH_STATES.LISTENING + : SERVICE_WORKER_FETCH_STATES.NOT_LISTENING; + status = getServiceWorkerStatus(worker); + pushServiceEndpoint = subscription ? subscription.endpoint : null; + } + + return { + details: { + fetch, + pushServiceEndpoint, + registrationFront, + scope, + status, + }, + icon, + id, + name, + type, + }; + }); +} + +module.exports = workerComponentDataMiddleware; |