Browse Source

feat(ecosystem): Add project ownership analytics (#46979)

Scott Cooper 1 year ago
parent
commit
89cbdeac20

+ 9 - 1
static/app/components/modals/issueOwnershipRuleModal.tsx

@@ -1,9 +1,10 @@
-import {Fragment} from 'react';
+import {Fragment, useEffect} from 'react';
 import {css} from '@emotion/react';
 import {css} from '@emotion/react';
 
 
 import {ModalRenderProps} from 'sentry/actionCreators/modal';
 import {ModalRenderProps} from 'sentry/actionCreators/modal';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
 import type {Event, Organization, Project} from 'sentry/types';
 import type {Event, Organization, Project} from 'sentry/types';
+import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
 import theme from 'sentry/utils/theme';
 import theme from 'sentry/utils/theme';
 import ProjectOwnershipModal from 'sentry/views/settings/project/projectOwnership/modal';
 import ProjectOwnershipModal from 'sentry/views/settings/project/projectOwnership/modal';
 
 
@@ -24,6 +25,13 @@ const IssueOwnershipRuleModal = ({
   eventData,
   eventData,
   closeModal,
   closeModal,
 }: CreateOwnershipRuleProps) => {
 }: CreateOwnershipRuleProps) => {
+  useEffect(() => {
+    trackIntegrationAnalytics('project_ownership.modal_opened', {
+      page: 'issue_details',
+      organization,
+    });
+  }, [organization]);
+
   return (
   return (
     <Fragment>
     <Fragment>
       <Header closeButton>
       <Header closeButton>

+ 9 - 0
static/app/utils/analytics/integrations/index.ts

@@ -58,6 +58,11 @@ type IntegrationInstallationInputValueChangeEventParams = {
   field_name: string;
   field_name: string;
 } & SingleIntegrationEventParams;
 } & SingleIntegrationEventParams;
 
 
+type ProjectOwnershipModalParams = {
+  page: 'issue_details' | 'project_settings';
+  net_change?: number;
+};
+
 // Event key to payload mappings
 // Event key to payload mappings
 export type IntegrationEventParameters = {
 export type IntegrationEventParameters = {
   'integrations.cloudformation_link_clicked': SingleIntegrationEventParams;
   'integrations.cloudformation_link_clicked': SingleIntegrationEventParams;
@@ -83,6 +88,8 @@ export type IntegrationEventParameters = {
   'integrations.uninstall_clicked': SingleIntegrationEventParams;
   'integrations.uninstall_clicked': SingleIntegrationEventParams;
   'integrations.uninstall_completed': SingleIntegrationEventParams;
   'integrations.uninstall_completed': SingleIntegrationEventParams;
   'integrations.upgrade_plan_modal_opened': SingleIntegrationEventParams;
   'integrations.upgrade_plan_modal_opened': SingleIntegrationEventParams;
+  'project_ownership.modal_opened': ProjectOwnershipModalParams;
+  'project_ownership.saved': ProjectOwnershipModalParams;
 } & CodeownersEventParameters &
 } & CodeownersEventParameters &
   StacktraceLinkEventParameters &
   StacktraceLinkEventParameters &
   PlatformEventParameters;
   PlatformEventParameters;
@@ -116,6 +123,8 @@ export const integrationEventMap: Record<IntegrationAnalyticsKey, string> = {
   'integrations.serverless_function_action': 'Integrations: Serverless Function Action',
   'integrations.serverless_function_action': 'Integrations: Serverless Function Action',
   'integrations.cloudformation_link_clicked': 'Integrations: CloudFormation Link Clicked',
   'integrations.cloudformation_link_clicked': 'Integrations: CloudFormation Link Clicked',
   'integrations.switch_manual_sdk_setup': 'Integrations: Switch Manual SDK Setup',
   'integrations.switch_manual_sdk_setup': 'Integrations: Switch Manual SDK Setup',
+  'project_ownership.modal_opened': 'Project Ownership: Modal Opened',
+  'project_ownership.saved': 'Project Ownership: Saved',
   ...codeownersEventMap,
   ...codeownersEventMap,
   ...stacktraceLinkEventMap,
   ...stacktraceLinkEventMap,
   ...platformEventMap,
   ...platformEventMap,

+ 2 - 3
static/app/views/settings/organizationIntegrations/abstractIntegrationDetailedView.tsx

@@ -202,15 +202,14 @@ class AbstractIntegrationDetailedView<
   ) => {
   ) => {
     options = options || {};
     options = options || {};
     // If we use this intermediate type we get type checking on the things we care about
     // If we use this intermediate type we get type checking on the things we care about
-    const params = {
+    trackIntegrationAnalytics(eventKey, {
       view: 'integrations_directory_integration_detail',
       view: 'integrations_directory_integration_detail',
       integration: this.integrationSlug,
       integration: this.integrationSlug,
       integration_type: this.integrationType,
       integration_type: this.integrationType,
       already_installed: this.installationStatus !== 'Not Installed', // pending counts as installed here
       already_installed: this.installationStatus !== 'Not Installed', // pending counts as installed here
       organization: this.props.organization,
       organization: this.props.organization,
       ...options,
       ...options,
-    };
-    trackIntegrationAnalytics(eventKey, params);
+    });
   };
   };
 
 
   // Returns the props as needed by the hooks integrations:feature-gates
   // Returns the props as needed by the hooks integrations:feature-gates

+ 1 - 0
static/app/views/settings/project/projectOwnership/editRulesModal.tsx

@@ -70,6 +70,7 @@ export function EditOwnershipRules({ownership, ...props}: EditOwnershipRulesModa
           {...props}
           {...props}
           dateUpdated={ownership.lastUpdated}
           dateUpdated={ownership.lastUpdated}
           initialText={ownership.raw || ''}
           initialText={ownership.raw || ''}
+          page="project_settings"
         />
         />
       )}
       )}
     </Fragment>
     </Fragment>

+ 1 - 0
static/app/views/settings/project/projectOwnership/modal.tsx

@@ -170,6 +170,7 @@ class ProjectOwnershipModal extends AsyncComponent<Props, State> {
           paths={paths}
           paths={paths}
           dateUpdated={ownership.lastUpdated}
           dateUpdated={ownership.lastUpdated}
           onCancel={onCancel}
           onCancel={onCancel}
+          page="issue_details"
         />
         />
       </Fragment>
       </Fragment>
     );
     );

+ 13 - 1
static/app/views/settings/project/projectOwnership/ownerInput.tsx

@@ -13,6 +13,7 @@ import MemberListStore from 'sentry/stores/memberListStore';
 import ProjectsStore from 'sentry/stores/projectsStore';
 import ProjectsStore from 'sentry/stores/projectsStore';
 import {Organization, Project, Team} from 'sentry/types';
 import {Organization, Project, Team} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import {defined} from 'sentry/utils';
+import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
 
 
 import RuleBuilder from './ruleBuilder';
 import RuleBuilder from './ruleBuilder';
 
 
@@ -27,6 +28,10 @@ type Props = {
   initialText: string;
   initialText: string;
   onCancel: () => void;
   onCancel: () => void;
   organization: Organization;
   organization: Organization;
+  /**
+   * Used for analytics
+   */
+  page: 'issue_details' | 'project_settings';
   project: Project;
   project: Project;
   onSave?: (text: string | null) => void;
   onSave?: (text: string | null) => void;
 } & typeof defaultProps;
 } & typeof defaultProps;
@@ -63,7 +68,7 @@ class OwnerInput extends Component<Props, State> {
   }
   }
 
 
   handleUpdateOwnership = () => {
   handleUpdateOwnership = () => {
-    const {organization, project, onSave} = this.props;
+    const {organization, project, onSave, page, initialText} = this.props;
     const {text} = this.state;
     const {text} = this.state;
     this.setState({error: null});
     this.setState({error: null});
 
 
@@ -86,6 +91,13 @@ class OwnerInput extends Component<Props, State> {
           },
           },
           () => onSave && onSave(text)
           () => onSave && onSave(text)
         );
         );
+        trackIntegrationAnalytics('project_ownership.saved', {
+          page,
+          organization,
+          net_change:
+            (text?.split('\n').filter(x => x).length ?? 0) -
+            initialText.split('\n').filter(x => x).length,
+        });
       })
       })
       .catch(error => {
       .catch(error => {
         this.setState({error: error.responseJSON});
         this.setState({error: error.responseJSON});