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

feat(ecosystem): Show codemap suggestions in stacktrace modal (#44145)

Scott Cooper 2 лет назад
Родитель
Сommit
0ba4deb2de

+ 80 - 9
static/app/components/events/interfaces/frame/stacktraceLinkModal.spec.tsx

@@ -8,6 +8,9 @@ import {
 
 import {openModal} from 'sentry/actionCreators/modal';
 import StacktraceLinkModal from 'sentry/components/events/interfaces/frame/stacktraceLinkModal';
+import * as analytics from 'sentry/utils/integrationUtil';
+
+jest.mock('sentry/utils/analytics/trackAdvancedAnalyticsEvent');
 
 describe('StacktraceLinkModal', () => {
   const org = TestStubs.Organization();
@@ -26,20 +29,25 @@ describe('StacktraceLinkModal', () => {
   };
   const onSubmit = jest.fn();
   const closeModal = jest.fn();
+  const analyticsSpy = jest.spyOn(analytics, 'trackIntegrationAnalytics');
 
   beforeEach(() => {
-    MockApiClient.clearMockResponses();
-    jest.clearAllMocks();
-
     MockApiClient.addMockResponse({
       url: `/organizations/${org.slug}/code-mappings/`,
       method: 'POST',
     });
-
     MockApiClient.addMockResponse({
       url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
       body: {config, sourceUrl, integrations: [integration]},
     });
+    MockApiClient.addMockResponse({
+      url: `/organizations/${org.slug}/derive-code-mappings/`,
+      body: [],
+    });
+  });
+  afterEach(() => {
+    MockApiClient.clearMockResponses();
+    jest.clearAllMocks();
   });
 
   it('links to source code with one GitHub integration', () => {
@@ -91,10 +99,7 @@ describe('StacktraceLinkModal', () => {
       ))
     );
 
-    userEvent.type(
-      screen.getByRole('textbox', {name: 'Copy the URL and paste it below'}),
-      'sourceUrl{enter}'
-    );
+    userEvent.paste(screen.getByRole('textbox', {name: 'Repository URL'}), 'sourceUrl');
     userEvent.click(screen.getByRole('button', {name: 'Save'}));
     await waitFor(() => {
       expect(closeModal).toHaveBeenCalled();
@@ -126,7 +131,7 @@ describe('StacktraceLinkModal', () => {
     );
 
     userEvent.type(
-      screen.getByRole('textbox', {name: 'Copy the URL and paste it below'}),
+      screen.getByRole('textbox', {name: 'Repository URL'}),
       'sourceUrl{enter}'
     );
     userEvent.click(screen.getByRole('button', {name: 'Save'}));
@@ -141,4 +146,70 @@ describe('StacktraceLinkModal', () => {
       '/settings/org-slug/integrations/github/1/'
     );
   });
+
+  it('displays suggestions from code mappings', async () => {
+    MockApiClient.addMockResponse({
+      url: `/organizations/${org.slug}/derive-code-mappings/`,
+      body: [
+        {
+          filename: 'stack/root/file/stack/root/file/stack/root/file.py',
+          repo_name: 'getsentry/codemap',
+          repo_branch: 'master',
+          stacktrace_root: '/stack/root',
+          source_path: '/source/root/',
+        },
+        {
+          filename: 'stack/root/file.py',
+          repo_name: 'getsentry/codemap',
+          repo_branch: 'master',
+          stacktrace_root: '/stack/root',
+          source_path: '/source/root/',
+        },
+      ],
+    });
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/repo-path-parsing/`,
+      method: 'POST',
+      body: {...configData},
+    });
+
+    renderGlobalModal();
+    act(() =>
+      openModal(modalProps => (
+        <StacktraceLinkModal
+          {...modalProps}
+          filename={filename}
+          closeModal={closeModal}
+          integrations={[integration]}
+          organization={org}
+          project={project}
+          onSubmit={onSubmit}
+        />
+      ))
+    );
+
+    expect(
+      await screen.findByText(
+        'Select from one of these suggestions or paste your URL below'
+      )
+    ).toBeInTheDocument();
+    const suggestion =
+      'https://github.com/getsentry/codemap/blob/master/stack/root/file.py';
+    expect(screen.getByText(suggestion)).toBeInTheDocument();
+    expect(screen.getByRole('dialog')).toSnapshot();
+
+    // Paste and save suggestion
+    userEvent.paste(screen.getByRole('textbox', {name: 'Repository URL'}), suggestion);
+    userEvent.click(screen.getByRole('button', {name: 'Save'}));
+    await waitFor(() => {
+      expect(closeModal).toHaveBeenCalled();
+    });
+
+    expect(analyticsSpy).toHaveBeenCalledWith(
+      'integrations.stacktrace_complete_setup',
+      expect.objectContaining({
+        is_suggestion: true,
+      })
+    );
+  });
 });

+ 72 - 1
static/app/components/events/interfaces/frame/stacktraceLinkModal.tsx

@@ -1,5 +1,7 @@
 import {Fragment, useState} from 'react';
 import styled from '@emotion/styled';
+import copy from 'copy-text-to-clipboard';
+import uniq from 'lodash/uniq';
 
 import {addSuccessMessage} from 'sentry/actionCreators/indicator';
 import {ModalRenderProps} from 'sentry/actionCreators/modal';
@@ -11,12 +13,22 @@ import ExternalLink from 'sentry/components/links/externalLink';
 import Link from 'sentry/components/links/link';
 import List from 'sentry/components/list';
 import TextCopyInput from 'sentry/components/textCopyInput';
+import {IconCopy} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import type {Integration, Organization, Project} from 'sentry/types';
 import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
+import {useQuery} from 'sentry/utils/queryClient';
 import useApi from 'sentry/utils/useApi';
 
+type DerivedCodeMapping = {
+  filename: string;
+  repo_branch: string;
+  repo_name: string;
+  source_path: string;
+  stacktrace_root: string;
+};
+
 interface StacktraceLinkModalProps extends ModalRenderProps {
   filename: string;
   integrations: Integration[];
@@ -40,6 +52,30 @@ function StacktraceLinkModal({
   const [error, setError] = useState<null | string>(null);
   const [sourceCodeInput, setSourceCodeInput] = useState('');
 
+  const {data: sugestedCodeMappings} = useQuery<DerivedCodeMapping[]>(
+    [
+      `/organizations/${organization.slug}/derive-code-mappings/`,
+      {
+        query: {
+          projectId: project.id,
+          stacktraceFilename: filename,
+        },
+      },
+    ],
+    {
+      staleTime: Infinity,
+      refetchOnWindowFocus: false,
+      retry: false,
+      notifyOnChangeProps: ['data'],
+    }
+  );
+
+  const suggestions = uniq(
+    (sugestedCodeMappings ?? []).map(suggestion => {
+      return `https://github.com/${suggestion.repo_name}/blob/${suggestion.repo_branch}/${suggestion.filename}`;
+    })
+  ).slice(0, 2);
+
   const onHandleChange = (input: string) => {
     setSourceCodeInput(input);
   };
@@ -100,6 +136,7 @@ function StacktraceLinkModal({
         provider: configData.config?.provider.key,
         view: 'stacktrace_issue_details',
         organization,
+        is_suggestion: suggestions.includes(sourceCodeInput),
       });
       closeModal();
       onSubmit();
@@ -177,9 +214,30 @@ function StacktraceLinkModal({
             </li>
             <li>
               <ItemContainer>
+                <div>
+                  {suggestions.length
+                    ? t('Select from one of these suggestions or paste your URL below')
+                    : t('Copy the URL and paste it below')}
+                </div>
+                {suggestions.length ? (
+                  <StyledSuggestions>
+                    {suggestions.map((suggestion, i) => {
+                      return (
+                        <div key={i} style={{display: 'flex', alignItems: 'center'}}>
+                          <SuggestionOverflow>{suggestion}</SuggestionOverflow>
+                          <Button borderless size="xs" onClick={() => copy(suggestion)}>
+                            <IconCopy size="xs" />
+                          </Button>
+                        </div>
+                      );
+                    })}
+                  </StyledSuggestions>
+                ) : null}
+
                 <StyledTextField
                   inline={false}
-                  label={t('Copy the URL and paste it below')}
+                  aria-label={t('Repository URL')}
+                  hideLabel
                   name="source-code-input"
                   value={sourceCodeInput}
                   onChange={onHandleChange}
@@ -215,15 +273,28 @@ const StyledList = styled(List)`
 
   & > li:before {
     position: relative;
+    min-width: 25px;
   }
 `;
 
+const StyledSuggestions = styled('div')`
+  background-color: ${p => p.theme.surface100};
+  border-radius: ${p => p.theme.borderRadius};
+  padding: ${space(1)} ${space(2)};
+`;
+
+const SuggestionOverflow = styled('div')`
+  ${p => p.theme.overflowEllipsis};
+  direction: rtl;
+`;
+
 const ItemContainer = styled('div')`
   display: flex;
   gap: ${space(1)};
   flex-direction: column;
   margin-top: ${space(0.25)};
   flex: 1;
+  max-width: 100%;
 `;
 
 const ModalContainer = styled('div')`

+ 1 - 0
static/app/utils/analytics/integrations/stacktraceLinkAnalyticsEvents.ts

@@ -22,6 +22,7 @@ type StacktraceLinkEventsLiterals = `${StacktraceLinkEvents}`;
 export type StacktraceLinkEventParameters = {
   [key in StacktraceLinkEventsLiterals]: {
     error_reason?: StacktraceErrorMessage;
+    is_suggestion?: boolean;
     platform?: PlatformType;
     project_id?: string;
     provider?: string;