import {Fragment} from 'react'; import {browserHistory} from 'react-router'; import {useTheme} from '@emotion/react'; import {Query} from 'history'; import EventsRequest from 'sentry/components/charts/eventsRequest'; import {HeaderTitleLegend} from 'sentry/components/charts/styles'; import {getInterval, getSeriesSelection} from 'sentry/components/charts/utils'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {t} from 'sentry/locale'; import {EventsStats, EventsStatsData, OrganizationSummary, Project} from 'sentry/types'; import {Series} from 'sentry/types/echarts'; import {getUtcToLocalDateObject} from 'sentry/utils/dates'; import EventView from 'sentry/utils/discover/eventView'; import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers'; import {getAggregateAlias} from 'sentry/utils/discover/fields'; import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality'; import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery'; import useApi from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; import useRouter from 'sentry/utils/useRouter'; import { TrendChangeType, TrendFunctionField, TrendView, } from 'sentry/views/performance/trends/types'; import { generateTrendFunctionAsString, modifyTrendView, normalizeTrends, } from 'sentry/views/performance/trends/utils'; import {ViewProps} from 'sentry/views/performance/types'; import {getSelectedTransaction} from 'sentry/views/performance/utils'; import Content from './content'; type Props = ViewProps & { eventView: EventView; organization: OrganizationSummary; projects: Project[]; queryExtra: Query; trendFunction: TrendFunctionField; trendParameter: string; withoutZerofill: boolean; withBreakpoint?: boolean; }; function TrendChart({ project, environment, organization, query, statsPeriod, trendFunction, trendParameter, queryExtra, withoutZerofill, withBreakpoint, eventView, start: propsStart, end: propsEnd, projects, }: Props) { const router = useRouter(); const location = useLocation(); const api = useApi(); const theme = useTheme(); const {isLoading: isCardinalityCheckLoading, outcome} = useMetricsCardinalityContext(); const shouldGetBreakpoint = withBreakpoint && !isCardinalityCheckLoading && !outcome?.forceTransactionsOnly; function handleLegendSelectChanged(legendChange: { name: string; selected: Record<string, boolean>; type: string; }) { const {selected} = legendChange; const unselected = Object.keys(selected).filter(key => !selected[key]); const to = { ...location, query: { ...location.query, unselectedSeries: unselected, }, }; browserHistory.push(to); } const start = propsStart ? getUtcToLocalDateObject(propsStart) : null; const end = propsEnd ? getUtcToLocalDateObject(propsEnd) : null; const utc = normalizeDateTimeParams(location.query)?.utc === 'true'; const period = statsPeriod; const legend = { right: 10, top: 0, selected: getSeriesSelection(location), }; const datetimeSelection = {start, end, period}; const contentCommonProps = { theme, router, start, end, utc, legend, queryExtra, period, projects: project, environments: environment, onLegendSelectChanged: handleLegendSelectChanged, }; const requestCommonProps = { api, start, end, project, environment, query, period, interval: getInterval(datetimeSelection, 'high'), }; const header = ( <HeaderTitleLegend> {t('Trend')} <QuestionTooltip size="sm" position="top" title={t('Trends shows the smoothed value of an aggregate over time.')} /> </HeaderTitleLegend> ); const trendDisplay = generateTrendFunctionAsString(trendFunction, trendParameter); const trendView = eventView.clone() as TrendView; modifyTrendView( trendView, location, TrendChangeType.ANY, projects, shouldGetBreakpoint ); function transformTimeseriesData( data: EventsStatsData, meta: EventsStats['meta'], seriesName: string ): Series[] { let scale = 1; if (seriesName) { const unit = meta?.units?.[getAggregateAlias(seriesName)]; // Scale series values to milliseconds or bytes depending on units from meta scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1; } return [ { seriesName, data: data.map(([timestamp, countsForTimestamp]) => ({ name: timestamp * 1000, value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0) * scale, })), }, ]; } return ( <Fragment> {header} {shouldGetBreakpoint ? ( // queries events-trends-statsv2 for breakpoint data (feature flag only) <TrendsDiscoverQuery eventView={trendView} orgSlug={organization.slug} location={location} limit={1} withBreakpoint > {({isLoading, trendsData}) => { const events = normalizeTrends( (trendsData && trendsData.events && trendsData.events.data) || [] ); // keep trend change type as regression until the backend can support passing the type const selectedTransaction = getSelectedTransaction( location, TrendChangeType.ANY, events ); const statsData = trendsData?.stats || {}; const transactionEvent = ( statsData && selectedTransaction?.project && selectedTransaction?.transaction ? statsData[ [selectedTransaction?.project, selectedTransaction?.transaction].join( ',' ) ] : undefined ) as EventsStats; const data = transactionEvent?.data ?? []; const meta = transactionEvent?.meta ?? ({} as EventsStats['meta']); const timeSeriesMetricsData = transformTimeseriesData( data, meta, trendDisplay ); const metricsTimeFrame = transactionEvent && transactionEvent.start && transactionEvent.end ? {start: transactionEvent.start * 1000, end: transactionEvent.end * 1000} : undefined; return data.length !== 0 ? ( <Content series={timeSeriesMetricsData} errored={!trendsData && !isLoading} loading={isLoading || isCardinalityCheckLoading} reloading={isLoading} timeFrame={metricsTimeFrame} withBreakpoint transaction={selectedTransaction} {...contentCommonProps} /> ) : ( // queries events-stats for trend data if metrics trend data not found <EventsRequest {...requestCommonProps} organization={organization} showLoading={false} includePrevious={false} yAxis={trendDisplay} currentSeriesNames={[trendDisplay]} partial withoutZerofill={withoutZerofill} referrer="api.performance.transaction-summary.trends-chart" > {({errored, loading, reloading, timeseriesData, timeframe}) => { return ( <Content series={timeseriesData} errored={errored} loading={loading || isLoading} reloading={reloading} timeFrame={timeframe} withBreakpoint transaction={selectedTransaction} {...contentCommonProps} /> ); }} </EventsRequest> ); }} </TrendsDiscoverQuery> ) : ( <EventsRequest {...requestCommonProps} organization={organization} showLoading={false} includePrevious={false} yAxis={trendDisplay} currentSeriesNames={[trendDisplay]} partial withoutZerofill={withoutZerofill} referrer="api.performance.transaction-summary.trends-chart" > {({errored, loading, reloading, timeseriesData, timeframe: timeFrame}) => { return ( <Content series={timeseriesData} errored={errored} loading={loading || isCardinalityCheckLoading} reloading={reloading} timeFrame={timeFrame} {...contentCommonProps} /> ); }} </EventsRequest> )} </Fragment> ); } export default TrendChart;