httpSamplesPanel.tsx 10 KB

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