Browse Source

feat(new-trace): Adding trace configurations hovercard to header. (#75839)

Added trace configurations hovercard: 
<img width="1283" alt="Screenshot 2024-08-19 at 4 19 03 PM"
src="https://github.com/user-attachments/assets/8c1abdd4-32a3-468c-8975-4a287fe16c87">

Removed tracewarnings from the trace tab, for example, the banner for
the broken sub traces case. They have been raising confusion and the sdk
team doesn't look at them as addressable problems.

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdullah Khan 6 months ago
parent
commit
e8533a45d6

+ 68 - 0
static/app/data/platformCategories.tsx

@@ -187,6 +187,74 @@ export const performance: PlatformKey[] = [
   'node-connect',
   'node-connect',
 ];
 ];
 
 
+// List of platforms that have tracing custom instrumentation guide docs for its nested frameworks
+// i.e. for a platform like `javascript-react`, we have a custom instrumentation guide for `react` that can be
+// accessed at `https://docs.sentry.io/platforms/javascript/guides/react/tracing/instrumentation/custom-instrumentation/`
+export const platformsWithNestedInstrumentationGuides: PlatformKey[] = [
+  'apple',
+  'apple-ios',
+  'apple-macos',
+  'dart',
+  'dart-flutter',
+  'go',
+  'go-echo',
+  'go-fasthttp',
+  'go-fiber',
+  'go-gin',
+  'go-http',
+  'go-iris',
+  'go-martini',
+  'go-negroni',
+  'java',
+  'java-android',
+  'java-appengine',
+  'java-log4j',
+  'java-log4j2',
+  'java-logback',
+  'java-logging',
+  'java-spring',
+  'java-spring-boot',
+  'javascript',
+  'javascript-angular',
+  'javascript-angularjs',
+  'javascript-astro',
+  'javascript-backbone',
+  'javascript-browser',
+  'javascript-capacitor',
+  'javascript-cordova',
+  'javascript-electron',
+  'javascript-ember',
+  'javascript-gatsby',
+  'javascript-nextjs',
+  'javascript-react',
+  'javascript-remix',
+  'javascript-solid',
+  'javascript-svelte',
+  'javascript-sveltekit',
+  'javascript-vue',
+  'dotnet',
+  'dotnet-aspnet',
+  'dotnet-aspnetcore',
+  'dotnet-awslambda',
+  'dotnet-gcpfunctions',
+  'dotnet-google-cloud-functions',
+  'dotnet-maui',
+  'dotnet-uwp',
+  'dotnet-winforms',
+  'dotnet-wpf',
+  'dotnet-xamarin',
+  'php',
+  'php-laravel',
+  'php-monolog',
+  'php-symfony',
+  'php-symfony2',
+  'powershell',
+  'react-native',
+  'ruby',
+  'rust',
+  'unity',
+];
+
 // List of platforms that have performance onboarding checklist content
 // List of platforms that have performance onboarding checklist content
 export const withPerformanceOnboarding: Set<PlatformKey> = new Set([
 export const withPerformanceOnboarding: Set<PlatformKey> = new Set([
   'javascript',
   'javascript',

+ 4 - 0
static/app/utils/analytics/tracingEventMap.tsx

@@ -1,4 +1,7 @@
 export type TracingEventParameters = {
 export type TracingEventParameters = {
+  'trace.configurations_docs_link_clicked': {
+    title: string;
+  };
   'trace.metadata': {
   'trace.metadata': {
     num_nodes: number;
     num_nodes: number;
     num_root_children: number;
     num_root_children: number;
@@ -103,6 +106,7 @@ export const tracingEventMap: Record<TracingEventKey, string | null> = {
     'Clicked Increase Budget in Quota Exceeded Banner',
     'Clicked Increase Budget in Quota Exceeded Banner',
   'trace.quality.quota_exceeded.learn_more_clicked':
   'trace.quality.quota_exceeded.learn_more_clicked':
     'Clicked Learn More in Quota Exceeded Banner',
     'Clicked Learn More in Quota Exceeded Banner',
+  'trace.configurations_docs_link_clicked': 'Clicked Traces Configurations Docs Link',
   'trace.quality.quota_exceeded.banner_loaded':
   'trace.quality.quota_exceeded.banner_loaded':
     'Performance Quota Exceeded Banner Loaded',
     'Performance Quota Exceeded Banner Loaded',
   'trace.trace_layout.view_shortcuts': 'Viewed Trace Shortcuts',
   'trace.trace_layout.view_shortcuts': 'Viewed Trace Shortcuts',

+ 1 - 0
static/app/views/performance/newTraceDetails/index.tsx

@@ -209,6 +209,7 @@ export function TraceView() {
         <NoProjectMessage organization={organization}>
         <NoProjectMessage organization={organization}>
           <TraceExternalLayout>
           <TraceExternalLayout>
             <TraceMetadataHeader
             <TraceMetadataHeader
+              rootEventResults={rootEvent}
               organization={organization}
               organization={organization}
               traceSlug={traceSlug}
               traceSlug={traceSlug}
               traceEventView={traceEventView}
               traceEventView={traceEventView}

+ 7 - 0
static/app/views/performance/newTraceDetails/traceAnalytics.tsx

@@ -133,6 +133,12 @@ const trackTraceWarningType = (type: TraceType, organization: Organization) =>
     type,
     type,
   });
   });
 
 
+const trackTraceConfigurationsDocsClicked = (organization: Organization, title: string) =>
+  trackAnalytics('trace.configurations_docs_link_clicked', {
+    organization,
+    title,
+  });
+
 const traceAnalytics = {
 const traceAnalytics = {
   // Trace shape
   // Trace shape
   trackTraceMetadata,
   trackTraceMetadata,
@@ -158,6 +164,7 @@ const traceAnalytics = {
   trackQuotaExceededIncreaseBudgetClicked,
   trackQuotaExceededIncreaseBudgetClicked,
   trackQuotaExceededLearnMoreClicked,
   trackQuotaExceededLearnMoreClicked,
   trackQuotaExceededBannerLoaded,
   trackQuotaExceededBannerLoaded,
+  trackTraceConfigurationsDocsClicked,
 };
 };
 
 
 export {traceAnalytics};
 export {traceAnalytics};

+ 195 - 0
static/app/views/performance/newTraceDetails/traceConfigurations.tsx

@@ -0,0 +1,195 @@
+import {type ReactNode, useMemo} from 'react';
+import {ClassNames} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {Button, LinkButton} from 'sentry/components/button';
+import {Hovercard} from 'sentry/components/hovercard';
+import {platformsWithNestedInstrumentationGuides} from 'sentry/data/platformCategories';
+import {IconOpen, IconQuestion} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {EventTransaction} from 'sentry/types/event';
+import type {Project} from 'sentry/types/project';
+import type {UseApiQueryResult} from 'sentry/utils/queryClient';
+import type RequestError from 'sentry/utils/requestError/requestError';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
+import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
+
+function Resource({
+  title,
+  subtitle,
+  link,
+}: {
+  link: string;
+  subtitle: ReactNode;
+  title: string;
+}) {
+  const organization = useOrganization();
+
+  return (
+    <StyledLinkButton
+      icon={<IconOpen />}
+      borderless
+      external
+      href={link}
+      onClick={() => {
+        traceAnalytics.trackTraceConfigurationsDocsClicked(organization, title);
+      }}
+    >
+      <ButtonContent>
+        <ButtonTitle>{title}</ButtonTitle>
+        <ButtonSubtitle>{subtitle}</ButtonSubtitle>
+      </ButtonContent>
+    </StyledLinkButton>
+  );
+}
+
+type ParsedPlatform = {
+  platformName: string;
+  framework?: string;
+};
+
+function parsePlatform(platform: string): ParsedPlatform {
+  // Except react-native, all other project platforms have the following two structures:
+  // 1. "{language}-{framework}", e.g. "javascript-nextjs"
+  // 2. "{language}", e.g. "python"
+  const [platformName, framework] =
+    platform === 'react-native' ? ['react-native', undefined] : platform.split('-');
+
+  return {platformName, framework};
+}
+
+function getCustomInstrumentationLink(project: Project | undefined): string {
+  // Default to JavaScript guide if project or platform is not available
+  if (!project || !project.platform) {
+    return `https://docs.sentry.io/platforms/javascript/tracing/instrumentation/custom-instrumentation/`;
+  }
+
+  const {platformName, framework} = parsePlatform(project.platform);
+
+  return platformsWithNestedInstrumentationGuides.includes(project.platform) && framework
+    ? `https://docs.sentry.io/platforms/${platformName}/guides/${framework}/tracing/instrumentation/custom-instrumentation/`
+    : `https://docs.sentry.io/platforms/${platformName}/tracing/instrumentation/custom-instrumentation/`;
+}
+
+function getDistributedTracingLink(project: Project | undefined): string {
+  // Default to JavaScript guide if project or platform is not available
+  if (!project || !project.platform) {
+    return `https://docs.sentry.io/platforms/javascript/tracing/trace-propagation/`;
+  }
+
+  const {platformName, framework} = parsePlatform(project.platform);
+
+  return framework
+    ? `https://docs.sentry.io/platforms/${platformName}/guides/${framework}/tracing/trace-propagation/`
+    : `https://docs.sentry.io/platforms/${platformName}/tracing/trace-propagation/`;
+}
+
+type ResourceButtonsProps = {
+  customInstrumentationLink: string;
+  distributedTracingLink: string;
+};
+
+function ResourceButtons({
+  customInstrumentationLink,
+  distributedTracingLink,
+}: ResourceButtonsProps) {
+  return (
+    <ButtonContainer>
+      <Resource
+        title={t('Custom Instrumentation')}
+        subtitle={t('Add Custom Spans or Transactions to your traces')}
+        link={customInstrumentationLink}
+      />
+      <Resource
+        title={t('Distributed Tracing')}
+        subtitle={t('See the whole trace across all your services')}
+        link={distributedTracingLink}
+      />
+    </ButtonContainer>
+  );
+}
+
+type TraceConfigurationsProps = {
+  rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
+};
+
+export default function TraceConfigurations({
+  rootEventResults,
+}: TraceConfigurationsProps) {
+  const {projects} = useProjects();
+
+  const traceProject = useMemo(() => {
+    return rootEventResults.data
+      ? projects.find(p => p.id === rootEventResults.data.projectID)
+      : undefined;
+  }, [projects, rootEventResults.data]);
+
+  const customInstrumentationLink = useMemo(
+    () => getCustomInstrumentationLink(traceProject),
+    [traceProject]
+  );
+
+  const distributedTracingLink = useMemo(
+    () => getDistributedTracingLink(traceProject),
+    [traceProject]
+  );
+
+  return (
+    <ClassNames>
+      {({css}) => (
+        <Hovercard
+          body={
+            <ResourceButtons
+              customInstrumentationLink={customInstrumentationLink}
+              distributedTracingLink={distributedTracingLink}
+            />
+          }
+          bodyClassName={css`
+            padding: ${space(1)};
+          `}
+          position="top-end"
+        >
+          <Button
+            size="sm"
+            icon={<IconQuestion />}
+            aria-label={t('trace configure resources')}
+          >
+            {t('Configure Traces')}
+          </Button>
+        </Hovercard>
+      )}
+    </ClassNames>
+  );
+}
+
+const ButtonContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(1)};
+  align-items: flex-start;
+`;
+
+const ButtonContent = styled('div')`
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  white-space: pre-line;
+  gap: ${space(0.25)};
+`;
+
+const ButtonTitle = styled('div')`
+  font-weight: ${p => p.theme.fontWeightNormal};
+`;
+
+const ButtonSubtitle = styled('div')`
+  color: ${p => p.theme.gray300};
+  font-weight: ${p => p.theme.fontWeightNormal};
+  font-size: ${p => p.theme.fontSizeSmall};
+`;
+
+const StyledLinkButton = styled(LinkButton)`
+  padding: ${space(1)};
+  height: auto;
+`;

+ 0 - 2
static/app/views/performance/newTraceDetails/traceDrawer/tabs/trace/index.tsx

@@ -12,7 +12,6 @@ import type {UseApiQueryResult, UseInfiniteQueryResult} from 'sentry/utils/query
 import type RequestError from 'sentry/utils/requestError/requestError';
 import type RequestError from 'sentry/utils/requestError/requestError';
 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 {TraceWarnings} from 'sentry/views/performance/newTraceDetails/traceWarnings';
 
 
 import {isTraceNode} from '../../../guards';
 import {isTraceNode} from '../../../guards';
 import type {TraceMetaQueryResults} from '../../../traceApi/useTraceMeta';
 import type {TraceMetaQueryResults} from '../../../traceApi/useTraceMeta';
@@ -58,7 +57,6 @@ export function TraceDetails(props: TraceDetailsProps) {
 
 
   return (
   return (
     <Fragment>
     <Fragment>
-      {props.tree.type === 'trace' ? <TraceWarnings type={props.traceType} /> : null}
       <IssueList issues={issues} node={props.node} organization={organization} />
       <IssueList issues={issues} node={props.node} organization={organization} />
       <TraceDrawerComponents.SectionCardGroup>
       <TraceDrawerComponents.SectionCardGroup>
         <GeneralInfo
         <GeneralInfo

+ 6 - 0
static/app/views/performance/newTraceDetails/traceMetadataHeader.tsx

@@ -9,18 +9,23 @@ import DiscoverButton from 'sentry/components/discoverButton';
 import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
 import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
 import * as Layout from 'sentry/components/layouts/thirds';
 import * as Layout from 'sentry/components/layouts/thirds';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
+import type {EventTransaction} from 'sentry/types/event';
 import type {Organization} from 'sentry/types/organization';
 import type {Organization} from 'sentry/types/organization';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import type EventView from 'sentry/utils/discover/eventView';
 import type EventView from 'sentry/utils/discover/eventView';
 import {SavedQueryDatasets} from 'sentry/utils/discover/types';
 import {SavedQueryDatasets} from 'sentry/utils/discover/types';
+import type {UseApiQueryResult} from 'sentry/utils/queryClient';
+import type RequestError from 'sentry/utils/requestError/requestError';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
 import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
 import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
+import TraceConfigurations from 'sentry/views/performance/newTraceDetails/traceConfigurations';
 
 
 import Tab from '../transactionSummary/tabs';
 import Tab from '../transactionSummary/tabs';
 
 
 interface TraceMetadataHeaderProps {
 interface TraceMetadataHeaderProps {
   organization: Organization;
   organization: Organization;
+  rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
   traceEventView: EventView;
   traceEventView: EventView;
   traceSlug: string;
   traceSlug: string;
 }
 }
@@ -367,6 +372,7 @@ export function TraceMetadataHeader(props: TraceMetadataHeaderProps) {
       </Layout.HeaderContent>
       </Layout.HeaderContent>
       <Layout.HeaderActions>
       <Layout.HeaderActions>
         <ButtonBar gap={1}>
         <ButtonBar gap={1}>
+          <TraceConfigurations rootEventResults={props.rootEventResults} />
           <DiscoverButton
           <DiscoverButton
             size="sm"
             size="sm"
             to={props.traceEventView.getResultsViewUrlTarget(
             to={props.traceEventView.getResultsViewUrlTarget(

+ 0 - 64
static/app/views/performance/newTraceDetails/traceWarnings.tsx

@@ -1,64 +0,0 @@
-import {useEffect} from 'react';
-import * as Sentry from '@sentry/react';
-
-import Alert from 'sentry/components/alert';
-import ExternalLink from 'sentry/components/links/externalLink';
-import {t} from 'sentry/locale';
-import useOrganization from 'sentry/utils/useOrganization';
-import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
-
-import {TraceType} from './traceType';
-
-type TraceWarningsProps = {
-  type: TraceType;
-};
-
-export function TraceWarnings({type}: TraceWarningsProps) {
-  const organization = useOrganization();
-
-  useEffect(() => {
-    traceAnalytics.trackTraceWarningType(type, organization);
-  }, [type, organization]);
-
-  switch (type) {
-    case TraceType.NO_ROOT:
-      return (
-        <Alert type="info" showIcon>
-          <ExternalLink href="https://docs.sentry.io/concepts/key-terms/tracing/trace-view/#orphan-traces-and-broken-subtraces">
-            {t(
-              'A root transaction is missing. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
-            )}
-          </ExternalLink>
-        </Alert>
-      );
-    case TraceType.BROKEN_SUBTRACES:
-      return (
-        <Alert type="info" showIcon>
-          <ExternalLink href="https://docs.sentry.io/concepts/key-terms/tracing/trace-view/#orphan-traces-and-broken-subtraces">
-            {t(
-              'This trace has broken subtraces. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
-            )}
-          </ExternalLink>
-        </Alert>
-      );
-    // Multiple roots are an edge case in browser SDKs and should be handled by the SDK.
-    // The user should not see a warning as they cannot do anything about it.
-    case TraceType.BROWSER_MULTIPLE_ROOTS:
-      return null;
-    case TraceType.MULTIPLE_ROOTS:
-      return (
-        <Alert type="info" showIcon>
-          <ExternalLink href="https://docs.sentry.io/concepts/key-terms/tracing/trace-view/#multiple-roots">
-            {t('Multiple root transactions have been found with this trace ID.')}
-          </ExternalLink>
-        </Alert>
-      );
-    case TraceType.ONLY_ERRORS:
-    case TraceType.ONE_ROOT:
-    case TraceType.EMPTY_TRACE:
-      return null;
-    default:
-      Sentry.captureMessage(`Unhandled trace type - ${type}`);
-      return null;
-  }
-}