import {useCallback, useEffect, useMemo, useState} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import StackTraceContent from 'sentry/components/events/interfaces/crashContent/stackTrace/content';
import StackTraceContentV2 from 'sentry/components/events/interfaces/crashContent/stackTrace/contentV2';
import StackTraceContentV3 from 'sentry/components/events/interfaces/crashContent/stackTrace/contentV3';
import findBestThread from 'sentry/components/events/interfaces/threads/threadSelector/findBestThread';
import getThreadStacktrace from 'sentry/components/events/interfaces/threads/threadSelector/getThreadStacktrace';
import {isStacktraceNewestFirst} from 'sentry/components/events/interfaces/utils';
import {GroupPreviewHovercard} from 'sentry/components/groupPreviewTooltip/groupPreviewHovercard';
import {useDelayedLoadingState} from 'sentry/components/groupPreviewTooltip/utils';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
import space from 'sentry/styles/space';
import {PlatformType} from 'sentry/types';
import {EntryType, Event} from 'sentry/types/event';
import {StacktraceType} from 'sentry/types/stacktrace';
import {defined} from 'sentry/utils';
import {isNativePlatform} from 'sentry/utils/platform';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
function getStacktrace(event: Event): StacktraceType | null {
const exceptionsWithStacktrace =
event.entries
.find(e => e.type === EntryType.EXCEPTION)
?.data?.values.filter(({stacktrace}) => defined(stacktrace)) ?? [];
const exceptionStacktrace: StacktraceType | undefined = isStacktraceNewestFirst()
? exceptionsWithStacktrace[exceptionsWithStacktrace.length - 1]?.stacktrace
: exceptionsWithStacktrace[0]?.stacktrace;
if (exceptionStacktrace) {
return exceptionStacktrace;
}
const threads =
event.entries.find(e => e.type === EntryType.THREADS)?.data?.values ?? [];
const bestThread = findBestThread(threads);
if (!bestThread) {
return null;
}
const bestThreadStacktrace = getThreadStacktrace(false, bestThread);
if (bestThreadStacktrace) {
return bestThreadStacktrace;
}
return null;
}
function StackTracePreviewContent({
event,
stacktrace,
orgFeatures = [],
groupingCurrentLevel,
}: {
event: Event;
stacktrace: StacktraceType;
groupingCurrentLevel?: number;
orgFeatures?: string[];
}) {
const includeSystemFrames = useMemo(() => {
return stacktrace?.frames?.every(frame => !frame.inApp) ?? false;
}, [stacktrace]);
const framePlatform = stacktrace?.frames?.find(frame => !!frame.platform)?.platform;
const platform = (framePlatform ?? event.platform ?? 'other') as PlatformType;
const newestFirst = isStacktraceNewestFirst();
const commonProps = {
data: stacktrace,
expandFirstFrame: false,
includeSystemFrames,
platform,
newestFirst,
event,
isHoverPreviewed: true,
};
if (orgFeatures.includes('native-stack-trace-v2') && isNativePlatform(platform)) {
return (
);
}
if (orgFeatures.includes('grouping-stacktrace-ui')) {
return (
);
}
return ;
}
type StackTracePreviewProps = {
children: React.ReactChild;
issueId: string;
eventId?: string;
groupingCurrentLevel?: number;
projectSlug?: string;
};
interface StackTracePreviewBodyProps
extends Pick<
StackTracePreviewProps,
'issueId' | 'eventId' | 'groupingCurrentLevel' | 'projectSlug'
> {
onRequestBegin: () => void;
onRequestEnd: () => void;
onUnmount: () => void;
}
function StackTracePreviewBody({
issueId,
eventId,
groupingCurrentLevel,
projectSlug,
onRequestBegin,
onRequestEnd,
onUnmount,
}: StackTracePreviewBodyProps) {
const api = useApi();
const organization = useOrganization();
const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');
const [event, setEvent] = useState(null);
const fetchData = useCallback(async () => {
onRequestBegin();
// Data is already loaded
if (event) {
onRequestEnd();
return;
}
// These are required props to load data
if (issueId && eventId && !projectSlug) {
onRequestEnd();
return;
}
try {
const evt = await api.requestPromise(
eventId && projectSlug
? `/projects/${organization.slug}/${projectSlug}/events/${eventId}/`
: `/issues/${issueId}/events/latest/?collapse=stacktraceOnly`,
{query: {referrer: 'api.issues.preview-error'}}
);
setEvent(evt);
setStatus('loaded');
} catch {
setEvent(null);
setStatus('error');
} finally {
onRequestEnd();
}
}, [
event,
issueId,
eventId,
projectSlug,
onRequestEnd,
onRequestBegin,
api,
organization.slug,
]);
useEffect(() => {
fetchData();
return () => {
onUnmount();
};
}, [fetchData, onUnmount]);
const stacktrace = useMemo(() => (event ? getStacktrace(event) : null), [event]);
switch (status) {
case 'loading':
return (
);
case 'error':
return (
{t('Failed to load stack trace.')}
);
case 'loaded': {
if (stacktrace && event) {
return (
);
}
return (
{t('There is no stack trace available for this issue.')}
);
}
default: {
return null;
}
}
}
function StackTracePreview({children, ...props}: StackTracePreviewProps) {
const organization = useOrganization();
const {shouldShowLoadingState, onRequestBegin, onRequestEnd, reset} =
useDelayedLoadingState();
const hasGroupingStacktraceUI = organization.features.includes(
'grouping-stacktrace-ui'
);
return (
}
>
{children}
);
}
export {StackTracePreview};
const Wrapper = styled('span')<{
hasGroupingStacktraceUI: boolean;
}>`
${p =>
p.hasGroupingStacktraceUI &&
css`
display: inline-flex;
overflow: hidden;
height: 100%;
> span:first-child {
${p.theme.overflowEllipsis}
}
`}
`;
const StackTracePreviewWrapper = styled('div')`
width: 700px;
.traceback {
margin-bottom: 0;
border: 0;
box-shadow: none;
}
`;
const NoStackTraceWrapper = styled('div')`
color: ${p => p.theme.subText};
padding: ${space(1.5)};
font-size: ${p => p.theme.fontSizeMedium};
display: flex;
align-items: center;
justify-content: center;
min-height: 56px;
`;