From 4b1e57561e06513fa508270cdd68047958d51756 Mon Sep 17 00:00:00 2001 From: Abdkhan14 <60121741+Abdkhan14@users.noreply.github.com> Date: Mon, 13 May 2024 19:45:47 -0400 Subject: [PATCH] feat(new-trace): Adding trace header information to drawer. (#70717) Before: Screenshot 2024-05-12 at 11 20 05 PM After: Screenshot 2024-05-12 at 11 19 50 PM --------- Co-authored-by: Abdullah Khan --- .../events/interfaces/spans/types.tsx | 1 + static/app/utils/queryClient.tsx | 2 +- .../performance/newTraceDetails/index.tsx | 16 +- .../newTraceDetails/trace.spec.tsx | 4 +- .../traceDrawer/details/styles.tsx | 4 +- .../traceDrawer/tabs/trace/generalInfo.tsx | 229 ++++++++++++++++++ .../tabs/{trace.tsx => trace/index.tsx} | 55 +++-- .../traceDrawer/tabs/trace/tagsSummary.tsx | 174 +++++++++++++ .../traceDrawer/traceDrawer.tsx | 16 +- 9 files changed, 450 insertions(+), 51 deletions(-) create mode 100644 static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/generalInfo.tsx rename static/app/views/performance/newTraceDetails/traceDrawer/tabs/{trace.tsx => trace/index.tsx} (58%) create mode 100644 static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/tagsSummary.tsx diff --git a/static/app/components/events/interfaces/spans/types.tsx b/static/app/components/events/interfaces/spans/types.tsx index ee1c8cd797a872..716e965f4dd8cb 100644 --- a/static/app/components/events/interfaces/spans/types.tsx +++ b/static/app/components/events/interfaces/spans/types.tsx @@ -214,6 +214,7 @@ export enum TickAlignment { } export type TraceContextType = { + client_sample_rate?: number; count?: number; description?: string; exclusive_time?: number; diff --git a/static/app/utils/queryClient.tsx b/static/app/utils/queryClient.tsx index 6c3469116a956a..a0ccdaa88fec45 100644 --- a/static/app/utils/queryClient.tsx +++ b/static/app/utils/queryClient.tsx @@ -268,7 +268,7 @@ export function fetchInfiniteQuery(api: Client) { function parsePageParam(dir: 'previous' | 'next') { return ([, , resp]: ApiResult) => { const parsed = parseLinkHeader(resp?.getResponseHeader('Link') ?? null); - return parsed[dir].results ? parsed[dir] : null; + return parsed[dir]?.results ? parsed[dir] : null; }; } diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index 37a1943174c444..a378da11fe6133 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -67,7 +67,6 @@ import {TraceSearchInput} from './traceSearch/traceSearchInput'; import {searchInTraceTree} from './traceState/traceSearch'; import {isTraceNode} from './guards'; import {Trace} from './trace'; -import {TraceHeader} from './traceHeader'; import {TraceMetadataHeader} from './traceMetadataHeader'; import {TraceReducer, type TraceReducerState} from './traceState'; import {TraceUXChangeAlert} from './traceUXChangeBanner'; @@ -802,14 +801,6 @@ function TraceViewContent(props: TraceViewContentProps) { traceSlug={props.traceSlug} traceEventView={props.traceEventView} /> - p.theme.background}; --info: ${p => p.theme.purple400}; @@ -920,8 +913,7 @@ const TraceGrid = styled('div')<{ box-shadow: 0 0 0 1px ${p => p.theme.border}; flex: 1 1 100%; display: grid; - border-top-left-radius: ${p => p.theme.borderRadius}; - border-top-right-radius: ${p => p.theme.borderRadius}; + border-radius: ${p => p.theme.borderRadius}; overflow: hidden; position: relative; /* false positive for grid layout */ diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 8b1e9f4d21394d..e5b88248147576 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -102,7 +102,7 @@ function mockTraceTagsResponse(resp?: Partial) { url: '/organizations/org-slug/events-facets/', method: 'GET', asyncDelay: 1, - ...(resp ?? {}), + ...(resp ?? []), }); } @@ -523,7 +523,7 @@ describe('trace view', () => { }, }); mockTraceMetaResponse(); - mockTraceTagsResponse({}); + mockTraceTagsResponse(); render(); expect( diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx index 193d1a9d670dba..93696e9f8781e6 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx @@ -596,7 +596,7 @@ function SectionCard({ title: React.ReactNode; disableTruncate?: boolean; }) { - const [showingAll, setShowingAll] = useState(disableTruncate ?? false); + const [showingAll, setShowingAll] = useState(false); const renderText = showingAll ? t('Show less') : t('Show more') + '...'; if (items.length === 0) { @@ -606,7 +606,7 @@ function SectionCard({ return ( {title} - {items.slice(0, showingAll ? items.length : 5).map(item => ( + {items.slice(0, showingAll || disableTruncate ? items.length : 5).map(item => ( ))} {items.length > 5 && !disableTruncate ? ( diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/generalInfo.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/generalInfo.tsx new file mode 100644 index 00000000000000..eb7ddd1f4b1976 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/generalInfo.tsx @@ -0,0 +1,229 @@ +import {Fragment, useMemo} from 'react'; + +import Link from 'sentry/components/links/link'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {Tooltip} from 'sentry/components/tooltip'; +import {t, tn} from 'sentry/locale'; +import type {Organization} from 'sentry/types'; +import type {EventTransaction} from 'sentry/types/event'; +import getDuration from 'sentry/utils/duration/getDuration'; +import {getShortEventId} from 'sentry/utils/events'; +import type { + TraceErrorOrIssue, + TraceFullDetailed, + TraceMeta, + TraceSplitResults, +} from 'sentry/utils/performance/quickTrace/types'; +import type {UseApiQueryResult} from 'sentry/utils/queryClient'; +import type RequestError from 'sentry/utils/requestError/requestError'; +import {useParams} from 'sentry/utils/useParams'; +import {normalizeUrl} from 'sentry/utils/withDomainRequired'; +import {SpanTimeRenderer} from 'sentry/views/performance/traces/fieldRenderers'; + +import {isTraceNode} from '../../../guards'; +import type {TraceTree, TraceTreeNode} from '../../../traceModels/traceTree'; +import {type SectionCardKeyValueList, TraceDrawerComponents} from '../../details/styles'; + +type GeneralInfoProps = { + metaResults: UseApiQueryResult; + node: TraceTreeNode | null; + organization: Organization; + rootEventResults: UseApiQueryResult; + traces: TraceSplitResults | null; + tree: TraceTree; +}; + +export function GeneralInfo(props: GeneralInfoProps) { + const params = useParams<{traceSlug?: string}>(); + + const traceNode = props.tree.root.children[0]; + + const uniqueErrorIssues = useMemo(() => { + if (!traceNode) { + return []; + } + + const unique: TraceErrorOrIssue[] = []; + + const seenIssues: Set = new Set(); + + for (const issue of traceNode.errors) { + if (seenIssues.has(issue.issue_id)) { + continue; + } + seenIssues.add(issue.issue_id); + unique.push(issue); + } + + return unique; + }, [traceNode]); + + const uniquePerformanceIssues = useMemo(() => { + if (!traceNode) { + return []; + } + + const unique: TraceErrorOrIssue[] = []; + const seenIssues: Set = new Set(); + + for (const issue of traceNode.performance_issues) { + if (seenIssues.has(issue.issue_id)) { + continue; + } + seenIssues.add(issue.issue_id); + unique.push(issue); + } + + return unique; + }, [traceNode]); + + const uniqueIssuesCount = uniqueErrorIssues.length + uniquePerformanceIssues.length; + + const traceSlug = useMemo(() => { + return params.traceSlug?.trim() ?? ''; + }, [params.traceSlug]); + + const isLoading = useMemo(() => { + return ( + props.metaResults.isLoading || + (props.rootEventResults.isLoading && props.rootEventResults.fetchStatus !== 'idle') + ); + }, [ + props.metaResults.isLoading, + props.rootEventResults.isLoading, + props.rootEventResults.fetchStatus, + ]); + + if (isLoading) { + return ( + , + }, + ]} + title={t('General')} + /> + ); + } + + if (!(traceNode && isTraceNode(traceNode))) { + throw new Error('Expected a trace node'); + } + + if ( + props.traces?.transactions.length === 0 && + props.traces.orphan_errors.length === 0 + ) { + return null; + } + + const replay_id = props.rootEventResults?.data?.contexts?.replay?.replay_id; + const browser = props.rootEventResults?.data?.contexts?.browser; + + const items: SectionCardKeyValueList = [ + { + key: 'trace_id', + subject: t('Trace ID'), + value: , + }, + { + key: 'events', + subject: t('Events'), + value: props.metaResults.data + ? props.metaResults.data.transactions + props.metaResults.data.errors + : '\u2014', + }, + { + key: 'issues', + subject: t('Issues'), + value: ( + 0 ? ( + +
+ {tn('%s error issue', '%s error issues', uniqueErrorIssues.length)} +
+
+ {tn( + '%s performance issue', + '%s performance issues', + uniquePerformanceIssues.length + )} +
+
+ ) : null + } + showUnderline + position="bottom" + > + {uniqueIssuesCount > 0 ? ( + + {uniqueIssuesCount} + + ) : uniqueIssuesCount === 0 ? ( + 0 + ) : ( + '\u2014' + )} +
+ ), + }, + { + key: 'start_timestamp', + subject: t('Start Timestamp'), + value: traceNode.space?.[1] ? ( + + ) : ( + '\u2014' + ), + }, + { + key: 'total_duration', + subject: t('Total Duration'), + value: traceNode.space?.[1] + ? getDuration(traceNode.space[1] / 1000, 2, true) + : '\u2014', + }, + { + key: 'user', + subject: t('User'), + value: + props.rootEventResults?.data?.user?.email ?? + props.rootEventResults?.data?.user?.name ?? + '\u2014', + }, + { + key: 'browser', + subject: t('Browser'), + value: browser ? browser.name + ' ' + browser.version : '\u2014', + }, + ]; + + if (replay_id) { + items.push({ + key: 'replay_id', + subject: t('Replay ID'), + value: ( + + {getShortEventId(replay_id)} + + ), + }); + } + + return ( + + ); +} diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/index.tsx similarity index 58% rename from static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace.tsx rename to static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/index.tsx index 3db0a10e6422a6..02f010364b3925 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/index.tsx @@ -1,30 +1,34 @@ import {Fragment, useMemo} from 'react'; import type {Tag} from 'sentry/actionCreators/events'; +import type {ApiResult} from 'sentry/api'; import type {EventTransaction} from 'sentry/types/event'; -import {generateQueryWithTag} from 'sentry/utils'; import type EventView from 'sentry/utils/discover/eventView'; -import {formatTagKey} from 'sentry/utils/discover/fields'; import type { TraceFullDetailed, + TraceMeta, TraceSplitResults, } from 'sentry/utils/performance/quickTrace/types'; -import type {UseApiQueryResult} from 'sentry/utils/queryClient'; +import type {UseApiQueryResult, UseInfiniteQueryResult} from 'sentry/utils/queryClient'; import type RequestError from 'sentry/utils/requestError/requestError'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; -import Tags from 'sentry/views/discover/tags'; import {TraceWarnings} from 'sentry/views/performance/newTraceDetails/traceWarnings'; import type {TraceType} from 'sentry/views/performance/traceDetails/newTraceDetailsContent'; -import {isTraceNode} from '../../guards'; -import type {TraceTree, TraceTreeNode} from '../../traceModels/traceTree'; -import {IssueList} from '../details/issues/issues'; +import {isTraceNode} from '../../../guards'; +import type {TraceTree, TraceTreeNode} from '../../../traceModels/traceTree'; +import {IssueList} from '../../details/issues/issues'; +import {TraceDrawerComponents} from '../../details/styles'; + +import {GeneralInfo} from './generalInfo'; +import {TagsSummary} from './tagsSummary'; type TraceDetailsProps = { + metaResults: UseApiQueryResult; node: TraceTreeNode | null; rootEventResults: UseApiQueryResult; - tagsQueryResults: UseApiQueryResult; + tagsInfiniteQueryResults: UseInfiniteQueryResult, unknown>; traceEventView: EventView; traceType: TraceType; traces: TraceSplitResults | null; @@ -56,26 +60,25 @@ export function TraceDetails(props: TraceDetailsProps) { {props.tree.type === 'trace' ? : null} - {rootEvent ? ( - { - const url = props.traceEventView.getResultsViewUrlTarget( - organization.slug, - false - ); - url.query = generateQueryWithTag(url.query, { - key: formatTagKey(key), - value, - }); - return url; - }} - totalValues={props.tree.eventsCount} - eventView={props.traceEventView} + + - ) : null} + {rootEvent ? ( + + ) : null} + ); } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/tagsSummary.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/tagsSummary.tsx new file mode 100644 index 00000000000000..6f43ee3d3c8b78 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/tagsSummary.tsx @@ -0,0 +1,174 @@ +import {Fragment, useMemo} from 'react'; +import styled from '@emotion/styled'; +import type {Location} from 'history'; +import isEmpty from 'lodash/isEmpty'; + +import type {Tag, TagSegment} from 'sentry/actionCreators/events'; +import type {ApiResult} from 'sentry/api'; +import {TagFacetsList} from 'sentry/components/group/tagFacets'; +import TagFacetsDistributionMeter from 'sentry/components/group/tagFacets/tagFacetsDistributionMeter'; +import Placeholder from 'sentry/components/placeholder'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {Organization} from 'sentry/types'; +import {generateQueryWithTag} from 'sentry/utils'; +import type EventView from 'sentry/utils/discover/eventView'; +import {formatTagKey} from 'sentry/utils/discover/fields'; +import type {UseInfiniteQueryResult} from 'sentry/utils/queryClient'; +import StyledEmptyStateWarning from 'sentry/views/replays/detail/emptyState'; + +import {TraceDrawerComponents} from '../../details/styles'; + +const getTagTarget = ( + tagKey: string, + tagValue: string, + eventView: EventView, + organization: Organization +) => { + const url = eventView.getResultsViewUrlTarget(organization.slug, false); + url.query = generateQueryWithTag(url.query, { + key: formatTagKey(tagKey), + value: tagValue, + }); + return url; +}; + +type TagSummaryProps = { + eventView: EventView; + location: Location; + organization: Organization; + tagsInfiniteQueryResults: UseInfiniteQueryResult, unknown>; + totalValues: number | null; +}; + +function TagsSummaryPlaceholder() { + return ( + + + + + + + + + ); +} + +const StyledPlaceholder = styled(Placeholder)` + border-radius: ${p => p.theme.borderRadius}; + height: 16px; + margin-bottom: ${space(1.5)}; +`; + +const StyledPlaceholderTitle = styled(Placeholder)` + width: 100px; + height: 12px; + margin-bottom: ${space(0.5)}; +`; + +type TagProps = { + eventView: EventView; + index: number; + organization: Organization; + tag: Tag; + totalValues: number | null; +}; + +function TagRow(props: TagProps) { + const segments: TagSegment[] = props.tag.topValues.map(segment => { + segment.url = getTagTarget( + props.tag.key, + segment.value, + props.eventView, + props.organization + ); + + return segment; + }); + + // Ensure we don't show >100% if there's a slight mismatch between the facets + // endpoint and the totals endpoint + const maxTotalValues = + segments.length > 0 + ? Math.max(Number(props.totalValues), segments[0].count) + : props.totalValues; + return ( +
  • + +
  • + ); +} + +export function TagsSummary(props: TagSummaryProps) { + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, // If anything is loaded yet + } = props.tagsInfiniteQueryResults; + + const tags: Tag[] = useMemo(() => { + if (!data) { + return []; + } + return data.pages.flatMap(([pageData]) => (isEmpty(pageData) ? [] : pageData)); + }, [data]); + + return ( + + {tags.length > 0 ? ( + + {tags.map((tag, index) => ( + + ))} + + ) : null} + {isLoading || isFetchingNextPage ? ( + + ) : tags.length === 0 ? ( + + {t('No tags found')} + + ) : null} + {hasNextPage ? ( + + fetchNextPage()}>{t('Show more')} + + ) : null} + + ), + }, + ]} + title={t('Tags')} + /> + ); +} + +const StyledTagFacetList = styled(TagFacetsList)` + margin-bottom: 0; + width: 100%; +`; + +const ShowMoreWrapper = styled('div')` + display: flex; + justify-content: center; +`; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx index 5cc9c753d4f5cd..d352328edcbed7 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx @@ -13,6 +13,7 @@ import type EventView from 'sentry/utils/discover/eventView'; import {PERFORMANCE_URL_PARAM} from 'sentry/utils/performance/constants'; import type { TraceFullDetailed, + TraceMeta, TraceSplitResults, } from 'sentry/utils/performance/quickTrace/types'; import { @@ -20,7 +21,7 @@ import { requestAnimationTimeout, } from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils'; import type {UseApiQueryResult} from 'sentry/utils/queryClient'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {useInfiniteApiQuery} from 'sentry/utils/queryClient'; import type RequestError from 'sentry/utils/requestError/requestError'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; @@ -55,6 +56,7 @@ import {TraceTreeNodeDetails} from './tabs/traceTreeNodeDetails'; type TraceDrawerProps = { manager: VirtualizedViewManager; + metaResults: UseApiQueryResult; onScrollToNode: (node: TraceTreeNode) => void; onTabScrollToNode: (node: TraceTreeNode) => void; rootEventResults: UseApiQueryResult; @@ -89,8 +91,8 @@ export function TraceDrawer(props: TraceDrawerProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const tagsQueryResults = useApiQuery( - [ + const tagsInfiniteQueryResults = useInfiniteApiQuery({ + queryKey: [ `/organizations/${organization.slug}/events-facets/`, { query: { @@ -100,10 +102,7 @@ export function TraceDrawer(props: TraceDrawerProps) { }, }, ], - { - staleTime: Infinity, - } - ); + }); const traceStateRef = useRef(props.trace_state); traceStateRef.current = props.trace_state; @@ -433,12 +432,13 @@ export function TraceDrawer(props: TraceDrawerProps) { {props.trace_state.tabs.current_tab ? ( props.trace_state.tabs.current_tab.node === 'trace' ? ( ) : props.trace_state.tabs.current_tab.node === 'vitals' ? (