errors.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import isEqual from 'lodash/isEqual';
  5. import uniqWith from 'lodash/uniqWith';
  6. import {Client} from 'sentry/api';
  7. import Alert from 'sentry/components/alert';
  8. import {ErrorItem, ErrorItemProps} from 'sentry/components/events/errorItem';
  9. import List from 'sentry/components/list';
  10. import {JavascriptProcessingErrors} from 'sentry/constants/eventErrors';
  11. import {t, tn} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {Artifact, Organization, Project} from 'sentry/types';
  14. import {Event} from 'sentry/types/event';
  15. import withApi from 'sentry/utils/withApi';
  16. import {DataSection} from './styles';
  17. const MAX_ERRORS = 100;
  18. export type Error = ErrorItemProps['error'];
  19. type Props = {
  20. api: Client;
  21. event: Event;
  22. orgSlug: Organization['slug'];
  23. proGuardErrors: Array<Error>;
  24. projectSlug: Project['slug'];
  25. };
  26. type State = {
  27. releaseArtifacts?: Array<Artifact>;
  28. };
  29. class Errors extends Component<Props, State> {
  30. state: State = {};
  31. componentDidMount() {
  32. this.checkSourceCodeErrors();
  33. }
  34. shouldComponentUpdate(nextProps: Props) {
  35. return this.props.event.id !== nextProps.event.id;
  36. }
  37. componentDidUpdate(prevProps: Props) {
  38. if (this.props.event.id !== prevProps.event.id) {
  39. this.checkSourceCodeErrors();
  40. }
  41. }
  42. async fetchReleaseArtifacts(query: string) {
  43. const {api, orgSlug, event, projectSlug} = this.props;
  44. const {release} = event;
  45. const releaseVersion = release?.version;
  46. if (!releaseVersion || !query) {
  47. return;
  48. }
  49. try {
  50. const releaseArtifacts = await api.requestPromise(
  51. `/projects/${orgSlug}/${projectSlug}/releases/${encodeURIComponent(
  52. releaseVersion
  53. )}/files/?query=${query}`,
  54. {
  55. method: 'GET',
  56. }
  57. );
  58. this.setState({releaseArtifacts});
  59. } catch (error) {
  60. Sentry.captureException(error);
  61. // do nothing, the UI will not display extra error details
  62. }
  63. }
  64. checkSourceCodeErrors() {
  65. const {event} = this.props;
  66. const {errors} = event;
  67. const sourceCodeErrors = (errors ?? []).filter(
  68. error => error.type === 'js_no_source' && error.data.url
  69. );
  70. if (!sourceCodeErrors.length) {
  71. return;
  72. }
  73. const pathNames: Array<string> = [];
  74. for (const sourceCodeError of sourceCodeErrors) {
  75. const url = sourceCodeError.data.url;
  76. if (url) {
  77. const pathName = this.getURLPathname(url);
  78. if (pathName) {
  79. pathNames.push(encodeURIComponent(pathName));
  80. }
  81. }
  82. }
  83. this.fetchReleaseArtifacts(pathNames.join('&query='));
  84. }
  85. getURLPathname(url: string) {
  86. try {
  87. return new URL(url).pathname;
  88. } catch {
  89. return undefined;
  90. }
  91. }
  92. render() {
  93. const {event, proGuardErrors} = this.props;
  94. const {releaseArtifacts} = this.state;
  95. const {dist: eventDistribution, errors: eventErrors = [], _meta} = event;
  96. // XXX: uniqWith returns unique errors and is not performant with large datasets
  97. const otherErrors: Array<Error> =
  98. eventErrors.length > MAX_ERRORS ? eventErrors : uniqWith(eventErrors, isEqual);
  99. const errors = [...otherErrors, ...proGuardErrors];
  100. return (
  101. <StyledDataSection>
  102. <StyledAlert
  103. type="error"
  104. showIcon
  105. data-test-id="event-error-alert"
  106. expand={[
  107. <ErrorList
  108. key="event-error-details"
  109. data-test-id="event-error-details"
  110. symbol="bullet"
  111. >
  112. {errors.map((error, errorIdx) => {
  113. const data = error.data ?? {};
  114. const meta = _meta?.errors?.[errorIdx];
  115. if (
  116. error.type === JavascriptProcessingErrors.JS_MISSING_SOURCE &&
  117. data.url &&
  118. !!releaseArtifacts?.length
  119. ) {
  120. const releaseArtifact = releaseArtifacts.find(releaseArt => {
  121. const pathname = data.url ? this.getURLPathname(data.url) : undefined;
  122. if (pathname) {
  123. return releaseArt.name.includes(pathname);
  124. }
  125. return false;
  126. });
  127. const releaseArtifactDistribution = releaseArtifact?.dist ?? null;
  128. // Neither event nor file have dist -> matching
  129. // Event has dist, file doesn’t -> not matching
  130. // File has dist, event doesn’t -> not matching
  131. // Both have dist, same value -> matching
  132. // Both have dist, different values -> not matching
  133. if (releaseArtifactDistribution !== eventDistribution) {
  134. error.message = t(
  135. 'Source code was not found because the distribution did not match'
  136. );
  137. data['expected-distribution'] = eventDistribution;
  138. data['current-distribution'] = releaseArtifactDistribution;
  139. }
  140. }
  141. return <ErrorItem key={errorIdx} error={{...error, data}} meta={meta} />;
  142. })}
  143. </ErrorList>,
  144. ]}
  145. >
  146. {tn(
  147. 'There was %s problem processing this event',
  148. 'There were %s problems processing this event',
  149. errors.length
  150. )}
  151. </StyledAlert>
  152. </StyledDataSection>
  153. );
  154. }
  155. }
  156. const StyledDataSection = styled(DataSection)`
  157. border-top: none;
  158. @media (min-width: ${p => p.theme.breakpoints.small}) {
  159. padding-top: 0;
  160. }
  161. `;
  162. const StyledAlert = styled(Alert)`
  163. margin: ${space(0.5)} 0;
  164. `;
  165. const ErrorList = styled(List)`
  166. li:last-child {
  167. margin-bottom: 0;
  168. }
  169. `;
  170. export default withApi(Errors);