messageConsumerSamplesPanel.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {Link} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import {Button} from 'sentry/components/button';
  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/useSpanMetricsSeries';
  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.task.celery OR span.op:queue.submit.celery';
  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. const {data: transactionMetrics, isFetching: aretransactionMetricsFetching} =
  57. useQueuesMetricsQuery({
  58. destination: query.destination,
  59. transaction: query.transaction,
  60. enabled: isPanelOpen,
  61. });
  62. const {
  63. isFetching: isDurationDataFetching,
  64. data: durationData,
  65. error: durationError,
  66. } = useSpanMetricsSeries({
  67. search,
  68. yAxis: [`avg(span.self_time)`],
  69. enabled: isPanelOpen,
  70. });
  71. const durationAxisMax = computeAxisMax([durationData?.[`avg(span.self_time)`]]);
  72. const {
  73. data: durationSamplesData,
  74. isFetching: isDurationSamplesDataFetching,
  75. error: durationSamplesDataError,
  76. refetch: refetchDurationSpanSamples,
  77. } = useSpanSamples({
  78. search,
  79. min: 0,
  80. max: durationAxisMax,
  81. enabled: isPanelOpen && durationAxisMax > 0,
  82. });
  83. const sampledSpanDataSeries = useSampleScatterPlotSeries(
  84. durationSamplesData,
  85. transactionMetrics?.[0]?.['avg(span.self_time)'],
  86. highlightedSpanId
  87. );
  88. const findSampleFromDataPoint = (dataPoint: {name: string | number; value: number}) => {
  89. return durationSamplesData.find(
  90. s => s.timestamp === dataPoint.name && s['span.self_time'] === dataPoint.value
  91. );
  92. };
  93. const handleClose = () => {
  94. router.replace({
  95. pathname: router.location.pathname,
  96. query: {
  97. ...router.location.query,
  98. transaction: undefined,
  99. transactionMethod: undefined,
  100. },
  101. });
  102. };
  103. return (
  104. <PageAlertProvider>
  105. <DetailPanel detailKey={detailKey} onClose={handleClose}>
  106. <ModuleLayout.Layout>
  107. <ModuleLayout.Full>
  108. <HeaderContainer>
  109. {project && (
  110. <SpanSummaryProjectAvatar
  111. project={project}
  112. direction="left"
  113. size={40}
  114. hasTooltip
  115. tooltip={project.slug}
  116. />
  117. )}
  118. <TitleContainer>
  119. <Title>
  120. <Link
  121. to={normalizeUrl(
  122. `/organizations/${organization.slug}/performance/summary?${qs.stringify(
  123. {
  124. project: query.project,
  125. transaction: query.transaction,
  126. }
  127. )}`
  128. )}
  129. >
  130. {query.transaction}
  131. </Link>
  132. </Title>
  133. </TitleContainer>
  134. </HeaderContainer>
  135. </ModuleLayout.Full>
  136. <ModuleLayout.Full>
  137. <MetricsRibbon>
  138. <MetricReadout
  139. align="left"
  140. title={t('Processed')}
  141. value={transactionMetrics?.[0]?.['count()']}
  142. unit={'count'}
  143. isLoading={aretransactionMetricsFetching}
  144. />
  145. <MetricReadout
  146. align="left"
  147. title={t('Error Rate')}
  148. value={undefined}
  149. unit={'percentage'}
  150. isLoading={aretransactionMetricsFetching}
  151. />
  152. <MetricReadout
  153. title={t('Avg Time In Queue')}
  154. value={undefined}
  155. unit={DurationUnit.MILLISECOND}
  156. isLoading={false}
  157. />
  158. <MetricReadout
  159. title={t('Avg Processing Latency')}
  160. value={
  161. transactionMetrics[0]?.[
  162. 'avg_if(span.self_time,span.op,queue.task.celery)'
  163. ]
  164. }
  165. unit={DurationUnit.MILLISECOND}
  166. isLoading={false}
  167. />
  168. </MetricsRibbon>
  169. </ModuleLayout.Full>
  170. <ModuleLayout.Full>
  171. <DurationChart
  172. series={[
  173. {
  174. ...durationData[`avg(span.self_time)`],
  175. markLine: AverageValueMarkLine(),
  176. },
  177. ]}
  178. scatterPlot={sampledSpanDataSeries}
  179. onHighlight={highlights => {
  180. const firstHighlight = highlights[0];
  181. if (!firstHighlight) {
  182. setHighlightedSpanId(undefined);
  183. return;
  184. }
  185. const sample = findSampleFromDataPoint(firstHighlight.dataPoint);
  186. setHighlightedSpanId(sample?.span_id);
  187. }}
  188. isLoading={isDurationDataFetching}
  189. error={durationError}
  190. />
  191. </ModuleLayout.Full>
  192. <ModuleLayout.Full>
  193. <MessageSpanSamplesTable
  194. data={durationSamplesData}
  195. isLoading={isDurationDataFetching || isDurationSamplesDataFetching}
  196. highlightedSpanId={highlightedSpanId}
  197. onSampleMouseOver={sample => setHighlightedSpanId(sample.span_id)}
  198. onSampleMouseOut={() => setHighlightedSpanId(undefined)}
  199. error={durationSamplesDataError}
  200. // Samples endpoint doesn't provide meta data, so we need to provide it here
  201. meta={{
  202. fields: {
  203. 'span.self_time': 'duration',
  204. },
  205. units: {},
  206. }}
  207. />
  208. </ModuleLayout.Full>
  209. <ModuleLayout.Full>
  210. <Button onClick={() => refetchDurationSpanSamples()}>
  211. {t('Try Different Samples')}
  212. </Button>
  213. </ModuleLayout.Full>
  214. </ModuleLayout.Layout>
  215. </DetailPanel>
  216. </PageAlertProvider>
  217. );
  218. }
  219. const SAMPLE_HOVER_DEBOUNCE = 10;
  220. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  221. padding-right: ${space(1)};
  222. `;
  223. const HeaderContainer = styled('div')`
  224. display: grid;
  225. grid-template-rows: auto auto auto;
  226. @media (min-width: ${p => p.theme.breakpoints.small}) {
  227. grid-template-rows: auto;
  228. grid-template-columns: auto 1fr auto;
  229. }
  230. `;
  231. const TitleContainer = styled('div')`
  232. width: 100%;
  233. position: relative;
  234. height: 40px;
  235. `;
  236. const Title = styled('h4')`
  237. position: absolute;
  238. bottom: 0;
  239. margin-bottom: 0;
  240. `;
  241. const MetricsRibbon = styled('div')`
  242. display: flex;
  243. flex-wrap: wrap;
  244. gap: ${space(4)};
  245. `;