httpSamplesPanel.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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 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 {ResponseCodeBarChart} from 'sentry/views/performance/http/responseCodeBarChart';
  18. import {MetricReadout} from 'sentry/views/performance/metricReadout';
  19. import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
  20. import DetailPanel from 'sentry/views/starfish/components/detailPanel';
  21. import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
  22. import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
  23. import {
  24. ModuleName,
  25. SpanFunction,
  26. SpanMetricsField,
  27. type SpanMetricsQueryFilters,
  28. } from 'sentry/views/starfish/types';
  29. import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
  30. export function HTTPSamplesPanel() {
  31. const router = useRouter();
  32. const query = useLocationQuery({
  33. fields: {
  34. project: decodeScalar,
  35. domain: decodeScalar,
  36. transaction: decodeScalar,
  37. transactionMethod: decodeScalar,
  38. },
  39. });
  40. const organization = useOrganization();
  41. const {projects} = useProjects();
  42. const project = projects.find(p => query.project === 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. <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. query.transactionMethod &&
  132. !query.transaction.startsWith(query.transactionMethod)
  133. ? `${query.transactionMethod} ${query.transaction}`
  134. : query.transaction}
  135. </Link>
  136. </Title>
  137. </TitleContainer>
  138. </HeaderContainer>
  139. </ModuleLayout.Full>
  140. <ModuleLayout.Full>
  141. <MetricsRibbon>
  142. <MetricReadout
  143. align="left"
  144. title={getThroughputTitle('http')}
  145. value={domainTransactionMetrics?.[0]?.[`${SpanFunction.SPM}()`]}
  146. unit={RateUnit.PER_MINUTE}
  147. isLoading={areDomainTransactionMetricsFetching}
  148. />
  149. <MetricReadout
  150. align="left"
  151. title={DataTitles.avg}
  152. value={
  153. domainTransactionMetrics?.[0]?.[
  154. `avg(${SpanMetricsField.SPAN_SELF_TIME})`
  155. ]
  156. }
  157. unit={DurationUnit.MILLISECOND}
  158. isLoading={areDomainTransactionMetricsFetching}
  159. />
  160. <MetricReadout
  161. align="left"
  162. title={t('3XXs')}
  163. value={domainTransactionMetrics?.[0]?.[`http_response_rate(3)`]}
  164. unit="percentage"
  165. isLoading={areDomainTransactionMetricsFetching}
  166. />
  167. <MetricReadout
  168. align="left"
  169. title={t('4XXs')}
  170. value={domainTransactionMetrics?.[0]?.[`http_response_rate(4)`]}
  171. unit="percentage"
  172. isLoading={areDomainTransactionMetricsFetching}
  173. />
  174. <MetricReadout
  175. align="left"
  176. title={t('5XXs')}
  177. value={domainTransactionMetrics?.[0]?.[`http_response_rate(5)`]}
  178. unit="percentage"
  179. isLoading={areDomainTransactionMetricsFetching}
  180. />
  181. <MetricReadout
  182. align="left"
  183. title={DataTitles.timeSpent}
  184. value={domainTransactionMetrics?.[0]?.['sum(span.self_time)']}
  185. unit={DurationUnit.MILLISECOND}
  186. tooltip={getTimeSpentExplanation(
  187. domainTransactionMetrics?.[0]?.['time_spent_percentage()'],
  188. 'db'
  189. )}
  190. isLoading={areDomainTransactionMetricsFetching}
  191. />
  192. </MetricsRibbon>
  193. </ModuleLayout.Full>
  194. <ModuleLayout.Full>
  195. <ResponseCodeBarChart
  196. series={responseCodeBarChartSeries}
  197. isLoading={isResponseCodeDataLoading}
  198. error={responseCodeDataError}
  199. />
  200. </ModuleLayout.Full>
  201. </ModuleLayout.Layout>
  202. </DetailPanel>
  203. </PageAlertProvider>
  204. );
  205. }
  206. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  207. padding-right: ${space(1)};
  208. `;
  209. const HeaderContainer = styled('div')`
  210. display: grid;
  211. grid-template-rows: auto auto auto;
  212. @media (min-width: ${p => p.theme.breakpoints.small}) {
  213. grid-template-rows: auto;
  214. grid-template-columns: auto 1fr auto;
  215. }
  216. `;
  217. const TitleContainer = styled('div')`
  218. width: 100%;
  219. position: relative;
  220. height: 40px;
  221. `;
  222. const Title = styled('h4')`
  223. position: absolute;
  224. bottom: 0;
  225. margin-bottom: 0;
  226. `;
  227. const MetricsRibbon = styled('div')`
  228. display: flex;
  229. flex-wrap: wrap;
  230. gap: ${space(4)};
  231. `;