Files
2024-02-15 17:01:50 +00:00

406 lines
13 KiB
TypeScript

import {Fragment, useEffect, useState} from 'react';
import styled from '@emotion/styled';
import ErrorBoundary from 'sentry/components/errorBoundary';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners';
import {getLockReason} from 'sentry/components/events/interfaces/threads/threadSelector/lockReason';
import {
getMappedThreadState,
getThreadStateHelpText,
ThreadStates,
} from 'sentry/components/events/interfaces/threads/threadSelector/threadStates';
import Pill from 'sentry/components/pill';
import Pills from 'sentry/components/pills';
import QuestionTooltip from 'sentry/components/questionTooltip';
import TextOverflow from 'sentry/components/textOverflow';
import {IconClock, IconInfo, IconLock, IconPlay, IconTimer} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event, Organization, Project, Thread} from 'sentry/types';
import {EntryType, StackType, StackView} from 'sentry/types';
import {defined} from 'sentry/utils';
import {PermalinkTitle, TraceEventDataSection} from '../traceEventDataSection';
import {ExceptionContent} from './crashContent/exception';
import {StackTraceContent} from './crashContent/stackTrace';
import ThreadSelector from './threads/threadSelector';
import findBestThread from './threads/threadSelector/findBestThread';
import getThreadException from './threads/threadSelector/getThreadException';
import getThreadStacktrace from './threads/threadSelector/getThreadStacktrace';
import NoStackTraceMessage from './noStackTraceMessage';
import {inferPlatform, isStacktraceNewestFirst} from './utils';
type ExceptionProps = React.ComponentProps<typeof ExceptionContent>;
type Props = Pick<ExceptionProps, 'groupingCurrentLevel' | 'hasHierarchicalGrouping'> & {
data: {
values?: Array<Thread>;
};
event: Event;
organization: Organization;
projectSlug: Project['slug'];
};
function getIntendedStackView(
thread: Thread,
exception: ReturnType<typeof getThreadException>
): StackView {
if (exception) {
return exception.values.find(value => !!value.stacktrace?.hasSystemFrames)
? StackView.APP
: StackView.FULL;
}
const stacktrace = getThreadStacktrace(false, thread);
return stacktrace?.hasSystemFrames ? StackView.APP : StackView.FULL;
}
export function getThreadStateIcon(state: ThreadStates | undefined) {
if (state === null || state === undefined) {
return null;
}
switch (state) {
case ThreadStates.BLOCKED:
return <IconLock isSolid />;
case ThreadStates.TIMED_WAITING:
return <IconTimer />;
case ThreadStates.WAITING:
return <IconClock />;
case ThreadStates.RUNNABLE:
return <IconPlay />;
default:
return <IconInfo />;
}
}
// We want to set the active thread every time the event changes because the best thread might not be the same between events
const useActiveThreadState = (
event: Event,
threads: Thread[]
): [Thread | undefined, (newState: Thread | undefined) => void] => {
const bestThread = threads.length ? findBestThread(threads) : undefined;
const [activeThread, setActiveThread] = useState<Thread | undefined>(() => bestThread);
useEffect(() => {
setActiveThread(bestThread);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [event.id]);
return [activeThread, setActiveThread];
};
export function Threads({
data,
event,
projectSlug,
hasHierarchicalGrouping,
groupingCurrentLevel,
organization,
}: Props) {
const threads = data.values ?? [];
const [activeThread, setActiveThread] = useActiveThreadState(event, threads);
const stackTraceNotFound = !threads.length;
const hasMoreThanOneThread = threads.length > 1;
const exception = getThreadException(event, activeThread);
const entryIndex = exception
? event.entries.findIndex(entry => entry.type === EntryType.EXCEPTION)
: event.entries.findIndex(entry => entry.type === EntryType.THREADS);
const meta = event._meta?.entries?.[entryIndex]?.data?.values;
const stackView = activeThread
? getIntendedStackView(activeThread, exception)
: undefined;
function renderPills() {
const {
id,
name,
current,
crashed,
state: threadState,
heldLocks,
} = activeThread ?? {};
if (id === null || id === undefined || !name) {
return null;
}
const threadStateDisplay = getMappedThreadState(threadState);
const lockReason = getLockReason(heldLocks);
return (
<Pills>
<Pill name={t('id')} value={id} />
{!!name?.trim() && <Pill name={t('name')} value={name} />}
{current !== undefined && <Pill name={t('was active')} value={current} />}
{crashed !== undefined && (
<Pill name={t('errored')} className={crashed ? 'false' : 'true'}>
{crashed ? t('yes') : t('no')}
</Pill>
)}
{threadStateDisplay !== undefined && (
<Pill name={t('state')} value={threadStateDisplay} />
)}
{defined(lockReason) && <Pill name={t('lock reason')} value={lockReason} />}
</Pills>
);
}
function renderContent({
display,
recentFirst,
fullStackTrace,
}: Parameters<React.ComponentProps<typeof TraceEventDataSection>['children']>[0]) {
const stackType = display.includes('minified')
? StackType.MINIFIED
: StackType.ORIGINAL;
if (exception) {
return (
<ExceptionContent
stackType={stackType}
stackView={
display.includes('raw-stack-trace')
? StackView.RAW
: fullStackTrace
? StackView.FULL
: StackView.APP
}
projectSlug={projectSlug}
newestFirst={recentFirst}
event={event}
values={exception.values}
groupingCurrentLevel={groupingCurrentLevel}
hasHierarchicalGrouping={hasHierarchicalGrouping}
meta={meta}
threadId={activeThread?.id}
/>
);
}
const stackTrace = getThreadStacktrace(
stackType !== StackType.ORIGINAL,
activeThread
);
if (stackTrace) {
return (
<StackTraceContent
stacktrace={stackTrace}
stackView={
display.includes('raw-stack-trace')
? StackView.RAW
: fullStackTrace
? StackView.FULL
: StackView.APP
}
newestFirst={recentFirst}
event={event}
platform={platform}
groupingCurrentLevel={groupingCurrentLevel}
hasHierarchicalGrouping={hasHierarchicalGrouping}
meta={meta}
threadId={activeThread?.id}
/>
);
}
return (
<NoStackTraceMessage
message={activeThread?.crashed ? t('Thread Errored') : undefined}
/>
);
}
const platform = inferPlatform(event, activeThread);
const threadStateDisplay = getMappedThreadState(activeThread?.state);
const {id: activeThreadId, name: activeThreadName} = activeThread ?? {};
const hideThreadTags = activeThreadId === undefined || !activeThreadName;
return (
<Fragment>
{hasMoreThanOneThread && organization.features.includes('anr-improvements') && (
<Fragment>
<Grid>
<EventDataSection type={EntryType.THREADS} title={t('Threads')}>
{activeThread && (
<Wrapper>
<ThreadSelector
threads={threads}
activeThread={activeThread}
event={event}
onChange={thread => {
setActiveThread(thread);
}}
exception={exception}
/>
</Wrapper>
)}
</EventDataSection>
{activeThread?.state && (
<EventDataSection type={EntryType.THREAD_STATE} title={t('Thread State')}>
<ThreadStateWrapper>
{getThreadStateIcon(threadStateDisplay)}
<ThreadState>{threadStateDisplay}</ThreadState>
{threadStateDisplay && (
<QuestionTooltip
position="top"
size="xs"
containerDisplayMode="block"
title={getThreadStateHelpText(threadStateDisplay)}
/>
)}
<LockReason>{getLockReason(activeThread?.heldLocks)}</LockReason>
</ThreadStateWrapper>
</EventDataSection>
)}
</Grid>
{!hideThreadTags && (
<EventDataSection type={EntryType.THREAD_TAGS} title={t('Thread Tags')}>
{renderPills()}
</EventDataSection>
)}
</Fragment>
)}
<TraceEventDataSection
type={EntryType.THREADS}
projectSlug={projectSlug}
eventId={event.id}
recentFirst={isStacktraceNewestFirst()}
fullStackTrace={stackView === StackView.FULL}
title={
hasMoreThanOneThread &&
activeThread &&
!organization.features.includes('anr-improvements') ? (
<ThreadSelector
threads={threads}
activeThread={activeThread}
event={event}
onChange={thread => {
setActiveThread(thread);
}}
exception={exception}
fullWidth
/>
) : (
<PermalinkTitle>
{hasMoreThanOneThread ? t('Thread Stack Trace') : t('Stack Trace')}
</PermalinkTitle>
)
}
platform={platform}
hasMinified={
!!exception?.values?.find(value => value.rawStacktrace) ||
!!activeThread?.rawStacktrace
}
hasVerboseFunctionNames={
!!exception?.values?.some(
value =>
!!value.stacktrace?.frames?.some(
frame =>
!!frame.rawFunction &&
!!frame.function &&
frame.rawFunction !== frame.function
)
) ||
!!activeThread?.stacktrace?.frames?.some(
frame =>
!!frame.rawFunction &&
!!frame.function &&
frame.rawFunction !== frame.function
)
}
hasAbsoluteFilePaths={
!!exception?.values?.some(
value => !!value.stacktrace?.frames?.some(frame => !!frame.filename)
) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.filename)
}
hasAbsoluteAddresses={
!!exception?.values?.some(
value => !!value.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
) || !!activeThread?.stacktrace?.frames?.some(frame => !!frame.instructionAddr)
}
hasAppOnlyFrames={
!!exception?.values?.some(
value => !!value.stacktrace?.frames?.some(frame => frame.inApp !== true)
) || !!activeThread?.stacktrace?.frames?.some(frame => frame.inApp !== true)
}
hasNewestFirst={
!!exception?.values?.some(
value => (value.stacktrace?.frames ?? []).length > 1
) || (activeThread?.stacktrace?.frames ?? []).length > 1
}
stackTraceNotFound={stackTraceNotFound}
wrapTitle={false}
>
{childrenProps => {
// TODO(scttcper): These are duplicated from renderContent, should consolidate
const stackType = childrenProps.display.includes('minified')
? StackType.MINIFIED
: StackType.ORIGINAL;
const isRaw = childrenProps.display.includes('raw-stack-trace');
const stackTrace = getThreadStacktrace(
stackType !== StackType.ORIGINAL,
activeThread
);
return (
<Fragment>
{!organization.features.includes('anr-improvements') && renderPills()}
{stackTrace && !isRaw && (
<ErrorBoundary customComponent={null}>
<StacktraceBanners event={event} stacktrace={stackTrace} />
</ErrorBoundary>
)}
{renderContent(childrenProps)}
</Fragment>
);
}}
</TraceEventDataSection>
</Fragment>
);
}
const Grid = styled('div')`
display: grid;
grid-template-columns: auto 1fr;
`;
const ThreadStateWrapper = styled('div')`
display: flex;
position: relative;
flex-direction: row;
align-items: flex-start;
gap: ${space(0.5)};
`;
const ThreadState = styled(TextOverflow)`
max-width: 100%;
text-align: left;
font-weight: bold;
`;
const LockReason = styled(TextOverflow)`
font-weight: 400;
color: ${p => p.theme.gray300};
`;
const Wrapper = styled('div')`
align-items: center;
flex-wrap: wrap;
flex-grow: 1;
justify-content: flex-start;
`;