123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- import {Fragment, memo} from 'react';
- import styled from '@emotion/styled';
- import isNil from 'lodash/isNil';
- import Access from 'sentry/components/acl/access';
- import Button from 'sentry/components/button';
- import DebugFileFeature from 'sentry/components/debugFileFeature';
- import {formatAddress, getImageRange} from 'sentry/components/events/interfaces/utils';
- import {PanelItem} from 'sentry/components/panels';
- import Tooltip from 'sentry/components/tooltip';
- import {IconCheckmark, IconCircle, IconFlag, IconSearch} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {Organization, Project} from 'sentry/types';
- import {DebugImage as DebugImageType, DebugStatus} from './types';
- import {combineStatus, getFileName} from './utils';
- type Status = ReturnType<typeof combineStatus>;
- const IMAGE_ADDR_LEN = 12;
- function getImageStatusText(status: Status) {
- switch (status) {
- case 'found':
- return t('ok');
- case 'unused':
- return t('unused');
- case 'missing':
- return t('missing');
- case 'malformed':
- case 'fetching_failed':
- case 'timeout':
- case 'other':
- return t('failed');
- default:
- return null;
- }
- }
- function getImageStatusDetails(status: Status) {
- switch (status) {
- case 'found':
- return t('Debug information for this image was found and successfully processed.');
- case 'unused':
- return t('The image was not required for processing the stack trace.');
- case 'missing':
- return t('No debug information could be found in any of the specified sources.');
- case 'malformed':
- return t('The debug information file for this image failed to process.');
- case 'timeout':
- case 'fetching_failed':
- return t('The debug information file for this image could not be downloaded.');
- case 'other':
- return t('An internal error occurred while handling this image.');
- default:
- return null;
- }
- }
- type Props = {
- image: DebugImageType;
- organization: Organization;
- projectId: Project['id'];
- showDetails: boolean;
- style?: React.CSSProperties;
- };
- const DebugImage = memo(({image, organization, projectId, showDetails, style}: Props) => {
- const orgSlug = organization.slug;
- const getSettingsLink = () => {
- if (!orgSlug || !projectId || !image.debug_id) {
- return null;
- }
- return `/settings/${orgSlug}/projects/${projectId}/debug-symbols/?query=${image.debug_id}`;
- };
- const renderStatus = (title: string, status: DebugStatus) => {
- if (isNil(status)) {
- return null;
- }
- const text = getImageStatusText(status);
- if (!text) {
- return null;
- }
- return (
- <SymbolicationStatus>
- <Tooltip title={getImageStatusDetails(status)}>
- <span>
- <ImageProp>{title}</ImageProp>: {text}
- </span>
- </Tooltip>
- </SymbolicationStatus>
- );
- };
- const combinedStatus = combineStatus(image.debug_status, image.unwind_status);
- const [startAddress, endAddress] = getImageRange(image);
- const renderIconElement = () => {
- switch (combinedStatus) {
- case 'unused':
- return (
- <IconWrapper>
- <IconCircle />
- </IconWrapper>
- );
- case 'found':
- return (
- <IconWrapper>
- <IconCheckmark isCircled color="green300" />
- </IconWrapper>
- );
- default:
- return (
- <IconWrapper>
- <IconFlag color="red300" />
- </IconWrapper>
- );
- }
- };
- const codeFile = getFileName(image.code_file);
- const debugFile = image.debug_file && getFileName(image.debug_file);
- // The debug file is only realistically set on Windows. All other platforms
- // either leave it empty or set it to a filename that's equal to the code
- // file name. In this case, do not show it.
- const showDebugFile = debugFile && codeFile !== debugFile;
- // Availability only makes sense if the image is actually referenced.
- // Otherwise, the processing pipeline does not resolve this kind of
- // information and it will always be false.
- const showAvailability = !isNil(image.features) && combinedStatus !== 'unused';
- // The code id is sometimes missing, and sometimes set to the equivalent of
- // the debug id (e.g. for Mach symbols). In this case, it is redundant
- // information and we do not want to show it.
- const showCodeId = !!image.code_id && image.code_id !== image.debug_id;
- // Old versions of the event pipeline did not store the symbolication
- // status. In this case, default to display the debug_id instead of stack
- // unwind information.
- const legacyRender = isNil(image.debug_status);
- const debugIdElement = (
- <ImageSubtext>
- <ImageProp>{t('Debug ID')}</ImageProp>: <Formatted>{image.debug_id}</Formatted>
- </ImageSubtext>
- );
- const formattedImageStartAddress = startAddress ? (
- <Formatted>{formatAddress(startAddress, IMAGE_ADDR_LEN)}</Formatted>
- ) : null;
- const formattedImageEndAddress = endAddress ? (
- <Formatted>{formatAddress(endAddress, IMAGE_ADDR_LEN)}</Formatted>
- ) : null;
- return (
- <DebugImageItem style={style}>
- <ImageInfoGroup>{renderIconElement()}</ImageInfoGroup>
- <ImageInfoGroup>
- {startAddress && endAddress ? (
- <Fragment>
- {formattedImageStartAddress}
- {' \u2013 '}
- <AddressDivider />
- {formattedImageEndAddress}
- </Fragment>
- ) : null}
- </ImageInfoGroup>
- <ImageInfoGroup fullWidth>
- <ImageTitle>
- <Tooltip title={image.code_file}>
- <CodeFile>{codeFile}</CodeFile>
- </Tooltip>
- {showDebugFile && <DebugFile> ({debugFile})</DebugFile>}
- </ImageTitle>
- {legacyRender ? (
- debugIdElement
- ) : (
- <StatusLine>
- {renderStatus(t('Stack Unwinding'), image.unwind_status)}
- {renderStatus(t('Symbolication'), image.debug_status)}
- </StatusLine>
- )}
- {showDetails && (
- <Fragment>
- {showAvailability && (
- <ImageSubtext>
- <ImageProp>{t('Availability')}</ImageProp>:
- <DebugFileFeature
- feature="symtab"
- available={image.features?.has_symbols}
- />
- <DebugFileFeature
- feature="debug"
- available={image.features?.has_debug_info}
- />
- <DebugFileFeature
- feature="unwind"
- available={image.features?.has_unwind_info}
- />
- <DebugFileFeature
- feature="sources"
- available={image.features?.has_sources}
- />
- </ImageSubtext>
- )}
- {!legacyRender && debugIdElement}
- {showCodeId && (
- <ImageSubtext>
- <ImageProp>{t('Code ID')}</ImageProp>:{' '}
- <Formatted>{image.code_id}</Formatted>
- </ImageSubtext>
- )}
- {!!image.arch && (
- <ImageSubtext>
- <ImageProp>{t('Architecture')}</ImageProp>: {image.arch}
- </ImageSubtext>
- )}
- </Fragment>
- )}
- </ImageInfoGroup>
- <Access access={['project:releases']}>
- {({hasAccess}) => {
- if (!hasAccess) {
- return null;
- }
- const settingsUrl = getSettingsLink();
- if (!settingsUrl) {
- return null;
- }
- return (
- <ImageActions>
- <Button
- size="xs"
- icon={<IconSearch size="xs" />}
- to={settingsUrl}
- title={t('Search for debug files in settings')}
- aria-label={t('Search for debug files in settings')}
- />
- </ImageActions>
- );
- }}
- </Access>
- </DebugImageItem>
- );
- });
- export default DebugImage;
- const DebugImageItem = styled(PanelItem)`
- font-size: ${p => p.theme.fontSizeSmall};
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- display: grid;
- gap: ${space(1)};
- position: relative;
- }
- `;
- const Formatted = styled('span')`
- font-family: ${p => p.theme.text.familyMono};
- `;
- const ImageInfoGroup = styled('div')<{fullWidth?: boolean}>`
- margin-left: 1em;
- flex-grow: ${p => (p.fullWidth ? 1 : null)};
- &:first-child {
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- margin-left: 0;
- }
- }
- `;
- const ImageActions = styled(ImageInfoGroup)`
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- position: absolute;
- top: 15px;
- right: 20px;
- }
- display: flex;
- align-items: center;
- `;
- const ImageTitle = styled('div')`
- font-size: ${p => p.theme.fontSizeLarge};
- `;
- const CodeFile = styled('span')`
- font-weight: bold;
- `;
- const DebugFile = styled('span')`
- color: ${p => p.theme.gray300};
- `;
- const ImageSubtext = styled('div')`
- color: ${p => p.theme.gray300};
- `;
- const ImageProp = styled('span')`
- font-weight: bold;
- `;
- const StatusLine = styled(ImageSubtext)`
- display: flex;
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- display: grid;
- }
- `;
- const AddressDivider = styled('br')`
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- display: none;
- }
- `;
- const IconWrapper = styled('span')`
- display: inline-block;
- margin-top: ${space(0.5)};
- height: 16px;
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- margin-top: ${space(0.25)};
- }
- `;
- const SymbolicationStatus = styled('span')`
- flex-grow: 1;
- flex-basis: 0;
- margin-right: 1em;
- svg {
- margin-left: 0.66ex;
- }
- `;
|