eventErrors.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import {useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import uniq from 'lodash/uniq';
  5. import uniqWith from 'lodash/uniqWith';
  6. import {Alert} from 'sentry/components/alert';
  7. import {ErrorItem, EventErrorData} from 'sentry/components/events/errorItem';
  8. import findBestThread from 'sentry/components/events/interfaces/threads/threadSelector/findBestThread';
  9. import getThreadException from 'sentry/components/events/interfaces/threads/threadSelector/getThreadException';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import List from 'sentry/components/list';
  12. import {JavascriptProcessingErrors} from 'sentry/constants/eventErrors';
  13. import {t, tct, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {Artifact, Project} from 'sentry/types';
  16. import {DebugFile} from 'sentry/types/debugFiles';
  17. import {Image} from 'sentry/types/debugImage';
  18. import {EntryType, Event, ExceptionValue, Thread} from 'sentry/types/event';
  19. import {defined} from 'sentry/utils';
  20. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  21. import {useQuery} from 'sentry/utils/queryClient';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import {projectProcessingIssuesMessages} from 'sentry/views/settings/project/projectProcessingIssues';
  24. import {DataSection} from './styles';
  25. const MAX_ERRORS = 100;
  26. const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
  27. /^(([\w\$]\.[\w\$]{1,2})|([\w\$]{2}\.[\w\$]\.[\w\$]))(\.|$)/g;
  28. type EventErrorsProps = {
  29. event: Event;
  30. isShare: boolean;
  31. project: Project;
  32. };
  33. function isDataMinified(str: string | null) {
  34. if (!str) {
  35. return false;
  36. }
  37. return !![...str.matchAll(MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH)].length;
  38. }
  39. const getURLPathname = (url: string) => {
  40. try {
  41. return new URL(url).pathname;
  42. } catch {
  43. return undefined;
  44. }
  45. };
  46. const hasThreadOrExceptionMinifiedFrameData = (
  47. definedEvent: Event,
  48. bestThread?: Thread
  49. ) => {
  50. if (!bestThread) {
  51. const exceptionValues: Array<ExceptionValue> =
  52. definedEvent.entries?.find(e => e.type === EntryType.EXCEPTION)?.data?.values ?? [];
  53. return !!exceptionValues.find(exceptionValue =>
  54. exceptionValue.stacktrace?.frames?.find(frame => isDataMinified(frame.module))
  55. );
  56. }
  57. const threadExceptionValues = getThreadException(definedEvent, bestThread)?.values;
  58. return !!(threadExceptionValues
  59. ? threadExceptionValues.find(threadExceptionValue =>
  60. threadExceptionValue.stacktrace?.frames?.find(frame =>
  61. isDataMinified(frame.module)
  62. )
  63. )
  64. : bestThread?.stacktrace?.frames?.find(frame => isDataMinified(frame.module)));
  65. };
  66. const useFetchProguardMappingFiles = ({
  67. event,
  68. isShare,
  69. project,
  70. }: {
  71. event: Event;
  72. isShare: boolean;
  73. project: Project;
  74. }): {proguardErrors: EventErrorData[]; proguardErrorsLoading: boolean} => {
  75. const organization = useOrganization();
  76. const hasEventErrorsProGuardMissingMapping = event.errors?.find(
  77. error => error.type === 'proguard_missing_mapping'
  78. );
  79. const debugImages = event.entries?.find(e => e.type === EntryType.DEBUGMETA)?.data
  80. .images as undefined | Array<Image>;
  81. // When debugImages contains a 'proguard' entry, it must always be only one entry
  82. const proGuardImage = debugImages?.find(debugImage => debugImage?.type === 'proguard');
  83. const proGuardImageUuid = proGuardImage?.uuid;
  84. const shouldFetch =
  85. defined(proGuardImageUuid) &&
  86. event.platform === 'java' &&
  87. !hasEventErrorsProGuardMissingMapping &&
  88. !isShare;
  89. const {
  90. data: proguardMappingFiles,
  91. isSuccess,
  92. isLoading,
  93. } = useQuery<DebugFile[]>(
  94. [
  95. `/projects/${organization.slug}/${project.slug}/files/dsyms/`,
  96. {
  97. query: {
  98. query: proGuardImageUuid,
  99. file_formats: 'proguard',
  100. },
  101. },
  102. ],
  103. {
  104. staleTime: Infinity,
  105. enabled: shouldFetch,
  106. }
  107. );
  108. const getProguardErrorsFromMappingFiles = () => {
  109. if (isShare) {
  110. return [];
  111. }
  112. if (shouldFetch) {
  113. if (!isSuccess || proguardMappingFiles.length > 0) {
  114. return [];
  115. }
  116. return [
  117. {
  118. type: 'proguard_missing_mapping',
  119. message: projectProcessingIssuesMessages.proguard_missing_mapping,
  120. data: {mapping_uuid: proGuardImageUuid},
  121. },
  122. ];
  123. }
  124. const threads: Array<Thread> =
  125. event.entries?.find(e => e.type === EntryType.THREADS)?.data?.values ?? [];
  126. const bestThread = findBestThread(threads);
  127. const hasThreadOrExceptionMinifiedData = hasThreadOrExceptionMinifiedFrameData(
  128. event,
  129. bestThread
  130. );
  131. if (hasThreadOrExceptionMinifiedData) {
  132. return [
  133. {
  134. type: 'proguard_potentially_misconfigured_plugin',
  135. message: tct(
  136. 'Some frames appear to be minified. Did you configure the [plugin]?',
  137. {
  138. plugin: (
  139. <ExternalLink
  140. href="https://docs.sentry.io/platforms/android/proguard/#gradle"
  141. onClick={() => {
  142. trackAdvancedAnalyticsEvent(
  143. 'issue_error_banner.proguard_misconfigured.clicked',
  144. {
  145. organization,
  146. group: event?.groupID,
  147. }
  148. );
  149. }}
  150. >
  151. Sentry Gradle Plugin
  152. </ExternalLink>
  153. ),
  154. }
  155. ),
  156. },
  157. ];
  158. }
  159. return [];
  160. };
  161. return {
  162. proguardErrorsLoading: shouldFetch && isLoading,
  163. proguardErrors: getProguardErrorsFromMappingFiles(),
  164. };
  165. };
  166. const useFetchReleaseArtifacts = ({event, project}: {event: Event; project: Project}) => {
  167. const organization = useOrganization();
  168. const releaseVersion = event.release?.version;
  169. const pathNames = (event.errors ?? [])
  170. .filter(
  171. error =>
  172. error.type === 'js_no_source' && error.data.url && getURLPathname(error.data.url)
  173. )
  174. .map(sourceCodeError => getURLPathname(sourceCodeError.data.url));
  175. const {data: releaseArtifacts} = useQuery<Artifact[]>(
  176. [
  177. `/projects/${organization.slug}/${project.slug}/releases/${encodeURIComponent(
  178. releaseVersion ?? ''
  179. )}/files/`,
  180. {query: {query: pathNames}},
  181. ],
  182. {staleTime: Infinity, enabled: pathNames.length > 0 && defined(releaseVersion)}
  183. );
  184. return releaseArtifacts;
  185. };
  186. const useRecordAnalyticsEvent = ({event, project}: {event: Event; project: Project}) => {
  187. const organization = useOrganization();
  188. useEffect(() => {
  189. if (!event || !event.errors || !(event.errors.length > 0)) {
  190. return;
  191. }
  192. const errors = event.errors;
  193. const errorTypes = errors.map(errorEntries => errorEntries.type);
  194. const errorMessages = errors.map(errorEntries => errorEntries.message);
  195. const platform = project.platform;
  196. // uniquify the array types
  197. trackAdvancedAnalyticsEvent('issue_error_banner.viewed', {
  198. organization,
  199. group: event?.groupID,
  200. error_type: uniq(errorTypes),
  201. error_message: uniq(errorMessages),
  202. ...(platform && {platform}),
  203. });
  204. }, [event, organization, project.platform]);
  205. };
  206. export const EventErrors = ({event, project, isShare}: EventErrorsProps) => {
  207. const organization = useOrganization();
  208. useRecordAnalyticsEvent({event, project});
  209. const releaseArtifacts = useFetchReleaseArtifacts({event, project});
  210. const {proguardErrorsLoading, proguardErrors} = useFetchProguardMappingFiles({
  211. event,
  212. project,
  213. isShare,
  214. });
  215. useEffect(() => {
  216. if (proguardErrors?.length) {
  217. if (proguardErrors[0]?.type === 'proguard_potentially_misconfigured_plugin') {
  218. trackAdvancedAnalyticsEvent(
  219. 'issue_error_banner.proguard_misconfigured.displayed',
  220. {
  221. organization,
  222. group: event?.groupID,
  223. platform: project.platform,
  224. }
  225. );
  226. } else if (proguardErrors[0]?.type === 'proguard_missing_mapping') {
  227. trackAdvancedAnalyticsEvent(
  228. 'issue_error_banner.proguard_missing_mapping.displayed',
  229. {
  230. organization,
  231. group: event?.groupID,
  232. platform: project.platform,
  233. }
  234. );
  235. }
  236. }
  237. // Just for analytics, only track this once per visit
  238. // eslint-disable-next-line react-hooks/exhaustive-deps
  239. }, []);
  240. const {dist: eventDistribution, errors: eventErrors = [], _meta} = event;
  241. // XXX: uniqWith returns unique errors and is not performant with large datasets
  242. const otherErrors: Array<EventErrorData> =
  243. eventErrors.length > MAX_ERRORS ? eventErrors : uniqWith(eventErrors, isEqual);
  244. const errors = [...otherErrors, ...proguardErrors];
  245. if (proguardErrorsLoading) {
  246. // XXX: This is necessary for acceptance tests to wait until removal since there is
  247. // no visual loading state.
  248. return <HiddenDiv data-test-id="event-errors-loading" />;
  249. }
  250. if (errors.length === 0) {
  251. return null;
  252. }
  253. return (
  254. <StyledDataSection>
  255. <StyledAlert
  256. type="error"
  257. showIcon
  258. data-test-id="event-error-alert"
  259. expand={
  260. <ErrorList data-test-id="event-error-details" symbol="bullet">
  261. {errors.map((error, errorIdx) => {
  262. const data = error.data ?? {};
  263. const meta = _meta?.errors?.[errorIdx];
  264. if (
  265. error.type === JavascriptProcessingErrors.JS_MISSING_SOURCE &&
  266. data.url &&
  267. !!releaseArtifacts?.length
  268. ) {
  269. const releaseArtifact = releaseArtifacts.find(releaseArt => {
  270. const pathname = data.url ? getURLPathname(data.url) : undefined;
  271. if (pathname) {
  272. return releaseArt.name.includes(pathname);
  273. }
  274. return false;
  275. });
  276. const releaseArtifactDistribution = releaseArtifact?.dist ?? null;
  277. // Neither event nor file have dist -> matching
  278. // Event has dist, file doesn’t -> not matching
  279. // File has dist, event doesn’t -> not matching
  280. // Both have dist, same value -> matching
  281. // Both have dist, different values -> not matching
  282. if (releaseArtifactDistribution !== eventDistribution) {
  283. error.message = t(
  284. 'Source code was not found because the distribution did not match'
  285. );
  286. data['expected-distribution'] = eventDistribution;
  287. data['current-distribution'] = releaseArtifactDistribution;
  288. }
  289. }
  290. return <ErrorItem key={errorIdx} error={{...error, data}} meta={meta} />;
  291. })}
  292. </ErrorList>
  293. }
  294. >
  295. {tn(
  296. 'There was %s problem processing this event',
  297. 'There were %s problems processing this event',
  298. errors.length
  299. )}
  300. </StyledAlert>
  301. </StyledDataSection>
  302. );
  303. };
  304. const HiddenDiv = styled('div')`
  305. display: none;
  306. `;
  307. const StyledDataSection = styled(DataSection)`
  308. border-top: none;
  309. @media (min-width: ${p => p.theme.breakpoints.small}) {
  310. padding-top: 0;
  311. }
  312. `;
  313. const StyledAlert = styled(Alert)`
  314. margin: ${space(0.5)} 0;
  315. `;
  316. const ErrorList = styled(List)`
  317. li:last-child {
  318. margin-bottom: 0;
  319. }
  320. `;