@samitouri / QOS-React-1 / commits / c498bfce8b

[devtools] Allow inspecting cause, name, message, stack of Errors in props (#33023)

Sebastian "Sebbie" Silbermann committed Apr 26, 2025 at 07:20 UTC c498bfce8b9baa3dd21bd0d5124eb3a4549886f1
4 files changed +100 -3
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+2 -2
@@ -906,8 +906,8 @@ describe('InspectedElement', () => {
906 },
907 "usedRejectedPromise": {
908 "reason": Dehydrated {
909 - "preview_short": Error,
910 - "preview_long": Error,
909 + "preview_short": Error: test-error-do-not-surface,
910 + "preview_long": Error: test-error-do-not-surface,
911 },
912 },
913 }
packages/react-devtools-shared/src/hydration.js
+63 -1
@@ -397,7 +397,7 @@ export function dehydrate(
397 return object;
398 }
399
400 - case 'class_instance':
400 + case 'class_instance': {
401 isPathAllowedCheck = isPathAllowed(path);
402
403 if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
@@ -433,7 +433,69 @@ export function dehydrate(
433 unserializable.push(path);
434
435 return value;
436 + }
437 + case 'error': {
438 + isPathAllowedCheck = isPathAllowed(path);
439 +
440 + if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
441 + return createDehydrated(type, true, data, cleaned, path);
442 + }
443 +
444 + const value: Unserializable = {
445 + unserializable: true,
446 + type,
447 + readonly: true,
448 + preview_short: formatDataForPreview(data, false),
449 + preview_long: formatDataForPreview(data, true),
450 + name: data.name,
451 + };
452 +
453 + // name, message, stack and cause are not enumerable yet still interesting.
454 + value.message = dehydrate(
455 + data.message,
456 + cleaned,
457 + unserializable,
458 + path.concat(['message']),
459 + isPathAllowed,
460 + isPathAllowedCheck ? 1 : level + 1,
461 + );
462 + value.stack = dehydrate(
463 + data.stack,
464 + cleaned,
465 + unserializable,
466 + path.concat(['stack']),
467 + isPathAllowed,
468 + isPathAllowedCheck ? 1 : level + 1,
469 + );
470 +
471 + if ('cause' in data) {
472 + value.cause = dehydrate(
473 + data.cause,
474 + cleaned,
475 + unserializable,
476 + path.concat(['cause']),
477 + isPathAllowed,
478 + isPathAllowedCheck ? 1 : level + 1,
479 + );
480 + }
481 +
482 + getAllEnumerableKeys(data).forEach(key => {
483 + const keyAsString = key.toString();
484
485 + value[keyAsString] = dehydrate(
486 + data[key],
487 + cleaned,
488 + unserializable,
489 + path.concat([keyAsString]),
490 + isPathAllowed,
491 + isPathAllowedCheck ? 1 : level + 1,
492 + );
493 + });
494 +
495 + unserializable.push(path);
496 +
497 + return value;
498 + }
499 case 'infinity':
500 case 'nan':
501 case 'undefined':
packages/react-devtools-shared/src/utils.js
+20
@@ -554,6 +554,7 @@ export type DataType =
554 | 'class_instance'
555 | 'data_view'
556 | 'date'
557 + | 'error'
558 | 'function'
559 | 'html_all_collection'
560 | 'html_element'
@@ -573,6 +574,21 @@ export type DataType =
574 | 'undefined'
575 | 'unknown';
576
577 +function isError(data: Object): boolean {
578 + // If it doesn't event look like an error, it won't be an actual error.
579 + if ('name' in data && 'message' in data) {
580 + while (data) {
581 + // $FlowFixMe[method-unbinding]
582 + if (Object.prototype.toString.call(data) === '[object Error]') {
583 + return true;
584 + }
585 + data = Object.getPrototypeOf(data);
586 + }
587 + }
588 +
589 + return false;
590 +}
591 +
592 /**
593 * Get a enhanced/artificial type string based on the object instance
594 */
@@ -634,6 +650,8 @@ export function getDataType(data: Object): DataType {
650 return 'regexp';
651 } else if (typeof data.then === 'function') {
652 return 'thenable';
653 + } else if (isError(data)) {
654 + return 'error';
655 } else {
656 // $FlowFixMe[method-unbinding]
657 const toStringValue = Object.prototype.toString.call(data);
@@ -996,6 +1014,8 @@ export function formatDataForPreview(
1014 } else {
1015 return '{…}';
1016 }
1017 + case 'error':
1018 + return truncateForDisplay(String(data));
1019 case 'boolean':
1020 case 'number':
1021 case 'infinity':
packages/react-devtools-shell/src/app/Hydration/index.js
+15
@@ -130,6 +130,14 @@ const usedRejectedPromise = Promise.reject(
130 new Error('test-error-do-not-surface'),
131 );
132
133 +class DigestError extends Error {
134 + digest: string;
135 + constructor(message: string, options: any, digest: string) {
136 + super(message, options);
137 + this.digest = digest;
138 + }
139 +}
140 +
141 export default function Hydration(): React.Node {
142 return (
143 <Fragment>
@@ -149,6 +157,13 @@ export default function Hydration(): React.Node {
157 usedFulfilledRichPromise={usedFulfilledRichPromise}
158 usedPendingPromise={usedPendingPromise}
159 usedRejectedPromise={usedRejectedPromise}
160 + // eslint-disable-next-line react-internal/prod-error-codes
161 + error={new Error('test')}
162 + // eslint-disable-next-line react-internal/prod-error-codes
163 + errorWithCause={new Error('one', {cause: new TypeError('two')})}
164 + errorWithDigest={new DigestError('test', {}, 'some-digest')}
165 + // $FlowFixMe[cannot-resolve-name] Flow doesn't know about DOMException
166 + domexception={new DOMException('test')}
167 />
168 <DeepHooks />
169 </Fragment>