Repository: facebook/react
Pull request: [DevTools] Extension reports logged events when feature flag is enabled

## Summary

This commit implements an event logger for the DevTools extensions (`react-devtools-extension`), which will report events that are logged from the DevTools runtime to an iframe that knows how to process those events. Note that this will **only happen when the `enableLogger` feature flag is enabled**, which will only be enabled for internal (fb-only) builds of DevTools.

Note that for now, the `enableLogger` feature flag is fully disabled, so this commit should have no observable effect on DevTools after landing.

## How did you test this change?

- yarn flow
- yarn test
- yarn test-build-devtools
- manually enabled `enableLogger` feature flag and verified that events were correctly communicated to iframe that loads an internal endpoint.


Changed files:
filename: packages/react-devtools-extensions/src/main.js
patch: @@ -18,9 +18,9 @@ import {
   localStorageRemoveItem,
   localStorageSetItem,
 } from 'react-devtools-shared/src/storage';
-import {registerEventLogger} from 'react-devtools-shared/src/Logger';
 import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
 import {__DEBUG__} from 'react-devtools-shared/src/constants';
+import {registerExtensionsEventLogger} from './registerExtensionsEventLogger';
 
 const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
   'React::DevTools::supportsProfiling';
@@ -88,9 +88,7 @@ function createPanelIfReactLoaded() {
 
       const tabId = chrome.devtools.inspectedWindow.tabId;
 
-      registerEventLogger((event: LogEvent) => {
-        // TODO: hook up event logging
-      });
+      registerExtensionsEventLogger();
 
       function initBridgeAndStore() {
         const port = chrome.runtime.connect({

filename: packages/react-devtools-extensions/src/registerExtensionsEventLogger.js
patch: @@ -0,0 +1,69 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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
+ */
+
+import type {LogEvent} from 'react-devtools-shared/src/Logger';
+
+import {registerEventLogger} from 'react-devtools-shared/src/Logger';
+import {enableLogger} from 'react-devtools-feature-flags';
+
+let loggingIFrame = null;
+let missedEvents = [];
+function logEvent(event: LogEvent) {
+  if (enableLogger) {
+    if (loggingIFrame != null) {
+      loggingIFrame.contentWindow.postMessage(
+        {
+          source: 'react-devtools-logging',
+          event: event,
+          context: {
+            surface: 'extension',
+          },
+        },
+        '*',
+      );
+    } else {
+      missedEvents.push(event);
+    }
+  }
+}
+
+function handleLoggingIFrameLoaded(iframe) {
+  if (loggingIFrame != null) {
+    return;
+  }
+
+  loggingIFrame = iframe;
+  if (missedEvents.length > 0) {
+    missedEvents.forEach(logEvent);
+    missedEvents = [];
+  }
+}
+
+export function registerExtensionsEventLogger() {
+  // If logger is enabled, register a logger that captures logged events
+  // and render iframe where the logged events will be reported to
+  if (enableLogger) {
+    const loggingUrl = process.env.LOGGING_URL;
+    const body = document.body;
+    if (
+      typeof loggingUrl === 'string' &&
+      loggingUrl.length > 0 &&
+      body != null
+    ) {
+      registerEventLogger(logEvent);
+
+      const iframe = document.createElement('iframe');
+      iframe.src = loggingUrl;
+      iframe.onload = function(...args) {
+        handleLoggingIFrameLoaded(iframe);
+      };
+      body.appendChild(iframe);
+    }
+  }
+}

filename: packages/react-devtools-extensions/webpack.config.js
patch: @@ -32,6 +32,8 @@ const __DEV__ = NODE_ENV === 'development';
 
 const DEVTOOLS_VERSION = getVersionString();
 
+const LOGGING_URL = process.env.LOGGING_URL || null;
+
 const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
 
 const babelOptions = {
@@ -91,6 +93,7 @@ module.exports = {
       'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
       'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
       'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
+      'process.env.LOGGING_URL': `"${LOGGING_URL}"`,
       'process.env.NODE_ENV': `"${NODE_ENV}"`,
       'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
       'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,

filename: packages/react-devtools-shared/src/Logger.js
patch: @@ -10,11 +10,11 @@
 import {enableLogger} from 'react-devtools-feature-flags';
 
 type LoadHookNamesEvent = {|
-  +name: 'loadHookNames',
-  +displayName: string | null,
-  +numberOfHooks: number | null,
-  +durationMs: number,
-  +resolution: 'success' | 'error' | 'timeout' | 'unknown',
+  +event_name: 'loadHookNames',
+  +event_status: 'success' | 'error' | 'timeout' | 'unknown',
+  +duration_ms: number,
+  +inspected_element_display_name: string | null,
+  +inspected_element_number_of_hooks: number | null,
 |};
 
 // prettier-ignore
@@ -23,25 +23,27 @@ export type LogEvent =
 
 export type LogFunction = LogEvent => void;
 
-let loggers: Array<LogFunction> = [];
+let logFunctions: Array<LogFunction> = [];
 export const logEvent: LogFunction =
   enableLogger === true
     ? function logEvent(event: LogEvent): void {
-        loggers.forEach(log => {
+        logFunctions.forEach(log => {
           log(event);
         });
       }
     : function logEvent() {};
 
 export const registerEventLogger =
   enableLogger === true
-    ? function registerEventLogger(eventLogger: LogFunction): () => void {
+    ? function registerEventLogger(logFunction: LogFunction): () => void {
         if (enableLogger) {
-          loggers.push(eventLogger);
+          logFunctions.push(logFunction);
           return function unregisterEventLogger() {
-            loggers = loggers.filter(logger => logger !== eventLogger);
+            logFunctions = logFunctions.filter(log => log !== logFunction);
           };
         }
         return () => {};
       }
-    : function registerEventLogger() {};
+    : function registerEventLogger(logFunction: LogFunction) {
+        return () => {};
+      };

filename: packages/react-devtools-shared/src/hookNamesCache.js
patch: @@ -99,7 +99,7 @@ export function loadHookNames(
 
     let timeoutID;
     let didTimeout = false;
-    let resolution = 'unknown';
+    let status = 'unknown';
     let resolvedHookNames: HookNames | null = null;
 
     const wake = () => {
@@ -116,11 +116,11 @@ export function loadHookNames(
     const handleLoadComplete = (durationMs: number): void => {
       // Log duration for parsing hook names
       logEvent({
-        name: 'loadHookNames',
-        displayName: element.displayName,
-        numberOfHooks: resolvedHookNames?.size ?? null,
-        durationMs,
-        resolution,
+        event_name: 'loadHookNames',
+        event_status: status,
+        duration_ms: durationMs,
+        inspected_element_display_name: element.displayName,
+        inspected_element_number_of_hooks: resolvedHookNames?.size ?? null,
       });
     };
 
@@ -152,7 +152,7 @@ export function loadHookNames(
               notFoundRecord.value = null;
             }
 
-            resolution = 'success';
+            status = 'success';
             resolvedHookNames = hookNames;
             done();
             wake();
@@ -172,7 +172,7 @@ export function loadHookNames(
             thrownRecord.status = Rejected;
             thrownRecord.value = null;
 
-            resolution = 'error';
+            status = 'error';
             done();
             wake();
           },
@@ -192,7 +192,7 @@ export function loadHookNames(
           timedoutRecord.status = Rejected;
           timedoutRecord.value = null;
 
-          resolution = 'timeout';
+          status = 'timeout';
           done();
           wake();
         }, TIMEOUT);