messageFormatter.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import isObject from 'lodash/isObject';
  2. import {sprintf, vsprintf} from 'sprintf-js';
  3. import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
  4. import {getMeta} from 'sentry/components/events/meta/metaProxy';
  5. import {BreadcrumbTypeDefault, Crumb} from 'sentry/types/breadcrumbs';
  6. import {objectIsEmpty} from 'sentry/utils';
  7. interface Props {
  8. breadcrumb: Extract<Crumb, BreadcrumbTypeDefault>;
  9. }
  10. /**
  11. * Attempt to emulate the browser console as much as possible
  12. */
  13. function MessageFormatter({breadcrumb}: Props) {
  14. let logMessage = '';
  15. if (!breadcrumb.data?.arguments) {
  16. // There is a possibility that we don't have arguments as we could be receiving an exception type breadcrumb.
  17. // In these cases we just need the message prop.
  18. // There are cases in which our prop message is an array, we want to force it to become a string
  19. logMessage = breadcrumb.message?.toString() || '';
  20. return <AnnotatedText meta={getMeta(breadcrumb, 'message')} value={logMessage} />;
  21. }
  22. // Browser's console formatter only works on the first arg
  23. const [message, ...args] = breadcrumb.data?.arguments;
  24. const isMessageString = typeof message === 'string';
  25. const placeholders = isMessageString
  26. ? sprintf.parse(message).filter(parsed => Array.isArray(parsed))
  27. : [];
  28. // Placeholders can only occur in the first argument and only if it is a string.
  29. // We can skip the below code and avoid using `sprintf` if there are no placeholders.
  30. if (placeholders.length) {
  31. // TODO `%c` is console specific, it applies colors to messages
  32. // for now we are stripping it as this is potentially risky to implement due to xss
  33. const consoleColorPlaceholderIndexes = placeholders
  34. .filter(([placeholder]) => placeholder === '%c')
  35. .map((_, i) => i);
  36. // Retrieve message formatting args
  37. const messageArgs = args.slice(0, placeholders.length);
  38. // Filter out args that were for %c
  39. for (const colorIndex of consoleColorPlaceholderIndexes) {
  40. messageArgs.splice(colorIndex, 1);
  41. }
  42. // Attempt to stringify the rest of the args
  43. const restArgs = args.slice(placeholders.length).map(renderString);
  44. const formattedMessage = isMessageString
  45. ? vsprintf(message.replaceAll('%c', ''), messageArgs)
  46. : renderString(message);
  47. logMessage = [formattedMessage, ...restArgs].join(' ').trim();
  48. } else if (
  49. breadcrumb.data?.arguments.length === 1 &&
  50. isObject(message) &&
  51. objectIsEmpty(message)
  52. ) {
  53. // There is a special case where `console.error()` is called with an Error object.
  54. // The SDK uses the Error's `message` property as the breadcrumb message, but we lose the Error type,
  55. // resulting in an empty object in the breadcrumb arguments. In this case, we
  56. // only want to use `breadcrumb.message`.
  57. logMessage = breadcrumb.message || JSON.stringify(message);
  58. } else {
  59. // If the string `[object Object]` is found in message, it means the SDK attempted to stringify an object,
  60. // but the actual object should be captured in the arguments.
  61. //
  62. // Likewise if arrays are found e.g. [test,test] the SDK will serialize it to 'test, test'.
  63. //
  64. // In those cases, we'll want to use our pretty print in every argument that was passed to the logger instead of using
  65. // the SDK's serialization.
  66. const argValues = breadcrumb.data?.arguments.map(renderString);
  67. logMessage = argValues.join(' ').trim();
  68. }
  69. // TODO(replays): Add better support for AnnotatedText (e.g. we use message
  70. // args from breadcrumb.data.arguments and not breadcrumb.message directly)
  71. return <AnnotatedText meta={getMeta(breadcrumb, 'message')} value={logMessage} />;
  72. }
  73. /**
  74. * Attempt to stringify
  75. */
  76. function renderString(arg: string | number | boolean | object) {
  77. if (typeof arg !== 'object') {
  78. return arg;
  79. }
  80. try {
  81. return JSON.stringify(arg);
  82. } catch {
  83. return arg.toString();
  84. }
  85. }
  86. export default MessageFormatter;