Browse Source

feat(performance): Add guide anchor for team key transactions (#26572)

This adds in an introduction for the new team key transactions as wells as to
let existing users know that if they previously had key transactions, they'll
have to migrate them over by re-keying them for their teams.
Tony Xiao 3 years ago
parent
commit
40a7983cba

+ 6 - 0
src/sentry/api/urls.py

@@ -6,6 +6,7 @@ from sentry.discover.endpoints.discover_key_transactions import (
     IsKeyTransactionEndpoint,
     KeyTransactionEndpoint,
     KeyTransactionListEndpoint,
+    LegacyKeyTransactionCountEndpoint,
 )
 from sentry.discover.endpoints.discover_query import DiscoverQueryEndpoint
 from sentry.discover.endpoints.discover_saved_queries import DiscoverSavedQueriesEndpoint
@@ -801,6 +802,11 @@ urlpatterns = [
                     KeyTransactionListEndpoint.as_view(),
                     name="sentry-api-0-organization-key-transactions-list",
                 ),
+                url(
+                    r"^(?P<organization_slug>[^\/]+)/legacy-key-transactions-count/$",
+                    LegacyKeyTransactionCountEndpoint.as_view(),
+                    name="sentry-api-0-organization-legacy-key-transactions-count",
+                ),
                 url(
                     r"^(?P<organization_slug>[^\/]+)/is-key-transactions/$",
                     IsKeyTransactionEndpoint.as_view(),

+ 1 - 0
src/sentry/assistant/guides.py

@@ -40,6 +40,7 @@ GUIDES = {
             "tag_explorer",
         ],
     },
+    "team_key_transactions": {"id": 18, "required_targets": ["team_key_transaction_header"]},
 }
 
 # demo mode has different guides

+ 26 - 0
src/sentry/discover/endpoints/discover_key_transactions.py

@@ -24,6 +24,32 @@ class KeyTransactionPermission(OrganizationPermission):
     }
 
 
+class LegacyKeyTransactionCountEndpoint(KeyTransactionBase):
+    permission_classes = (KeyTransactionPermission,)
+
+    def get(self, request, organization):
+        """
+        Check how many legacy Key Transactions a user has
+
+        This is used to show the guide to users who previously had key
+        transactions to update their team key transactions
+        """
+        if not self.has_feature(request, organization):
+            return Response(status=404)
+
+        projects = self.get_projects(request, organization)
+
+        try:
+            count = KeyTransaction.objects.filter(
+                organization=organization,
+                owner=request.user,
+                project__in=projects,
+            ).count()
+            return Response({"keyed": count}, status=200)
+        except KeyTransaction.DoesNotExist:
+            return Response({"keyed": 0}, status=200)
+
+
 class IsKeyTransactionEndpoint(KeyTransactionBase):
     permission_classes = (KeyTransactionPermission,)
 

+ 11 - 0
static/app/actionCreators/performance.tsx

@@ -20,6 +20,17 @@ type TeamKeyTransaction = {
 
 export type TeamKeyTransactions = TeamKeyTransaction[];
 
+export async function fetchLegacyKeyTransactionsCount(orgSlug): Promise<number> {
+  const api = new Client();
+  const url = `/organizations/${orgSlug}/legacy-key-transactions-count/`;
+
+  const [data] = await api.requestPromise(url, {
+    method: 'GET',
+    includeAllArgs: true,
+  });
+  return data.keyed;
+}
+
 export async function fetchTeamKeyTransactions(
   api: Client,
   orgSlug: string,

+ 22 - 0
static/app/components/assistant/getGuidesContent.tsx

@@ -286,6 +286,28 @@ export default function getGuidesContent(orgSlug: string | null): GuidesContent
         },
       ],
     },
+    {
+      guide: 'team_key_transactions',
+      requiredTargets: ['team_key_transaction_header'],
+      steps: [
+        {
+          title: t('Key Transactions'),
+          target: 'team_key_transaction_header',
+          description: t(
+            'Software development is a team sport. Key Transactions allow you to mark important transactions and share them with your team.'
+          ),
+          nextText: t('Great'),
+        },
+        {
+          title: t('Migrating Key Transactions'),
+          target: 'team_key_transaction_existing',
+          description: t(
+            'To migrate your previous key transactions, you will have to mark them as a key transaction again for your team. Sorry about that.'
+          ),
+          nextText: t('Fine'),
+        },
+      ],
+    },
   ];
 }
 

+ 38 - 7
static/app/views/performance/table.tsx

@@ -2,7 +2,8 @@ import * as React from 'react';
 import * as ReactRouter from 'react-router';
 import {Location, LocationDescriptorObject} from 'history';
 
-import {GuideAnchor} from 'app/components/assistant/guideAnchor';
+import {fetchLegacyKeyTransactionsCount} from 'app/actionCreators/performance';
+import GuideAnchor from 'app/components/assistant/guideAnchor';
 import GridEditable, {COL_WIDTH_UNDEFINED, GridColumn} from 'app/components/gridEditable';
 import SortLink from 'app/components/gridEditable/sortLink';
 import Link from 'app/components/links/link';
@@ -55,12 +56,28 @@ type Props = {
 
 type State = {
   widths: number[];
+  keyedTransactions: number | null;
 };
 class Table extends React.Component<Props, State> {
   state: State = {
     widths: [],
+    keyedTransactions: null,
   };
 
+  componentDidMount() {
+    this.fetchKeyTransactionCount();
+  }
+
+  async fetchKeyTransactionCount() {
+    const {organization} = this.props;
+    try {
+      const count = await fetchLegacyKeyTransactionsCount(organization.slug);
+      this.setState({keyedTransactions: count});
+    } catch (error) {
+      this.setState({keyedTransactions: null});
+    }
+  }
+
   handleCellAction = (column: TableColumn<keyof TableDataRow>) => {
     return (action: Actions, value: React.ReactText) => {
       const {eventView, location, organization} = this.props;
@@ -261,6 +278,8 @@ class Table extends React.Component<Props, State> {
 
   renderPrependCellWithData = (tableData: TableData | null) => {
     const {eventView} = this.props;
+    const {keyedTransactions} = this.state;
+
     const keyTransactionColumn = eventView
       .getColumns()
       .find((col: TableColumn<React.ReactText>) => col.name === 'key_transaction');
@@ -285,12 +304,24 @@ class Table extends React.Component<Props, State> {
       } else if (teamKeyTransactionColumn) {
         if (isHeader) {
           const star = (
-            <IconStar
-              key="keyTransaction"
-              color="yellow300"
-              isSolid
-              data-test-id="team-key-transaction-header"
-            />
+            <GuideAnchor
+              target="team_key_transaction_header"
+              position="top"
+              disabled={keyedTransactions === null} // wait for the legacy counts to load
+            >
+              <GuideAnchor
+                target="team_key_transaction_existing"
+                position="top"
+                disabled={!keyedTransactions}
+              >
+                <IconStar
+                  key="keyTransaction"
+                  color="yellow300"
+                  isSolid
+                  data-test-id="team-key-transaction-header"
+                />
+              </GuideAnchor>
+            </GuideAnchor>
           );
           return [this.renderHeadCell(tableData?.meta, teamKeyTransactionColumn, star)];
         } else {

+ 5 - 0
tests/js/spec/views/performance/content.spec.jsx

@@ -243,6 +243,11 @@ describe('Performance > Content', function () {
       url: `/organizations/org-slug/key-transactions-list/`,
       body: [],
     });
+    MockApiClient.addMockResponse({
+      method: 'GET',
+      url: `/organizations/org-slug/legacy-key-transactions-count/`,
+      body: [],
+    });
   });
 
   afterEach(function () {

+ 30 - 0
tests/snuba/api/endpoints/test_discover_key_transactions.py

@@ -1251,3 +1251,33 @@ class KeyTransactionTest(APITestCase, SnubaTestCase):
         url = reverse("sentry-api-0-organization-is-key-transactions", args=[self.org.slug])
         response = self.client.get(url)
         assert response.status_code == 404
+
+    def test_legacy_key_transactions_count(self):
+        with self.feature("organizations:performance-view"):
+            url = reverse(
+                "sentry-api-0-organization-legacy-key-transactions-count", args=[self.org.slug]
+            )
+            response = self.client.get(url, {"project": [self.project.id]})
+
+        assert response.status_code == 200
+        assert response.data["keyed"] == 0
+
+        event_data = load_data("transaction")
+        start_timestamp = iso_format(before_now(minutes=1))
+        end_timestamp = iso_format(before_now(minutes=1))
+        event_data.update({"start_timestamp": start_timestamp, "timestamp": end_timestamp})
+        KeyTransaction.objects.create(
+            owner=self.user,
+            organization=self.org,
+            transaction=event_data["transaction"],
+            project=self.project,
+        )
+
+        with self.feature("organizations:performance-view"):
+            url = reverse(
+                "sentry-api-0-organization-legacy-key-transactions-count", args=[self.org.slug]
+            )
+            response = self.client.get(url, {"project": [self.project.id]})
+
+        assert response.status_code == 200
+        assert response.data["keyed"] == 1