Browse Source

feat(stat-detectors): Decouple chart from trends endpoint (#57667)

Implements a basic LineChart that takes a timeseries and an occurrence
and plots it. This is because the previous implementation relied on the
trends endpoint and wouldn't render the chart properly in some cases
Nar Saynorath 1 year ago
parent
commit
e73b9dd3b9

+ 44 - 44
static/app/components/events/eventStatisticalDetector/breakpointChart.tsx

@@ -1,19 +1,24 @@
-import {LineSeriesOption} from 'echarts';
-
-import {Event} from 'sentry/types';
-import EventView from 'sentry/utils/discover/eventView';
-import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery';
+import TransitionChart from 'sentry/components/charts/transitionChart';
+import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
+import {Event, EventsStatsData} from 'sentry/types';
+import EventView, {MetaType} from 'sentry/utils/discover/eventView';
+import {
+  DiscoverQueryProps,
+  useGenericDiscoverQuery,
+} from 'sentry/utils/discover/genericDiscoverQuery';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
-import {TrendsChart} from 'sentry/views/performance/landing/widgets/widgets/trendsWidget';
 import {
   NormalizedTrendsTransaction,
-  TrendChangeType,
   TrendFunctionField,
 } from 'sentry/views/performance/trends/types';
+import {generateTrendFunctionAsString} from 'sentry/views/performance/trends/utils';
 
 import {DataSection} from '../styles';
 
+import Chart from './lineChart';
+
 function camelToUnderscore(key: string) {
   return key.replace(/([A-Z\d])/g, '_$1').toLowerCase();
 }
@@ -30,9 +35,9 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) {
 
   const eventView = EventView.fromLocation(location);
   eventView.query = `event.type:transaction transaction:"${transaction}"`;
-  eventView.fields = [{field: 'transaction'}, {field: 'project'}];
   eventView.start = new Date(requestStart * 1000).toISOString();
   eventView.end = new Date(requestEnd * 1000).toISOString();
+  eventView.dataset = DiscoverDatasets.METRICS;
 
   // If start and end were defined, then do not use default 14d stats period
   eventView.statsPeriod = requestStart && requestEnd ? '' : eventView.statsPeriod;
@@ -46,45 +51,40 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) {
     return acc;
   }, {}) as NormalizedTrendsTransaction;
 
-  const additionalSeries: LineSeriesOption[] = [];
+  const {data, isLoading} = useGenericDiscoverQuery<
+    {
+      data: EventsStatsData;
+      meta: MetaType;
+    },
+    DiscoverQueryProps
+  >({
+    route: 'events-stats',
+    location,
+    eventView,
+    orgSlug: organization.slug,
+    getRequestPayload: () => ({
+      // Manually inject y-axis for events-stats because
+      // getEventsAPIPayload doesn't pass it along
+      ...eventView.getEventsAPIPayload(location),
+      yAxis: 'p95(transaction.duration)',
+    }),
+  });
 
   return (
     <DataSection>
-      <TrendsDiscoverQuery
-        orgSlug={organization.slug}
-        eventView={eventView}
-        location={location}
-        trendChangeType={TrendChangeType.REGRESSION}
-        trendFunctionField={TrendFunctionField.P95}
-        limit={1}
-        queryExtras={{
-          withTimeseries: 'true',
-          interval: '1h',
-        }}
-        withBreakpoint
-      >
-        {({trendsData, isLoading}) => {
-          return (
-            <TrendsChart
-              organization={organization}
-              isLoading={isLoading}
-              statsData={trendsData?.stats ?? {}}
-              query={eventView.query}
-              project={eventView.project}
-              environment={eventView.environment}
-              start={eventView.start}
-              end={eventView.end}
-              statsPeriod={eventView.statsPeriod}
-              transaction={normalizedOccurrenceEvent}
-              trendChangeType={TrendChangeType.REGRESSION}
-              trendFunctionField={TrendFunctionField.P95}
-              additionalSeries={additionalSeries}
-              applyRegressionFormatToInterval
-              disableLegend
-            />
-          );
-        }}
-      </TrendsDiscoverQuery>
+      <TransitionChart loading={isLoading} reloading>
+        <TransparentLoadingMask visible={isLoading} />
+        <Chart
+          statsData={data?.data ?? []}
+          evidenceData={normalizedOccurrenceEvent}
+          start={eventView.start}
+          end={eventView.end}
+          chartLabel={generateTrendFunctionAsString(
+            TrendFunctionField.P95,
+            'transaction.duration'
+          )}
+        />
+      </TransitionChart>
     </DataSection>
   );
 }

+ 130 - 0
static/app/components/events/eventStatisticalDetector/lineChart.tsx

@@ -0,0 +1,130 @@
+import {useTheme} from '@emotion/react';
+
+import ChartZoom from 'sentry/components/charts/chartZoom';
+import {
+  LineChart as EchartsLineChart,
+  LineChartProps,
+} from 'sentry/components/charts/lineChart';
+import {EventsStatsData} from 'sentry/types';
+import {getUserTimezone} from 'sentry/utils/dates';
+import {
+  axisLabelFormatter,
+  getDurationUnit,
+  tooltipFormatter,
+} from 'sentry/utils/discover/charts';
+import {aggregateOutputType} from 'sentry/utils/discover/fields';
+import useRouter from 'sentry/utils/useRouter';
+import {transformEventStats} from 'sentry/views/performance/trends/chart';
+import {
+  NormalizedTrendsTransaction,
+  TrendChangeType,
+} from 'sentry/views/performance/trends/types';
+import {
+  transformEventStatsSmoothed,
+  trendToColor,
+} from 'sentry/views/performance/trends/utils';
+import {getIntervalLine} from 'sentry/views/performance/utils';
+
+interface ChartProps {
+  chartLabel: string;
+  end: string;
+  evidenceData: NormalizedTrendsTransaction;
+  start: string;
+  statsData: EventsStatsData;
+}
+
+function LineChart({statsData, evidenceData, start, end, chartLabel}: ChartProps) {
+  const theme = useTheme();
+  const router = useRouter();
+
+  const results = transformEventStats(statsData, chartLabel);
+  const {smoothedResults, minValue, maxValue} = transformEventStatsSmoothed(
+    results,
+    chartLabel
+  );
+
+  const yMax = Math.max(
+    maxValue,
+    evidenceData?.aggregate_range_2 || 0,
+    evidenceData?.aggregate_range_1 || 0
+  );
+  const yMin = Math.min(
+    minValue,
+    evidenceData?.aggregate_range_1 || Number.MAX_SAFE_INTEGER,
+    evidenceData?.aggregate_range_2 || Number.MAX_SAFE_INTEGER
+  );
+
+  const smoothedSeries = smoothedResults
+    ? smoothedResults.map(values => {
+        return {
+          ...values,
+          color: trendToColor[TrendChangeType.REGRESSION].default,
+          lineStyle: {
+            opacity: 1,
+          },
+        };
+      })
+    : [];
+
+  const needsLabel = true;
+  const intervalSeries = getIntervalLine(
+    theme,
+    smoothedResults || [],
+    0.5,
+    needsLabel,
+    evidenceData,
+    true
+  );
+
+  const yDiff = yMax - yMin;
+  const yMargin = yDiff * 0.1;
+  const series = [...smoothedSeries, ...intervalSeries];
+
+  const durationUnit = getDurationUnit(series);
+
+  const chartOptions: Omit<LineChartProps, 'series'> = {
+    tooltip: {
+      valueFormatter: (value, seriesName) => {
+        return tooltipFormatter(value, aggregateOutputType(seriesName));
+      },
+    },
+    yAxis: {
+      min: Math.max(0, yMin - yMargin),
+      max: yMax + yMargin,
+      minInterval: durationUnit,
+      axisLabel: {
+        color: theme.chartLabel,
+        formatter: (value: number) =>
+          axisLabelFormatter(value, 'duration', undefined, durationUnit),
+      },
+    },
+  };
+
+  return (
+    <ChartZoom router={router} start={start} end={end} utc={getUserTimezone() === 'UTC'}>
+      {zoomRenderProps => {
+        return (
+          <EchartsLineChart
+            {...zoomRenderProps}
+            {...chartOptions}
+            series={series}
+            seriesOptions={{
+              showSymbol: false,
+            }}
+            toolBox={{
+              show: false,
+            }}
+            grid={{
+              left: '10px',
+              right: '10px',
+              top: '40px',
+              bottom: '0px',
+            }}
+          />
+        );
+      }}
+    </ChartZoom>
+  );
+}
+
+export default LineChart;

+ 4 - 1
static/app/views/performance/trends/chart.tsx

@@ -56,7 +56,10 @@ type Props = ViewProps & {
   trendFunctionField?: TrendFunctionField;
 };
 
-function transformEventStats(data: EventsStatsData, seriesName?: string): Series[] {
+export function transformEventStats(
+  data: EventsStatsData,
+  seriesName?: string
+): Series[] {
   return [
     {
       seriesName: seriesName || 'Current',