|
@@ -1,12 +1,14 @@
|
|
|
import React from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
+import * as Sentry from '@sentry/react';
|
|
|
import {Location} from 'history';
|
|
|
|
|
|
+import {Client} from 'app/api';
|
|
|
import ErrorBoundary from 'app/components/errorBoundary';
|
|
|
import EventContexts from 'app/components/events/contexts';
|
|
|
import EventContextSummary from 'app/components/events/contextSummary/contextSummary';
|
|
|
import EventDevice from 'app/components/events/device';
|
|
|
-import EventErrors from 'app/components/events/errors';
|
|
|
+import EventErrors, {Error} from 'app/components/events/errors';
|
|
|
import EventAttachments from 'app/components/events/eventAttachments';
|
|
|
import EventCause from 'app/components/events/eventCause';
|
|
|
import EventCauseEmpty from 'app/components/events/eventCauseEmpty';
|
|
@@ -20,23 +22,41 @@ import RRWebIntegration from 'app/components/events/rrwebIntegration';
|
|
|
import EventSdkUpdates from 'app/components/events/sdkUpdates';
|
|
|
import {DataSection} from 'app/components/events/styles';
|
|
|
import EventUserFeedback from 'app/components/events/userFeedback';
|
|
|
-import {t} from 'app/locale';
|
|
|
+import ExternalLink from 'app/components/links/externalLink';
|
|
|
+import {t, tct} from 'app/locale';
|
|
|
import space from 'app/styles/space';
|
|
|
-import {Group, Organization, Project, SharedViewOrganization} from 'app/types';
|
|
|
-import {Entry, Event} from 'app/types/event';
|
|
|
+import {
|
|
|
+ ExceptionValue,
|
|
|
+ Group,
|
|
|
+ Organization,
|
|
|
+ Project,
|
|
|
+ SharedViewOrganization,
|
|
|
+} from 'app/types';
|
|
|
+import {DebugFile} from 'app/types/debugFiles';
|
|
|
+import {Image} from 'app/types/debugImage';
|
|
|
+import {Entry, EntryType, Event} from 'app/types/event';
|
|
|
+import {Thread} from 'app/types/events';
|
|
|
import {isNotSharedOrganization} from 'app/types/utils';
|
|
|
-import {objectIsEmpty} from 'app/utils';
|
|
|
+import {defined, objectIsEmpty} from 'app/utils';
|
|
|
import {analytics} from 'app/utils/analytics';
|
|
|
+import withApi from 'app/utils/withApi';
|
|
|
import withOrganization from 'app/utils/withOrganization';
|
|
|
+import {projectProcessingIssuesMessages} from 'app/views/settings/project/projectProcessingIssues';
|
|
|
|
|
|
+import findBestThread from './interfaces/threads/threadSelector/findBestThread';
|
|
|
+import getThreadException from './interfaces/threads/threadSelector/getThreadException';
|
|
|
import EventEntry from './eventEntry';
|
|
|
|
|
|
+const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH = /^(\w|\w{2}\.\w{1,2}|\w{3}((\.\w)|(\.\w{2}){2}))(\.|$)/g;
|
|
|
+
|
|
|
const defaultProps = {
|
|
|
isShare: false,
|
|
|
showExampleCommit: false,
|
|
|
showTagSummary: true,
|
|
|
};
|
|
|
|
|
|
+type ProGuardErrors = Array<Error>;
|
|
|
+
|
|
|
type Props = {
|
|
|
/**
|
|
|
* The organization can be the shared view on a public issue view.
|
|
@@ -45,39 +65,210 @@ type Props = {
|
|
|
event: Event;
|
|
|
project: Project;
|
|
|
location: Location;
|
|
|
-
|
|
|
+ api: Client;
|
|
|
group?: Group;
|
|
|
className?: string;
|
|
|
} & typeof defaultProps;
|
|
|
|
|
|
-class EventEntries extends React.Component<Props> {
|
|
|
+type State = {
|
|
|
+ isLoading: boolean;
|
|
|
+ proGuardErrors: ProGuardErrors;
|
|
|
+};
|
|
|
+
|
|
|
+class EventEntries extends React.Component<Props, State> {
|
|
|
static defaultProps = defaultProps;
|
|
|
|
|
|
- componentDidMount() {
|
|
|
- const {event} = this.props;
|
|
|
+ state: State = {
|
|
|
+ isLoading: true,
|
|
|
+ proGuardErrors: [],
|
|
|
+ };
|
|
|
|
|
|
- if (!event || !event.errors || !(event.errors.length > 0)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const errors = event.errors;
|
|
|
- const errorTypes = errors.map(errorEntries => errorEntries.type);
|
|
|
- const errorMessages = errors.map(errorEntries => errorEntries.message);
|
|
|
-
|
|
|
- this.recordIssueError(errorTypes, errorMessages);
|
|
|
+ componentDidMount() {
|
|
|
+ this.checkProGuardError();
|
|
|
+ this.recordIssueError();
|
|
|
}
|
|
|
|
|
|
- shouldComponentUpdate(nextProps: Props) {
|
|
|
+ shouldComponentUpdate(nextProps: Props, nextState: State) {
|
|
|
const {event, showExampleCommit} = this.props;
|
|
|
|
|
|
return (
|
|
|
(event && nextProps.event && event.id !== nextProps.event.id) ||
|
|
|
- showExampleCommit !== nextProps.showExampleCommit
|
|
|
+ showExampleCommit !== nextProps.showExampleCommit ||
|
|
|
+ nextState.isLoading !== this.state.isLoading
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ async fetchProguardMappingFiles(query: string): Promise<Array<DebugFile>> {
|
|
|
+ const {api, organization, project} = this.props;
|
|
|
+ try {
|
|
|
+ const proguardMappingFiles = await api.requestPromise(
|
|
|
+ `/projects/${organization.slug}/${project.slug}/files/dsyms/`,
|
|
|
+ {
|
|
|
+ method: 'GET',
|
|
|
+ query: {
|
|
|
+ query,
|
|
|
+ file_formats: 'proguard',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+ return proguardMappingFiles;
|
|
|
+ } catch (error) {
|
|
|
+ Sentry.captureException(error);
|
|
|
+ // do nothing, the UI will not display extra error details
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ isDataMinified(str: string | null) {
|
|
|
+ if (!str) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return !![...str.matchAll(MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH)].length;
|
|
|
+ }
|
|
|
+
|
|
|
+ hasThreadOrExceptionMinifiedFrameData(event: Event, bestThread?: Thread) {
|
|
|
+ if (!bestThread) {
|
|
|
+ const exceptionValues: Array<ExceptionValue> =
|
|
|
+ event.entries?.find(e => e.type === EntryType.EXCEPTION)?.data?.values ?? [];
|
|
|
+
|
|
|
+ return !!exceptionValues.find(exceptionValue =>
|
|
|
+ exceptionValue.stacktrace?.frames?.find(frame =>
|
|
|
+ this.isDataMinified(frame.module)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const threadExceptionValues = getThreadException(event, bestThread)?.values;
|
|
|
+
|
|
|
+ return !!(threadExceptionValues
|
|
|
+ ? threadExceptionValues.find(threadExceptionValue =>
|
|
|
+ threadExceptionValue.stacktrace?.frames?.find(frame =>
|
|
|
+ this.isDataMinified(frame.module)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ : bestThread?.stacktrace?.frames?.find(frame => this.isDataMinified(frame.module)));
|
|
|
+ }
|
|
|
+
|
|
|
+ async checkProGuardError() {
|
|
|
+ const {event, isShare} = this.props;
|
|
|
+
|
|
|
+ if (!event || event.platform !== 'java') {
|
|
|
+ this.setState({isLoading: false});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const hasEventErrorsProGuardMissingMapping = event.errors?.find(
|
|
|
+ error => error.type === 'proguard_missing_mapping'
|
|
|
);
|
|
|
+
|
|
|
+ if (hasEventErrorsProGuardMissingMapping) {
|
|
|
+ this.setState({isLoading: false});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const proGuardErrors: ProGuardErrors = [];
|
|
|
+
|
|
|
+ const debugImages = event.entries?.find(e => e.type === EntryType.DEBUGMETA)?.data
|
|
|
+ .images as undefined | Array<Image>;
|
|
|
+
|
|
|
+ // When debugImages contains a 'proguard' entry, it must always be only one entry
|
|
|
+ const proGuardImage = debugImages?.find(
|
|
|
+ debugImage => debugImage?.type === 'proguard'
|
|
|
+ );
|
|
|
+
|
|
|
+ const proGuardImageUuid = proGuardImage?.uuid;
|
|
|
+
|
|
|
+ // If an entry is of type 'proguard' and has 'uuid',
|
|
|
+ // it means that the Sentry Gradle plugin has been executed,
|
|
|
+ // otherwise the proguard id wouldn't be in the event.
|
|
|
+ // But maybe it failed to upload the mappings file
|
|
|
+ if (defined(proGuardImageUuid)) {
|
|
|
+ if (isShare) {
|
|
|
+ this.setState({isLoading: false});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const proguardMappingFiles = await this.fetchProguardMappingFiles(
|
|
|
+ proGuardImageUuid
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!proguardMappingFiles.length) {
|
|
|
+ proGuardErrors.push({
|
|
|
+ type: 'proguard_missing_mapping',
|
|
|
+ message: projectProcessingIssuesMessages.proguard_missing_mapping,
|
|
|
+ data: {mapping_uuid: proGuardImageUuid},
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState({proGuardErrors, isLoading: false});
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ if (proGuardImage) {
|
|
|
+ Sentry.withScope(function (s) {
|
|
|
+ s.setLevel(Sentry.Severity.Warning);
|
|
|
+ if (event.sdk) {
|
|
|
+ s.setTag('offending.event.sdk.name', event.sdk.name);
|
|
|
+ s.setTag('offending.event.sdk.version', event.sdk.version);
|
|
|
+ }
|
|
|
+ Sentry.captureMessage('Event contains proguard image but not uuid');
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const threads: Array<Thread> =
|
|
|
+ event.entries?.find(e => e.type === EntryType.THREADS)?.data?.values ?? [];
|
|
|
+
|
|
|
+ const bestThread = findBestThread(threads);
|
|
|
+ const hasThreadOrExceptionMinifiedData = this.hasThreadOrExceptionMinifiedFrameData(
|
|
|
+ event,
|
|
|
+ bestThread
|
|
|
+ );
|
|
|
+
|
|
|
+ if (hasThreadOrExceptionMinifiedData) {
|
|
|
+ proGuardErrors.push({
|
|
|
+ type: 'proguard_potentially_misconfigured_plugin',
|
|
|
+ message: tct(
|
|
|
+ 'Some frames appear to be minified. Did you configure the [plugin]?',
|
|
|
+ {
|
|
|
+ plugin: (
|
|
|
+ <ExternalLink href="https://docs.sentry.io/platforms/android/proguard/#gradle">
|
|
|
+ Sentry Gradle Plugin
|
|
|
+ </ExternalLink>
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ });
|
|
|
+
|
|
|
+ // This capture will be removed once we're confident with the level of effectiveness
|
|
|
+ Sentry.withScope(function (s) {
|
|
|
+ s.setLevel(Sentry.Severity.Warning);
|
|
|
+ if (event.sdk) {
|
|
|
+ s.setTag('offending.event.sdk.name', event.sdk.name);
|
|
|
+ s.setTag('offending.event.sdk.version', event.sdk.version);
|
|
|
+ }
|
|
|
+ Sentry.captureMessage(
|
|
|
+ !proGuardImage
|
|
|
+ ? 'No Proguard is used at all, but a frame did match the regex'
|
|
|
+ : "Displaying ProGuard warning 'proguard_potentially_misconfigured_plugin' for suspected event"
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState({proGuardErrors, isLoading: false});
|
|
|
}
|
|
|
|
|
|
- recordIssueError(errorTypes: any[], errorMessages: string[]) {
|
|
|
+ recordIssueError() {
|
|
|
const {organization, project, event} = this.props;
|
|
|
|
|
|
+ if (!event || !event.errors || !(event.errors.length > 0)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const errors = event.errors;
|
|
|
+ const errorTypes = errors.map(errorEntries => errorEntries.type);
|
|
|
+ const errorMessages = errors.map(errorEntries => errorEntries.message);
|
|
|
+
|
|
|
const orgId = organization.id;
|
|
|
const platform = project.platform;
|
|
|
|
|
@@ -131,9 +322,9 @@ class EventEntries extends React.Component<Props> {
|
|
|
showTagSummary,
|
|
|
location,
|
|
|
} = this.props;
|
|
|
+ const {proGuardErrors, isLoading} = this.state;
|
|
|
|
|
|
- const features =
|
|
|
- organization && organization.features ? new Set(organization.features) : new Set();
|
|
|
+ const features = new Set(organization?.features);
|
|
|
const hasQueryFeature = features.has('discover-query');
|
|
|
|
|
|
if (!event) {
|
|
@@ -143,19 +334,19 @@ class EventEntries extends React.Component<Props> {
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
const hasContext = !objectIsEmpty(event.user) || !objectIsEmpty(event.contexts);
|
|
|
- const hasErrors = !objectIsEmpty(event.errors);
|
|
|
+ const hasErrors = !objectIsEmpty(event.errors) || !!proGuardErrors.length;
|
|
|
|
|
|
return (
|
|
|
- <div className={className} data-test-id="event-entries">
|
|
|
- {hasErrors && (
|
|
|
- <ErrorContainer>
|
|
|
- <EventErrors
|
|
|
- event={event}
|
|
|
- orgSlug={organization.slug}
|
|
|
- projectSlug={project.slug}
|
|
|
- />
|
|
|
- </ErrorContainer>
|
|
|
+ <div className={className} data-test-id={`event-entries-loading-${isLoading}`}>
|
|
|
+ {hasErrors && !isLoading && (
|
|
|
+ <EventErrors
|
|
|
+ event={event}
|
|
|
+ orgSlug={organization.slug}
|
|
|
+ projectSlug={project.slug}
|
|
|
+ proGuardErrors={proGuardErrors}
|
|
|
+ />
|
|
|
)}
|
|
|
{!isShare &&
|
|
|
isNotSharedOrganization(organization) &&
|
|
@@ -260,5 +451,5 @@ const StyledEventUserFeedback = styled(EventUserFeedback)<StyledEventUserFeedbac
|
|
|
`;
|
|
|
|
|
|
// TODO(ts): any required due to our use of SharedViewOrganization
|
|
|
-export default withOrganization<any>(EventEntries);
|
|
|
+export default withOrganization<any>(withApi(EventEntries));
|
|
|
export {BorderlessEventEntries};
|