diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx index 40bb442e75efd6..10312eb51c9be8 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx @@ -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( BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY, BreadcrumbTimeDisplay.RELATIVE diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDrawerContent.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDrawerContent.tsx index 30294d3fc216fc..c154e8f1929f7d 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsDrawerContent.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsDrawerContent.tsx @@ -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()); + const [sort, setSort] = useLocalStorageState( + BREADCRUMB_SORT_LOCALSTORAGE_KEY, + BreadcrumbSort.NEWEST + ); + const [timeDisplay, setTimeDisplay] = useLocalStorageState( + 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', @@ -59,41 +114,21 @@ export function BreadcrumbsDrawerContent({ }: BreadcrumbsDrawerContentProps) { const organization = useOrganization(); const theme = useTheme(); - - const [search, setSearch] = useState(''); - const [filterSet, setFilterSet] = useState(new Set()); - const [sort, setSort] = useLocalStorageState( - BREADCRUMB_SORT_LOCALSTORAGE_KEY, - BreadcrumbSort.NEWEST - ); const {getFocusProps} = useFocusControl(initialFocusControl); - const [timeDisplay, setTimeDisplay] = useLocalStorageState( - 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 = ( @@ -192,9 +227,7 @@ export function BreadcrumbsDrawerContent({ }} value={timeDisplay} options={BREADCRUMB_TIME_DISPLAY_OPTIONS} - > - {null} - + /> ); @@ -206,22 +239,16 @@ export function BreadcrumbsDrawerContent({ {displayCrumbs.length === 0 ? ( - - {t('No breadcrumbs found.')} - - + { + setFilterSet(new Set()); + setSearch(''); + trackAnalytics('breadcrumbs.drawer.action', { + control: 'clear_filters', + organization, + }); + }} + /> ) : ( ); } +export function EmptyBreadcrumbMessage({onClear}: {onClear: () => void}) { + return ( + + {t('No breadcrumbs found.')} + + + ); +} const VisibleFocusButton = styled(Button)` box-shadow: ${p => (p.autoFocus ? p.theme.button.default.focusBorder : 'transparent')} 0 diff --git a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx index ea4a8b1eafe140..9f8513cc174723 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx @@ -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. */ @@ -36,6 +40,7 @@ export default function BreadcrumbsTimeline({ breadcrumbs, startTimeString, isCompact = false, + fixedHeight, showLastLine = false, }: BreadcrumbsTimelineProps) { const containerRef = useRef(null); @@ -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]; @@ -114,11 +120,21 @@ export default function BreadcrumbsTimeline({
- {items} +
+ + {items} + +
); } @@ -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 ( +
+ {p.children} +
+ ); +} diff --git a/static/app/utils/analytics/issueAnalyticsEvents.tsx b/static/app/utils/analytics/issueAnalyticsEvents.tsx index bbca17ab0aeee8..2e3e90fd134327 100644 --- a/static/app/utils/analytics/issueAnalyticsEvents.tsx +++ b/static/app/utils/analytics/issueAnalyticsEvents.tsx @@ -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; @@ -300,6 +301,7 @@ export const issueEventMap: Record = { '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', diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx index 4862ceadb2832d..0ae10a549b9a8a 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx @@ -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'; @@ -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, @@ -97,6 +99,8 @@ export function TransactionNodeDetails({ }: TraceTreeNodeDetailsProps>) { const location = useLocation(); const {projects} = useProjects(); + const hasNewTimelineUI = useHasNewTimelineUI(); + const issues = useMemo(() => { return [...node.errors, ...node.performance_issues]; }, [node.errors, node.performance_issues]); @@ -183,8 +187,11 @@ export function TransactionNodeDetails({ {project ? : null} {replayRecord ? null : } - - + {hasNewTimelineUI ? ( + + ) : ( + + )} {event.projectSlug ? ( diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/traceBreadcrumbs.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/traceBreadcrumbs.tsx new file mode 100644 index 00000000000000..88739a1d5bdc2b --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/traceBreadcrumbs.tsx @@ -0,0 +1,202 @@ +import {useMemo} from 'react'; +import {useTheme} from '@emotion/react'; + +import {Button} from 'sentry/components/button'; +import ButtonBar from 'sentry/components/buttonBar'; +import {CompactSelect} from 'sentry/components/compactSelect'; +import ErrorBoundary from 'sentry/components/errorBoundary'; +import { + BreadcrumbControlOptions, + EmptyBreadcrumbMessage, + useBreadcrumbControls, +} from 'sentry/components/events/breadcrumbs/breadcrumbsDrawerContent'; +import BreadcrumbsTimeline from 'sentry/components/events/breadcrumbs/breadcrumbsTimeline'; +import { + BREADCRUMB_TIME_DISPLAY_OPTIONS, + BreadcrumbTimeDisplay, + getEnhancedBreadcrumbs, +} from 'sentry/components/events/breadcrumbs/utils'; +import {EventDataSection} from 'sentry/components/events/eventDataSection'; +import {BREADCRUMB_SORT_OPTIONS} from 'sentry/components/events/interfaces/breadcrumbs'; +import {InputGroup} from 'sentry/components/inputGroup'; +import {LazyRender} from 'sentry/components/lazyRender'; +import ExternalLink from 'sentry/components/links/externalLink'; +import {IconClock, IconFilter, IconSearch, IconSort, IconTimer} from 'sentry/icons'; +import {t, tct} from 'sentry/locale'; +import type {EventTransaction, Organization} from 'sentry/types'; +import {trackAnalytics} from 'sentry/utils/analytics'; + +import {TraceDrawerComponents} from '../../styles'; + +export function TraceBreadcrumbs({ + event, + organization, +}: { + event: EventTransaction; + organization: Organization; +}) { + const theme = useTheme(); + const enhancedCrumbs = useMemo(() => getEnhancedBreadcrumbs(event), [event]); + + const { + search, + setSearch, + filterSet, + setFilterSet, + filterOptions, + sort, + setSort, + timeDisplay, + setTimeDisplay, + displayCrumbs, + startTimeString, + } = useBreadcrumbControls({enhancedCrumbs}); + + if (enhancedCrumbs.length === 0) { + return null; + } + + const actions = ( + + + { + setSearch(e.target.value); + trackAnalytics('breadcrumbs.drawer.action', { + control: BreadcrumbControlOptions.SEARCH, + organization, + }); + }} + aria-label={t('Search All Breadcrumbs')} + /> + + + + + { + const newFilters = options.map(({value}) => value); + setFilterSet(new Set(newFilters)); + trackAnalytics('breadcrumbs.trace_view.action', { + control: BreadcrumbControlOptions.FILTER, + organization, + }); + }} + multiple + options={filterOptions} + maxMenuHeight={400} + trigger={props => ( + + )} + /> + ( +