import {createRef, Fragment, PureComponent} from 'react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, } from 'react-virtualized'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; import isNil from 'lodash/isNil'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Button from 'sentry/components/button'; import Checkbox from 'sentry/components/checkbox'; import EventDataSection from 'sentry/components/events/eventDataSection'; import {getImageRange, parseAddress} from 'sentry/components/events/interfaces/utils'; import {Panel, PanelBody} from 'sentry/components/panels'; import SearchBar from 'sentry/components/searchBar'; import {IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import DebugMetaStore, {DebugMetaActions} from 'sentry/stores/debugMetaStore'; import space from 'sentry/styles/space'; import {Frame, Organization, Project} from 'sentry/types'; import {Event} from 'sentry/types/event'; import EmptyMessage from 'sentry/views/settings/components/emptyMessage'; import {shouldSkipSection} from '../debugMeta-v2/utils'; import DebugImage from './debugImage'; import ImageForBar from './imageForBar'; import {getFileName} from './utils'; const MIN_FILTER_LEN = 3; const PANEL_MAX_HEIGHT = 400; type Image = React.ComponentProps['image']; type DefaultProps = { data: { images: Array; }; }; type Props = DefaultProps & { event: Event; organization: Organization; projectId: Project['id']; }; type State = { debugImages: Array; filter: string; filteredImages: Array; showDetails: boolean; showUnused: boolean; foundFrame?: Frame; panelBodyHeight?: number; }; function normalizeId(id: string | undefined): string { return id ? id.trim().toLowerCase().replace(/[- ]/g, '') : ''; } const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 81, }); class DebugMeta extends PureComponent { static defaultProps: DefaultProps = { data: {images: []}, }; state: State = { filter: '', debugImages: [], filteredImages: [], showUnused: false, showDetails: false, }; componentDidMount() { this.unsubscribeFromStore = DebugMetaStore.listen(this.onStoreChange, undefined); cache.clearAll(); this.filterImages(); } componentDidUpdate(_prevProps: Props, prevState: State) { if ( prevState.showUnused !== this.state.showUnused || prevState.filter !== this.state.filter ) { this.filterImages(); } if ( !isEqual(prevState.foundFrame, this.state.foundFrame) || this.state.showDetails !== prevState.showDetails || prevState.showUnused !== this.state.showUnused || (prevState.filter && !this.state.filter) ) { this.updateGrid(); } if (prevState.filteredImages.length === 0 && this.state.filteredImages.length > 0) { this.getPanelBodyHeight(); } } componentWillUnmount() { if (this.unsubscribeFromStore) { this.unsubscribeFromStore(); } } unsubscribeFromStore: any; panelBodyRef = createRef(); listRef: List | null = null; updateGrid() { cache.clearAll(); this.listRef?.forceUpdateGrid(); } getPanelBodyHeight() { const panelBodyHeight = this.panelBodyRef?.current?.offsetHeight; if (!panelBodyHeight) { return; } this.setState({panelBodyHeight}); } onStoreChange = (store: {filter: string}) => { this.setState({ filter: store.filter, }); }; filterImage(image: Image) { const {showUnused, filter} = this.state; const searchTerm = filter.trim().toLowerCase(); if (searchTerm.length < MIN_FILTER_LEN) { if (showUnused) { return true; } // A debug status of `null` indicates that this information is not yet // available in an old event. Default to showing the image. if (image.debug_status !== 'unused') { return true; } // An unwind status of `null` indicates that symbolicator did not unwind. // Ignore the status in this case. if (!isNil(image.unwind_status) && image.unwind_status !== 'unused') { return true; } return false; } // 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); 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 ); } filterImages() { const foundFrame = this.getFrame(); // skip null values indicating invalid debug images const debugImages = this.getDebugImages(); if (!debugImages.length) { return; } const filteredImages = debugImages.filter(image => this.filterImage(image)); this.setState({debugImages, filteredImages, foundFrame}); } isValidImage(image: Image) { // 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; } getFrame(): Frame | undefined { const { event: {entries}, } = this.props; const frames: Array | undefined = entries.find( ({type}) => type === 'exception' )?.data?.values?.[0]?.stacktrace?.frames; if (!frames) { return undefined; } const searchTerm = normalizeId(this.state.filter); const relMatch = searchTerm.match(/^\s*(.*?)!(.*)$/); // debug_id!address if (relMatch) { const debugImages = this.getDebugImages().map( (image, idx) => [idx, image] as [number, Image] ); const filteredImages = debugImages.filter(([_, image]) => this.filterImage(image)); if (filteredImages.length === 1) { return frames.find( frame => frame.addrMode === `rel:${filteredImages[0][0]}` && frame.instructionAddr?.toLowerCase() === relMatch[2] ); } return undefined; } return frames.find(frame => frame.instructionAddr?.toLowerCase() === searchTerm); } getDebugImages() { const { data: {images}, } = this.props; // 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 filtered = images.filter(image => this.isValidImage(image)); // Sort images by their start address. We assume that images have // non-overlapping ranges. Each address is given as hex string (e.g. // "0xbeef"). filtered.sort((a, b) => parseAddress(a.image_addr) - parseAddress(b.image_addr)); return filtered; } getNoImagesMessage() { const {filter, showUnused, debugImages} = this.state; if (debugImages.length === 0) { return t('No loaded images available.'); } if (!showUnused && !filter) { return tct( 'No images are referenced in the stack trace. [toggle: Show Unreferenced]', { toggle: (