123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- import {memo, 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 {Client} from 'sentry/api';
- import ErrorBoundary from 'sentry/components/errorBoundary';
- import EventContexts from 'sentry/components/events/contexts';
- import EventContextSummary from 'sentry/components/events/contextSummary';
- import EventDevice from 'sentry/components/events/device';
- import EventErrors, {Error} from 'sentry/components/events/errors';
- import EventAttachments from 'sentry/components/events/eventAttachments';
- import EventCause from 'sentry/components/events/eventCause';
- import EventCauseEmpty from 'sentry/components/events/eventCauseEmpty';
- import EventDataSection from 'sentry/components/events/eventDataSection';
- import EventExtraData from 'sentry/components/events/eventExtraData';
- import {EventSdk} from 'sentry/components/events/eventSdk';
- import {EventTags} from 'sentry/components/events/eventTags';
- import EventGroupingInfo from 'sentry/components/events/groupingInfo';
- import {EventPackageData} from 'sentry/components/events/packageData';
- import RRWebIntegration from 'sentry/components/events/rrwebIntegration';
- import EventSdkUpdates from 'sentry/components/events/sdkUpdates';
- import {DataSection} from 'sentry/components/events/styles';
- import EventUserFeedback from 'sentry/components/events/userFeedback';
- 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,
- 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 withApi from 'sentry/utils/withApi';
- import withOrganization from 'sentry/utils/withOrganization';
- import {projectProcessingIssuesMessages} from 'sentry/views/settings/project/projectProcessingIssues';
- import findBestThread from './interfaces/threads/threadSelector/findBestThread';
- import getThreadException from './interfaces/threads/threadSelector/getThreadException';
- import EventEntry from './eventEntry';
- import EventReplay from './eventReplay';
- import EventTagsAndScreenshot from './eventTagsAndScreenshot';
- const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
- /^(([\w\$]\.[\w\$]{1,2})|([\w\$]{2}\.[\w\$]\.[\w\$]))(\.|$)/g;
- type ProGuardErrors = Array<Error>;
- type Props = Pick<React.ComponentProps<typeof EventEntry>, 'route' | 'router'> & {
- api: Client;
- 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;
- showExampleCommit?: boolean;
- showTagSummary?: boolean;
- };
- const EventEntries = memo(
- ({
- organization,
- project,
- location,
- api,
- event,
- group,
- className,
- router,
- route,
- isShare = false,
- showExampleCommit = false,
- showTagSummary = true,
- }: Props) => {
- 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 replayId = event?.tags?.find(({key}) => key === 'replayId')?.value;
- useEffect(() => {
- checkProGuardError();
- recordIssueError();
- fetchAttachments();
- }, []);
- function recordIssueError() {
- 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}),
- });
- }
- async function fetchProguardMappingFiles(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 [];
- }
- }
- 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)));
- }
- async function checkProGuardError() {
- 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);
- }
- async function fetchAttachments() {
- 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');
- }
- }
- function renderEntries(definedEvent: Event) {
- const entries = definedEvent.entries;
- if (!Array.isArray(entries)) {
- return null;
- }
- return (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}
- route={route}
- router={router}
- />
- </ErrorBoundary>
- ));
- }
- async function handleDeleteAttachment(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');
- }
- }
- if (!event) {
- return (
- <LatestEventNotAvailable>
- <h3>{t('Latest Event Not Available')}</h3>
- </LatestEventNotAvailable>
- );
- }
- const hasMobileScreenshotsFeature = orgFeatures.includes('mobile-screenshots');
- 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) &&
- (showExampleCommit ? (
- <EventCauseEmpty
- event={event}
- organization={organization}
- project={project}
- />
- ) : (
- <EventCause project={project} event={event} group={group} />
- ))}
- {event.userReport && group && (
- <StyledEventUserFeedback
- report={event.userReport}
- orgId={orgSlug}
- issueId={group.id}
- includeBorder={!hasErrors}
- />
- )}
- {showTagSummary &&
- (hasMobileScreenshotsFeature ? (
- <EventTagsAndScreenshot
- event={event}
- organization={organization as Organization}
- projectId={projectSlug}
- location={location}
- isShare={isShare}
- hasContext={hasContext}
- attachments={attachments}
- onDeleteScreenshot={handleDeleteAttachment}
- />
- ) : (
- (!!(event.tags ?? []).length || hasContext) && (
- <StyledEventDataSection title={t('Tags')} type="tags">
- {hasContext && <EventContextSummary event={event} />}
- <EventTags
- event={event}
- organization={organization as Organization}
- projectId={projectSlug}
- location={location}
- />
- </StyledEventDataSection>
- )
- ))}
- {renderEntries(event)}
- {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 && 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')}
- />
- )}
- {!isShare && (
- <MiniReplayView
- event={event}
- orgFeatures={orgFeatures}
- orgSlug={orgSlug}
- projectSlug={projectSlug}
- replayId={replayId}
- />
- )}
- </div>
- );
- }
- );
- type MiniReplayViewProps = {
- event: Event;
- orgFeatures: string[];
- orgSlug: string;
- projectSlug: string;
- replayId: undefined | string;
- };
- function MiniReplayView({
- event,
- orgFeatures,
- orgSlug,
- projectSlug,
- replayId,
- }: MiniReplayViewProps) {
- const hasEventAttachmentsFeature = orgFeatures.includes('event-attachments');
- const hasSessionReplayFeature = orgFeatures.includes('session-replay-ui');
- if (replayId && hasSessionReplayFeature) {
- return (
- <EventReplay replayId={replayId} orgSlug={orgSlug} projectSlug={projectSlug} />
- );
- }
- if (hasEventAttachmentsFeature) {
- return (
- <RRWebIntegration
- event={event}
- orgId={orgSlug}
- projectId={projectSlug}
- renderer={children => (
- <StyledReplayEventDataSection type="context-replay" title={t('Replay')}>
- {children}
- </StyledReplayEventDataSection>
- )}
- />
- );
- }
- return null;
- }
- const StyledEventDataSection = styled(EventDataSection)`
- /* Hiding the top border because of the event section appears at this breakpoint */
- @media (max-width: 767px) {
- &:first-of-type {
- border-top: none;
- }
- }
- `;
- const LatestEventNotAvailable = styled('div')`
- padding: ${space(2)} ${space(4)};
- `;
- const ErrorContainer = styled('div')`
- /*
- Remove border on adjacent context summary box.
- Once that component uses emotion this will be harder.
- */
- & + .context-summary {
- border-top: none;
- }
- `;
- const BorderlessEventEntries = styled(EventEntries)`
- & ${/* sc-selector */ DataSection} {
- margin-left: 0 !important;
- margin-right: 0 !important;
- padding: ${space(3)} 0 0 0;
- }
- & ${/* sc-selector */ DataSection}:first-child {
- padding-top: 0;
- border-top: 0;
- }
- & ${/* sc-selector */ ErrorContainer} {
- margin-bottom: ${space(2)};
- }
- `;
- 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)};
- `;
- // TODO(ts): any required due to our use of SharedViewOrganization
- export default withOrganization<any>(withApi(EventEntries));
- export {BorderlessEventEntries};
|