messageFormatter.tsx 3.0 KB

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