actionableItems.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import React, {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import moment from 'moment';
  5. import Alert from 'sentry/components/alert';
  6. import {Button} from 'sentry/components/button';
  7. import {EventErrorData} from 'sentry/components/events/errorItem';
  8. import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
  9. import List from 'sentry/components/list';
  10. import ListItem from 'sentry/components/list/listItem';
  11. import {
  12. GenericSchemaErrors,
  13. HttpProcessingErrors,
  14. JavascriptProcessingErrors,
  15. NativeProcessingErrors,
  16. ProguardProcessingErrors,
  17. } from 'sentry/constants/eventErrors';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {Event, Project} from 'sentry/types';
  21. import {defined} from 'sentry/utils';
  22. import {trackAnalytics} from 'sentry/utils/analytics';
  23. import {getAnalyticsDataForEvent} from 'sentry/utils/events';
  24. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import {
  27. ActionableItemErrors,
  28. ActionableItemTypes,
  29. ActionableItemWarning,
  30. shouldErrorBeShown,
  31. useFetchProguardMappingFiles,
  32. } from './actionableItemsUtils';
  33. import {ActionableItemsResponse, useActionableItems} from './useActionableItems';
  34. interface ErrorMessage {
  35. desc: React.ReactNode;
  36. expandTitle: string;
  37. title: string;
  38. data?: {
  39. absPath?: string;
  40. image_path?: string;
  41. mage_name?: string;
  42. message?: string;
  43. name?: string;
  44. partialMatchPath?: string;
  45. sdk_time?: string;
  46. server_time?: string;
  47. url?: string;
  48. urlPrefix?: string;
  49. } & Record<string, any>;
  50. meta?: Record<string, any>;
  51. }
  52. const keyMapping = {
  53. image_uuid: 'Debug ID',
  54. image_name: 'File Name',
  55. image_path: 'File Path',
  56. };
  57. function getErrorMessage(
  58. error: ActionableItemErrors | EventErrorData,
  59. meta?: Record<string, any>
  60. ): Array<ErrorMessage> {
  61. const errorData = error.data ?? {};
  62. const metaData = meta ?? {};
  63. switch (error.type) {
  64. // Event Errors
  65. case ProguardProcessingErrors.PROGUARD_MISSING_LINENO:
  66. return [
  67. {
  68. title: t('A proguard mapping file does not contain line info'),
  69. desc: null,
  70. expandTitle: t('Fix Proguard Processing Error'),
  71. data: errorData,
  72. meta: metaData,
  73. },
  74. ];
  75. case ProguardProcessingErrors.PROGUARD_MISSING_MAPPING:
  76. return [
  77. {
  78. title: t('A proguard mapping file was missing'),
  79. desc: null,
  80. expandTitle: t('Fix Proguard Processing Error'),
  81. data: errorData,
  82. meta: metaData,
  83. },
  84. ];
  85. case NativeProcessingErrors.NATIVE_MISSING_OPTIONALLY_BUNDLED_DSYM:
  86. return [
  87. {
  88. title: t('An optional debug information file was missing'),
  89. desc: null,
  90. expandTitle: t('Fix Native Processing Error'),
  91. data: errorData,
  92. meta: metaData,
  93. },
  94. ];
  95. case NativeProcessingErrors.NATIVE_MISSING_DSYM:
  96. return [
  97. {
  98. title: t('A required debug information file was missing'),
  99. desc: null,
  100. expandTitle: t('Fix Native Processing Error'),
  101. data: errorData,
  102. meta: metaData,
  103. },
  104. ];
  105. case NativeProcessingErrors.NATIVE_BAD_DSYM:
  106. return [
  107. {
  108. title: t('The debug information file used was broken'),
  109. desc: null,
  110. expandTitle: t('Fix Native Processing Error'),
  111. data: errorData,
  112. meta: metaData,
  113. },
  114. ];
  115. case JavascriptProcessingErrors.JS_MISSING_SOURCES_CONTEXT:
  116. return [
  117. {
  118. title: t('Missing Sources Context'),
  119. desc: null,
  120. expandTitle: t('Fix Processing Error'),
  121. data: errorData,
  122. meta: metaData,
  123. },
  124. ];
  125. case HttpProcessingErrors.FETCH_GENERIC_ERROR:
  126. return [
  127. {
  128. title: t('Unable to fetch HTTP resource'),
  129. desc: null,
  130. expandTitle: t('Fix Processing Error'),
  131. data: errorData,
  132. meta: metaData,
  133. },
  134. ];
  135. case HttpProcessingErrors.RESTRICTED_IP:
  136. return [
  137. {
  138. title: t('Cannot fetch resource due to restricted IP address'),
  139. desc: null,
  140. expandTitle: t('Fix Processing Error'),
  141. data: errorData,
  142. meta: metaData,
  143. },
  144. ];
  145. case HttpProcessingErrors.SECURITY_VIOLATION:
  146. return [
  147. {
  148. title: t('Cannot fetch resource due to security violation'),
  149. desc: null,
  150. expandTitle: t('Fix Processing Error'),
  151. data: errorData,
  152. meta: metaData,
  153. },
  154. ];
  155. case GenericSchemaErrors.FUTURE_TIMESTAMP:
  156. return [
  157. {
  158. title: t('Invalid timestamp (in future)'),
  159. desc: null,
  160. expandTitle: t('Fix Processing Error'),
  161. data: errorData,
  162. meta: metaData,
  163. },
  164. ];
  165. case GenericSchemaErrors.CLOCK_DRIFT:
  166. return [
  167. {
  168. title: t('Clock drift detected in SDK'),
  169. desc: null,
  170. expandTitle: t('Fix Processing Error'),
  171. data: errorData,
  172. meta: metaData,
  173. },
  174. ];
  175. case GenericSchemaErrors.PAST_TIMESTAMP:
  176. return [
  177. {
  178. title: t('Invalid timestamp (too old)'),
  179. desc: null,
  180. expandTitle: t('Fix Processing Error'),
  181. data: errorData,
  182. meta: metaData,
  183. },
  184. ];
  185. case GenericSchemaErrors.VALUE_TOO_LONG:
  186. return [
  187. {
  188. title: t('Discarded value due to exceeding maximum length'),
  189. desc: null,
  190. expandTitle: t('Fix Processing Error'),
  191. data: errorData,
  192. meta: metaData,
  193. },
  194. ];
  195. case GenericSchemaErrors.INVALID_DATA:
  196. return [
  197. {
  198. title: t('Discarded invalid value'),
  199. desc: null,
  200. expandTitle: t('Fix Processing Error'),
  201. data: errorData,
  202. meta: metaData,
  203. },
  204. ];
  205. case GenericSchemaErrors.INVALID_ENVIRONMENT:
  206. return [
  207. {
  208. title: t('Environment cannot contain "/" or newlines'),
  209. desc: null,
  210. expandTitle: t('Fix Processing Error'),
  211. data: errorData,
  212. meta: metaData,
  213. },
  214. ];
  215. case GenericSchemaErrors.INVALID_ATTRIBUTE:
  216. return [
  217. {
  218. title: t('Discarded unknown attribute'),
  219. desc: null,
  220. expandTitle: t('Fix Processing Error'),
  221. data: errorData,
  222. meta: metaData,
  223. },
  224. ];
  225. default:
  226. return [];
  227. }
  228. }
  229. interface ExpandableErrorListProps {
  230. errorList: ErrorMessageType[];
  231. handleExpandClick: (type: ActionableItemTypes) => void;
  232. }
  233. function ExpandableErrorList({handleExpandClick, errorList}: ExpandableErrorListProps) {
  234. const [expanded, setExpanded] = useState(false);
  235. const firstError = errorList[0];
  236. const {title, desc, expandTitle, type} = firstError;
  237. const numErrors = errorList.length;
  238. const errorDataList = errorList.map(error => error.data ?? {});
  239. const cleanedData = useMemo(() => {
  240. const cleaned = errorDataList.map(errorData => {
  241. const data = {...errorData};
  242. // The name is rendered as path in front of the message
  243. if (typeof data.name === 'string') {
  244. delete data.name;
  245. }
  246. if (data.message === 'None') {
  247. // Python ensures a message string, but "None" doesn't make sense here
  248. delete data.message;
  249. }
  250. if (typeof data.image_path === 'string') {
  251. // Separate the image name for readability
  252. const separator = /^([a-z]:\\|\\\\)/i.test(data.image_path) ? '\\' : '/';
  253. const path = data.image_path.split(separator);
  254. data.image_name = path.splice(-1, 1)[0];
  255. data.image_path = path.length ? path.join(separator) + separator : '';
  256. }
  257. if (typeof data.server_time === 'string' && typeof data.sdk_time === 'string') {
  258. data.message = t(
  259. 'Adjusted timestamps by %s',
  260. moment
  261. .duration(moment.utc(data.server_time).diff(moment.utc(data.sdk_time)))
  262. .humanize()
  263. );
  264. }
  265. return Object.entries(data)
  266. .map(([key, value]) => ({
  267. key,
  268. value,
  269. subject: keyMapping[key] || startCase(key),
  270. }))
  271. .filter(d => {
  272. if (!d.value) {
  273. return true;
  274. }
  275. return !!d.value;
  276. });
  277. });
  278. return cleaned;
  279. // eslint-disable-next-line react-hooks/exhaustive-deps
  280. }, [errorDataList]);
  281. return (
  282. <List symbol="bullet">
  283. <StyledListItem>
  284. <ErrorTitleFlex>
  285. <strong>
  286. {title} ({numErrors})
  287. </strong>
  288. <ToggleButton
  289. priority="link"
  290. size="zero"
  291. onClick={() => {
  292. setExpanded(!expanded);
  293. handleExpandClick(type);
  294. }}
  295. >
  296. {expandTitle}
  297. </ToggleButton>
  298. </ErrorTitleFlex>
  299. {expanded && (
  300. <div>
  301. {desc && <Description>{desc}</Description>}
  302. {cleanedData.map((data, idx) => {
  303. return (
  304. <div key={idx}>
  305. <KeyValueList data={data} isContextData />
  306. {idx !== numErrors - 1 && <hr />}
  307. </div>
  308. );
  309. })}
  310. </div>
  311. )}
  312. </StyledListItem>
  313. </List>
  314. );
  315. }
  316. interface ErrorMessageType extends ErrorMessage {
  317. type: ActionableItemTypes;
  318. }
  319. function groupedErrors(
  320. event: Event,
  321. data?: ActionableItemsResponse,
  322. progaurdErrors?: EventErrorData[]
  323. ): Record<ActionableItemTypes, ErrorMessageType[]> | {} {
  324. if (!data || !progaurdErrors || !event) {
  325. return {};
  326. }
  327. const {_meta} = event;
  328. const errors = [...data.errors, ...progaurdErrors]
  329. .filter(error => shouldErrorBeShown(error, event))
  330. .map((error, errorIdx) =>
  331. getErrorMessage(error, _meta?.errors?.[errorIdx]).map(message => ({
  332. ...message,
  333. type: error.type,
  334. }))
  335. )
  336. .flat();
  337. const grouped = errors.reduce((rv, error) => {
  338. rv[error.type] = rv[error.type] || [];
  339. rv[error.type].push(error);
  340. return rv;
  341. }, Object.create(null));
  342. return grouped;
  343. }
  344. interface ActionableItemsProps {
  345. event: Event;
  346. isShare: boolean;
  347. project: Project;
  348. }
  349. export function ActionableItems({event, project, isShare}: ActionableItemsProps) {
  350. const organization = useOrganization();
  351. const {data, isLoading} = useActionableItems({
  352. eventId: event.id,
  353. orgSlug: organization.slug,
  354. projectSlug: project.slug,
  355. });
  356. const {proguardErrorsLoading, proguardErrors} = useFetchProguardMappingFiles({
  357. event,
  358. project,
  359. isShare,
  360. });
  361. useEffect(() => {
  362. if (proguardErrors?.length) {
  363. if (proguardErrors[0]?.type === 'proguard_potentially_misconfigured_plugin') {
  364. trackAnalytics('issue_error_banner.proguard_misconfigured.displayed', {
  365. organization,
  366. group: event?.groupID,
  367. platform: project.platform,
  368. });
  369. } else if (proguardErrors[0]?.type === 'proguard_missing_mapping') {
  370. trackAnalytics('issue_error_banner.proguard_missing_mapping.displayed', {
  371. organization,
  372. group: event?.groupID,
  373. platform: project.platform,
  374. });
  375. }
  376. }
  377. // Just for analytics, only track this once per visit
  378. // eslint-disable-next-line react-hooks/exhaustive-deps
  379. }, []);
  380. const errorMessages = groupedErrors(event, data, proguardErrors);
  381. useRouteAnalyticsParams({
  382. show_actionable_items_cta: data ? data.errors.length > 0 : false,
  383. actionable_items: data ? Object.keys(errorMessages) : [],
  384. });
  385. if (isLoading || !defined(data) || data.errors.length === 0) {
  386. return null;
  387. }
  388. if (proguardErrorsLoading) {
  389. // XXX: This is necessary for acceptance tests to wait until removal since there is
  390. // no visual loading state.
  391. return <HiddenDiv data-test-id="event-errors-loading" />;
  392. }
  393. const analyticsParams = {
  394. organization,
  395. project_id: event.projectID,
  396. group_id: event.groupID,
  397. ...getAnalyticsDataForEvent(event),
  398. };
  399. const handleExpandClick = (type: ActionableItemTypes) => {
  400. trackAnalytics('actionable_items.expand_clicked', {
  401. ...analyticsParams,
  402. type,
  403. });
  404. };
  405. const hasErrorAlert = Object.keys(errorMessages).some(error =>
  406. ActionableItemWarning.includes(
  407. error as ProguardProcessingErrors | NativeProcessingErrors | GenericSchemaErrors
  408. )
  409. );
  410. for (const errorKey in Object.keys(errorMessages)) {
  411. const isWarning = ActionableItemWarning.includes(
  412. errorKey as ProguardProcessingErrors | NativeProcessingErrors | GenericSchemaErrors
  413. );
  414. const shouldDelete = hasErrorAlert ? isWarning : !isWarning;
  415. if (shouldDelete) {
  416. delete errorMessages[errorKey];
  417. }
  418. }
  419. return (
  420. <StyledAlert
  421. defaultExpanded
  422. showIcon
  423. type={hasErrorAlert ? 'error' : 'warning'}
  424. expand={
  425. <Fragment>
  426. {Object.keys(errorMessages).map((error, idx) => {
  427. return (
  428. <ExpandableErrorList
  429. key={idx}
  430. errorList={errorMessages[error]}
  431. handleExpandClick={handleExpandClick}
  432. />
  433. );
  434. })}
  435. </Fragment>
  436. }
  437. >
  438. {t("There are problems you'll need to fix for future events")}
  439. </StyledAlert>
  440. );
  441. }
  442. const Description = styled('div')`
  443. margin-top: ${space(0.5)};
  444. `;
  445. const StyledAlert = styled(Alert)`
  446. margin: 0 30px;
  447. `;
  448. const StyledListItem = styled(ListItem)`
  449. margin-bottom: ${space(0.75)};
  450. `;
  451. const ToggleButton = styled(Button)`
  452. color: ${p => p.theme.subText};
  453. text-decoration: underline;
  454. :hover,
  455. :focus {
  456. color: ${p => p.theme.textColor};
  457. }
  458. `;
  459. const ErrorTitleFlex = styled('div')`
  460. display: flex;
  461. justify-content: space-between;
  462. align-items: center;
  463. gap: ${space(1)};
  464. `;
  465. const HiddenDiv = styled('div')`
  466. display: none;
  467. `;