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 | null; transactionName: string; isShowingMetricsEventCount?: boolean; }; interface ChartData { chartOptions: Omit; errored: boolean; loading: boolean; reloading: boolean; series: LineChartProps['series']; } type Props = Pick & { 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 ( {zoomRenderProps => { const {errored, loading, reloading, chartOptions, series} = chartData; if (errored) { return ( ); } return ( {getDynamicText({ value: ( ), fixed: , })} ); }} ); } 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 ( {t('Count')} ); } return ( {t('Apdex')} {t('Failure Rate')} ); } 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 = { 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 = { 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 ( {({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 ( {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 ( ); }} ); }} ); } type ChartValueProps = { 'data-test-id': string; error: QueryError | null; isLoading: boolean; value: React.ReactNode; }; function ChartSummaryValue({error, isLoading, value, ...props}: ChartValueProps) { if (error) { return
{'\u2014'}
; } if (isLoading) { return ; } return {value}; } 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);