123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import {Fragment} from 'react';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import partition from 'lodash/partition';
- import sortBy from 'lodash/sortBy';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import {ModalRenderProps} from 'sentry/actionCreators/modal';
- import AsyncComponent from 'sentry/components/asyncComponent';
- import Button from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {Organization, Project} from 'sentry/types';
- import {DebugFile, DebugFileFeature} from 'sentry/types/debugFiles';
- import {CandidateDownloadStatus, Image, ImageStatus} from 'sentry/types/debugImage';
- import {Event} from 'sentry/types/event';
- import {displayReprocessEventAction} from 'sentry/utils/displayReprocessEventAction';
- import theme from 'sentry/utils/theme';
- import {getFileType} from 'sentry/views/settings/projectDebugFiles/utils';
- import {getFileName} from '../utils';
- import Candidates from './candidates';
- import GeneralInfo from './generalInfo';
- import ReprocessAlert from './reprocessAlert';
- import {INTERNAL_SOURCE, INTERNAL_SOURCE_LOCATION} from './utils';
- type ImageCandidates = Image['candidates'];
- type Props = AsyncComponent['props'] &
- ModalRenderProps & {
- event: Event;
- organization: Organization;
- projSlug: Project['slug'];
- image?: Image & {status: ImageStatus};
- onReprocessEvent?: () => void;
- };
- type State = AsyncComponent['state'] & {
- debugFiles: Array<DebugFile> | null;
- };
- class DebugImageDetails extends AsyncComponent<Props, State> {
- getDefaultState(): State {
- return {
- ...super.getDefaultState(),
- debugFiles: [],
- };
- }
- componentDidUpdate(prevProps: Props, prevState: State) {
- if (!prevProps.image && !!this.props.image) {
- this.remountComponent();
- }
- super.componentDidUpdate(prevProps, prevState);
- }
- getUplodedDebugFiles(candidates: ImageCandidates) {
- return candidates.find(candidate => candidate.source === INTERNAL_SOURCE);
- }
- getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
- const {organization, projSlug, image} = this.props;
- if (!image) {
- return [];
- }
- const {debug_id, candidates = []} = image;
- const uploadedDebugFiles = this.getUplodedDebugFiles(candidates);
- const endpoints: ReturnType<AsyncComponent['getEndpoints']> = [];
- if (uploadedDebugFiles) {
- endpoints.push([
- 'debugFiles',
- `/projects/${organization.slug}/${projSlug}/files/dsyms/?debug_id=${debug_id}`,
- {
- query: {
- file_formats: ['breakpad', 'macho', 'elf', 'pe', 'pdb', 'sourcebundle'],
- },
- },
- ]);
- }
- return endpoints;
- }
- sortCandidates(
- candidates: ImageCandidates,
- unAppliedCandidates: ImageCandidates
- ): ImageCandidates {
- const [noPermissionCandidates, restNoPermissionCandidates] = partition(
- candidates,
- candidate => candidate.download.status === CandidateDownloadStatus.NO_PERMISSION
- );
- const [malFormedCandidates, restMalFormedCandidates] = partition(
- restNoPermissionCandidates,
- candidate => candidate.download.status === CandidateDownloadStatus.MALFORMED
- );
- const [errorCandidates, restErrorCandidates] = partition(
- restMalFormedCandidates,
- candidate => candidate.download.status === CandidateDownloadStatus.ERROR
- );
- const [okCandidates, restOKCandidates] = partition(
- restErrorCandidates,
- candidate => candidate.download.status === CandidateDownloadStatus.OK
- );
- const [deletedCandidates, notFoundCandidates] = partition(
- restOKCandidates,
- candidate => candidate.download.status === CandidateDownloadStatus.DELETED
- );
- return [
- ...sortBy(noPermissionCandidates, ['source_name', 'location']),
- ...sortBy(malFormedCandidates, ['source_name', 'location']),
- ...sortBy(errorCandidates, ['source_name', 'location']),
- ...sortBy(okCandidates, ['source_name', 'location']),
- ...sortBy(deletedCandidates, ['source_name', 'location']),
- ...sortBy(unAppliedCandidates, ['source_name', 'location']),
- ...sortBy(notFoundCandidates, ['source_name', 'location']),
- ];
- }
- getCandidates() {
- const {debugFiles, loading} = this.state;
- const {image} = this.props;
- const {candidates = []} = image ?? {};
- if (!debugFiles || loading) {
- return candidates;
- }
- const debugFileCandidates = candidates.map(({location, ...candidate}) => ({
- ...candidate,
- location: location?.includes(INTERNAL_SOURCE_LOCATION)
- ? location.split(INTERNAL_SOURCE_LOCATION)[1]
- : location,
- }));
- const candidateLocations = new Set(
- debugFileCandidates.map(({location}) => location).filter(location => !!location)
- );
- const [unAppliedDebugFiles, appliedDebugFiles] = partition(
- debugFiles,
- debugFile => !candidateLocations.has(debugFile.id)
- );
- const unAppliedCandidates = unAppliedDebugFiles.map(debugFile => {
- const {
- data,
- symbolType,
- objectName: filename,
- id: location,
- size,
- dateCreated,
- cpuName,
- } = debugFile;
- const features = data?.features ?? [];
- return {
- download: {
- status: CandidateDownloadStatus.UNAPPLIED,
- features: {
- has_sources: features.includes(DebugFileFeature.SOURCES),
- has_debug_info: features.includes(DebugFileFeature.DEBUG),
- has_unwind_info: features.includes(DebugFileFeature.UNWIND),
- has_symbols: features.includes(DebugFileFeature.SYMTAB),
- },
- },
- cpuName,
- location,
- filename,
- size,
- dateCreated,
- symbolType,
- fileType: getFileType(debugFile),
- source: INTERNAL_SOURCE,
- source_name: t('Sentry'),
- };
- });
- const [debugFileInternalOkCandidates, debugFileOtherCandidates] = partition(
- debugFileCandidates,
- debugFileCandidate =>
- debugFileCandidate.download.status === CandidateDownloadStatus.OK &&
- debugFileCandidate.source === INTERNAL_SOURCE
- );
- const convertedDebugFileInternalOkCandidates = debugFileInternalOkCandidates.map(
- debugFileOkCandidate => {
- const internalDebugFileInfo = appliedDebugFiles.find(
- appliedDebugFile => appliedDebugFile.id === debugFileOkCandidate.location
- );
- if (!internalDebugFileInfo) {
- return {
- ...debugFileOkCandidate,
- download: {
- ...debugFileOkCandidate.download,
- status: CandidateDownloadStatus.DELETED,
- },
- };
- }
- const {
- symbolType,
- objectName: filename,
- id: location,
- size,
- dateCreated,
- cpuName,
- } = internalDebugFileInfo;
- return {
- ...debugFileOkCandidate,
- cpuName,
- location,
- filename,
- size,
- dateCreated,
- symbolType,
- fileType: getFileType(internalDebugFileInfo),
- };
- }
- );
- return this.sortCandidates(
- [
- ...convertedDebugFileInternalOkCandidates,
- ...debugFileOtherCandidates,
- ] as ImageCandidates,
- unAppliedCandidates as ImageCandidates
- );
- }
- handleDelete = async (debugId: string) => {
- const {organization, projSlug} = this.props;
- this.setState({loading: true});
- try {
- await this.api.requestPromise(
- `/projects/${organization.slug}/${projSlug}/files/dsyms/?id=${debugId}`,
- {method: 'DELETE'}
- );
- this.fetchData();
- } catch {
- addErrorMessage(t('An error occurred while deleting the debug file.'));
- this.setState({loading: false});
- }
- };
- getDebugFilesSettingsLink() {
- const {organization, projSlug, image} = this.props;
- const orgSlug = organization.slug;
- const debugId = image?.debug_id;
- if (!orgSlug || !projSlug || !debugId) {
- return undefined;
- }
- return `/settings/${orgSlug}/projects/${projSlug}/debug-symbols/?query=${debugId}`;
- }
- renderBody() {
- const {Header, Body, Footer, image, organization, projSlug, event, onReprocessEvent} =
- this.props;
- const {loading} = this.state;
- const {code_file, status} = image ?? {};
- const debugFilesSettingsLink = this.getDebugFilesSettingsLink();
- const candidates = this.getCandidates();
- const baseUrl = this.api.baseUrl;
- const fileName = getFileName(code_file);
- const haveCandidatesUnappliedDebugFile = candidates.some(
- candidate => candidate.download.status === CandidateDownloadStatus.UNAPPLIED
- );
- const hasReprocessWarning =
- haveCandidatesUnappliedDebugFile &&
- displayReprocessEventAction(organization.features, event) &&
- !!onReprocessEvent;
- return (
- <Fragment>
- <Header closeButton>
- <Title>
- {t('Image')}
- <FileName>{fileName ?? t('Unknown')}</FileName>
- </Title>
- </Header>
- <Body>
- <Content>
- <GeneralInfo image={image} />
- {hasReprocessWarning && (
- <ReprocessAlert
- api={this.api}
- orgSlug={organization.slug}
- projSlug={projSlug}
- eventId={event.id}
- onReprocessEvent={onReprocessEvent}
- />
- )}
- <Candidates
- imageStatus={status}
- candidates={candidates}
- organization={organization}
- projSlug={projSlug}
- baseUrl={baseUrl}
- isLoading={loading}
- eventDateReceived={event.dateReceived}
- onDelete={this.handleDelete}
- hasReprocessWarning={hasReprocessWarning}
- />
- </Content>
- </Body>
- <Footer>
- <StyledButtonBar gap={1}>
- <Button
- href="https://docs.sentry.io/platforms/native/data-management/debug-files/"
- external
- >
- {t('Read the docs')}
- </Button>
- {debugFilesSettingsLink && (
- <Button
- title={t(
- 'Search for this debug file in all images for the %s project',
- projSlug
- )}
- to={debugFilesSettingsLink}
- >
- {t('Open in Settings')}
- </Button>
- )}
- </StyledButtonBar>
- </Footer>
- </Fragment>
- );
- }
- }
- export default DebugImageDetails;
- const Content = styled('div')`
- display: grid;
- gap: ${space(3)};
- font-size: ${p => p.theme.fontSizeMedium};
- `;
- const Title = styled('div')`
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: ${space(1)};
- align-items: center;
- font-size: ${p => p.theme.fontSizeExtraLarge};
- max-width: calc(100% - 40px);
- word-break: break-all;
- `;
- const FileName = styled('span')`
- font-family: ${p => p.theme.text.familyMono};
- `;
- const StyledButtonBar = styled(ButtonBar)`
- white-space: nowrap;
- `;
- export const modalCss = css`
- [role='document'] {
- overflow: initial;
- }
- @media (min-width: ${theme.breakpoints.small}) {
- width: 90%;
- }
- @media (min-width: ${theme.breakpoints.xlarge}) {
- width: 70%;
- }
- @media (min-width: ${theme.breakpoints.xxlarge}) {
- width: 50%;
- }
- `;
|