index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import partition from 'lodash/partition';
  5. import sortBy from 'lodash/sortBy';
  6. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  7. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  8. import AsyncComponent from 'sentry/components/asyncComponent';
  9. import Button from 'sentry/components/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import {t} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {Organization, Project} from 'sentry/types';
  14. import {DebugFile, DebugFileFeature} from 'sentry/types/debugFiles';
  15. import {CandidateDownloadStatus, Image, ImageStatus} from 'sentry/types/debugImage';
  16. import {Event} from 'sentry/types/event';
  17. import {displayReprocessEventAction} from 'sentry/utils/displayReprocessEventAction';
  18. import theme from 'sentry/utils/theme';
  19. import {getFileType} from 'sentry/views/settings/projectDebugFiles/utils';
  20. import {getFileName} from '../utils';
  21. import Candidates from './candidates';
  22. import GeneralInfo from './generalInfo';
  23. import ReprocessAlert from './reprocessAlert';
  24. import {INTERNAL_SOURCE, INTERNAL_SOURCE_LOCATION} from './utils';
  25. type ImageCandidates = Image['candidates'];
  26. type Props = AsyncComponent['props'] &
  27. ModalRenderProps & {
  28. event: Event;
  29. organization: Organization;
  30. projSlug: Project['slug'];
  31. image?: Image & {status: ImageStatus};
  32. onReprocessEvent?: () => void;
  33. };
  34. type State = AsyncComponent['state'] & {
  35. debugFiles: Array<DebugFile> | null;
  36. };
  37. class DebugImageDetails extends AsyncComponent<Props, State> {
  38. getDefaultState(): State {
  39. return {
  40. ...super.getDefaultState(),
  41. debugFiles: [],
  42. };
  43. }
  44. componentDidUpdate(prevProps: Props, prevState: State) {
  45. if (!prevProps.image && !!this.props.image) {
  46. this.remountComponent();
  47. }
  48. super.componentDidUpdate(prevProps, prevState);
  49. }
  50. getUplodedDebugFiles(candidates: ImageCandidates) {
  51. return candidates.find(candidate => candidate.source === INTERNAL_SOURCE);
  52. }
  53. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  54. const {organization, projSlug, image} = this.props;
  55. if (!image) {
  56. return [];
  57. }
  58. const {debug_id, candidates = []} = image;
  59. const uploadedDebugFiles = this.getUplodedDebugFiles(candidates);
  60. const endpoints: ReturnType<AsyncComponent['getEndpoints']> = [];
  61. if (uploadedDebugFiles) {
  62. endpoints.push([
  63. 'debugFiles',
  64. `/projects/${organization.slug}/${projSlug}/files/dsyms/?debug_id=${debug_id}`,
  65. {
  66. query: {
  67. file_formats: ['breakpad', 'macho', 'elf', 'pe', 'pdb', 'sourcebundle'],
  68. },
  69. },
  70. ]);
  71. }
  72. return endpoints;
  73. }
  74. sortCandidates(
  75. candidates: ImageCandidates,
  76. unAppliedCandidates: ImageCandidates
  77. ): ImageCandidates {
  78. const [noPermissionCandidates, restNoPermissionCandidates] = partition(
  79. candidates,
  80. candidate => candidate.download.status === CandidateDownloadStatus.NO_PERMISSION
  81. );
  82. const [malFormedCandidates, restMalFormedCandidates] = partition(
  83. restNoPermissionCandidates,
  84. candidate => candidate.download.status === CandidateDownloadStatus.MALFORMED
  85. );
  86. const [errorCandidates, restErrorCandidates] = partition(
  87. restMalFormedCandidates,
  88. candidate => candidate.download.status === CandidateDownloadStatus.ERROR
  89. );
  90. const [okCandidates, restOKCandidates] = partition(
  91. restErrorCandidates,
  92. candidate => candidate.download.status === CandidateDownloadStatus.OK
  93. );
  94. const [deletedCandidates, notFoundCandidates] = partition(
  95. restOKCandidates,
  96. candidate => candidate.download.status === CandidateDownloadStatus.DELETED
  97. );
  98. return [
  99. ...sortBy(noPermissionCandidates, ['source_name', 'location']),
  100. ...sortBy(malFormedCandidates, ['source_name', 'location']),
  101. ...sortBy(errorCandidates, ['source_name', 'location']),
  102. ...sortBy(okCandidates, ['source_name', 'location']),
  103. ...sortBy(deletedCandidates, ['source_name', 'location']),
  104. ...sortBy(unAppliedCandidates, ['source_name', 'location']),
  105. ...sortBy(notFoundCandidates, ['source_name', 'location']),
  106. ];
  107. }
  108. getCandidates() {
  109. const {debugFiles, loading} = this.state;
  110. const {image} = this.props;
  111. const {candidates = []} = image ?? {};
  112. if (!debugFiles || loading) {
  113. return candidates;
  114. }
  115. const debugFileCandidates = candidates.map(({location, ...candidate}) => ({
  116. ...candidate,
  117. location: location?.includes(INTERNAL_SOURCE_LOCATION)
  118. ? location.split(INTERNAL_SOURCE_LOCATION)[1]
  119. : location,
  120. }));
  121. const candidateLocations = new Set(
  122. debugFileCandidates.map(({location}) => location).filter(location => !!location)
  123. );
  124. const [unAppliedDebugFiles, appliedDebugFiles] = partition(
  125. debugFiles,
  126. debugFile => !candidateLocations.has(debugFile.id)
  127. );
  128. const unAppliedCandidates = unAppliedDebugFiles.map(debugFile => {
  129. const {
  130. data,
  131. symbolType,
  132. objectName: filename,
  133. id: location,
  134. size,
  135. dateCreated,
  136. cpuName,
  137. } = debugFile;
  138. const features = data?.features ?? [];
  139. return {
  140. download: {
  141. status: CandidateDownloadStatus.UNAPPLIED,
  142. features: {
  143. has_sources: features.includes(DebugFileFeature.SOURCES),
  144. has_debug_info: features.includes(DebugFileFeature.DEBUG),
  145. has_unwind_info: features.includes(DebugFileFeature.UNWIND),
  146. has_symbols: features.includes(DebugFileFeature.SYMTAB),
  147. },
  148. },
  149. cpuName,
  150. location,
  151. filename,
  152. size,
  153. dateCreated,
  154. symbolType,
  155. fileType: getFileType(debugFile),
  156. source: INTERNAL_SOURCE,
  157. source_name: t('Sentry'),
  158. };
  159. });
  160. const [debugFileInternalOkCandidates, debugFileOtherCandidates] = partition(
  161. debugFileCandidates,
  162. debugFileCandidate =>
  163. debugFileCandidate.download.status === CandidateDownloadStatus.OK &&
  164. debugFileCandidate.source === INTERNAL_SOURCE
  165. );
  166. const convertedDebugFileInternalOkCandidates = debugFileInternalOkCandidates.map(
  167. debugFileOkCandidate => {
  168. const internalDebugFileInfo = appliedDebugFiles.find(
  169. appliedDebugFile => appliedDebugFile.id === debugFileOkCandidate.location
  170. );
  171. if (!internalDebugFileInfo) {
  172. return {
  173. ...debugFileOkCandidate,
  174. download: {
  175. ...debugFileOkCandidate.download,
  176. status: CandidateDownloadStatus.DELETED,
  177. },
  178. };
  179. }
  180. const {
  181. symbolType,
  182. objectName: filename,
  183. id: location,
  184. size,
  185. dateCreated,
  186. cpuName,
  187. } = internalDebugFileInfo;
  188. return {
  189. ...debugFileOkCandidate,
  190. cpuName,
  191. location,
  192. filename,
  193. size,
  194. dateCreated,
  195. symbolType,
  196. fileType: getFileType(internalDebugFileInfo),
  197. };
  198. }
  199. );
  200. return this.sortCandidates(
  201. [
  202. ...convertedDebugFileInternalOkCandidates,
  203. ...debugFileOtherCandidates,
  204. ] as ImageCandidates,
  205. unAppliedCandidates as ImageCandidates
  206. );
  207. }
  208. handleDelete = async (debugId: string) => {
  209. const {organization, projSlug} = this.props;
  210. this.setState({loading: true});
  211. try {
  212. await this.api.requestPromise(
  213. `/projects/${organization.slug}/${projSlug}/files/dsyms/?id=${debugId}`,
  214. {method: 'DELETE'}
  215. );
  216. this.fetchData();
  217. } catch {
  218. addErrorMessage(t('An error occurred while deleting the debug file.'));
  219. this.setState({loading: false});
  220. }
  221. };
  222. getDebugFilesSettingsLink() {
  223. const {organization, projSlug, image} = this.props;
  224. const orgSlug = organization.slug;
  225. const debugId = image?.debug_id;
  226. if (!orgSlug || !projSlug || !debugId) {
  227. return undefined;
  228. }
  229. return `/settings/${orgSlug}/projects/${projSlug}/debug-symbols/?query=${debugId}`;
  230. }
  231. renderBody() {
  232. const {Header, Body, Footer, image, organization, projSlug, event, onReprocessEvent} =
  233. this.props;
  234. const {loading} = this.state;
  235. const {code_file, status} = image ?? {};
  236. const debugFilesSettingsLink = this.getDebugFilesSettingsLink();
  237. const candidates = this.getCandidates();
  238. const baseUrl = this.api.baseUrl;
  239. const fileName = getFileName(code_file);
  240. const haveCandidatesUnappliedDebugFile = candidates.some(
  241. candidate => candidate.download.status === CandidateDownloadStatus.UNAPPLIED
  242. );
  243. const hasReprocessWarning =
  244. haveCandidatesUnappliedDebugFile &&
  245. displayReprocessEventAction(organization.features, event) &&
  246. !!onReprocessEvent;
  247. return (
  248. <Fragment>
  249. <Header closeButton>
  250. <Title>
  251. {t('Image')}
  252. <FileName>{fileName ?? t('Unknown')}</FileName>
  253. </Title>
  254. </Header>
  255. <Body>
  256. <Content>
  257. <GeneralInfo image={image} />
  258. {hasReprocessWarning && (
  259. <ReprocessAlert
  260. api={this.api}
  261. orgSlug={organization.slug}
  262. projSlug={projSlug}
  263. eventId={event.id}
  264. onReprocessEvent={onReprocessEvent}
  265. />
  266. )}
  267. <Candidates
  268. imageStatus={status}
  269. candidates={candidates}
  270. organization={organization}
  271. projSlug={projSlug}
  272. baseUrl={baseUrl}
  273. isLoading={loading}
  274. eventDateReceived={event.dateReceived}
  275. onDelete={this.handleDelete}
  276. hasReprocessWarning={hasReprocessWarning}
  277. />
  278. </Content>
  279. </Body>
  280. <Footer>
  281. <StyledButtonBar gap={1}>
  282. <Button
  283. href="https://docs.sentry.io/platforms/native/data-management/debug-files/"
  284. external
  285. >
  286. {t('Read the docs')}
  287. </Button>
  288. {debugFilesSettingsLink && (
  289. <Button
  290. title={t(
  291. 'Search for this debug file in all images for the %s project',
  292. projSlug
  293. )}
  294. to={debugFilesSettingsLink}
  295. >
  296. {t('Open in Settings')}
  297. </Button>
  298. )}
  299. </StyledButtonBar>
  300. </Footer>
  301. </Fragment>
  302. );
  303. }
  304. }
  305. export default DebugImageDetails;
  306. const Content = styled('div')`
  307. display: grid;
  308. gap: ${space(3)};
  309. font-size: ${p => p.theme.fontSizeMedium};
  310. `;
  311. const Title = styled('div')`
  312. display: grid;
  313. grid-template-columns: max-content 1fr;
  314. gap: ${space(1)};
  315. align-items: center;
  316. font-size: ${p => p.theme.fontSizeExtraLarge};
  317. max-width: calc(100% - 40px);
  318. word-break: break-all;
  319. `;
  320. const FileName = styled('span')`
  321. font-family: ${p => p.theme.text.familyMono};
  322. `;
  323. const StyledButtonBar = styled(ButtonBar)`
  324. white-space: nowrap;
  325. `;
  326. export const modalCss = css`
  327. [role='document'] {
  328. overflow: initial;
  329. }
  330. @media (min-width: ${theme.breakpoints.small}) {
  331. width: 90%;
  332. }
  333. @media (min-width: ${theme.breakpoints.xlarge}) {
  334. width: 70%;
  335. }
  336. @media (min-width: ${theme.breakpoints.xxlarge}) {
  337. width: 50%;
  338. }
  339. `;