debugImage.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import {Fragment, memo} from 'react';
  2. import styled from '@emotion/styled';
  3. import isNil from 'lodash/isNil';
  4. import Access from 'sentry/components/acl/access';
  5. import Button from 'sentry/components/button';
  6. import DebugFileFeature from 'sentry/components/debugFileFeature';
  7. import {formatAddress, getImageRange} from 'sentry/components/events/interfaces/utils';
  8. import {PanelItem} from 'sentry/components/panels';
  9. import Tooltip from 'sentry/components/tooltip';
  10. import {IconCheckmark, IconCircle, IconFlag, IconSearch} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {Organization, Project} from 'sentry/types';
  14. import {DebugImage as DebugImageType, DebugStatus} from './types';
  15. import {combineStatus, getFileName} from './utils';
  16. type Status = ReturnType<typeof combineStatus>;
  17. const IMAGE_ADDR_LEN = 12;
  18. function getImageStatusText(status: Status) {
  19. switch (status) {
  20. case 'found':
  21. return t('ok');
  22. case 'unused':
  23. return t('unused');
  24. case 'missing':
  25. return t('missing');
  26. case 'malformed':
  27. case 'fetching_failed':
  28. case 'timeout':
  29. case 'other':
  30. return t('failed');
  31. default:
  32. return null;
  33. }
  34. }
  35. function getImageStatusDetails(status: Status) {
  36. switch (status) {
  37. case 'found':
  38. return t('Debug information for this image was found and successfully processed.');
  39. case 'unused':
  40. return t('The image was not required for processing the stack trace.');
  41. case 'missing':
  42. return t('No debug information could be found in any of the specified sources.');
  43. case 'malformed':
  44. return t('The debug information file for this image failed to process.');
  45. case 'timeout':
  46. case 'fetching_failed':
  47. return t('The debug information file for this image could not be downloaded.');
  48. case 'other':
  49. return t('An internal error occurred while handling this image.');
  50. default:
  51. return null;
  52. }
  53. }
  54. type Props = {
  55. image: DebugImageType;
  56. organization: Organization;
  57. projectId: Project['id'];
  58. showDetails: boolean;
  59. style?: React.CSSProperties;
  60. };
  61. const DebugImage = memo(({image, organization, projectId, showDetails, style}: Props) => {
  62. const orgSlug = organization.slug;
  63. const getSettingsLink = () => {
  64. if (!orgSlug || !projectId || !image.debug_id) {
  65. return null;
  66. }
  67. return `/settings/${orgSlug}/projects/${projectId}/debug-symbols/?query=${image.debug_id}`;
  68. };
  69. const renderStatus = (title: string, status: DebugStatus) => {
  70. if (isNil(status)) {
  71. return null;
  72. }
  73. const text = getImageStatusText(status);
  74. if (!text) {
  75. return null;
  76. }
  77. return (
  78. <SymbolicationStatus>
  79. <Tooltip title={getImageStatusDetails(status)}>
  80. <span>
  81. <ImageProp>{title}</ImageProp>: {text}
  82. </span>
  83. </Tooltip>
  84. </SymbolicationStatus>
  85. );
  86. };
  87. const combinedStatus = combineStatus(image.debug_status, image.unwind_status);
  88. const [startAddress, endAddress] = getImageRange(image);
  89. const renderIconElement = () => {
  90. switch (combinedStatus) {
  91. case 'unused':
  92. return (
  93. <IconWrapper>
  94. <IconCircle />
  95. </IconWrapper>
  96. );
  97. case 'found':
  98. return (
  99. <IconWrapper>
  100. <IconCheckmark isCircled color="green300" />
  101. </IconWrapper>
  102. );
  103. default:
  104. return (
  105. <IconWrapper>
  106. <IconFlag color="red300" />
  107. </IconWrapper>
  108. );
  109. }
  110. };
  111. const codeFile = getFileName(image.code_file);
  112. const debugFile = image.debug_file && getFileName(image.debug_file);
  113. // The debug file is only realistically set on Windows. All other platforms
  114. // either leave it empty or set it to a filename that's equal to the code
  115. // file name. In this case, do not show it.
  116. const showDebugFile = debugFile && codeFile !== debugFile;
  117. // Availability only makes sense if the image is actually referenced.
  118. // Otherwise, the processing pipeline does not resolve this kind of
  119. // information and it will always be false.
  120. const showAvailability = !isNil(image.features) && combinedStatus !== 'unused';
  121. // The code id is sometimes missing, and sometimes set to the equivalent of
  122. // the debug id (e.g. for Mach symbols). In this case, it is redundant
  123. // information and we do not want to show it.
  124. const showCodeId = !!image.code_id && image.code_id !== image.debug_id;
  125. // Old versions of the event pipeline did not store the symbolication
  126. // status. In this case, default to display the debug_id instead of stack
  127. // unwind information.
  128. const legacyRender = isNil(image.debug_status);
  129. const debugIdElement = (
  130. <ImageSubtext>
  131. <ImageProp>{t('Debug ID')}</ImageProp>: <Formatted>{image.debug_id}</Formatted>
  132. </ImageSubtext>
  133. );
  134. const formattedImageStartAddress = startAddress ? (
  135. <Formatted>{formatAddress(startAddress, IMAGE_ADDR_LEN)}</Formatted>
  136. ) : null;
  137. const formattedImageEndAddress = endAddress ? (
  138. <Formatted>{formatAddress(endAddress, IMAGE_ADDR_LEN)}</Formatted>
  139. ) : null;
  140. return (
  141. <DebugImageItem style={style}>
  142. <ImageInfoGroup>{renderIconElement()}</ImageInfoGroup>
  143. <ImageInfoGroup>
  144. {startAddress && endAddress ? (
  145. <Fragment>
  146. {formattedImageStartAddress}
  147. {' \u2013 '}
  148. <AddressDivider />
  149. {formattedImageEndAddress}
  150. </Fragment>
  151. ) : null}
  152. </ImageInfoGroup>
  153. <ImageInfoGroup fullWidth>
  154. <ImageTitle>
  155. <Tooltip title={image.code_file}>
  156. <CodeFile>{codeFile}</CodeFile>
  157. </Tooltip>
  158. {showDebugFile && <DebugFile> ({debugFile})</DebugFile>}
  159. </ImageTitle>
  160. {legacyRender ? (
  161. debugIdElement
  162. ) : (
  163. <StatusLine>
  164. {renderStatus(t('Stack Unwinding'), image.unwind_status)}
  165. {renderStatus(t('Symbolication'), image.debug_status)}
  166. </StatusLine>
  167. )}
  168. {showDetails && (
  169. <Fragment>
  170. {showAvailability && (
  171. <ImageSubtext>
  172. <ImageProp>{t('Availability')}</ImageProp>:
  173. <DebugFileFeature
  174. feature="symtab"
  175. available={image.features?.has_symbols}
  176. />
  177. <DebugFileFeature
  178. feature="debug"
  179. available={image.features?.has_debug_info}
  180. />
  181. <DebugFileFeature
  182. feature="unwind"
  183. available={image.features?.has_unwind_info}
  184. />
  185. <DebugFileFeature
  186. feature="sources"
  187. available={image.features?.has_sources}
  188. />
  189. </ImageSubtext>
  190. )}
  191. {!legacyRender && debugIdElement}
  192. {showCodeId && (
  193. <ImageSubtext>
  194. <ImageProp>{t('Code ID')}</ImageProp>:{' '}
  195. <Formatted>{image.code_id}</Formatted>
  196. </ImageSubtext>
  197. )}
  198. {!!image.arch && (
  199. <ImageSubtext>
  200. <ImageProp>{t('Architecture')}</ImageProp>: {image.arch}
  201. </ImageSubtext>
  202. )}
  203. </Fragment>
  204. )}
  205. </ImageInfoGroup>
  206. <Access access={['project:releases']}>
  207. {({hasAccess}) => {
  208. if (!hasAccess) {
  209. return null;
  210. }
  211. const settingsUrl = getSettingsLink();
  212. if (!settingsUrl) {
  213. return null;
  214. }
  215. return (
  216. <ImageActions>
  217. <Button
  218. size="xs"
  219. icon={<IconSearch size="xs" />}
  220. to={settingsUrl}
  221. title={t('Search for debug files in settings')}
  222. aria-label={t('Search for debug files in settings')}
  223. />
  224. </ImageActions>
  225. );
  226. }}
  227. </Access>
  228. </DebugImageItem>
  229. );
  230. });
  231. export default DebugImage;
  232. const DebugImageItem = styled(PanelItem)`
  233. font-size: ${p => p.theme.fontSizeSmall};
  234. @media (max-width: ${p => p.theme.breakpoints.small}) {
  235. display: grid;
  236. gap: ${space(1)};
  237. position: relative;
  238. }
  239. `;
  240. const Formatted = styled('span')`
  241. font-family: ${p => p.theme.text.familyMono};
  242. `;
  243. const ImageInfoGroup = styled('div')<{fullWidth?: boolean}>`
  244. margin-left: 1em;
  245. flex-grow: ${p => (p.fullWidth ? 1 : null)};
  246. &:first-child {
  247. @media (min-width: ${p => p.theme.breakpoints.small}) {
  248. margin-left: 0;
  249. }
  250. }
  251. `;
  252. const ImageActions = styled(ImageInfoGroup)`
  253. @media (max-width: ${p => p.theme.breakpoints.small}) {
  254. position: absolute;
  255. top: 15px;
  256. right: 20px;
  257. }
  258. display: flex;
  259. align-items: center;
  260. `;
  261. const ImageTitle = styled('div')`
  262. font-size: ${p => p.theme.fontSizeLarge};
  263. `;
  264. const CodeFile = styled('span')`
  265. font-weight: bold;
  266. `;
  267. const DebugFile = styled('span')`
  268. color: ${p => p.theme.gray300};
  269. `;
  270. const ImageSubtext = styled('div')`
  271. color: ${p => p.theme.gray300};
  272. `;
  273. const ImageProp = styled('span')`
  274. font-weight: bold;
  275. `;
  276. const StatusLine = styled(ImageSubtext)`
  277. display: flex;
  278. @media (max-width: ${p => p.theme.breakpoints.small}) {
  279. display: grid;
  280. }
  281. `;
  282. const AddressDivider = styled('br')`
  283. @media (max-width: ${p => p.theme.breakpoints.small}) {
  284. display: none;
  285. }
  286. `;
  287. const IconWrapper = styled('span')`
  288. display: inline-block;
  289. margin-top: ${space(0.5)};
  290. height: 16px;
  291. @media (max-width: ${p => p.theme.breakpoints.small}) {
  292. margin-top: ${space(0.25)};
  293. }
  294. `;
  295. const SymbolicationStatus = styled('span')`
  296. flex-grow: 1;
  297. flex-basis: 0;
  298. margin-right: 1em;
  299. svg {
  300. margin-left: 0.66ex;
  301. }
  302. `;