Repository: getsentry/sentry
Pull request: feat(ddm): include measurements in metric dropdown
- closes #61025 
- refactors `useMetricsMeta`
- adds icons in context menu

<img width="1054" alt="image" src="https://github.com/getsentry/sentry/assets/86684834/c7d4f17f-636d-42e7-947a-bfe8ef0ceb89">


Problem statement:
Show selected transactions/ metrics in DDM
On top of custom metrics, we'd like to show selected metrics from the transactions namespace in DDM.
The ones we've been discussing:
- transaction.duration
- measurements (both default ones and custom ones)

When plotting transaction.duration and known measurements, we need to redirect to the correct (old) alert wizard. If you are plotting custom measurements, create alert button needs to be disabled.
In alerts and dashboards, we have only custom metrics in the dropdown. For everything else, there's a different "dataset".

Hints from issue discussion:

Commit message: formatting

Patch to review:
diff --git a/static/app/utils/metrics/index.tsx b/static/app/utils/metrics/index.tsx
index 7852c85bae078e..e19a8f7cbbc3ef 100644
--- a/static/app/utils/metrics/index.tsx
+++ b/static/app/utils/metrics/index.tsx
@@ -28,6 +28,7 @@ import {
   UseCase,
 } from 'sentry/types/metrics';
 import {defined, formatBytesBase2, formatBytesBase10} from 'sentry/utils';
+import {isMeasurement as isMeasurementName} from 'sentry/utils/discover/fields';
 import {
   DAY,
   formatNumberWithDynamicDecimalPoints,
@@ -37,7 +38,15 @@ import {
   SECOND,
   WEEK,
 } from 'sentry/utils/formatters';
-import {formatMRI, getUseCaseFromMRI, parseField} from 'sentry/utils/metrics/mri';
+import {
+  formatMRI,
+  formatMRIField,
+  getUseCaseFromMRI,
+  MRIToField,
+  parseField,
+  parseMRI,
+} from 'sentry/utils/metrics/mri';
+import {isCustomMeasurement as isCustomMeasurementName} from 'sentry/views/dashboards/utils';
 
 import {DateString, PageFilters} from '../../types/core';
 
@@ -484,6 +493,36 @@ export function groupByOp(metrics: MetricMeta[]): Record<string, MetricMeta[]> {
   return groupedByOp;
 }
 
+export function isMeasurement({mri}: {mri: MRI}) {
+  const {name} = parseMRI(mri) ?? {name: ''};
+  return isMeasurementName(name);
+}
+
+export function isCustomMeasurement({mri}: {mri: MRI}) {
+  const {name} = parseMRI(mri) ?? {name: ''};
+  return isCustomMeasurementName(name);
+}
+
+export function isStandardMeasurement({mri}: {mri: MRI}) {
+  return isMeasurement({mri}) && !isCustomMeasurement({mri});
+}
+
+export function isTransactionDuration({mri}: {mri: MRI}) {
+  return mri === 'd:transactions/duration@millisecond';
+}
+
+export function isCustomMetric({mri}: {mri: MRI}) {
+  return mri.includes(':custom/');
+}
+
+export function getFieldFromMetricsQuery(metricsQuery: MetricsQuery) {
+  if (isCustomMetric(metricsQuery)) {
+    return MRIToField(metricsQuery.mri, metricsQuery.op!);
+  }
+
+  return formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!));
+}
+
 // TODO(ddm): remove this and all of its usages once backend sends mri fields
 export function mapToMRIFields(
   data: MetricsApiResponse | undefined,
diff --git a/static/app/utils/metrics/mri.spec.tsx b/static/app/utils/metrics/mri.spec.tsx
index 3ed40983159153..bbcb7ca66d64ae 100644
--- a/static/app/utils/metrics/mri.spec.tsx
+++ b/static/app/utils/metrics/mri.spec.tsx
@@ -24,13 +24,27 @@ describe('parseMRI', () => {
     }
   );
 
-  it.each(['sessions', 'transactions', 'custom'])(
+  it.each(['transactions', 'custom'])(
     'should correctly parse a valid MRI string - use case %s',
     useCase => {
       const mri: MRI = `c:${useCase as UseCase}/xyz@test`;
       const parsedMRI = {
         type: 'c',
-        name: 'xyz',
+        name: `xyz`,
+        unit: 'test',
+        useCase,
+      };
+      expect(parseMRI(mri)).toEqual(parsedMRI);
+    }
+  );
+
+  it.each(['sessions', 'spans'])(
+    'should correctly parse a valid MRI string - use case %s',
+    useCase => {
+      const mri: MRI = `c:${useCase as UseCase}/xyz@test`;
+      const parsedMRI = {
+        type: 'c',
+        name: `${useCase}.xyz`,
         unit: 'test',
         useCase,
       };
diff --git a/static/app/utils/metrics/mri.tsx b/static/app/utils/metrics/mri.tsx
index 0741f33bfe328c..5b96b49a41f814 100644
--- a/static/app/utils/metrics/mri.tsx
+++ b/static/app/utils/metrics/mri.tsx
@@ -35,12 +35,25 @@ function _parseMRI(mri: MRI): ParsedMRI {
 
   return {
     type: metricType as MetricType,
-    name,
+    name: parseName(name, useCase as UseCase),
     unit,
     useCase: useCase as UseCase,
   };
 }
 
+function parseName(name: string, useCase: UseCase): string {
+  if (useCase === 'custom') {
+    return name;
+  }
+  if (useCase === 'transactions') {
+    if (name === 'duration') {
+      return `transaction.${name}`;
+    }
+    return name;
+  }
+  return `${useCase}.${name}`;
+}
+
 export function toMRI({type, useCase, name, unit}: ParsedMRI): MRI {
   return `${type}:${useCase}/${name}@${unit}`;
 }
diff --git a/static/app/utils/metrics/useMetricsCodeLocations.tsx b/static/app/utils/metrics/useMetricsCodeLocations.tsx
index 1f18f06e3c0251..25705438d62424 100644
--- a/static/app/utils/metrics/useMetricsCodeLocations.tsx
+++ b/static/app/utils/metrics/useMetricsCodeLocations.tsx
@@ -1,5 +1,3 @@
-import isArray from 'lodash/isArray';
-
 import {getDateTimeParams, MetricMetaCodeLocation} from 'sentry/utils/metrics';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -28,7 +26,11 @@ export function useMetricsCodeLocations(mri: string | undefined) {
     }
   );
 
-  if (!data || !isArray(data?.codeLocations) || !isArray(data?.codeLocations[0].frames)) {
+  if (
+    !data ||
+    !Array.isArray(data?.codeLocations) ||
+    !Array.isArray(data?.codeLocations[0]?.frames)
+  ) {
     return {data, isLoading};
   }
 
diff --git a/static/app/utils/metrics/useMetricsMeta.tsx b/static/app/utils/metrics/useMetricsMeta.tsx
index 200b17a91720ff..8070597d6642b5 100644
--- a/static/app/utils/metrics/useMetricsMeta.tsx
+++ b/static/app/utils/metrics/useMetricsMeta.tsx
@@ -1,83 +1,58 @@
-import {useMemo} from 'react';
-
 import {PageFilters} from 'sentry/types';
-import {ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient';
+import {useApiQuery, UseApiQueryOptions} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
 
 import {MetricMeta, UseCase} from '../../types/metrics';
 
-interface Options {
-  useCases?: UseCase[];
-}
-
 const DEFAULT_USE_CASES = ['sessions', 'transactions', 'custom', 'spans'];
 
-export function useMetricsMeta(
+function useMetaUseCase(
+  useCase: UseCase,
   projects: PageFilters['projects'],
-  options?: Options
-): {data: Record<string, MetricMeta>; isLoading: boolean} {
+  options: Omit<UseApiQueryOptions<MetricMeta[]>, 'staleTime'>
+) {
   const {slug} = useOrganization();
-  const enabledUseCases = options?.useCases ?? DEFAULT_USE_CASES;
 
-  const getKey = (useCase: UseCase): ApiQueryKey => {
-    return [
-      `/organizations/${slug}/metrics/meta/`,
-      {query: {useCase, project: projects}},
-    ];
-  };
+  const apiQueryResult = useApiQuery<MetricMeta[]>(
+    [`/organizations/${slug}/metrics/meta/`, {query: {useCase, project: projects}}],
+    {
+      ...options,
+      staleTime: Infinity,
+    }
+  );
 
-  const hasSessions = enabledUseCases.includes('sessions');
-  const hasTransactions = enabledUseCases.includes('transactions');
-  const hasCustom = enabledUseCases.includes('custom');
-  const hasSpans = enabledUseCases.includes('spans');
+  if (apiQueryResult.data && Array.isArray(apiQueryResult.data)) {
+    apiQueryResult.data = apiQueryResult.data.sort((a, b) => a.mri.localeCompare(b.mri));
+  }
 
-  const commonOptions = {
-    staleTime: Infinity,
-  };
+  return apiQueryResult;
+}
 
-  const sessionsMeta = useApiQuery<MetricMeta[]>(getKey('sessions'), {
-    ...commonOptions,
-    enabled: hasSessions,
+export function useMetricsMeta(
+  projects: PageFilters['projects'],
+  useCases?: UseCase[]
+): {data: MetricMeta[]; isLoading: boolean} {
+  const enabledUseCases = useCases ?? DEFAULT_USE_CASES;
+
+  const {data: sessionMeta = [], ...sessionsReq} = useMetaUseCase('sessions', projects, {
+    enabled: enabledUseCases.includes('sessions'),
   });
-  const txnsMeta = useApiQuery<MetricMeta[]>(getKey('transactions'), {
-    ...commonOptions,
-    enabled: hasTransactions,
+  const {data: txnsMeta = [], ...txnsReq} = useMetaUseCase('transactions', projects, {
+    enabled: enabledUseCases.includes('transactions'),
   });
-  const customMeta = useApiQuery<MetricMeta[]>(getKey('custom'), {
-    ...commonOptions,
-    enabled: hasCustom,
+  const {data: customMeta = [], ...customReq} = useMetaUseCase('custom', projects, {
+    enabled: enabledUseCases.includes('custom'),
   });
-  const spansMeta = useApiQuery<MetricMeta[]>(getKey('spans'), {
-    ...commonOptions,
-    enabled: hasSpans,
+  const {data: spansMeta = [], ...spansReq} = useMetaUseCase('spans', projects, {
+    enabled: enabledUseCases.includes('spans'),
   });
 
-  const combinedMeta = useMemo<Record<string, MetricMeta>>(() => {
-    return [
-      ...(hasSessions ? sessionsMeta.data ?? [] : []),
-      ...(hasTransactions ? txnsMeta.data ?? [] : []),
-      ...(hasCustom ? customMeta.data ?? [] : []),
-      ...(hasSpans ? spansMeta.data ?? [] : []),
-    ].reduce((acc, metricMeta) => {
-      return {...acc, [metricMeta.mri]: metricMeta};
-    }, {});
-  }, [
-    hasSessions,
-    sessionsMeta.data,
-    hasTransactions,
-    txnsMeta.data,
-    hasCustom,
-    customMeta.data,
-    hasSpans,
-    spansMeta.data,
-  ]);
-
   return {
-    data: combinedMeta,
+    data: [...sessionMeta, ...txnsMeta, ...customMeta, ...spansMeta],
     isLoading:
-      (sessionsMeta.isLoading && sessionsMeta.fetchStatus !== 'idle') ||
-      (txnsMeta.isLoading && txnsMeta.fetchStatus !== 'idle') ||
-      (customMeta.isLoading && customMeta.fetchStatus !== 'idle') ||
-      (spansMeta.isLoading && spansMeta.fetchStatus !== 'idle'),
+      (sessionsReq.isLoading && sessionsReq.fetchStatus !== 'idle') ||
+      (txnsReq.isLoading && txnsReq.fetchStatus !== 'idle') ||
+      (customReq.isLoading && customReq.fetchStatus !== 'idle') ||
+      (spansReq.isLoading && spansReq.fetchStatus !== 'idle'),
   };
 }
diff --git a/static/app/views/alerts/rules/metric/mriField.tsx b/static/app/views/alerts/rules/metric/mriField.tsx
index a793a9eeff3294..fc4437f28d0f0b 100644
--- a/static/app/views/alerts/rules/metric/mriField.tsx
+++ b/static/app/views/alerts/rules/metric/mriField.tsx
@@ -28,19 +28,16 @@ function filterAndSortOperations(operations: string[]) {
 }
 
 function MriField({aggregate, project, onChange}: Props) {
-  const {data: meta, isLoading} = useMetricsMeta([parseInt(project.id, 10)], {
-    useCases: ['custom'],
-  });
+  const {data: meta, isLoading} = useMetricsMeta([parseInt(project.id, 10)], ['custom']);
+
   const metaArr = useMemo(() => {
-    return Object.values(meta)
-      .map(
-        metric =>
-          ({
-            ...metric,
-            ...parseMRI(metric.mri),
-          }) as ParsedMRI & MetricMeta
-      )
-      .sort((a, b) => a.name.localeCompare(b.name));
+    return meta.map(
+      metric =>
+        ({
+          ...metric,
+          ...parseMRI(metric.mri),
+        }) as ParsedMRI & MetricMeta
+    );
   }, [meta]);
 
   const selectedValues = parseField(aggregate) ?? {mri: '' as MRI, op: ''};
diff --git a/static/app/views/ddm/codeLocations.tsx b/static/app/views/ddm/codeLocations.tsx
index 4bff6c18177997..96939827f01716 100644
--- a/static/app/views/ddm/codeLocations.tsx
+++ b/static/app/views/ddm/codeLocations.tsx
@@ -1,6 +1,5 @@
 import {useCallback, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
-import isArray from 'lodash/isArray';
 
 import {Button} from 'sentry/components/button';
 import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
@@ -39,12 +38,13 @@ export function CodeLocations({mri}: {mri: string}) {
   if (!hasDDMExperimentalFeature(organization)) {
     return null;
   }
-  const codeLocations = data?.codeLocations;
 
-  if (!codeLocations || !isArray(codeLocations) || codeLocations.length === 0) {
+  if (!Array.isArray(data?.codeLocations) || data?.codeLocations.length === 0) {
     return null;
   }
 
+  const codeLocations = data?.codeLocations ?? [];
+
   // We only want to show the first 5 code locations
   const reversedCodeLocations = codeLocations.slice(0, 5);
 
diff --git a/static/app/views/ddm/contextMenu.tsx b/static/app/views/ddm/contextMenu.tsx
index a278cd9dbafdfc..cf7fd8e877bd85 100644
--- a/static/app/views/ddm/contextMenu.tsx
+++ b/static/app/views/ddm/contextMenu.tsx
@@ -4,13 +4,18 @@ import {urlEncode} from '@sentry/utils';
 
 import {openAddToDashboardModal, openModal} from 'sentry/actionCreators/modal';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import {IconEllipsis} from 'sentry/icons';
+import {IconDashboard, IconEllipsis, IconSiren} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
-import {MetricDisplayType, MetricsQuery} from 'sentry/utils/metrics';
+import {
+  getFieldFromMetricsQuery,
+  isCustomMeasurement,
+  isCustomMetric,
+  MetricDisplayType,
+  MetricsQuery,
+} from 'sentry/utils/metrics';
 import {hasDDMFeature} from 'sentry/utils/metrics/features';
-import {MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
 import useOrganization from 'sentry/utils/useOrganization';
 import useRouter from 'sentry/utils/useRouter';
 import {DashboardWidgetSource, WidgetType} from 'sentry/views/dashboards/types';
@@ -25,7 +30,7 @@ type ContextMenuProps = {
 export function MetricWidgetContextMenu({metricsQuery, displayType}: ContextMenuProps) {
   const organization = useOrganization();
   const createAlert = useCreateAlert(organization, metricsQuery);
-  const handleAddQueryToDashboard = useHandleAddQueryToDashboard(
+  const createDashboardWidget = useCreateDashboardWidget(
     organization,
     metricsQuery,
     displayType
@@ -39,16 +44,18 @@ export function MetricWidgetContextMenu({metricsQuery, displayType}: ContextMenu
     <StyledDropdownMenuControl
       items={[
         {
+          leadingItems: [<IconSiren key="icon" />],
           key: 'add-alert',
           label: t('Create Alert'),
           disabled: !createAlert,
           onAction: createAlert,
         },
         {
+          leadingItems: [<IconDashboard key="icon" />],
           key: 'add-dashoard',
           label: t('Add to Dashboard'),
-          disabled: !handleAddQueryToDashboard,
-          onAction: handleAddQueryToDashboard,
+          disabled: !createDashboardWidget,
+          onAction: createDashboardWidget,
         },
       ]}
       triggerProps={{
@@ -63,50 +70,51 @@ export function MetricWidgetContextMenu({metricsQuery, displayType}: ContextMenu
   );
 }
 
-function useHandleAddQueryToDashboard(
+const StyledDropdownMenuControl = styled(DropdownMenu)`
+  margin: ${space(1)};
+`;
+
+export function useCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
+  return useMemo(() => {
+    if (
+      !metricsQuery.mri ||
+      !metricsQuery.op ||
+      isCustomMeasurement(metricsQuery) ||
+      !organization.access.includes('alerts:write')
+    ) {
+      return undefined;
+    }
+    return function () {
+      return openModal(deps => (
+        <OrganizationContext.Provider value={organization}>
+          <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
+        </OrganizationContext.Provider>
+      ));
+    };
+  }, [metricsQuery, organization]);
+}
+
+export function useCreateDashboardWidget(
   organization: Organization,
-  {projects, environments, datetime, op, mri, groupBy, query}: MetricsQuery,
+  metricsQuery: MetricsQuery,
   displayType?: MetricDisplayType
 ) {
   const router = useRouter();
-  const {start, end, period} = datetime;
+  const {projects, environments, datetime} = metricsQuery;
+  const isCustomMetricQuery = isCustomMetric(metricsQuery);
 
   return useMemo(() => {
-    if (!mri || !op) {
+    if (!metricsQuery.mri || !metricsQuery.op || isCustomMeasurement(metricsQuery)) {
       return undefined;
     }
 
-    const field = MRIToField(mri, op);
-    const limit = !groupBy?.length ? 1 : 10;
-
-    const widgetQuery = {
-      name: '',
-      aggregates: [field],
-      columns: groupBy ?? [],
-      fields: [field],
-      conditions: query ?? '',
-      orderby: '',
-    };
-
-    const urlWidgetQuery = urlEncode({
-      ...widgetQuery,
-      aggregates: field,
-      fields: field,
-      columns: groupBy?.join(',') ?? '',
-    });
-
-    const widgetAsQueryParams = {
-      source: DashboardWidgetSource.DDM,
-      start,
-      end,
-      statsPeriod: period,
-      defaultWidgetQuery: urlWidgetQuery,
-      defaultTableColumns: [],
-      defaultTitle: 'DDM Widget',
-      environment: environments,
-      displayType,
-      project: projects,
-    };
+    const widgetQuery = getWidgetQuery(metricsQuery);
+    const urlWidgetQuery = encodeWidgetQuery(widgetQuery);
+    const widgetAsQueryParams = getWidgetAsQueryParams(
+      metricsQuery,
+      urlWidgetQuery,
+      displayType
+    );
 
     return () =>
       openAddToDashboardModal({
@@ -119,8 +127,8 @@ function useHandleAddQueryToDashboard(
         widget: {
           title: 'DDM Widget',
           displayType,
-          widgetType: WidgetType.METRICS,
-          limit,
+          widgetType: isCustomMetricQuery ? WidgetType.METRICS : WidgetType.DISCOVER,
+          limit: !metricsQuery.groupBy?.length ? 1 : 10,
           queries: [widgetQuery],
         },
         router,
@@ -128,41 +136,57 @@ function useHandleAddQueryToDashboard(
         location: router.location,
       });
   }, [
+    isCustomMetricQuery,
+    metricsQuery,
     datetime,
     displayType,
-    end,
     environments,
-    groupBy,
-    mri,
-    op,
     organization,
-    period,
     projects,
-    query,
     router,
-    start,
   ]);
 }
 
-function useCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
-  return useMemo(() => {
-    if (
-      !metricsQuery.mri ||
-      !metricsQuery.op ||
-      parseMRI(metricsQuery.mri)?.useCase !== 'custom' ||
-      !organization.access.includes('alerts:write')
-    ) {
-      return undefined;
-    }
-    return () =>
-      openModal(deps => (
-        <OrganizationContext.Provider value={organization}>
-          <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
-        </OrganizationContext.Provider>
-      ));
-  }, [metricsQuery, organization]);
+function getWidgetQuery(metricsQuery: MetricsQuery) {
+  const field = getFieldFromMetricsQuery(metricsQuery);
+
+  return {
+    name: '',
+    aggregates: [field],
+    columns: metricsQuery.groupBy ?? [],
+    fields: [field],
+    conditions: metricsQuery.query ?? '',
+    orderby: '',
+  };
 }
 
-const StyledDropdownMenuControl = styled(DropdownMenu)`
-  margin: ${space(1)};
-`;
+function encodeWidgetQuery(query) {
+  return urlEncode({
+    ...query,
+    aggregates: query.aggregates.join(','),
+    fields: query.fields?.join(','),
+    columns: query.columns.join(','),
+  });
+}
+
+function getWidgetAsQueryParams(
+  metricsQuery: MetricsQuery,
+  urlWidgetQuery: string,
+  displayType?: MetricDisplayType
+) {
+  const {start, end, period} = metricsQuery.datetime;
+  const {projects} = metricsQuery;
+
+  return {
+    source: DashboardWidgetSource.DDM,
+    start,
+    end,
+    statsPeriod: period,
+    defaultWidgetQuery: urlWidgetQuery,
+    defaultTableColumns: [],
+    defaultTitle: 'DDM Widget',
+    environment: metricsQuery.environments,
+    displayType,
+    project: projects,
+  };
+}
diff --git a/static/app/views/ddm/createAlertModal.tsx b/static/app/views/ddm/createAlertModal.tsx
index a989aa1667615a..a48430b2cef50b 100644
--- a/static/app/views/ddm/createAlertModal.tsx
+++ b/static/app/views/ddm/createAlertModal.tsx
@@ -22,15 +22,11 @@ import {parsePeriodToHours, statsPeriodToDays} from 'sentry/utils/dates';
 import {
   formatMetricUsingFixedUnit,
   getDDMInterval,
+  getFieldFromMetricsQuery as getAlertAggregate,
   MetricDisplayType,
   MetricsQuery,
 } from 'sentry/utils/metrics';
-import {
-  formatMRIField,
-  getUseCaseFromMRI,
-  MRIToField,
-  parseMRI,
-} from 'sentry/utils/metrics/mri';
+import {formatMRIField, getUseCaseFromMRI, parseMRI} from 'sentry/utils/metrics/mri';
 import {useMetricsData} from 'sentry/utils/metrics/useMetricsData';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
@@ -45,10 +41,6 @@ import {
 import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
 import {getChartSeries} from 'sentry/views/ddm/widget';
 
-interface Props extends ModalRenderProps {
-  metricsQuery: MetricsQuery;
-}
-
 interface FormState {
   environment: string | null;
   project: string | null;
@@ -100,7 +92,7 @@ const TIME_WINDOWS_TO_CHECK = [
   TimeWindow.ONE_DAY,
 ];
 
-function getAlertInterval(metricsQuery, period: TimePeriod) {
+export function getAlertInterval(metricsQuery, period: TimePeriod) {
   const useCase = getUseCaseFromMRI(metricsQuery.mri) ?? 'custom';
   const interval = getDDMInterval(metricsQuery.datetime, useCase);
   const inMinutes = parsePeriodToHours(interval) * 60;
@@ -119,6 +111,35 @@ function getAlertInterval(metricsQuery, period: TimePeriod) {
   return toInterval(TimeWindow.ONE_HOUR);
 }
 
+export function getAlertFormPath({
+  organizationSlug,
+  interval,
+  statsPeriod,
+  environment,
+  project,
+  aggregate,
+  query,
+}) {
+  return `/organizations/${organizationSlug}/alerts/new/metric/?${qs.stringify({
+    // Needed, so alerts-create also collects environment via event view
+    createFromDiscover: true,
+    dataset: Dataset.GENERIC_METRICS,
+    eventTypes: EventTypes.TRANSACTION,
+    aggregate,
+    interval,
+    statsPeriod,
+    referrer: 'ddm',
+    // Event type also needs to be added to the query
+    query,
+    environment,
+    project,
+  })}`;
+}
+
+interface Props extends ModalRenderProps {
+  metricsQuery: MetricsQuery;
+}
+
 export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
   const router = useRouter();
   const organization = useOrganization();
@@ -136,6 +157,8 @@ export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
     [metricsQuery, alertPeriod]
   );
 
+  const aggregate = useMemo(() => getAlertAggregate(metricsQuery), [metricsQuery]);
+
   const {data, isLoading, refetch, isError} = useMetricsData(
     {
       mri: metricsQuery.mri,
@@ -213,28 +236,28 @@ export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
     router.push(
       `/organizations/${organization.slug}/alerts/new/metric/?${qs.stringify({
         // Needed, so alerts-create also collects environment via event view
+
+        aggregate,
+        query: `${metricsQuery.query} event.type:transaction`.trim(),
         createFromDiscover: true,
         dataset: Dataset.GENERIC_METRICS,
-        eventTypes: EventTypes.TRANSACTION,
-        aggregate: MRIToField(metricsQuery.mri, metricsQuery.op!),
         interval: alertInterval,
         statsPeriod: alertPeriod,
-        referrer: 'ddm',
-        // Event type also needs to be added to the query
-        query: `${metricsQuery.query}  event.type:transaction`.trim(),
         environment: formState.environment ?? undefined,
         project: selectedProject!.slug,
+        referrer: 'ddm',
+        // Event type also needs to be added to the query
+        eventTypes: EventTypes.TRANSACTION,
       })}`
     );
   }, [
+    router,
+    aggregate,
+    metricsQuery.query,
+    organization.slug,
     alertInterval,
     alertPeriod,
     formState.environment,
-    metricsQuery.mri,
-    metricsQuery.op,
-    metricsQuery.query,
-    organization.slug,
-    router,
     selectedProject,
   ]);
 
@@ -313,9 +336,7 @@ export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
                 <Tooltip
                   title={
                     <Fragment>
-                      <Filters>
-                        {formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!))}
-                      </Filters>
+                      <Filters>{formatMRIField(aggregate)}</Filters>
                       {metricsQuery.query}
                     </Fragment>
                   }
@@ -329,9 +350,7 @@ export function CreateAlertModal({Header, Body, Footer, metricsQuery}: Props) {
                   showOnlyOnOverflow
                 >
                   <QueryFilters>
-                    <Filters>
-                      {formatMRIField(MRIToField(metricsQuery.mri, metricsQuery.op!))}
-                    </Filters>
+                    <Filters>{formatMRIField(aggregate)}</Filters>
                     {metricsQuery.query}
                   </QueryFilters>
                 </Tooltip>
diff --git a/static/app/views/ddm/queryBuilder.tsx b/static/app/views/ddm/queryBuilder.tsx
index e1081b9a3eb33f..d876c79a19e4d9 100644
--- a/static/app/views/ddm/queryBuilder.tsx
+++ b/static/app/views/ddm/queryBuilder.tsx
@@ -9,11 +9,14 @@ import Tag from 'sentry/components/tag';
 import {IconLightning, IconReleases} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {MRI, SavedSearchType, TagCollection} from 'sentry/types';
+import {MetricMeta, MRI, SavedSearchType, TagCollection} from 'sentry/types';
 import {
   defaultMetricDisplayType,
   getReadableMetricType,
   isAllowedOp,
+  isCustomMetric,
+  isMeasurement,
+  isTransactionDuration,
   MetricDisplayType,
   MetricsQuery,
   MetricWidgetQueryParams,
@@ -34,6 +37,9 @@ type QueryBuilderProps = {
   powerUserMode?: boolean;
 };
 
+const isShownByDefault = (metric: MetricMeta) =>
+  isMeasurement(metric) || isCustomMetric(metric) || isTransactionDuration(metric);
+
 export function QueryBuilder({
   metricsQuery,
   projects,
@@ -54,30 +60,29 @@ export function QueryBuilder({
 
   const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects);
 
-  const metaArr = useMemo(() => {
+  const displayedMetrics = useMemo(() => {
     if (mriMode) {
-      return Object.values(meta);
+      return meta;
     }
 
-    return Object.values(meta).filter(
-      metric => metric.mri.includes(':custom/') || metric.mri === metricsQuery.mri
-    );
+    const isSelected = (metric: MetricMeta) => metric.mri === metricsQuery.mri;
+    return meta.filter(metric => isShownByDefault(metric) || isSelected(metric));
   }, [meta, metricsQuery.mri, mriMode]);
 
+  const selectedMeta = useMemo(() => {
+    return meta.find(metric => metric.mri === metricsQuery.mri);
+  }, [meta, metricsQuery.mri]);
+
   // Reset the query data if the selected metric is no longer available
   useEffect(() => {
     if (
       metricsQuery.mri &&
       !isMetaLoading &&
-      !metaArr.find(metric => metric.mri === metricsQuery.mri)
+      !displayedMetrics.find(metric => metric.mri === metricsQuery.mri)
     ) {
       onChange({mri: '' as MRI, op: '', groupBy: []});
     }
-  }, [isMetaLoading, metaArr, metricsQuery.mri, onChange]);
-
-  if (!meta) {
-    return null;
-  }
+  }, [isMetaLoading, displayedMetrics, metricsQuery.mri, onChange]);
 
   return (
     <QueryBuilderWrapper>
@@ -87,7 +92,7 @@ export function QueryBuilder({
             searchable
             sizeLimit={100}
             triggerProps={{prefix: t('Metric'), size: 'sm'}}
-            options={metaArr.map(metric => ({
+            options={displayedMetrics.map(metric => ({
               label: mriMode ? metric.mri : formatMRI(metric.mri),
               value: metric.mri,
               trailingItems: mriMode ? undefined : (
@@ -99,11 +104,15 @@ export function QueryBuilder({
             }))}
             value={metricsQuery.mri}
             onChange={option => {
-              const availableOps = meta[option.value]?.operations.filter(isAllowedOp);
+              const availableOps =
+                meta
+                  .find(metric => metric.mri === option.value)
+                  ?.operations.filter(isAllowedOp) ?? [];
+
               // @ts-expect-error .op is an operation
               const selectedOp = availableOps.includes(metricsQuery.op ?? '')
                 ? metricsQuery.op
-                : availableOps[0];
+                : availableOps?.[0];
               onChange({
                 mri: option.value,
                 op: selectedOp,
@@ -116,7 +125,7 @@ export function QueryBuilder({
           <CompactSelect
             triggerProps={{prefix: t('Op'), size: 'sm'}}
             options={
-              meta[metricsQuery.mri]?.operations.filter(isAllowedOp).map(op => ({
+              selectedMeta?.operations.filter(isAllowedOp).map(op => ({
                 label: op,
                 value: op,
               })) ?? []


Code review comments:
diff_hunk: @@ -119,6 +111,35 @@ function getAlertInterval(metricsQuery, period: TimePeriod) {
   return toInterval(TimeWindow.ONE_HOUR);
 }
 
+export function getAlertFormPath({
+  organizationSlug,
+  interval,
+  statsPeriod,
+  environment,
+  project,
+  aggregate,
+  query,
+}) {
+  return `/organizations/${organizationSlug}/alerts/new/metric/?${qs.stringify({
+    // Needed, so alerts-create also collects environment via event view
+    createFromDiscover: true,
+    dataset: Dataset.GENERIC_METRICS,
+    eventTypes: EventTypes.TRANSACTION,
+    aggregate,
+    interval,
+    statsPeriod,
+    referrer: 'ddm',
+    // Event type also needs to be added to the query
+    query,
path: static/app/views/ddm/createAlertModal.tsx
text: @user1:
Does it now work without? 🤔 
btw that comment disappeared in the other place where the url is constructed.
Maybe a common util would be nice so we keep the weirdness in one place?

@author:
It actually does, removed it