httpSamplesPanel.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import {Fragment} from 'react';
  2. import {Link} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as qs from 'query-string';
  5. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  6. import {SegmentedControl} from 'sentry/components/segmentedControl';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {Series} from 'sentry/types/echarts';
  10. import {DurationUnit, RateUnit} from 'sentry/utils/discover/fields';
  11. import {PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  14. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import useRouter from 'sentry/utils/useRouter';
  19. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  20. import {DurationChart} from 'sentry/views/performance/http/durationChart';
  21. import decodePanel from 'sentry/views/performance/http/queryParameterDecoders/panel';
  22. import {ResponseCodeBarChart} from 'sentry/views/performance/http/responseCodeBarChart';
  23. import {SpanSamplesTable} from 'sentry/views/performance/http/spanSamplesTable';
  24. import {useSpanSamples} from 'sentry/views/performance/http/useSpanSamples';
  25. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  26. import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
  27. import {computeAxisMax} from 'sentry/views/starfish/components/chart';
  28. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  29. import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
  30. import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
  31. import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
  32. import {
  33. ModuleName,
  34. SpanFunction,
  35. SpanIndexedField,
  36. SpanMetricsField,
  37. type SpanMetricsQueryFilters,
  38. } from 'sentry/views/starfish/types';
  39. import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
  40. export function HTTPSamplesPanel() {
  41. const router = useRouter();
  42. const location = useLocation();
  43. const query = useLocationQuery({
  44. fields: {
  45. project: decodeScalar,
  46. domain: decodeScalar,
  47. transaction: decodeScalar,
  48. transactionMethod: decodeScalar,
  49. panel: decodePanel,
  50. },
  51. });
  52. const organization = useOrganization();
  53. const {projects} = useProjects();
  54. const project = projects.find(p => query.project === p.id);
  55. // `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
  56. const detailKey =
  57. query.transaction && query.domain
  58. ? [query.domain, query.transactionMethod, query.transaction]
  59. .filter(Boolean)
  60. .join(':')
  61. : undefined;
  62. const handlePanelChange = newPanelName => {
  63. router.replace({
  64. pathname: location.pathname,
  65. query: {
  66. ...location.query,
  67. panel: newPanelName,
  68. },
  69. });
  70. };
  71. const isPanelOpen = Boolean(detailKey);
  72. const filters: SpanMetricsQueryFilters = {
  73. 'span.module': ModuleName.HTTP,
  74. 'span.domain': query.domain,
  75. transaction: query.transaction,
  76. };
  77. const {
  78. data: domainTransactionMetrics,
  79. isFetching: areDomainTransactionMetricsFetching,
  80. } = useSpanMetrics({
  81. search: MutableSearch.fromQueryObject(filters),
  82. fields: [
  83. `${SpanFunction.SPM}()`,
  84. `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
  85. `sum(${SpanMetricsField.SPAN_SELF_TIME})`,
  86. 'http_response_rate(3)',
  87. 'http_response_rate(4)',
  88. 'http_response_rate(5)',
  89. `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
  90. ],
  91. enabled: isPanelOpen,
  92. referrer: 'api.starfish.http-module-samples-panel-metrics-ribbon',
  93. });
  94. const {
  95. isFetching: isDurationDataFetching,
  96. data: durationData,
  97. error: durationError,
  98. } = useSpanMetricsSeries({
  99. search: MutableSearch.fromQueryObject(filters),
  100. yAxis: [`avg(span.self_time)`],
  101. enabled: isPanelOpen && query.panel === 'duration',
  102. referrer: 'api.starfish.http-module-samples-panel-duration-chart',
  103. });
  104. const {
  105. data: responseCodeData,
  106. isFetching: isResponseCodeDataLoading,
  107. error: responseCodeDataError,
  108. } = useSpanMetrics({
  109. search: MutableSearch.fromQueryObject(filters),
  110. fields: ['span.status_code', 'count()'],
  111. sorts: [{field: 'span.status_code', kind: 'asc'}],
  112. enabled: isPanelOpen && query.panel === 'status',
  113. referrer: 'api.starfish.http-module-samples-panel-response-bar-chart',
  114. });
  115. const responseCodeBarChartSeries: Series = {
  116. seriesName: 'span.status_code',
  117. data: (responseCodeData ?? []).map(item => {
  118. return {
  119. name: item['span.status_code'] || t('N/A'),
  120. value: item['count()'],
  121. };
  122. }),
  123. };
  124. const durationAxisMax = computeAxisMax([durationData?.[`avg(span.self_time)`]]);
  125. const {
  126. data: samplesData,
  127. isFetching: isSamplesDataFetching,
  128. error: samplesDataError,
  129. } = useSpanSamples({
  130. search: MutableSearch.fromQueryObject(filters),
  131. fields: [
  132. SpanIndexedField.TRANSACTION_ID,
  133. SpanIndexedField.SPAN_DESCRIPTION,
  134. SpanIndexedField.RESPONSE_CODE,
  135. ],
  136. min: 0,
  137. max: durationAxisMax,
  138. enabled: query.panel === 'duration' && durationAxisMax > 0,
  139. referrer: 'api.starfish.http-module-samples-panel-samples',
  140. });
  141. const handleClose = () => {
  142. router.replace({
  143. pathname: router.location.pathname,
  144. query: {
  145. ...router.location.query,
  146. transaction: undefined,
  147. transactionMethod: undefined,
  148. },
  149. });
  150. };
  151. return (
  152. <PageAlertProvider>
  153. <DetailPanel detailKey={detailKey} onClose={handleClose}>
  154. <ModuleLayout.Layout>
  155. <ModuleLayout.Full>
  156. <HeaderContainer>
  157. {project && (
  158. <SpanSummaryProjectAvatar
  159. project={project}
  160. direction="left"
  161. size={40}
  162. hasTooltip
  163. tooltip={project.slug}
  164. />
  165. )}
  166. <TitleContainer>
  167. <Title>
  168. <Link
  169. to={normalizeUrl(
  170. `/organizations/${organization.slug}/performance/summary?${qs.stringify(
  171. {
  172. project: query.project,
  173. transaction: query.transaction,
  174. }
  175. )}`
  176. )}
  177. >
  178. {query.transaction &&
  179. query.transactionMethod &&
  180. !query.transaction.startsWith(query.transactionMethod)
  181. ? `${query.transactionMethod} ${query.transaction}`
  182. : query.transaction}
  183. </Link>
  184. </Title>
  185. </TitleContainer>
  186. </HeaderContainer>
  187. </ModuleLayout.Full>
  188. <ModuleLayout.Full>
  189. <MetricsRibbon>
  190. <MetricReadout
  191. align="left"
  192. title={getThroughputTitle('http')}
  193. value={domainTransactionMetrics?.[0]?.[`${SpanFunction.SPM}()`]}
  194. unit={RateUnit.PER_MINUTE}
  195. isLoading={areDomainTransactionMetricsFetching}
  196. />
  197. <MetricReadout
  198. align="left"
  199. title={DataTitles.avg}
  200. value={
  201. domainTransactionMetrics?.[0]?.[
  202. `avg(${SpanMetricsField.SPAN_SELF_TIME})`
  203. ]
  204. }
  205. unit={DurationUnit.MILLISECOND}
  206. isLoading={areDomainTransactionMetricsFetching}
  207. />
  208. <MetricReadout
  209. align="left"
  210. title={t('3XXs')}
  211. value={domainTransactionMetrics?.[0]?.[`http_response_rate(3)`]}
  212. unit="percentage"
  213. isLoading={areDomainTransactionMetricsFetching}
  214. />
  215. <MetricReadout
  216. align="left"
  217. title={t('4XXs')}
  218. value={domainTransactionMetrics?.[0]?.[`http_response_rate(4)`]}
  219. unit="percentage"
  220. isLoading={areDomainTransactionMetricsFetching}
  221. />
  222. <MetricReadout
  223. align="left"
  224. title={t('5XXs')}
  225. value={domainTransactionMetrics?.[0]?.[`http_response_rate(5)`]}
  226. unit="percentage"
  227. isLoading={areDomainTransactionMetricsFetching}
  228. />
  229. <MetricReadout
  230. align="left"
  231. title={DataTitles.timeSpent}
  232. value={domainTransactionMetrics?.[0]?.['sum(span.self_time)']}
  233. unit={DurationUnit.MILLISECOND}
  234. tooltip={getTimeSpentExplanation(
  235. domainTransactionMetrics?.[0]?.['time_spent_percentage()'],
  236. 'db'
  237. )}
  238. isLoading={areDomainTransactionMetricsFetching}
  239. />
  240. </MetricsRibbon>
  241. </ModuleLayout.Full>
  242. <ModuleLayout.Full>
  243. <SegmentedControl
  244. value={query.panel}
  245. onChange={handlePanelChange}
  246. aria-label={t('Choose breakdown type')}
  247. >
  248. <SegmentedControl.Item key="duration">
  249. {t('By Duration')}
  250. </SegmentedControl.Item>
  251. <SegmentedControl.Item key="status">
  252. {t('By Response Code')}
  253. </SegmentedControl.Item>
  254. </SegmentedControl>
  255. </ModuleLayout.Full>
  256. {query.panel === 'duration' && (
  257. <Fragment>
  258. <ModuleLayout.Full>
  259. <DurationChart
  260. series={durationData[`avg(span.self_time)`]}
  261. isLoading={isDurationDataFetching}
  262. error={durationError}
  263. />
  264. </ModuleLayout.Full>
  265. <ModuleLayout.Full>
  266. <SpanSamplesTable
  267. data={samplesData}
  268. isLoading={isDurationDataFetching || isSamplesDataFetching}
  269. error={samplesDataError}
  270. />
  271. </ModuleLayout.Full>
  272. </Fragment>
  273. )}
  274. {query.panel === 'status' && (
  275. <ModuleLayout.Full>
  276. <ResponseCodeBarChart
  277. series={responseCodeBarChartSeries}
  278. isLoading={isResponseCodeDataLoading}
  279. error={responseCodeDataError}
  280. />
  281. </ModuleLayout.Full>
  282. )}
  283. </ModuleLayout.Layout>
  284. </DetailPanel>
  285. </PageAlertProvider>
  286. );
  287. }
  288. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  289. padding-right: ${space(1)};
  290. `;
  291. const HeaderContainer = styled('div')`
  292. display: grid;
  293. grid-template-rows: auto auto auto;
  294. @media (min-width: ${p => p.theme.breakpoints.small}) {
  295. grid-template-rows: auto;
  296. grid-template-columns: auto 1fr auto;
  297. }
  298. `;
  299. const TitleContainer = styled('div')`
  300. width: 100%;
  301. position: relative;
  302. height: 40px;
  303. `;
  304. const Title = styled('h4')`
  305. position: absolute;
  306. bottom: 0;
  307. margin-bottom: 0;
  308. `;
  309. const MetricsRibbon = styled('div')`
  310. display: flex;
  311. flex-wrap: wrap;
  312. gap: ${space(4)};
  313. `;