123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- import type {ReactNode} from 'react';
- import {useMemo} from 'react';
- import type {Location} from 'history';
- import moment from 'moment';
- import type {GridColumnOrder} from 'sentry/components/gridEditable';
- import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
- import SortLink from 'sentry/components/gridEditable/sortLink';
- import {t} from 'sentry/locale';
- import type {Organization} from 'sentry/types';
- import {parsePeriodToHours} from 'sentry/utils/dates';
- import type {TableData} from 'sentry/utils/discover/discoverQuery';
- import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
- import type EventView from 'sentry/utils/discover/eventView';
- import type {
- AggregationKeyWithAlias,
- ColumnType,
- QueryFieldValue,
- } from 'sentry/utils/discover/fields';
- import {fieldAlignment} from 'sentry/utils/discover/fields';
- import {Container} from 'sentry/utils/discover/styles';
- import {DiscoverDatasets} from 'sentry/utils/discover/types';
- import {formatPercentage} from 'sentry/utils/formatters';
- import theme from 'sentry/utils/theme';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {TransactionThresholdMetric} from 'sentry/views/performance/transactionSummary/transactionThresholdModal';
- import {ExplorerText} from 'sentry/views/performance/trends/changeExplorer';
- import type {
- NormalizedTrendsTransaction,
- TrendsTransaction,
- TrendView,
- } from 'sentry/views/performance/trends/types';
- import {TrendFunctionField} from 'sentry/views/performance/trends/types';
- type MetricsTableProps = {
- isLoading: boolean;
- location: Location;
- organization: Organization;
- transaction: NormalizedTrendsTransaction;
- trendFunction: string;
- trendView: TrendView;
- };
- const fieldsNeeded: AggregationKeyWithAlias[] = ['tps', 'p50', 'p95', 'failure_rate'];
- type MetricColumnKey = 'metric' | 'before' | 'after' | 'change';
- type MetricColumn = GridColumnOrder<MetricColumnKey>;
- type TableDataRow = Record<MetricColumnKey, any>;
- const MetricColumnOrder = ['metric', 'before', 'after', 'change'];
- export const COLUMNS: Record<MetricColumnKey, MetricColumn> = {
- metric: {
- key: 'metric',
- name: t('Metric'),
- width: COL_WIDTH_UNDEFINED,
- },
- before: {
- key: 'before',
- name: t('Before'),
- width: COL_WIDTH_UNDEFINED,
- },
- after: {
- key: 'after',
- name: t('After'),
- width: COL_WIDTH_UNDEFINED,
- },
- change: {
- key: 'change',
- name: t('Change'),
- width: COL_WIDTH_UNDEFINED,
- },
- };
- const COLUMN_TYPE: Record<MetricColumnKey, ColumnType> = {
- metric: 'string',
- before: 'duration',
- after: 'duration',
- change: 'percentage',
- };
- export function MetricsTable(props: MetricsTableProps) {
- const {trendFunction, transaction, trendView, organization, location, isLoading} =
- props;
- const p50 =
- trendFunction === TrendFunctionField.P50
- ? getTrendsRowData(transaction, TrendFunctionField.P50)
- : undefined;
- const p95 =
- trendFunction === TrendFunctionField.P95
- ? getTrendsRowData(transaction, TrendFunctionField.P95)
- : undefined;
- const breakpoint = transaction.breakpoint;
- const hours = trendView.statsPeriod ? parsePeriodToHours(trendView.statsPeriod) : 0;
- const startTime = useMemo(
- () =>
- trendView.start ? trendView.start : moment().subtract(hours, 'h').toISOString(),
- [hours, trendView.start]
- );
- const breakpointTime = breakpoint ? new Date(breakpoint * 1000).toISOString() : '';
- const endTime = useMemo(
- () => (trendView.end ? trendView.end : moment().toISOString()),
- [trendView.end]
- );
- const {data: beforeBreakpoint, isLoading: isLoadingBefore} = useDiscoverQuery(
- getQueryParams(
- startTime,
- breakpointTime,
- fieldsNeeded,
- 'transaction',
- DiscoverDatasets.METRICS,
- organization,
- trendView,
- transaction.transaction,
- location
- )
- );
- const {data: afterBreakpoint, isLoading: isLoadingAfter} = useDiscoverQuery(
- getQueryParams(
- breakpointTime,
- endTime,
- fieldsNeeded,
- 'transaction',
- DiscoverDatasets.METRICS,
- organization,
- trendView,
- transaction.transaction,
- location
- )
- );
- const throughput: TableDataRow = getEventsRowData(
- 'tps()',
- 'Throughput',
- 'ps',
- '-',
- false,
- beforeBreakpoint,
- afterBreakpoint
- );
- const p50Events = !p50
- ? getEventsRowData(
- 'p50()',
- 'P50',
- 'ms',
- '-',
- false,
- beforeBreakpoint,
- afterBreakpoint
- )
- : p50;
- const p95Events = !p95
- ? getEventsRowData(
- 'p95()',
- 'P95',
- 'ms',
- '-',
- false,
- beforeBreakpoint,
- afterBreakpoint
- )
- : p95;
- const failureRate: TableDataRow = getEventsRowData(
- 'failure_rate()',
- 'Failure Rate',
- '%',
- 0,
- true,
- beforeBreakpoint,
- afterBreakpoint
- );
- const columnOrder = MetricColumnOrder.map(column => COLUMNS[column]);
- return (
- <GridEditable
- data={[throughput, p50Events, p95Events, failureRate]}
- columnOrder={columnOrder}
- columnSortBy={[]}
- grid={{
- renderHeadCell,
- renderBodyCell,
- }}
- location={location}
- isLoading={isLoadingBefore || isLoadingAfter || isLoading}
- />
- );
- }
- function getEventsRowData(
- field: string,
- rowTitle: string,
- suffix: string,
- nullValue: string | number,
- percentage: boolean,
- beforeData?: TableData,
- afterData?: TableData
- ): TableDataRow {
- if (
- beforeData?.data[0][field] !== undefined &&
- afterData?.data[0][field] !== undefined
- ) {
- return {
- metric: rowTitle,
- before: !percentage
- ? toFormattedNumber(beforeData.data[0][field].toString(), 1) + ' ' + suffix
- : formatPercentage(beforeData.data[0][field] as number, 1),
- after: !percentage
- ? toFormattedNumber(afterData.data[0][field].toString(), 1) + ' ' + suffix
- : formatPercentage(afterData.data[0][field] as number, 1),
- change:
- beforeData.data[0][field] && afterData.data[0][field]
- ? formatPercentage(
- relativeChange(
- beforeData.data[0][field] as number,
- afterData.data[0][field] as number
- ),
- 1
- )
- : '-',
- };
- }
- return {
- metric: rowTitle,
- before: nullValue,
- after: nullValue,
- change: '-',
- };
- }
- function getTrendsRowData(
- aggregateData: TrendsTransaction | undefined,
- metric: TrendFunctionField
- ): TableDataRow | undefined {
- if (aggregateData) {
- return {
- metric: metric.toString().toUpperCase(),
- before: aggregateData?.aggregate_range_1.toFixed(1) + ' ms',
- after: aggregateData?.aggregate_range_2.toFixed(1) + ' ms',
- change:
- aggregateData?.trend_percentage !== 1
- ? formatPercentage(aggregateData?.trend_percentage! - 1, 1)
- : '-',
- };
- }
- return undefined;
- }
- function getEventViewWithFields(
- _organization: Organization,
- eventView: EventView,
- start: string,
- end: string,
- fields: AggregationKeyWithAlias[],
- eventType: string,
- transactionName: string,
- dataset: DiscoverDatasets
- ): EventView {
- const newEventView = eventView.clone();
- newEventView.start = start;
- newEventView.end = end;
- newEventView.statsPeriod = undefined;
- newEventView.dataset = dataset;
- newEventView.query = 'event.type:' + eventType + ' transaction:' + transactionName;
- newEventView.additionalConditions = new MutableSearch('');
- const chartFields: QueryFieldValue[] = fields.map(field => {
- return {
- kind: 'function',
- function: [field, '', undefined, undefined],
- };
- });
- return newEventView.withColumns(chartFields);
- }
- function toFormattedNumber(numberString: string, decimal: number) {
- return parseFloat(numberString).toFixed(decimal);
- }
- export function relativeChange(before: number, after: number) {
- return (after - before) / before;
- }
- function renderHeadCell(column: MetricColumn, _index: number): ReactNode {
- const align = fieldAlignment(column.key, COLUMN_TYPE[column.key]);
- return (
- <SortLink
- title={column.name}
- align={align}
- direction={undefined}
- canSort={false}
- generateSortLink={() => undefined}
- />
- );
- }
- export function renderBodyCell(
- column: GridColumnOrder<MetricColumnKey>,
- dataRow: TableDataRow
- ) {
- let data = '';
- let color = '';
- if (column.key === 'change') {
- if (
- dataRow[column.key] === '0%' ||
- dataRow[column.key] === '+NaN%' ||
- dataRow[column.key] === '-'
- ) {
- data = '-';
- } else if (dataRow[column.key].charAt(0) !== '-') {
- color = theme.red300;
- data = '+' + dataRow[column.key];
- } else {
- color = theme.green300;
- data = dataRow[column.key];
- }
- } else {
- data = dataRow[column.key];
- }
- return (
- <Container data-test-id={'pce-metrics-chart-row-' + column.key}>
- <ExplorerText
- data-test-id={'pce-metrics-text-' + column.key}
- align={column.key !== 'metric' ? 'right' : 'left'}
- color={color}
- >
- {data}
- </ExplorerText>
- </Container>
- );
- }
- export function getQueryParams(
- startTime: string,
- endTime: string,
- fields: AggregationKeyWithAlias[],
- query: string,
- dataset: DiscoverDatasets,
- organization: Organization,
- eventView: EventView,
- transactionName: string,
- location: Location
- ) {
- const newLocation = {
- ...location,
- start: startTime,
- end: endTime,
- statsPeriod: undefined,
- dataset,
- sort: undefined,
- query: {
- query: `event.type: ${query} transaction: ${transactionName}`,
- statsPeriod: undefined,
- start: startTime,
- end: endTime,
- },
- };
- const newEventView = getEventViewWithFields(
- organization,
- eventView,
- startTime,
- endTime,
- fields,
- query,
- transactionName,
- dataset
- );
- return {
- eventView: newEventView,
- location: newLocation,
- orgSlug: organization.slug,
- transactionName,
- transactionThresholdMetric: TransactionThresholdMetric.TRANSACTION_DURATION,
- options: {
- refetchOnWindowFocus: false,
- },
- };
- }
|