httpSamplesPanel.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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 {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import type {Series} from 'sentry/types/echarts';
  8. import {DurationUnit, RateUnit} 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 {useLocation} from 'sentry/utils/useLocation';
  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 {ResponseCodeBarChart} from 'sentry/views/performance/http/responseCodeBarChart';
  18. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  19. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  20. import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
  21. import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
  22. import {
  23. ModuleName,
  24. SpanFunction,
  25. SpanMetricsField,
  26. type SpanMetricsQueryFilters,
  27. } from 'sentry/views/starfish/types';
  28. import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
  29. type Query = {
  30. domain?: string;
  31. project?: string;
  32. transaction?: string;
  33. transactionMethod?: string;
  34. };
  35. export function HTTPSamplesPanel() {
  36. const location = useLocation<Query>();
  37. const query = location.query;
  38. const router = useRouter();
  39. const organization = useOrganization();
  40. const projectId = decodeScalar(query.project);
  41. const {projects} = useProjects();
  42. const project = projects.find(p => projectId === p.id);
  43. // `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
  44. const detailKey =
  45. query.transaction && query.domain
  46. ? [query.domain, query.transactionMethod, query.transaction]
  47. .filter(Boolean)
  48. .join(':')
  49. : undefined;
  50. const isPanelOpen = Boolean(detailKey);
  51. const filters: SpanMetricsQueryFilters = {
  52. 'span.module': ModuleName.HTTP,
  53. 'span.domain': query.domain,
  54. transaction: query.transaction,
  55. };
  56. const {
  57. data: domainTransactionMetrics,
  58. isFetching: areDomainTransactionMetricsFetching,
  59. } = useSpanMetrics({
  60. search: MutableSearch.fromQueryObject(filters),
  61. fields: [
  62. `${SpanFunction.SPM}()`,
  63. `avg(${SpanMetricsField.SPAN_SELF_TIME})`,
  64. `sum(${SpanMetricsField.SPAN_SELF_TIME})`,
  65. 'http_response_rate(3)',
  66. 'http_response_rate(4)',
  67. 'http_response_rate(5)',
  68. `${SpanFunction.TIME_SPENT_PERCENTAGE}()`,
  69. ],
  70. enabled: isPanelOpen,
  71. referrer: 'api.starfish.http-module-samples-panel-metrics-ribbon',
  72. });
  73. const {
  74. data: responseCodeData,
  75. isFetching: isResponseCodeDataLoading,
  76. error: responseCodeDataError,
  77. } = useSpanMetrics({
  78. search: MutableSearch.fromQueryObject(filters),
  79. fields: ['span.status_code', 'count()'],
  80. sorts: [{field: 'span.status_code', kind: 'asc'}],
  81. enabled: isPanelOpen,
  82. referrer: 'api.starfish.http-module-samples-panel-response-bar-chart',
  83. });
  84. const responseCodeBarChartSeries: Series = {
  85. seriesName: 'span.status_code',
  86. data: (responseCodeData ?? []).map(item => {
  87. return {
  88. name: item['span.status_code'] || t('N/A'),
  89. value: item['count()'],
  90. };
  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. <HeaderContainer>
  107. {project && (
  108. <SpanSummaryProjectAvatar
  109. project={project}
  110. direction="left"
  111. size={40}
  112. hasTooltip
  113. tooltip={project.slug}
  114. />
  115. )}
  116. <TitleContainer>
  117. <Title>
  118. <Link
  119. to={normalizeUrl(
  120. `/organizations/${organization.slug}/performance/summary?${qs.stringify(
  121. {
  122. project: query.project,
  123. transaction: query.transaction,
  124. }
  125. )}`
  126. )}
  127. >
  128. {query.transaction &&
  129. query.transactionMethod &&
  130. !query.transaction.startsWith(query.transactionMethod)
  131. ? `${query.transactionMethod} ${query.transaction}`
  132. : query.transaction}
  133. </Link>
  134. </Title>
  135. </TitleContainer>
  136. </HeaderContainer>
  137. <MetricsRibbon>
  138. <MetricReadout
  139. align="left"
  140. title={getThroughputTitle('http')}
  141. value={domainTransactionMetrics?.[0]?.[`${SpanFunction.SPM}()`]}
  142. unit={RateUnit.PER_MINUTE}
  143. isLoading={areDomainTransactionMetricsFetching}
  144. />
  145. <MetricReadout
  146. align="left"
  147. title={DataTitles.avg}
  148. value={
  149. domainTransactionMetrics?.[0]?.[`avg(${SpanMetricsField.SPAN_SELF_TIME})`]
  150. }
  151. unit={DurationUnit.MILLISECOND}
  152. isLoading={areDomainTransactionMetricsFetching}
  153. />
  154. <MetricReadout
  155. align="left"
  156. title={t('3XXs')}
  157. value={domainTransactionMetrics?.[0]?.[`http_response_rate(3)`]}
  158. unit="percentage"
  159. isLoading={areDomainTransactionMetricsFetching}
  160. />
  161. <MetricReadout
  162. align="left"
  163. title={t('4XXs')}
  164. value={domainTransactionMetrics?.[0]?.[`http_response_rate(4)`]}
  165. unit="percentage"
  166. isLoading={areDomainTransactionMetricsFetching}
  167. />
  168. <MetricReadout
  169. align="left"
  170. title={t('5XXs')}
  171. value={domainTransactionMetrics?.[0]?.[`http_response_rate(5)`]}
  172. unit="percentage"
  173. isLoading={areDomainTransactionMetricsFetching}
  174. />
  175. <MetricReadout
  176. align="left"
  177. title={DataTitles.timeSpent}
  178. value={domainTransactionMetrics?.[0]?.['sum(span.self_time)']}
  179. unit={DurationUnit.MILLISECOND}
  180. tooltip={getTimeSpentExplanation(
  181. domainTransactionMetrics?.[0]?.['time_spent_percentage()'],
  182. 'db'
  183. )}
  184. isLoading={areDomainTransactionMetricsFetching}
  185. />
  186. </MetricsRibbon>
  187. <ResponseCodeBarChart
  188. series={responseCodeBarChartSeries}
  189. isLoading={isResponseCodeDataLoading}
  190. error={responseCodeDataError}
  191. />
  192. </DetailPanel>
  193. </PageAlertProvider>
  194. );
  195. }
  196. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  197. padding-right: ${space(1)};
  198. `;
  199. const HeaderContainer = styled('div')`
  200. width: 100%;
  201. padding-bottom: ${space(2)};
  202. padding-top: ${space(1)};
  203. display: grid;
  204. grid-template-rows: auto auto auto;
  205. @media (min-width: ${p => p.theme.breakpoints.small}) {
  206. grid-template-rows: auto;
  207. grid-template-columns: auto 1fr auto;
  208. }
  209. `;
  210. const TitleContainer = styled('div')`
  211. width: 100%;
  212. position: relative;
  213. height: 40px;
  214. `;
  215. const Title = styled('h4')`
  216. position: absolute;
  217. bottom: 0;
  218. margin-bottom: 0;
  219. `;
  220. const MetricsRibbon = styled('div')`
  221. display: flex;
  222. flex-wrap: wrap;
  223. gap: ${space(4)};
  224. `;