import {Fragment, useCallback, useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; import omit from 'lodash/omit'; import moment from 'moment-timezone'; import {Alert} from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import Count from 'sentry/components/count'; import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning'; import * as Layout from 'sentry/components/layouts/thirds'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; import Panel from 'sentry/components/panels/panel'; import PanelHeader from 'sentry/components/panels/panelHeader'; import PanelItem from 'sentry/components/panels/panelItem'; import PerformanceDuration from 'sentry/components/performanceDuration'; import {IconChevron} from 'sentry/icons/iconChevron'; import {IconClose} from 'sentry/icons/iconClose'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {MetricAggregation, MRI} from 'sentry/types/metrics'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import {getUtcDateString} from 'sentry/utils/dates'; import {getFormattedMQL} from 'sentry/utils/metrics'; import {decodeInteger, decodeList} 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 * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout'; import {usePageParams} from './hooks/usePageParams'; import type {TraceResult} from './hooks/useTraces'; import {useTraces} from './hooks/useTraces'; import type {SpanResult} from './hooks/useTraceSpans'; import {useTraceSpans} from './hooks/useTraceSpans'; import {type Field, FIELDS, SORTS} from './data'; import { Description, ProjectBadgeWrapper, ProjectsRenderer, SpanBreakdownSliceRenderer, SpanDescriptionRenderer, SpanIdRenderer, SpanTimeRenderer, TraceBreakdownContainer, TraceBreakdownRenderer, TraceIdRenderer, TraceIssuesRenderer, } from './fieldRenderers'; import {TracesChart} from './tracesChart'; import {TracesSearchBar} from './tracesSearchBar'; import { areQueriesEmpty, getSecondaryNameFromSpan, getStylingSliceName, normalizeTraces, } from './utils'; const DEFAULT_PER_PAGE = 50; const SPAN_PROPS_DOCS_URL = 'https://docs.sentry.io/concepts/search/searchable-properties/spans/'; const ONE_MINUTE = 60 * 1000; // in milliseconds export function Content() { const location = useLocation(); const limit = useMemo(() => { return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE); }, [location.query.perPage]); const {queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri} = usePageParams(location); const hasMetric = metricsOp && mri; const removeMetric = useCallback(() => { browserHistory.push({ ...location, query: omit(location.query, [ 'mri', 'metricsOp', 'metricsQuery', 'metricsMax', 'metricsMin', ]), }); }, [location]); const handleSearch = useCallback( (searchIndex: number, searchQuery: string) => { const newQueries = [...queries]; if (newQueries.length === 0) { // In the odd case someone wants to add search bars before any query has been made, we add both the default one shown and a new one. newQueries[0] = ''; } newQueries[searchIndex] = searchQuery; browserHistory.push({ ...location, query: { ...location.query, cursor: undefined, query: typeof searchQuery === 'string' ? newQueries : queries, }, }); }, [location, queries] ); const handleClearSearch = useCallback( (searchIndex: number) => { const newQueries = [...queries]; if (typeof newQueries[searchIndex] !== undefined) { delete newQueries[searchIndex]; browserHistory.push({ ...location, query: { ...location.query, cursor: undefined, query: newQueries, }, }); return true; } return false; }, [location, queries] ); const tracesQuery = useTraces({ limit, query: queries, mri: hasMetric ? mri : undefined, metricsMax: hasMetric ? metricsMax : undefined, metricsMin: hasMetric ? metricsMin : undefined, metricsOp: hasMetric ? metricsOp : undefined, metricsQuery: hasMetric ? metricsQuery : undefined, }); const isLoading = tracesQuery.isFetching; const isError = !isLoading && tracesQuery.isError; const isEmpty = !isLoading && !isError && (tracesQuery?.data?.data?.length ?? 0) === 0; const rawData = !isLoading && !isError ? tracesQuery?.data?.data : undefined; const data = normalizeTraces(rawData); return ( {hasMetric && ( } > {tct('The metric query [metricQuery] is filtering the results below.', { metricQuery: ( {getFormattedMQL({ mri: mri as MRI, aggregation: metricsOp as MetricAggregation, query: metricsQuery, })} ), })} )} {isError && typeof tracesQuery.error?.responseJSON?.detail === 'string' ? ( {tracesQuery.error?.responseJSON?.detail} ) : null} {t('Trace ID')} {t('Trace Root')} {areQueriesEmpty(queries) ? t('Total Spans') : t('Matching Spans')} {t('Timeline')} {t('Duration')} {t('Timestamp')} {t('Issues')} {isLoading && ( )} {isError && ( // TODO: need an error state )} {isEmpty && ( {t('No trace results found')} {tct('Try adjusting your filters or refer to [docSearchProps].', { docSearchProps: ( {t('docs for search properties')} ), })} )} {data?.map((trace, i) => ( ))} ); } function TraceRow({defaultExpanded, trace}: {defaultExpanded; trace: TraceResult}) { const {selection} = usePageFilters(); const {projects} = useProjects(); const [expanded, setExpanded] = useState(defaultExpanded); const [highlightedSliceName, _setHighlightedSliceName] = useState(''); const location = useLocation(); const organization = useOrganization(); const queries = useMemo(() => { return decodeList(location.query.query); }, [location.query.query]); 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 (