messageConsumerSamplesPanel.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import styled from '@emotion/styled';
  2. import * as qs from 'query-string';
  3. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  4. import {Button} from 'sentry/components/button';
  5. import Link from 'sentry/components/links/link';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {DurationUnit} from 'sentry/utils/discover/fields';
  9. import {PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert';
  10. import {decodeScalar} from 'sentry/utils/queryString';
  11. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  12. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import useProjects from 'sentry/utils/useProjects';
  15. import useRouter from 'sentry/utils/useRouter';
  16. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  17. import {AverageValueMarkLine} from 'sentry/views/performance/charts/averageValueMarkLine';
  18. import {DurationChart} from 'sentry/views/performance/http/charts/durationChart';
  19. import {useSpanSamples} from 'sentry/views/performance/http/data/useSpanSamples';
  20. import {useDebouncedState} from 'sentry/views/performance/http/useDebouncedState';
  21. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  22. import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
  23. import {MessageSpanSamplesTable} from 'sentry/views/performance/queues/messageSpanSamplesTable';
  24. import {useQueuesMetricsQuery} from 'sentry/views/performance/queues/queries/useQueuesMetricsQuery';
  25. import {computeAxisMax} from 'sentry/views/starfish/components/chart';
  26. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  27. import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSeries';
  28. import {useSampleScatterPlotSeries} from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart/useSampleScatterPlotSeries';
  29. // We're defining our own query filter here, apart from settings.ts because the spans endpoint doesn't accept IN operations
  30. const DEFAULT_QUERY_FILTER = 'span.op:queue.process OR span.op:queue.publish';
  31. export function MessageConsumerSamplesPanel() {
  32. const router = useRouter();
  33. const query = useLocationQuery({
  34. fields: {
  35. project: decodeScalar,
  36. destination: decodeScalar,
  37. transaction: decodeScalar,
  38. },
  39. });
  40. const {projects} = useProjects();
  41. const project = projects.find(p => query.project === p.id);
  42. const organization = useOrganization();
  43. const [highlightedSpanId, setHighlightedSpanId] = useDebouncedState<string | undefined>(
  44. undefined,
  45. [],
  46. SAMPLE_HOVER_DEBOUNCE
  47. );
  48. // `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
  49. const detailKey = query.transaction
  50. ? [query.destination, query.transaction].filter(Boolean).join(':')
  51. : undefined;
  52. const isPanelOpen = Boolean(detailKey);
  53. // TODO: This should also filter on destination
  54. const search = new MutableSearch(DEFAULT_QUERY_FILTER);
  55. search.addFilterValue('transaction', query.transaction);
  56. search.addFilterValue('messaging.destination.name', query.destination);
  57. const {data: transactionMetrics, isFetching: aretransactionMetricsFetching} =
  58. useQueuesMetricsQuery({
  59. destination: query.destination,
  60. transaction: query.transaction,
  61. enabled: isPanelOpen,
  62. });
  63. const {
  64. isFetching: isDurationDataFetching,
  65. data: durationData,
  66. error: durationError,
  67. } = useSpanMetricsSeries({
  68. search,
  69. yAxis: [`avg(span.self_time)`],
  70. enabled: isPanelOpen,
  71. });
  72. const durationAxisMax = computeAxisMax([durationData?.[`avg(span.self_time)`]]);
  73. const {
  74. data: durationSamplesData,
  75. isFetching: isDurationSamplesDataFetching,
  76. error: durationSamplesDataError,
  77. refetch: refetchDurationSpanSamples,
  78. } = useSpanSamples({
  79. search,
  80. min: 0,
  81. max: durationAxisMax,
  82. enabled: isPanelOpen && durationAxisMax > 0,
  83. });
  84. const sampledSpanDataSeries = useSampleScatterPlotSeries(
  85. durationSamplesData,
  86. transactionMetrics?.[0]?.['avg(span.self_time)'],
  87. highlightedSpanId
  88. );
  89. const findSampleFromDataPoint = (dataPoint: {name: string | number; value: number}) => {
  90. return durationSamplesData.find(
  91. s => s.timestamp === dataPoint.name && s['span.self_time'] === dataPoint.value
  92. );
  93. };
  94. const handleClose = () => {
  95. router.replace({
  96. pathname: router.location.pathname,
  97. query: {
  98. ...router.location.query,
  99. transaction: undefined,
  100. transactionMethod: undefined,
  101. },
  102. });
  103. };
  104. return (
  105. <PageAlertProvider>
  106. <DetailPanel detailKey={detailKey} onClose={handleClose}>
  107. <ModuleLayout.Layout>
  108. <ModuleLayout.Full>
  109. <HeaderContainer>
  110. {project && (
  111. <SpanSummaryProjectAvatar
  112. project={project}
  113. direction="left"
  114. size={40}
  115. hasTooltip
  116. tooltip={project.slug}
  117. />
  118. )}
  119. <TitleContainer>
  120. <Title>
  121. <Link
  122. to={normalizeUrl(
  123. `/organizations/${organization.slug}/performance/summary?${qs.stringify(
  124. {
  125. project: query.project,
  126. transaction: query.transaction,
  127. }
  128. )}`
  129. )}
  130. >
  131. {query.transaction}
  132. </Link>
  133. </Title>
  134. </TitleContainer>
  135. </HeaderContainer>
  136. </ModuleLayout.Full>
  137. <ModuleLayout.Full>
  138. <MetricsRibbon>
  139. <MetricReadout
  140. align="left"
  141. title={t('Processed')}
  142. value={transactionMetrics?.[0]?.['count()']}
  143. unit={'count'}
  144. isLoading={aretransactionMetricsFetching}
  145. />
  146. <MetricReadout
  147. align="left"
  148. title={t('Error Rate')}
  149. value={undefined}
  150. unit={'percentage'}
  151. isLoading={aretransactionMetricsFetching}
  152. />
  153. <MetricReadout
  154. title={t('Avg Time In Queue')}
  155. value={transactionMetrics[0]?.['avg(messaging.message.receive.latency)']}
  156. unit={DurationUnit.MILLISECOND}
  157. isLoading={false}
  158. />
  159. <MetricReadout
  160. title={t('Avg Processing Latency')}
  161. value={
  162. transactionMetrics[0]?.['avg_if(span.self_time,span.op,queue.process)']
  163. }
  164. unit={DurationUnit.MILLISECOND}
  165. isLoading={false}
  166. />
  167. </MetricsRibbon>
  168. </ModuleLayout.Full>
  169. <ModuleLayout.Full>
  170. <DurationChart
  171. series={[
  172. {
  173. ...durationData[`avg(span.self_time)`],
  174. markLine: AverageValueMarkLine(),
  175. },
  176. ]}
  177. scatterPlot={sampledSpanDataSeries}
  178. onHighlight={highlights => {
  179. const firstHighlight = highlights[0];
  180. if (!firstHighlight) {
  181. setHighlightedSpanId(undefined);
  182. return;
  183. }
  184. const sample = findSampleFromDataPoint(firstHighlight.dataPoint);
  185. setHighlightedSpanId(sample?.span_id);
  186. }}
  187. isLoading={isDurationDataFetching}
  188. error={durationError}
  189. />
  190. </ModuleLayout.Full>
  191. <ModuleLayout.Full>
  192. <MessageSpanSamplesTable
  193. data={durationSamplesData}
  194. isLoading={isDurationDataFetching || isDurationSamplesDataFetching}
  195. highlightedSpanId={highlightedSpanId}
  196. onSampleMouseOver={sample => setHighlightedSpanId(sample.span_id)}
  197. onSampleMouseOut={() => setHighlightedSpanId(undefined)}
  198. error={durationSamplesDataError}
  199. // Samples endpoint doesn't provide meta data, so we need to provide it here
  200. meta={{
  201. fields: {
  202. 'span.self_time': 'duration',
  203. },
  204. units: {},
  205. }}
  206. />
  207. </ModuleLayout.Full>
  208. <ModuleLayout.Full>
  209. <Button onClick={() => refetchDurationSpanSamples()}>
  210. {t('Try Different Samples')}
  211. </Button>
  212. </ModuleLayout.Full>
  213. </ModuleLayout.Layout>
  214. </DetailPanel>
  215. </PageAlertProvider>
  216. );
  217. }
  218. const SAMPLE_HOVER_DEBOUNCE = 10;
  219. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  220. padding-right: ${space(1)};
  221. `;
  222. const HeaderContainer = styled('div')`
  223. display: grid;
  224. grid-template-rows: auto auto auto;
  225. @media (min-width: ${p => p.theme.breakpoints.small}) {
  226. grid-template-rows: auto;
  227. grid-template-columns: auto 1fr auto;
  228. }
  229. `;
  230. const TitleContainer = styled('div')`
  231. width: 100%;
  232. position: relative;
  233. height: 40px;
  234. `;
  235. const Title = styled('h4')`
  236. position: absolute;
  237. bottom: 0;
  238. margin-bottom: 0;
  239. `;
  240. const MetricsRibbon = styled('div')`
  241. display: flex;
  242. flex-wrap: wrap;
  243. gap: ${space(4)};
  244. `;