Просмотр исходного кода

feat(widget-builder): Add existing analytics to new builder (#83949)

Takes the old analytics and adds them to the new widget builder. The
changes are:

1. Add a prop to each of the 3 existing analytics events to
differentiate old vs new design (I called them page vs slideout because
numbered versioning might get kind of ambiguous in the future)
2. Move the title and description analytics to `onBlur` or else they
emit an event on each keystroke in the old builder
3. Add events to the new builder
- We only have open, save, and field change events being emitted for
title, description, and dataset changes. I added display type.

These events also take a `source` but I haven't implemented `source` at
all so I'll do that next so we can split up flows by dashboard,
discover, or trace explorer
Nar Saynorath 1 месяц назад
Родитель
Сommit
c513feaccb

+ 8 - 0
static/app/utils/analytics/dashboardsAnalyticsEvents.tsx

@@ -1,8 +1,14 @@
 import type {DashboardsLayout} from 'sentry/views/dashboards/manage';
 
+export enum WidgetBuilderVersion {
+  PAGE = 'page',
+  SLIDEOUT = 'slideout',
+}
+
 // Used in the full-page widget builder
 type DashboardsEventParametersWidgetBuilder = {
   'dashboards_views.widget_builder.change': {
+    builder_version: WidgetBuilderVersion;
     field: string;
     from: string;
     new_widget: boolean;
@@ -10,9 +16,11 @@ type DashboardsEventParametersWidgetBuilder = {
     widget_type: string;
   };
   'dashboards_views.widget_builder.opened': {
+    builder_version: WidgetBuilderVersion;
     new_widget: boolean;
   };
   'dashboards_views.widget_builder.save': {
+    builder_version: WidgetBuilderVersion;
     data_set: string;
     new_widget: boolean;
   };

+ 1 - 0
static/app/views/dashboards/types.tsx

@@ -174,4 +174,5 @@ export enum DashboardWidgetSource {
   DASHBOARDS = 'dashboards',
   LIBRARY = 'library',
   ISSUE_DETAILS = 'issueDetail',
+  TRACE_EXPLORER = 'traceExplorer',
 }

+ 18 - 3
static/app/views/dashboards/widgetBuilder/components/datasetSelector.tsx

@@ -6,15 +6,21 @@ import RadioGroup, {type RadioOption} from 'sentry/components/forms/controls/rad
 import ExternalLink from 'sentry/components/links/externalLink';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
 import useOrganization from 'sentry/utils/useOrganization';
 import {WidgetType} from 'sentry/views/dashboards/types';
 import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
 import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
+import useDashboardWidgetSource from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
+import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
 import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
 
 function WidgetBuilderDatasetSelector() {
   const organization = useOrganization();
   const {state, dispatch} = useWidgetBuilderContext();
+  const source = useDashboardWidgetSource();
+  const isEditing = useIsEditingWidget();
 
   const datasetChoices: Array<RadioOption<WidgetType>> = [];
   datasetChoices.push([WidgetType.ERRORS, t('Errors')]);
@@ -52,12 +58,21 @@ function WidgetBuilderDatasetSelector() {
         label={t('Dataset')}
         value={state.dataset ?? WidgetType.ERRORS}
         choices={datasetChoices}
-        onChange={(newValue: WidgetType) =>
+        onChange={(newValue: WidgetType) => {
           dispatch({
             type: BuilderStateAction.SET_DATASET,
             payload: newValue,
-          })
-        }
+          });
+          trackAnalytics('dashboards_views.widget_builder.change', {
+            from: source,
+            widget_type: state.dataset ?? '',
+            builder_version: WidgetBuilderVersion.SLIDEOUT,
+            field: 'dataSet',
+            value: newValue,
+            new_widget: !isEditing,
+            organization,
+          });
+        }}
       />
     </Fragment>
   );

+ 30 - 0
static/app/views/dashboards/widgetBuilder/components/nameAndDescFields.tsx

@@ -6,8 +6,13 @@ import TextArea from 'sentry/components/forms/controls/textarea';
 import TextField from 'sentry/components/forms/fields/textField';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
+import useOrganization from 'sentry/utils/useOrganization';
 import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
 import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
+import useDashboardWidgetSource from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
+import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
 import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
 
 interface WidgetBuilderNameAndDescriptionProps {
@@ -19,8 +24,11 @@ function WidgetBuilderNameAndDescription({
   error,
   setError,
 }: WidgetBuilderNameAndDescriptionProps) {
+  const organization = useOrganization();
   const {state, dispatch} = useWidgetBuilderContext();
   const [isDescSelected, setIsDescSelected] = useState(state.description ? true : false);
+  const isEditing = useIsEditingWidget();
+  const source = useDashboardWidgetSource();
 
   return (
     <Fragment>
@@ -37,6 +45,17 @@ function WidgetBuilderNameAndDescription({
           setError?.({...error, title: undefined});
           dispatch({type: BuilderStateAction.SET_TITLE, payload: newTitle});
         }}
+        onBlur={() => {
+          trackAnalytics('dashboards_views.widget_builder.change', {
+            from: source,
+            widget_type: state.dataset ?? '',
+            builder_version: WidgetBuilderVersion.SLIDEOUT,
+            field: 'title',
+            value: state.title ?? '',
+            new_widget: !isEditing,
+            organization,
+          });
+        }}
         required
         error={error?.title}
         inline={false}
@@ -63,6 +82,17 @@ function WidgetBuilderNameAndDescription({
           onChange={e => {
             dispatch({type: BuilderStateAction.SET_DESCRIPTION, payload: e.target.value});
           }}
+          onBlur={() => {
+            trackAnalytics('dashboards_views.widget_builder.change', {
+              from: source,
+              widget_type: state.dataset ?? '',
+              builder_version: WidgetBuilderVersion.SLIDEOUT,
+              field: 'description',
+              value: state.description ?? '',
+              new_widget: !isEditing,
+              organization,
+            });
+          }}
         />
       )}
     </Fragment>

+ 9 - 1
static/app/views/dashboards/widgetBuilder/components/saveButton.tsx

@@ -8,6 +8,8 @@ import {
 } from 'sentry/actionCreators/indicator';
 import {Button} from 'sentry/components/button';
 import {t} from 'sentry/locale';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
@@ -29,6 +31,12 @@ function SaveButton({isEditing, onSave, setError}: SaveButtonProps) {
   const [isSaving, setIsSaving] = useState(false);
 
   const handleSave = useCallback(async () => {
+    trackAnalytics('dashboards_views.widget_builder.save', {
+      builder_version: WidgetBuilderVersion.SLIDEOUT,
+      data_set: state.dataset ?? '',
+      new_widget: !isEditing,
+      organization: organization.slug,
+    });
     const widget = convertBuilderStateToWidget(state);
     setIsSaving(true);
     try {
@@ -42,7 +50,7 @@ function SaveButton({isEditing, onSave, setError}: SaveButtonProps) {
       setError(errorDetails);
       addErrorMessage(t('Unable to save widget'));
     }
-  }, [api, onSave, organization.slug, state, widgetIndex, setError]);
+  }, [api, onSave, organization.slug, state, widgetIndex, setError, isEditing]);
 
   return (
     <Button priority="primary" onClick={handleSave} busy={isSaving}>

+ 17 - 0
static/app/views/dashboards/widgetBuilder/components/typeSelector.tsx

@@ -7,10 +7,15 @@ import FieldGroup from 'sentry/components/forms/fieldGroup';
 import {IconGraph, IconNumber, IconTable} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
+import useOrganization from 'sentry/utils/useOrganization';
 import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
 import {DisplayType} from 'sentry/views/dashboards/types';
 import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
 import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
+import useDashboardWidgetSource from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
+import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
 import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
 
 const typeIcons = {
@@ -37,6 +42,9 @@ interface WidgetBuilderTypeSelectorProps {
 function WidgetBuilderTypeSelector({error, setError}: WidgetBuilderTypeSelectorProps) {
   const {state, dispatch} = useWidgetBuilderContext();
   const config = getDatasetConfig(state.dataset);
+  const source = useDashboardWidgetSource();
+  const isEditing = useIsEditingWidget();
+  const organization = useOrganization();
 
   return (
     <Fragment>
@@ -81,6 +89,15 @@ function WidgetBuilderTypeSelector({error, setError}: WidgetBuilderTypeSelectorP
                 payload: [state.query[0]!],
               });
             }
+            trackAnalytics('dashboards_views.widget_builder.change', {
+              from: source,
+              widget_type: state.dataset ?? '',
+              builder_version: WidgetBuilderVersion.SLIDEOUT,
+              field: 'displayType',
+              value: newValue?.value ?? '',
+              new_widget: !isEditing,
+              organization,
+            });
           }}
           components={{
             SingleValue: (containerProps: any) => {

+ 20 - 3
static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx

@@ -9,9 +9,11 @@ import SlideOverPanel from 'sentry/components/slideOverPanel';
 import {IconClose} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
 import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
 import useMedia from 'sentry/utils/useMedia';
-import {useParams} from 'sentry/utils/useParams';
+import useOrganization from 'sentry/utils/useOrganization';
 import {useValidateWidgetQuery} from 'sentry/views/dashboards/hooks/useValidateWidget';
 import {
   type DashboardDetails,
@@ -35,6 +37,7 @@ import WidgetBuilderTypeSelector from 'sentry/views/dashboards/widgetBuilder/com
 import Visualize from 'sentry/views/dashboards/widgetBuilder/components/visualize';
 import WidgetTemplatesList from 'sentry/views/dashboards/widgetBuilder/components/widgetTemplatesList';
 import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
+import useIsEditingWidget from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
 import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
 
 type WidgetBuilderSlideoutProps = {
@@ -66,17 +69,31 @@ function WidgetBuilderSlideout({
   onDataFetched,
   thresholdMetaState,
 }: WidgetBuilderSlideoutProps) {
+  const organization = useOrganization();
   const {state} = useWidgetBuilderContext();
   const [initialState] = useState(state);
   const [error, setError] = useState<Record<string, any>>({});
-  const {widgetIndex} = useParams();
   const theme = useTheme();
+  const isEditing = useIsEditingWidget();
 
   const validatedWidgetResponse = useValidateWidgetQuery(
     convertBuilderStateToWidget(state)
   );
 
-  const isEditing = widgetIndex !== undefined;
+  useEffect(() => {
+    if (!openWidgetTemplates) {
+      trackAnalytics('dashboards_views.widget_builder.opened', {
+        builder_version: WidgetBuilderVersion.SLIDEOUT,
+        new_widget: !isEditing,
+        organization,
+      });
+    }
+    // Ignore isEditing because it won't change during the
+    // useful lifetime of the widget builder, but it
+    // flickers when an edited widget is saved.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [openWidgetTemplates, organization]);
+
   const title = openWidgetTemplates
     ? t('Add from Widget Library')
     : isEditing

+ 18 - 0
static/app/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource.tsx

@@ -0,0 +1,18 @@
+import {defined} from 'sentry/utils';
+import useUrlParams from 'sentry/utils/useUrlParams';
+import {DashboardWidgetSource} from 'sentry/views/dashboards/types';
+
+function useDashboardWidgetSource(): DashboardWidgetSource | '' {
+  const {getParamValue} = useUrlParams('source');
+  const source = getParamValue();
+
+  const validSources = Object.values(
+    DashboardWidgetSource
+  ) satisfies DashboardWidgetSource[];
+
+  return defined(source) && validSources.includes(source as DashboardWidgetSource)
+    ? (source as DashboardWidgetSource)
+    : '';
+}
+
+export default useDashboardWidgetSource;

+ 8 - 0
static/app/views/dashboards/widgetBuilder/hooks/useIsEditingWidget.tsx

@@ -0,0 +1,8 @@
+import {useParams} from 'sentry/utils/useParams';
+
+function useIsEditingWidget() {
+  const {widgetIndex} = useParams();
+  return widgetIndex !== undefined;
+}
+
+export default useIsEditingWidget;

+ 45 - 10
static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx

@@ -25,6 +25,7 @@ import type {TagCollection} from 'sentry/types/group';
 import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
 import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
+import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
 import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
 import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
 import EventView from 'sentry/utils/discover/eventView';
@@ -316,6 +317,7 @@ function WidgetBuilder({
     trackAnalytics('dashboards_views.widget_builder.opened', {
       organization,
       new_widget: !isEditing,
+      builder_version: WidgetBuilderVersion.PAGE,
     });
 
     if (isEmptyObject(tags) && dataSet !== DataSet.SPANS) {
@@ -547,16 +549,6 @@ function WidgetBuilder({
   function handleDisplayTypeOrAnnotationChange<
     F extends keyof Pick<State, 'displayType' | 'title' | 'description'>,
   >(field: F, value: State[F]) {
-    if (value) {
-      trackAnalytics('dashboards_views.widget_builder.change', {
-        from: source,
-        field,
-        value,
-        widget_type: widgetType,
-        organization,
-        new_widget: !isEditing,
-      });
-    }
     setState(prevState => {
       const newState = cloneDeep(prevState);
       set(newState, field, value);
@@ -579,6 +571,7 @@ function WidgetBuilder({
       widget_type: widgetType,
       organization,
       new_widget: !isEditing,
+      builder_version: WidgetBuilderVersion.PAGE,
     });
     setState(prevState => {
       const newState = cloneDeep(prevState);
@@ -913,6 +906,7 @@ function WidgetBuilder({
         organization,
         data_set: widgetData.widgetType ?? defaultWidgetType,
         new_widget: false,
+        builder_version: WidgetBuilderVersion.PAGE,
       });
       return;
     }
@@ -924,6 +918,7 @@ function WidgetBuilder({
       organization,
       data_set: widgetData.widgetType ?? defaultWidgetType,
       new_widget: true,
+      builder_version: WidgetBuilderVersion.PAGE,
     });
   }
 
@@ -1197,6 +1192,20 @@ function WidgetBuilder({
                                         );
                                       }}
                                       value={state.title}
+                                      onBlur={() => {
+                                        trackAnalytics(
+                                          'dashboards_views.widget_builder.change',
+                                          {
+                                            from: source,
+                                            field: 'title',
+                                            value: state.title ?? '',
+                                            widget_type: widgetType,
+                                            organization,
+                                            new_widget: !isEditing,
+                                            builder_version: WidgetBuilderVersion.PAGE,
+                                          }
+                                        );
+                                      }}
                                     />
                                     <StyledTextAreaField
                                       name="description"
@@ -1213,6 +1222,20 @@ function WidgetBuilder({
                                         );
                                       }}
                                       value={state.description}
+                                      onBlur={() => {
+                                        trackAnalytics(
+                                          'dashboards_views.widget_builder.change',
+                                          {
+                                            from: source,
+                                            field: 'description',
+                                            value: state.description ?? '',
+                                            widget_type: widgetType,
+                                            organization,
+                                            new_widget: !isEditing,
+                                            builder_version: WidgetBuilderVersion.PAGE,
+                                          }
+                                        );
+                                      }}
                                     />
                                   </NameWidgetStep>
                                   <VisualizationStep
@@ -1228,6 +1251,18 @@ function WidgetBuilder({
                                         'displayType',
                                         newDisplayType
                                       );
+                                      trackAnalytics(
+                                        'dashboards_views.widget_builder.change',
+                                        {
+                                          from: source,
+                                          field: 'displayType',
+                                          value: newDisplayType,
+                                          widget_type: widgetType,
+                                          organization,
+                                          new_widget: !isEditing,
+                                          builder_version: WidgetBuilderVersion.PAGE,
+                                        }
+                                      );
                                     }}
                                     isWidgetInvalid={!state.queryConditionsValid}
                                     onWidgetSplitDecision={