consoleMessage.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import isObject from 'lodash/isObject';
  4. import {sprintf, vsprintf} from 'sprintf-js';
  5. import DateTime from 'sentry/components/dateTime';
  6. import ErrorBoundary from 'sentry/components/errorBoundary';
  7. import AnnotatedText from 'sentry/components/events/meta/annotatedText';
  8. import {getMeta} from 'sentry/components/events/meta/metaProxy';
  9. import {useReplayContext} from 'sentry/components/replays/replayContext';
  10. import {relativeTimeInMs, showPlayerTime} from 'sentry/components/replays/utils';
  11. import Tooltip from 'sentry/components/tooltip';
  12. import {IconClose, IconWarning} from 'sentry/icons';
  13. import space from 'sentry/styles/space';
  14. import {BreadcrumbTypeDefault} from 'sentry/types/breadcrumbs';
  15. import {objectIsEmpty} from 'sentry/utils';
  16. interface MessageFormatterProps {
  17. breadcrumb: BreadcrumbTypeDefault;
  18. }
  19. /**
  20. * Attempt to stringify
  21. */
  22. function renderString(arg: string | number | boolean | Object) {
  23. if (typeof arg !== 'object') {
  24. return arg;
  25. }
  26. try {
  27. return JSON.stringify(arg);
  28. } catch {
  29. return arg.toString();
  30. }
  31. }
  32. /**
  33. * Attempt to emulate the browser console as much as possible
  34. */
  35. export function MessageFormatter({breadcrumb}: MessageFormatterProps) {
  36. let logMessage = '';
  37. if (!breadcrumb.data?.arguments) {
  38. // There is a possibility that we don't have arguments as we could be receiving an exception type breadcrumb.
  39. // In these cases we just need the message prop.
  40. // There are cases in which our prop message is an array, we want to force it to become a string
  41. logMessage = breadcrumb.message?.toString() || '';
  42. return <AnnotatedText meta={getMeta(breadcrumb, 'message')} value={logMessage} />;
  43. }
  44. // Browser's console formatter only works on the first arg
  45. const [message, ...args] = breadcrumb.data?.arguments;
  46. const isMessageString = typeof message === 'string';
  47. const placeholders = isMessageString
  48. ? sprintf.parse(message).filter(parsed => Array.isArray(parsed))
  49. : [];
  50. // Placeholders can only occur in the first argument and only if it is a string.
  51. // We can skip the below code and avoid using `sprintf` if there are no placeholders.
  52. if (placeholders.length) {
  53. // TODO `%c` is console specific, it applies colors to messages
  54. // for now we are stripping it as this is potentially risky to implement due to xss
  55. const consoleColorPlaceholderIndexes = placeholders
  56. .filter(([placeholder]) => placeholder === '%c')
  57. .map((_, i) => i);
  58. // Retrieve message formatting args
  59. const messageArgs = args.slice(0, placeholders.length);
  60. // Filter out args that were for %c
  61. for (const colorIndex of consoleColorPlaceholderIndexes) {
  62. messageArgs.splice(colorIndex, 1);
  63. }
  64. // Attempt to stringify the rest of the args
  65. const restArgs = args.slice(placeholders.length).map(renderString);
  66. const formattedMessage = isMessageString
  67. ? vsprintf(message.replaceAll('%c', ''), messageArgs)
  68. : renderString(message);
  69. logMessage = [formattedMessage, ...restArgs].join(' ').trim();
  70. } else if (
  71. breadcrumb.data?.arguments.length === 1 &&
  72. isObject(message) &&
  73. objectIsEmpty(message)
  74. ) {
  75. // There is a special case where `console.error()` is called with an Error object.
  76. // The SDK uses the Error's `message` property as the breadcrumb message, but we lose the Error type,
  77. // resulting in an empty object in the breadcrumb arguments. In this case, we
  78. // only want to use `breadcrumb.message`.
  79. logMessage = breadcrumb.message || JSON.stringify(message);
  80. } else {
  81. // If the string `[object Object]` is found in message, it means the SDK attempted to stringify an object,
  82. // but the actual object should be captured in the arguments.
  83. //
  84. // Likewise if arrays are found e.g. [test,test] the SDK will serialize it to 'test, test'.
  85. //
  86. // In those cases, we'll want to use our pretty print in every argument that was passed to the logger instead of using
  87. // the SDK's serialization.
  88. const argValues = breadcrumb.data?.arguments.map(renderString);
  89. logMessage = argValues.join(' ').trim();
  90. }
  91. // TODO(replays): Add better support for AnnotatedText (e.g. we use message
  92. // args from breadcrumb.data.arguments and not breadcrumb.message directly)
  93. return <AnnotatedText meta={getMeta(breadcrumb, 'message')} value={logMessage} />;
  94. }
  95. interface ConsoleMessageProps extends MessageFormatterProps {
  96. hasOccurred: boolean;
  97. isActive: boolean;
  98. isLast: boolean;
  99. startTimestamp: number;
  100. }
  101. function ConsoleMessage({
  102. breadcrumb,
  103. isActive = false,
  104. hasOccurred,
  105. isLast,
  106. startTimestamp = 0,
  107. }: ConsoleMessageProps) {
  108. const ICONS = {
  109. error: <IconClose isCircled size="xs" />,
  110. warning: <IconWarning size="xs" />,
  111. };
  112. const {setCurrentTime, setCurrentHoverTime} = useReplayContext();
  113. const diff = relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp);
  114. const handleOnClick = () => setCurrentTime(diff);
  115. const handleOnMouseOver = () => setCurrentHoverTime(diff);
  116. const handleOnMouseOut = () => setCurrentHoverTime(undefined);
  117. return (
  118. <Fragment>
  119. <Icon
  120. isLast={isLast}
  121. level={breadcrumb.level}
  122. isActive={isActive}
  123. hasOccurred={hasOccurred}
  124. >
  125. {ICONS[breadcrumb.level]}
  126. </Icon>
  127. <Message isLast={isLast} level={breadcrumb.level} hasOccurred={hasOccurred}>
  128. <ErrorBoundary mini>
  129. <MessageFormatter breadcrumb={breadcrumb} />
  130. </ErrorBoundary>
  131. </Message>
  132. <ConsoleTimestamp
  133. isLast={isLast}
  134. level={breadcrumb.level}
  135. hasOccurred={hasOccurred}
  136. >
  137. <Tooltip title={<DateTime date={breadcrumb.timestamp} seconds />}>
  138. <div
  139. onClick={handleOnClick}
  140. onMouseOver={handleOnMouseOver}
  141. onMouseOut={handleOnMouseOut}
  142. >
  143. {showPlayerTime(breadcrumb.timestamp || '', startTimestamp)}
  144. </div>
  145. </Tooltip>
  146. </ConsoleTimestamp>
  147. </Fragment>
  148. );
  149. }
  150. const Common = styled('div')<{
  151. isLast: boolean;
  152. level: string;
  153. hasOccurred?: boolean;
  154. }>`
  155. background-color: ${p =>
  156. ['warning', 'error'].includes(p.level)
  157. ? p.theme.alert[p.level].backgroundLight
  158. : 'inherit'};
  159. color: ${({hasOccurred = true, ...p}) => {
  160. if (!hasOccurred) {
  161. return p.theme.gray300;
  162. }
  163. if (['warning', 'error'].includes(p.level)) {
  164. return p.theme.alert[p.level].iconHoverColor;
  165. }
  166. return 'inherit';
  167. }};
  168. ${p => (!p.isLast ? `border-bottom: 1px solid ${p.theme.innerBorder}` : '')};
  169. transition: color 0.5s ease;
  170. `;
  171. const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
  172. padding: ${space(0.25)} ${space(1)};
  173. cursor: pointer;
  174. `;
  175. const Icon = styled(Common)<{isActive: boolean}>`
  176. padding: ${space(0.5)} ${space(1)};
  177. border-left: 4px solid ${p => (p.isActive ? p.theme.focus : 'transparent')};
  178. `;
  179. const Message = styled(Common)`
  180. padding: ${space(0.25)} 0;
  181. white-space: pre-wrap;
  182. word-break: break-word;
  183. `;
  184. export default ConsoleMessage;