123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- import {useCallback, useEffect, useMemo, useRef, useState} from '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 {isStacktraceNewestFirst} from 'sentry/components/events/interfaces/utils';
- import {Body, Hovercard} from 'sentry/components/hovercard';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {Organization, 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 findBestThread from './events/interfaces/threads/threadSelector/findBestThread';
- import getThreadStacktrace from './events/interfaces/threads/threadSelector/getThreadStacktrace';
- const REQUEST_DELAY = 100;
- const HOVERCARD_CONTENT_DELAY = 400;
- export const STACKTRACE_PREVIEW_TOOLTIP_DELAY = 1000;
- 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 (
- <StackTraceContentV3 {...commonProps} groupingCurrentLevel={groupingCurrentLevel} />
- );
- }
- if (orgFeatures.includes('grouping-stacktrace-ui')) {
- return (
- <StackTraceContentV2 {...commonProps} groupingCurrentLevel={groupingCurrentLevel} />
- );
- }
- return <StackTraceContent {...commonProps} />;
- }
- type StackTracePreviewProps = {
- children: React.ReactNode;
- issueId: string;
- organization: Organization;
- className?: string;
- eventId?: string;
- groupingCurrentLevel?: number;
- projectSlug?: string;
- };
- function StackTracePreview(props: StackTracePreviewProps): React.ReactElement {
- const api = useApi();
- const [loadingVisible, setLoadingVisible] = useState<boolean>(false);
- const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');
- const [event, setEvent] = useState<Event | null>(null);
- const delayTimeoutRef = useRef<number | undefined>(undefined);
- const loaderTimeoutRef = useRef<number | undefined>(undefined);
- useEffect(() => {
- return () => {
- window.clearTimeout(loaderTimeoutRef.current);
- window.clearTimeout(delayTimeoutRef.current);
- };
- }, []);
- const fetchData = useCallback(async () => {
- // Data is already loaded
- if (event) {
- return;
- }
- // These are required props to load data
- if (!props.issueId && !props.eventId && !props.projectSlug) {
- return;
- }
- loaderTimeoutRef.current = window.setTimeout(
- () => setLoadingVisible(true),
- HOVERCARD_CONTENT_DELAY
- );
- try {
- const evt = await api.requestPromise(
- props.eventId && props.projectSlug
- ? `/projects/${props.organization.slug}/${props.projectSlug}/events/${props.eventId}/`
- : `/issues/${props.issueId}/events/latest/?collapse=stacktraceOnly`
- );
- window.clearTimeout(loaderTimeoutRef.current);
- setEvent(evt);
- setStatus('loaded');
- setLoadingVisible(false);
- } catch {
- window.clearTimeout(loaderTimeoutRef.current);
- setEvent(null);
- setStatus('error');
- setLoadingVisible(false);
- }
- }, [
- event,
- api,
- props.organization.slug,
- props.projectSlug,
- props.eventId,
- props.issueId,
- ]);
- const handleMouseEnter = useCallback(() => {
- window.clearTimeout(delayTimeoutRef.current);
- delayTimeoutRef.current = window.setTimeout(fetchData, REQUEST_DELAY);
- }, [fetchData]);
- const handleMouseLeave = useCallback(() => {
- window.clearTimeout(delayTimeoutRef.current);
- delayTimeoutRef.current = undefined;
- }, []);
- // Not sure why we need to stop propagation, maybe to prevent the
- // hovercard from closing? If we are doing this often, maybe it should be
- // part of the hovercard component.
- const handleStackTracePreviewClick = useCallback(
- (e: React.MouseEvent) => void e.stopPropagation(),
- []
- );
- const stacktrace = useMemo(() => (event ? getStacktrace(event) : null), [event]);
- return (
- <span
- className={props.className}
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- >
- <StacktraceHovercard
- body={
- status === 'loading' && !loadingVisible ? null : status === 'loading' ? (
- <NoStackTraceWrapper onClick={handleStackTracePreviewClick}>
- <LoadingIndicator hideMessage size={32} />
- </NoStackTraceWrapper>
- ) : status === 'error' ? (
- <NoStackTraceWrapper onClick={handleStackTracePreviewClick}>
- {t('Failed to load stack trace.')}
- </NoStackTraceWrapper>
- ) : !stacktrace ? (
- <NoStackTraceWrapper onClick={handleStackTracePreviewClick}>
- {t('There is no stack trace available for this issue.')}
- </NoStackTraceWrapper>
- ) : !event ? null : (
- <div onClick={handleStackTracePreviewClick}>
- <StackTracePreviewContent
- event={event}
- stacktrace={stacktrace}
- groupingCurrentLevel={props.groupingCurrentLevel}
- orgFeatures={props.organization.features}
- />
- </div>
- )
- }
- displayTimeout={200}
- position="right"
- state={
- status === 'loading' && loadingVisible
- ? 'loading'
- : !stacktrace
- ? 'empty'
- : 'done'
- }
- tipBorderColor="border"
- tipColor="background"
- >
- {props.children}
- </StacktraceHovercard>
- </span>
- );
- }
- export {StackTracePreview};
- const StacktraceHovercard = styled(Hovercard)<{state: 'loading' | 'empty' | 'done'}>`
- /* Lower z-index to match the modals (10000 vs 10002) to allow stackTraceLinkModal be on top of stack trace preview. */
- z-index: ${p => p.theme.zIndex.modal};
- width: ${p => {
- if (p.state === 'loading') {
- return 'auto';
- }
- if (p.state === 'empty') {
- return '340px';
- }
- return '700px';
- }};
- ${Body} {
- padding: 0;
- max-height: 300px;
- overflow-y: auto;
- overscroll-behavior: contain;
- border-bottom-left-radius: ${p => p.theme.borderRadius};
- border-bottom-right-radius: ${p => p.theme.borderRadius};
- }
- .traceback {
- margin-bottom: 0;
- border: 0;
- box-shadow: none;
- }
- .loading {
- margin: 0 auto;
- .loading-indicator {
- /**
- * Overriding the .less file - for default 64px loader we have the width of border set to 6px
- * For 32px we therefore need 3px to keep the same thickness ratio
- */
- border-width: 3px;
- }
- }
- @media (max-width: ${p => p.theme.breakpoints.large}) {
- display: 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;
- `;
|