import type {Dispatch, SetStateAction} from 'react'; import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; import {Button} from 'sentry/components/button'; import Count from 'sentry/components/count'; import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Pagination from 'sentry/components/pagination'; import PerformanceDuration from 'sentry/components/performanceDuration'; import {Tooltip} from 'sentry/components/tooltip'; import {DEFAULT_PER_PAGE, SPAN_PROPS_DOCS_URL} from 'sentry/constants'; import {IconChevron} from 'sentry/icons/iconChevron'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics'; import {useDataset} from 'sentry/views/explore/hooks/useDataset'; import {type TraceResult, useTraces} from 'sentry/views/explore/hooks/useTraces'; import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery'; import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes'; import { Description, ProjectBadgeWrapper, ProjectsRenderer, SpanTimeRenderer, TraceBreakdownRenderer, TraceIdRenderer, } from 'sentry/views/explore/tables/tracesTable/fieldRenderers'; import {SpanTable} from 'sentry/views/explore/tables/tracesTable/spansTable'; import { BreakdownPanelItem, EmptyStateText, EmptyValueContainer, StyledPanel, StyledPanelHeader, StyledPanelItem, TracePanelContent, WrappingText, } from 'sentry/views/explore/tables/tracesTable/styles'; interface TracesTableProps { setError: Dispatch>; } export function TracesTable({setError}: TracesTableProps) { const [dataset] = useDataset(); const [query] = useUserQuery(); const [visualizes] = useVisualizes(); const organization = useOrganization(); const location = useLocation(); const cursor = decodeScalar(location.query.cursor); const result = useTraces({ dataset, query, limit: DEFAULT_PER_PAGE, sort: '-timestamp', cursor, }); useEffect(() => { setError(result.error?.message ?? ''); }, [setError, result.error?.message]); useAnalytics({ resultLength: result.data?.data?.length, resultMode: 'trace samples', resultStatus: result.status, resultMissingRoot: result.data?.data?.filter(trace => !defined(trace.name))?.length, visualizes, organization, columns: [ 'trace id', 'trace root', 'total spans', 'timeline', 'root duration', 'timestamp', ], userQuery: query, }); const {data, isPending, isError, getResponseHeader} = result; const showErrorState = !isPending && isError; const showEmptyState = !isPending && !showErrorState && (data?.data?.length ?? 0) === 0; return ( {t('Trace ID')} {t('Trace Root')} {!query ? t('Total Spans') : t('Matching Spans')} {t('Timeline')} {t('Root Duration')} {t('Timestamp')} {isPending && ( )} {showErrorState && ( )} {showEmptyState && ( {t('No trace results found')} {tct('Try adjusting your filters or refer to [docSearchProps].', { docSearchProps: ( {t('docs for search properties')} ), })} )} {data?.data?.map((trace, i) => ( ))} ); } function TraceRow({ defaultExpanded, trace, query, }: { defaultExpanded; query: string; trace: TraceResult; }) { const {selection} = usePageFilters(); const {projects} = useProjects(); const [expanded, setExpanded] = useState(defaultExpanded); const [highlightedSliceName, _setHighlightedSliceName] = useState(''); const location = useLocation(); const organization = useOrganization(); const setHighlightedSliceName = useMemo( () => debounce(sliceName => _setHighlightedSliceName(sliceName), 100, { leading: true, }), [_setHighlightedSliceName] ); const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]); const selectedProjects = useMemo(() => { const selectedProjectIds = new Set( selection.projects.map(project => project.toString()) ); return new Set( projects .filter(project => selectedProjectIds.has(project.id)) .map(project => project.slug) ); }, [projects, selection.projects]); const traceProjects = useMemo(() => { const seenProjects: Set = new Set(); const leadingProjects: string[] = []; const trailingProjects: string[] = []; for (let i = 0; i < trace.breakdowns.length; i++) { const project = trace.breakdowns[i].project; if (!defined(project) || seenProjects.has(project)) { continue; } seenProjects.add(project); // Priotize projects that are selected in the page filters if (selectedProjects.has(project)) { leadingProjects.push(project); } else { trailingProjects.push(project); } } return [...leadingProjects, ...trailingProjects]; }, [selectedProjects, trace]); return ( } aria-label={t('Toggle trace details')} aria-expanded={expanded} size="zero" borderless onClick={() => trackAnalytics('trace_explorer.toggle_trace_details', { organization, expanded, source: 'new explore', }) } /> trackAnalytics('trace_explorer.open_trace', { organization, source: 'new explore', }) } location={location} /> 0 ? traceProjects : trace.project ? [trace.project] : [] } /> {trace.name ? ( {trace.name} ) : ( {t('Missing Trace Root')} )} {query ? ( tct('[numerator][space]of[space][denominator]', { numerator: , denominator: , space:  , }) ) : ( )} setHighlightedSliceName('')} > {defined(trace.rootDuration) ? ( ) : ( )} {expanded && ( )} ); } const StyledButton = styled(Button)` margin-right: ${space(0.5)}; `; const WarningStreamWrapper = styled(EmptyStreamWrapper)` > svg { fill: ${p => p.theme.gray300}; } `;