Browse Source

feat(alerts): Adds a create alert button to explore chart (#80268)

Adds a button to create and Alert off of an explore chart, behind
`alerts-eap` feature flag.
edwardgou-sentry 4 months ago
parent
commit
d3cb08e460

+ 1 - 1
static/app/views/alerts/rules/metric/constants.tsx

@@ -187,7 +187,7 @@ export function createDefaultRule(
   };
 }
 
-function getAlertTimeWindow(period: string | undefined): TimeWindow | undefined {
+export function getAlertTimeWindow(period: string | undefined): TimeWindow | undefined {
   if (!period) {
     return undefined;
   }

+ 3 - 0
static/app/views/alerts/rules/metric/create.tsx

@@ -9,6 +9,7 @@ import {
   createDefaultRule,
   createRuleFromEventView,
   createRuleFromWizardTemplate,
+  getAlertTimeWindow,
 } from 'sentry/views/alerts/rules/metric/constants';
 import type {WizardRuleTemplate} from 'sentry/views/alerts/wizard/options';
 
@@ -69,6 +70,8 @@ function MetricRulesCreate(props: Props) {
   const defaultOwnerId = userTeamIds.find(id => projectTeamIds.has(id)) ?? null;
   defaultRule.owner = defaultOwnerId && `team:${defaultOwnerId}`;
   const environment = decodeScalar(location?.query?.environment) ?? null;
+  const interval = decodeScalar(location?.query?.interval) ?? undefined;
+  defaultRule.timeWindow = getAlertTimeWindow(interval) ?? defaultRule.timeWindow;
 
   return (
     <RuleForm

+ 58 - 2
static/app/views/explore/charts/index.tsx

@@ -1,11 +1,13 @@
 import {Fragment, useCallback, useMemo} from 'react';
 import styled from '@emotion/styled';
 
+import Feature from 'sentry/components/acl/feature';
 import {getInterval} from 'sentry/components/charts/utils';
 import {CompactSelect} from 'sentry/components/compactSelect';
+import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import {Tooltip} from 'sentry/components/tooltip';
 import {CHART_PALETTE} from 'sentry/constants/chartPalette';
-import {IconClock, IconGraph} from 'sentry/icons';
+import {IconClock, IconGraph, IconSubscribed} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {dedupeArray} from 'sentry/utils/dedupeArray';
@@ -15,8 +17,11 @@ import {
   parseFunction,
 } from 'sentry/utils/discover/fields';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjects from 'sentry/utils/useProjects';
 import {formatVersion} from 'sentry/utils/versions/formatVersion';
+import {Dataset} from 'sentry/views/alerts/rules/metric/types';
 import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
 import {useDataset} from 'sentry/views/explore/hooks/useDataset';
 import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
@@ -26,6 +31,7 @@ import Chart, {
 } from 'sentry/views/insights/common/components/chart';
 import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
 import {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
+import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl';
 import {CHART_HEIGHT} from 'sentry/views/insights/database/settings';
 
 import {useGroupBys} from '../hooks/useGroupBys';
@@ -58,6 +64,8 @@ export const EXPLORE_CHART_GROUP = 'explore-charts_group';
 // TODO: Update to support aggregate mode and multiple queries / visualizations
 export function ExploreCharts({query}: ExploreChartsProps) {
   const pageFilters = usePageFilters();
+  const organization = useOrganization();
+  const {projects} = useProjects();
 
   const [dataset] = useDataset();
   const [visualizes, setVisualizes] = useVisualizes();
@@ -141,7 +149,7 @@ export function ExploreCharts({query}: ExploreChartsProps) {
           })
           .filter(Boolean);
 
-        const {chartType} = visualize;
+        const {chartType, yAxes: visualizeYAxes} = visualize;
         const chartIcon =
           chartType === ChartType.LINE
             ? 'line'
@@ -149,6 +157,29 @@ export function ExploreCharts({query}: ExploreChartsProps) {
               ? 'area'
               : 'bar';
 
+        const project =
+          projects.length === 1
+            ? projects[0]
+            : projects.find(p => p.id === `${pageFilters.selection.projects[0]}`);
+        const singleProject =
+          (pageFilters.selection.projects.length === 1 || projects.length === 1) &&
+          project;
+        const alertsUrls = singleProject
+          ? visualizeYAxes.map(yAxis => ({
+              key: yAxis,
+              label: yAxis,
+              to: getAlertsUrl({
+                project,
+                query,
+                pageFilters: pageFilters.selection,
+                aggregate: yAxis,
+                orgSlug: organization.slug,
+                dataset: Dataset.EVENTS_ANALYTICS_PLATFORM,
+                interval,
+              }),
+            }))
+          : undefined;
+
         return (
           <ChartContainer key={index}>
             <ChartPanel>
@@ -189,6 +220,31 @@ export function ExploreCharts({query}: ExploreChartsProps) {
                       options={intervalOptions}
                     />
                   </Tooltip>
+                  <Feature features="organizations:alerts-eap">
+                    <Tooltip
+                      title={
+                        singleProject
+                          ? t('Create an alert for this chart')
+                          : t(
+                              'Cannot create an alert when multiple projects are selected'
+                            )
+                      }
+                    >
+                      <DropdownMenu
+                        triggerProps={{
+                          'aria-label': t('Create Alert'),
+                          size: 'sm',
+                          borderless: true,
+                          showChevron: false,
+                          icon: <IconSubscribed />,
+                        }}
+                        position="bottom-end"
+                        items={alertsUrls ?? []}
+                        menuTitle={t('Create an alert for')}
+                        isDisabled={!alertsUrls || alertsUrls.length === 0}
+                      />
+                    </Tooltip>
+                  </Feature>
                 </ChartSettingsContainer>
               </ChartHeader>
               <Chart

+ 17 - 0
static/app/views/insights/common/utils/getAlertsUrl.spec.tsx

@@ -2,6 +2,7 @@ import {PageFiltersFixture} from 'sentry-fixture/pageFilters';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
 
+import {Dataset} from 'sentry/views/alerts/rules/metric/types';
 import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl';
 
 describe('getAlertsUrl', function () {
@@ -22,4 +23,20 @@ describe('getAlertsUrl', function () {
       '/organizations/orgSlug/alerts/new/metric/?aggregate=avg%28d%3Aspans%2Fduration%40millisecond%29&dataset=generic_metrics&eventTypes=transaction&project=project-slug&query=span.module%3Adb&statsPeriod=7d'
     );
   });
+  it('should return a url to an EAP alert rule', function () {
+    const aggregate = 'count(span.duration)';
+    const query = 'span.op:http.client';
+    const orgSlug = 'orgSlug';
+    const url = getAlertsUrl({
+      project,
+      aggregate,
+      query,
+      orgSlug,
+      pageFilters,
+      dataset: Dataset.EVENTS_ANALYTICS_PLATFORM,
+    });
+    expect(url).toEqual(
+      '/organizations/orgSlug/alerts/new/metric/?aggregate=count%28span.duration%29&dataset=events_analytics_platform&eventTypes=transaction&project=project-slug&query=span.op%3Ahttp.client&statsPeriod=7d'
+    );
+  });
 });

+ 6 - 1
static/app/views/insights/common/utils/getAlertsUrl.tsx

@@ -12,11 +12,15 @@ export function getAlertsUrl({
   orgSlug,
   pageFilters,
   name,
+  interval,
+  dataset = Dataset.GENERIC_METRICS,
 }: {
   aggregate: string;
   orgSlug: string;
   pageFilters: PageFilters;
   project: Project;
+  dataset?: Dataset;
+  interval?: string;
   name?: string;
   query?: string;
 }) {
@@ -24,13 +28,14 @@ export function getAlertsUrl({
   const environment = pageFilters.environments;
   const queryParams = {
     aggregate: aggregate,
-    dataset: Dataset.GENERIC_METRICS,
+    dataset,
     project: project.slug,
     eventTypes: 'transaction',
     query,
     statsPeriod,
     environment,
     name,
+    interval,
   };
   return normalizeUrl(
     `/organizations/${orgSlug}/alerts/new/metric/?${qs.stringify(queryParams)}`