errorItem.tsx 4.6 KB

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