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

feat(database): Add "open in" link to query source (#61190)

Uses the same internal logic (but pared down) as our "Open in GitHub"
links in stack frames to show an "Open in ..." link on query sources.
Fetches the release from the event, uses that to look up the
integration, and creates a link to either GitHub or GitLab.

- Loosen types on `useStacktraceLink`
- Rename incorrect prop
- Add "Open in..." link to query source
George Gritsouk 1 год назад
Родитель
Сommit
609aeb0950

+ 4 - 2
static/app/components/events/interfaces/frame/useStacktraceLink.tsx

@@ -2,8 +2,10 @@ import type {Event, Frame, StacktraceLinkResult} from 'sentry/types';
 import {ApiQueryKey, useApiQuery, UseApiQueryOptions} from 'sentry/utils/queryClient';
 import {ApiQueryKey, useApiQuery, UseApiQueryOptions} from 'sentry/utils/queryClient';
 
 
 interface UseStacktraceLinkProps {
 interface UseStacktraceLinkProps {
-  event: Event;
-  frame: Frame;
+  event: Partial<Pick<Event, 'platform' | 'release' | 'sdk'>>;
+  frame: Partial<
+    Pick<Frame, 'absPath' | 'filename' | 'function' | 'module' | 'package' | 'lineNo'>
+  >;
   orgSlug: string;
   orgSlug: string;
   projectSlug: string | undefined;
   projectSlug: string | undefined;
 }
 }

+ 2 - 4
static/app/views/performance/database/databaseSpanSummaryPage.tsx

@@ -13,7 +13,6 @@ import {space} from 'sentry/styles/space';
 import type {Sort} from 'sentry/utils/discover/fields';
 import type {Sort} from 'sentry/utils/discover/fields';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {DurationChart} from 'sentry/views/performance/database/durationChart';
 import {DurationChart} from 'sentry/views/performance/database/durationChart';
 import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
 import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
@@ -69,8 +68,7 @@ function SpanSummaryPage({params}: Props) {
     1
     1
   );
   );
 
 
-  const {projects} = useProjects();
-  const project = projects.find(p => p.id === String(indexedSpans?.[0]?.project_id));
+  const indexedSpan = indexedSpans?.[0];
 
 
   if (endpoint) {
   if (endpoint) {
     filters.transaction = endpoint;
     filters.transaction = endpoint;
@@ -169,9 +167,9 @@ function SpanSummaryPage({params}: Props) {
           {span?.[SpanMetricsField.SPAN_DESCRIPTION] && (
           {span?.[SpanMetricsField.SPAN_DESCRIPTION] && (
             <DescriptionContainer>
             <DescriptionContainer>
               <SpanDescription
               <SpanDescription
-                project={project}
                 span={{
                 span={{
                   ...span,
                   ...span,
+                  ...indexedSpan,
                   ...fullSpan,
                   ...fullSpan,
                   [SpanMetricsField.SPAN_DESCRIPTION]:
                   [SpanMetricsField.SPAN_DESCRIPTION]:
                     fullSpan?.description ??
                     fullSpan?.description ??

+ 9 - 7
static/app/views/starfish/components/spanDescription.tsx

@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
 
 
 import Feature from 'sentry/components/acl/feature';
 import Feature from 'sentry/components/acl/feature';
 import {CodeSnippet} from 'sentry/components/codeSnippet';
 import {CodeSnippet} from 'sentry/components/codeSnippet';
-import {Project} from 'sentry/types';
 import {StackTraceMiniFrame} from 'sentry/views/starfish/components/stackTraceMiniFrame';
 import {StackTraceMiniFrame} from 'sentry/views/starfish/components/stackTraceMiniFrame';
 import {MetricsResponse, SpanMetricsField} from 'sentry/views/starfish/types';
 import {MetricsResponse, SpanMetricsField} from 'sentry/views/starfish/types';
 import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
 import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
@@ -13,18 +12,20 @@ type Props = {
     MetricsResponse,
     MetricsResponse,
     SpanMetricsField.SPAN_OP | SpanMetricsField.SPAN_DESCRIPTION
     SpanMetricsField.SPAN_OP | SpanMetricsField.SPAN_DESCRIPTION
   > & {
   > & {
+    project_id?: number;
+    'transaction.id'?: string;
+  } & {
     data?: {
     data?: {
       'code.filepath'?: string;
       'code.filepath'?: string;
       'code.function'?: string;
       'code.function'?: string;
       'code.lineno'?: number;
       'code.lineno'?: number;
     };
     };
   };
   };
-  project?: Project;
 };
 };
 
 
-export function SpanDescription({span, project}: Props) {
+export function SpanDescription({span}: Props) {
   if (span[SpanMetricsField.SPAN_OP]?.startsWith('db')) {
   if (span[SpanMetricsField.SPAN_OP]?.startsWith('db')) {
-    return <DatabaseSpanDescription span={span} project={project} />;
+    return <DatabaseSpanDescription span={span} />;
   }
   }
 
 
   return <WordBreak>{span[SpanMetricsField.SPAN_DESCRIPTION]}</WordBreak>;
   return <WordBreak>{span[SpanMetricsField.SPAN_DESCRIPTION]}</WordBreak>;
@@ -32,7 +33,7 @@ export function SpanDescription({span, project}: Props) {
 
 
 const formatter = new SQLishFormatter();
 const formatter = new SQLishFormatter();
 
 
-function DatabaseSpanDescription({span, project}: Props) {
+function DatabaseSpanDescription({span}: Props) {
   const rawDescription = span[SpanMetricsField.SPAN_DESCRIPTION];
   const rawDescription = span[SpanMetricsField.SPAN_DESCRIPTION];
   const formatterDescription = useMemo(() => {
   const formatterDescription = useMemo(() => {
     return formatter.toString(rawDescription);
     return formatter.toString(rawDescription);
@@ -47,9 +48,10 @@ function DatabaseSpanDescription({span, project}: Props) {
       <Feature features={['organizations:performance-database-view-query-source']}>
       <Feature features={['organizations:performance-database-view-query-source']}>
         {span?.data?.['code.filepath'] && (
         {span?.data?.['code.filepath'] && (
           <StackTraceMiniFrame
           <StackTraceMiniFrame
-            project={project}
+            projectId={span.project_id?.toString()}
+            eventId={span['transaction.id']}
             frame={{
             frame={{
-              absPath: span?.data?.['code.filepath'],
+              filename: span?.data?.['code.filepath'],
               lineNo: span?.data?.['code.lineno'],
               lineNo: span?.data?.['code.lineno'],
               function: span?.data?.['code.function'],
               function: span?.data?.['code.function'],
             }}
             }}

+ 79 - 5
static/app/views/starfish/components/stackTraceMiniFrame.tsx

@@ -2,16 +2,28 @@ import {Fragment} from 'react';
 import styled from '@emotion/styled';
 import styled from '@emotion/styled';
 
 
 import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
 import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
+import useStacktraceLink from 'sentry/components/events/interfaces/frame/useStacktraceLink';
+import ExternalLink from 'sentry/components/links/externalLink';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
-import {Frame, Project} from 'sentry/types';
+import {Project} from 'sentry/types';
+import {getIntegrationIcon, getIntegrationSourceUrl} from 'sentry/utils/integrationUtil';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
+import {useEventDetails} from 'sentry/views/starfish/queries/useEventDetails';
 
 
 interface Props {
 interface Props {
-  frame: Partial<Pick<Frame, 'absPath' | 'colNo' | 'function' | 'lineNo'>>;
-  project?: Project;
+  frame: Parameters<typeof useStacktraceLink>[0]['frame'];
+  eventId?: string;
+  projectId?: string;
 }
 }
 
 
-export function StackTraceMiniFrame({frame, project}: Props) {
+export function StackTraceMiniFrame({frame, eventId, projectId}: Props) {
+  const {projects} = useProjects();
+  const project = projects.find(p => p.id === projectId);
+
+  const {data: event} = useEventDetails({eventId, projectSlug: project?.slug});
+
   return (
   return (
     <FrameContainer>
     <FrameContainer>
       {project && (
       {project && (
@@ -19,7 +31,7 @@ export function StackTraceMiniFrame({frame, project}: Props) {
           <ProjectAvatar project={project} size={16} />
           <ProjectAvatar project={project} size={16} />
         </ProjectAvatarContainer>
         </ProjectAvatarContainer>
       )}
       )}
-      {frame.absPath && <Emphasize>{frame?.absPath}</Emphasize>}
+      {frame.filename && <Emphasize>{frame?.filename}</Emphasize>}
       {frame.function && (
       {frame.function && (
         <Fragment>
         <Fragment>
           <Deemphasize> {t('in')} </Deemphasize>
           <Deemphasize> {t('in')} </Deemphasize>
@@ -32,6 +44,12 @@ export function StackTraceMiniFrame({frame, project}: Props) {
           <Emphasize>{frame?.lineNo}</Emphasize>
           <Emphasize>{frame?.lineNo}</Emphasize>
         </Fragment>
         </Fragment>
       )}
       )}
+
+      {event && project && (
+        <PushRight>
+          <SourceCodeIntegrationLink event={event} project={project} frame={frame} />
+        </PushRight>
+      )}
     </FrameContainer>
     </FrameContainer>
   );
   );
 }
 }
@@ -61,3 +79,59 @@ const Emphasize = styled('span')`
 const Deemphasize = styled('span')`
 const Deemphasize = styled('span')`
   color: ${p => p.theme.gray300};
   color: ${p => p.theme.gray300};
 `;
 `;
+
+const PushRight = styled('span')`
+  margin-left: auto;
+`;
+
+interface SourceCodeIntegrationLinkProps {
+  event: Parameters<typeof useStacktraceLink>[0]['event'];
+  frame: Parameters<typeof useStacktraceLink>[0]['frame'];
+  project: Project;
+}
+function SourceCodeIntegrationLink({
+  event,
+  project,
+  frame,
+}: SourceCodeIntegrationLinkProps) {
+  const organization = useOrganization();
+
+  const {data: match, isLoading} = useStacktraceLink({
+    event,
+    frame,
+    orgSlug: organization.slug,
+    projectSlug: project.slug,
+  });
+
+  if (match && match.config && match.sourceUrl && frame.lineNo && !isLoading) {
+    return (
+      <DeemphasizedExternalLink
+        href={getIntegrationSourceUrl(
+          match.config.provider.key,
+          match.sourceUrl,
+          frame.lineNo
+        )}
+        openInNewTab
+      >
+        <StyledIconWrapper>
+          {getIntegrationIcon(match.config.provider.key, 'sm')}
+        </StyledIconWrapper>
+        {t('Open this line in %s', match.config.provider.name)}
+      </DeemphasizedExternalLink>
+    );
+  }
+
+  return null;
+}
+
+const DeemphasizedExternalLink = styled(ExternalLink)`
+  display: flex;
+  align-items: center;
+  gap: ${space(0.75)};
+  color: ${p => p.theme.gray300};
+`;
+
+const StyledIconWrapper = styled('span')`
+  color: inherit;
+  line-height: 0;
+`;