projectProcessingIssues.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import {Component, Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {addLoadingMessage, clearIndicators} from 'sentry/actionCreators/indicator';
  5. import type {Client} from 'sentry/api';
  6. import Access from 'sentry/components/acl/access';
  7. import Alert from 'sentry/components/alert';
  8. import AlertLink from 'sentry/components/alertLink';
  9. import Tag from 'sentry/components/badge/tag';
  10. import {Button} from 'sentry/components/button';
  11. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  12. import Form from 'sentry/components/forms/form';
  13. import JsonForm from 'sentry/components/forms/jsonForm';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import LoadingError from 'sentry/components/loadingError';
  16. import LoadingIndicator from 'sentry/components/loadingIndicator';
  17. import Panel from 'sentry/components/panels/panel';
  18. import PanelAlert from 'sentry/components/panels/panelAlert';
  19. import {PanelTable} from 'sentry/components/panels/panelTable';
  20. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  21. import TimeSince from 'sentry/components/timeSince';
  22. import Version from 'sentry/components/version';
  23. import formGroups from 'sentry/data/forms/processingIssues';
  24. import {IconQuestion} from 'sentry/icons';
  25. import {t, tct, tn} from 'sentry/locale';
  26. import type {Organization, ProcessingIssue, ProcessingIssueItem} from 'sentry/types';
  27. import withApi from 'sentry/utils/withApi';
  28. import withOrganization from 'sentry/utils/withOrganization';
  29. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  30. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  31. export const projectProcessingIssuesMessages = {
  32. native_no_crashed_thread: t('No crashed thread found in crash report'),
  33. native_internal_failure: t('Internal failure when attempting to symbolicate: {error}'),
  34. native_bad_dsym: t('The debug information file used was broken.'),
  35. native_missing_optionally_bundled_dsym: t(
  36. 'An optional debug information file was missing.'
  37. ),
  38. native_missing_dsym: t('A required debug information file was missing.'),
  39. native_missing_system_dsym: t('A system debug information file was missing.'),
  40. native_missing_symbol: t(
  41. 'Could not resolve one or more frames in debug information file.'
  42. ),
  43. native_simulator_frame: t('Encountered an unprocessable simulator frame.'),
  44. native_unknown_image: t('A binary image is referenced that is unknown.'),
  45. proguard_missing_mapping: t('A proguard mapping file was missing.'),
  46. proguard_missing_lineno: t('A proguard mapping file does not contain line info.'),
  47. };
  48. const HELP_LINKS = {
  49. native_missing_dsym: 'https://docs.sentry.io/platforms/apple/dsym/',
  50. native_bad_dsym: 'https://docs.sentry.io/platforms/apple/dsym/',
  51. native_missing_system_dsym: 'https://develop.sentry.dev/self-hosted/',
  52. native_missing_symbol: 'https://develop.sentry.dev/self-hosted/',
  53. };
  54. type Props = {
  55. api: Client;
  56. organization: Organization;
  57. } & RouteComponentProps<{projectId: string}, {}>;
  58. type State = {
  59. error: boolean;
  60. expected: number;
  61. formData: object;
  62. loading: boolean;
  63. pageLinks: null | string;
  64. processingIssues: null | ProcessingIssue;
  65. reprocessing: boolean;
  66. };
  67. class ProjectProcessingIssues extends Component<Props, State> {
  68. state: State = {
  69. formData: {},
  70. loading: true,
  71. reprocessing: false,
  72. expected: 0,
  73. error: false,
  74. processingIssues: null,
  75. pageLinks: null,
  76. };
  77. componentDidMount() {
  78. this.fetchData();
  79. }
  80. fetchData = () => {
  81. const {organization} = this.props;
  82. const {projectId} = this.props.params;
  83. this.setState({
  84. expected: this.state.expected + 2,
  85. });
  86. this.props.api.request(`/projects/${organization.slug}/${projectId}/`, {
  87. success: data => {
  88. const expected = this.state.expected - 1;
  89. this.setState({
  90. expected,
  91. loading: expected > 0,
  92. formData: data.options,
  93. });
  94. },
  95. error: () => {
  96. const expected = this.state.expected - 1;
  97. this.setState({
  98. expected,
  99. error: true,
  100. loading: expected > 0,
  101. });
  102. },
  103. });
  104. this.props.api.request(
  105. `/projects/${organization.slug}/${projectId}/processingissues/?detailed=1`,
  106. {
  107. success: (data, _, resp) => {
  108. const expected = this.state.expected - 1;
  109. this.setState({
  110. expected,
  111. error: false,
  112. loading: expected > 0,
  113. processingIssues: data,
  114. pageLinks: resp?.getResponseHeader('Link') ?? null,
  115. });
  116. },
  117. error: () => {
  118. const expected = this.state.expected - 1;
  119. this.setState({
  120. expected,
  121. error: true,
  122. loading: expected > 0,
  123. });
  124. },
  125. }
  126. );
  127. };
  128. sendReprocessing = (e: React.MouseEvent<Element>) => {
  129. e.preventDefault();
  130. this.setState({
  131. loading: true,
  132. reprocessing: true,
  133. });
  134. addLoadingMessage(t('Started reprocessing\u2026'));
  135. const {organization} = this.props;
  136. const {projectId} = this.props.params;
  137. this.props.api.request(`/projects/${organization.slug}/${projectId}/reprocessing/`, {
  138. method: 'POST',
  139. success: () => {
  140. this.fetchData();
  141. this.setState({
  142. reprocessing: false,
  143. });
  144. },
  145. error: () => {
  146. this.setState({
  147. reprocessing: false,
  148. });
  149. },
  150. complete: () => {
  151. clearIndicators();
  152. },
  153. });
  154. };
  155. discardEvents = () => {
  156. const {organization} = this.props;
  157. const {projectId} = this.props.params;
  158. this.setState({
  159. expected: this.state.expected + 1,
  160. });
  161. this.props.api.request(
  162. `/projects/${organization.slug}/${projectId}/processingissues/discard/`,
  163. {
  164. method: 'DELETE',
  165. success: () => {
  166. const expected = this.state.expected - 1;
  167. this.setState({
  168. expected,
  169. error: false,
  170. loading: expected > 0,
  171. });
  172. // TODO (billyvg): Need to fix this
  173. // we reload to get rid of the badge in the sidebar
  174. window.location.reload();
  175. },
  176. error: () => {
  177. const expected = this.state.expected - 1;
  178. this.setState({
  179. expected,
  180. error: true,
  181. loading: expected > 0,
  182. });
  183. },
  184. }
  185. );
  186. };
  187. deleteProcessingIssues = () => {
  188. const {organization} = this.props;
  189. const {projectId} = this.props.params;
  190. this.setState({
  191. expected: this.state.expected + 1,
  192. });
  193. this.props.api.request(
  194. `/projects/${organization.slug}/${projectId}/processingissues/`,
  195. {
  196. method: 'DELETE',
  197. success: () => {
  198. const expected = this.state.expected - 1;
  199. this.setState({
  200. expected,
  201. error: false,
  202. loading: expected > 0,
  203. });
  204. // TODO (billyvg): Need to fix this
  205. // we reload to get rid of the badge in the sidebar
  206. window.location.reload();
  207. },
  208. error: () => {
  209. const expected = this.state.expected - 1;
  210. this.setState({
  211. expected,
  212. error: true,
  213. loading: expected > 0,
  214. });
  215. },
  216. }
  217. );
  218. };
  219. renderDebugTable() {
  220. let body: React.ReactNode;
  221. const {loading, error, processingIssues} = this.state;
  222. if (loading) {
  223. body = this.renderLoading();
  224. } else if (error) {
  225. body = <LoadingError onRetry={this.fetchData} />;
  226. } else if (
  227. processingIssues?.hasIssues ||
  228. processingIssues?.resolveableIssues ||
  229. processingIssues?.issuesProcessing
  230. ) {
  231. body = this.renderResults();
  232. } else {
  233. body = this.renderEmpty();
  234. }
  235. return body;
  236. }
  237. renderLoading() {
  238. return (
  239. <Panel>
  240. <LoadingIndicator />
  241. </Panel>
  242. );
  243. }
  244. renderEmpty() {
  245. return (
  246. <Panel>
  247. <EmptyStateWarning>
  248. <p>{t('Good news! There are no processing issues.')}</p>
  249. </EmptyStateWarning>
  250. </Panel>
  251. );
  252. }
  253. getProblemDescription(item: ProcessingIssueItem) {
  254. const msg = projectProcessingIssuesMessages[item.type];
  255. return msg || t('Unknown Error');
  256. }
  257. getImageName(path: string) {
  258. const pathSegments = path.split(/^([a-z]:\\|\\\\)/i.test(path) ? '\\' : '/');
  259. return pathSegments[pathSegments.length - 1];
  260. }
  261. renderProblem(item: ProcessingIssueItem) {
  262. const description = this.getProblemDescription(item);
  263. const helpLink = HELP_LINKS[item.type];
  264. return (
  265. <div>
  266. <span>{description}</span>{' '}
  267. {helpLink && (
  268. <ExternalLink href={helpLink}>
  269. <IconQuestion size="xs" />
  270. </ExternalLink>
  271. )}
  272. </div>
  273. );
  274. }
  275. renderDetails(item: ProcessingIssueItem) {
  276. const {release, dist} = item.data;
  277. let dsymUUID: React.ReactNode = null;
  278. let dsymName: React.ReactNode = null;
  279. let dsymArch: React.ReactNode = null;
  280. if (item.data._scope === 'native') {
  281. if (item.data.image_uuid) {
  282. dsymUUID = <code className="uuid">{item.data.image_uuid}</code>;
  283. }
  284. if (item.data.image_path) {
  285. dsymName = <em>{this.getImageName(item.data.image_path)}</em>;
  286. }
  287. if (item.data.image_arch) {
  288. dsymArch = item.data.image_arch;
  289. }
  290. }
  291. return (
  292. <span>
  293. {dsymUUID && <span> {dsymUUID}</span>}
  294. {dsymArch && <span> {dsymArch}</span>}
  295. {dsymName && <span> (for {dsymName})</span>}
  296. {(release || dist) && (
  297. <div>
  298. <Tag tooltipText={t('Latest Release Observed with Issue')}>
  299. {release ? <Version version={release} /> : t('none')}
  300. </Tag>{' '}
  301. <Tag tooltipText={t('Latest Distribution Observed with Issue')}>
  302. {dist || t('none')}
  303. </Tag>
  304. </div>
  305. )}
  306. </span>
  307. );
  308. }
  309. renderResolveButton() {
  310. const issues = this.state.processingIssues;
  311. if (issues === null || this.state.reprocessing) {
  312. return null;
  313. }
  314. if (issues.resolveableIssues <= 0) {
  315. return null;
  316. }
  317. const fixButton = tn(
  318. 'Click here to trigger processing for %s pending event',
  319. 'Click here to trigger processing for %s pending events',
  320. issues.resolveableIssues
  321. );
  322. return (
  323. <AlertLink priority="info" onClick={this.sendReprocessing}>
  324. {t('Pro Tip')}: {fixButton}
  325. </AlertLink>
  326. );
  327. }
  328. renderResults() {
  329. const {processingIssues} = this.state;
  330. let processingRow: React.ReactNode = null;
  331. if (processingIssues && processingIssues.issuesProcessing > 0) {
  332. processingRow = (
  333. <StyledPanelAlert type="info" showIcon>
  334. {tn(
  335. 'Reprocessing %s event …',
  336. 'Reprocessing %s events …',
  337. processingIssues.issuesProcessing
  338. )}
  339. </StyledPanelAlert>
  340. );
  341. }
  342. return (
  343. <Fragment>
  344. <h3>
  345. {t('Pending Issues')}
  346. <Access access={['project:write']}>
  347. {({hasAccess}) => (
  348. <Button
  349. size="sm"
  350. className="pull-right"
  351. disabled={!hasAccess}
  352. onClick={() => this.discardEvents()}
  353. >
  354. {t('Discard all')}
  355. </Button>
  356. )}
  357. </Access>
  358. </h3>
  359. <PanelTable headers={[t('Problem'), t('Details'), t('Events'), t('Last seen')]}>
  360. {processingRow}
  361. {processingIssues?.issues?.map((item, idx) => (
  362. <Fragment key={idx}>
  363. <div>{this.renderProblem(item)}</div>
  364. <div>{this.renderDetails(item)}</div>
  365. <div>{item.numEvents + ''}</div>
  366. <div>
  367. <TimeSince date={item.lastSeen} />
  368. </div>
  369. </Fragment>
  370. ))}
  371. </PanelTable>
  372. </Fragment>
  373. );
  374. }
  375. renderReprocessingSettings() {
  376. const access = new Set(this.props.organization.access);
  377. if (this.state.loading) {
  378. return this.renderLoading();
  379. }
  380. const {formData} = this.state;
  381. const {organization} = this.props;
  382. const {projectId} = this.props.params;
  383. return (
  384. <Form
  385. saveOnBlur
  386. onSubmitSuccess={this.deleteProcessingIssues}
  387. apiEndpoint={`/projects/${organization.slug}/${projectId}/`}
  388. apiMethod="PUT"
  389. initialData={formData}
  390. >
  391. <JsonForm
  392. access={access}
  393. forms={formGroups}
  394. renderHeader={() => (
  395. <PanelAlert type="warning">
  396. <TextBlock noMargin>
  397. {t(`Reprocessing does not apply to Minidumps. Even when enabled,
  398. Minidump events with processing issues will show up in the
  399. issues stream immediately and cannot be reprocessed.`)}
  400. </TextBlock>
  401. </PanelAlert>
  402. )}
  403. />
  404. </Form>
  405. );
  406. }
  407. render() {
  408. const {projectId} = this.props.params;
  409. const title = t('Processing Issues');
  410. return (
  411. <div>
  412. <SentryDocumentTitle title={title} projectSlug={projectId} />
  413. <SettingsPageHeader title={title} />
  414. <Alert type="warning">
  415. <TextBlock noMargin>
  416. {t(
  417. `Processing Issues, along with Legacy Reprocessing, has been deprecated,
  418. and will be removed in the future.`
  419. )}
  420. </TextBlock>
  421. <TextBlock noMargin>
  422. {tct(
  423. `Please refer to the documentation on [link:Reprocessing] to learn more about the new method to
  424. reprocess events.`,
  425. {
  426. link: (
  427. <ExternalLink href="https://docs.sentry.io/product/issues/reprocessing/" />
  428. ),
  429. }
  430. )}
  431. </TextBlock>
  432. </Alert>
  433. <TextBlock>
  434. {t(
  435. `For some platforms the event processing requires configuration or
  436. manual action. If a misconfiguration happens or some necessary
  437. steps are skipped, issues can occur during processing. (The most common
  438. reason for this is missing debug symbols.) In these cases you can see
  439. all the problems here with guides of how to correct them.`
  440. )}
  441. </TextBlock>
  442. {this.renderDebugTable()}
  443. {this.renderResolveButton()}
  444. {this.renderReprocessingSettings()}
  445. </div>
  446. );
  447. }
  448. }
  449. const StyledPanelAlert = styled(PanelAlert)`
  450. grid-column: 1/5;
  451. `;
  452. export {ProjectProcessingIssues};
  453. export default withApi(withOrganization(ProjectProcessingIssues));