Repository: getsentry/sentry
Pull request: feat(issue-details): Add analytics events for issue tracking
Closes https://github.com/getsentry/sentry/issues/46422

Problem statement:
Track external issues being created
We have no analytics for a user creating an external issue (ex: Jira). We should add the following analytics events to the front-end:

* integrations.external_issue_modal_opened
* integrations.external_issue_created

We need to make sure to record the `provider`, `group_id`, and `project_id`.

Hints from issue discussion:

Commit message: feat(issue-details): Add analytics events for issue tracking

Patch to review:
diff --git a/static/app/components/group/externalIssueActions.tsx b/static/app/components/group/externalIssueActions.tsx
index 5a10557a2ac2fc..6938e67f5ab913 100644
--- a/static/app/components/group/externalIssueActions.tsx
+++ b/static/app/components/group/externalIssueActions.tsx
@@ -8,7 +8,10 @@ import IssueSyncListElement from 'sentry/components/issueSyncListElement';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {Group, GroupIntegration} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForGroup} from 'sentry/utils/events';
 import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
 import IntegrationItem from 'sentry/views/settings/organizationIntegrations/integrationItem';
 
 import ExternalIssueForm from './externalIssueForm';
@@ -25,6 +28,7 @@ type LinkedIssues = {
 };
 
 const ExternalIssueActions = ({configurations, group, onChange}: Props) => {
+  const organization = useOrganization();
   const api = useApi();
   const {linked, unlinked} = configurations
     .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
@@ -62,11 +66,21 @@ const ExternalIssueActions = ({configurations, group, onChange}: Props) => {
     });
   };
 
-  const doOpenModal = (integration: GroupIntegration) =>
+  const doOpenModal = (integration: GroupIntegration) => {
+    trackAdvancedAnalyticsEvent('issue_details.external_issue_modal_opened', {
+      organization,
+      ...getAnalyticsDataForGroup(group),
+      external_issue_provider: integration.provider.key,
+      external_issue_type: 'integration',
+    });
+
     openModal(
-      deps => <ExternalIssueForm {...deps} {...{group, onChange, integration}} />,
+      deps => (
+        <ExternalIssueForm {...deps} {...{group, onChange, integration, organization}} />
+      ),
       {closeEvents: 'escape-key'}
     );
+  };
 
   return (
     <Fragment>
diff --git a/static/app/components/group/externalIssueForm.tsx b/static/app/components/group/externalIssueForm.tsx
index abe8cc373346cb..5a760066fd3721 100644
--- a/static/app/components/group/externalIssueForm.tsx
+++ b/static/app/components/group/externalIssueForm.tsx
@@ -8,7 +8,9 @@ import AbstractExternalIssueForm, {
 import {FormProps} from 'sentry/components/forms/form';
 import NavTabs from 'sentry/components/navTabs';
 import {t, tct} from 'sentry/locale';
-import {Group, Integration, IntegrationExternalIssue} from 'sentry/types';
+import {Group, Integration, IntegrationExternalIssue, Organization} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForGroup} from 'sentry/utils/events';
 
 const MESSAGES_BY_ACTION = {
   link: t('Successfully linked issue.'),
@@ -24,6 +26,7 @@ type Props = {
   group: Group;
   integration: Integration;
   onChange: (onSuccess?: () => void, onError?: () => void) => void;
+  organization: Organization;
 } & AbstractExternalIssueForm['props'];
 
 type State = AbstractExternalIssueForm['state'];
@@ -69,6 +72,14 @@ export default class ExternalIssueForm extends AbstractExternalIssueForm<Props,
   onSubmitSuccess = (_data: IntegrationExternalIssue): void => {
     const {onChange, closeModal} = this.props;
     const {action} = this.state;
+
+    trackAdvancedAnalyticsEvent('issue_details.external_issue_created', {
+      organization: this.props.organization,
+      ...getAnalyticsDataForGroup(this.props.group),
+      external_issue_provider: this.props.integration.provider.key,
+      external_issue_type: 'integration',
+    });
+
     onChange(() => addSuccessMessage(MESSAGES_BY_ACTION[action]));
     closeModal();
 
diff --git a/static/app/components/group/pluginActions.tsx b/static/app/components/group/pluginActions.tsx
index 424cc0ba56c25c..59372f35d2e9dc 100644
--- a/static/app/components/group/pluginActions.tsx
+++ b/static/app/components/group/pluginActions.tsx
@@ -8,6 +8,8 @@ import NavTabs from 'sentry/components/navTabs';
 import {t, tct} from 'sentry/locale';
 import plugins from 'sentry/plugins';
 import {Group, Organization, Plugin, Project} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForGroup} from 'sentry/utils/events';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
 
@@ -98,6 +100,13 @@ class PluginActions extends Component<Props, State> {
     const {project, group, organization} = this.props;
     const plugin = {...this.props.plugin, issue};
 
+    trackAdvancedAnalyticsEvent('issue_details.external_issue_modal_opened', {
+      organization,
+      ...getAnalyticsDataForGroup(group),
+      external_issue_provider: plugin.slug,
+      external_issue_type: 'plugin',
+    });
+
     openModal(
       deps => (
         <PluginActionsModal
diff --git a/static/app/plugins/components/issueActions.tsx b/static/app/plugins/components/issueActions.tsx
index 520c3781c6340b..2160ca7a8f0205 100644
--- a/static/app/plugins/components/issueActions.tsx
+++ b/static/app/plugins/components/issueActions.tsx
@@ -6,6 +6,8 @@ import {t} from 'sentry/locale';
 import PluginComponentBase from 'sentry/plugins/pluginComponentBase';
 import GroupStore from 'sentry/stores/groupStore';
 import {Group, Organization, Plugin, Project} from 'sentry/types';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {getAnalyticsDataForGroup} from 'sentry/utils/events';
 
 type Field = {
   depends?: string[];
@@ -306,6 +308,13 @@ class IssueActions extends PluginComponentBase<Props, State> {
     // a refetch in GroupDetails
     type StaleGroup = Group & {stale?: boolean};
 
+    trackAdvancedAnalyticsEvent('issue_details.external_issue_created', {
+      organization: this.props.organization,
+      ...getAnalyticsDataForGroup(this.props.group),
+      external_issue_provider: this.props.plugin.slug,
+      external_issue_type: 'plugin',
+    });
+
     GroupStore.onUpdateSuccess('', [this.getGroup().id], {stale: true} as StaleGroup);
     this.props.onSuccess && this.props.onSuccess(data);
   }
diff --git a/static/app/utils/analytics/issueAnalyticsEvents.tsx b/static/app/utils/analytics/issueAnalyticsEvents.tsx
index ea3974a60e5bc4..aa8c672cc9f1c3 100644
--- a/static/app/utils/analytics/issueAnalyticsEvents.tsx
+++ b/static/app/utils/analytics/issueAnalyticsEvents.tsx
@@ -15,6 +15,11 @@ type SourceMapDebugParam = {
 
 interface GroupEventParams extends CommonGroupAnalyticsData, BaseEventAnalyticsParams {}
 
+interface ExternalIssueParams extends CommonGroupAnalyticsData {
+  external_issue_provider: string;
+  external_issue_type: 'plugin' | 'integration';
+}
+
 export type IssueEventParameters = {
   'event_cause.dismissed': {};
   'event_cause.docs_clicked': {};
@@ -34,6 +39,8 @@ export type IssueEventParameters = {
   'issue.shared_publicly': {};
   'issue_details.copy_event_link_clicked': GroupEventParams;
   'issue_details.event_details_clicked': GroupEventParams;
+  'issue_details.external_issue_created': ExternalIssueParams;
+  'issue_details.external_issue_modal_opened': ExternalIssueParams;
   'issue_details.header_view_replay_clicked': GroupEventParams;
   'issue_details.performance.autogrouped_siblings_toggle': {};
   'issue_details.performance.hidden_spans_expanded': {};
@@ -237,4 +244,7 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
   'issue_details.copy_event_link_clicked': 'Issue Details: Copy Event Link Clicked',
   'issue_details.event_details_clicked': 'Issue Details: Full Event Details Clicked',
   'issue_details.header_view_replay_clicked': 'Issue Details: Header View Replay Clicked',
+  'issue_details.external_issue_modal_opened':
+    'Issue Details: External Issue Modal Opened',
+  'issue_details.external_issue_created': 'Issue Details: External Issue Created',
 };


Code review comments:
diff_hunk: @@ -15,6 +15,11 @@ type SourceMapDebugParam = {
 
 interface GroupEventParams extends CommonGroupAnalyticsData, BaseEventAnalyticsParams {}
 
+interface ExternalIssueParams extends CommonGroupAnalyticsData {
+  external_issue_provider: string;
+  external_issue_type: 'plugin' | 'integration';
path: static/app/utils/analytics/issueAnalyticsEvents.tsx
text: @user1:
@author we should use `IntegrationType` here

diff_hunk: @@ -69,6 +72,14 @@ export default class ExternalIssueForm extends AbstractExternalIssueForm<Props,
   onSubmitSuccess = (_data: IntegrationExternalIssue): void => {
     const {onChange, closeModal} = this.props;
     const {action} = this.state;
+
+    trackAdvancedAnalyticsEvent('issue_details.external_issue_created', {
+      organization: this.props.organization,
+      ...getAnalyticsDataForGroup(this.props.group),
+      external_issue_provider: this.props.integration.provider.key,
+      external_issue_type: 'integration',
path: static/app/components/group/externalIssueForm.tsx
text: @user1:
@author can we call it `first_party`? That's the standard we follow in other places. See `IntegrationType`

@author:
Thanks for pointing this out - didn't know about that! Changed to `IntegrationType`

Out of curiosity what's the difference between `first_party` and `sentry_app`?

@user1:
@author first_party means we built it, sentry_app means it's on the integration platform and a 3rd party built it