Browse Source

feat(growth): Alert Bar on issue page for sample events (#28864)

* feat(growth): Alert Bar on issue page for sample events

* changes

* changes

* adding tests

* Update static/app/views/organizationGroupDetails/sampleEventAlert.tsx

Co-authored-by: Alberto Leal <mail4alberto@gmail.com>

* fix alert event message

* add to storybook

* rename AlertBar

* add platform to analytics

* pass State as a secondary arg to the generic

* fix warning

Co-authored-by: Alberto Leal <mail4alberto@gmail.com>
Zhixing Zhang 3 years ago
parent
commit
147049bee4

+ 10 - 0
docs-ui/stories/components/alertBar.stories.js

@@ -0,0 +1,10 @@
+import PageAlertBar from 'app/components/pageAlertBar';
+
+export default {
+  title: 'Components/Alerts/Alert Bar',
+  component: PageAlertBar,
+};
+
+export const Default = ({...args}) => (
+  <PageAlertBar {...args}>Alert message</PageAlertBar>
+);

+ 13 - 0
static/app/components/pageAlertBar.tsx

@@ -0,0 +1,13 @@
+import styled from '@emotion/styled';
+
+const PageAlertBar = styled('div')`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: ${p => p.theme.headerBackground};
+  background-color: ${p => p.theme.bannerBackground};
+  padding: 6px 30px;
+  font-size: 14px;
+`;
+
+export default PageAlertBar;

+ 6 - 0
static/app/utils/analytics/growthAnalyticsEvents.tsx

@@ -66,6 +66,10 @@ export type GrowthEventParameters = {
   'growth.sample_transaction_docs_link_clicked': {
     project_id: string;
   };
+  'growth.sample_error_onboarding_link_clicked': {
+    project_id: string;
+    platform?: string;
+  };
   'growth.issue_open_in_discover_btn_clicked': {};
 };
 
@@ -101,6 +105,8 @@ export const growthEventMap: Record<GrowthAnalyticsKey, string> = {
   'growth.demo_modal_clicked_continue': 'Growth: Demo Modal Clicked Continue',
   'growth.sample_transaction_docs_link_clicked':
     'Growth: Sample Transacton Docs Link Clicked',
+  'growth.sample_error_onboarding_link_clicked':
+    'Growth: Sample Transacton Onboarding Link Clicked',
   'growth.issue_open_in_discover_btn_clicked':
     'Growth: Open in Discover Button in Issue Details clicked',
 };

+ 34 - 10
static/app/views/issueList/container.tsx

@@ -1,31 +1,55 @@
-import {Component} from 'react';
+import React, {Component} from 'react';
 import DocumentTitle from 'react-document-title';
 
 import NoProjectMessage from 'app/components/noProjectMessage';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
-import {Organization} from 'app/types';
+import GroupStore from 'app/stores/groupStore';
+import {Organization, Project} from 'app/types';
+import {callIfFunction} from 'app/utils/callIfFunction';
 import withOrganization from 'app/utils/withOrganization';
+import SampleEventAlert from 'app/views/organizationGroupDetails/sampleEventAlert';
 
 type Props = {
   organization: Organization;
+  projects: Project[];
 };
 
-class IssueListContainer extends Component<Props> {
-  getTitle() {
-    return `Issues - ${this.props.organization.slug} - Sentry`;
-  }
+type State = {
+  showSampleEventBanner: boolean;
+};
+class IssueListContainer extends Component<Props, State> {
+  state: State = {
+    showSampleEventBanner: false,
+  };
 
+  listener = GroupStore.listen(() => this.onGroupChange(), undefined);
   render() {
     const {organization, children} = this.props;
-
     return (
       <DocumentTitle title={this.getTitle()}>
-        <GlobalSelectionHeader>
-          <NoProjectMessage organization={organization}>{children}</NoProjectMessage>
-        </GlobalSelectionHeader>
+        <React.Fragment>
+          {this.state.showSampleEventBanner && <SampleEventAlert />}
+          <GlobalSelectionHeader>
+            <NoProjectMessage organization={organization}>{children}</NoProjectMessage>
+          </GlobalSelectionHeader>
+        </React.Fragment>
       </DocumentTitle>
     );
   }
+
+  onGroupChange() {
+    this.setState({
+      showSampleEventBanner: GroupStore.getAllItemIds().length === 1,
+    });
+  }
+
+  componentWillUnmount() {
+    callIfFunction(this.listener);
+  }
+
+  getTitle() {
+    return `Issues - ${this.props.organization.slug} - Sentry`;
+  }
 }
 export default withOrganization(IssueListContainer);
 export {IssueListContainer};

+ 10 - 6
static/app/views/organizationGroupDetails/index.tsx

@@ -8,6 +8,7 @@ import withOrganization from 'app/utils/withOrganization';
 import withProjects from 'app/utils/withProjects';
 
 import GroupDetails from './groupDetails';
+import SampleEventAlert from './sampleEventAlert';
 
 type Props = {
   selection: GlobalSelection;
@@ -27,13 +28,16 @@ class OrganizationGroupDetails extends React.Component<Props> {
 
   render() {
     const {selection, ...props} = this.props;
-
     return (
-      <GroupDetails
-        key={`${this.props.params.groupId}-envs:${selection.environments.join(',')}`}
-        environments={selection.environments}
-        {...props}
-      />
+      <React.Fragment>
+        <SampleEventAlert />
+
+        <GroupDetails
+          key={`${this.props.params.groupId}-envs:${selection.environments.join(',')}`}
+          environments={selection.environments}
+          {...props}
+        />
+      </React.Fragment>
     );
   }
 }

+ 65 - 0
static/app/views/organizationGroupDetails/sampleEventAlert.tsx

@@ -0,0 +1,65 @@
+import styled from '@emotion/styled';
+
+import Button from 'app/components/button';
+import PageAlertBar from 'app/components/pageAlertBar';
+import {IconLightning} from 'app/icons';
+import {t} from 'app/locale';
+import space from 'app/styles/space';
+import {GlobalSelection, Organization, Project} from 'app/types';
+import trackAdvancedAnalyticsEvent from 'app/utils/analytics/trackAdvancedAnalyticsEvent';
+import withGlobalSelection from 'app/utils/withGlobalSelection';
+import withOrganization from 'app/utils/withOrganization';
+import withProjects from 'app/utils/withProjects';
+
+function SampleEventAlert({
+  selection,
+  organization,
+  projects,
+}: {
+  selection: GlobalSelection;
+  organization: Organization;
+  projects: Project[];
+}) {
+  if (projects.length === 0) {
+    return null;
+  }
+  if (selection.projects.length !== 1) {
+    return null;
+  }
+  const selectedProject = projects.find(p => p.id === selection.projects[0].toString());
+  if (!selectedProject || selectedProject.firstEvent) {
+    return null;
+  }
+  return (
+    <PageAlertBar>
+      <IconLightning />
+      <TextWrapper>
+        {t(
+          'You are viewing a sample error. Configure Sentry to start viewing real errors.'
+        )}
+      </TextWrapper>
+      <Button
+        size="xsmall"
+        priority="primary"
+        to={`/${organization.slug}/${selectedProject.slug}/getting-started/${
+          selectedProject.platform || ''
+        }`}
+        onClick={() =>
+          trackAdvancedAnalyticsEvent('growth.sample_error_onboarding_link_clicked', {
+            project_id: selectedProject.id,
+            organization,
+            platform: selectedProject.platform,
+          })
+        }
+      >
+        {t('Get Started')}
+      </Button>
+    </PageAlertBar>
+  );
+}
+
+export default withProjects(withOrganization(withGlobalSelection(SampleEventAlert)));
+
+const TextWrapper = styled('span')`
+  margin: 0 ${space(1)};
+`;

+ 3 - 12
static/app/views/performance/transactionDetails/finishSetupAlert.tsx

@@ -1,6 +1,7 @@
 import styled from '@emotion/styled';
 
 import Button from 'app/components/button';
+import PageAlertBar from 'app/components/pageAlertBar';
 import {IconLightning} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
@@ -15,7 +16,7 @@ export default function FinishSetupAlert({
   project: Project;
 }) {
   return (
-    <AlertBar>
+    <PageAlertBar>
       <IconLightning />
       <TextWrapper>
         {t(
@@ -37,20 +38,10 @@ export default function FinishSetupAlert({
       >
         {t('Get Started')}
       </Button>
-    </AlertBar>
+    </PageAlertBar>
   );
 }
 
-const AlertBar = styled('div')`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: ${p => p.theme.headerBackground};
-  background-color: ${p => p.theme.bannerBackground};
-  padding: 6px 30px;
-  font-size: 14px;
-`;
-
 const TextWrapper = styled('span')`
   margin: 0 ${space(1)};
 `;

+ 20 - 0
tests/js/spec/views/organizationGroupDetails/groupDetails.spec.jsx

@@ -10,6 +10,9 @@ import GroupDetails from 'app/views/organizationGroupDetails';
 
 jest.unmock('app/utils/recreateRoute');
 
+const SAMPLE_EVENT_ALERT_TEXT =
+  'You are viewing a sample error. Configure Sentry to start viewing real errors.';
+
 describe('groupDetails', () => {
   const group = TestStubs.Group();
   const event = TestStubs.Event();
@@ -216,4 +219,21 @@ describe('groupDetails', () => {
 
     expect(await findByText('New Issue')).toBeTruthy();
   });
+
+  it('renders alert for sample event', async function () {
+    const aProject = TestStubs.Project({firstEvent: false});
+    ProjectsStore.reset();
+    ProjectsStore.loadInitialData([aProject]);
+    const {findByText} = createWrapper();
+
+    expect(await findByText(SAMPLE_EVENT_ALERT_TEXT)).toBeTruthy();
+  });
+  it('does not render alert for non sample events', async function () {
+    const aProject = TestStubs.Project({firstEvent: false});
+    ProjectsStore.reset();
+    ProjectsStore.loadInitialData([aProject]);
+    const {queryByText} = createWrapper();
+
+    expect(await queryByText(SAMPLE_EVENT_ALERT_TEXT)).toBeNull();
+  });
 });