messageFormatter.tsx 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import {memo} from 'react';
  2. import isObject from 'lodash/isObject';
  3. import {OnExpandCallback} from 'sentry/components/objectInspector';
  4. import {objectIsEmpty} from 'sentry/utils';
  5. import type {BreadcrumbFrame, ConsoleFrame} from 'sentry/utils/replays/types';
  6. import {isConsoleFrame} from 'sentry/utils/replays/types';
  7. import Format from 'sentry/views/replays/detail/console/format';
  8. interface Props {
  9. frame: BreadcrumbFrame;
  10. expandPaths?: string[];
  11. onExpand?: OnExpandCallback;
  12. }
  13. // There is a special case where `console.error()` is called with an Error object.
  14. // The SDK uses the Error's `message` property as the breadcrumb message, but we lose the Error type,
  15. // resulting in an empty object in the breadcrumb arguments.
  16. //
  17. // In this special case, we re-create the error object
  18. function isSerializedError(frame: ConsoleFrame) {
  19. const args = frame.data.arguments;
  20. return (
  21. frame.message &&
  22. typeof frame.message === 'string' &&
  23. Array.isArray(args) &&
  24. args.length <= 2 &&
  25. isObject(args[0]) &&
  26. objectIsEmpty(args[0])
  27. );
  28. }
  29. /**
  30. * Attempt to emulate the browser console as much as possible
  31. */
  32. function UnmemoizedMessageFormatter({frame, expandPaths, onExpand}: Props) {
  33. if (!isConsoleFrame(frame)) {
  34. return (
  35. <Format
  36. expandPaths={expandPaths}
  37. onExpand={onExpand}
  38. args={[frame.category, frame.data]}
  39. />
  40. );
  41. }
  42. const args = frame.data.arguments;
  43. // Turn this back into an Error object so <Format> can pretty print it
  44. if (args && isSerializedError(frame)) {
  45. // Sometimes message can include stacktrace
  46. const splitMessage = frame.message.split('\n');
  47. const errorMessagePiece = splitMessage[0].trim();
  48. // Error.prototype.toString() will prepend the error type meaning it will
  49. // not be the same as `message` property. We want message only when
  50. // creating a new Error instance, otherwise the type will repeat.
  51. const errorMessageSplit = errorMessagePiece.split('Error: ');
  52. // Restitch together in case there were other `Error: ` strings in the message
  53. const errorMessage = errorMessageSplit
  54. .splice(errorMessageSplit.length - 1)
  55. .join('Error: ');
  56. const fakeError = new Error(errorMessage);
  57. try {
  58. // Messages generally do not include stack trace due to SDK serialization
  59. fakeError.stack = args.length === 2 ? (args[1] as string) : undefined;
  60. // Re-create the error name
  61. if (errorMessageSplit.length > 1) {
  62. fakeError.name = errorMessageSplit[0] + 'Error: ';
  63. }
  64. } catch {
  65. // Some browsers won't allow you to write to error properties
  66. }
  67. return <Format expandPaths={expandPaths} onExpand={onExpand} args={[fakeError]} />;
  68. }
  69. return (
  70. <Format
  71. expandPaths={expandPaths}
  72. onExpand={onExpand}
  73. args={args ?? [frame.message]}
  74. />
  75. );
  76. }
  77. const MessageFormatter = memo(UnmemoizedMessageFormatter);
  78. export default MessageFormatter;