Browse Source

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 year ago
parent
commit
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';
 
 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;
   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 {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {DurationChart} from 'sentry/views/performance/database/durationChart';
 import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
@@ -69,8 +68,7 @@ function SpanSummaryPage({params}: Props) {
     1
   );
 
-  const {projects} = useProjects();
-  const project = projects.find(p => p.id === String(indexedSpans?.[0]?.project_id));
+  const indexedSpan = indexedSpans?.[0];
 
   if (endpoint) {
     filters.transaction = endpoint;
@@ -169,9 +167,9 @@ function SpanSummaryPage({params}: Props) {
           {span?.[SpanMetricsField.SPAN_DESCRIPTION] && (
             <DescriptionContainer>
               <SpanDescription
-                project={project}
                 span={{
                   ...span,
+                  ...indexedSpan,
                   ...fullSpan,
                   [SpanMetricsField.SPAN_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 {CodeSnippet} from 'sentry/components/codeSnippet';
-import {Project} from 'sentry/types';
 import {StackTraceMiniFrame} from 'sentry/views/starfish/components/stackTraceMiniFrame';
 import {MetricsResponse, SpanMetricsField} from 'sentry/views/starfish/types';
 import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
@@ -13,18 +12,20 @@ type Props = {
     MetricsResponse,
     SpanMetricsField.SPAN_OP | SpanMetricsField.SPAN_DESCRIPTION
   > & {
+    project_id?: number;
+    'transaction.id'?: string;
+  } & {
     data?: {
       'code.filepath'?: string;
       'code.function'?: string;
       'code.lineno'?: number;
     };
   };
-  project?: Project;
 };
 
-export function SpanDescription({span, project}: Props) {
+export function SpanDescription({span}: Props) {
   if (span[SpanMetricsField.SPAN_OP]?.startsWith('db')) {
-    return <DatabaseSpanDescription span={span} project={project} />;
+    return <DatabaseSpanDescription span={span} />;
   }
 
   return <WordBreak>{span[SpanMetricsField.SPAN_DESCRIPTION]}</WordBreak>;
@@ -32,7 +33,7 @@ export function SpanDescription({span, project}: Props) {
 
 const formatter = new SQLishFormatter();
 
-function DatabaseSpanDescription({span, project}: Props) {
+function DatabaseSpanDescription({span}: Props) {
   const rawDescription = span[SpanMetricsField.SPAN_DESCRIPTION];
   const formatterDescription = useMemo(() => {
     return formatter.toString(rawDescription);
@@ -47,9 +48,10 @@ function DatabaseSpanDescription({span, project}: Props) {
       <Feature features={['organizations:performance-database-view-query-source']}>
         {span?.data?.['code.filepath'] && (
           <StackTraceMiniFrame
-            project={project}
+            projectId={span.project_id?.toString()}
+            eventId={span['transaction.id']}
             frame={{
-              absPath: span?.data?.['code.filepath'],
+              filename: span?.data?.['code.filepath'],
               lineNo: span?.data?.['code.lineno'],
               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 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 {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 {
-  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 (
     <FrameContainer>
       {project && (
@@ -19,7 +31,7 @@ export function StackTraceMiniFrame({frame, project}: Props) {
           <ProjectAvatar project={project} size={16} />
         </ProjectAvatarContainer>
       )}
-      {frame.absPath && <Emphasize>{frame?.absPath}</Emphasize>}
+      {frame.filename && <Emphasize>{frame?.filename}</Emphasize>}
       {frame.function && (
         <Fragment>
           <Deemphasize> {t('in')} </Deemphasize>
@@ -32,6 +44,12 @@ export function StackTraceMiniFrame({frame, project}: Props) {
           <Emphasize>{frame?.lineNo}</Emphasize>
         </Fragment>
       )}
+
+      {event && project && (
+        <PushRight>
+          <SourceCodeIntegrationLink event={event} project={project} frame={frame} />
+        </PushRight>
+      )}
     </FrameContainer>
   );
 }
@@ -61,3 +79,59 @@ const Emphasize = styled('span')`
 const Deemphasize = styled('span')`
   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;
+`;