import type {AnchorHTMLAttributes} from 'react'; import {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 {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 type {PlatformKey, Project} from 'sentry/types'; import type {Event} from 'sentry/types/event'; import {trackAnalytics} from 'sentry/utils/analytics'; 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'), }; 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: PlatformKey; projectSlug: Project['slug']; recentFirst: boolean; stackTraceNotFound: boolean; title: React.ReactElement; type: string; wrapTitle?: boolean; }; export const TraceEventDataSectionContext = createContext( undefined ); export function TraceEventDataSection({ type, title, wrapTitle, stackTraceNotFound, fullStackTrace, recentFirst, children, platform, 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'; trackAnalytics( 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'; trackAnalytics( 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')) { trackAnalytics('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')) { trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: false, }); } if (vals.includes('absolute-addresses')) { trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: true, }); } else if (state.display.includes('absolute-addresses')) { trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: false, }); } if (vals.includes('absolute-file-paths')) { trackAnalytics('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')) { trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: false, }); } if (vals.includes('minified')) { trackAnalytics( 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')) { trackAnalytics( 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')) { trackAnalytics('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')) { trackAnalytics('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') || platform.startsWith('node')) { 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 = state.display.includes('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; } `;