Skip to content

Commit

Permalink
✨ Implement new breadcrumbs for trace view
Browse files Browse the repository at this point in the history
  • Loading branch information
leeandher committed Jul 12, 2024
1 parent 9576387 commit 33db12d
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export default function BreadcrumbsDataSection({
}: BreadcrumbsDataSectionProps) {
const {openDrawer} = useDrawer();
const organization = useOrganization();
// Use the local storage preferences, but allow the drawer to do updates
const [timeDisplay, setTimeDisplay] = useLocalStorageState<BreadcrumbTimeDisplay>(
BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
BreadcrumbTimeDisplay.RELATIVE
Expand Down
141 changes: 89 additions & 52 deletions static/app/components/events/breadcrumbs/breadcrumbsDrawerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,61 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
import useOrganization from 'sentry/utils/useOrganization';

/**
* Highly opinionated hook for breadcrumb controls.
* Used to share controls across trace view and issue details so they do not diverge.
* The controls appearances are different though, so components cannot be shared directly.
*/
export function useBreadcrumbControls({
enhancedCrumbs: breadcrumbs,
}: {
enhancedCrumbs: EnhancedCrumb[];
}) {
const [search, setSearch] = useState('');
const [filterSet, setFilterSet] = useState(new Set<string>());
const [sort, setSort] = useLocalStorageState<BreadcrumbSort>(
BREADCRUMB_SORT_LOCALSTORAGE_KEY,
BreadcrumbSort.NEWEST
);
const [timeDisplay, setTimeDisplay] = useLocalStorageState<BreadcrumbTimeDisplay>(
BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
BreadcrumbTimeDisplay.RELATIVE
);
const filterOptions = useMemo(
() => getBreadcrumbFilterOptions(breadcrumbs),
[breadcrumbs]
);
const displayCrumbs = useMemo(() => {
const sortedCrumbs =
sort === BreadcrumbSort.OLDEST ? breadcrumbs : [...breadcrumbs].reverse();
const filteredCrumbs = sortedCrumbs.filter(ec =>
filterSet.size === 0 ? true : filterSet.has(ec.filter)
);
const searchedCrumbs = applyBreadcrumbSearch(search, filteredCrumbs);
return searchedCrumbs;
}, [breadcrumbs, sort, filterSet, search]);
const startTimeString = useMemo(
() =>
timeDisplay === BreadcrumbTimeDisplay.RELATIVE
? displayCrumbs?.at(0)?.breadcrumb?.timestamp
: undefined,
[displayCrumbs, timeDisplay]
);
return {
search,
setSearch,
filterSet,
setFilterSet,
filterOptions,
sort,
setSort,
timeDisplay,
setTimeDisplay,
displayCrumbs,
startTimeString,
};
}

export const enum BreadcrumbControlOptions {
SEARCH = 'search',
FILTER = 'filter',
Expand Down Expand Up @@ -59,41 +114,21 @@ export function BreadcrumbsDrawerContent({
}: BreadcrumbsDrawerContentProps) {
const organization = useOrganization();
const theme = useTheme();

const [search, setSearch] = useState('');
const [filterSet, setFilterSet] = useState(new Set<string>());
const [sort, setSort] = useLocalStorageState<BreadcrumbSort>(
BREADCRUMB_SORT_LOCALSTORAGE_KEY,
BreadcrumbSort.NEWEST
);
const {getFocusProps} = useFocusControl(initialFocusControl);

const [timeDisplay, setTimeDisplay] = useLocalStorageState<BreadcrumbTimeDisplay>(
BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY,
BreadcrumbTimeDisplay.RELATIVE
);
const filterOptions = useMemo(
() => getBreadcrumbFilterOptions(breadcrumbs),
[breadcrumbs]
);

const displayCrumbs = useMemo(() => {
const sortedCrumbs =
sort === BreadcrumbSort.OLDEST ? breadcrumbs : [...breadcrumbs].reverse();
const filteredCrumbs = sortedCrumbs.filter(ec =>
filterSet.size === 0 ? true : filterSet.has(ec.filter)
);
const searchedCrumbs = applyBreadcrumbSearch(search, filteredCrumbs);
return searchedCrumbs;
}, [breadcrumbs, sort, filterSet, search]);

const startTimeString = useMemo(
() =>
timeDisplay === BreadcrumbTimeDisplay.RELATIVE
? displayCrumbs?.at(0)?.breadcrumb?.timestamp
: undefined,
[displayCrumbs, timeDisplay]
);
const {
search,
setSearch,
filterSet,
setFilterSet,
filterOptions,
sort,
setSort,
timeDisplay,
setTimeDisplay,
displayCrumbs,
startTimeString,
} = useBreadcrumbControls({enhancedCrumbs: breadcrumbs});

const actions = (
<ButtonBar gap={1}>
Expand Down Expand Up @@ -192,9 +227,7 @@ export function BreadcrumbsDrawerContent({
}}
value={timeDisplay}
options={BREADCRUMB_TIME_DISPLAY_OPTIONS}
>
{null}
</CompactSelect>
/>
</ButtonBar>
);

Expand All @@ -206,22 +239,16 @@ export function BreadcrumbsDrawerContent({
</HeaderGrid>
<TimelineContainer>
{displayCrumbs.length === 0 ? (
<EmptyMessage>
{t('No breadcrumbs found.')}
<Button
priority="link"
onClick={() => {
setFilterSet(new Set());
setSearch('');
trackAnalytics('breadcrumbs.drawer.action', {
control: 'clear_filters',
organization,
});
}}
>
{t('Clear Filters?')}
</Button>
</EmptyMessage>
<EmptyBreadcrumbMessage
onClear={() => {
setFilterSet(new Set());
setSearch('');
trackAnalytics('breadcrumbs.drawer.action', {
control: 'clear_filters',
organization,
});
}}
/>
) : (
<BreadcrumbsTimeline
breadcrumbs={displayCrumbs}
Expand All @@ -232,6 +259,16 @@ export function BreadcrumbsDrawerContent({
</Fragment>
);
}
export function EmptyBreadcrumbMessage({onClear}: {onClear: () => void}) {
return (
<EmptyMessage>
{t('No breadcrumbs found.')}
<Button priority="link" onClick={onClear}>
{t('Clear Filters?')}
</Button>
</EmptyMessage>
);
}

const VisibleFocusButton = styled(Button)`
box-shadow: ${p => (p.autoFocus ? p.theme.button.default.focusBorder : 'transparent')} 0
Expand Down
38 changes: 35 additions & 3 deletions static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {shouldUse24Hours} from 'sentry/utils/dates';

interface BreadcrumbsTimelineProps {
breadcrumbs: EnhancedCrumb[];
/**
* If false, expands the contents of the breadcrumb's data payload, adds padding.
*/
fixedHeight?: number;
/**
* If false, expands the contents of the breadcrumb's data payload, adds padding.
*/
Expand All @@ -36,6 +40,7 @@ export default function BreadcrumbsTimeline({
breadcrumbs,
startTimeString,
isCompact = false,
fixedHeight,
showLastLine = false,
}: BreadcrumbsTimelineProps) {
const containerRef = useRef<HTMLDivElement>(null);
Expand All @@ -53,6 +58,7 @@ export default function BreadcrumbsTimeline({
}

const virtualItems = virtualizer.getVirtualItems();

const items = virtualItems.map(virtualizedRow => {
const {breadcrumb, raw, title, meta, iconComponent, colorConfig, levelComponent} =
breadcrumbs[virtualizedRow.index];
Expand Down Expand Up @@ -114,11 +120,21 @@ export default function BreadcrumbsTimeline({
<div
ref={containerRef}
style={{
height: virtualizer.getTotalSize(),
contain: 'layout size',
height: fixedHeight,
overflowY: fixedHeight ? 'auto' : undefined,
paddingRight: fixedHeight ? space(2) : undefined,
}}
>
<Timeline.Container>{items}</Timeline.Container>
<div
style={{
height: virtualizer.getTotalSize(),
contain: 'layout size',
}}
>
<VirtualOffset offset={virtualItems[0]?.start ?? 0}>
<Timeline.Container>{items}</Timeline.Container>
</VirtualOffset>
</div>
</div>
);
}
Expand All @@ -142,3 +158,19 @@ const Timestamp = styled('div')`
const ContentWrapper = styled('div')<{isCompact: boolean}>`
padding-bottom: ${p => space(p.isCompact ? 0.5 : 1.0)};
`;

function VirtualOffset(p: {children: React.ReactNode; offset: number}) {
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${p.offset}px)`,
}}
>
{p.children}
</div>
);
}
2 changes: 2 additions & 0 deletions static/app/utils/analytics/issueAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type IssueEventParameters = {
'breadcrumbs.drawer.action': {control: string; value?: string};
'breadcrumbs.issue_details.change_time_display': {value: string};
'breadcrumbs.issue_details.drawer_opened': {control: string};
'breadcrumbs.trace_view.action': {control: string; value?: string};
'device.classification.high.end.android.device': {
processor_count: number;
processor_frequency: number;
Expand Down Expand Up @@ -300,6 +301,7 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
'breadcrumbs.issue_details.change_time_display': 'Breadcrumb Time Display Toggled',
'breadcrumbs.issue_details.drawer_opened': 'Breadcrumb Drawer Opened',
'breadcrumbs.drawer.action': 'Breadcrumb Drawer Action Taken',
'breadcrumbs.trace_view.action': 'Breadcrumb Trace View Action Taken',
'event_cause.viewed': null,
'event_cause.docs_clicked': 'Event Cause Docs Clicked',
'event_cause.snoozed': 'Event Cause Snoozed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {LazyRenderProps} from 'sentry/components/lazyRender';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {CustomMetricsEventData} from 'sentry/components/metrics/customMetricsEventData';
import {useHasNewTimelineUI} from 'sentry/components/timeline/utils';
import {Tooltip} from 'sentry/components/tooltip';
import {t} from 'sentry/locale';
import type {EventTransaction} from 'sentry/types/event';
Expand All @@ -23,6 +24,7 @@ import type {SpanMetricsQueryFilters} from 'sentry/views/insights/types';
import {Referrer} from 'sentry/views/performance/newTraceDetails/referrers';
import {useTransaction} from 'sentry/views/performance/newTraceDetails/traceApi/useTransaction';
import {CacheMetrics} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/cacheMetrics';
import {TraceBreadcrumbs} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/traceBreadcrumbs';
import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
import type {
TraceTree,
Expand Down Expand Up @@ -97,6 +99,8 @@ export function TransactionNodeDetails({
}: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.Transaction>>) {
const location = useLocation();
const {projects} = useProjects();
const hasNewTimelineUI = useHasNewTimelineUI();

const issues = useMemo(() => {
return [...node.errors, ...node.performance_issues];
}, [node.errors, node.performance_issues]);
Expand Down Expand Up @@ -183,8 +187,11 @@ export function TransactionNodeDetails({
{project ? <EventEvidence event={event} project={project} /> : null}

{replayRecord ? null : <ReplayPreview event={event} organization={organization} />}

<BreadCrumbs event={event} organization={organization} />
{hasNewTimelineUI ? (
<TraceBreadcrumbs event={event} organization={organization} />
) : (
<BreadCrumbs event={event} organization={organization} />
)}

{event.projectSlug ? (
<EventAttachments event={event} projectSlug={event.projectSlug} />
Expand Down
Loading

0 comments on commit 33db12d

Please sign in to comment.