diff --git a/packages/dev-middleware/src/createDevMiddleware.js b/packages/dev-middleware/src/createDevMiddleware.js index ce93856a5188c1..4666819d145cc9 100644 --- a/packages/dev-middleware/src/createDevMiddleware.js +++ b/packages/dev-middleware/src/createDevMiddleware.js @@ -10,6 +10,7 @@ */ import type {NextHandleFunction} from 'connect'; +import type {EventReporter} from './types/EventReporter'; import type {Logger} from './types/Logger'; import connect from 'connect'; @@ -21,6 +22,7 @@ type Options = $ReadOnly<{ port: number, projectRoot: string, logger?: Logger, + unstable_eventReporter?: EventReporter, }>; type DevMiddlewareAPI = $ReadOnly<{ @@ -33,11 +35,21 @@ export default function createDevMiddleware({ port, projectRoot, logger, + unstable_eventReporter, }: Options): DevMiddlewareAPI { - const inspectorProxy = new InspectorProxy(projectRoot); + const inspectorProxy = new InspectorProxy( + projectRoot, + unstable_eventReporter, + ); const middleware = connect() - .use('/open-debugger', openDebuggerMiddleware({logger})) + .use( + '/open-debugger', + openDebuggerMiddleware({ + logger, + eventReporter: unstable_eventReporter, + }), + ) .use((...args) => inspectorProxy.processRequest(...args)); return { diff --git a/packages/dev-middleware/src/index.flow.js b/packages/dev-middleware/src/index.flow.js index 4d7b092a1431ec..63630f81ca48b4 100644 --- a/packages/dev-middleware/src/index.flow.js +++ b/packages/dev-middleware/src/index.flow.js @@ -10,3 +10,5 @@ */ export {default as createDevMiddleware} from './createDevMiddleware'; + +export type {EventReporter, ReportableEvent} from './types/EventReporter'; diff --git a/packages/dev-middleware/src/inspector-proxy/Device.js b/packages/dev-middleware/src/inspector-proxy/Device.js index 3838495207012f..253aa573790384 100644 --- a/packages/dev-middleware/src/inspector-proxy/Device.js +++ b/packages/dev-middleware/src/inspector-proxy/Device.js @@ -24,6 +24,7 @@ import * as fs from 'fs'; import * as path from 'path'; import fetch from 'node-fetch'; import WS from 'ws'; +import type {EventReporter} from '../types/EventReporter'; const debug = require('debug')('Metro:InspectorProxy'); @@ -89,12 +90,15 @@ export default class Device { // Root of the project used for relative to absolute source path conversion. _projectRoot: string; + _eventReporter: ?EventReporter; + constructor( id: string, name: string, app: string, socket: WS, projectRoot: string, + eventReporter: ?EventReporter, ) { this._id = id; this._name = name; @@ -102,6 +106,7 @@ export default class Device { this._pages = []; this._deviceSocket = socket; this._projectRoot = projectRoot; + this._eventReporter = eventReporter; // $FlowFixMe[incompatible-call] this._deviceSocket.on('message', (message: string) => { @@ -158,6 +163,10 @@ export default class Device { // 2. Forwards all messages from the debugger to device as wrappedEvent // 3. Sends disconnect event to device when debugger connection socket closes. handleDebuggerConnection(socket: WS, pageId: string) { + this._eventReporter?.logEvent({ + type: 'connect_debugger_frontend', + status: 'success', + }); // Disconnect current debugger if we already have debugger connected. if (this._debuggerConnection) { this._debuggerConnection.socket.close(); diff --git a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js index a9b62a69c6eb7e..bf831fe019dec7 100644 --- a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js +++ b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js @@ -15,6 +15,7 @@ import type { Page, PageDescription, } from './types'; +import type {EventReporter} from '../types/EventReporter'; import type {IncomingMessage, ServerResponse} from 'http'; import Device from './Device'; @@ -49,9 +50,12 @@ export default class InspectorProxy { // by debugger to know where to connect. _serverBaseUrl: string = ''; - constructor(projectRoot: string) { + _eventReporter: ?EventReporter; + + constructor(projectRoot: string, eventReporter: ?EventReporter) { this._projectRoot = projectRoot; this._devices = new Map(); + this._eventReporter = eventReporter; } // Process HTTP request sent to server. We only respond to 2 HTTP requests: @@ -169,6 +173,7 @@ export default class InspectorProxy { appName, socket, this._projectRoot, + this._eventReporter, ); if (oldDevice) { @@ -223,6 +228,11 @@ export default class InspectorProxy { } catch (e) { console.error(e); socket.close(INTERNAL_ERROR_CODE, e?.toString() ?? 'Unknown error'); + this._eventReporter?.logEvent({ + type: 'connect_debugger_frontend', + status: 'error', + error: e, + }); } }); return wss; diff --git a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js index 89f0fef9efd96c..b8020806b1aa38 100644 --- a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js +++ b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local * @format * @oncall react_native */ @@ -12,6 +12,7 @@ import type {LaunchedChrome} from 'chrome-launcher'; import type {NextHandleFunction} from 'connect'; import type {IncomingMessage, ServerResponse} from 'http'; +import type {EventReporter} from '../types/EventReporter'; import type {Logger} from '../types/Logger'; import url from 'url'; @@ -23,6 +24,7 @@ const debuggerInstances = new Map(); type Options = $ReadOnly<{ logger?: Logger, + eventReporter?: EventReporter, }>; /** @@ -34,6 +36,7 @@ type Options = $ReadOnly<{ * @see https://chromedevtools.github.io/devtools-protocol/ */ export default function openDebuggerMiddleware({ + eventReporter, logger, }: Options): NextHandleFunction { return async ( @@ -48,6 +51,11 @@ export default function openDebuggerMiddleware({ if (typeof appId !== 'string') { res.writeHead(400); res.end(); + eventReporter?.logEvent({ + type: 'launch_debugger_frontend', + status: 'coded_error', + errorCode: 'MISSING_APP_ID', + }); return; } @@ -60,6 +68,11 @@ export default function openDebuggerMiddleware({ logger?.warn( 'No compatible apps connected. JavaScript debugging can only be used with the Hermes engine.', ); + eventReporter?.logEvent({ + type: 'launch_debugger_frontend', + status: 'coded_error', + errorCode: 'NO_APPS_FOUND', + }); return; } @@ -71,6 +84,11 @@ export default function openDebuggerMiddleware({ await launchChromeDevTools(target.webSocketDebuggerUrl), ); res.end(); + eventReporter?.logEvent({ + type: 'launch_debugger_frontend', + status: 'success', + appId, + }); return; } catch (e) { logger?.error( @@ -78,6 +96,11 @@ export default function openDebuggerMiddleware({ ); res.writeHead(500); res.end(); + eventReporter?.logEvent({ + type: 'launch_debugger_frontend', + status: 'error', + error: e, + }); return; } } diff --git a/packages/dev-middleware/src/types/EventReporter.js b/packages/dev-middleware/src/types/EventReporter.js new file mode 100644 index 00000000000000..ef1757a4041caa --- /dev/null +++ b/packages/dev-middleware/src/types/EventReporter.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +type SuccessResult = { + status: 'success', + ...Props, +}; + +type ErrorResult = { + status: 'error', + error: ErrorT, +}; + +type CodedErrorResult = { + status: 'coded_error', + errorCode: ErrorCode, + errorDetails?: string, +}; + +export type ReportableEvent = + | { + type: 'launch_debugger_frontend', + ... + | SuccessResult<{appId: string}> + | ErrorResult + | CodedErrorResult<'MISSING_APP_ID' | 'NO_APPS_FOUND'>, + } + | { + type: 'connect_debugger_frontend', + ...SuccessResult | ErrorResult, + }; + +/** + * A simple interface for logging events, to be implemented by integrators of + * `dev-middleware`. + * + * This is an unstable API with no semver guarantees. + */ +export interface EventReporter { + logEvent(event: ReportableEvent): void; +}