import {Fragment, useCallback} from 'react'; import styled from '@emotion/styled'; import * as qs from 'query-string'; import Feature from 'sentry/components/acl/feature'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import {Button} from 'sentry/components/button'; import {CompactSelect, type SelectOption} from 'sentry/components/compactSelect'; import SearchBar from 'sentry/components/events/searchBar'; import Link from 'sentry/components/links/link'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {DurationUnit, SizeUnit} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; 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 useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import {AverageValueMarkLine} from 'sentry/views/performance/charts/averageValueMarkLine'; import {DurationChart} from 'sentry/views/performance/http/charts/durationChart'; import {useSpanSamples} from 'sentry/views/performance/http/data/useSpanSamples'; import {useDebouncedState} from 'sentry/views/performance/http/useDebouncedState'; import {MetricReadout} from 'sentry/views/performance/metricReadout'; import * as ModuleLayout from 'sentry/views/performance/moduleLayout'; import {MessageSpanSamplesTable} from 'sentry/views/performance/queues/destinationSummary/messageSpanSamplesTable'; import {useQueuesMetricsQuery} from 'sentry/views/performance/queues/queries/useQueuesMetricsQuery'; import decodeRetryCount from 'sentry/views/performance/queues/queryParameterDecoders/retryCount'; import decodeTraceStatus from 'sentry/views/performance/queues/queryParameterDecoders/traceStatus'; import {Referrer} from 'sentry/views/performance/queues/referrers'; import { CONSUMER_QUERY_FILTER, MessageActorType, PRODUCER_QUERY_FILTER, RETRY_COUNT_OPTIONS, TRACE_STATUS_OPTIONS, } from 'sentry/views/performance/queues/settings'; import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags'; import {Subtitle} from 'sentry/views/profiling/landing/styles'; import {computeAxisMax} from 'sentry/views/starfish/components/chart'; import DetailPanel from 'sentry/views/starfish/components/detailPanel'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useDiscoverSeries'; import { ModuleName, SpanIndexedField, type SpanMetricsResponse, } from 'sentry/views/starfish/types'; import {useSampleScatterPlotSeries} from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart/useSampleScatterPlotSeries'; export function MessageSpanSamplesPanel() { const router = useRouter(); const location = useLocation(); const query = useLocationQuery({ fields: { project: decodeScalar, destination: decodeScalar, transaction: decodeScalar, retryCount: decodeRetryCount, traceStatus: decodeTraceStatus, spanSearchQuery: decodeScalar, 'span.op': decodeScalar, }, }); const {projects} = useProjects(); const {selection} = usePageFilters(); const supportedTags = useSpanFieldSupportedTags(); const project = projects.find(p => query.project === p.id); const organization = useOrganization(); const [highlightedSpanId, setHighlightedSpanId] = useDebouncedState( undefined, [], SAMPLE_HOVER_DEBOUNCE ); // `detailKey` controls whether the panel is open. If all required properties are available, concat them to make a key, otherwise set to `undefined` and hide the panel const detailKey = query.transaction ? [query.destination, query.transaction].filter(Boolean).join(':') : undefined; const handleTraceStatusChange = (newTraceStatus: SelectOption) => { trackAnalytics('performance_views.sample_spans.filter_updated', { filter: 'trace_status', new_state: newTraceStatus.value, organization, source: ModuleName.QUEUE, }); router.replace({ pathname: location.pathname, query: { ...location.query, traceStatus: newTraceStatus.value, }, }); }; const handleRetryCountChange = (newRetryCount: SelectOption) => { trackAnalytics('performance_views.sample_spans.filter_updated', { filter: 'retry_count', new_state: newRetryCount.value, organization, source: ModuleName.QUEUE, }); router.replace({ pathname: location.pathname, query: { ...location.query, retryCount: newRetryCount.value, }, }); }; const isPanelOpen = Boolean(detailKey); const messageActorType = query['span.op'] === 'queue.publish' ? MessageActorType.PRODUCER : MessageActorType.CONSUMER; const queryFilter = messageActorType === MessageActorType.PRODUCER ? PRODUCER_QUERY_FILTER : CONSUMER_QUERY_FILTER; const timeseriesFilters = new MutableSearch(queryFilter); timeseriesFilters.addFilterValue('transaction', query.transaction); timeseriesFilters.addFilterValue('messaging.destination.name', query.destination); const sampleFilters = new MutableSearch(queryFilter); sampleFilters.addFilterValue('transaction', query.transaction); sampleFilters.addFilterValue('messaging.destination.name', query.destination); // filter by key-value filters specified in the search bar query sampleFilters.addStringMultiFilter(query.spanSearchQuery); if (query.traceStatus.length > 0) { sampleFilters.addFilterValue('trace.status', query.traceStatus); } // Note: only consumer panels should allow filtering by retry count if (messageActorType === MessageActorType.CONSUMER) { if (query.retryCount === '0') { sampleFilters.addFilterValue('measurements.messaging.message.retry.count', '0'); } else if (query.retryCount === '1-3') { sampleFilters.addFilterValues('measurements.messaging.message.retry.count', [ '>=1', '<=3', ]); } else if (query.retryCount === '4+') { sampleFilters.addFilterValue('measurements.messaging.message.retry.count', '>=4'); } } const {data: transactionMetrics, isFetching: aretransactionMetricsFetching} = useQueuesMetricsQuery({ destination: query.destination, transaction: query.transaction, enabled: isPanelOpen, referrer: Referrer.QUEUES_SAMPLES_PANEL, }); const avg = transactionMetrics?.[0]?.['avg(span.duration)']; const { isFetching: isDurationDataFetching, data: durationData, error: durationError, } = useSpanMetricsSeries( { search: timeseriesFilters, yAxis: [`avg(span.duration)`], enabled: isPanelOpen, }, 'api.performance.queues.avg-duration-chart' ); const durationAxisMax = computeAxisMax([durationData?.[`avg(span.duration)`]]); const { data: durationSamplesData, isFetching: isDurationSamplesDataFetching, error: durationSamplesDataError, refetch: refetchDurationSpanSamples, } = useSpanSamples({ search: sampleFilters, min: 0, max: durationAxisMax, enabled: isPanelOpen && durationAxisMax > 0, fields: [ SpanIndexedField.TRACE, SpanIndexedField.TRANSACTION_ID, SpanIndexedField.SPAN_DESCRIPTION, SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE, SpanIndexedField.MESSAGING_MESSAGE_RECEIVE_LATENCY, SpanIndexedField.MESSAGING_MESSAGE_RETRY_COUNT, SpanIndexedField.MESSAGING_MESSAGE_ID, SpanIndexedField.TRACE_STATUS, SpanIndexedField.SPAN_DURATION, ], }); const sampledSpanDataSeries = useSampleScatterPlotSeries( durationSamplesData, transactionMetrics?.[0]?.['avg(span.duration)'], highlightedSpanId, 'span.duration' ); const findSampleFromDataPoint = (dataPoint: {name: string | number; value: number}) => { return durationSamplesData.find( s => s.timestamp === dataPoint.name && s['span.duration'] === dataPoint.value ); }; const handleSearch = (newSpanSearchQuery: string) => { router.replace({ pathname: location.pathname, query: { ...query, spanSearchQuery: newSpanSearchQuery, }, }); }; const handleClose = () => { router.replace({ pathname: router.location.pathname, query: { ...router.location.query, transaction: undefined, transactionMethod: undefined, }, }); }; const handleOpen = useCallback(() => { if (query.transaction) { trackAnalytics('performance_views.sample_spans.opened', { organization, source: ModuleName.QUEUE, }); } }, [organization, query.transaction]); return ( {project ? ( ) : (
)} {messageActorType === MessageActorType.PRODUCER ? t('Producer') : t('Consumer')} <Link to={normalizeUrl( `/organizations/${organization.slug}/performance/summary?${qs.stringify( { project: query.project, transaction: query.transaction, } )}` )} > {query.transaction} </Link> {messageActorType === MessageActorType.PRODUCER ? ( ) : ( )} {messageActorType === MessageActorType.CONSUMER && ( )} { const firstHighlight = highlights[0]; if (!firstHighlight) { setHighlightedSpanId(undefined); return; } const sample = findSampleFromDataPoint(firstHighlight.dataPoint); setHighlightedSpanId(sample?.span_id); }} isLoading={isDurationDataFetching} error={durationError} /> setHighlightedSpanId(sample.span_id)} onSampleMouseOut={() => setHighlightedSpanId(undefined)} error={durationSamplesDataError} // Samples endpoint doesn't provide meta data, so we need to provide it here meta={{ fields: { [SpanIndexedField.SPAN_DURATION]: 'duration', [SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE]: 'size', [SpanIndexedField.MESSAGING_MESSAGE_RETRY_COUNT]: 'number', }, units: { [SpanIndexedField.SPAN_DURATION]: DurationUnit.MILLISECOND, [SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE]: SizeUnit.BYTE, }, }} type={messageActorType} /> ); } function ProducerMetricsRibbon({ metrics, isLoading, }: { isLoading: boolean; metrics: Partial[]; }) { const errorRate = 1 - (metrics[0]?.['trace_status_rate(ok)'] ?? 0); return ( ); } function ConsumerMetricsRibbon({ metrics, isLoading, }: { isLoading: boolean; metrics: Partial[]; }) { const errorRate = 1 - (metrics[0]?.['trace_status_rate(ok)'] ?? 0); return ( ); } const SAMPLE_HOVER_DEBOUNCE = 10; const TRACE_STATUS_SELECT_OPTIONS = [ { value: '', label: t('All'), }, ...TRACE_STATUS_OPTIONS.map(status => { return { value: status, label: status, }; }), ]; const RETRY_COUNT_SELECT_OPTIONS = [ { value: '', label: t('Any'), }, ...RETRY_COUNT_OPTIONS.map(status => { return { value: status, label: status, }; }), ]; const SpanSummaryProjectAvatar = styled(ProjectAvatar)` padding-right: ${space(1)}; `; const HeaderContainer = styled('div')` display: grid; grid-template-rows: auto auto auto; @media (min-width: ${p => p.theme.breakpoints.small}) { grid-template-rows: auto; grid-template-columns: auto 1fr auto; } `; const TitleContainer = styled('div')` width: 100%; position: relative; height: 40px; `; const Title = styled('h4')` position: absolute; bottom: 0; margin-bottom: 0; `; const MetricsRibbonContainer = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(4)}; `; const PanelControls = styled('div')` display: flex; gap: ${space(2)}; `;