metricsTable.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import type {ReactNode} from 'react';
  2. import {useMemo} from 'react';
  3. import type {Location} from 'history';
  4. import moment from 'moment-timezone';
  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/organization';
  10. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  11. import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  12. import type EventView from 'sentry/utils/discover/eventView';
  13. import type {
  14. AggregationKeyWithAlias,
  15. ColumnType,
  16. QueryFieldValue,
  17. } from 'sentry/utils/discover/fields';
  18. import {fieldAlignment} from 'sentry/utils/discover/fields';
  19. import {Container} from 'sentry/utils/discover/styles';
  20. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  21. import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
  22. import {formatPercentage} from 'sentry/utils/number/formatPercentage';
  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, isPending: 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, isPending: 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. isLoading={isLoadingBefore || isLoadingAfter || isLoading}
  174. />
  175. );
  176. }
  177. function getEventsRowData(
  178. field: string,
  179. rowTitle: string,
  180. suffix: string,
  181. nullValue: string | number,
  182. percentage: boolean,
  183. beforeData?: TableData,
  184. afterData?: TableData
  185. ): TableDataRow {
  186. if (
  187. beforeData?.data[0][field] !== undefined &&
  188. afterData?.data[0][field] !== undefined
  189. ) {
  190. return {
  191. metric: rowTitle,
  192. before: !percentage
  193. ? toFormattedNumber(beforeData.data[0][field].toString(), 1) + ' ' + suffix
  194. : formatPercentage(beforeData.data[0][field] as number, 1),
  195. after: !percentage
  196. ? toFormattedNumber(afterData.data[0][field].toString(), 1) + ' ' + suffix
  197. : formatPercentage(afterData.data[0][field] as number, 1),
  198. change:
  199. beforeData.data[0][field] && afterData.data[0][field]
  200. ? formatPercentage(
  201. relativeChange(
  202. beforeData.data[0][field] as number,
  203. afterData.data[0][field] as number
  204. ),
  205. 1
  206. )
  207. : '-',
  208. };
  209. }
  210. return {
  211. metric: rowTitle,
  212. before: nullValue,
  213. after: nullValue,
  214. change: '-',
  215. };
  216. }
  217. function getTrendsRowData(
  218. aggregateData: TrendsTransaction | undefined,
  219. metric: TrendFunctionField
  220. ): TableDataRow | undefined {
  221. if (aggregateData) {
  222. return {
  223. metric: metric.toString().toUpperCase(),
  224. before: aggregateData?.aggregate_range_1.toFixed(1) + ' ms',
  225. after: aggregateData?.aggregate_range_2.toFixed(1) + ' ms',
  226. change:
  227. aggregateData?.trend_percentage !== 1
  228. ? formatPercentage(aggregateData?.trend_percentage! - 1, 1)
  229. : '-',
  230. };
  231. }
  232. return undefined;
  233. }
  234. function getEventViewWithFields(
  235. _organization: Organization,
  236. eventView: EventView,
  237. start: string,
  238. end: string,
  239. fields: AggregationKeyWithAlias[],
  240. eventType: string,
  241. transactionName: string,
  242. dataset: DiscoverDatasets
  243. ): EventView {
  244. const newEventView = eventView.clone();
  245. newEventView.start = start;
  246. newEventView.end = end;
  247. newEventView.statsPeriod = undefined;
  248. newEventView.dataset = dataset;
  249. newEventView.query = 'event.type:' + eventType + ' transaction:' + transactionName;
  250. newEventView.additionalConditions = new MutableSearch('');
  251. const chartFields: QueryFieldValue[] = fields.map(field => {
  252. return {
  253. kind: 'function',
  254. function: [field, '', undefined, undefined],
  255. };
  256. });
  257. return newEventView.withColumns(chartFields);
  258. }
  259. function toFormattedNumber(numberString: string, decimal: number) {
  260. return parseFloat(numberString).toFixed(decimal);
  261. }
  262. export function relativeChange(before: number, after: number) {
  263. return (after - before) / before;
  264. }
  265. function renderHeadCell(column: MetricColumn, _index: number): ReactNode {
  266. const align = fieldAlignment(column.key, COLUMN_TYPE[column.key]);
  267. return (
  268. <SortLink
  269. title={column.name}
  270. align={align}
  271. direction={undefined}
  272. canSort={false}
  273. generateSortLink={() => undefined}
  274. />
  275. );
  276. }
  277. export function renderBodyCell(
  278. column: GridColumnOrder<MetricColumnKey>,
  279. dataRow: TableDataRow
  280. ) {
  281. let data = '';
  282. let color = '';
  283. if (column.key === 'change') {
  284. if (
  285. dataRow[column.key] === '0%' ||
  286. dataRow[column.key] === '+NaN%' ||
  287. dataRow[column.key] === '-'
  288. ) {
  289. data = '-';
  290. } else if (dataRow[column.key].charAt(0) !== '-') {
  291. color = theme.red300;
  292. data = '+' + dataRow[column.key];
  293. } else {
  294. color = theme.green300;
  295. data = dataRow[column.key];
  296. }
  297. } else {
  298. data = dataRow[column.key];
  299. }
  300. return (
  301. <Container data-test-id={'pce-metrics-chart-row-' + column.key}>
  302. <ExplorerText
  303. data-test-id={'pce-metrics-text-' + column.key}
  304. align={column.key !== 'metric' ? 'right' : 'left'}
  305. color={color}
  306. >
  307. {data}
  308. </ExplorerText>
  309. </Container>
  310. );
  311. }
  312. export function getQueryParams(
  313. startTime: string,
  314. endTime: string,
  315. fields: AggregationKeyWithAlias[],
  316. query: string,
  317. dataset: DiscoverDatasets,
  318. organization: Organization,
  319. eventView: EventView,
  320. transactionName: string,
  321. location: Location
  322. ) {
  323. const newLocation = {
  324. ...location,
  325. start: startTime,
  326. end: endTime,
  327. statsPeriod: undefined,
  328. dataset,
  329. sort: undefined,
  330. query: {
  331. query: `event.type: ${query} transaction: ${transactionName}`,
  332. statsPeriod: undefined,
  333. start: startTime,
  334. end: endTime,
  335. },
  336. };
  337. const newEventView = getEventViewWithFields(
  338. organization,
  339. eventView,
  340. startTime,
  341. endTime,
  342. fields,
  343. query,
  344. transactionName,
  345. dataset
  346. );
  347. return {
  348. eventView: newEventView,
  349. location: newLocation,
  350. orgSlug: organization.slug,
  351. transactionName,
  352. transactionThresholdMetric: TransactionThresholdMetric.TRANSACTION_DURATION,
  353. options: {
  354. refetchOnWindowFocus: false,
  355. },
  356. };
  357. }