123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- import {Fragment, useCallback, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import {Location} from 'history';
- import uniq from 'lodash/uniq';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import ErrorBoundary from 'sentry/components/errorBoundary';
- import ExternalLink from 'sentry/components/links/externalLink';
- import {t, tct} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {
- Entry,
- EntryType,
- Event,
- ExceptionValue,
- Group,
- IssueAttachment,
- IssueCategory,
- Organization,
- Project,
- SharedViewOrganization,
- Thread,
- } from 'sentry/types';
- import {DebugFile} from 'sentry/types/debugFiles';
- import {Image} from 'sentry/types/debugImage';
- import {isNotSharedOrganization} from 'sentry/types/utils';
- import {defined, objectIsEmpty} from 'sentry/utils';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import useApi from 'sentry/utils/useApi';
- import {projectProcessingIssuesMessages} from 'sentry/views/settings/project/projectProcessingIssues';
- import {CommitRow} from '../commitRow';
- import findBestThread from './interfaces/threads/threadSelector/findBestThread';
- import getThreadException from './interfaces/threads/threadSelector/getThreadException';
- import {EventContexts} from './contexts';
- import {EventDevice} from './device';
- import {Error, EventErrors} from './errors';
- import {EventAttachments} from './eventAttachments';
- import {EventCause} from './eventCause';
- import {EventDataSection} from './eventDataSection';
- import {EventEntry} from './eventEntry';
- import {EventExtraData} from './eventExtraData';
- import {EventSdk} from './eventSdk';
- import {EventTagsAndScreenshot} from './eventTagsAndScreenshot';
- import {EventViewHierarchy} from './eventViewHierarchy';
- import {EventGroupingInfo} from './groupingInfo';
- import {EventPackageData} from './packageData';
- import {EventRRWebIntegration} from './rrwebIntegration';
- import {EventSdkUpdates} from './sdkUpdates';
- import {DataSection} from './styles';
- import {EventUserFeedback} from './userFeedback';
- const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
- /^(([\w\$]\.[\w\$]{1,2})|([\w\$]{2}\.[\w\$]\.[\w\$]))(\.|$)/g;
- function isDataMinified(str: string | null) {
- if (!str) {
- return false;
- }
- return !![...str.matchAll(MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH)].length;
- }
- function hasThreadOrExceptionMinifiedFrameData(definedEvent: Event, bestThread?: Thread) {
- if (!bestThread) {
- const exceptionValues: Array<ExceptionValue> =
- definedEvent.entries?.find(e => e.type === EntryType.EXCEPTION)?.data?.values ?? [];
- return !!exceptionValues.find(exceptionValue =>
- exceptionValue.stacktrace?.frames?.find(frame => isDataMinified(frame.module))
- );
- }
- const threadExceptionValues = getThreadException(definedEvent, bestThread)?.values;
- return !!(threadExceptionValues
- ? threadExceptionValues.find(threadExceptionValue =>
- threadExceptionValue.stacktrace?.frames?.find(frame =>
- isDataMinified(frame.module)
- )
- )
- : bestThread?.stacktrace?.frames?.find(frame => isDataMinified(frame.module)));
- }
- type ProGuardErrors = Array<Error>;
- type Props = {
- location: Location;
- /**
- * The organization can be the shared view on a public issue view.
- */
- organization: Organization | SharedViewOrganization;
- project: Project;
- className?: string;
- event?: Event;
- group?: Group;
- isShare?: boolean;
- showTagSummary?: boolean;
- };
- const EventEntries = ({
- organization,
- project,
- location,
- event,
- group,
- className,
- isShare = false,
- showTagSummary = true,
- }: Props) => {
- const api = useApi();
- const [isLoading, setIsLoading] = useState(true);
- const [proGuardErrors, setProGuardErrors] = useState<ProGuardErrors>([]);
- const [attachments, setAttachments] = useState<IssueAttachment[]>([]);
- const orgSlug = organization.slug;
- const projectSlug = project.slug;
- const orgFeatures = organization?.features ?? [];
- const hasEventAttachmentsFeature = orgFeatures.includes('event-attachments');
- const hasReplay = Boolean(event?.tags?.find(({key}) => key === 'replayId')?.value);
- const recordIssueError = useCallback(() => {
- 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 platform = project.platform;
- // uniquify the array types
- trackAdvancedAnalyticsEvent('issue_error_banner.viewed', {
- organization: organization as Organization,
- group: event?.groupID,
- error_type: uniq(errorTypes),
- error_message: uniq(errorMessages),
- ...(platform && {platform}),
- });
- }, [event, organization, project.platform]);
- const fetchProguardMappingFiles = useCallback(
- async (query: string): Promise<Array<DebugFile>> => {
- try {
- const proguardMappingFiles = await api.requestPromise(
- `/projects/${orgSlug}/${projectSlug}/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 [];
- }
- },
- [api, orgSlug, projectSlug]
- );
- const checkProGuardError = useCallback(async () => {
- if (!event || event.platform !== 'java') {
- setIsLoading(false);
- return;
- }
- const hasEventErrorsProGuardMissingMapping = event.errors?.find(
- error => error.type === 'proguard_missing_mapping'
- );
- if (hasEventErrorsProGuardMissingMapping) {
- setIsLoading(false);
- return;
- }
- const newProGuardErrors: 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) {
- setIsLoading(false);
- return;
- }
- const proguardMappingFiles = await fetchProguardMappingFiles(proGuardImageUuid);
- if (!proguardMappingFiles.length) {
- newProGuardErrors.push({
- type: 'proguard_missing_mapping',
- message: projectProcessingIssuesMessages.proguard_missing_mapping,
- data: {mapping_uuid: proGuardImageUuid},
- });
- }
- setProGuardErrors(newProGuardErrors);
- setIsLoading(false);
- return;
- }
- if (proGuardImage) {
- Sentry.withScope(function (s) {
- s.setLevel('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 = hasThreadOrExceptionMinifiedFrameData(
- event,
- bestThread
- );
- if (hasThreadOrExceptionMinifiedData) {
- newProGuardErrors.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>
- ),
- }
- ),
- });
- }
- setProGuardErrors(newProGuardErrors);
- setIsLoading(false);
- }, [event, fetchProguardMappingFiles, isShare]);
- const fetchAttachments = useCallback(async () => {
- if (!event || isShare || !hasEventAttachmentsFeature) {
- return;
- }
- try {
- const response = await api.requestPromise(
- `/projects/${orgSlug}/${projectSlug}/events/${event.id}/attachments/`
- );
- setAttachments(response);
- } catch (error) {
- Sentry.captureException(error);
- addErrorMessage('An error occurred while fetching attachments');
- }
- }, [api, event, hasEventAttachmentsFeature, isShare, orgSlug, projectSlug]);
- const handleDeleteAttachment = useCallback(
- async (attachmentId: IssueAttachment['id']) => {
- if (!event) {
- return;
- }
- try {
- await api.requestPromise(
- `/projects/${orgSlug}/${projectSlug}/events/${event.id}/attachments/${attachmentId}/`,
- {
- method: 'DELETE',
- }
- );
- setAttachments(attachments.filter(attachment => attachment.id !== attachmentId));
- } catch (error) {
- Sentry.captureException(error);
- addErrorMessage('An error occurred while deleting the attachment');
- }
- },
- [api, attachments, event, orgSlug, projectSlug]
- );
- useEffect(() => {
- checkProGuardError();
- }, [checkProGuardError]);
- useEffect(() => {
- recordIssueError();
- }, [recordIssueError]);
- useEffect(() => {
- fetchAttachments();
- }, [fetchAttachments]);
- if (!event) {
- return (
- <LatestEventNotAvailable>
- <h3>{t('Latest Event Not Available')}</h3>
- </LatestEventNotAvailable>
- );
- }
- const hasContext = !objectIsEmpty(event.user ?? {}) || !objectIsEmpty(event.contexts);
- const hasErrors = !objectIsEmpty(event.errors) || !!proGuardErrors.length;
- return (
- <div className={className} data-test-id={`event-entries-loading-${isLoading}`}>
- {hasErrors && !isLoading && (
- <EventErrors
- event={event}
- orgSlug={orgSlug}
- projectSlug={projectSlug}
- proGuardErrors={proGuardErrors}
- />
- )}
- {!isShare && isNotSharedOrganization(organization) && (
- <EventCause
- project={project}
- eventId={event.id}
- group={group}
- commitRow={CommitRow}
- />
- )}
- {event.userReport && group && (
- <StyledEventUserFeedback
- report={event.userReport}
- orgId={orgSlug}
- issueId={group.id}
- includeBorder={!hasErrors}
- />
- )}
- {showTagSummary && (
- <EventTagsAndScreenshot
- event={event}
- organization={organization as Organization}
- projectId={projectSlug}
- location={location}
- isShare={isShare}
- hasContext={hasContext}
- attachments={attachments}
- onDeleteScreenshot={handleDeleteAttachment}
- />
- )}
- <Entries
- definedEvent={event}
- projectSlug={projectSlug}
- group={group}
- organization={organization}
- isShare={isShare}
- />
- {hasContext && <EventContexts group={group} event={event} />}
- {event && !objectIsEmpty(event.context) && <EventExtraData event={event} />}
- {event && !objectIsEmpty(event.packages) && <EventPackageData event={event} />}
- {event && !objectIsEmpty(event.device) && <EventDevice event={event} />}
- {!isShare &&
- organization.features?.includes('mobile-view-hierarchies') &&
- hasEventAttachmentsFeature &&
- !!attachments.filter(attachment => attachment.type === 'event.view_hierarchy')
- .length && (
- <EventViewHierarchy
- projectSlug={projectSlug}
- viewHierarchies={attachments.filter(
- attachment => attachment.type === 'event.view_hierarchy'
- )}
- />
- )}
- {!isShare && hasEventAttachmentsFeature && (
- <EventAttachments
- event={event}
- orgId={orgSlug}
- projectId={projectSlug}
- location={location}
- attachments={attachments}
- onDeleteAttachment={handleDeleteAttachment}
- />
- )}
- {event.sdk && !objectIsEmpty(event.sdk) && (
- <EventSdk sdk={event.sdk} meta={event._meta?.sdk} />
- )}
- {!isShare && event?.sdkUpdates && event.sdkUpdates.length > 0 && (
- <EventSdkUpdates event={{sdkUpdates: event.sdkUpdates, ...event}} />
- )}
- {!isShare && event.groupID && (
- <EventGroupingInfo
- projectId={projectSlug}
- event={event}
- showGroupingConfig={
- orgFeatures.includes('set-grouping-config') && 'groupingConfig' in event
- }
- />
- )}
- {!isShare && !hasReplay && hasEventAttachmentsFeature && (
- <EventRRWebIntegration
- event={event}
- orgId={orgSlug}
- projectId={projectSlug}
- renderer={children => (
- <StyledReplayEventDataSection type="context-replay" title={t('Replay')}>
- {children}
- </StyledReplayEventDataSection>
- )}
- />
- )}
- </div>
- );
- };
- function injectResourcesEntry(definedEvent: Event) {
- const entries = definedEvent.entries;
- let adjustedEntries: Entry[] = [];
- // This check is to ensure we are not injecting multiple Resources entries
- const resourcesIndex = entries.findIndex(entry => entry.type === EntryType.RESOURCES);
- if (resourcesIndex === -1) {
- const spansIndex = entries.findIndex(entry => entry.type === EntryType.SPANS);
- const breadcrumbsIndex = entries.findIndex(
- entry => entry.type === EntryType.BREADCRUMBS
- );
- // We want the Resources section to appear after Breadcrumbs.
- // If Breadcrumbs are included on this event, we will inject this entry right after it.
- // Otherwise, we inject it after the Spans entry.
- const resourcesEntry: Entry = {type: EntryType.RESOURCES, data: null};
- if (breadcrumbsIndex > -1) {
- adjustedEntries = [
- ...entries.slice(0, breadcrumbsIndex + 1),
- resourcesEntry,
- ...entries.slice(breadcrumbsIndex + 1, entries.length),
- ];
- } else if (spansIndex > -1) {
- adjustedEntries = [
- ...entries.slice(0, spansIndex + 1),
- resourcesEntry,
- ...entries.slice(spansIndex + 1, entries.length),
- ];
- }
- }
- if (adjustedEntries.length > 0) {
- definedEvent.entries = adjustedEntries;
- }
- }
- function Entries({
- definedEvent,
- projectSlug,
- isShare,
- group,
- organization,
- }: {
- definedEvent: Event;
- projectSlug: string;
- isShare?: boolean;
- } & Pick<Props, 'group' | 'organization'>) {
- if (!Array.isArray(definedEvent.entries)) {
- return null;
- }
- if (group?.issueCategory === IssueCategory.PERFORMANCE) {
- injectResourcesEntry(definedEvent);
- }
- return (
- <Fragment>
- {(definedEvent.entries as Array<Entry>).map((entry, entryIdx) => (
- <ErrorBoundary
- key={`entry-${entryIdx}`}
- customComponent={
- <EventDataSection type={entry.type} title={entry.type}>
- <p>{t('There was an error rendering this data.')}</p>
- </EventDataSection>
- }
- >
- <EventEntry
- projectSlug={projectSlug}
- group={group}
- organization={organization}
- event={definedEvent}
- entry={entry}
- isShare={isShare}
- />
- </ErrorBoundary>
- ))}
- </Fragment>
- );
- }
- const LatestEventNotAvailable = styled('div')`
- padding: ${space(2)} ${space(4)};
- `;
- const BorderlessEventEntries = styled(EventEntries)`
- & ${DataSection} {
- margin-left: 0 !important;
- margin-right: 0 !important;
- padding: ${space(3)} 0 0 0;
- }
- & ${DataSection}:first-child {
- padding-top: 0;
- border-top: 0;
- }
- `;
- type StyledEventUserFeedbackProps = {
- includeBorder: boolean;
- };
- const StyledEventUserFeedback = styled(EventUserFeedback)<StyledEventUserFeedbackProps>`
- border-radius: 0;
- box-shadow: none;
- padding: ${space(3)} ${space(4)} 0 40px;
- border: 0;
- ${p => (p.includeBorder ? `border-top: 1px solid ${p.theme.innerBorder};` : '')}
- margin: 0;
- `;
- const StyledReplayEventDataSection = styled(EventDataSection)`
- overflow: hidden;
- margin-bottom: ${space(3)};
- `;
- export default EventEntries;
- export {BorderlessEventEntries};
|