APIModuleView.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import {Fragment, useRef, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import moment from 'moment';
  6. import {getInterval} from 'sentry/components/charts/utils';
  7. import {CompactSelect} from 'sentry/components/compactSelect';
  8. import DatePageFilter from 'sentry/components/datePageFilter';
  9. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {ReactEchartsRef, Series} from 'sentry/types/echarts';
  13. import {useApiQuery} from 'sentry/utils/queryClient';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import usePageFilters from 'sentry/utils/usePageFilters';
  16. import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
  17. import ChartPanel from 'sentry/views/starfish/components/chartPanel';
  18. import {INTERNAL_API_REGEX} from 'sentry/views/starfish/modules/APIModule/constants';
  19. import {HostDetails} from 'sentry/views/starfish/modules/APIModule/hostDetails';
  20. import {queryToSeries} from 'sentry/views/starfish/modules/databaseModule/utils';
  21. import {PERIOD_REGEX} from 'sentry/views/starfish/utils/dates';
  22. import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
  23. import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
  24. import {EndpointDataRow} from 'sentry/views/starfish/views/endpointDetails';
  25. import EndpointTable from './endpointTable';
  26. import HostTable from './hostTable';
  27. import {
  28. getEndpointDomainsEventView,
  29. getEndpointDomainsQuery,
  30. getEndpointGraphEventView,
  31. getEndpointGraphQuery,
  32. useGetTransactionsForHosts,
  33. } from './queries';
  34. const HTTP_ACTION_OPTIONS = [
  35. {value: '', label: 'All'},
  36. ...['GET', 'POST', 'PUT', 'DELETE'].map(action => ({
  37. value: action,
  38. label: action,
  39. })),
  40. ];
  41. type Props = {
  42. location: Location;
  43. onSelect: (row: EndpointDataRow) => void;
  44. };
  45. export type DataRow = {
  46. count: number;
  47. description: string;
  48. domain: string;
  49. };
  50. export default function APIModuleView({location, onSelect}: Props) {
  51. const themes = useTheme();
  52. const pageFilter = usePageFilters();
  53. const [state, setState] = useState<{
  54. action: string;
  55. domain: string;
  56. transaction: string;
  57. }>({
  58. action: '',
  59. domain: '',
  60. transaction: '',
  61. });
  62. const endpointTableRef = useRef<HTMLInputElement>(null);
  63. const organization = useOrganization();
  64. const endpointsDomainEventView = getEndpointDomainsEventView({
  65. datetime: pageFilter.selection.datetime,
  66. });
  67. const endpointsDomainQuery = getEndpointDomainsQuery({
  68. datetime: pageFilter.selection.datetime,
  69. });
  70. const {selection} = pageFilter;
  71. const {projects, environments, datetime} = selection;
  72. useApiQuery<null>(
  73. [
  74. `/organizations/${organization.slug}/events-starfish/`,
  75. {
  76. query: {
  77. ...{
  78. environment: environments,
  79. project: projects.map(proj => String(proj)),
  80. },
  81. ...normalizeDateTimeParams(datetime),
  82. },
  83. },
  84. ],
  85. {
  86. staleTime: 10,
  87. }
  88. );
  89. const {isLoading: _isDomainsLoading, data: domains} = useSpansQuery({
  90. eventView: endpointsDomainEventView,
  91. queryString: endpointsDomainQuery,
  92. initialData: [],
  93. });
  94. const endpointsGraphEventView = getEndpointGraphEventView({
  95. datetime: pageFilter.selection.datetime,
  96. });
  97. const {isLoading: isGraphLoading, data: graphData} = useSpansQuery({
  98. eventView: endpointsGraphEventView,
  99. queryString: getEndpointGraphQuery({
  100. datetime: pageFilter.selection.datetime,
  101. }),
  102. initialData: [],
  103. });
  104. const quantiles = [
  105. 'p50(span.self_time)',
  106. 'p75(span.self_time)',
  107. 'p95(span.self_time)',
  108. 'p99(span.self_time)',
  109. ];
  110. const seriesByQuantile: {[quantile: string]: Series} = {};
  111. quantiles.forEach(quantile => {
  112. seriesByQuantile[quantile] = {
  113. seriesName: quantile,
  114. data: [],
  115. };
  116. });
  117. const countSeries: Series = {
  118. seriesName: 'count',
  119. data: [],
  120. };
  121. const failureRateSeries: Series = {
  122. seriesName: 'failure_rate',
  123. data: [],
  124. };
  125. graphData.forEach(datum => {
  126. quantiles.forEach(quantile => {
  127. seriesByQuantile[quantile].data.push({
  128. value: datum[quantile],
  129. name: datum.interval,
  130. });
  131. });
  132. countSeries.data.push({
  133. value: datum['count()'],
  134. name: datum.interval,
  135. });
  136. failureRateSeries.data.push({
  137. value: datum['failure_rate()'],
  138. name: datum.interval,
  139. });
  140. });
  141. const [_, num, unit] = pageFilter.selection.datetime.period?.match(PERIOD_REGEX) ?? [];
  142. const startTime =
  143. num && unit
  144. ? moment().subtract(num, unit as 'h' | 'd')
  145. : moment(pageFilter.selection.datetime.start);
  146. const endTime = moment(pageFilter.selection.datetime.end ?? undefined);
  147. const [zeroFilledQuantiles, zeroFilledCounts, zeroFilledFailureRate] = [
  148. seriesByQuantile,
  149. [countSeries],
  150. [failureRateSeries],
  151. ].map(seriesGroup =>
  152. Object.values(seriesGroup).map(series =>
  153. zeroFillSeries(series, moment.duration(12, 'hours'), startTime, endTime)
  154. )
  155. );
  156. const setAction = (action: string) => {
  157. setState({
  158. ...state,
  159. action,
  160. });
  161. };
  162. const setDomain = (domain: string) => {
  163. setState({
  164. ...state,
  165. domain,
  166. });
  167. };
  168. const domainOptions = [
  169. {value: '', label: 'All'},
  170. ...domains
  171. .filter(({domain}) => domain !== '')
  172. .map(({domain}) => ({
  173. value: domain,
  174. label: domain,
  175. })),
  176. ];
  177. const interval = getInterval(pageFilter.selection.datetime, 'low');
  178. const {isLoading: isTopTransactionDataLoading, data: topTransactionsData} =
  179. useGetTransactionsForHosts(
  180. domains
  181. .map(({domain}) => domain)
  182. .filter(domain => !domain.match(INTERNAL_API_REGEX)),
  183. interval
  184. );
  185. const tpmTransactionSeries = queryToSeries(topTransactionsData, 'group', 'epm()');
  186. const p75TransactionSeries = queryToSeries(
  187. topTransactionsData,
  188. 'group',
  189. 'p75(transaction.duration)'
  190. );
  191. const loading = isGraphLoading || isTopTransactionDataLoading;
  192. useSynchronizeCharts([!loading]);
  193. return (
  194. <Fragment>
  195. <FilterOptionsContainer>
  196. <CompactSelect
  197. triggerProps={{prefix: t('Service')}}
  198. value="project"
  199. options={[{value: 'project', label: 'Project'}]}
  200. onChange={() => void 0}
  201. />
  202. <DatePageFilter alignDropdown="left" />
  203. </FilterOptionsContainer>
  204. <ChartsContainer>
  205. <ChartsContainerItem>
  206. <ChartPanel title={t('Top Transactions Throughput')}>
  207. <APIModuleChart data={tpmTransactionSeries} loading={loading} />
  208. </ChartPanel>
  209. </ChartsContainerItem>
  210. <ChartsContainerItem>
  211. <ChartPanel title={t('Top Transactions p75')}>
  212. <APIModuleChart data={p75TransactionSeries} loading={loading} />
  213. </ChartPanel>
  214. </ChartsContainerItem>
  215. </ChartsContainer>
  216. <ChartsContainer>
  217. <ChartsContainerItem>
  218. <ChartPanel title={t('Throughput')}>
  219. <APIModuleChart data={zeroFilledCounts} loading={loading} />
  220. </ChartPanel>
  221. </ChartsContainerItem>
  222. <ChartsContainerItem>
  223. <ChartPanel title={t('Response Time')}>
  224. <APIModuleChart data={zeroFilledQuantiles} loading={loading} />
  225. </ChartPanel>
  226. </ChartsContainerItem>
  227. <ChartsContainerItem>
  228. <ChartPanel title={t('Error Rate')}>
  229. <APIModuleChart
  230. data={zeroFilledFailureRate}
  231. loading={loading}
  232. chartColors={[themes.charts.getColorPalette(2)[2]]}
  233. />
  234. </ChartPanel>
  235. </ChartsContainerItem>
  236. </ChartsContainer>
  237. <HostTable
  238. location={location}
  239. setDomainFilter={domain => {
  240. setDomain(domain);
  241. // TODO: Cheap way to scroll to the endpoints table without waiting for async request
  242. setTimeout(() => {
  243. endpointTableRef.current?.scrollIntoView({
  244. behavior: 'smooth',
  245. inline: 'start',
  246. });
  247. }, 200);
  248. }}
  249. />
  250. <FilterOptionsContainer>
  251. <CompactSelect
  252. triggerProps={{prefix: t('Operation')}}
  253. value={state.action}
  254. options={HTTP_ACTION_OPTIONS}
  255. onChange={({value}) => setAction(value)}
  256. />
  257. <CompactSelect
  258. triggerProps={{prefix: t('Domain')}}
  259. value={state.domain}
  260. options={domainOptions}
  261. onChange={({value}) => setDomain(value)}
  262. />
  263. </FilterOptionsContainer>
  264. <div ref={endpointTableRef}>
  265. {state.domain && <HostDetails host={state.domain} />}
  266. <EndpointTable
  267. location={location}
  268. onSelect={onSelect}
  269. filterOptions={{...state, datetime: pageFilter.selection.datetime}}
  270. />
  271. </div>
  272. </Fragment>
  273. );
  274. }
  275. function APIModuleChart({
  276. data,
  277. loading,
  278. chartColors,
  279. forwardedRef,
  280. chartGroup,
  281. }: {
  282. data: Series[];
  283. loading: boolean;
  284. chartColors?: string[];
  285. chartGroup?: string;
  286. forwardedRef?: React.RefObject<ReactEchartsRef>;
  287. }) {
  288. return (
  289. <Chart
  290. statsPeriod="24h"
  291. height={140}
  292. data={data}
  293. start=""
  294. end=""
  295. loading={loading}
  296. utc={false}
  297. grid={{
  298. left: '0',
  299. right: '0',
  300. top: '8px',
  301. bottom: '0',
  302. }}
  303. definedAxisTicks={4}
  304. stacked
  305. isLineChart
  306. chartColors={chartColors}
  307. disableXAxis
  308. forwardedRef={forwardedRef}
  309. chartGroup={chartGroup}
  310. />
  311. );
  312. }
  313. const ChartsContainer = styled('div')`
  314. display: flex;
  315. flex-direction: row;
  316. flex-wrap: wrap;
  317. gap: ${space(2)};
  318. `;
  319. const ChartsContainerItem = styled('div')`
  320. flex: 1;
  321. `;
  322. const FilterOptionsContainer = styled('div')`
  323. display: flex;
  324. flex-direction: row;
  325. gap: ${space(1)};
  326. margin-bottom: ${space(2)};
  327. `;