Browse Source

feat(issue_details): Implement new UI for loaded images (#15667)

Implements a new UI for "Loaded Images" in issue details. It is a visual combination of our Breadcrumbs section and the settings UI for uploaded debug files.
Jan Michael Auer 5 years ago
parent
commit
50ffca475f

+ 1 - 1
src/sentry/lang/native/processing.py

@@ -114,7 +114,7 @@ def _merge_image(raw_image, complete_image, sdk_info, handle_symbolication_faile
     for k, v in six.iteritems(complete_image):
         if k in IMAGE_STATUS_FIELDS:
             statuses.add(v)
-        elif not (v is None or (k, v) == ("arch", "unknown")):
+        if not (v is None or (k, v) == ("arch", "unknown")):
             raw_image[k] = v
 
     for status in set(statuses):

+ 54 - 0
src/sentry/static/sentry/app/components/debugFileFeature.jsx

@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import styled from 'react-emotion';
+
+import Tooltip from 'app/components/tooltip';
+import InlineSvg from 'app/components/inlineSvg';
+import {t} from 'app/locale';
+import Tag from 'app/views/settings/components/tag';
+
+const FEATURE_TOOLTIPS = {
+  symtab: t(
+    'Symbol tables are used as a fallback when full debug information is not available'
+  ),
+  debug: t(
+    'Debug information provides function names and resolves inlined frames during symbolication'
+  ),
+  unwind: t(
+    'Stack unwinding information improves the quality of stack traces extracted from minidumps'
+  ),
+  sources: t(
+    'Source code information allows Sentry to display source code context for stack frames'
+  ),
+};
+
+function DebugFileFeature({available, feature}) {
+  let icon = null;
+
+  if (available === true) {
+    icon = <Icon type="success" src="icon-checkmark-sm" />;
+  } else if (available === false) {
+    icon = <Icon type="error" src="icon-close" />;
+  }
+
+  return (
+    <Tooltip title={FEATURE_TOOLTIPS[feature]}>
+      <Tag inline>
+        {icon}
+        {feature}
+      </Tag>
+    </Tooltip>
+  );
+}
+
+DebugFileFeature.propTypes = {
+  available: PropTypes.bool,
+  feature: PropTypes.oneOf(Object.keys(FEATURE_TOOLTIPS)).isRequired,
+};
+
+const Icon = styled(InlineSvg)`
+  color: ${p => p.theme.alert[p.type].iconColor};
+  margin-right: 1ex;
+`;
+
+export default DebugFileFeature;

+ 3 - 2
src/sentry/static/sentry/app/components/events/eventEntries.jsx

@@ -109,7 +109,7 @@ class EventEntries extends React.Component {
   }
 
   renderEntries() {
-    const {event, project, isShare} = this.props;
+    const {event, project, organization, isShare} = this.props;
 
     const entries = event && event.entries;
 
@@ -130,7 +130,8 @@ class EventEntries extends React.Component {
         return (
           <Component
             key={'entry-' + entryIdx}
-            projectId={project.slug}
+            projectId={project ? project.slug : null}
+            orgId={organization ? organization.slug : null}
             event={event}
             type={entry.type}
             data={entry.data}

+ 521 - 22
src/sentry/static/sentry/app/components/events/interfaces/debugmeta.jsx

@@ -1,57 +1,556 @@
+import isNil from 'lodash/isNil';
 import PropTypes from 'prop-types';
 import React from 'react';
-import SentryTypes from 'app/sentryTypes';
+import styled from 'react-emotion';
+
+import Access from 'app/components/acl/access';
+import GuideAnchor from 'app/components/assistant/guideAnchor';
+import Button from 'app/components/button';
+import Checkbox from 'app/components/checkbox';
+import DebugFileFeature from 'app/components/debugFileFeature';
 import EventDataSection from 'app/components/events/eventDataSection';
-import ClippedBox from 'app/components/clippedBox';
-import KeyValueList from 'app/components/events/interfaces/keyValueList';
+import InlineSvg from 'app/components/inlineSvg';
+import Input from 'app/components/forms/input';
+import {Panel, PanelBody, PanelItem} from 'app/components/panels';
+import Tooltip from 'app/components/tooltip';
+
 import {t} from 'app/locale';
+import SentryTypes from 'app/sentryTypes';
+
+const IMAGE_ADDR_LEN = 12;
+const MIN_FILTER_LEN = 3;
+
+function formatAddr(addr) {
+  return `0x${addr.toString(16).padStart(IMAGE_ADDR_LEN, '0')}`;
+}
+
+function parseAddr(addr) {
+  try {
+    return parseInt(addr, 16) || 0;
+  } catch (_e) {
+    return 0;
+  }
+}
+
+function getImageRange(image) {
+  // The start address is normalized to a `0x` prefixed hex string. The event
+  // schema also allows ingesting plain numbers, but this is converted during
+  // ingestion.
+  const startAddress = parseAddr(image.image_addr);
+
+  // The image size is normalized to a regular number. However, it can also be
+  // `null`, in which case we assume that it counts up to the next image.
+  const endAddress = startAddress + (image.image_size || 0);
+
+  return [startAddress, endAddress];
+}
 
-class DebugMetaInterface extends React.Component {
+function getFileName(path) {
+  const directorySeparator = /^([a-z]:\\|\\\\)/i.test(path) ? '\\' : '/';
+  return path.split(directorySeparator).pop();
+}
+
+function getStatusWeight(status) {
+  switch (status) {
+    case null:
+    case undefined:
+    case 'unused':
+      return 0;
+    case 'found':
+      return 1;
+    default:
+      return 2;
+  }
+}
+
+function getImageStatusText(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) {
+  switch (status) {
+    case 'found':
+      return t('The file was found and successfully processed.');
+    case 'unused':
+      return t('The file was not required for processing the stack trace.');
+    case 'missing':
+      return t('The file could not be found in any of the specified sources.');
+    case 'malformed':
+      return t('The file failed to process.');
+    case 'fetching_failed':
+      return t('The file could not be downloaded.');
+    case 'timeout':
+      return t('Downloading or processing the file took too long.');
+    case 'other':
+      return t('An internal error occurred while handling this image.');
+    default:
+      return null;
+  }
+}
+
+function combineStatus(debugStatus, unwindStatus) {
+  const debugWeight = getStatusWeight(debugStatus);
+  const unwindWeight = getStatusWeight(unwindStatus);
+
+  const combined = debugWeight >= unwindWeight ? debugStatus : unwindStatus;
+  return combined || 'unused';
+}
+
+class DebugImage extends React.PureComponent {
+  static propTypes = {
+    image: PropTypes.object.isRequired,
+    orgId: PropTypes.string,
+    projectId: PropTypes.string,
+    showDetails: PropTypes.bool.isRequired,
+  };
+
+  getSettingsLink(image) {
+    const {orgId, projectId} = this.props;
+    if (!orgId || !projectId || !image.debug_id) {
+      return null;
+    }
+
+    return `/settings/${orgId}/projects/${projectId}/debug-symbols/?query=${
+      image.debug_id
+    }`;
+  }
+
+  renderStatus(title, status) {
+    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>
+    );
+  }
+
+  render() {
+    const {image, showDetails} = this.props;
+
+    const combinedStatus = combineStatus(image.debug_status, image.unwind_status);
+    const [startAddress, endAddress] = getImageRange(image);
+
+    let iconElement = null;
+    switch (combinedStatus) {
+      case 'unused':
+        iconElement = <ImageIcon type="muted" src="icon-circle-empty" />;
+        break;
+      case 'found':
+        iconElement = <ImageIcon type="success" src="icon-circle-check" />;
+        break;
+      default:
+        iconElement = <ImageIcon type="error" src="icon-circle-exclamation" />;
+        break;
+    }
+
+    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 thats 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>
+    );
+
+    return (
+      <DebugImageItem>
+        <ImageInfoGroup>{iconElement}</ImageInfoGroup>
+
+        <ImageInfoGroup>
+          <Formatted>{formatAddr(startAddress)}</Formatted> &ndash; <br />
+          <Formatted>{formatAddr(endAddress)}</Formatted>
+        </ImageInfoGroup>
+
+        <ImageInfoGroup fullWidth>
+          <ImageTitle>
+            <Tooltip title={image.code_file}>
+              <CodeFile>{codeFile}</CodeFile>
+            </Tooltip>
+            {showDebugFile && <DebugFile> ({debugFile})</DebugFile>}
+          </ImageTitle>
+
+          {legacyRender ? (
+            debugIdElement
+          ) : (
+            <StatusLine>
+              {this.renderStatus(t('Stack Unwinding'), image.unwind_status)}
+              {this.renderStatus(t('Symbolication'), image.debug_status)}
+            </StatusLine>
+          )}
+
+          {showDetails && (
+            <React.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>
+              )}
+            </React.Fragment>
+          )}
+        </ImageInfoGroup>
+
+        <Access access={['project:releases']}>
+          {({hasAccess}) => {
+            if (!hasAccess) {
+              return null;
+            }
+
+            const settingsUrl = this.getSettingsLink(image);
+            if (!settingsUrl) {
+              return null;
+            }
+
+            return (
+              <ImageActions>
+                <Tooltip title={t('Search for debug files in settings')}>
+                  <Button size="xsmall" icon="icon-settings" href={settingsUrl} />
+                </Tooltip>
+              </ImageActions>
+            );
+          }}
+        </Access>
+      </DebugImageItem>
+    );
+  }
+}
+
+class DebugMetaInterface extends React.PureComponent {
   static propTypes = {
     event: SentryTypes.Event.isRequired,
     data: PropTypes.object.isRequired,
+    orgId: PropTypes.string,
+    projectId: PropTypes.string,
   };
 
-  getImageDetail(img) {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      filter: null,
+      showUnused: false,
+      showDetails: false,
+    };
+  }
+
+  filterImage(image) {
+    const {showUnused, filter} = this.state;
+    if (!filter || filter.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.
+    if (filter.indexOf('0x') === 0) {
+      const needle = parseAddr(filter);
+      if (needle > 0) {
+        const [startAddress, endAddress] = getImageRange(image);
+        return needle >= startAddress && needle < endAddress;
+      }
+    }
+
+    return (
+      // Prefix match for identifiers
+      (image.code_id || '').indexOf(filter) === 0 ||
+      (image.debug_id || '').indexOf(filter) === 0 ||
+      // Any match for file paths
+      (image.code_file || '').indexOf(filter) >= 0 ||
+      (image.debug_file || '').indexOf(filter) >= 0
+    );
+  }
+
+  handleChangeShowUnused = e => {
+    const showUnused = e.target.checked;
+    this.setState({showUnused});
+  };
+
+  handleChangeShowDetails = e => {
+    const showDetails = e.target.checked;
+    this.setState({showDetails});
+  };
+
+  handleChangeFilter = e => {
+    this.setState({filter: e.target.value});
+  };
+
+  isValidImage(image) {
     // in particular proguard images do not have a code file, skip them
-    if (img === null || img.code_file === null || img.type === 'proguard') {
-      return null;
+    if (image === null || image.code_file === null || image.type === 'proguard') {
+      return false;
     }
 
-    const directorySeparator = /^([a-z]:\\|\\\\)/i.test(img.code_file) ? '\\' : '/';
-    const code_file = img.code_file.split(directorySeparator).pop();
-    if (code_file === 'dyld_sim') {
+    if (getFileName(image.code_file) === 'dyld_sim') {
       // this is only for simulator builds
-      return null;
+      return false;
     }
 
-    const version = img.debug_id || '<none>';
-    return [code_file, version];
+    return true;
   }
 
-  render() {
-    const data = this.props.data;
+  getDebugImages() {
+    const images = this.props.data.images || [];
+
+    // 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) => parseAddr(a.image_addr) - parseAddr(b.image_addr));
+
+    return filtered;
+  }
+
+  renderToolbar() {
+    const {filter, showDetails, showUnused} = this.state;
+    return (
+      <Toolbar>
+        <Label>
+          <Checkbox checked={showDetails} onChange={this.handleChangeShowDetails} />
+          {t('details')}
+        </Label>
+
+        <Label>
+          <Checkbox
+            checked={showUnused || !!filter}
+            disabled={!!filter}
+            onChange={this.handleChangeShowUnused}
+          />
+          {t('show unreferenced')}
+        </Label>
 
+        <SearchBox
+          onChange={this.handleChangeFilter}
+          placeholder={t('Search loaded images\u2026')}
+        />
+      </Toolbar>
+    );
+  }
+
+  render() {
     // skip null values indicating invalid debug images
-    const images = data.images.map(img => this.getImageDetail(img)).filter(img => img);
+    const images = this.getDebugImages();
     if (images.length === 0) {
       return null;
     }
 
-    return (
+    const filteredImages = images.filter(image => this.filterImage(image));
+
+    const titleElement = (
       <div>
+        <GuideAnchor target="packages" position="top">
+          <Title>
+            <strong>{t('Images Loaded')}</strong>
+          </Title>
+          {this.renderToolbar()}
+        </GuideAnchor>
+      </div>
+    );
+
+    return (
+      <React.Fragment>
         <EventDataSection
           event={this.props.event}
           type="packages"
-          title={t('Images Loaded')}
+          title={titleElement}
+          wrapTitle={false}
         >
-          <ClippedBox>
-            <KeyValueList data={images} isSorted={false} />
-          </ClippedBox>
+          <DebugImagesPanel>
+            <PanelBody>
+              {filteredImages.map(image => (
+                <DebugImage
+                  key={image.debug_id}
+                  image={image}
+                  orgId={this.props.orgId}
+                  projectId={this.props.projectId}
+                  showDetails={this.state.showDetails}
+                />
+              ))}
+            </PanelBody>
+          </DebugImagesPanel>
         </EventDataSection>
-      </div>
+      </React.Fragment>
     );
   }
 }
 
+const Title = styled('h3')`
+  float: left;
+`;
+
+const Toolbar = styled('div')`
+  float: right;
+`;
+
+const Label = styled('label')`
+  font-weight: normal;
+  margin-right: 1em;
+
+  > input {
+    margin-right: 1ex;
+  }
+`;
+
+const SearchBox = styled(Input)`
+  width: auto;
+  display: inline;
+`;
+
+const DebugImagesPanel = styled(Panel)`
+  max-height: 600px;
+  overflow-y: auto;
+`;
+
+const DebugImageItem = styled(PanelItem)`
+  font-size: ${p => p.theme.fontSizeSmall};
+`;
+
+const ImageIcon = styled(InlineSvg)`
+  font-size: ${p => p.theme.fontSizeLarge};
+  color: ${p => p.theme.alert[p.type].iconColor};
+`;
+
+const Formatted = styled('span')`
+  font-family: ${p => p.theme.text.familyMono};
+`;
+
+const ImageInfoGroup = styled('div')`
+  margin-left: 1em;
+  flex-grow: ${p => (p.fullWidth ? 1 : null)};
+
+  &:first-child {
+    margin-left: 0;
+  }
+`;
+
+const ImageActions = styled(ImageInfoGroup)``;
+
+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.gray2};
+`;
+
+const ImageSubtext = styled('div')`
+  color: ${p => p.theme.gray2};
+`;
+
+const ImageProp = styled('span')`
+  font-weight: bold;
+`;
+
+const StatusLine = styled(ImageSubtext)`
+  display: flex;
+`;
+
+const SymbolicationStatus = styled('span')`
+  flex-grow: 1;
+  flex-basis: 0;
+  margin-right: 1em;
+
+  ${ImageIcon} {
+    margin-left: 0.66ex;
+  }
+`;
+
 export default DebugMetaInterface;

+ 5 - 1
src/sentry/static/sentry/app/views/eventsV2/eventDetails/content.tsx

@@ -127,7 +127,11 @@ class EventDetailsContent extends AsyncComponent<Props, State & AsyncComponent['
             })}
         </HeaderBox>
         <ContentColumn>
-          <EventInterfaces event={event} projectId={this.projectId} />
+          <EventInterfaces
+            event={event}
+            projectId={this.projectId}
+            orgId={organization.slug}
+          />
         </ContentColumn>
         <SidebarColumn>
           {event.groupID && (

+ 11 - 3
src/sentry/static/sentry/app/views/eventsV2/eventInterfaces.tsx

@@ -24,6 +24,7 @@ const OTHER_SECTIONS = {
 
 type ActiveTabProps = {
   projectId: string;
+  orgId: string;
   event: Event;
   activeTab: string;
 };
@@ -33,7 +34,7 @@ type ActiveTabProps = {
  * Some but not all interface elements require a projectId.
  */
 const ActiveTab = (props: ActiveTabProps) => {
-  const {projectId, event, activeTab} = props;
+  const {projectId, orgId, event, activeTab} = props;
   if (!activeTab) {
     return null;
   }
@@ -43,6 +44,7 @@ const ActiveTab = (props: ActiveTabProps) => {
     return (
       <Component
         projectId={projectId}
+        orgId={orgId}
         event={event}
         type={entry.type}
         data={entry.data}
@@ -78,6 +80,7 @@ ActiveTab.propTypes = {
 type EventInterfacesProps = {
   event: Event;
   projectId: string;
+  orgId: string;
 };
 type EventInterfacesState = {
   activeTab: string;
@@ -102,7 +105,7 @@ class EventInterfaces extends React.Component<
   handleTabChange = tab => this.setState({activeTab: tab});
 
   render() {
-    const {event, projectId} = this.props;
+    const {event, projectId, orgId} = this.props;
     const {activeTab} = this.state;
 
     return (
@@ -153,7 +156,12 @@ class EventInterfaces extends React.Component<
           })}
         </NavTabs>
         <ErrorBoundary message={t('Could not render event details')}>
-          <ActiveTab event={event} activeTab={activeTab} projectId={projectId} />
+          <ActiveTab
+            event={event}
+            activeTab={activeTab}
+            projectId={projectId}
+            orgId={orgId}
+          />
         </ErrorBoundary>
       </React.Fragment>
     );

+ 1 - 0
tests/js/spec/views/sharedGroupDetails/__snapshots__/index.spec.jsx.snap

@@ -853,6 +853,7 @@ exports[`SharedGroupDetails renders 1`] = `
                                 }
                                 isShare={true}
                                 key="entry-0"
+                                orgId="org-slug"
                                 projectId="project-slug"
                                 type="message"
                               >

+ 16 - 2
tests/sentry/lang/native/test_processing.py

@@ -32,7 +32,13 @@ def test_merge_symbolicator_image_basic():
     _merge_image(raw_image, complete_image, sdk_info, errors.append)
 
     assert not errors
-    assert raw_image == {"instruction_addr": 0xFEEBEE, "other": "foo", "other2": "bar"}
+    assert raw_image == {
+        "debug_status": "found",
+        "unwind_status": "found",
+        "instruction_addr": 0xFEEBEE,
+        "other": "foo",
+        "other2": "bar",
+    }
 
 
 def test_merge_symbolicator_image_basic_success():
@@ -50,6 +56,8 @@ def test_merge_symbolicator_image_basic_success():
 
     assert not errors
     assert raw_image == {
+        "debug_status": "found",
+        "unwind_status": "found",
         "instruction_addr": 0xFEEBEE,
         "other": "foo",
         "other2": "bar",
@@ -66,7 +74,11 @@ def test_merge_symbolicator_image_remove_unknown_arch():
     _merge_image(raw_image, complete_image, sdk_info, errors.append)
 
     assert not errors
-    assert raw_image == {"instruction_addr": 0xFEEBEE}
+    assert raw_image == {
+        "debug_status": "found",
+        "unwind_status": "found",
+        "instruction_addr": 0xFEEBEE,
+    }
 
 
 @pytest.mark.parametrize(
@@ -98,6 +110,8 @@ def test_merge_symbolicator_image_errors(code_file, error):
     assert e.type == error
 
     assert raw_image == {
+        "debug_status": "found",
+        "unwind_status": "missing",
         "instruction_addr": 0xFEEBEE,
         "other": "foo",
         "other2": "bar",

+ 120 - 1
tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_full_minidump.pysnap

@@ -1,5 +1,5 @@
 ---
-created: '2019-06-12T09:33:12.372012Z'
+created: '2019-11-19T11:19:42.001799Z'
 creator: sentry
 source: tests/symbolicator/test_minidump_full.py
 ---
@@ -18,121 +18,240 @@ debug_meta:
     code_id: 5ab380779000
     debug_file: C:\projects\breakpad-tools\windows\Release\crash.pdb
     debug_id: 3249d99d-0c40-4931-8610-f4e4fb0b6936-1
+    debug_status: found
+    features:
+      has_debug_info: true
+      has_sources: false
+      has_symbols: true
+      has_unwind_info: true
     image_addr: '0x2a0000'
     image_size: 36864
     type: pe
+    unwind_status: found
   - code_file: C:\Windows\System32\dbghelp.dll
     code_id: 57898e12145000
     debug_file: dbghelp.pdb
     debug_id: 9c2a902b-6fdf-40ad-8308-588a41d572a0-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70850000'
     image_size: 1331200
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\msvcp140.dll
     code_id: 589abc846c000
     debug_file: msvcp140.i386.pdb
     debug_id: bf5257f7-8c26-43dd-9bb7-901625e1136a-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x709a0000'
     image_size: 442368
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\apphelp.dll
     code_id: 57898eeb92000
     debug_file: apphelp.pdb
     debug_id: 8daf7773-372f-460a-af38-944e193f7e33-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70a10000'
     image_size: 598016
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\dbgcore.dll
     code_id: 57898dab25000
     debug_file: dbgcore.pdb
     debug_id: aec7ef2f-df4b-4642-a471-4c3e5fe8760a-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70b70000'
     image_size: 151552
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\VCRUNTIME140.dll
     code_id: 589abc7714000
     debug_file: vcruntime140.i386.pdb
     debug_id: 0ed80a50-ecda-472b-86a4-eb6c833f8e1b-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70c60000'
     image_size: 81920
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\CRYPTBASE.dll
     code_id: 57899141a000
     debug_file: cryptbase.pdb
     debug_id: 147c51fb-7ca1-408f-85b5-285f2ad6f9c5-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73ba0000'
     image_size: 40960
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\sspicli.dll
     code_id: 59bf30e31f000
     debug_file: wsspicli.pdb
     debug_id: 51e432b1-0450-4b19-8ed1-6d4335f9f543-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73bb0000'
     image_size: 126976
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\advapi32.dll
     code_id: 5a49bb7677000
     debug_file: advapi32.pdb
     debug_id: 0c799483-b549-417d-8433-4331852031fe-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73c70000'
     image_size: 487424
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\msvcrt.dll
     code_id: 57899155be000
     debug_file: msvcrt.pdb
     debug_id: 6f6409b3-d520-43c7-9b2f-62e00bfe761c-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73cf0000'
     image_size: 778240
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\sechost.dll
     code_id: 598942c741000
     debug_file: sechost.pdb
     debug_id: 6f6a05dd-0a80-478b-a419-9b88703bf75b-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x74450000'
     image_size: 266240
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\kernel32.dll
     code_id: 590285e9e0000
     debug_file: wkernel32.pdb
     debug_id: d3474559-96f7-47d6-bf43-c176b2171e68-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75050000'
     image_size: 917504
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\bcryptPrimitives.dll
     code_id: 59b0df8f5a000
     debug_file: bcryptprimitives.pdb
     debug_id: 287b19c3-9209-4a2b-bb8f-bcc37f411b11-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75130000'
     image_size: 368640
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\rpcrt4.dll
     code_id: 5a49bb75c1000
     debug_file: wrpcrt4.pdb
     debug_id: ae131c67-27a7-4fa1-9916-b5a4aef41190-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75810000'
     image_size: 790528
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\ucrtbase.dll
     code_id: 59bf2b5ae0000
     debug_file: ucrtbase.pdb
     debug_id: 6bedcbce-0a3a-40e9-8040-81c2c8c6cc2f-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x758f0000'
     image_size: 917504
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\KERNELBASE.dll
     code_id: 59bf2bcf1a1000
     debug_file: wkernelbase.pdb
     debug_id: 8462294a-c645-402d-ac82-a4e95f61ddf9-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x76db0000'
     image_size: 1708032
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\ntdll.dll
     code_id: 59b0d8f3183000
     debug_file: wntdll.pdb
     debug_id: 971f98e5-ce60-41ff-b2d7-235bbeb34578-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x77170000'
     image_size: 1585152
     type: pe
+    unwind_status: missing
 errors:
 - name: timestamp
   type: past_timestamp

+ 120 - 1
tests/symbolicator/snapshots/SymbolicatorMinidumpIntegrationTest/test_missing_dsym.pysnap

@@ -1,5 +1,5 @@
 ---
-created: '2019-06-12T09:33:15.441451Z'
+created: '2019-11-19T11:22:56.297739Z'
 creator: sentry
 source: tests/symbolicator/test_minidump_full.py
 ---
@@ -17,121 +17,240 @@ debug_meta:
     code_id: 5ab380779000
     debug_file: C:\projects\breakpad-tools\windows\Release\crash.pdb
     debug_id: 3249d99d-0c40-4931-8610-f4e4fb0b6936-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x2a0000'
     image_size: 36864
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\dbghelp.dll
     code_id: 57898e12145000
     debug_file: dbghelp.pdb
     debug_id: 9c2a902b-6fdf-40ad-8308-588a41d572a0-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70850000'
     image_size: 1331200
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\msvcp140.dll
     code_id: 589abc846c000
     debug_file: msvcp140.i386.pdb
     debug_id: bf5257f7-8c26-43dd-9bb7-901625e1136a-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x709a0000'
     image_size: 442368
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\apphelp.dll
     code_id: 57898eeb92000
     debug_file: apphelp.pdb
     debug_id: 8daf7773-372f-460a-af38-944e193f7e33-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70a10000'
     image_size: 598016
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\dbgcore.dll
     code_id: 57898dab25000
     debug_file: dbgcore.pdb
     debug_id: aec7ef2f-df4b-4642-a471-4c3e5fe8760a-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70b70000'
     image_size: 151552
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\VCRUNTIME140.dll
     code_id: 589abc7714000
     debug_file: vcruntime140.i386.pdb
     debug_id: 0ed80a50-ecda-472b-86a4-eb6c833f8e1b-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x70c60000'
     image_size: 81920
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\CRYPTBASE.dll
     code_id: 57899141a000
     debug_file: cryptbase.pdb
     debug_id: 147c51fb-7ca1-408f-85b5-285f2ad6f9c5-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73ba0000'
     image_size: 40960
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\sspicli.dll
     code_id: 59bf30e31f000
     debug_file: wsspicli.pdb
     debug_id: 51e432b1-0450-4b19-8ed1-6d4335f9f543-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73bb0000'
     image_size: 126976
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\advapi32.dll
     code_id: 5a49bb7677000
     debug_file: advapi32.pdb
     debug_id: 0c799483-b549-417d-8433-4331852031fe-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73c70000'
     image_size: 487424
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\msvcrt.dll
     code_id: 57899155be000
     debug_file: msvcrt.pdb
     debug_id: 6f6409b3-d520-43c7-9b2f-62e00bfe761c-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x73cf0000'
     image_size: 778240
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\sechost.dll
     code_id: 598942c741000
     debug_file: sechost.pdb
     debug_id: 6f6a05dd-0a80-478b-a419-9b88703bf75b-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x74450000'
     image_size: 266240
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\kernel32.dll
     code_id: 590285e9e0000
     debug_file: wkernel32.pdb
     debug_id: d3474559-96f7-47d6-bf43-c176b2171e68-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75050000'
     image_size: 917504
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\bcryptPrimitives.dll
     code_id: 59b0df8f5a000
     debug_file: bcryptprimitives.pdb
     debug_id: 287b19c3-9209-4a2b-bb8f-bcc37f411b11-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75130000'
     image_size: 368640
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\rpcrt4.dll
     code_id: 5a49bb75c1000
     debug_file: wrpcrt4.pdb
     debug_id: ae131c67-27a7-4fa1-9916-b5a4aef41190-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x75810000'
     image_size: 790528
     type: pe
+    unwind_status: missing
   - code_file: C:\Windows\System32\ucrtbase.dll
     code_id: 59bf2b5ae0000
     debug_file: ucrtbase.pdb
     debug_id: 6bedcbce-0a3a-40e9-8040-81c2c8c6cc2f-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x758f0000'
     image_size: 917504
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\KERNELBASE.dll
     code_id: 59bf2bcf1a1000
     debug_file: wkernelbase.pdb
     debug_id: 8462294a-c645-402d-ac82-a4e95f61ddf9-1
+    debug_status: unused
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x76db0000'
     image_size: 1708032
     type: pe
+    unwind_status: unused
   - code_file: C:\Windows\System32\ntdll.dll
     code_id: 59b0d8f3183000
     debug_file: wntdll.pdb
     debug_id: 971f98e5-ce60-41ff-b2d7-235bbeb34578-1
+    debug_status: missing
+    features:
+      has_debug_info: false
+      has_sources: false
+      has_symbols: false
+      has_unwind_info: false
     image_addr: '0x77170000'
     image_size: 1585152
     type: pe
+    unwind_status: missing
 errors:
 - name: timestamp
   type: past_timestamp

Some files were not shown because too many files changed in this diff