Browse Source

analytics(issue-views): Add analytics to issue views (#77797)

Adds analytics for the following events:

- User switched views
- User updated a view with a new query or sort 
- User discarded changes in a view 
- User renamed a view 
- User duplicated a view
- User deleted a view
- User reordered their views
- User clicked on `+ Add View`
- User saved a custom query from add view menu 
- User saved a deprecated saved search from add view menu 
- User saved a recommended view from add view menu 
- User opened a shared view from another user 
- User discarded a temporary view
- User saved a temporary view
Michael Sun 5 months ago
parent
commit
548ff1da78

+ 15 - 1
static/app/components/draggableTabs/draggableTabList.tsx

@@ -28,9 +28,11 @@ import {IconAdd, IconEllipsis} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {defined} from 'sentry/utils';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import {browserHistory} from 'sentry/utils/browserHistory';
 import {useDimensions} from 'sentry/utils/useDimensions';
 import {useDimensionsMultiple} from 'sentry/utils/useDimensionsMultiple';
+import useOrganization from 'sentry/utils/useOrganization';
 
 import type {DraggableTabListItemProps} from './item';
 import {Item} from './item';
@@ -262,6 +264,7 @@ function BaseDraggableTabList({
 }: BaseDraggableTabListProps) {
   const [hoveringKey, setHoveringKey] = useState<Key | null>(null);
   const {rootProps, setTabListState} = useContext(TabsContext);
+  const organization = useOrganization();
   const {
     value,
     defaultValue,
@@ -284,6 +287,11 @@ function BaseDraggableTabList({
       if (!linkTo) {
         return;
       }
+
+      trackAnalytics('issue_views.switched_views', {
+        organization,
+      });
+
       browserHistory.push(linkTo);
     },
     isDisabled: disabled,
@@ -329,7 +337,13 @@ function BaseDraggableTabList({
           onHoverStart={() => setHoveringKey('addView')}
           onHoverEnd={() => setHoveringKey(null)}
         >
-          <AddViewButton borderless size="zero" onClick={onAddView}>
+          <AddViewButton
+            borderless
+            size="zero"
+            onClick={onAddView}
+            analyticsEventName="Issue Views: Add View Clicked"
+            analyticsEventKey="issue_views.add_view.clicked"
+          >
             <StyledIconAdd size="xs" />
             {t('Add View')}
           </AddViewButton>

+ 40 - 0
static/app/utils/analytics/issueAnalyticsEvents.tsx

@@ -221,6 +221,30 @@ export type IssueEventParameters = {
     search_source: string;
     search_type: string;
   };
+  'issue_views.add_view.clicked': {};
+  'issue_views.add_view.custom_query_saved': {
+    query: string;
+  };
+  'issue_views.add_view.recommended_view_saved': {
+    label: string;
+    persisted: boolean;
+    query: string;
+  };
+  'issue_views.add_view.saved_search_saved': {
+    query: string;
+  };
+  'issue_views.deleted_view': {};
+  'issue_views.discarded_changes': {};
+  'issue_views.duplicated_view': {};
+  'issue_views.renamed_view': {};
+  'issue_views.reordered_views': {};
+  'issue_views.saved_changes': {};
+  'issue_views.shared_view_opened': {
+    query: string;
+  };
+  'issue_views.switched_views': {};
+  'issue_views.temp_view_discarded': {};
+  'issue_views.temp_view_saved': {};
   'issues_stream.archived': {
     action_status_details?: string;
     action_substatus?: string | null;
@@ -248,6 +272,7 @@ export type IssueEventParameters = {
     priority: PriorityLevel;
   };
   'issues_tab.viewed': {
+    issue_views_enabled: boolean;
     num_issues: number;
     num_new_issues: number;
     num_old_issues: number;
@@ -349,6 +374,21 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
   'issue_error_banner.proguard_misconfigured.clicked':
     'Proguard Potentially Misconfigured Issue Error Banner Link Clicked',
   'issues_tab.viewed': 'Viewed Issues Tab',
+  'issue_views.switched_views': 'Issue Views: Switched Views',
+  'issue_views.saved_changes': 'Issue Views: Updated View',
+  'issue_views.discarded_changes': 'Issue Views: Discarded Changes',
+  'issue_views.renamed_view': 'Issue Views: Renamed View',
+  'issue_views.duplicated_view': 'Issue Views: Duplicated View',
+  'issue_views.deleted_view': 'Issue Views: Deleted View',
+  'issue_views.reordered_views': 'Issue Views: Views Reordered',
+  'issue_views.add_view.clicked': 'Issue Views: Add View Clicked',
+  'issue_views.add_view.custom_query_saved':
+    'Issue Views: Custom Query Saved From Add View',
+  'issue_views.add_view.saved_search_saved': 'Issue Views: Saved Search Saved',
+  'issue_views.add_view.recommended_view_saved': 'Issue Views: Recommended View Saved',
+  'issue_views.shared_view_opened': 'Issue Views: Shared View Opened',
+  'issue_views.temp_view_discarded': 'Issue Views: Temporary View Discarded',
+  'issue_views.temp_view_saved': 'Issue Views: Temporary View Saved',
   'issue_search.failed': 'Issue Search: Failed',
   'issue_search.empty': 'Issue Search: Empty',
   'issue.search_sidebar_clicked': 'Issue Search Sidebar Clicked',

+ 34 - 2
static/app/views/issueList/addViewPage.tsx

@@ -11,7 +11,9 @@ import {IconMegaphone} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {SavedSearch} from 'sentry/types/group';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
+import useOrganization from 'sentry/utils/useOrganization';
 import {OverflowEllipsisTextContainer} from 'sentry/views/insights/common/components/textAlign';
 import {NewTabContext} from 'sentry/views/issueList/utils/newTabContext';
 
@@ -23,6 +25,7 @@ type SearchSuggestion = {
 interface SearchSuggestionListProps {
   searchSuggestions: SearchSuggestion[];
   title: React.ReactNode;
+  type: 'recommended' | 'saved_searches';
 }
 
 const RECOMMENDED_SEARCHES: SearchSuggestion[] = [
@@ -71,6 +74,7 @@ function AddViewPage({savedSearches}: {savedSearches: SavedSearch[]}) {
       <SearchSuggestionList
         title={'Recommended Searches'}
         searchSuggestions={RECOMMENDED_SEARCHES}
+        type="recommended"
       />
       {savedSearches && savedSearches.length !== 0 && (
         <SearchSuggestionList
@@ -81,14 +85,20 @@ function AddViewPage({savedSearches}: {savedSearches: SavedSearch[]}) {
               query: search.query,
             };
           })}
+          type="saved_searches"
         />
       )}
     </AddViewWrapper>
   );
 }
 
-function SearchSuggestionList({title, searchSuggestions}: SearchSuggestionListProps) {
+function SearchSuggestionList({
+  title,
+  searchSuggestions,
+  type,
+}: SearchSuggestionListProps) {
   const {onNewViewSaved} = useContext(NewTabContext);
+  const organization = useOrganization();
 
   return (
     <Suggestions>
@@ -97,7 +107,19 @@ function SearchSuggestionList({title, searchSuggestions}: SearchSuggestionListPr
         {searchSuggestions.map((suggestion, index) => (
           <Suggestion
             key={index}
-            onClick={() => onNewViewSaved?.(suggestion.label, suggestion.query, false)}
+            onClick={() => {
+              onNewViewSaved?.(suggestion.label, suggestion.query, false);
+              const analyticsKey =
+                type === 'recommended'
+                  ? 'issue_views.add_view.recommended_view_saved'
+                  : 'issue_views.add_view.saved_search_saved';
+              trackAnalytics(analyticsKey, {
+                organization,
+                persisted: false,
+                label: suggestion.label,
+                query: suggestion.query,
+              });
+            }}
           >
             {/*
             Saved search labels have an average length of approximately 16 characters
@@ -114,6 +136,16 @@ function SearchSuggestionList({title, searchSuggestions}: SearchSuggestionListPr
                   onClick={e => {
                     e.stopPropagation();
                     onNewViewSaved?.(suggestion.label, suggestion.query, true);
+                    const analyticsKey =
+                      type === 'recommended'
+                        ? 'issue_views.add_view.recommended_view_saved'
+                        : 'issue_views.add_view.saved_search_saved';
+                    trackAnalytics(analyticsKey, {
+                      organization,
+                      persisted: true,
+                      label: suggestion.label,
+                      query: suggestion.query,
+                    });
                   }}
                   borderless
                 >

+ 9 - 0
static/app/views/issueList/customViewsHeader.tsx

@@ -15,6 +15,7 @@ import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
 import type {Organization} from 'sentry/types/organization';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useNavigate} from 'sentry/utils/useNavigate';
@@ -275,6 +276,10 @@ function CustomViewsIssueListHeaderTabsContent({
             },
           })
         );
+        trackAnalytics('issue_views.shared_view_opened', {
+          organization,
+          query,
+        });
         return;
       }
       return;
@@ -336,6 +341,10 @@ function CustomViewsIssueListHeaderTabsContent({
         );
         setDraggableTabs(updatedTabs);
         debounceUpdateViews(updatedTabs);
+        trackAnalytics('issue_views.add_view.custom_query_saved', {
+          organization,
+          query,
+        });
       } else {
         setNewViewActive(true);
       }

+ 30 - 0
static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx

@@ -14,9 +14,11 @@ import {TabsContext} from 'sentry/components/tabs';
 import {t} from 'sentry/locale';
 import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
 import {defined} from 'sentry/utils';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useNavigate} from 'sentry/utils/useNavigate';
+import useOrganization from 'sentry/utils/useOrganization';
 import {DraggableTabMenuButton} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabMenuButton';
 import EditableTabTitle from 'sentry/views/issueList/groupSearchViewTabs/editableTabTitle';
 import {IssueSortOptions} from 'sentry/views/issueList/utils';
@@ -108,6 +110,7 @@ export function DraggableTabBar({
   // TODO: Extract this to a separate component encompassing Tab.Item in the future
   const [editingTabKey, setEditingTabKey] = useState<string | null>(null);
 
+  const organization = useOrganization();
   const navigate = useNavigate();
   const location = useLocation();
 
@@ -126,6 +129,9 @@ export function DraggableTabBar({
       .filter(defined);
     setTabs(newTabs);
     onReorder?.(newTabs);
+    trackAnalytics('issue_views.reordered_views', {
+      organization,
+    });
   };
 
   const handleOnSaveChanges = () => {
@@ -143,6 +149,9 @@ export function DraggableTabBar({
       });
       setTabs(newTabs);
       onSave?.(newTabs);
+      trackAnalytics('issue_views.saved_changes', {
+        organization,
+      });
     }
   };
 
@@ -166,6 +175,9 @@ export function DraggableTabBar({
         },
       });
       onDiscard?.();
+      trackAnalytics('issue_views.discarded_changes', {
+        organization,
+      });
     }
   };
 
@@ -177,6 +189,9 @@ export function DraggableTabBar({
       );
       setTabs(newTabs);
       onTabRenamed?.(newTabs, newLabel);
+      trackAnalytics('issue_views.renamed_view', {
+        organization,
+      });
     }
   };
 
@@ -205,6 +220,9 @@ export function DraggableTabBar({
       setTabs(newTabs);
       tabListState?.setSelectedKey(tempId);
       onDuplicate?.(newTabs);
+      trackAnalytics('issue_views.duplicated_view', {
+        organization,
+      });
     }
   };
 
@@ -214,6 +232,9 @@ export function DraggableTabBar({
       setTabs(newTabs);
       tabListState?.setSelectedKey(newTabs[0].key);
       onDelete?.(newTabs);
+      trackAnalytics('issue_views.deleted_view', {
+        organization,
+      });
     }
   };
 
@@ -232,6 +253,9 @@ export function DraggableTabBar({
       setTempTab(undefined);
       tabListState?.setSelectedKey(tempId);
       onSaveTempView?.(newTabs);
+      trackAnalytics('issue_views.temp_view_saved', {
+        organization,
+      });
     }
   };
 
@@ -239,6 +263,9 @@ export function DraggableTabBar({
     tabListState?.setSelectedKey(tabs[0].key);
     setTempTab(undefined);
     onDiscardTempView?.();
+    trackAnalytics('issue_views.temp_view_discarded', {
+      organization,
+    });
   };
 
   const handleCreateNewView = () => {
@@ -267,6 +294,9 @@ export function DraggableTabBar({
       });
       setTabs(newTabs);
       tabListState?.setSelectedKey(tempId);
+      trackAnalytics('issue_views.add_view.clicked', {
+        organization,
+      });
     }
   };
 

+ 6 - 0
static/app/views/issueList/overview.tsx

@@ -192,6 +192,11 @@ class IssueListOverview extends Component<Props, State> {
     }
     this.fetchTags();
     this.fetchMemberList();
+    this.props.setRouteAnalyticsParams?.({
+      issue_views_enabled: this.props.organization.features.includes(
+        'issue-stream-custom-views'
+      ),
+    });
     // let custom analytics take control
     this.props.setDisableRouteAnalytics?.();
   }
@@ -840,6 +845,7 @@ class IssueListOverview extends Component<Props, State> {
       num_new_issues: numNewIssues,
       num_issues: data.length,
       total_issues_count: numHits,
+      issue_views_enabled: organization.features.includes('issue-stream-custom-views'),
       sort: this.getSort(),
     });
   }