Browse Source

feta(perf-views) Add an onboarding state for performance views (#18960)

Expose the has_transactions flag on projects to conditionally display an
onboarding state for performance views. The icon is temporary until we
get more polished assets from creative.

The getting started state has some copy paste from releases v2 but I'll
clean that up separately.
Mark Story 4 years ago
parent
commit
d1afbada8e

+ 2 - 0
src/sentry/api/serializers/models/project.py

@@ -223,6 +223,7 @@ class ProjectSerializer(Serializer):
             "color": obj.color,
             "dateCreated": obj.date_added,
             "firstEvent": obj.first_event,
+            "firstTransactionEvent": True if obj.flags.has_transactions else False,
             "features": attrs["features"],
             "status": status_label,
             "platform": obj.platform,
@@ -386,6 +387,7 @@ class ProjectSummarySerializer(ProjectWithTeamSerializer):
             "environments": attrs["environments"],
             "features": attrs["features"],
             "firstEvent": obj.first_event,
+            "firstTransactionEvent": True if obj.flags.has_transactions else False,
             "platform": obj.platform,
             "platforms": attrs["platforms"],
             "latestDeploys": attrs["deploys"],

+ 1 - 0
src/sentry/static/sentry/app/types/index.tsx

@@ -149,6 +149,7 @@ export type Project = {
   hasUserReports?: boolean;
   hasAccess: boolean;
   firstEvent: 'string' | null;
+  firstTransactionEvent: boolean;
 
   // XXX: These are part of the DetailedProject serializer
   plugins: Plugin[];

+ 18 - 18
src/sentry/static/sentry/app/views/performance/charts/index.tsx

@@ -94,10 +94,6 @@ class Container extends React.Component<Props> {
               );
             }
 
-            if (!results) {
-              return <LoadingPanel data-test-id="events-request-loading" />;
-            }
-
             return (
               <React.Fragment>
                 <HeaderContainer>
@@ -114,20 +110,24 @@ class Container extends React.Component<Props> {
                     </div>
                   ))}
                 </HeaderContainer>
-                {getDynamicText({
-                  value: (
-                    <Chart
-                      data={results}
-                      loading={loading || reloading}
-                      router={router}
-                      statsPeriod={globalSelection.statsPeriod}
-                      utc={utc === 'true'}
-                      projects={globalSelection.project}
-                      environments={globalSelection.environment}
-                    />
-                  ),
-                  fixed: 'apdex and throughput charts',
-                })}
+                {results ? (
+                  getDynamicText({
+                    value: (
+                      <Chart
+                        data={results}
+                        loading={loading || reloading}
+                        router={router}
+                        statsPeriod={globalSelection.statsPeriod}
+                        utc={utc === 'true'}
+                        projects={globalSelection.project}
+                        environments={globalSelection.environment}
+                      />
+                    ),
+                    fixed: 'apdex and throughput charts',
+                  })
+                ) : (
+                  <LoadingPanel data-test-id="events-request-loading" />
+                )}
               </React.Fragment>
             );
           }}

+ 37 - 19
src/sentry/static/sentry/app/views/performance/landing.tsx

@@ -4,8 +4,7 @@ import * as ReactRouter from 'react-router';
 import styled from '@emotion/styled';
 
 import {t} from 'app/locale';
-import {Organization} from 'app/types';
-import withOrganization from 'app/utils/withOrganization';
+import {Organization, Project} from 'app/types';
 import FeatureBadge from 'app/components/featureBadge';
 import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
@@ -16,10 +15,13 @@ import EventView from 'app/utils/discover/eventView';
 import space from 'app/styles/space';
 import Button from 'app/components/button';
 import ButtonBar from 'app/components/buttonBar';
+import withOrganization from 'app/utils/withOrganization';
+import withProjects from 'app/utils/withProjects';
 
 import {generatePerformanceEventView, DEFAULT_STATS_PERIOD} from './data';
 import Table from './table';
 import Charts from './charts/index';
+import Onboarding from './onboarding';
 
 enum FilterViews {
   ALL_TRANSACTIONS = 'ALL_TRANSACTIONS',
@@ -32,6 +34,8 @@ type Props = {
   organization: Organization;
   location: Location;
   router: ReactRouter.InjectedRouter;
+  projects: Project[];
+  loadingProjects: boolean;
 };
 
 type State = {
@@ -108,9 +112,16 @@ class PerformanceLanding extends React.Component<Props, State> {
   }
 
   render() {
-    const {organization, location, router} = this.props;
+    const {organization, location, router, projects} = this.props;
     const {eventView} = this.state;
 
+    const noFirstEvent =
+      projects.filter(
+        p =>
+          eventView.project.includes(parseInt(p.id, 10)) &&
+          p.firstTransactionEvent === false
+      ).length === eventView.project.length;
+
     return (
       <SentryDocumentTitle title={t('Performance')} objSlug={organization.slug}>
         <GlobalSelectionHeader
@@ -129,23 +140,30 @@ class PerformanceLanding extends React.Component<Props, State> {
                 <div>
                   {t('Performance')} <FeatureBadge type="beta" />
                 </div>
-                <div>{this.renderHeaderButtons()}</div>
+                {!noFirstEvent && <div>{this.renderHeaderButtons()}</div>}
               </StyledPageHeader>
               {this.renderError()}
-              <Charts
-                eventView={eventView}
-                organization={organization}
-                location={location}
-                router={router}
-                keyTransactions={this.state.currentView === 'KEY_TRANSACTIONS'}
-              />
-              <Table
-                eventView={eventView}
-                organization={organization}
-                location={location}
-                setError={this.setError}
-                keyTransactions={this.state.currentView === 'KEY_TRANSACTIONS'}
-              />
+              {noFirstEvent ? (
+                <Onboarding />
+              ) : (
+                <React.Fragment>
+                  <Charts
+                    eventView={eventView}
+                    organization={organization}
+                    location={location}
+                    router={router}
+                    keyTransactions={this.state.currentView === 'KEY_TRANSACTIONS'}
+                  />
+                  <Table
+                    eventView={eventView}
+                    projects={projects}
+                    organization={organization}
+                    location={location}
+                    setError={this.setError}
+                    keyTransactions={this.state.currentView === 'KEY_TRANSACTIONS'}
+                  />
+                </React.Fragment>
+              )}
             </LightWeightNoProjectMessage>
           </PageContent>
         </GlobalSelectionHeader>
@@ -164,4 +182,4 @@ export const StyledPageHeader = styled('div')`
   margin-bottom: ${space(1)};
 `;
 
-export default withOrganization(PerformanceLanding);
+export default withOrganization(withProjects(PerformanceLanding));

+ 57 - 0
src/sentry/static/sentry/app/views/performance/onboarding.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import styled from '@emotion/styled';
+
+import Button from 'app/components/button';
+import {IconLightning} from 'app/icons';
+import {Panel} from 'app/components/panels';
+import {t} from 'app/locale';
+import space from 'app/styles/space';
+
+function Onboarding() {
+  return (
+    <Panel>
+      <Container>
+        <IllustrationContainer>
+          <IconLightning size="200px" />
+        </IllustrationContainer>
+        <StyledBox>
+          <h3>{t('No transactions yet')}</h3>
+          <p>
+            {t(
+              'View transactions sorted by slowest duration time, related issues, and number of users having a slow experience in one consolidated view. Trace those 10-second page loads to poor-performing API calls and its children.'
+            )}
+          </p>
+          <Button
+            priority="primary"
+            target="_blank"
+            href="https://docs.sentry.io/performance/distributed-tracing/#setting-up-tracing"
+          >
+            {t('Start Setup')}
+          </Button>
+        </StyledBox>
+      </Container>
+    </Panel>
+  );
+}
+
+const Container = styled('div')`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  min-height: 450px;
+  padding: ${space(1)};
+`;
+
+const StyledBox = styled('div')`
+  flex: 1;
+  padding: ${space(3)};
+`;
+
+const IllustrationContainer = styled(StyledBox)`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+export default Onboarding;

+ 33 - 33
src/sentry/static/sentry/app/views/performance/table.tsx

@@ -14,7 +14,6 @@ import GridEditable, {COL_WIDTH_UNDEFINED, GridColumn} from 'app/components/grid
 import SortLink from 'app/components/gridEditable/sortLink';
 import HeaderCell from 'app/views/eventsV2/table/headerCell';
 import {decodeScalar} from 'app/utils/queryString';
-import withProjects from 'app/utils/withProjects';
 import SearchBar from 'app/components/searchBar';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
@@ -50,7 +49,6 @@ type Props = {
   keyTransactions: boolean;
 
   projects: Project[];
-  loadingProjects: boolean;
 };
 
 type State = {
@@ -193,37 +191,39 @@ class Table extends React.Component<Props, State> {
       });
 
     const columnSortBy = eventView.getSorts();
-
+    const filterString = this.getTransactionSearchQuery();
     return (
-      <DiscoverQuery
-        eventView={eventView}
-        orgSlug={organization.slug}
-        location={location}
-        keyTransactions={keyTransactions}
-      >
-        {({pageLinks, isLoading, tableData}) => (
-          <div>
-            <StyledSearchBar
-              query={this.getTransactionSearchQuery()}
-              placeholder={t('Filter Transactions')}
-              onSearch={this.handleTransactionSearchQuery}
-            />
-            <GridEditable
-              isLoading={isLoading}
-              data={tableData ? tableData.data : []}
-              columnOrder={columnOrder}
-              columnSortBy={columnSortBy}
-              grid={{
-                onResizeColumn: this.handleResizeColumn,
-                renderHeadCell: this.renderHeadCell(tableData?.meta) as any,
-                renderBodyCell: this.renderBodyCell(tableData?.meta) as any,
-              }}
-              location={location}
-            />
-            <Pagination pageLinks={pageLinks} />
-          </div>
-        )}
-      </DiscoverQuery>
+      <div>
+        <StyledSearchBar
+          query={filterString}
+          placeholder={t('Filter Transactions')}
+          onSearch={this.handleTransactionSearchQuery}
+        />
+        <DiscoverQuery
+          eventView={eventView}
+          orgSlug={organization.slug}
+          location={location}
+          keyTransactions={keyTransactions}
+        >
+          {({pageLinks, isLoading, tableData}) => (
+            <React.Fragment>
+              <GridEditable
+                isLoading={isLoading}
+                data={tableData ? tableData.data : []}
+                columnOrder={columnOrder}
+                columnSortBy={columnSortBy}
+                grid={{
+                  onResizeColumn: this.handleResizeColumn,
+                  renderHeadCell: this.renderHeadCell(tableData?.meta) as any,
+                  renderBodyCell: this.renderBodyCell(tableData?.meta) as any,
+                }}
+                location={location}
+              />
+              <Pagination pageLinks={pageLinks} />
+            </React.Fragment>
+          )}
+        </DiscoverQuery>
+      </div>
     );
   }
 }
@@ -234,4 +234,4 @@ const StyledSearchBar = styled(SearchBar)`
   margin-bottom: ${space(1)};
 `;
 
-export default withProjects(Table);
+export default Table;

+ 5 - 2
tests/acceptance/test_performance_overview.py

@@ -6,6 +6,8 @@ import time
 
 from mock import patch
 
+from django.db.models import F
+from sentry.models import Project
 from sentry.testutils import AcceptanceTestCase
 from sentry.testutils.helpers.datetime import before_now
 from sentry.utils.samples import load_data
@@ -48,13 +50,13 @@ class PerformanceOverviewTest(AcceptanceTestCase):
         self.dismiss_assistant()
 
     @patch("django.utils.timezone.now")
-    def test_empty_state(self, mock_now):
+    def test_onboarding(self, mock_now):
         mock_now.return_value = before_now().replace(tzinfo=pytz.utc)
 
         with self.feature(FEATURE_NAMES):
             self.browser.get(self.path)
             self.page.wait_until_loaded()
-            self.browser.snapshot("performance overview - empty")
+            self.browser.snapshot("performance overview - onboarding")
 
     @patch("django.utils.timezone.now")
     def test_with_data(self, mock_now):
@@ -62,6 +64,7 @@ class PerformanceOverviewTest(AcceptanceTestCase):
 
         event = make_event(load_data("transaction"))
         self.store_event(data=event, project_id=self.project.id)
+        self.project.update(flags=F("flags").bitor(Project.flags.has_transactions))
 
         with self.feature(FEATURE_NAMES):
             self.browser.get(self.path)

+ 14 - 0
tests/sentry/api/serializers/test_project.py

@@ -6,6 +6,7 @@ from datetime import timedelta
 
 import six
 import datetime
+from django.db.models import F
 from django.utils import timezone
 from exam import fixture
 
@@ -21,6 +22,7 @@ from sentry.models import (
     Deploy,
     Environment,
     EnvironmentProject,
+    Project,
     Release,
     ReleaseProjectEnvironment,
     UserReport,
@@ -264,6 +266,18 @@ class ProjectSummarySerializerTest(TestCase):
         assert result["latestRelease"] == {"version": self.release.version}
         assert result["environments"] == ["production", "staging"]
 
+    def test_first_event_properties(self):
+        result = serialize(self.project, self.user, ProjectSummarySerializer())
+        assert result["firstEvent"] is None
+        assert result["firstTransactionEvent"] is False
+
+        self.project.first_event = timezone.now()
+        self.project.update(flags=F("flags").bitor(Project.flags.has_transactions))
+
+        result = serialize(self.project, self.user, ProjectSummarySerializer())
+        assert result["firstEvent"]
+        assert result["firstTransactionEvent"] is True
+
     def test_user_reports(self):
         result = serialize(self.project, self.user, ProjectSummarySerializer())
         assert result["hasUserReports"] is False