import {
AnchorHTMLAttributes,
cloneElement,
createContext,
useCallback,
useState,
} from 'react';
import styled from '@emotion/styled';
import {Button} from 'sentry/components/button';
import ButtonBar from 'sentry/components/buttonBar';
import {CompactSelect} from 'sentry/components/compactSelect';
import HookOrDefault from 'sentry/components/hookOrDefault';
import {SegmentedControl} from 'sentry/components/segmentedControl';
import {Tooltip} from 'sentry/components/tooltip';
import {IconEllipsis, IconLink, IconSort} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {PlatformType, Project} from 'sentry/types';
import {Event} from 'sentry/types/event';
import {STACK_TYPE} from 'sentry/types/stacktrace';
import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import {EventDataSection} from './eventDataSection';
const sortByOptions = {
'recent-first': t('Newest'),
'recent-last': t('Oldest'),
};
export const displayOptions = {
'absolute-addresses': t('Absolute addresses'),
'absolute-file-paths': t('Absolute file paths'),
minified: t('Unsymbolicated'),
'raw-stack-trace': t('Raw stack trace'),
'verbose-function-names': t('Verbose function names'),
};
const HookCodecovCTA = HookOrDefault({hookName: 'component:codecov-integration-cta'});
type State = {
display: Array;
fullStackTrace: boolean;
sortBy: keyof typeof sortByOptions;
};
type ChildProps = Omit & {recentFirst: boolean};
type Props = {
children: (childProps: ChildProps) => React.ReactNode;
eventId: Event['id'];
fullStackTrace: boolean;
hasAbsoluteAddresses: boolean;
hasAbsoluteFilePaths: boolean;
hasAppOnlyFrames: boolean;
hasMinified: boolean;
hasNewestFirst: boolean;
hasVerboseFunctionNames: boolean;
platform: PlatformType;
projectSlug: Project['slug'];
recentFirst: boolean;
stackTraceNotFound: boolean;
stackType: STACK_TYPE;
title: React.ReactElement;
type: string;
wrapTitle?: boolean;
};
export const TraceEventDataSectionContext = createContext(
undefined
);
export function TraceEventDataSection({
type,
title,
wrapTitle,
stackTraceNotFound,
fullStackTrace,
recentFirst,
children,
platform,
stackType,
projectSlug,
eventId,
hasNewestFirst,
hasMinified,
hasVerboseFunctionNames,
hasAbsoluteFilePaths,
hasAbsoluteAddresses,
hasAppOnlyFrames,
}: Props) {
const api = useApi();
const organization = useOrganization();
const [state, setState] = useState({
sortBy: recentFirst ? 'recent-first' : 'recent-last',
fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace,
display: [],
});
const isMobile = isMobilePlatform(platform);
const handleFilterFramesChange = useCallback(
(val: 'full' | 'relevant') => {
const isFullOptionClicked = val === 'full';
trackAdvancedAnalyticsEvent(
isFullOptionClicked
? 'stack-trace.full_stack_trace_clicked'
: 'stack-trace.most_relevant_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
}
);
setState(currentState => ({...currentState, fullStackTrace: isFullOptionClicked}));
},
[organization, platform, projectSlug, isMobile]
);
const handleSortByChange = useCallback(
(val: keyof typeof sortByOptions) => {
const isRecentFirst = val === 'recent-first';
trackAdvancedAnalyticsEvent(
isRecentFirst
? 'stack-trace.sort_option_recent_first_clicked'
: 'stack-trace.sort_option_recent_last_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
}
);
setState(currentState => ({...currentState, sortBy: val}));
},
[organization, platform, projectSlug, isMobile]
);
const handleDisplayChange = useCallback(
(vals: (keyof typeof displayOptions)[]) => {
if (vals.includes('raw-stack-trace')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_raw_stack_trace_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: true,
}
);
} else if (state.display.includes('raw-stack-trace')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_raw_stack_trace_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: false,
}
);
}
if (vals.includes('absolute-addresses')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_absolute_addresses_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: true,
}
);
} else if (state.display.includes('absolute-addresses')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_absolute_addresses_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: false,
}
);
}
if (vals.includes('absolute-file-paths')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_absolute_file_paths_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: true,
}
);
} else if (state.display.includes('absolute-file-paths')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_absolute_file_paths_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: false,
}
);
}
if (vals.includes('minified')) {
trackAdvancedAnalyticsEvent(
platform.startsWith('javascript')
? 'stack-trace.display_option_minified_clicked'
: 'stack-trace.display_option_unsymbolicated_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: true,
}
);
} else if (state.display.includes('minified')) {
trackAdvancedAnalyticsEvent(
platform.startsWith('javascript')
? 'stack-trace.display_option_minified_clicked'
: 'stack-trace.display_option_unsymbolicated_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: false,
}
);
}
if (vals.includes('verbose-function-names')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_verbose_function_names_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: true,
}
);
} else if (state.display.includes('verbose-function-names')) {
trackAdvancedAnalyticsEvent(
'stack-trace.display_option_verbose_function_names_clicked',
{
organization,
project_slug: projectSlug,
platform,
is_mobile: isMobile,
checked: false,
}
);
}
setState(currentState => ({...currentState, display: vals}));
},
[organization, platform, projectSlug, isMobile, state]
);
function getDisplayOptions(): {
label: string;
value: keyof typeof displayOptions;
disabled?: boolean;
tooltip?: string;
}[] {
if (platform === 'objc' || platform === 'native' || platform === 'cocoa') {
return [
{
label: displayOptions['absolute-addresses'],
value: 'absolute-addresses',
disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteAddresses,
tooltip: state.display.includes('raw-stack-trace')
? t('Not available on raw stack trace')
: !hasAbsoluteAddresses
? t('Absolute addresses not available')
: undefined,
},
{
label: displayOptions['absolute-file-paths'],
value: 'absolute-file-paths',
disabled: state.display.includes('raw-stack-trace') || !hasAbsoluteFilePaths,
tooltip: state.display.includes('raw-stack-trace')
? t('Not available on raw stack trace')
: !hasAbsoluteFilePaths
? t('Absolute file paths not available')
: undefined,
},
{
label: displayOptions.minified,
value: 'minified',
disabled: !hasMinified,
tooltip: !hasMinified ? t('Unsymbolicated version not available') : undefined,
},
{
label: displayOptions['raw-stack-trace'],
value: 'raw-stack-trace',
},
{
label: displayOptions['verbose-function-names'],
value: 'verbose-function-names',
disabled: state.display.includes('raw-stack-trace') || !hasVerboseFunctionNames,
tooltip: state.display.includes('raw-stack-trace')
? t('Not available on raw stack trace')
: !hasVerboseFunctionNames
? t('Verbose function names not available')
: undefined,
},
];
}
if (platform.startsWith('python')) {
return [
{
label: displayOptions['raw-stack-trace'],
value: 'raw-stack-trace',
},
];
}
// This logic might be incomplete, but according to the SDK folks, this is 99.9% of the cases
if (platform.startsWith('javascript')) {
return [
{
label: t('Minified'),
value: 'minified',
disabled: !hasMinified,
tooltip: !hasMinified ? t('Minified version not available') : undefined,
},
{
label: displayOptions['raw-stack-trace'],
value: 'raw-stack-trace',
},
];
}
return [
{
label: displayOptions.minified,
value: 'minified',
disabled: !hasMinified,
tooltip: !hasMinified ? t('Minified version not available') : undefined,
},
{
label: displayOptions['raw-stack-trace'],
value: 'raw-stack-trace',
},
];
}
const nativePlatform = isNativePlatform(platform);
const minified = stackType === STACK_TYPE.MINIFIED;
// Apple crash report endpoint
const appleCrashEndpoint = `/projects/${organization.slug}/${projectSlug}/events/${eventId}/apple-crash-report?minified=${minified}`;
const rawStackTraceDownloadLink = `${api.baseUrl}${appleCrashEndpoint}&download=1`;
const sortByTooltip = !hasNewestFirst
? t('Not available on stack trace with single frame')
: state.display.includes('raw-stack-trace')
? t('Not available on raw stack trace')
: undefined;
const childProps = {
recentFirst: state.sortBy === 'recent-first',
display: state.display,
fullStackTrace: state.fullStackTrace,
};
return (
{!state.display.includes('raw-stack-trace') && (
{t('Most Relevant')}
{t('Full Stack Trace')}
)}
{state.display.includes('raw-stack-trace') && nativePlatform && (
)}
,
size: 'xs',
title: sortByTooltip,
}}
disabled={!!sortByTooltip}
position="bottom-end"
onChange={selectedOption => {
handleSortByChange(selectedOption.value);
}}
value={state.sortBy}
options={Object.entries(sortByOptions).map(([value, label]) => ({
label,
value: value as keyof typeof sortByOptions,
}))}
/>
,
size: 'xs',
showChevron: false,
'aria-label': t('Options'),
}}
multiple
triggerLabel=""
position="bottom-end"
value={state.display}
onChange={opts => handleDisplayChange(opts.map(opt => opt.value))}
options={[{label: t('Display'), options: getDisplayOptions()}]}
/>
)
}
showPermalink={false}
wrapTitle={wrapTitle}
>
{children(childProps)}
);
}
interface PermalinkTitleProps
extends React.DetailedHTMLProps<
AnchorHTMLAttributes,
HTMLAnchorElement
> {}
export function PermalinkTitle(props: PermalinkTitleProps) {
return (
{props.children}
);
}
const StyledIconLink = styled(IconLink)`
display: none;
position: absolute;
top: 50%;
left: -${space(2)};
transform: translateY(-50%);
`;
const Permalink = styled('a')`
display: inline-flex;
justify-content: flex-start;
&:hover ${StyledIconLink} {
display: block;
}
`;