import {useCallback} from 'react'; import styled from '@emotion/styled'; import * as qs from 'query-string'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import {Button} from 'sentry/components/button'; import {CompactSelect, type SelectOption} from 'sentry/components/compactSelect'; import Link from 'sentry/components/links/link'; import {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder'; 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 {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 {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import {computeAxisMax} from 'sentry/views/insights/common/components/chart'; import DetailPanel from 'sentry/views/insights/common/components/detailPanel'; import {MetricReadout} from 'sentry/views/insights/common/components/metricReadout'; import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout'; import {ReadoutRibbon} from 'sentry/views/insights/common/components/ribbon'; import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries'; import {AverageValueMarkLine} from 'sentry/views/insights/common/utils/averageValueMarkLine'; import {useSampleScatterPlotSeries} from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/durationChart/useSampleScatterPlotSeries'; import {DurationChart} from 'sentry/views/insights/http/components/charts/durationChart'; import {useSpanSamples} from 'sentry/views/insights/http/queries/useSpanSamples'; import {useDebouncedState} from 'sentry/views/insights/http/utils/useDebouncedState'; import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters'; import {MessageSpanSamplesTable} from 'sentry/views/insights/queues/components/tables/messageSpanSamplesTable'; import {useQueuesMetricsQuery} from 'sentry/views/insights/queues/queries/useQueuesMetricsQuery'; import {Referrer} from 'sentry/views/insights/queues/referrers'; import { CONSUMER_QUERY_FILTER, MessageActorType, PRODUCER_QUERY_FILTER, RETRY_COUNT_OPTIONS, TRACE_STATUS_OPTIONS, } from 'sentry/views/insights/queues/settings'; import decodeRetryCount from 'sentry/views/insights/queues/utils/queryParameterDecoders/retryCount'; import decodeTraceStatus from 'sentry/views/insights/queues/utils/queryParameterDecoders/traceStatus'; import { ModuleName, SpanIndexedField, type SpanMetricsResponse, } from 'sentry/views/insights/types'; import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils'; import {Subtitle} from 'sentry/views/profiling/landing/styles'; export function MessageSpanSamplesPanel() { const navigate = useNavigate(); 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 project = projects.find(p => query.project === p.id); const organization = useOrganization(); const {view} = useDomainViewFilters(); 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, }); navigate({ 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, }); navigate({ 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) => { navigate({ pathname: location.pathname, query: { ...location.query, spanSearchQuery: newSpanSearchQuery, }, }); }; const handleClose = () => { navigate({ pathname: location.pathname, query: { ...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={`${getTransactionSummaryBaseUrl(organization.slug, view)}?${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} filters={timeseriesFilters.getFilterKeys().reduce((acc, key) => { acc[key] = timeseriesFilters.getFilterValues(key)[0]; return acc; }, {})} /> 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; } `; const TitleContainer = styled('div')` width: 100%; overflow: hidden; `; const Title = styled('h4')` overflow: inherit; text-overflow: ellipsis; white-space: nowrap; margin: 0; `; const MetricsRibbonContainer = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(4)}; `; const PanelControls = styled('div')` display: flex; gap: ${space(2)}; `;