Browse Source

feat(workflow): Track issue actions/views with alert details (#32765)

Scott Cooper 3 years ago
parent
commit
f0c0777748

+ 26 - 0
static/app/utils/analytics/workflowAnalyticsEvents.tsx

@@ -1,8 +1,21 @@
+import type {ResolutionStatus} from 'sentry/types';
+
 type RuleViewed = {
   alert_type: 'issue' | 'metric';
   project_id: string;
 };
 
+type IssueDetailsWithAlert = {
+  group_id: number;
+  project_id: number;
+  /** The time that the alert was initially fired. */
+  alert_date?: string;
+  /** Id of the rule that triggered the alert */
+  alert_rule_id?: string;
+  /**  The type of alert notification - email/slack */
+  alert_type?: string;
+};
+
 export type TeamInsightsEventParameters = {
   'alert_builder.filter': {query: string; session_id?: string};
   'alert_details.viewed': {alert_id: number};
@@ -19,6 +32,17 @@ export type TeamInsightsEventParameters = {
   'edit_alert_rule.viewed': RuleViewed;
   'issue_alert_rule_details.edit_clicked': {rule_id: number};
   'issue_alert_rule_details.viewed': {rule_id: number};
+  'issue_details.action_clicked': IssueDetailsWithAlert & {
+    action_type:
+      | 'deleted'
+      | 'mark_reviewed'
+      | 'bookmarked'
+      | 'subscribed'
+      | 'shared'
+      | 'discarded'
+      | ResolutionStatus;
+  };
+  'issue_details.viewed': IssueDetailsWithAlert;
   'new_alert_rule.viewed': RuleViewed & {
     session_id: string;
   };
@@ -39,6 +63,8 @@ export const workflowEventMap: Record<TeamInsightsEventKey, string | null> = {
   'edit_alert_rule.viewed': 'Edit Alert Rule: Viewed',
   'issue_alert_rule_details.edit_clicked': 'Issue Alert Rule Details: Edit Clicked',
   'issue_alert_rule_details.viewed': 'Issue Alert Rule Details: Viewed',
+  'issue_details.viewed': 'Issue Details: Viewed',
+  'issue_details.action_clicked': 'Issue Details: Action Clicked',
   'new_alert_rule.viewed': 'New Alert Rule: Viewed',
   'team_insights.viewed': 'Team Insights: Viewed',
 };

+ 1 - 1
static/app/views/issueList/actions/reviewAction.tsx

@@ -3,7 +3,7 @@ import {IconIssues} from 'sentry/icons';
 import {t} from 'sentry/locale';
 
 type Props = {
-  onUpdate: (data?: any) => void;
+  onUpdate: (data: {inbox: boolean}) => void;
   disabled?: boolean;
 };
 

+ 43 - 0
static/app/views/organizationGroupDetails/actions/index.tsx

@@ -1,6 +1,7 @@
 import * as React from 'react';
 import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
+import {Query} from 'history';
 
 import {bulkDelete, bulkUpdate} from 'sentry/actionCreators/group';
 import {
@@ -26,11 +27,13 @@ import {
   Group,
   Organization,
   Project,
+  ResolutionStatus,
   SavedQueryVersions,
   UpdateResolutionStatus,
 } from 'sentry/types';
 import {Event} from 'sentry/types/event';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getUtcDateString} from 'sentry/utils/dates';
 import EventView from 'sentry/utils/discover/eventView';
 import {displayReprocessEventAction} from 'sentry/utils/displayReprocessEventAction';
 import {uniqueId} from 'sentry/utils/guid';
@@ -49,6 +52,7 @@ type Props = {
   organization: Organization;
   project: Project;
   event?: Event;
+  query?: Query;
 };
 
 type State = {
@@ -95,6 +99,31 @@ class Actions extends React.Component<Props, State> {
     return discoverView.getResultsViewUrlTarget(organization.slug);
   }
 
+  trackIssueAction(
+    action:
+      | 'shared'
+      | 'deleted'
+      | 'bookmarked'
+      | 'subscribed'
+      | 'mark_reviewed'
+      | 'discarded'
+      | ResolutionStatus
+  ) {
+    const {group, project, organization, query = {}} = this.props;
+    const {alert_date, alert_rule_id, alert_type} = query;
+    trackAdvancedAnalyticsEvent('issue_details.action_clicked', {
+      organization,
+      project_id: parseInt(project.id, 10),
+      group_id: parseInt(group.id, 10),
+      action_type: action,
+      // Alert properties track if the user came from email/slack alerts
+      alert_date:
+        typeof alert_date === 'string' ? getUtcDateString(alert_date) : undefined,
+      alert_rule_id: typeof alert_rule_id === 'string' ? alert_rule_id : undefined,
+      alert_type: typeof alert_type === 'string' ? alert_type : undefined,
+    });
+  }
+
   onDelete = () => {
     const {group, project, organization, api} = this.props;
 
@@ -115,6 +144,8 @@ class Actions extends React.Component<Props, State> {
         },
       }
     );
+
+    this.trackIssueAction('deleted');
   };
 
   onUpdate = (
@@ -140,6 +171,13 @@ class Actions extends React.Component<Props, State> {
         complete: clearIndicators,
       }
     );
+
+    if ((data as UpdateResolutionStatus).status) {
+      this.trackIssueAction((data as UpdateResolutionStatus).status);
+    }
+    if ((data as {inbox: boolean}).inbox !== undefined) {
+      this.trackIssueAction('mark_reviewed');
+    }
   };
 
   onReprocessEvent = () => {
@@ -172,6 +210,8 @@ class Actions extends React.Component<Props, State> {
         },
       }
     );
+
+    this.trackIssueAction('shared');
   }
 
   onToggleShare = () => {
@@ -186,10 +226,12 @@ class Actions extends React.Component<Props, State> {
 
   onToggleBookmark = () => {
     this.onUpdate({isBookmarked: !this.props.group.isBookmarked});
+    this.trackIssueAction('bookmarked');
   };
 
   onToggleSubscribe = () => {
     this.onUpdate({isSubscribed: !this.props.group.isSubscribed});
+    this.trackIssueAction('subscribed');
   };
 
   onDiscard = () => {
@@ -211,6 +253,7 @@ class Actions extends React.Component<Props, State> {
       },
       complete: clearIndicators,
     });
+    this.trackIssueAction('discarded');
   };
 
   handleClick(disabled: boolean, onClick: (event: React.MouseEvent) => void) {

+ 29 - 4
static/app/views/organizationGroupDetails/groupDetails.tsx

@@ -15,7 +15,9 @@ import GroupStore from 'sentry/stores/groupStore';
 import {PageContent} from 'sentry/styles/organization';
 import {AvatarProject, Group, Organization, Project} from 'sentry/types';
 import {Event} from 'sentry/types/event';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {callIfFunction} from 'sentry/utils/callIfFunction';
+import {getUtcDateString} from 'sentry/utils/dates';
 import {getMessage, getTitle} from 'sentry/utils/events';
 import Projects from 'sentry/utils/projects';
 import recreateRoute from 'sentry/utils/recreateRoute';
@@ -71,16 +73,20 @@ class GroupDetails extends React.Component<Props, State> {
   }
 
   componentDidMount() {
-    this.fetchData();
+    this.fetchData(true);
     this.updateReprocessingProgress();
   }
 
   componentDidUpdate(prevProps: Props, prevState: State) {
+    const globalSelectionReadyChanged =
+      prevProps.isGlobalSelectionReady !== this.props.isGlobalSelectionReady;
+
     if (
-      prevProps.isGlobalSelectionReady !== this.props.isGlobalSelectionReady ||
+      globalSelectionReadyChanged ||
       prevProps.location.pathname !== this.props.location.pathname
     ) {
-      this.fetchData();
+      // Skip tracking for other navigation events like switching events
+      this.fetchData(globalSelectionReadyChanged);
     }
 
     if (
@@ -112,6 +118,21 @@ class GroupDetails extends React.Component<Props, State> {
     };
   }
 
+  trackView(project: Project) {
+    const {organization, params, location} = this.props;
+    const {alert_date, alert_rule_id, alert_type} = location.query;
+    trackAdvancedAnalyticsEvent('issue_details.viewed', {
+      organization,
+      project_id: parseInt(project.id, 10),
+      group_id: parseInt(params.groupId, 10),
+      // Alert properties track if the user came from email/slack alerts
+      alert_date:
+        typeof alert_date === 'string' ? getUtcDateString(alert_date) : undefined,
+      alert_rule_id: typeof alert_rule_id === 'string' ? alert_rule_id : undefined,
+      alert_type: typeof alert_type === 'string' ? alert_type : undefined,
+    });
+  }
+
   remountComponent = () => {
     this.setState(this.initialState);
     this.fetchData();
@@ -326,7 +347,7 @@ class GroupDetails extends React.Component<Props, State> {
     GroupStore.onPopulateReleases(this.props.params.groupId, releases);
   }
 
-  async fetchData() {
+  async fetchData(trackView = false) {
     const {api, isGlobalSelectionReady, params} = this.props;
 
     // Need to wait for global selection store to be ready before making request
@@ -391,6 +412,10 @@ class GroupDetails extends React.Component<Props, State> {
       this.setState({project, loadingGroup: false});
 
       GroupStore.loadInitialData([data]);
+
+      if (trackView) {
+        this.trackView(project);
+      }
     } catch (error) {
       this.handleRequestError(error);
     }

+ 1 - 0
static/app/views/organizationGroupDetails/header.tsx

@@ -259,6 +259,7 @@ class GroupHeader extends React.Component<Props, State> {
           project={project}
           disabled={disableActions}
           event={event}
+          query={location.query}
         />
         <NavTabs>
           <ListLink