123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639 |
- import {Fragment} from 'react';
- // eslint-disable-next-line no-restricted-imports
- import {InjectedRouter, withRouter, WithRouterProps} from 'react-router';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import {Client} from 'sentry/api';
- import ChartZoom from 'sentry/components/charts/chartZoom';
- import ErrorPanel from 'sentry/components/charts/errorPanel';
- import EventsRequest from 'sentry/components/charts/eventsRequest';
- import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
- import {SectionHeading} from 'sentry/components/charts/styles';
- import TransitionChart from 'sentry/components/charts/transitionChart';
- import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
- import {getInterval} from 'sentry/components/charts/utils';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import Placeholder from 'sentry/components/placeholder';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {IconWarning} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import {Organization} from 'sentry/types';
- import {Series} from 'sentry/types/echarts';
- import {getUtcToLocalDateObject} from 'sentry/utils/dates';
- import {tooltipFormatter} from 'sentry/utils/discover/charts';
- import EventView from 'sentry/utils/discover/eventView';
- import {aggregateOutputType} from 'sentry/utils/discover/fields';
- import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
- import {
- formatAbbreviatedNumber,
- formatFloat,
- formatPercentage,
- } from 'sentry/utils/formatters';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import {
- MetricsCardinalityContext,
- useMetricsCardinalityContext,
- } from 'sentry/utils/performance/contexts/metricsCardinality';
- import {Theme} from 'sentry/utils/theme';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import useApi from 'sentry/utils/useApi';
- import {getTermHelp, PERFORMANCE_TERM} from 'sentry/views/performance/data';
- import {getMetricOnlyQueryParams} from '../../landing/widgets/utils';
- type ContainerProps = WithRouterProps & {
- error: QueryError | null;
- eventView: EventView;
- isLoading: boolean;
- location: Location;
- organization: Organization;
- totals: Record<string, number> | null;
- transactionName: string;
- isShowingMetricsEventCount?: boolean;
- };
- interface ChartData {
- chartOptions: Omit<LineChartProps, 'series'>;
- errored: boolean;
- loading: boolean;
- reloading: boolean;
- series: LineChartProps['series'];
- }
- type Props = Pick<ContainerProps, 'organization' | 'isLoading' | 'error' | 'totals'> & {
- chartData: ChartData;
- eventView: EventView;
- location: Location;
- router: InjectedRouter;
- transactionName: string;
- utc: boolean;
- end?: Date;
- isShowingMetricsEventCount?: boolean;
- isUsingMEP?: boolean;
- metricsChartData?: ChartData;
- start?: Date;
- statsPeriod?: string | null;
- };
- function SidebarCharts(props: Props) {
- const {isShowingMetricsEventCount, start, end, utc, router, statsPeriod, chartData} =
- props;
- const placeholderHeight = isShowingMetricsEventCount ? '200px' : '300px';
- const boxHeight = isShowingMetricsEventCount ? '300px' : '400px';
- return (
- <RelativeBox>
- <ChartLabels {...props} />
- <ChartZoom
- router={router}
- period={statsPeriod}
- start={start}
- end={end}
- utc={utc}
- xAxisIndex={[0, 1, 2]}
- >
- {zoomRenderProps => {
- const {errored, loading, reloading, chartOptions, series} = chartData;
- if (errored) {
- return (
- <ErrorPanel height={boxHeight}>
- <IconWarning color="gray300" size="lg" />
- </ErrorPanel>
- );
- }
- return (
- <TransitionChart loading={loading} reloading={reloading} height={boxHeight}>
- <TransparentLoadingMask visible={reloading} />
- {getDynamicText({
- value: (
- <LineChart {...zoomRenderProps} {...chartOptions} series={series} />
- ),
- fixed: <Placeholder height={placeholderHeight} testId="skeleton-ui" />,
- })}
- </TransitionChart>
- );
- }}
- </ChartZoom>
- </RelativeBox>
- );
- }
- function getDatasetCounts({
- chartData,
- metricsChartData,
- metricsCardinality,
- }: {
- metricsCardinality: MetricsCardinalityContext;
- chartData?: ChartData;
- metricsChartData?: ChartData;
- }) {
- const transactionCount =
- chartData?.series[0]?.data.reduce((sum, {value}) => sum + value, 0) ?? 0;
- const metricsCount =
- metricsChartData?.series[0]?.data.reduce((sum, {value}) => sum + value, 0) ?? 0;
- const missingMetrics =
- (!metricsCount && transactionCount) ||
- metricsCount < transactionCount ||
- metricsCardinality.outcome?.forceTransactionsOnly;
- return {
- transactionCount,
- metricsCount,
- missingMetrics,
- };
- }
- function ChartLabels({
- organization,
- isLoading,
- totals,
- error,
- isShowingMetricsEventCount,
- chartData,
- metricsChartData,
- }: Props) {
- const useAggregateAlias = !organization.features.includes(
- 'performance-frontend-use-events-endpoint'
- );
- const metricsCardinality = useMetricsCardinalityContext();
- if (isShowingMetricsEventCount) {
- const {transactionCount, metricsCount, missingMetrics} = getDatasetCounts({
- chartData,
- metricsChartData,
- metricsCardinality,
- });
- return (
- <Fragment>
- <ChartLabel top="0px">
- <ChartTitle>
- {t('Count')}
- <QuestionTooltip
- position="top"
- title={t(
- 'The count of events for the selected time period, showing the indexed events powering this page with filters compared to total processed events.'
- )}
- size="sm"
- />
- </ChartTitle>
- <ChartSummaryValue
- data-test-id="tpm-summary-value"
- isLoading={isLoading}
- error={error}
- value={
- totals
- ? missingMetrics
- ? tct('[txnCount]', {
- txnCount: formatAbbreviatedNumber(transactionCount),
- })
- : tct('[txnCount] of [metricCount]', {
- txnCount: formatAbbreviatedNumber(transactionCount),
- metricCount: formatAbbreviatedNumber(metricsCount),
- })
- : null
- }
- />
- </ChartLabel>
- </Fragment>
- );
- }
- return (
- <Fragment>
- <ChartLabel top="0px">
- <ChartTitle>
- {t('Apdex')}
- <QuestionTooltip
- position="top"
- title={getTermHelp(organization, PERFORMANCE_TERM.APDEX)}
- size="sm"
- />
- </ChartTitle>
- <ChartSummaryValue
- data-test-id="apdex-summary-value"
- isLoading={isLoading}
- error={error}
- value={
- totals
- ? formatFloat(useAggregateAlias ? totals.apdex : totals['apdex()'], 4)
- : null
- }
- />
- </ChartLabel>
- <ChartLabel top="160px">
- <ChartTitle>
- {t('Failure Rate')}
- <QuestionTooltip
- position="top"
- title={getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)}
- size="sm"
- />
- </ChartTitle>
- <ChartSummaryValue
- data-test-id="failure-rate-summary-value"
- isLoading={isLoading}
- error={error}
- value={
- totals
- ? formatPercentage(
- useAggregateAlias ? totals.failure_rate : totals['failure_rate()']
- )
- : null
- }
- />
- </ChartLabel>
- </Fragment>
- );
- }
- function getSideChartsOptions({
- theme,
- utc,
- isShowingMetricsEventCount,
- }: {
- theme: Theme;
- utc: boolean;
- isShowingMetricsEventCount?: boolean;
- }) {
- const colors = theme.charts.getColorPalette(3);
- const axisLineConfig = {
- scale: true,
- axisLine: {
- show: false,
- },
- axisTick: {
- show: false,
- },
- splitLine: {
- show: false,
- },
- };
- if (isShowingMetricsEventCount) {
- const chartOptions: Omit<LineChartProps, 'series'> = {
- height: 200,
- grid: [
- {
- top: '60px',
- left: '10px',
- right: '10px',
- height: '160px',
- },
- ],
- axisPointer: {
- // Link each x-axis together.
- link: [{xAxisIndex: [0]}],
- },
- xAxes: Array.from(new Array(1)).map((_i, index) => ({
- gridIndex: index,
- type: 'time',
- show: false,
- })),
- yAxes: [
- {
- // throughput
- gridIndex: 0,
- splitNumber: 4,
- axisLabel: {
- formatter: formatAbbreviatedNumber,
- color: theme.chartLabel,
- },
- ...axisLineConfig,
- },
- {
- // throughput
- gridIndex: 0,
- splitNumber: 4,
- axisLabel: {
- formatter: formatAbbreviatedNumber,
- color: theme.chartLabel,
- },
- ...axisLineConfig,
- },
- ],
- utc,
- isGroupedByDate: true,
- showTimeInTooltip: true,
- colors: [colors[0], theme.gray300],
- tooltip: {
- trigger: 'axis',
- truncate: 80,
- valueFormatter: (value, label) =>
- tooltipFormatter(value, aggregateOutputType(label)),
- nameFormatter(value: string) {
- return value === 'epm()' ? 'tpm()' : value;
- },
- },
- };
- return chartOptions;
- }
- const chartOptions: Omit<LineChartProps, 'series'> = {
- height: 300,
- grid: [
- {
- top: '60px',
- left: '10px',
- right: '10px',
- height: '100px',
- },
- {
- top: '220px',
- left: '10px',
- right: '10px',
- height: '100px',
- },
- ],
- axisPointer: {
- // Link each x-axis together.
- link: [{xAxisIndex: [0, 1]}],
- },
- xAxes: Array.from(new Array(2)).map((_i, index) => ({
- gridIndex: index,
- type: 'time',
- show: false,
- })),
- yAxes: [
- {
- // apdex
- gridIndex: 0,
- interval: 0.2,
- axisLabel: {
- formatter: (value: number) => `${formatFloat(value, 1)}`,
- color: theme.chartLabel,
- },
- ...axisLineConfig,
- },
- {
- // failure rate
- gridIndex: 1,
- splitNumber: 4,
- interval: 0.5,
- max: 1.0,
- axisLabel: {
- formatter: (value: number) => formatPercentage(value, 0),
- color: theme.chartLabel,
- },
- ...axisLineConfig,
- },
- ],
- utc,
- isGroupedByDate: true,
- showTimeInTooltip: true,
- colors: [colors[1], colors[2]],
- tooltip: {
- trigger: 'axis',
- truncate: 80,
- valueFormatter: (value, label) =>
- tooltipFormatter(value, aggregateOutputType(label)),
- nameFormatter(value: string) {
- return value === 'epm()' ? 'tpm()' : value;
- },
- },
- };
- return chartOptions;
- }
- /**
- * Temporary function to remove 0 values from beginning and end of the metrics time series.
- * TODO(): Fix the data coming back from the api so it's consistent with existing count data.
- */
- function trimLeadingTrailingZeroCounts(series: Series | undefined) {
- if (!series?.data) {
- return undefined;
- }
- if (series.data[0] && series.data[0].value === 0) {
- series.data.shift();
- }
- if (
- series.data[series.data.length - 1] &&
- series.data[series.data.length - 1].value === 0
- ) {
- series.data.pop();
- }
- return series;
- }
- const ALLOWED_QUERY_KEYS = ['transaction.op', 'transaction'];
- function SidebarChartsContainer({
- location,
- eventView,
- organization,
- router,
- isLoading,
- error,
- totals,
- transactionName,
- isShowingMetricsEventCount,
- }: ContainerProps) {
- const api = useApi();
- const theme = useTheme();
- const metricsCardinality = useMetricsCardinalityContext();
- const statsPeriod = eventView.statsPeriod;
- const start = eventView.start ? getUtcToLocalDateObject(eventView.start) : undefined;
- const end = eventView.end ? getUtcToLocalDateObject(eventView.end) : undefined;
- const project = eventView.project;
- const environment = eventView.environment;
- const query = eventView.query;
- const utc = normalizeDateTimeParams(location.query).utc === 'true';
- const chartOptions = getSideChartsOptions({
- theme,
- utc,
- isShowingMetricsEventCount,
- });
- const requestCommonProps = {
- api,
- start,
- end,
- period: statsPeriod,
- project,
- environment,
- query,
- };
- const contentCommonProps = {
- organization,
- router,
- error,
- isLoading,
- start,
- end,
- utc,
- totals,
- };
- const datetimeSelection = {
- start: start || null,
- end: end || null,
- period: statsPeriod,
- };
- const yAxis = isShowingMetricsEventCount
- ? ['count()', 'tpm()']
- : ['apdex()', 'failure_rate()'];
- const requestProps = {
- ...requestCommonProps,
- organization,
- interval: getInterval(datetimeSelection),
- showLoading: false,
- includePrevious: false,
- yAxis,
- partial: true,
- referrer: 'api.performance.transaction-summary.sidebar-chart',
- };
- return (
- <EventsRequest {...requestProps}>
- {({results: eventsResults, errored, loading, reloading}) => {
- const _results = isShowingMetricsEventCount
- ? (eventsResults || []).slice(0, 1)
- : eventsResults;
- const series = _results
- ? _results.map((values, i: number) => ({
- ...values,
- yAxisIndex: i,
- xAxisIndex: i,
- }))
- : [];
- const metricsCompatibleQueryProps = {...requestProps};
- const eventsQuery = new MutableSearch(query);
- const compatibleQuery = new MutableSearch('');
- for (const queryKey of ALLOWED_QUERY_KEYS) {
- if (eventsQuery.hasFilter(queryKey)) {
- compatibleQuery.setFilterValues(
- queryKey,
- eventsQuery.getFilterValues(queryKey)
- );
- }
- }
- metricsCompatibleQueryProps.query = compatibleQuery.formatString();
- return (
- <EventsRequest
- {...metricsCompatibleQueryProps}
- api={new Client()}
- queryExtras={getMetricOnlyQueryParams()}
- >
- {metricsChartData => {
- const metricSeries = metricsChartData.results
- ? metricsChartData.results.map((values, i: number) => ({
- ...values,
- yAxisIndex: i,
- xAxisIndex: i,
- }))
- : [];
- const chartData = {series, errored, loading, reloading, chartOptions};
- const _metricsChartData = {
- ...metricsChartData,
- series: metricSeries,
- chartOptions,
- };
- if (isShowingMetricsEventCount && metricSeries.length) {
- const countSeries = series[0];
- if (countSeries) {
- countSeries.seriesName = t('Indexed Events');
- const trimmed = trimLeadingTrailingZeroCounts(countSeries);
- if (trimmed) {
- series[0] = {...countSeries, ...trimmed};
- }
- }
- const {missingMetrics} = getDatasetCounts({
- chartData,
- metricsChartData: _metricsChartData,
- metricsCardinality,
- });
- const metricsCountSeries = metricSeries[0];
- if (!missingMetrics) {
- if (metricsCountSeries) {
- metricsCountSeries.seriesName = t('Processed Events');
- metricsCountSeries.lineStyle = {
- type: 'dashed',
- width: 1.5,
- };
- const trimmed = trimLeadingTrailingZeroCounts(metricsCountSeries);
- if (trimmed) {
- metricSeries[0] = {...metricsCountSeries, ...trimmed};
- }
- }
- series.push(metricsCountSeries);
- }
- }
- return (
- <SidebarCharts
- {...contentCommonProps}
- transactionName={transactionName}
- location={location}
- eventView={eventView}
- chartData={chartData}
- isShowingMetricsEventCount={isShowingMetricsEventCount}
- metricsChartData={_metricsChartData}
- />
- );
- }}
- </EventsRequest>
- );
- }}
- </EventsRequest>
- );
- }
- type ChartValueProps = {
- 'data-test-id': string;
- error: QueryError | null;
- isLoading: boolean;
- value: React.ReactNode;
- };
- function ChartSummaryValue({error, isLoading, value, ...props}: ChartValueProps) {
- if (error) {
- return <div {...props}>{'\u2014'}</div>;
- }
- if (isLoading) {
- return <Placeholder height="24px" {...props} />;
- }
- return <ChartValue {...props}>{value}</ChartValue>;
- }
- const RelativeBox = styled('div')`
- position: relative;
- `;
- const ChartTitle = styled(SectionHeading)`
- margin: 0;
- `;
- const ChartLabel = styled('div')<{top: string}>`
- position: absolute;
- top: ${p => p.top};
- z-index: 1;
- `;
- const ChartValue = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- `;
- export default withRouter(SidebarChartsContainer);
|