123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- import {createRef, Fragment, PureComponent} from 'react';
- // eslint-disable-next-line no-restricted-imports
- import {withRouter, WithRouterProps} from 'react-router';
- import {
- AutoSizer,
- CellMeasurer,
- CellMeasurerCache,
- List,
- ListRowProps,
- } from 'react-virtualized';
- import styled from '@emotion/styled';
- import {openModal, openReprocessEventModal} from 'sentry/actionCreators/modal';
- import GuideAnchor from 'sentry/components/assistant/guideAnchor';
- import Button from 'sentry/components/button';
- import EventDataSection from 'sentry/components/events/eventDataSection';
- import {getImageRange, parseAddress} from 'sentry/components/events/interfaces/utils';
- import {PanelTable} from 'sentry/components/panels';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {t} from 'sentry/locale';
- import DebugMetaStore, {DebugMetaActions} from 'sentry/stores/debugMetaStore';
- import space from 'sentry/styles/space';
- import {Group, Organization, Project} from 'sentry/types';
- import {Image, ImageStatus} from 'sentry/types/debugImage';
- import {Event} from 'sentry/types/event';
- import {defined} from 'sentry/utils';
- import SearchBarAction from '../searchBarAction';
- import Status from './debugImage/status';
- import DebugImage from './debugImage';
- import layout from './layout';
- import {
- combineStatus,
- getFileName,
- IMAGE_AND_CANDIDATE_LIST_MAX_HEIGHT,
- normalizeId,
- shouldSkipSection,
- } from './utils';
- const IMAGE_INFO_UNAVAILABLE = '-1';
- type DefaultProps = {
- data: {
- images: Array<Image | null>;
- };
- };
- type FilterOptions = NonNullable<
- React.ComponentProps<typeof SearchBarAction>['filterOptions']
- >;
- type Images = Array<React.ComponentProps<typeof DebugImage>['image']>;
- type Props = DefaultProps &
- WithRouterProps & {
- event: Event;
- organization: Organization;
- projectId: Project['id'];
- groupId?: Group['id'];
- };
- type State = {
- filterOptions: FilterOptions;
- filterSelections: FilterOptions;
- filteredImages: Images;
- filteredImagesByFilter: Images;
- filteredImagesBySearch: Images;
- isOpen: boolean;
- scrollbarWidth: number;
- searchTerm: string;
- panelTableHeight?: number;
- };
- const cache = new CellMeasurerCache({
- fixedWidth: true,
- defaultHeight: 81,
- });
- class DebugMeta extends PureComponent<Props, State> {
- static defaultProps: DefaultProps = {
- data: {images: []},
- };
- state: State = {
- searchTerm: '',
- scrollbarWidth: 0,
- isOpen: false,
- filterOptions: [],
- filterSelections: [],
- filteredImages: [],
- filteredImagesByFilter: [],
- filteredImagesBySearch: [],
- };
- componentDidMount() {
- this.unsubscribeFromDebugMetaStore = DebugMetaStore.listen(
- this.onDebugMetaStoreChange,
- undefined
- );
- cache.clearAll();
- this.getRelevantImages();
- this.openImageDetailsModal();
- }
- componentDidUpdate(_prevProps: Props, prevState: State) {
- if (
- this.state.isOpen ||
- (prevState.filteredImages.length === 0 && this.state.filteredImages.length > 0)
- ) {
- this.getPanelBodyHeight();
- }
- this.openImageDetailsModal();
- }
- componentWillUnmount() {
- if (this.unsubscribeFromDebugMetaStore) {
- this.unsubscribeFromDebugMetaStore();
- }
- }
- unsubscribeFromDebugMetaStore: any;
- panelTableRef = createRef<HTMLDivElement>();
- listRef: List | null = null;
- onDebugMetaStoreChange = (store: {filter: string}) => {
- const {searchTerm} = this.state;
- if (store.filter !== searchTerm) {
- this.setState({searchTerm: store.filter}, this.filterImagesBySearchTerm);
- }
- };
- getScrollbarWidth() {
- const panelTableWidth = this.panelTableRef?.current?.clientWidth ?? 0;
- const gridInnerWidth =
- this.panelTableRef?.current?.querySelector(
- '.ReactVirtualized__Grid__innerScrollContainer'
- )?.clientWidth ?? 0;
- const scrollbarWidth = panelTableWidth - gridInnerWidth;
- if (scrollbarWidth !== this.state.scrollbarWidth) {
- this.setState({scrollbarWidth});
- }
- }
- updateGrid = () => {
- if (this.listRef) {
- cache.clearAll();
- this.listRef.forceUpdateGrid();
- this.getScrollbarWidth();
- }
- };
- isValidImage(image: Image | null) {
- // in particular proguard images do not have a code file, skip them
- if (image === null || image.code_file === null || image.type === 'proguard') {
- return false;
- }
- if (getFileName(image.code_file) === 'dyld_sim') {
- // this is only for simulator builds
- return false;
- }
- return true;
- }
- filterImage(image: Image, searchTerm: string) {
- // When searching for an address, check for the address range of the image
- // instead of an exact match. Note that images cannot be found by index
- // if they are at 0x0. For those relative addressing has to be used.
- if (searchTerm.indexOf('0x') === 0) {
- const needle = parseAddress(searchTerm);
- if (needle > 0 && image.image_addr !== '0x0') {
- const [startAddress, endAddress] = getImageRange(image as any); // TODO(PRISCILA): remove any
- return needle >= startAddress && needle < endAddress;
- }
- }
- // the searchTerm ending at "!" is the end of the ID search.
- const relMatch = searchTerm.match(/^\s*(.*?)!/); // debug_id!address
- const idSearchTerm = normalizeId(relMatch?.[1] || searchTerm);
- return (
- // Prefix match for identifiers
- normalizeId(image.code_id).indexOf(idSearchTerm) === 0 ||
- normalizeId(image.debug_id).indexOf(idSearchTerm) === 0 ||
- // Any match for file paths
- (image.code_file?.toLowerCase() || '').indexOf(searchTerm) >= 0 ||
- (image.debug_file?.toLowerCase() || '').indexOf(searchTerm) >= 0
- );
- }
- filterImagesBySearchTerm() {
- const {filteredImages, filterSelections, searchTerm} = this.state;
- const filteredImagesBySearch = filteredImages.filter(image =>
- this.filterImage(image, searchTerm.toLowerCase())
- );
- const filteredImagesByFilter = this.getFilteredImagesByFilter(
- filteredImagesBySearch,
- filterSelections
- );
- this.setState(
- {
- filteredImagesBySearch,
- filteredImagesByFilter,
- },
- this.updateGrid
- );
- }
- openImageDetailsModal = async () => {
- const {filteredImages} = this.state;
- if (!filteredImages.length) {
- return;
- }
- const {location, organization, projectId: projSlug, groupId, event} = this.props;
- const {query} = location;
- const {imageCodeId, imageDebugId} = query;
- if (!imageCodeId && !imageDebugId) {
- return;
- }
- const image =
- imageCodeId !== IMAGE_INFO_UNAVAILABLE || imageDebugId !== IMAGE_INFO_UNAVAILABLE
- ? filteredImages.find(
- ({code_id, debug_id}) => code_id === imageCodeId || debug_id === imageDebugId
- )
- : undefined;
- const mod = await import(
- 'sentry/components/events/interfaces/debugMeta-v2/debugImageDetails'
- );
- const {default: Modal, modalCss} = mod;
- openModal(
- deps => (
- <Modal
- {...deps}
- image={image}
- organization={organization}
- projSlug={projSlug}
- event={event}
- onReprocessEvent={
- defined(groupId) ? this.handleReprocessEvent(groupId) : undefined
- }
- />
- ),
- {
- modalCss,
- onClose: this.handleCloseImageDetailsModal,
- }
- );
- };
- toggleImagesLoaded = () => {
- this.setState(state => ({
- isOpen: !state.isOpen,
- }));
- };
- getPanelBodyHeight() {
- const panelTableHeight = this.panelTableRef?.current?.offsetHeight;
- if (!panelTableHeight) {
- return;
- }
- this.setState({panelTableHeight});
- }
- getRelevantImages() {
- const {data} = this.props;
- const {images} = data;
- // There are a bunch of images in debug_meta that are not relevant to this
- // component. Filter those out to reduce the noise. Most importantly, this
- // includes proguard images, which are rendered separately.
- const relevantImages = images.filter(this.isValidImage);
- if (!relevantImages.length) {
- return;
- }
- const formattedRelevantImages = relevantImages.map(releventImage => {
- const {debug_status, unwind_status} = releventImage as Image;
- return {
- ...releventImage,
- status: combineStatus(debug_status, unwind_status),
- };
- }) as Images;
- // Sort images by their start address. We assume that images have
- // non-overlapping ranges. Each address is given as hex string (e.g.
- // "0xbeef").
- formattedRelevantImages.sort(
- (a, b) => parseAddress(a.image_addr) - parseAddress(b.image_addr)
- );
- const unusedImages: Images = [];
- const usedImages = formattedRelevantImages.filter(image => {
- if (image.debug_status === ImageStatus.UNUSED) {
- unusedImages.push(image as Images[0]);
- return false;
- }
- return true;
- }) as Images;
- const filteredImages = [...usedImages, ...unusedImages];
- const filterOptions = this.getFilterOptions(filteredImages);
- const defaultFilterSelections = (filterOptions[0].options ?? []).filter(
- opt => opt.value !== ImageStatus.UNUSED
- );
- this.setState({
- filteredImages,
- filterOptions,
- filterSelections: defaultFilterSelections,
- filteredImagesByFilter: this.getFilteredImagesByFilter(
- filteredImages,
- defaultFilterSelections
- ),
- filteredImagesBySearch: filteredImages,
- });
- }
- getFilterOptions(images: Images): FilterOptions {
- return [
- {
- value: 'status',
- label: t('Status'),
- options: [...new Set(images.map(image => image.status))].map(status => ({
- value: status,
- label: <Status status={status} />,
- })),
- },
- ];
- }
- getFilteredImagesByFilter(filteredImages: Images, filterOptions: FilterOptions) {
- const checkedOptions = new Set(filterOptions.map(option => option.value));
- if (![...checkedOptions].length) {
- return filteredImages;
- }
- return filteredImages.filter(image => checkedOptions.has(image.status));
- }
- handleChangeFilter = (filterSelections: FilterOptions) => {
- const {filteredImagesBySearch} = this.state;
- const filteredImagesByFilter = this.getFilteredImagesByFilter(
- filteredImagesBySearch,
- filterSelections
- );
- this.setState({filterSelections, filteredImagesByFilter}, this.updateGrid);
- };
- handleChangeSearchTerm = (searchTerm = '') => {
- DebugMetaActions.updateFilter(searchTerm);
- };
- handleResetFilter = () => {
- this.setState({filterSelections: []}, this.filterImagesBySearchTerm);
- };
- handleResetSearchBar = () => {
- this.setState(prevState => ({
- searchTerm: '',
- filteredImagesByFilter: prevState.filteredImages,
- filteredImagesBySearch: prevState.filteredImages,
- }));
- };
- handleOpenImageDetailsModal = (
- code_id: Image['code_id'],
- debug_id: Image['debug_id']
- ) => {
- const {location, router} = this.props;
- router.push({
- ...location,
- query: {
- ...location.query,
- imageCodeId: code_id ?? IMAGE_INFO_UNAVAILABLE,
- imageDebugId: debug_id ?? IMAGE_INFO_UNAVAILABLE,
- },
- });
- };
- handleCloseImageDetailsModal = () => {
- const {location, router} = this.props;
- router.push({
- ...location,
- query: {...location.query, imageCodeId: undefined, imageDebugId: undefined},
- });
- };
- handleReprocessEvent = (groupId: Group['id']) => () => {
- const {organization} = this.props;
- openReprocessEventModal({
- organization,
- groupId,
- onClose: this.openImageDetailsModal,
- });
- };
- renderRow = ({index, key, parent, style}: ListRowProps) => {
- const {filteredImagesByFilter: images} = this.state;
- return (
- <CellMeasurer
- cache={cache}
- columnIndex={0}
- key={key}
- parent={parent}
- rowIndex={index}
- >
- <DebugImage
- style={style}
- image={images[index]}
- onOpenImageDetailsModal={this.handleOpenImageDetailsModal}
- />
- </CellMeasurer>
- );
- };
- renderList() {
- const {filteredImagesByFilter: images, panelTableHeight} = this.state;
- if (!panelTableHeight) {
- return images.map((image, index) => (
- <DebugImage
- key={index}
- image={image}
- onOpenImageDetailsModal={this.handleOpenImageDetailsModal}
- />
- ));
- }
- return (
- <AutoSizer disableHeight onResize={this.updateGrid}>
- {({width}) => (
- <StyledList
- ref={(el: List | null) => {
- this.listRef = el;
- }}
- deferredMeasurementCache={cache}
- height={IMAGE_AND_CANDIDATE_LIST_MAX_HEIGHT}
- overscanRowCount={5}
- rowCount={images.length}
- rowHeight={cache.rowHeight}
- rowRenderer={this.renderRow}
- width={width}
- isScrolling={false}
- />
- )}
- </AutoSizer>
- );
- }
- getEmptyMessage() {
- const {searchTerm, filteredImagesByFilter: images, filterSelections} = this.state;
- if (!!images.length) {
- return {};
- }
- if (searchTerm && !images.length) {
- const hasActiveFilter = filterSelections.length > 0;
- return {
- emptyMessage: t('Sorry, no images match your search query'),
- emptyAction: hasActiveFilter ? (
- <Button onClick={this.handleResetFilter} priority="primary">
- {t('Reset filter')}
- </Button>
- ) : (
- <Button onClick={this.handleResetSearchBar} priority="primary">
- {t('Clear search bar')}
- </Button>
- ),
- };
- }
- return {
- emptyMessage: t('There are no images to be displayed'),
- };
- }
- render() {
- const {
- searchTerm,
- filterOptions,
- scrollbarWidth,
- isOpen,
- filterSelections,
- filteredImagesByFilter: filteredImages,
- } = this.state;
- const {data} = this.props;
- const {images} = data;
- if (shouldSkipSection(filteredImages, images)) {
- return null;
- }
- const showFilters = filterOptions.some(section => (section.options ?? []).length > 1);
- const actions = (
- <ToggleButton onClick={this.toggleImagesLoaded} priority="link">
- {isOpen ? t('Hide Details') : t('Show Details')}
- </ToggleButton>
- );
- return (
- <EventDataSection
- type="images-loaded"
- title={
- <TitleWrapper>
- <GuideAnchor target="images-loaded" position="bottom">
- <Title>{t('Images Loaded')}</Title>
- </GuideAnchor>
- <QuestionTooltip
- size="xs"
- position="top"
- title={t(
- 'A list of dynamic libraries or shared objects loaded into process memory at the time of the crash. Images contribute application code that is referenced in stack traces.'
- )}
- />
- </TitleWrapper>
- }
- actions={actions}
- wrapTitle={false}
- isCentered
- >
- {isOpen && (
- <Fragment>
- <StyledSearchBarAction
- placeholder={t('Search images loaded')}
- onChange={value => this.handleChangeSearchTerm(value)}
- query={searchTerm}
- filterOptions={showFilters ? filterOptions : undefined}
- onFilterChange={this.handleChangeFilter}
- filterSelections={filterSelections}
- />
- <StyledPanelTable
- isEmpty={!filteredImages.length}
- scrollbarWidth={scrollbarWidth}
- headers={[t('Status'), t('Image'), t('Processing'), t('Details'), '']}
- {...this.getEmptyMessage()}
- >
- <div ref={this.panelTableRef}>{this.renderList()}</div>
- </StyledPanelTable>
- </Fragment>
- )}
- </EventDataSection>
- );
- }
- }
- export default withRouter(DebugMeta);
- const StyledPanelTable = styled(PanelTable)<{scrollbarWidth?: number}>`
- overflow: hidden;
- > * {
- :nth-child(-n + 5) {
- ${p => p.theme.overflowEllipsis};
- border-bottom: 1px solid ${p => p.theme.border};
- :nth-child(5n) {
- height: 100%;
- ${p => !p.scrollbarWidth && `display: none`}
- }
- }
- :nth-child(n + 6) {
- grid-column: 1/-1;
- ${p =>
- !p.isEmpty &&
- `
- display: grid;
- padding: 0;
- `}
- }
- }
- ${p => layout(p.theme, p.scrollbarWidth)}
- `;
- const TitleWrapper = styled('div')`
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: ${space(0.5)};
- align-items: center;
- padding: ${space(0.75)} 0;
- `;
- const Title = styled('h3')`
- margin-bottom: 0;
- padding: 0 !important;
- height: 14px;
- `;
- // XXX(ts): Emotion11 has some trouble with List's defaultProps
- const StyledList = styled(List as any)<React.ComponentProps<typeof List>>`
- height: auto !important;
- max-height: ${p => p.height}px;
- overflow-y: auto !important;
- outline: none;
- `;
- const StyledSearchBarAction = styled(SearchBarAction)`
- z-index: 1;
- margin-bottom: ${space(1)};
- `;
- const ToggleButton = styled(Button)`
- font-weight: 700;
- color: ${p => p.theme.subText};
- &:hover,
- &:focus {
- color: ${p => p.theme.textColor};
- }
- `;
|