import { AnchorHTMLAttributes, cloneElement, createContext, Fragment, 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 CompositeSelect from 'sentry/components/compositeSelect'; 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 {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<keyof typeof displayOptions>; fullStackTrace: boolean; sortBy: keyof typeof sortByOptions; }; type ChildProps = Omit<State, 'sortBy'> & {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; projectId: Project['id']; recentFirst: boolean; stackTraceNotFound: boolean; stackType: STACK_TYPE; title: React.ReactElement<any, any>; type: string; wrapTitle?: boolean; }; export const TraceEventDataSectionContext = createContext<ChildProps | undefined>( undefined ); export function TraceEventDataSection({ type, title, wrapTitle, stackTraceNotFound, fullStackTrace, recentFirst, children, platform, stackType, projectId, eventId, hasNewestFirst, hasMinified, hasVerboseFunctionNames, hasAbsoluteFilePaths, hasAbsoluteAddresses, hasAppOnlyFrames, }: Props) { const api = useApi(); const organization = useOrganization(); const [state, setState] = useState<State>({ sortBy: recentFirst ? 'recent-first' : 'recent-last', fullStackTrace: !hasAppOnlyFrames ? true : fullStackTrace, display: [], }); 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', }, ]; } 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}/${projectId}/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 ( <EventDataSection type={type} title={ <Header> <Title>{cloneElement(title, {type})}</Title> <ActionWrapper> {!stackTraceNotFound && ( <Fragment> {!state.display.includes('raw-stack-trace') && ( <Tooltip title={t('Only full version available')} disabled={hasAppOnlyFrames} > <ButtonBar active={state.fullStackTrace ? 'full' : 'relevant'} merged> <Button size="xs" barId="relevant" onClick={() => setState({ ...state, fullStackTrace: false, }) } disabled={!hasAppOnlyFrames} > {t('Most Relevant')} </Button> <Button size="xs" barId="full" priority={!hasAppOnlyFrames ? 'primary' : undefined} onClick={() => setState({ ...state, fullStackTrace: true, }) } > {t('Full Stack Trace')} </Button> </ButtonBar> </Tooltip> )} {state.display.includes('raw-stack-trace') && nativePlatform && ( <Button size="xs" href={rawStackTraceDownloadLink} title={t('Download raw stack trace file')} > {t('Download')} </Button> )} <CompactSelect triggerProps={{ icon: <IconSort size="xs" />, size: 'xs', title: sortByTooltip, }} isDisabled={!!sortByTooltip} position="bottom-end" onChange={selectedOption => { setState({...state, sortBy: selectedOption.value}); }} value={state.sortBy} options={Object.entries(sortByOptions).map(([value, label]) => ({ label, value: value as keyof typeof sortByOptions, }))} /> <CompositeSelect triggerProps={{ icon: <IconEllipsis size="xs" />, size: 'xs', showChevron: false, 'aria-label': t('Options'), }} triggerLabel="" position="bottom-end" sections={[ { label: t('Display'), value: 'display', defaultValue: state.display, multiple: true, options: getDisplayOptions().map(option => ({ ...option, value: String(option.value), })), onChange: display => setState({...state, display}), }, ]} /> </Fragment> )} </ActionWrapper> </Header> } showPermalink={false} wrapTitle={wrapTitle} > <TraceEventDataSectionContext.Provider value={childProps}> {children(childProps)} </TraceEventDataSectionContext.Provider> </EventDataSection> ); } interface PermalinkTitleProps extends React.DetailedHTMLProps< AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement > {} export function PermalinkTitle(props: PermalinkTitleProps) { return ( <Permalink {...props} href={'#' + props.type} className="permalink"> <StyledIconLink size="xs" color="subText" /> <h3>{props.children}</h3> </Permalink> ); } 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; } `; const Header = styled('div')` width: 100%; display: flex; flex-wrap: wrap; gap: ${space(1)}; align-items: center; justify-content: space-between; `; const Title = styled('div')` flex: 1; @media (min-width: ${props => props.theme.breakpoints.small}) { flex: unset; } `; const ActionWrapper = styled('div')` display: flex; gap: ${space(1)}; `;