Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DevTools] Implement getComponentStack and onErrorOrWarning for replayed Flight logs #30930

Merged
merged 3 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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
*/

// This is a DevTools fork of ReactComponentInfoStack.
// This fork enables DevTools to use the same "native" component stack format,
// while still maintaining support for multiple renderer versions
// (which use different values for ReactTypeOfWork).

import type {ReactComponentInfo} from 'shared/ReactTypes';

import {describeBuiltInComponentFrame} from '../shared/DevToolsComponentStackFrame';

import {formatOwnerStack} from '../shared/DevToolsOwnerStack';

export function getOwnerStackByComponentInfoInDev(
componentInfo: ReactComponentInfo,
): string {
try {
let info = '';

// The owner stack of the current component will be where it was created, i.e. inside its owner.
// There's no actual name of the currently executing component. Instead, that is available
// on the regular stack that's currently executing. However, if there is no owner at all, then
// there's no stack frame so we add the name of the root component to the stack to know which
// component is currently executing.
if (!componentInfo.owner && typeof componentInfo.name === 'string') {
return describeBuiltInComponentFrame(componentInfo.name);
}

let owner: void | null | ReactComponentInfo = componentInfo;

while (owner) {
const ownerStack: ?Error = owner.debugStack;
if (ownerStack != null) {
// Server Component
owner = owner.owner;
if (owner) {
// TODO: Should we stash this somewhere for caching purposes?
info += '\n' + formatOwnerStack(ownerStack);
}
} else {
break;
}
}
return info;
} catch (x) {
return '\nError generating stack: ' + x.message + '\n' + x.stack;
}
}
127 changes: 126 additions & 1 deletion packages/react-devtools-shared/src/backend/flight/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,146 @@
* @flow
*/

import type {ReactComponentInfo} from 'shared/ReactTypes';

import type {DevToolsHook, ReactRenderer, RendererInterface} from '../types';

import {getOwnerStackByComponentInfoInDev} from './DevToolsComponentInfoStack';

import {formatOwnerStack} from '../shared/DevToolsOwnerStack';

import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';

import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils';

import {
patchConsoleUsingWindowValues,
registerRenderer as registerRendererWithConsole,
} from '../console';

function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
// If this ReactComponentInfo supports native console.createTask then we are already running
// inside a native async stack trace if it's active - meaning the DevTools is open.
// Ideally we'd detect if this task was created while the DevTools was open or not.
return !!componentInfo.debugTask;
}

export function attach(
hook: DevToolsHook,
rendererID: number,
renderer: ReactRenderer,
global: Object,
): RendererInterface {
const {getCurrentComponentInfo} = renderer;

function getComponentStack(
topFrame: Error,
): null | {enableOwnerStacks: boolean, componentStack: string} {
if (getCurrentComponentInfo === undefined) {
// Expected this to be part of the renderer. Ignore.
return null;
}
const current = getCurrentComponentInfo();
if (current === null) {
// Outside of our render scope.
return null;
}

if (supportsConsoleTasks(current)) {
// This will be handled natively by console.createTask. No need for
// DevTools to add it.
return null;
}

const enableOwnerStacks = current.debugStack != null;
let componentStack = '';
if (enableOwnerStacks) {
// Prefix the owner stack with the current stack. I.e. what called
// console.error. While this will also be part of the native stack,
// it is hidden and not presented alongside this argument so we print
// them all together.
const topStackFrames = formatOwnerStack(topFrame);
if (topStackFrames) {
componentStack += '\n' + topStackFrames;
}
componentStack += getOwnerStackByComponentInfoInDev(current);
}
return {enableOwnerStacks, componentStack};
}

// Called when an error or warning is logged during render, commit, or passive (including unmount functions).
function onErrorOrWarning(
type: 'error' | 'warn',
args: $ReadOnlyArray<any>,
): void {
if (getCurrentComponentInfo === undefined) {
// Expected this to be part of the renderer. Ignore.
return;
}
const componentInfo = getCurrentComponentInfo();
if (componentInfo === null) {
// Outside of our render scope.
return;
}

if (
args.length > 3 &&
typeof args[0] === 'string' &&
args[0].startsWith('%c%s%c ') &&
typeof args[1] === 'string' &&
typeof args[2] === 'string' &&
typeof args[3] === 'string'
) {
// This looks like the badge we prefixed to the log. Our UI doesn't support formatted logs.
// We remove the formatting. If the environment of the log is the same as the environment of
// the component (the common case) we remove the badge completely otherwise leave it plain
const format = args[0].slice(7);
const env = args[2].trim();
args = args.slice(4);
if (env !== componentInfo.env) {
args.unshift('[' + env + '] ' + format);
} else {
args.unshift(format);
}
}

// We can't really use this message as a unique key, since we can't distinguish
// different objects in this implementation. We have to delegate displaying of the objects
// to the environment, the browser console, for example, so this is why this should be kept
// as an array of arguments, instead of the plain string.
// [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,
// even if objects are different
const message = formatConsoleArgumentsToSingleString(...args);

// Track the warning/error for later.
let componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
if (componentLogsEntry === undefined) {
componentLogsEntry = {
errors: new Map(),
errorsCount: 0,
warnings: new Map(),
warningsCount: 0,
};
componentInfoToComponentLogsMap.set(componentInfo, componentLogsEntry);
}

const messageMap =
type === 'error'
? componentLogsEntry.errors
: componentLogsEntry.warnings;
const count = messageMap.get(message) || 0;
messageMap.set(message, count + 1);
if (type === 'error') {
componentLogsEntry.errorsCount++;
} else {
componentLogsEntry.warningsCount++;
}

// The changes will be flushed later when we commit this tree to Fiber.
}

patchConsoleUsingWindowValues();
registerRendererWithConsole(); // TODO: Fill in the impl
registerRendererWithConsole(onErrorOrWarning, getComponentStack);

return {
cleanup() {},
Expand Down
8 changes: 4 additions & 4 deletions packages/react-devtools-shared/src/backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ export function initBackend(

// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
if (rendererInterface == null) {
if (
if (typeof renderer.getCurrentComponentInfo === 'function') {
// react-flight/client
rendererInterface = attachFlight(hook, id, renderer, global);
} else if (
// v16-19
typeof renderer.findFiberByHostInstance === 'function' ||
// v16.8+
renderer.currentDispatcherRef != null
) {
// react-reconciler v16+
rendererInterface = attachFiber(hook, id, renderer, global);
} else if (typeof renderer.getCurrentComponentInfo === 'function') {
// react-flight/client
rendererInterface = attachFlight(hook, id, renderer, global);
} else if (renderer.ComponentTree) {
// react-dom v15
rendererInterface = attachLegacy(hook, id, renderer, global);
Expand Down
Loading