import {Fragment, useCallback, useEffect, 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'; 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 {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; 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 {Tooltip} from 'sentry/components/tooltip'; import {IconArrow} from 'sentry/icons/iconArrow'; 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 {PageFilters} from 'sentry/types/core'; import type {MRI} from 'sentry/types/metrics'; import type {Organization} from 'sentry/types/organization'; 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 {useApiQuery} from 'sentry/utils/queryClient'; import {decodeInteger, decodeList, 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 * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout'; import {type Field, FIELDS, SORTS} from './data'; import { BREAKDOWN_SLICES, ProjectRenderer, SpanBreakdownSliceRenderer, SpanDescriptionRenderer, SpanIdRenderer, SpanTimeRenderer, TraceBreakdownContainer, TraceBreakdownRenderer, TraceIdRenderer, TraceIssuesRenderer, } from './fieldRenderers'; import {TracesChart} from './tracesChart'; import {TracesSearchBar} from './tracesSearchBar'; import { ALL_PROJECTS, 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 function usePageParams(location) { const queries = useMemo(() => { return decodeList(location.query.query); }, [location.query.query]); const metricsMax = decodeScalar(location.query.metricsMax); const metricsMin = decodeScalar(location.query.metricsMin); const metricsOp = decodeScalar(location.query.metricsOp); const metricsQuery = decodeScalar(location.query.metricsQuery); const mri = decodeScalar(location.query.mri); return { queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri, }; } export function Content() { const location = useLocation(); const organization = useOrganization(); 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 sortByTimestamp = organization.features.includes( 'performance-trace-explorer-sorting' ); const tracesQuery = useTraces({ limit, query: queries, sort: sortByTimestamp ? '-timestamp' : undefined, 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 = sortByTimestamp ? rawData : normalizeTraces(rawData); return ( , code: , } )} position="bottom" > {hasMetric && ( } > {tct('The metric query [metricQuery] is filtering the results below.', { metricQuery: ( {getFormattedMQL({mri: mri as MRI, op: metricsOp, 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')} {sortByTimestamp ? : null} {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 [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]); return (