metricsTable.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import {ReactNode, useMemo} from 'react';
  2. import {Location} from 'history';
  3. import moment from 'moment';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnOrder,
  7. } from 'sentry/components/gridEditable';
  8. import SortLink from 'sentry/components/gridEditable/sortLink';
  9. import {t} from 'sentry/locale';
  10. import {Organization} from 'sentry/types';
  11. import {parsePeriodToHours} from 'sentry/utils/dates';
  12. import {TableData, useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  13. import EventView from 'sentry/utils/discover/eventView';
  14. import {
  15. AggregationKeyWithAlias,
  16. ColumnType,
  17. fieldAlignment,
  18. QueryFieldValue,
  19. } from 'sentry/utils/discover/fields';
  20. import {Container} from 'sentry/utils/discover/styles';
  21. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  22. import {formatPercentage} from 'sentry/utils/formatters';
  23. import theme from 'sentry/utils/theme';
  24. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  25. import {TransactionThresholdMetric} from 'sentry/views/performance/transactionSummary/transactionThresholdModal';
  26. import {ExplorerText} from 'sentry/views/performance/trends/changeExplorer';
  27. import {
  28. NormalizedTrendsTransaction,
  29. TrendFunctionField,
  30. TrendsTransaction,
  31. TrendView,
  32. } from 'sentry/views/performance/trends/types';
  33. type MetricsTableProps = {
  34. isLoading: boolean;
  35. location: Location;
  36. organization: Organization;
  37. transaction: NormalizedTrendsTransaction;
  38. trendFunction: string;
  39. trendView: TrendView;
  40. };
  41. const fieldsNeeded: AggregationKeyWithAlias[] = ['tps', 'p50', 'p95'];
  42. type MetricColumnKey = 'metric' | 'before' | 'after' | 'change';
  43. type MetricColumn = GridColumnOrder<MetricColumnKey>;
  44. type TableDataRow = Record<MetricColumnKey, any>;
  45. const MetricColumnOrder = ['metric', 'before', 'after', 'change'];
  46. export const COLUMNS: Record<MetricColumnKey, MetricColumn> = {
  47. metric: {
  48. key: 'metric',
  49. name: t('Metric'),
  50. width: COL_WIDTH_UNDEFINED,
  51. },
  52. before: {
  53. key: 'before',
  54. name: t('Before'),
  55. width: COL_WIDTH_UNDEFINED,
  56. },
  57. after: {
  58. key: 'after',
  59. name: t('After'),
  60. width: COL_WIDTH_UNDEFINED,
  61. },
  62. change: {
  63. key: 'change',
  64. name: t('Change'),
  65. width: COL_WIDTH_UNDEFINED,
  66. },
  67. };
  68. const COLUMN_TYPE: Record<MetricColumnKey, ColumnType> = {
  69. metric: 'string',
  70. before: 'duration',
  71. after: 'duration',
  72. change: 'percentage',
  73. };
  74. export function MetricsTable(props: MetricsTableProps) {
  75. const {trendFunction, transaction, trendView, organization, location, isLoading} =
  76. props;
  77. const p50 =
  78. trendFunction === TrendFunctionField.P50
  79. ? getTrendsRowData(transaction, TrendFunctionField.P50)
  80. : undefined;
  81. const p95 =
  82. trendFunction === TrendFunctionField.P95
  83. ? getTrendsRowData(transaction, TrendFunctionField.P95)
  84. : undefined;
  85. const breakpoint = transaction.breakpoint;
  86. const hours = trendView.statsPeriod ? parsePeriodToHours(trendView.statsPeriod) : 0;
  87. const startTime = useMemo(
  88. () =>
  89. trendView.start ? trendView.start : moment().subtract(hours, 'h').toISOString(),
  90. [hours, trendView.start]
  91. );
  92. const breakpointTime = breakpoint ? new Date(breakpoint * 1000).toISOString() : '';
  93. const endTime = useMemo(
  94. () => (trendView.end ? trendView.end : moment().toISOString()),
  95. [trendView.end]
  96. );
  97. const {data: beforeBreakpoint, isLoading: isLoadingBefore} = useDiscoverQuery(
  98. getQueryParams(
  99. startTime,
  100. breakpointTime,
  101. fieldsNeeded,
  102. 'transaction',
  103. DiscoverDatasets.METRICS,
  104. organization,
  105. trendView,
  106. transaction.transaction,
  107. location
  108. )
  109. );
  110. const {data: afterBreakpoint, isLoading: isLoadingAfter} = useDiscoverQuery(
  111. getQueryParams(
  112. breakpointTime,
  113. endTime,
  114. fieldsNeeded,
  115. 'transaction',
  116. DiscoverDatasets.METRICS,
  117. organization,
  118. trendView,
  119. transaction.transaction,
  120. location
  121. )
  122. );
  123. const {data: beforeBreakpointErrors, isLoading: isLoadingBeforeErrors} =
  124. useDiscoverQuery(
  125. getQueryParams(
  126. startTime,
  127. breakpointTime,
  128. ['count'],
  129. 'error',
  130. DiscoverDatasets.DISCOVER,
  131. organization,
  132. trendView,
  133. transaction.transaction,
  134. location
  135. )
  136. );
  137. const {data: afterBreakpointErrors, isLoading: isLoadingAfterErrors} = useDiscoverQuery(
  138. getQueryParams(
  139. breakpointTime,
  140. endTime,
  141. ['count'],
  142. 'error',
  143. DiscoverDatasets.DISCOVER,
  144. organization,
  145. trendView,
  146. transaction.transaction,
  147. location
  148. )
  149. );
  150. const throughput: TableDataRow = getEventsRowData(
  151. 'tps()',
  152. 'Throughput',
  153. 'ps',
  154. '-',
  155. false,
  156. beforeBreakpoint,
  157. afterBreakpoint
  158. );
  159. const p50Events = !p50
  160. ? getEventsRowData(
  161. 'p50()',
  162. 'P50',
  163. 'ms',
  164. '-',
  165. false,
  166. beforeBreakpoint,
  167. afterBreakpoint
  168. )
  169. : p50;
  170. const p95Events = !p95
  171. ? getEventsRowData(
  172. 'p95()',
  173. 'P95',
  174. 'ms',
  175. '-',
  176. false,
  177. beforeBreakpoint,
  178. afterBreakpoint
  179. )
  180. : p95;
  181. const errors: TableDataRow = getEventsRowData(
  182. 'count()',
  183. 'Errors',
  184. '',
  185. 0,
  186. true,
  187. beforeBreakpointErrors,
  188. afterBreakpointErrors
  189. );
  190. const columnOrder = MetricColumnOrder.map(column => COLUMNS[column]);
  191. return (
  192. <GridEditable
  193. data={[throughput, p50Events, p95Events, errors]}
  194. columnOrder={columnOrder}
  195. columnSortBy={[]}
  196. grid={{
  197. renderHeadCell,
  198. renderBodyCell,
  199. }}
  200. location={location}
  201. isLoading={
  202. isLoadingBefore ||
  203. isLoadingAfter ||
  204. isLoading ||
  205. isLoadingBeforeErrors ||
  206. isLoadingAfterErrors
  207. }
  208. />
  209. );
  210. }
  211. function getEventsRowData(
  212. field: string,
  213. rowTitle: string,
  214. suffix: string,
  215. nullValue: string | number,
  216. wholeNumbers: boolean,
  217. beforeData?: TableData,
  218. afterData?: TableData
  219. ): TableDataRow {
  220. if (beforeData?.data[0][field] && afterData?.data[0][field]) {
  221. return {
  222. metric: rowTitle,
  223. before: !wholeNumbers
  224. ? toFormattedNumber(beforeData.data[0][field].toString(), 1) + ' ' + suffix
  225. : beforeData.data[0][field],
  226. after: !wholeNumbers
  227. ? toFormattedNumber(afterData.data[0][field].toString(), 1) + ' ' + suffix
  228. : afterData.data[0][field],
  229. change: formatPercentage(
  230. relativeChange(
  231. beforeData.data[0][field] as number,
  232. afterData.data[0][field] as number
  233. ),
  234. 1
  235. ),
  236. };
  237. }
  238. return {
  239. metric: rowTitle,
  240. before: nullValue,
  241. after: nullValue,
  242. change: '-',
  243. };
  244. }
  245. function getTrendsRowData(
  246. aggregateData: TrendsTransaction | undefined,
  247. metric: TrendFunctionField
  248. ): TableDataRow | undefined {
  249. if (aggregateData) {
  250. return {
  251. metric: metric.toString().toUpperCase(),
  252. before: aggregateData?.aggregate_range_1.toFixed(1) + ' ms',
  253. after: aggregateData?.aggregate_range_2.toFixed(1) + ' ms',
  254. change:
  255. aggregateData?.trend_percentage !== 1
  256. ? formatPercentage(aggregateData?.trend_percentage! - 1, 1)
  257. : '-',
  258. };
  259. }
  260. return undefined;
  261. }
  262. function getEventViewWithFields(
  263. _organization: Organization,
  264. eventView: EventView,
  265. start: string,
  266. end: string,
  267. fields: AggregationKeyWithAlias[],
  268. eventType: string,
  269. transactionName: string,
  270. dataset: DiscoverDatasets
  271. ): EventView {
  272. const newEventView = eventView.clone();
  273. newEventView.start = start;
  274. newEventView.end = end;
  275. newEventView.statsPeriod = undefined;
  276. newEventView.dataset = dataset;
  277. newEventView.query = 'event.type:' + eventType + ' transaction:' + transactionName;
  278. newEventView.additionalConditions = new MutableSearch('');
  279. const chartFields: QueryFieldValue[] = fields.map(field => {
  280. return {
  281. kind: 'function',
  282. function: [field, '', undefined, undefined],
  283. };
  284. });
  285. return newEventView.withColumns(chartFields);
  286. }
  287. function toFormattedNumber(numberString: string, decimal: number) {
  288. return parseFloat(numberString).toFixed(decimal);
  289. }
  290. export function relativeChange(before: number, after: number) {
  291. return (after - before) / before;
  292. }
  293. function renderHeadCell(column: MetricColumn, _index: number): ReactNode {
  294. const align = fieldAlignment(column.key, COLUMN_TYPE[column.key]);
  295. return (
  296. <SortLink
  297. title={column.name}
  298. align={align}
  299. direction={undefined}
  300. canSort={false}
  301. generateSortLink={() => undefined}
  302. />
  303. );
  304. }
  305. export function renderBodyCell(
  306. column: GridColumnOrder<MetricColumnKey>,
  307. dataRow: TableDataRow
  308. ) {
  309. let data = '';
  310. let color = '';
  311. if (column.key === 'change') {
  312. if (
  313. dataRow[column.key] === '0%' ||
  314. dataRow[column.key] === '+NaN%' ||
  315. dataRow[column.key] === '-'
  316. ) {
  317. data = '-';
  318. } else if (dataRow[column.key].charAt(0) !== '-') {
  319. color = theme.red300;
  320. data = '+' + dataRow[column.key];
  321. } else {
  322. color = theme.green300;
  323. data = dataRow[column.key];
  324. }
  325. } else {
  326. data = dataRow[column.key];
  327. }
  328. return (
  329. <Container data-test-id={'pce-metrics-chart-row-' + column.key}>
  330. <ExplorerText
  331. data-test-id={'pce-metrics-text-' + column.key}
  332. align={column.key !== 'metric' ? 'right' : 'left'}
  333. color={color}
  334. >
  335. {data}
  336. </ExplorerText>
  337. </Container>
  338. );
  339. }
  340. export function getQueryParams(
  341. startTime: string,
  342. endTime: string,
  343. fields: AggregationKeyWithAlias[],
  344. query: string,
  345. dataset: DiscoverDatasets,
  346. organization: Organization,
  347. eventView: EventView,
  348. transactionName: string,
  349. location: Location
  350. ) {
  351. const newLocation = {
  352. ...location,
  353. start: startTime,
  354. end: endTime,
  355. statsPeriod: undefined,
  356. dataset,
  357. sort: undefined,
  358. query: {
  359. query: `event.type: ${query} transaction: ${transactionName}`,
  360. statsPeriod: undefined,
  361. start: startTime,
  362. end: endTime,
  363. },
  364. };
  365. const newEventView = getEventViewWithFields(
  366. organization,
  367. eventView,
  368. startTime,
  369. endTime,
  370. fields,
  371. query,
  372. transactionName,
  373. dataset
  374. );
  375. return {
  376. eventView: newEventView,
  377. location: newLocation,
  378. orgSlug: organization.slug,
  379. transactionName,
  380. transactionThresholdMetric: TransactionThresholdMetric.TRANSACTION_DURATION,
  381. options: {
  382. refetchOnWindowFocus: false,
  383. },
  384. };
  385. }