Skip to content

Commit ef8b6fa

Browse files
authored
[Flight] Don't double badge consoles that are replayed from a third party (#33685)
If a FlightClient runs inside a FlightServer like fetching from a third party and that logs, then we currently double badge them since we just add on another badge. The issue is that this might be unnecessarily noisy but we also transfer the original format of the current server into the second badge. This extracts our own badge and then adds the environment name as structured data which lets the client decide how to format it. Before: <img width="599" alt="Screenshot 2025-07-02 at 2 30 07 PM" src="https://github.com/user-attachments/assets/4bf26a29-b3a8-4024-8eb9-a3f90dbff97a" /> After: <img width="590" alt="Screenshot 2025-07-02 at 2 32 56 PM" src="https://github.com/user-attachments/assets/f06bbb6d-fbb1-4ae6-b0e3-775849fe3c53" />
1 parent 0b78161 commit ef8b6fa

22 files changed

+202
-3
lines changed

packages/react-client/src/ReactClientConsoleConfigBrowser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
// Keep in sync with ReactServerConsoleConfig
1011
const badgeFormat = '%c%s%c ';
1112
// Same badge styling as DevTools.
1213
const badgeStyle =

packages/react-client/src/ReactClientConsoleConfigPlain.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
// Keep in sync with ReactServerConsoleConfig
1011
const badgeFormat = '[%s] ';
1112
const pad = ' ';
1213

packages/react-client/src/ReactClientConsoleConfigServer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
// Keep in sync with ReactServerConsoleConfig
1011
// This flips color using ANSI, then sets a color styling, then resets.
1112
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
1213
// Same badge styling as DevTools.

packages/react-server/src/ReactFlightServer.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
parseStackTrace,
9797
supportsComponentStorage,
9898
componentStorage,
99+
unbadgeConsole,
99100
} from './ReactFlightServerConfig';
100101

101102
import {
@@ -275,7 +276,15 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
275276
);
276277
request.pendingChunks++;
277278
const owner: null | ReactComponentInfo = resolveOwner();
278-
emitConsoleChunk(request, methodName, owner, stack, arguments);
279+
const args = Array.from(arguments);
280+
// Extract the env if this is a console log that was replayed from another env.
281+
let env = unbadgeConsole(methodName, args);
282+
if (env === null) {
283+
// Otherwise add the current environment.
284+
env = (0, request.environmentName)();
285+
}
286+
287+
emitConsoleChunk(request, methodName, owner, env, stack, args);
279288
}
280289
// $FlowFixMe[prop-missing]
281290
return originalMethod.apply(this, arguments);
@@ -4711,6 +4720,7 @@ function emitConsoleChunk(
47114720
request: Request,
47124721
methodName: string,
47134722
owner: null | ReactComponentInfo,
4723+
env: string,
47144724
stackTrace: ReactStackTrace,
47154725
args: Array<any>,
47164726
): void {
@@ -4727,8 +4737,6 @@ function emitConsoleChunk(
47274737
outlineComponentInfo(request, owner);
47284738
}
47294739

4730-
// TODO: Don't double badge if this log came from another Flight Client.
4731-
const env = (0, request.environmentName)();
47324740
const payload = [methodName, stackTrace, owner, env];
47334741
// $FlowFixMe[method-unbinding]
47344742
payload.push.apply(payload, args);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// Keep in sync with ReactClientConsoleConfig
11+
const badgeFormat = '%c%s%c ';
12+
// Same badge styling as DevTools.
13+
const badgeStyle =
14+
// We use a fixed background if light-dark is not supported, otherwise
15+
// we use a transparent background.
16+
'background: #e6e6e6;' +
17+
'background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));' +
18+
'color: #000000;' +
19+
'color: light-dark(#000000, #ffffff);' +
20+
'border-radius: 2px';
21+
22+
const padLength = 1;
23+
24+
// This mutates the args to remove any badges that was added by a FlightClient and
25+
// returns the name in the badge. This is used when a FlightClient replays inside
26+
// a FlightServer and we capture those replays.
27+
export function unbadgeConsole(
28+
methodName: string,
29+
args: Array<any>,
30+
): null | string {
31+
let offset = 0;
32+
switch (methodName) {
33+
case 'dir':
34+
case 'dirxml':
35+
case 'groupEnd':
36+
case 'table': {
37+
// These methods cannot be colorized because they don't take a formatting string.
38+
// So we wouldn't have added any badge in the first place.
39+
// $FlowFixMe
40+
return null;
41+
}
42+
case 'assert': {
43+
// assert takes formatting options as the second argument.
44+
offset = 1;
45+
}
46+
}
47+
const format = args[offset];
48+
const style = args[offset + 1];
49+
const badge = args[offset + 2];
50+
if (
51+
typeof format === 'string' &&
52+
format.startsWith(badgeFormat) &&
53+
style === badgeStyle &&
54+
typeof badge === 'string'
55+
) {
56+
// Remove our badging from the arguments.
57+
args.splice(offset, 4, format.slice(badgeFormat.length));
58+
return badge.slice(padLength, badge.length - padLength);
59+
}
60+
return null;
61+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// Keep in sync with ReactClientConsoleConfig
11+
const badgeFormat = '[%s] ';
12+
const padLength = 1;
13+
const pad = ' ';
14+
15+
// This mutates the args to remove any badges that was added by a FlightClient and
16+
// returns the name in the badge. This is used when a FlightClient replays inside
17+
// a FlightServer and we capture those replays.
18+
export function unbadgeConsole(
19+
methodName: string,
20+
args: Array<any>,
21+
): null | string {
22+
let offset = 0;
23+
switch (methodName) {
24+
case 'dir':
25+
case 'dirxml':
26+
case 'groupEnd':
27+
case 'table': {
28+
// These methods cannot be colorized because they don't take a formatting string.
29+
// So we wouldn't have added any badge in the first place.
30+
// $FlowFixMe
31+
return null;
32+
}
33+
case 'assert': {
34+
// assert takes formatting options as the second argument.
35+
offset = 1;
36+
}
37+
}
38+
const format = args[offset];
39+
const badge = args[offset + 1];
40+
if (
41+
typeof format === 'string' &&
42+
format.startsWith(badgeFormat) &&
43+
typeof badge === 'string' &&
44+
badge.startsWith(pad) &&
45+
badge.endsWith(pad)
46+
) {
47+
// Remove our badging from the arguments.
48+
args.splice(offset, 2, format.slice(badgeFormat.length));
49+
return badge.slice(padLength, badge.length - padLength);
50+
}
51+
return null;
52+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// Keep in sync with ReactClientConsoleConfig
11+
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
12+
// Same badge styling as DevTools.
13+
const badgeStyle =
14+
// We use a fixed background if light-dark is not supported, otherwise
15+
// we use a transparent background.
16+
'background: #e6e6e6;' +
17+
'background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));' +
18+
'color: #000000;' +
19+
'color: light-dark(#000000, #ffffff);' +
20+
'border-radius: 2px';
21+
const padLength = 1;
22+
23+
// This mutates the args to remove any badges that was added by a FlightClient and
24+
// returns the name in the badge. This is used when a FlightClient replays inside
25+
// a FlightServer and we capture those replays.
26+
export function unbadgeConsole(
27+
methodName: string,
28+
args: Array<any>,
29+
): null | string {
30+
let offset = 0;
31+
switch (methodName) {
32+
case 'dir':
33+
case 'dirxml':
34+
case 'groupEnd':
35+
case 'table': {
36+
// These methods cannot be colorized because they don't take a formatting string.
37+
// So we wouldn't have added any badge in the first place.
38+
// $FlowFixMe
39+
return null;
40+
}
41+
case 'assert': {
42+
// assert takes formatting options as the second argument.
43+
offset = 1;
44+
}
45+
}
46+
const format = args[offset];
47+
const style = args[offset + 1];
48+
const badge = args[offset + 2];
49+
if (
50+
typeof format === 'string' &&
51+
format.startsWith(badgeFormat) &&
52+
style === badgeStyle &&
53+
typeof badge === 'string'
54+
) {
55+
// Remove our badging from the arguments.
56+
args.splice(offset, 4, format.slice(badgeFormat.length));
57+
return badge.slice(padLength, badge.length - padLength);
58+
}
59+
return null;
60+
}

packages/react-server/src/forks/ReactFlightServerConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from '../ReactFlightServerConfigBundlerCustom';
1515
export * from '../ReactFlightServerConfigDebugNoop';
1616

1717
export * from '../ReactFlightStackConfigV8';
18+
export * from '../ReactServerConsoleConfigPlain';
1819

1920
export type Hints = any;
2021
export type HintCode = any;

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2323
export * from '../ReactFlightServerConfigDebugNoop';
2424

2525
export * from '../ReactFlightStackConfigV8';
26+
export * from '../ReactServerConsoleConfigBrowser';

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-parcel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2323
export * from '../ReactFlightServerConfigDebugNoop';
2424

2525
export * from '../ReactFlightStackConfigV8';
26+
export * from '../ReactServerConsoleConfigBrowser';

0 commit comments

Comments
 (0)