Browse Source

ref(ddm): Prepare metrics query helpers for multi queries (#65146)

- relates to https://github.com/getsentry/sentry/issues/64773
ArthurKnaus 1 year ago
parent
commit
b103829d55

+ 0 - 176
static/app/utils/metrics/useMetricsData.tsx

@@ -1,176 +0,0 @@
-import {useCallback, useEffect, useState} from 'react';
-
-import type {DateString, PageFilters} from 'sentry/types';
-import {getDateTimeParams, getDDMInterval} from 'sentry/utils/metrics';
-import {getUseCaseFromMRI, parseField} from 'sentry/utils/metrics/mri';
-import type {MetricsQuery} from 'sentry/utils/metrics/types';
-import {useApiQuery} from 'sentry/utils/queryClient';
-import useOrganization from 'sentry/utils/useOrganization';
-
-import type {
-  MetricsDataIntervalLadder,
-  MetricsQueryApiResponse,
-} from '../../types/metrics';
-
-export function createMqlQuery({
-  field,
-  query,
-  groupBy = [],
-}: {field: string; groupBy?: string[]; query?: string}) {
-  let mql = field;
-  if (query) {
-    mql = `${mql}{${query}}`;
-  }
-  if (groupBy.length) {
-    mql = `${mql} by (${groupBy.join(',')})`;
-  }
-  return mql;
-}
-
-export function getMetricsQueryApiRequestPayload(
-  {
-    field,
-    query,
-    groupBy,
-    orderBy,
-    limit,
-  }: {
-    field: string;
-    groupBy?: string[];
-    limit?: number;
-    orderBy?: 'asc' | 'desc';
-    query?: string;
-  },
-  {projects, environments, datetime}: PageFilters,
-  {
-    intervalLadder,
-    interval: intervalParam,
-  }: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {}
-) {
-  const {mri: mri} = parseField(field) ?? {};
-  const useCase = getUseCaseFromMRI(mri) ?? 'custom';
-  const interval = intervalParam ?? getDDMInterval(datetime, useCase, intervalLadder);
-  const hasGoupBy = groupBy && groupBy.length > 0;
-
-  return {
-    query: {
-      ...getDateTimeParams(datetime),
-      project: projects,
-      environment: environments,
-      interval,
-    },
-    body: {
-      queries: [
-        {
-          name: 'query_1',
-          mql: createMqlQuery({field, query, groupBy}),
-        },
-      ],
-      formulas: [
-        {mql: '$query_1', limit: limit, order: hasGoupBy ? orderBy ?? 'desc' : undefined},
-      ],
-    },
-  };
-}
-
-export function useMetricsQuery(
-  {mri, op, datetime, projects, environments, query, groupBy}: MetricsQuery,
-  overrides: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {}
-) {
-  const organization = useOrganization();
-
-  const field = op ? `${op}(${mri})` : mri;
-
-  const {query: queryToSend, body} = getMetricsQueryApiRequestPayload(
-    {
-      field,
-      query: query ?? '',
-      groupBy,
-    },
-    {datetime, projects, environments},
-    {...overrides}
-  );
-
-  return useApiQuery<MetricsQueryApiResponse>(
-    [
-      `/organizations/${organization.slug}/metrics/query/`,
-      {query: queryToSend, data: body, method: 'POST'},
-    ],
-    {
-      retry: 0,
-      staleTime: 0,
-      refetchOnReconnect: true,
-      refetchOnWindowFocus: true,
-      refetchInterval: false,
-    }
-  );
-}
-
-// Wraps useMetricsData and provides two additional features:
-// 1. return data is undefined only during the initial load
-// 2. provides a callback to trim the data to a specific time range when chart zoom is used
-export function useMetricsQueryZoom(
-  metricsQuery: MetricsQuery,
-  overrides: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {}
-) {
-  const [metricsData, setMetricsData] = useState<MetricsQueryApiResponse | undefined>();
-  const {
-    data: rawData,
-    isLoading,
-    isError,
-    error,
-  } = useMetricsQuery(metricsQuery, overrides);
-
-  useEffect(() => {
-    if (rawData) {
-      setMetricsData(rawData);
-    }
-  }, [rawData]);
-
-  const trimData = useCallback(
-    (
-      currentData: MetricsQueryApiResponse | undefined,
-      start,
-      end
-    ): MetricsQueryApiResponse | undefined => {
-      if (!currentData) {
-        return currentData;
-      }
-      // find the index of the first interval that is greater than the start time
-      const startIndex =
-        currentData.intervals.findIndex(interval => interval >= start) - 1;
-      const endIndex = currentData.intervals.findIndex(interval => interval >= end);
-
-      if (startIndex === -1 || endIndex === -1) {
-        return currentData;
-      }
-
-      return {
-        ...currentData,
-        intervals: currentData.intervals.slice(startIndex, endIndex),
-        data: currentData.data.map(group =>
-          group.map(entry => ({
-            ...entry,
-            series: entry.series.slice(startIndex, endIndex),
-          }))
-        ),
-      };
-    },
-    []
-  );
-
-  const handleZoom = useCallback(
-    (start: DateString, end: DateString) => {
-      setMetricsData(currentData => trimData(currentData, start, end));
-    },
-    [trimData]
-  );
-
-  return {
-    data: metricsData,
-    isLoading,
-    isError,
-    error,
-    onZoom: handleZoom,
-  };
-}

+ 9 - 7
static/app/utils/metrics/useMetricsData.spec.tsx → static/app/utils/metrics/useMetricsQuery.spec.tsx

@@ -2,7 +2,7 @@ import type {PageFilters} from 'sentry/types';
 import {
   createMqlQuery,
   getMetricsQueryApiRequestPayload,
-} from 'sentry/utils/metrics/useMetricsData';
+} from 'sentry/utils/metrics/useMetricsQuery';
 
 describe('createMqlQuery', () => {
   it('should create a basic mql query', () => {
@@ -49,7 +49,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {start: '2023-01-01', end: '2023-01-31', period: null, utc: true},
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters);
+    const result = getMetricsQueryApiRequestPayload([metric], filters);
 
     expect(result.query).toEqual({
       start: '2023-01-01T00:00:00.000Z',
@@ -78,7 +78,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {period: '7d', utc: true} as PageFilters['datetime'],
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters);
+    const result = getMetricsQueryApiRequestPayload([metric], filters);
 
     expect(result.query).toEqual({
       statsPeriod: '7d',
@@ -106,7 +106,9 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {start: '2023-01-01', end: '2023-01-02', period: null, utc: true},
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters, {interval: '123m'});
+    const result = getMetricsQueryApiRequestPayload([metric], filters, {
+      interval: '123m',
+    });
 
     expect(result.query).toEqual({
       start: '2023-01-01T00:00:00.000Z',
@@ -140,7 +142,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {start: '2023-01-01', end: '2023-01-02', period: null, utc: true},
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters);
+    const result = getMetricsQueryApiRequestPayload([metric], filters);
 
     expect(result.query).toEqual({
       start: '2023-01-01T00:00:00.000Z',
@@ -173,7 +175,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {start: '2023-01-01', end: '2023-01-02', period: null, utc: true},
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters);
+    const result = getMetricsQueryApiRequestPayload([metric], filters);
 
     expect(result.query).toEqual({
       start: '2023-01-01T00:00:00.000Z',
@@ -206,7 +208,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
       datetime: {start: '2023-01-01', end: '2023-01-02', period: null, utc: true},
     };
 
-    const result = getMetricsQueryApiRequestPayload(metric, filters, {
+    const result = getMetricsQueryApiRequestPayload([metric], filters, {
       intervalLadder: 'ddm',
     });
 

+ 116 - 0
static/app/utils/metrics/useMetricsQuery.tsx

@@ -0,0 +1,116 @@
+import {useMemo} from 'react';
+
+import type {PageFilters} from 'sentry/types';
+import {getDateTimeParams, getDDMInterval} from 'sentry/utils/metrics';
+import {getUseCaseFromMRI, MRIToField, parseField} from 'sentry/utils/metrics/mri';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+
+import type {
+  MetricsDataIntervalLadder,
+  MetricsQueryApiResponse,
+  MRI,
+} from '../../types/metrics';
+
+export function createMqlQuery({
+  field,
+  query,
+  groupBy = [],
+}: {field: string; groupBy?: string[]; query?: string}) {
+  let mql = field;
+  if (query) {
+    mql = `${mql}{${query}}`;
+  }
+  if (groupBy.length) {
+    mql = `${mql} by (${groupBy.join(',')})`;
+  }
+  return mql;
+}
+
+interface MetricsQueryApiRequestQuery {
+  field: string;
+  groupBy?: string[];
+  limit?: number;
+  name?: string;
+  orderBy?: 'asc' | 'desc';
+  query?: string;
+}
+
+export function getMetricsQueryApiRequestPayload(
+  queries: MetricsQueryApiRequestQuery[],
+  {projects, environments, datetime}: PageFilters,
+  {
+    intervalLadder,
+    interval: intervalParam,
+  }: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {}
+) {
+  // We take the first queries useCase to determine the interval
+  // If no useCase is found we default to custom
+  // The backend will error if the interval is not valid for any of the useCases
+  const {mri: mri} = parseField(queries[0]?.field) ?? {};
+  const useCase = getUseCaseFromMRI(mri) ?? 'custom';
+  const interval = intervalParam ?? getDDMInterval(datetime, useCase, intervalLadder);
+
+  const requestQueries: {mql: string; name: string}[] = [];
+  const requestFormulas: {mql: string; limit?: number; order?: 'asc' | 'desc'}[] = [];
+
+  queries.forEach((query, index) => {
+    const {field, groupBy, limit, orderBy, query: queryParam, name: nameParam} = query;
+    const name = nameParam || `query_${index + 1}`;
+    const hasGoupBy = groupBy && groupBy.length > 0;
+    requestQueries.push({name, mql: createMqlQuery({field, query: queryParam, groupBy})});
+    requestFormulas.push({
+      mql: `$${name}`,
+      limit,
+      order: hasGoupBy ? orderBy ?? 'desc' : undefined,
+    });
+  });
+
+  return {
+    query: {
+      ...getDateTimeParams(datetime),
+      project: projects,
+      environment: environments,
+      interval,
+    },
+    body: {
+      queries: requestQueries,
+      formulas: requestFormulas,
+    },
+  };
+}
+
+export function useMetricsQuery(
+  queries: (Omit<MetricsQueryApiRequestQuery, 'field'> & {mri: MRI; op?: string})[],
+  {projects, environments, datetime}: PageFilters,
+  overrides: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {}
+) {
+  const organization = useOrganization();
+
+  const queryIsComplete = queries.every(({op}) => op);
+
+  const {query: queryToSend, body} = useMemo(
+    () =>
+      getMetricsQueryApiRequestPayload(
+        queries.map(query => ({...query, field: MRIToField(query.mri, query.op!)})),
+        {datetime, projects, environments},
+        {...overrides}
+      ),
+    [queries, datetime, projects, environments, overrides]
+  );
+
+  return useApiQuery<MetricsQueryApiResponse>(
+    [
+      `/organizations/${organization.slug}/metrics/query/`,
+      {query: queryToSend, data: body, method: 'POST'},
+    ],
+    {
+      retry: 0,
+      staleTime: 0,
+      refetchOnReconnect: true,
+      refetchOnWindowFocus: true,
+      refetchInterval: false,
+      enabled: queryIsComplete,
+    }
+  );
+}

+ 14 - 12
static/app/views/dashboards/datasetConfig/metrics.tsx

@@ -23,7 +23,7 @@ import {
   parseField,
   parseMRI,
 } from 'sentry/utils/metrics/mri';
-import {getMetricsQueryApiRequestPayload} from 'sentry/utils/metrics/useMetricsData';
+import {getMetricsQueryApiRequestPayload} from 'sentry/utils/metrics/useMetricsQuery';
 import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
 import {MetricSearchBar} from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/metricSearchBar';
 import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
@@ -402,17 +402,19 @@ function getMetricRequest(
   }
 
   const payload = getMetricsQueryApiRequestPayload(
-    {
-      field: query.aggregates[0],
-      query: query.conditions || undefined,
-      groupBy: query.columns || undefined,
-      orderBy: query.orderby
-        ? query.orderby.indexOf('-') === 0
-          ? 'desc'
-          : 'asc'
-        : undefined,
-      limit: limit || undefined,
-    },
+    [
+      {
+        field: query.aggregates[0],
+        query: query.conditions || undefined,
+        groupBy: query.columns || undefined,
+        orderBy: query.orderby
+          ? query.orderby.indexOf('-') === 0
+            ? 'desc'
+            : 'asc'
+          : undefined,
+        limit: limit || undefined,
+      },
+    ],
     pageFilters,
     {
       intervalLadder: displayType === DisplayType.BAR ? 'bar' : 'dashboard',

+ 10 - 6
static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx

@@ -18,7 +18,7 @@ import {
   MetricDisplayType,
   type MetricWidgetQueryParams,
 } from 'sentry/utils/metrics/types';
-import {useMetricsQueryZoom} from 'sentry/utils/metrics/useMetricsData';
+import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
 import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
 import type {AugmentedEChartDataZoomHandler} from 'sentry/views/dashboards/widgetCard/chart';
 import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
@@ -222,12 +222,16 @@ export function MetricWidgetChartContainer({
     isLoading,
     isError,
     error,
-  } = useMetricsQueryZoom(
+  } = useMetricsQuery(
+    [
+      {
+        mri,
+        op,
+        query: extendQuery(metricWidgetQueryParams.query, dashboardFilters),
+        groupBy,
+      },
+    ],
     {
-      mri,
-      op,
-      query: extendQuery(metricWidgetQueryParams.query, dashboardFilters),
-      groupBy,
       projects,
       environments,
       datetime,

+ 3 - 4
static/app/views/ddm/createAlertModal.tsx

@@ -1,5 +1,6 @@
 import {Fragment, useCallback, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
+import pick from 'lodash/pick';
 import * as qs from 'query-string';
 
 import type {ModalRenderProps} from 'sentry/actionCreators/modal';
@@ -31,7 +32,7 @@ import {
   parseMRI,
 } from 'sentry/utils/metrics/mri';
 import type {MetricsQuery} from 'sentry/utils/metrics/types';
-import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsData';
+import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
 import useRouter from 'sentry/utils/useRouter';
@@ -140,13 +141,11 @@ export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
   const aggregate = useMemo(() => getAlertAggregate(metricsQuery), [metricsQuery]);
 
   const {data, isLoading, refetch, isError} = useMetricsQuery(
+    [pick(metricsQuery, 'op', 'mri', 'query')],
     {
-      mri: metricsQuery.mri,
-      op: metricsQuery.op,
       projects: formState.project ? [parseInt(formState.project, 10)] : [],
       environments: formState.environment ? [formState.environment] : [],
       datetime: {period: alertPeriod} as PageFilters['datetime'],
-      query: metricsQuery.query,
     },
     {
       interval: alertInterval,

+ 3 - 6
static/app/views/ddm/widget.tsx

@@ -32,7 +32,7 @@ import type {
 import {MetricDisplayType} from 'sentry/utils/metrics/types';
 import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric';
 import {useMetricSamples} from 'sentry/utils/metrics/useMetricsCorrelations';
-import {useMetricsQueryZoom} from 'sentry/utils/metrics/useMetricsData';
+import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
 import {MetricChart} from 'sentry/views/ddm/chart';
 import type {FocusAreaProps} from 'sentry/views/ddm/context';
 import {createChartPalette} from 'sentry/views/ddm/metricsChartPalette';
@@ -245,12 +245,9 @@ const MetricWidgetBody = memo(
       isLoading,
       isError,
       error,
-    } = useMetricsQueryZoom(
+    } = useMetricsQuery(
+      [{mri, op, query, groupBy}],
       {
-        mri,
-        op,
-        query,
-        groupBy,
         projects,
         environments,
         datetime,

+ 2 - 3
static/app/views/settings/projectMetrics/projectMetricsDetails.tsx

@@ -27,7 +27,7 @@ import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
 import {formatMRI, formatMRIField, MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
 import {MetricDisplayType} from 'sentry/utils/metrics/types';
 import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric';
-import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsData';
+import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
 import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
 import routeTitleGen from 'sentry/utils/routeTitle';
 import {CodeLocations} from 'sentry/views/ddm/codeLocations';
@@ -76,6 +76,7 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
   const {type, name, unit} = parseMRI(mri) ?? {};
   const operation = getSettingsOperationForType(type ?? 'c');
   const {data: metricsData, isLoading} = useMetricsQuery(
+    [{mri, op: operation}],
     {
       datetime: {
         period: '30d',
@@ -84,9 +85,7 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
         utc: false,
       },
       environments: [],
-      mri,
       projects: projectIds,
-      op: operation,
     },
     {interval: '1d'}
   );