Browse Source

feat(commit-context): Show pull request with suspect commit (#39812)

Scott Cooper 2 years ago
parent
commit
76528a921c

+ 4 - 2
static/app/components/commitLink.tsx

@@ -43,11 +43,12 @@ const SUPPORTED_PROVIDERS: Readonly<CommitProvider[]> = [
 type Props = {
   commitId?: string;
   inline?: boolean;
+  onClick?: () => void;
   repository?: Repository;
   showIcon?: boolean;
 };
 
-function CommitLink({inline, commitId, repository, showIcon = true}: Props) {
+function CommitLink({inline, commitId, repository, showIcon = true, onClick}: Props) {
   if (!commitId || !repository) {
     return <span>{t('Unknown Commit')}</span>;
   }
@@ -78,11 +79,12 @@ function CommitLink({inline, commitId, repository, showIcon = true}: Props) {
       href={commitUrl}
       size="sm"
       icon={showIcon ? providerData.icon : null}
+      onClick={onClick}
     >
       {shortId}
     </Button>
   ) : (
-    <ExternalLink href={commitUrl}>
+    <ExternalLink href={commitUrl} onClick={onClick}>
       {showIcon ? providerData.icon : null}
       {' ' + shortId}
     </ExternalLink>

+ 39 - 2
static/app/components/commitRow.spec.tsx

@@ -1,9 +1,9 @@
-import {fireEvent, render, screen} from 'sentry-test/reactTestingLibrary';
+import {fireEvent, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import {openInviteMembersModal} from 'sentry/actionCreators/modal';
 import {CommitRow} from 'sentry/components/commitRow';
-import {Commit, Repository, User} from 'sentry/types';
+import {Commit, Repository, RepositoryStatus, User} from 'sentry/types';
 
 jest.mock('sentry/components/hovercard', () => {
   return {
@@ -88,4 +88,41 @@ describe('commitRow', () => {
 
     expect(screen.getByText(/ref\(commitRow\): refactor to fc/)).toBeInTheDocument();
   });
+
+  it('renders pull request', () => {
+    const commit: Commit = {
+      ...baseCommit,
+      pullRequest: {
+        id: '9',
+        title: 'cool pr',
+        externalUrl: 'https://github.com/getsentry/sentry/pull/1',
+        repository: {
+          id: '14',
+          name: 'example',
+          url: '',
+          provider: {
+            id: 'unknown',
+            name: 'Unknown Provider',
+          },
+          status: RepositoryStatus.ACTIVE,
+          dateCreated: '2022-10-07T19:35:27.370422Z',
+          integrationId: '14',
+          externalSlug: 'org-slug',
+        },
+      },
+    };
+
+    const handlePullRequestClick = jest.fn();
+    render(<CommitRow commit={commit} onPullRequestClick={handlePullRequestClick} />);
+
+    const pullRequestButton = screen.getByRole('button', {name: 'View Pull Request'});
+    expect(pullRequestButton).toBeInTheDocument();
+    expect(pullRequestButton).toHaveAttribute(
+      'href',
+      'https://github.com/getsentry/sentry/pull/1'
+    );
+
+    userEvent.click(pullRequestButton);
+    expect(handlePullRequestClick).toHaveBeenCalledTimes(1);
+  });
 });

+ 53 - 24
static/app/components/commitRow.tsx

@@ -12,9 +12,12 @@ import TextOverflow from 'sentry/components/textOverflow';
 import TimeSince from 'sentry/components/timeSince';
 import {IconWarning} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
 import space from 'sentry/styles/space';
 import {Commit} from 'sentry/types';
 
+import Button from './button';
+
 function formatCommitMessage(message: string | null) {
   if (!message) {
     return t('No message provided');
@@ -25,11 +28,17 @@ function formatCommitMessage(message: string | null) {
 
 interface CommitRowProps {
   commit: Commit;
-  className?: string;
   customAvatar?: React.ReactNode;
+  onCommitClick?: () => void;
+  onPullRequestClick?: () => void;
 }
 
-function CommitRow({commit, customAvatar, className}: CommitRowProps) {
+function CommitRow({
+  commit,
+  customAvatar,
+  onPullRequestClick,
+  onCommitClick,
+}: CommitRowProps) {
   const handleInviteClick = useCallback(() => {
     if (!commit.author?.email) {
       Sentry.captureException(
@@ -48,13 +57,17 @@ function CommitRow({commit, customAvatar, className}: CommitRowProps) {
     });
   }, [commit.author]);
 
+  const user = ConfigStore.get('user');
+  const isUser = user?.id === commit.author?.id;
+
   return (
-    <PanelItem key={commit.id} className={className} data-test-id="commit-row">
+    <StyledPanelItem key={commit.id} data-test-id="commit-row">
       {customAvatar ? (
         customAvatar
       ) : commit.author && commit.author.id === undefined ? (
         <AvatarWrapper>
           <Hovercard
+            skipWrapper
             body={
               <EmailWarning>
                 {tct(
@@ -75,32 +88,53 @@ function CommitRow({commit, customAvatar, className}: CommitRowProps) {
           </Hovercard>
         </AvatarWrapper>
       ) : (
-        <AvatarWrapper>
+        <div>
           <UserAvatar size={36} user={commit.author} />
-        </AvatarWrapper>
+        </div>
       )}
 
       <CommitMessage>
-        <Message>{formatCommitMessage(commit.message)}</Message>
-        <Meta>
-          {tct('[author] committed [timeago]', {
-            author: <strong>{commit.author?.name ?? t('Unknown author')}</strong>,
-            timeago: <TimeSince date={commit.dateCreated} />,
+        <Message>
+          {tct('[author] committed [commitLink]', {
+            author: isUser ? t('You') : commit.author?.name ?? t('Unknown author'),
+            commitLink: (
+              <CommitLink
+                inline
+                showIcon={false}
+                commitId={commit.id}
+                repository={commit.repository}
+                onClick={onCommitClick}
+              />
+            ),
           })}
+        </Message>
+        <Meta>
+          {formatCommitMessage(commit.message)} &bull;{' '}
+          <TimeSince date={commit.dateCreated} />
         </Meta>
       </CommitMessage>
 
-      <div>
-        <CommitLink commitId={commit.id} repository={commit.repository} />
-      </div>
-    </PanelItem>
+      {commit.pullRequest && commit.pullRequest.externalUrl && (
+        <Button
+          external
+          href={commit.pullRequest.externalUrl}
+          onClick={onPullRequestClick}
+        >
+          {t('View Pull Request')}
+        </Button>
+      )}
+    </StyledPanelItem>
   );
 }
 
+const StyledPanelItem = styled(PanelItem)`
+  display: flex;
+  align-items: center;
+  gap: ${space(2)};
+`;
+
 const AvatarWrapper = styled('div')`
   position: relative;
-  align-self: flex-start;
-  margin-right: ${space(2)};
 `;
 
 const EmailWarning = styled('div')`
@@ -137,9 +171,8 @@ const CommitMessage = styled('div')`
 `;
 
 const Message = styled(TextOverflow)`
-  font-size: 15px;
-  line-height: 1.1;
-  font-weight: bold;
+  font-size: ${p => p.theme.fontSizeLarge};
+  line-height: 1.2;
 `;
 
 const Meta = styled(TextOverflow)`
@@ -149,8 +182,4 @@ const Meta = styled(TextOverflow)`
   color: ${p => p.theme.subText};
 `;
 
-const StyledCommitRow = styled(CommitRow)`
-  align-items: center;
-`;
-
-export {StyledCommitRow as CommitRow};
+export {CommitRow};

+ 25 - 4
static/app/components/events/eventCause.tsx

@@ -68,6 +68,24 @@ function EventCause({group, event, project}: Props) {
     return null;
   }
 
+  const handlePullRequestClick = () => {
+    trackAdvancedAnalyticsEvent('issue_details.suspect_commits.pull_request_clicked', {
+      organization,
+      project_id: parseInt(project.id as string, 10),
+      group_id: parseInt(group?.id as string, 10),
+      issue_category: group?.issueCategory ?? IssueCategory.ERROR,
+    });
+  };
+
+  const handleCommitClick = () => {
+    trackAdvancedAnalyticsEvent('issue_details.suspect_commits.commit_clicked', {
+      organization,
+      project_id: parseInt(project.id as string, 10),
+      group_id: parseInt(group?.id as string, 10),
+      issue_category: group?.issueCategory ?? IssueCategory.ERROR,
+    });
+  };
+
   const commits = getUniqueCommitsWithAuthors();
 
   return (
@@ -92,7 +110,12 @@ function EventCause({group, event, project}: Props) {
       </CauseHeader>
       <Panel>
         {commits.slice(0, isExpanded ? 100 : 1).map(commit => (
-          <CommitRow key={commit.id} commit={commit} />
+          <CommitRow
+            key={commit.id}
+            commit={commit}
+            onCommitClick={handleCommitClick}
+            onPullRequestClick={handlePullRequestClick}
+          />
         ))}
       </Panel>
     </DataSection>
@@ -102,9 +125,7 @@ function EventCause({group, event, project}: Props) {
 const ExpandButton = styled('button')`
   display: flex;
   align-items: center;
-  & > svg {
-    margin-left: ${space(0.5)};
-  }
+  gap: ${space(0.5)};
 `;
 
 export default EventCause;

+ 1 - 0
static/app/types/integrations.tsx

@@ -87,6 +87,7 @@ export type Commit = {
   message: string | null;
   releases: BaseRelease[];
   author?: User;
+  pullRequest?: PullRequest | null;
   repository?: Repository;
 };
 

+ 5 - 0
static/app/utils/analytics/workflowAnalyticsEvents.tsx

@@ -66,6 +66,8 @@ export type TeamInsightsEventParameters = {
   'issue_details.event_json_clicked': {group_id: number};
   'issue_details.event_navigation_clicked': {button: string; project_id: number};
   'issue_details.suspect_commits': IssueDetailsWithAlert & {count: number};
+  'issue_details.suspect_commits.commit_clicked': IssueDetailsWithAlert;
+  'issue_details.suspect_commits.pull_request_clicked': IssueDetailsWithAlert;
   'issue_details.tab_changed': IssueDetailsWithAlert & {
     tab: Tab;
   };
@@ -116,6 +118,9 @@ export const workflowEventMap: Record<TeamInsightsEventKey, string | null> = {
   'issue_details.event_navigation_clicked': 'Issue Details: Event Navigation Clicked',
   'issue_details.viewed': 'Issue Details: Viewed',
   'issue_details.suspect_commits': 'Issue Details: Suspect Commits',
+  'issue_details.suspect_commits.commit_clicked': 'Issue Details: Suspect Commit Clicked',
+  'issue_details.suspect_commits.pull_request_clicked':
+    'Issue Details: Suspect Pull Request Clicked',
   'issue_details.tab_changed': 'Issue Details: Tab Changed',
   'new_alert_rule.viewed': 'New Alert Rule: Viewed',
   'team_insights.viewed': 'Team Insights: Viewed',