123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- 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<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: PlatformKey;
- projectSlug: Project['slug'];
- recentFirst: boolean;
- stackTraceNotFound: boolean;
- 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,
- projectSlug,
- 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: [],
- });
- 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 (
- <EventDataSection
- type={type}
- title={cloneElement(title, {type})}
- guideTarget="stacktrace"
- actions={
- !stackTraceNotFound && (
- <ButtonBar gap={1}>
- {!state.display.includes('raw-stack-trace') && (
- <Tooltip
- title={t('Only full version available')}
- disabled={hasAppOnlyFrames}
- >
- <SegmentedControl
- size="xs"
- aria-label={t('Filter frames')}
- value={state.fullStackTrace ? 'full' : 'relevant'}
- onChange={handleFilterFramesChange}
- >
- <SegmentedControl.Item key="relevant" disabled={!hasAppOnlyFrames}>
- {t('Most Relevant')}
- </SegmentedControl.Item>
- <SegmentedControl.Item key="full">
- {t('Full Stack Trace')}
- </SegmentedControl.Item>
- </SegmentedControl>
- </Tooltip>
- )}
- {state.display.includes('raw-stack-trace') && nativePlatform && (
- <Button
- size="xs"
- href={rawStackTraceDownloadLink}
- title={t('Download raw stack trace file')}
- onClick={() => {
- trackAnalytics('stack-trace.download_clicked', {
- organization,
- project_slug: projectSlug,
- platform,
- is_mobile: isMobile,
- });
- }}
- >
- {t('Download')}
- </Button>
- )}
- <CompactSelect
- triggerProps={{
- icon: <IconSort />,
- 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,
- }))}
- />
- <CompactSelect
- triggerProps={{
- icon: <IconEllipsis />,
- 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()}]}
- />
- </ButtonBar>
- )
- }
- 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;
- }
- `;
|