@samitouri / QOS-React-1 / commits / 5e83d9ab3b

feat[react-devtools]: add settings to global hook object (#30564)

Right now we are patching console 2 times: when hook is installed (before page is loaded) and when backend is connected. Because of this, even if user had `appendComponentStack` setting enabled, all emitted error and warning logs are not going to have component stacks appended. They also won't have component stacks appended retroactively when user opens browser DevTools (this is when frontend is initialized and connects to backend). This behavior adds potential race conditions with LogBox in React Native, and also unpredictable to the user, because in order to get component stacks logged you have to open browser DevTools, but by the time you do it, error or warning log was already emitted. To solve this, we are going to only patch console in the hook object, because it is guaranteed to load even before React. Settings are going to be synchronized with the hook via Bridge, and React DevTools Backend Host (React Native or browser extension shell) will be responsible for persisting these settings across the session, this is going to be implemented in a separate PR.

Ruslan Lesiutin committed Sep 18, 2024 at 17:37 UTC 5e83d9ab3b3f88853591dff43cd70ee4e5c90c5d
5 files changed +55 -26
packages/react-devtools-shared/src/backend/agent.js
+15 -24
@@ -37,11 +37,9 @@ import type {
37 RendererID,
38 RendererInterface,
39 ConsolePatchSettings,
40 + DevToolsHookSettings,
41 } from './types';
41 -import type {
42 - ComponentFilter,
43 - BrowserTheme,
44 -} from 'react-devtools-shared/src/frontend/types';
42 +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
43 import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils';
44
45 const debug = (methodName: string, ...args: Array<string>) => {
@@ -153,6 +151,7 @@ export default class Agent extends EventEmitter<{
151 drawTraceUpdates: [Array<HostInstance>],
152 disableTraceUpdates: [],
153 getIfHasUnsupportedRendererVersion: [],
154 + updateHookSettings: [DevToolsHookSettings],
155 }> {
156 _bridge: BackendBridge;
157 _isProfiling: boolean = false;
@@ -805,30 +804,22 @@ export default class Agent extends EventEmitter<{
804 }
805 };
806
808 - updateConsolePatchSettings: ({
809 - appendComponentStack: boolean,
810 - breakOnConsoleErrors: boolean,
811 - browserTheme: BrowserTheme,
812 - hideConsoleLogsInStrictMode: boolean,
813 - showInlineWarningsAndErrors: boolean,
814 - }) => void = ({
815 - appendComponentStack,
816 - breakOnConsoleErrors,
817 - showInlineWarningsAndErrors,
818 - hideConsoleLogsInStrictMode,
819 - browserTheme,
820 - }: ConsolePatchSettings) => {
807 + updateConsolePatchSettings: (
808 + settings: $ReadOnly<ConsolePatchSettings>,
809 + ) => void = settings => {
810 + // Propagate the settings, so Backend can subscribe to it and modify hook
811 + this.emit('updateHookSettings', {
812 + appendComponentStack: settings.appendComponentStack,
813 + breakOnConsoleErrors: settings.breakOnConsoleErrors,
814 + showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
815 + hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
816 + });
817 +
818 // If the frontend preferences have changed,
819 // or in the case of React Native- if the backend is just finding out the preferences-
820 // then reinstall the console overrides.
821 // It's safe to call `patchConsole` multiple times.
825 - patchConsole({
826 - appendComponentStack,
827 - breakOnConsoleErrors,
828 - showInlineWarningsAndErrors,
829 - hideConsoleLogsInStrictMode,
830 - browserTheme,
831 - });
822 + patchConsole(settings);
823 };
824
825 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
packages/react-devtools-shared/src/backend/console.js
+1 -1
@@ -135,7 +135,7 @@ export function patch({
135 showInlineWarningsAndErrors,
136 hideConsoleLogsInStrictMode,
137 browserTheme,
138 -}: ConsolePatchSettings): void {
138 +}: $ReadOnly<ConsolePatchSettings>): void {
139 // Settings may change after we've patched the console.
140 // Using a shared ref allows the patch function to read the latest values.
141 consoleSettingsRef.appendComponentStack = appendComponentStack;
packages/react-devtools-shared/src/backend/index.js
+4
@@ -83,6 +83,10 @@ export function initBackend(
83 agent.removeListener('shutdown', onAgentShutdown);
84 });
85
86 + agent.addListener('updateHookSettings', settings => {
87 + hook.settings = settings;
88 + });
89 +
90 return () => {
91 subs.forEach(fn => fn());
92 };
packages/react-devtools-shared/src/backend/types.js
+8
@@ -527,6 +527,7 @@ export type DevToolsHook = {
527 // Testing
528 dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void,
529
530 + settings?: DevToolsHookSettings,
531 ...
532 };
533
@@ -537,3 +538,10 @@ export type ConsolePatchSettings = {
538 hideConsoleLogsInStrictMode: boolean,
539 browserTheme: BrowserTheme,
540 };
541 +
542 +export type DevToolsHookSettings = {
543 + appendComponentStack: boolean,
544 + breakOnConsoleErrors: boolean,
545 + showInlineWarningsAndErrors: boolean,
546 + hideConsoleLogsInStrictMode: boolean,
547 +};
packages/react-devtools-shared/src/hook.js
+27 -1
@@ -15,6 +15,7 @@ import type {
15 RendererID,
16 RendererInterface,
17 DevToolsBackend,
18 + DevToolsHookSettings,
19 } from './backend/types';
20
21 import {
@@ -25,7 +26,12 @@ import attachRenderer from './attachRenderer';
26
27 declare var window: any;
28
28 -export function installHook(target: any): DevToolsHook | null {
29 +export function installHook(
30 + target: any,
31 + maybeSettingsOrSettingsPromise?:
32 + | DevToolsHookSettings
33 + | Promise<DevToolsHookSettings>,
34 +): DevToolsHook | null {
35 if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
36 return null;
37 }
@@ -566,6 +572,26 @@ export function installHook(target: any): DevToolsHook | null {
572 registerInternalModuleStop,
573 };
574
575 + if (maybeSettingsOrSettingsPromise == null) {
576 + // Set default settings
577 + hook.settings = {
578 + appendComponentStack: true,
579 + breakOnConsoleErrors: false,
580 + showInlineWarningsAndErrors: true,
581 + hideConsoleLogsInStrictMode: false,
582 + };
583 + } else {
584 + Promise.resolve(maybeSettingsOrSettingsPromise)
585 + .then(settings => {
586 + hook.settings = settings;
587 + })
588 + .catch(() => {
589 + targetConsole.error(
590 + "React DevTools failed to get Console Patching settings. Console won't be patched and some console features will not work.",
591 + );
592 + });
593 + }
594 +
595 if (__TEST__) {
596 hook.dangerous_setTargetConsoleForTesting =
597 dangerous_setTargetConsoleForTesting;