errorItem.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import moment from 'moment';
  5. import Button from 'sentry/components/button';
  6. import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
  7. import AnnotatedText from 'sentry/components/events/meta/annotatedText';
  8. import ListItem from 'sentry/components/list/listItem';
  9. import {JavascriptProcessingErrors} from 'sentry/constants/eventErrors';
  10. import {t, tct} from 'sentry/locale';
  11. import space from 'sentry/styles/space';
  12. import ExternalLink from '../links/externalLink';
  13. type Error = {
  14. message: React.ReactNode;
  15. type: string;
  16. data?: {
  17. image_name?: string;
  18. image_path?: string;
  19. message?: string;
  20. name?: string;
  21. sdk_time?: string;
  22. server_time?: string;
  23. url?: string;
  24. } & Record<string, any>;
  25. };
  26. const keyMapping = {
  27. image_uuid: 'Debug ID',
  28. image_name: 'File Name',
  29. image_path: 'File Path',
  30. };
  31. export type ErrorItemProps = {
  32. error: Error;
  33. meta?: Record<any, any>;
  34. };
  35. export function ErrorItem({error, meta}: ErrorItemProps) {
  36. const [expanded, setExpanded] = useState(false);
  37. const cleanedData = useMemo(() => {
  38. const data = {...(error.data ?? {})};
  39. // The name is rendered as path in front of the message
  40. if (typeof data.name === 'string') {
  41. delete data.name;
  42. }
  43. if (data.message === 'None') {
  44. // Python ensures a message string, but "None" doesn't make sense here
  45. delete data.message;
  46. }
  47. if (typeof data.image_path === 'string') {
  48. // Separate the image name for readability
  49. const separator = /^([a-z]:\\|\\\\)/i.test(data.image_path) ? '\\' : '/';
  50. const path = data.image_path.split(separator);
  51. data.image_name = path.splice(-1, 1)[0];
  52. data.image_path = path.length ? path.join(separator) + separator : '';
  53. }
  54. if (typeof data.server_time === 'string' && typeof data.sdk_time === 'string') {
  55. data.message = t(
  56. 'Adjusted timestamps by %s',
  57. moment
  58. .duration(moment.utc(data.server_time).diff(moment.utc(data.sdk_time)))
  59. .humanize()
  60. );
  61. }
  62. return Object.entries(data)
  63. .map(([key, value]) => ({
  64. key,
  65. value,
  66. subject: keyMapping[key] || startCase(key),
  67. meta: key === 'image_name' ? meta?.image_path?.[''] : meta?.[key]?.[''],
  68. }))
  69. .filter(d => {
  70. if (!d.value && !!d.meta) {
  71. return true;
  72. }
  73. return !!d.value;
  74. });
  75. }, [error.data, meta]);
  76. return (
  77. <StyledListItem data-test-id="event-error-item">
  78. <OverallInfo>
  79. <div>
  80. {meta?.data?.name?.[''] ? (
  81. <AnnotatedText value={error.message} meta={meta?.data?.name?.['']} />
  82. ) : !error.data?.name || typeof error.data?.name !== 'string' ? null : (
  83. <Fragment>
  84. <strong>{error.data?.name}</strong>
  85. {': '}
  86. </Fragment>
  87. )}
  88. {meta?.message?.[''] ? (
  89. <AnnotatedText value={error.message} meta={meta?.message?.['']} />
  90. ) : (
  91. error.message
  92. )}
  93. {Object.values(JavascriptProcessingErrors).includes(
  94. error.type as JavascriptProcessingErrors
  95. ) && (
  96. <Fragment>
  97. {' '}
  98. (
  99. {tct('see [docsLink]', {
  100. docsLink: (
  101. <StyledExternalLink href="https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/">
  102. {t('Troubleshooting for JavaScript')}
  103. </StyledExternalLink>
  104. ),
  105. })}
  106. )
  107. </Fragment>
  108. )}
  109. </div>
  110. {!!cleanedData.length && (
  111. <ToggleButton
  112. onClick={event => {
  113. event.stopPropagation();
  114. setExpanded(!expanded);
  115. }}
  116. priority="link"
  117. size="zero"
  118. >
  119. {expanded ? t('Collapse') : t('Expand')}
  120. </ToggleButton>
  121. )}
  122. </OverallInfo>
  123. {expanded && <KeyValueList data={cleanedData} isContextData />}
  124. </StyledListItem>
  125. );
  126. }
  127. const ToggleButton = styled(Button)`
  128. margin-left: ${space(1.5)};
  129. font-weight: 700;
  130. color: ${p => p.theme.subText};
  131. :hover,
  132. :focus {
  133. color: ${p => p.theme.textColor};
  134. }
  135. `;
  136. const StyledListItem = styled(ListItem)`
  137. margin-bottom: ${space(0.75)};
  138. `;
  139. const StyledExternalLink = styled(ExternalLink)`
  140. /* && is here to increase specificity to override default styles*/
  141. && {
  142. font-weight: inherit;
  143. color: inherit;
  144. text-decoration: underline;
  145. }
  146. `;
  147. const OverallInfo = styled('div')`
  148. display: grid;
  149. grid-template-columns: repeat(2, minmax(auto, max-content));
  150. word-break: break-all;
  151. `;