metricsTable.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import type {ReactNode} from 'react';
  2. import {useMemo} from 'react';
  3. import type {Location} from 'history';
  4. import moment from 'moment';
  5. import type {GridColumnOrder} from 'sentry/components/gridEditable';
  6. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  7. import SortLink from 'sentry/components/gridEditable/sortLink';
  8. import {t} from 'sentry/locale';
  9. import type {Organization} from 'sentry/types';
  10. import {parsePeriodToHours} from 'sentry/utils/dates';
  11. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  12. import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  13. import type EventView from 'sentry/utils/discover/eventView';
  14. import type {
  15. AggregationKeyWithAlias,
  16. ColumnType,
  17. QueryFieldValue,
  18. } from 'sentry/utils/discover/fields';
  19. import {fieldAlignment} 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 type {
  28. NormalizedTrendsTransaction,
  29. TrendsTransaction,
  30. TrendView,
  31. } from 'sentry/views/performance/trends/types';
  32. import {TrendFunctionField} 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', 'failure_rate'];
  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 throughput: TableDataRow = getEventsRowData(
  124. 'tps()',
  125. 'Throughput',
  126. 'ps',
  127. '-',
  128. false,
  129. beforeBreakpoint,
  130. afterBreakpoint
  131. );
  132. const p50Events = !p50
  133. ? getEventsRowData(
  134. 'p50()',
  135. 'P50',
  136. 'ms',
  137. '-',
  138. false,
  139. beforeBreakpoint,
  140. afterBreakpoint
  141. )
  142. : p50;
  143. const p95Events = !p95
  144. ? getEventsRowData(
  145. 'p95()',
  146. 'P95',
  147. 'ms',
  148. '-',
  149. false,
  150. beforeBreakpoint,
  151. afterBreakpoint
  152. )
  153. : p95;
  154. const failureRate: TableDataRow = getEventsRowData(
  155. 'failure_rate()',
  156. 'Failure Rate',
  157. '%',
  158. 0,
  159. true,
  160. beforeBreakpoint,
  161. afterBreakpoint
  162. );
  163. const columnOrder = MetricColumnOrder.map(column => COLUMNS[column]);
  164. return (
  165. <GridEditable
  166. data={[throughput, p50Events, p95Events, failureRate]}
  167. columnOrder={columnOrder}
  168. columnSortBy={[]}
  169. grid={{
  170. renderHeadCell,
  171. renderBodyCell,
  172. }}
  173. location={location}
  174. isLoading={isLoadingBefore || isLoadingAfter || isLoading}
  175. />
  176. );
  177. }
  178. function getEventsRowData(
  179. field: string,
  180. rowTitle: string,
  181. suffix: string,
  182. nullValue: string | number,
  183. percentage: boolean,
  184. beforeData?: TableData,
  185. afterData?: TableData
  186. ): TableDataRow {
  187. if (
  188. beforeData?.data[0][field] !== undefined &&
  189. afterData?.data[0][field] !== undefined
  190. ) {
  191. return {
  192. metric: rowTitle,
  193. before: !percentage
  194. ? toFormattedNumber(beforeData.data[0][field].toString(), 1) + ' ' + suffix
  195. : formatPercentage(beforeData.data[0][field] as number, 1),
  196. after: !percentage
  197. ? toFormattedNumber(afterData.data[0][field].toString(), 1) + ' ' + suffix
  198. : formatPercentage(afterData.data[0][field] as number, 1),
  199. change:
  200. beforeData.data[0][field] && afterData.data[0][field]
  201. ? formatPercentage(
  202. relativeChange(
  203. beforeData.data[0][field] as number,
  204. afterData.data[0][field] as number
  205. ),
  206. 1
  207. )
  208. : '-',
  209. };
  210. }
  211. return {
  212. metric: rowTitle,
  213. before: nullValue,
  214. after: nullValue,
  215. change: '-',
  216. };
  217. }
  218. function getTrendsRowData(
  219. aggregateData: TrendsTransaction | undefined,
  220. metric: TrendFunctionField
  221. ): TableDataRow | undefined {
  222. if (aggregateData) {
  223. return {
  224. metric: metric.toString().toUpperCase(),
  225. before: aggregateData?.aggregate_range_1.toFixed(1) + ' ms',
  226. after: aggregateData?.aggregate_range_2.toFixed(1) + ' ms',
  227. change:
  228. aggregateData?.trend_percentage !== 1
  229. ? formatPercentage(aggregateData?.trend_percentage! - 1, 1)
  230. : '-',
  231. };
  232. }
  233. return undefined;
  234. }
  235. function getEventViewWithFields(
  236. _organization: Organization,
  237. eventView: EventView,
  238. start: string,
  239. end: string,
  240. fields: AggregationKeyWithAlias[],
  241. eventType: string,
  242. transactionName: string,
  243. dataset: DiscoverDatasets
  244. ): EventView {
  245. const newEventView = eventView.clone();
  246. newEventView.start = start;
  247. newEventView.end = end;
  248. newEventView.statsPeriod = undefined;
  249. newEventView.dataset = dataset;
  250. newEventView.query = 'event.type:' + eventType + ' transaction:' + transactionName;
  251. newEventView.additionalConditions = new MutableSearch('');
  252. const chartFields: QueryFieldValue[] = fields.map(field => {
  253. return {
  254. kind: 'function',
  255. function: [field, '', undefined, undefined],
  256. };
  257. });
  258. return newEventView.withColumns(chartFields);
  259. }
  260. function toFormattedNumber(numberString: string, decimal: number) {
  261. return parseFloat(numberString).toFixed(decimal);
  262. }
  263. export function relativeChange(before: number, after: number) {
  264. return (after - before) / before;
  265. }
  266. function renderHeadCell(column: MetricColumn, _index: number): ReactNode {
  267. const align = fieldAlignment(column.key, COLUMN_TYPE[column.key]);
  268. return (
  269. <SortLink
  270. title={column.name}
  271. align={align}
  272. direction={undefined}
  273. canSort={false}
  274. generateSortLink={() => undefined}
  275. />
  276. );
  277. }
  278. export function renderBodyCell(
  279. column: GridColumnOrder<MetricColumnKey>,
  280. dataRow: TableDataRow
  281. ) {
  282. let data = '';
  283. let color = '';
  284. if (column.key === 'change') {
  285. if (
  286. dataRow[column.key] === '0%' ||
  287. dataRow[column.key] === '+NaN%' ||
  288. dataRow[column.key] === '-'
  289. ) {
  290. data = '-';
  291. } else if (dataRow[column.key].charAt(0) !== '-') {
  292. color = theme.red300;
  293. data = '+' + dataRow[column.key];
  294. } else {
  295. color = theme.green300;
  296. data = dataRow[column.key];
  297. }
  298. } else {
  299. data = dataRow[column.key];
  300. }
  301. return (
  302. <Container data-test-id={'pce-metrics-chart-row-' + column.key}>
  303. <ExplorerText
  304. data-test-id={'pce-metrics-text-' + column.key}
  305. align={column.key !== 'metric' ? 'right' : 'left'}
  306. color={color}
  307. >
  308. {data}
  309. </ExplorerText>
  310. </Container>
  311. );
  312. }
  313. export function getQueryParams(
  314. startTime: string,
  315. endTime: string,
  316. fields: AggregationKeyWithAlias[],
  317. query: string,
  318. dataset: DiscoverDatasets,
  319. organization: Organization,
  320. eventView: EventView,
  321. transactionName: string,
  322. location: Location
  323. ) {
  324. const newLocation = {
  325. ...location,
  326. start: startTime,
  327. end: endTime,
  328. statsPeriod: undefined,
  329. dataset,
  330. sort: undefined,
  331. query: {
  332. query: `event.type: ${query} transaction: ${transactionName}`,
  333. statsPeriod: undefined,
  334. start: startTime,
  335. end: endTime,
  336. },
  337. };
  338. const newEventView = getEventViewWithFields(
  339. organization,
  340. eventView,
  341. startTime,
  342. endTime,
  343. fields,
  344. query,
  345. transactionName,
  346. dataset
  347. );
  348. return {
  349. eventView: newEventView,
  350. location: newLocation,
  351. orgSlug: organization.slug,
  352. transactionName,
  353. transactionThresholdMetric: TransactionThresholdMetric.TRANSACTION_DURATION,
  354. options: {
  355. refetchOnWindowFocus: false,
  356. },
  357. };
  358. }