errors.tsx 5.7 KB

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