diff --git a/static/app/utils/analytics/alertsAnalyticsEvents.tsx b/static/app/utils/analytics/alertsAnalyticsEvents.tsx index 5fe86d0ce6b1de..85ab925ef3c46e 100644 --- a/static/app/utils/analytics/alertsAnalyticsEvents.tsx +++ b/static/app/utils/analytics/alertsAnalyticsEvents.tsx @@ -1,12 +1,31 @@ +import type {MetricRule} from 'sentry/views/alerts/rules/metric/types'; +import type {UptimeMonitorMode} from 'sentry/views/alerts/rules/uptime/types'; +import type {MonitorConfig} from 'sentry/views/insights/crons/types'; + export type AlertsEventParameters = { 'anomaly-detection.feedback-submitted': { choice_selected: boolean; incident_id: string; }; + 'cron_monitor.created': { + cron_schedule_type: MonitorConfig['schedule_type']; + }; + 'issue_alert_rule.created': Record; + 'metric_alert_rule.created': { + aggregate: MetricRule['aggregate']; + dataset: MetricRule['dataset']; + }; + 'uptime_monitor.created': { + uptime_mode: UptimeMonitorMode; + }; }; type AlertsEventKey = keyof AlertsEventParameters; export const alertsEventMap: Record = { 'anomaly-detection.feedback-submitted': 'Anomaly Detection Feedback Submitted', + 'issue_alert_rule.created': 'Issue Alert Rule Created', + 'metric_alert_rule.created': 'Metric Alert Rule Created', + 'cron_monitor.created': 'Cron Monitor Created', + 'uptime_monitor.created': 'Uptime Monitor Created', }; diff --git a/static/app/utils/analytics/monitorsAnalyticsEvents.tsx b/static/app/utils/analytics/monitorsAnalyticsEvents.tsx index 15b1ebc5ad7088..2eb5b95e74cf33 100644 --- a/static/app/utils/analytics/monitorsAnalyticsEvents.tsx +++ b/static/app/utils/analytics/monitorsAnalyticsEvents.tsx @@ -6,9 +6,16 @@ type DetectorAnalyticsEventPayload = ReturnType; +type DetectorCreateAnalyticsEventPayload = + | (DetectorAnalyticsEventPayload & { + success: true; + }) + | {detector_type: string; success: false}; + export type MonitorsEventParameters = { 'automation.created': AutomationAnalyticsEventPayload & { organization: Organization; + success: boolean; }; 'automation.updated': AutomationAnalyticsEventPayload & { organization: Organization; @@ -17,7 +24,7 @@ export type MonitorsEventParameters = { guide: string; platform: string; }; - 'monitor.created': DetectorAnalyticsEventPayload; + 'monitor.created': DetectorCreateAnalyticsEventPayload; 'monitor.updated': DetectorAnalyticsEventPayload; }; diff --git a/static/app/views/alerts/create.tsx b/static/app/views/alerts/create.tsx index ed8dfa847f42c2..e14df5d6adc33d 100644 --- a/static/app/views/alerts/create.tsx +++ b/static/app/views/alerts/create.tsx @@ -7,6 +7,7 @@ import {t} from 'sentry/locale'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import type {Member, Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; +import {trackAnalytics} from 'sentry/utils/analytics'; import EventView from 'sentry/utils/discover/eventView'; import {uniqueId} from 'sentry/utils/guid'; import {decodeScalar} from 'sentry/utils/queryString'; @@ -166,14 +167,18 @@ function Create(props: Props) { + onSubmitSuccess={(data: Monitor) => { + trackAnalytics('cron_monitor.created', { + organization, + cron_schedule_type: data.config.schedule_type, + }); navigate( makeAlertsPathname({ path: `/rules/crons/${data.project.slug}/${data.slug}/details/`, organization, }) - ) - } + ); + }} submitLabel={t('Create')} /> ) : !hasMetricAlerts || alertType === AlertRuleType.ISSUE ? ( diff --git a/static/app/views/alerts/rules/issue/index.tsx b/static/app/views/alerts/rules/issue/index.tsx index 6183abac70df0c..8d19ade8811ff7 100644 --- a/static/app/views/alerts/rules/issue/index.tsx +++ b/static/app/views/alerts/rules/issue/index.tsx @@ -457,6 +457,12 @@ class IssueRuleEditor extends DeprecatedAsyncComponent { metric.endSpan({name: 'saveAlertRule'}); + if (isNew) { + trackAnalytics('issue_alert_rule.created', { + organization, + }); + } + router.push( makeAlertsPathname({ path: `/rules/${project.slug}/${rule.id}/details/`, diff --git a/static/app/views/alerts/rules/metric/ruleForm.tsx b/static/app/views/alerts/rules/metric/ruleForm.tsx index b652695285638e..19df33de9230b3 100644 --- a/static/app/views/alerts/rules/metric/ruleForm.tsx +++ b/static/app/views/alerts/rules/metric/ruleForm.tsx @@ -370,6 +370,15 @@ class RuleFormContainer extends DeprecatedAsyncComponent { } if (alertRule) { addSuccessMessage(ruleId ? t('Updated alert rule') : t('Created alert rule')); + + if (!ruleId) { + trackAnalytics('metric_alert_rule.created', { + organization, + aggregate: alertRule.aggregate, + dataset: alertRule.dataset, + }); + } + if (onSubmitSuccess) { onSubmitSuccess(alertRule, model); } @@ -849,6 +858,15 @@ class RuleFormContainer extends DeprecatedAsyncComponent { IndicatorStore.remove(loadingIndicator); this.setState({loading: false}); addSuccessMessage(ruleId ? t('Updated alert rule') : t('Created alert rule')); + + if (!ruleId) { + trackAnalytics('metric_alert_rule.created', { + organization, + aggregate: data.aggregate, + dataset: data.dataset, + }); + } + if (onSubmitSuccess) { onSubmitSuccess(data, model); } diff --git a/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx b/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx index bc1a6ac93fe543..3a5c7f59a3c930 100644 --- a/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx +++ b/static/app/views/alerts/rules/uptime/uptimeAlertForm.tsx @@ -26,6 +26,7 @@ import ListItem from 'sentry/components/list/listItem'; import Panel from 'sentry/components/panels/panel'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import {trackAnalytics} from 'sentry/utils/analytics'; import getDuration from 'sentry/utils/duration/getDuration'; import {useQueryClient} from 'sentry/utils/queryClient'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -129,6 +130,14 @@ export function UptimeAlertForm({handleDelete, rule}: Props) { exact: true, }); } + + if (!rule) { + trackAnalytics('uptime_monitor.created', { + organization, + uptime_mode: response.mode, + }); + } + navigate( makeAlertsPathname({ path: `/rules/uptime/${projectSlug}/${response.id}/details/`, diff --git a/static/app/views/automations/components/forms/common/getAutomationAnalyticsPayload.ts b/static/app/views/automations/components/forms/common/getAutomationAnalyticsPayload.ts index 81d9eedc2e2b3c..97ad94837a5adb 100644 --- a/static/app/views/automations/components/forms/common/getAutomationAnalyticsPayload.ts +++ b/static/app/views/automations/components/forms/common/getAutomationAnalyticsPayload.ts @@ -1,6 +1,6 @@ -import type {Automation} from 'sentry/types/workflowEngine/automations'; +import type {NewAutomation} from 'sentry/types/workflowEngine/automations'; -export function getAutomationAnalyticsPayload(automation: Automation): { +export function getAutomationAnalyticsPayload(automation: NewAutomation): { actions_count: number; detectors_count: number; environment: string | null; diff --git a/static/app/views/automations/new.spec.tsx b/static/app/views/automations/new.spec.tsx index bbb877eb5650c7..866ba62af55b61 100644 --- a/static/app/views/automations/new.spec.tsx +++ b/static/app/views/automations/new.spec.tsx @@ -28,6 +28,7 @@ describe('AutomationNewSettings', () => { }); beforeEach(() => { + jest.clearAllMocks(); MockApiClient.clearMockResponses(); // Available actions (include Slack with a default integration) @@ -205,15 +206,14 @@ describe('AutomationNewSettings', () => { ); // Verify analytics was called with correct event and payload structure - await waitFor(() => { - expect(trackAnalytics).toHaveBeenCalledWith('automation.created', { - organization, - frequency_minutes: expect.any(Number), - environment: expect.anything(), - detectors_count: expect.any(Number), - trigger_conditions_count: expect.any(Number), - actions_count: expect.any(Number), - }); + expect(trackAnalytics).toHaveBeenCalledWith('automation.created', { + organization, + frequency_minutes: expect.any(Number), + environment: null, + detectors_count: expect.any(Number), + trigger_conditions_count: expect.any(Number), + success: true, + actions_count: expect.any(Number), }); }); }); diff --git a/static/app/views/automations/new.tsx b/static/app/views/automations/new.tsx index 1b309cb2cca0da..d87c7675d7a0cd 100644 --- a/static/app/views/automations/new.tsx +++ b/static/app/views/automations/new.tsx @@ -108,16 +108,30 @@ export default function AutomationNewSettings() { async (data, _, __, ___, ____) => { const errors = validateAutomationBuilderState(state); setAutomationBuilderErrors(errors); + const newAutomationData = getNewAutomationData(data as AutomationFormData, state); if (Object.keys(errors).length === 0) { - const automation = await createAutomation( - getNewAutomationData(data as AutomationFormData, state) - ); + try { + const automation = await createAutomation(newAutomationData); + trackAnalytics('automation.created', { + organization, + ...getAutomationAnalyticsPayload(newAutomationData), + success: true, + }); + navigate(makeAutomationDetailsPathname(organization.slug, automation.id)); + } catch { + trackAnalytics('automation.created', { + organization, + ...getAutomationAnalyticsPayload(newAutomationData), + success: false, + }); + } + } else { trackAnalytics('automation.created', { organization, - ...getAutomationAnalyticsPayload(automation), + ...getAutomationAnalyticsPayload(newAutomationData), + success: false, }); - navigate(makeAutomationDetailsPathname(organization.slug, automation.id)); } }, [createAutomation, state, navigate, organization] diff --git a/static/app/views/detectors/components/forms/newDetectorLayout.tsx b/static/app/views/detectors/components/forms/newDetectorLayout.tsx index 8e13e34e946578..8a78906687bcd6 100644 --- a/static/app/views/detectors/components/forms/newDetectorLayout.tsx +++ b/static/app/views/detectors/components/forms/newDetectorLayout.tsx @@ -44,6 +44,7 @@ export function NewDetectorLayout< const maxWidth = theme.breakpoints.xl; const formSubmitHandler = useCreateDetectorFormSubmit({ + detectorType, formDataToEndpointPayload, }); diff --git a/static/app/views/detectors/hooks/useCreateDetectorFormSubmit.tsx b/static/app/views/detectors/hooks/useCreateDetectorFormSubmit.tsx index b335ca7459937a..74949a525e6127 100644 --- a/static/app/views/detectors/hooks/useCreateDetectorFormSubmit.tsx +++ b/static/app/views/detectors/hooks/useCreateDetectorFormSubmit.tsx @@ -6,6 +6,7 @@ import {t} from 'sentry/locale'; import type { BaseDetectorUpdatePayload, Detector, + DetectorType, } from 'sentry/types/workflowEngine/detectors'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -15,6 +16,10 @@ import {useCreateDetector} from 'sentry/views/detectors/hooks'; import {makeMonitorDetailsPathname} from 'sentry/views/detectors/pathnames'; interface UseCreateDetectorFormSubmitOptions { + /** + * Detector type for analytics tracking when validation fails + */ + detectorType: DetectorType; /** * Function to transform form data to API payload */ @@ -27,6 +32,7 @@ export function useCreateDetectorFormSubmit< TFormData extends Data, TUpdatePayload extends BaseDetectorUpdatePayload, >({ + detectorType, formDataToEndpointPayload, onError, onSuccess, @@ -39,16 +45,23 @@ export function useCreateDetectorFormSubmit< async (data, onSubmitSuccess, onSubmitError, _, formModel) => { const isValid = formModel.validateForm(); if (!isValid) { + trackAnalytics('monitor.created', { + organization, + detector_type: detectorType, + success: false, + }); return; } + const payload = formDataToEndpointPayload(data as TFormData); + try { - const payload = formDataToEndpointPayload(data as TFormData); const resultDetector = await createDetector(payload); trackAnalytics('monitor.created', { organization, ...getDetectorAnalyticsPayload(resultDetector), + success: true, }); addSuccessMessage(t('Monitor created successfully')); @@ -61,6 +74,12 @@ export function useCreateDetectorFormSubmit< onSubmitSuccess?.(resultDetector); } catch (error) { + trackAnalytics('monitor.created', { + organization, + detector_type: payload.type, + success: false, + }); + addErrorMessage(t('Unable to create monitor')); if (onError) { @@ -71,6 +90,7 @@ export function useCreateDetectorFormSubmit< } }, [ + detectorType, formDataToEndpointPayload, createDetector, organization,