eventErrors.tsx 9.4 KB

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