Просмотр исходного кода

feat(issues): New issue details guide (#66850)

Scott Cooper 1 год назад
Родитель
Сommit
bc8f29d556

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

@@ -11,37 +11,36 @@ export default function getGuidesContent(orgSlug: string | null): GuidesContent
   return [
   return [
     {
     {
       guide: 'issue',
       guide: 'issue',
-      requiredTargets: ['issue_number', 'exception'],
+      requiredTargets: ['issue_header_stats', 'breadcrumbs', 'issue_sidebar_owners'],
       steps: [
       steps: [
         {
         {
-          title: t('Identify Your Issues'),
-          target: 'issue_number',
-          description: tct(
-            `You have Issues. That's fine. Use the Issue number in your commit message,
-                and we'll automatically resolve the Issue when your code is deployed. [link:Learn more]`,
-            {link: <ExternalLink href="https://docs.sentry.io/product/releases/" />}
+          title: t('How bad is it?'),
+          target: 'issue_header_stats',
+          description: t(
+            `You have Issues and that's fine.
+              Understand impact at a glance by viewing total issue frequency and affected users.`
           ),
           ),
         },
         },
         {
         {
-          title: t('Annoy the Right People'),
-          target: 'owners',
-          description: tct(
-            `Notification overload makes it tempting to hurl your phone into the ocean.
-                Define who is responsible for what, so alerts reach the right people and your
-                devices stay on dry land. [link:Learn more]`,
-            {
-              link: (
-                <ExternalLink href="https://docs.sentry.io/product/error-monitoring/issue-owners/" />
-              ),
-            }
+          title: t('Find problematic releases'),
+          target: 'issue_sidebar_releases',
+          description: t(
+            `See which release introduced the issue and which release it last appeared in.`
+          ),
+        },
+        {
+          title: t('Not your typical stack trace'),
+          target: 'stacktrace',
+          description: t(
+            `Sentry can show your source code in the stack trace.
+              See the exact sequence of function calls leading to the error in question.`
           ),
           ),
         },
         },
         {
         {
-          title: t('Narrow Down Suspects'),
-          target: 'exception',
+          title: t('Pinpoint hotspots'),
+          target: 'issue_sidebar_tags',
           description: t(
           description: t(
-            `We've got stack trace. See the exact sequence of function calls leading to the error
-                in question, no detective skills necessary.`
+            `Tags are key/value string pairs that are automatically indexed and searchable in Sentry.`
           ),
           ),
         },
         },
         {
         {
@@ -52,6 +51,20 @@ export default function getGuidesContent(orgSlug: string | null): GuidesContent
                 frameworks to lead you straight to your error.`
                 frameworks to lead you straight to your error.`
           ),
           ),
         },
         },
+        {
+          title: t('Annoy the Right People'),
+          target: 'issue_sidebar_owners',
+          description: tct(
+            `Notification overload makes it tempting to hurl your phone into the ocean.
+                Define who is responsible for what, so alerts reach the right people and your
+                devices stay on dry land. [link:Learn more]`,
+            {
+              link: (
+                <ExternalLink href="https://docs.sentry.io/product/error-monitoring/issue-owners/" />
+              ),
+            }
+          ),
+        },
       ],
       ],
     },
     },
     {
     {

+ 20 - 14
static/app/components/assistant/guideAnchor.spec.tsx

@@ -14,6 +14,7 @@ describe('GuideAnchor', function () {
       seen: false,
       seen: false,
     },
     },
   ];
   ];
+  const firstGuideHeader = 'How bad is it?';
 
 
   beforeEach(function () {
   beforeEach(function () {
     ConfigStore.config = ConfigFixture({
     ConfigStore.config = ConfigFixture({
@@ -27,13 +28,14 @@ describe('GuideAnchor', function () {
   it('renders, async advances, async and finishes', async function () {
   it('renders, async advances, async and finishes', async function () {
     render(
     render(
       <div>
       <div>
-        <GuideAnchor target="issue_number" />
-        <GuideAnchor target="exception" />
+        <GuideAnchor target="issue_header_stats" />
+        <GuideAnchor target="breadcrumbs" />
+        <GuideAnchor target="issue_sidebar_owners" />
       </div>
       </div>
     );
     );
 
 
     act(() => GuideStore.fetchSucceeded(serverGuide));
     act(() => GuideStore.fetchSucceeded(serverGuide));
-    expect(await screen.findByText('Identify Your Issues')).toBeInTheDocument();
+    expect(await screen.findByText(firstGuideHeader)).toBeInTheDocument();
 
 
     // XXX(epurkhiser): Skip pointer event checks due to a bug with how Popper
     // XXX(epurkhiser): Skip pointer event checks due to a bug with how Popper
     // renders the hovercard with pointer-events: none. See [0]
     // renders the hovercard with pointer-events: none. See [0]
@@ -44,8 +46,10 @@ describe('GuideAnchor', function () {
     // when we're on popper >= 1.
     // when we're on popper >= 1.
     await userEvent.click(screen.getByLabelText('Next'));
     await userEvent.click(screen.getByLabelText('Next'));
 
 
-    expect(await screen.findByText('Narrow Down Suspects')).toBeInTheDocument();
-    expect(screen.queryByText('Identify Your Issues')).not.toBeInTheDocument();
+    expect(await screen.findByText('Retrace Your Steps')).toBeInTheDocument();
+    expect(screen.queryByText(firstGuideHeader)).not.toBeInTheDocument();
+
+    await userEvent.click(screen.getByLabelText('Next'));
 
 
     // Clicking on the button in the last step should finish the guide.
     // Clicking on the button in the last step should finish the guide.
     const finishMock = MockApiClient.addMockResponse({
     const finishMock = MockApiClient.addMockResponse({
@@ -70,13 +74,14 @@ describe('GuideAnchor', function () {
   it('dismisses', async function () {
   it('dismisses', async function () {
     render(
     render(
       <div>
       <div>
-        <GuideAnchor target="issue_number" />
-        <GuideAnchor target="exception" />
+        <GuideAnchor target="issue_header_stats" />
+        <GuideAnchor target="breadcrumbs" />
+        <GuideAnchor target="issue_sidebar_owners" />
       </div>
       </div>
     );
     );
 
 
     act(() => GuideStore.fetchSucceeded(serverGuide));
     act(() => GuideStore.fetchSucceeded(serverGuide));
-    expect(await screen.findByText('Identify Your Issues')).toBeInTheDocument();
+    expect(await screen.findByText(firstGuideHeader)).toBeInTheDocument();
 
 
     const dismissMock = MockApiClient.addMockResponse({
     const dismissMock = MockApiClient.addMockResponse({
       method: 'PUT',
       method: 'PUT',
@@ -96,7 +101,7 @@ describe('GuideAnchor', function () {
       })
       })
     );
     );
 
 
-    expect(screen.queryByText('Identify Your Issues')).not.toBeInTheDocument();
+    expect(screen.queryByText(firstGuideHeader)).not.toBeInTheDocument();
   });
   });
 
 
   it('renders no container when inactive', function () {
   it('renders no container when inactive', function () {
@@ -124,16 +129,17 @@ describe('GuideAnchor', function () {
   it('if forceHide is true, async do not render guide', async function () {
   it('if forceHide is true, async do not render guide', async function () {
     render(
     render(
       <div>
       <div>
-        <GuideAnchor target="issue_number" />
-        <GuideAnchor target="exception" />
+        <GuideAnchor target="issue_header_stats" />
+        <GuideAnchor target="breadcrumbs" />
+        <GuideAnchor target="issue_sidebar_owners" />
       </div>
       </div>
     );
     );
 
 
     act(() => GuideStore.fetchSucceeded(serverGuide));
     act(() => GuideStore.fetchSucceeded(serverGuide));
-    expect(await screen.findByText('Identify Your Issues')).toBeInTheDocument();
+    expect(await screen.findByText(firstGuideHeader)).toBeInTheDocument();
     act(() => GuideStore.setForceHide(true));
     act(() => GuideStore.setForceHide(true));
-    expect(screen.queryByText('Identify Your Issues')).not.toBeInTheDocument();
+    expect(screen.queryByText(firstGuideHeader)).not.toBeInTheDocument();
     act(() => GuideStore.setForceHide(false));
     act(() => GuideStore.setForceHide(false));
-    expect(await screen.findByText('Identify Your Issues')).toBeInTheDocument();
+    expect(await screen.findByText(firstGuideHeader)).toBeInTheDocument();
   });
   });
 });
 });

+ 11 - 13
static/app/components/events/interfaces/breadcrumbs/index.tsx

@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
 import omit from 'lodash/omit';
 import omit from 'lodash/omit';
 import pick from 'lodash/pick';
 import pick from 'lodash/pick';
 
 
-import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import {Button} from 'sentry/components/button';
 import {Button} from 'sentry/components/button';
 import type {SelectOption, SelectSection} from 'sentry/components/compactSelect';
 import type {SelectOption, SelectSection} from 'sentry/components/compactSelect';
 import {CompactSelect} from 'sentry/components/compactSelect';
 import {CompactSelect} from 'sentry/components/compactSelect';
@@ -312,21 +311,20 @@ function BreadcrumbsContainer({data, event, organization, hideTitle = false}: Pr
       showPermalink={!hideTitle}
       showPermalink={!hideTitle}
       type={EntryType.BREADCRUMBS}
       type={EntryType.BREADCRUMBS}
       title={hideTitle ? '' : t('Breadcrumbs')}
       title={hideTitle ? '' : t('Breadcrumbs')}
+      guideTarget="breadcrumbs"
       actions={actions}
       actions={actions}
     >
     >
       <ErrorBoundary>
       <ErrorBoundary>
-        <GuideAnchor target="breadcrumbs" position="bottom">
-          <Breadcrumbs
-            emptyMessage={getEmptyMessage()}
-            breadcrumbs={displayedBreadcrumbs}
-            event={event}
-            organization={organization}
-            onSwitchTimeFormat={() => setDisplayRelativeTime(old => !old)}
-            displayRelativeTime={displayRelativeTime}
-            searchTerm={searchTerm}
-            relativeTime={relativeTime}
-          />
-        </GuideAnchor>
+        <Breadcrumbs
+          emptyMessage={getEmptyMessage()}
+          breadcrumbs={displayedBreadcrumbs}
+          event={event}
+          organization={organization}
+          onSwitchTimeFormat={() => setDisplayRelativeTime(old => !old)}
+          displayRelativeTime={displayRelativeTime}
+          searchTerm={searchTerm}
+          relativeTime={relativeTime}
+        />
       </ErrorBoundary>
       </ErrorBoundary>
     </EventDataSection>
     </EventDataSection>
   );
   );

+ 1 - 0
static/app/components/events/traceEventDataSection.tsx

@@ -354,6 +354,7 @@ export function TraceEventDataSection({
     <EventDataSection
     <EventDataSection
       type={type}
       type={type}
       title={cloneElement(title, {type})}
       title={cloneElement(title, {type})}
+      guideTarget="stacktrace"
       actions={
       actions={
         !stackTraceNotFound && (
         !stackTraceNotFound && (
           <ButtonBar gap={1}>
           <ButtonBar gap={1}>

+ 17 - 14
static/app/components/group/assignedTo.tsx

@@ -9,6 +9,7 @@ import type {
   SuggestedAssignee,
   SuggestedAssignee,
 } from 'sentry/components/assigneeSelectorDropdown';
 } from 'sentry/components/assigneeSelectorDropdown';
 import {AssigneeSelectorDropdown} from 'sentry/components/assigneeSelectorDropdown';
 import {AssigneeSelectorDropdown} from 'sentry/components/assigneeSelectorDropdown';
+import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import {Button} from 'sentry/components/button';
 import {Button} from 'sentry/components/button';
 import {AutoCompleteRoot} from 'sentry/components/dropdownAutoComplete/menu';
 import {AutoCompleteRoot} from 'sentry/components/dropdownAutoComplete/menu';
@@ -219,20 +220,22 @@ function AssignedTo({
       <StyledSidebarTitle>
       <StyledSidebarTitle>
         {t('Assigned To')}
         {t('Assigned To')}
         <Access access={['project:read']}>
         <Access access={['project:read']}>
-          <Button
-            onClick={() => {
-              openIssueOwnershipRuleModal({
-                project,
-                organization,
-                issueId: group.id,
-                eventData: event!,
-              });
-            }}
-            aria-label={t('Create Ownership Rule')}
-            icon={<IconSettings />}
-            borderless
-            size="xs"
-          />
+          <GuideAnchor target="issue_sidebar_owners" position="bottom">
+            <Button
+              onClick={() => {
+                openIssueOwnershipRuleModal({
+                  project,
+                  organization,
+                  issueId: group.id,
+                  eventData: event!,
+                });
+              }}
+              aria-label={t('Create Ownership Rule')}
+              icon={<IconSettings />}
+              borderless
+              size="xs"
+            />
+          </GuideAnchor>
         </Access>
         </Access>
       </StyledSidebarTitle>
       </StyledSidebarTitle>
       <StyledSidebarSectionContent>
       <StyledSidebarSectionContent>

+ 4 - 1
static/app/components/group/releaseStats.tsx

@@ -2,6 +2,7 @@ import {Fragment, memo} from 'react';
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
 import AlertLink from 'sentry/components/alertLink';
 import AlertLink from 'sentry/components/alertLink';
+import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import GroupReleaseChart from 'sentry/components/group/releaseChart';
 import GroupReleaseChart from 'sentry/components/group/releaseChart';
 import SeenInfo from 'sentry/components/group/seenInfo';
 import SeenInfo from 'sentry/components/group/seenInfo';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
@@ -105,7 +106,9 @@ function GroupReleaseStats({
 
 
           <SidebarSection.Wrap>
           <SidebarSection.Wrap>
             <SidebarSection.Title>
             <SidebarSection.Title>
-              {t('Last Seen')}
+              <GuideAnchor target="issue_sidebar_releases" position="left">
+                {t('Last Seen')}
+              </GuideAnchor>
               <QuestionTooltip
               <QuestionTooltip
                 title={t('When the most recent event in this issue was captured.')}
                 title={t('When the most recent event in this issue was captured.')}
                 size="xs"
                 size="xs"

+ 4 - 1
static/app/components/group/tagFacets/index.tsx

@@ -6,6 +6,7 @@ import keyBy from 'lodash/keyBy';
 
 
 import type {Tag} from 'sentry/actionCreators/events';
 import type {Tag} from 'sentry/actionCreators/events';
 import type {GroupTagResponseItem} from 'sentry/actionCreators/group';
 import type {GroupTagResponseItem} from 'sentry/actionCreators/group';
+import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import LoadingError from 'sentry/components/loadingError';
 import LoadingError from 'sentry/components/loadingError';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import QuestionTooltip from 'sentry/components/questionTooltip';
@@ -242,7 +243,9 @@ function WrapperWithTitle({children}: {children: ReactNode}) {
   return (
   return (
     <SidebarSection.Wrap>
     <SidebarSection.Wrap>
       <SidebarSection.Title>
       <SidebarSection.Title>
-        {t('All Tags')}
+        <GuideAnchor target="issue_sidebar_tags" position="left">
+          {t('All Tags')}
+        </GuideAnchor>
         <QuestionTooltip
         <QuestionTooltip
           size="xs"
           size="xs"
           position="top"
           position="top"

+ 2 - 2
static/app/stores/guideStore.spec.tsx

@@ -28,8 +28,8 @@ describe('GuideStore', function () {
       },
       },
       {guide: 'issue_stream', seen: true},
       {guide: 'issue_stream', seen: true},
     ];
     ];
-    GuideStore.registerAnchor('issue_number');
-    GuideStore.registerAnchor('exception');
+    GuideStore.registerAnchor('issue_header_stats');
+    GuideStore.registerAnchor('issue_sidebar_owners');
     GuideStore.registerAnchor('breadcrumbs');
     GuideStore.registerAnchor('breadcrumbs');
     GuideStore.registerAnchor('issue_stream');
     GuideStore.registerAnchor('issue_stream');
   });
   });

+ 9 - 6
static/app/views/issueDetails/header.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
 import type {LocationDescriptor} from 'history';
 import type {LocationDescriptor} from 'history';
 import omit from 'lodash/omit';
 import omit from 'lodash/omit';
 
 
+import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import Badge from 'sentry/components/badge';
 import Badge from 'sentry/components/badge';
 import {Breadcrumbs} from 'sentry/components/breadcrumbs';
 import {Breadcrumbs} from 'sentry/components/breadcrumbs';
 import Count from 'sentry/components/count';
 import Count from 'sentry/components/count';
@@ -283,12 +284,14 @@ function GroupHeader({
           </TitleWrapper>
           </TitleWrapper>
           {issueTypeConfig.stats.enabled && (
           {issueTypeConfig.stats.enabled && (
             <StatsWrapper>
             <StatsWrapper>
-              <div className="count">
-                <h6 className="nav-header">{t('Events')}</h6>
-                <Link disabled={disableActions} to={eventRoute}>
-                  <Count className="count" value={group.count} />
-                </Link>
-              </div>
+              <GuideAnchor target="issue_header_stats">
+                <div className="count">
+                  <h6 className="nav-header">{t('Events')}</h6>
+                  <Link disabled={disableActions} to={eventRoute}>
+                    <Count className="count" value={group.count} />
+                  </Link>
+                </div>
+              </GuideAnchor>
               <div className="count">
               <div className="count">
                 <h6 className="nav-header">{t('Users')}</h6>
                 <h6 className="nav-header">{t('Users')}</h6>
                 {userCount !== 0 ? (
                 {userCount !== 0 ? (

+ 47 - 50
static/app/views/issueDetails/shortIdBreadcrumb.tsx

@@ -1,6 +1,5 @@
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
-import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ShortId from 'sentry/components/shortId';
 import ShortId from 'sentry/components/shortId';
@@ -47,56 +46,54 @@ export function ShortIdBreadcrumb({
   }
   }
 
 
   return (
   return (
-    <GuideAnchor target="issue_number" position="bottom">
-      <Wrapper>
-        <ProjectBadge
-          project={project}
-          avatarSize={16}
-          hideName
-          avatarProps={{hasTooltip: true, tooltip: project.slug}}
+    <Wrapper>
+      <ProjectBadge
+        project={project}
+        avatarSize={16}
+        hideName
+        avatarProps={{hasTooltip: true, tooltip: project.slug}}
+      />
+      <ShortIdCopyable>
+        <Tooltip
+          className="help-link"
+          title={t(
+            'This identifier is unique across your organization, and can be used to reference an issue in various places, like commit messages.'
+          )}
+          position="bottom"
+          delay={1000}
+        >
+          <StyledShortId shortId={group.shortId} />
+        </Tooltip>
+        <DropdownMenu
+          triggerProps={{
+            'aria-label': t('Short-ID copy actions'),
+            icon: <IconChevron direction="down" size="xs" />,
+            size: 'zero',
+            borderless: true,
+            showChevron: false,
+          }}
+          position="bottom"
+          size="xs"
+          items={[
+            {
+              key: 'copy-url',
+              label: t('Copy Issue URL'),
+              onAction: handleCopyUrl,
+            },
+            {
+              key: 'copy-short-id',
+              label: t('Copy Short-ID'),
+              onAction: handleCopyShortId,
+            },
+            {
+              key: 'copy-markdown-link',
+              label: t('Copy Markdown Link'),
+              onAction: handleCopyMarkdown,
+            },
+          ]}
         />
         />
-        <ShortIdCopyable>
-          <Tooltip
-            className="help-link"
-            title={t(
-              'This identifier is unique across your organization, and can be used to reference an issue in various places, like commit messages.'
-            )}
-            position="bottom"
-            delay={1000}
-          >
-            <StyledShortId shortId={group.shortId} />
-          </Tooltip>
-          <DropdownMenu
-            triggerProps={{
-              'aria-label': t('Short-ID copy actions'),
-              icon: <IconChevron direction="down" size="xs" />,
-              size: 'zero',
-              borderless: true,
-              showChevron: false,
-            }}
-            position="bottom"
-            size="xs"
-            items={[
-              {
-                key: 'copy-url',
-                label: t('Copy Issue URL'),
-                onAction: handleCopyUrl,
-              },
-              {
-                key: 'copy-short-id',
-                label: t('Copy Short-ID'),
-                onAction: handleCopyShortId,
-              },
-              {
-                key: 'copy-markdown-link',
-                label: t('Copy Markdown Link'),
-                onAction: handleCopyMarkdown,
-              },
-            ]}
-          />
-        </ShortIdCopyable>
-      </Wrapper>
-    </GuideAnchor>
+      </ShortIdCopyable>
+    </Wrapper>
   );
   );
 }
 }