Browse Source

feat(new-trace): Redesigning span details content. (#81366)

Designs:
[link](https://www.figma.com/design/zfReOSQiHfODLLoFWpYVdW/Trace-V3?node-id=42-1924&node-type=frame&t=WGeiXNC70KIF6zWC-0)

Feature Flag: trace-view-new-ui

To summarize the changes, the current ui components are converted to
`LegacyComponent` and the new code exists under `Component`. It'll make
it easier to cleanup once we've reached general availability.

<img width="1278" alt="Screenshot 2024-11-27 at 11 50 15 AM"
src="https://github.com/user-attachments/assets/03ca58ee-f60a-4bdd-b3dc-211d7d2378ad">

---------------

<img width="1311" alt="Screenshot 2024-11-27 at 11 50 46 AM"
src="https://github.com/user-attachments/assets/80b9a032-7689-499c-b7f2-ad409f5d561b">

---------------

<img width="1303" alt="Screenshot 2024-11-27 at 11 52 20 AM"
src="https://github.com/user-attachments/assets/33a9791f-249d-4087-a3f9-505de03cb232">

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdullah Khan 3 months ago
parent
commit
6751c26de3

+ 39 - 11
static/app/components/events/interfaces/spans/spanProfileDetails.tsx

@@ -10,8 +10,7 @@ import QuestionTooltip from 'sentry/components/questionTooltip';
 import {IconChevron, IconProfiling} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {EventTransaction, Frame} from 'sentry/types/event';
-import {EntryType} from 'sentry/types/event';
+import {EntryType, type EventTransaction, type Frame} from 'sentry/types/event';
 import type {PlatformKey} from 'sentry/types/project';
 import {StackView} from 'sentry/types/stacktrace';
 import {defined} from 'sentry/utils';
@@ -38,15 +37,7 @@ interface SpanProfileDetailsProps {
   onNoProfileFound?: () => void;
 }
 
-export function SpanProfileDetails({
-  event,
-  span,
-  onNoProfileFound,
-}: SpanProfileDetailsProps) {
-  const organization = useOrganization();
-  const {projects} = useProjects();
-  const project = projects.find(p => p.id === event.projectID);
-
+export function useSpanProfileDetails(event, span) {
   const profileGroup = useProfileGroup();
 
   const processedEvent = useMemo(() => {
@@ -140,6 +131,43 @@ export function SpanProfileDetails({
     };
   }, [index, maxNodes, event, nodes]);
 
+  return {
+    processedEvent,
+    profileGroup,
+    profile,
+    nodes,
+    index,
+    setIndex,
+    totalWeight,
+    maxNodes,
+    frames,
+    hasPrevious,
+    hasNext,
+  };
+}
+
+export function SpanProfileDetails({
+  event,
+  span,
+  onNoProfileFound,
+}: SpanProfileDetailsProps) {
+  const organization = useOrganization();
+  const {projects} = useProjects();
+  const project = projects.find(p => p.id === event.projectID);
+  const {
+    processedEvent,
+    profileGroup,
+    profile,
+    nodes,
+    index,
+    setIndex,
+    maxNodes,
+    hasNext,
+    hasPrevious,
+    totalWeight,
+    frames,
+  } = useSpanProfileDetails(event, span);
+
   const spanTarget =
     project &&
     profileGroup &&

+ 1 - 0
static/app/components/events/interfaces/spans/types.tsx

@@ -56,6 +56,7 @@ export type RawSpanType = {
   op?: string;
   origin?: string;
   parent_span_id?: string;
+  project_slug?: string;
   same_process_as_parent?: boolean;
   sentry_tags?: Record<string, string>;
   'span.averageResults'?: {

+ 89 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/autogroup/index.tsx

@@ -0,0 +1,89 @@
+import styled from '@emotion/styled';
+
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+
+import {isSiblingAutogroupedNode} from '../../../traceGuards';
+import type {ParentAutogroupNode} from '../../../traceModels/parentAutogroupNode';
+import type {SiblingAutogroupNode} from '../../../traceModels/siblingAutogroupNode';
+import {useHasTraceNewUi} from '../../../useHasTraceNewUi';
+import type {TraceTreeNodeDetailsProps} from '../../tabs/traceTreeNodeDetails';
+import {TraceDrawerComponents} from '../styles';
+
+import {ParentAutogroupNodeDetails} from './parentAutogroup';
+import {SiblingAutogroupNodeDetails} from './siblingAutogroup';
+
+export function AutogroupNodeDetails(
+  props: TraceTreeNodeDetailsProps<ParentAutogroupNode | SiblingAutogroupNode>
+) {
+  const hasTraceNewUi = useHasTraceNewUi();
+  const {node, organization, onTabScrollToNode} = props;
+
+  if (!hasTraceNewUi) {
+    if (isSiblingAutogroupedNode(node)) {
+      return (
+        <SiblingAutogroupNodeDetails
+          {...(props as TraceTreeNodeDetailsProps<SiblingAutogroupNode>)}
+        />
+      );
+    }
+
+    return (
+      <ParentAutogroupNodeDetails
+        {...(props as TraceTreeNodeDetailsProps<ParentAutogroupNode>)}
+      />
+    );
+  }
+
+  return (
+    <TraceDrawerComponents.DetailContainer hasNewTraceUi={hasTraceNewUi}>
+      <TraceDrawerComponents.HeaderContainer>
+        <TraceDrawerComponents.Title>
+          <TraceDrawerComponents.LegacyTitleText>
+            <TraceDrawerComponents.TitleText>
+              {t('Autogroup')}
+            </TraceDrawerComponents.TitleText>
+            <TraceDrawerComponents.SubtitleWithCopyButton
+              text={`ID: ${node.value.span_id}`}
+            />
+          </TraceDrawerComponents.LegacyTitleText>
+        </TraceDrawerComponents.Title>
+        <TraceDrawerComponents.NodeActions
+          node={node}
+          organization={organization}
+          onTabScrollToNode={onTabScrollToNode}
+        />
+      </TraceDrawerComponents.HeaderContainer>
+      <TextBlock>
+        {t(
+          'This block represents autogrouped spans. We do this to reduce noise whenever it fits one of the following criteria:'
+        )}
+      </TextBlock>
+      <BulletList>
+        <li>{t('5 or more siblings with the same operation and description')}</li>
+        <li>{t('2 or more descendants with the same operation')}</li>
+      </BulletList>
+      <TextBlock>
+        {t(
+          'You can either open this autogroup using the chevron on the span or turn this functionality off using the settings dropdown above.'
+        )}
+      </TextBlock>
+    </TraceDrawerComponents.DetailContainer>
+  );
+}
+
+const TextBlock = styled('div')`
+  font-size: ${p => p.theme.fontSizeLarge};
+  line-height: 1.5;
+  margin-bottom: ${space(2)};
+`;
+
+const BulletList = styled('ul')`
+  list-style-type: disc;
+  padding-left: 20px;
+  margin-bottom: ${space(2)};
+
+  li {
+    margin-bottom: ${space(1)};
+  }
+`;

+ 7 - 8
static/app/views/performance/newTraceDetails/traceDrawer/details/parentAutogroup.tsx → static/app/views/performance/newTraceDetails/traceDrawer/details/autogroup/parentAutogroup.tsx

@@ -4,14 +4,13 @@ import {useTheme} from '@emotion/react';
 import {IconGroup} from 'sentry/icons';
 import {t} from 'sentry/locale';
 
-import type {TraceTreeNodeDetailsProps} from '../../traceDrawer/tabs/traceTreeNodeDetails';
-import type {ParentAutogroupNode} from '../../traceModels/parentAutogroupNode';
-import {TraceTree} from '../../traceModels/traceTree';
-import {makeTraceNodeBarColor} from '../../traceRow/traceBar';
-import {getTraceTabTitle} from '../../traceState/traceTabs';
-
-import {IssueList} from './issues/issues';
-import {type SectionCardKeyValueList, TraceDrawerComponents} from './styles';
+import type {TraceTreeNodeDetailsProps} from '../../../traceDrawer/tabs/traceTreeNodeDetails';
+import type {ParentAutogroupNode} from '../../../traceModels/parentAutogroupNode';
+import {TraceTree} from '../../../traceModels/traceTree';
+import {makeTraceNodeBarColor} from '../../../traceRow/traceBar';
+import {getTraceTabTitle} from '../../../traceState/traceTabs';
+import {IssueList} from '.././issues/issues';
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '.././styles';
 
 export function ParentAutogroupNodeDetails({
   node,

+ 7 - 8
static/app/views/performance/newTraceDetails/traceDrawer/details/siblingAutogroup.tsx → static/app/views/performance/newTraceDetails/traceDrawer/details/autogroup/siblingAutogroup.tsx

@@ -4,14 +4,13 @@ import {useTheme} from '@emotion/react';
 import {IconGroup} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 
-import type {TraceTreeNodeDetailsProps} from '../../traceDrawer/tabs/traceTreeNodeDetails';
-import type {SiblingAutogroupNode} from '../../traceModels/siblingAutogroupNode';
-import {TraceTree} from '../../traceModels/traceTree';
-import {makeTraceNodeBarColor} from '../../traceRow/traceBar';
-import {getTraceTabTitle} from '../../traceState/traceTabs';
-
-import {IssueList} from './issues/issues';
-import {type SectionCardKeyValueList, TraceDrawerComponents} from './styles';
+import type {TraceTreeNodeDetailsProps} from '../../../traceDrawer/tabs/traceTreeNodeDetails';
+import type {SiblingAutogroupNode} from '../../../traceModels/siblingAutogroupNode';
+import {TraceTree} from '../../../traceModels/traceTree';
+import {makeTraceNodeBarColor} from '../../../traceRow/traceBar';
+import {getTraceTabTitle} from '../../../traceState/traceTabs';
+import {IssueList} from '.././issues/issues';
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '.././styles';
 
 export function SiblingAutogroupNodeDetails({
   node,

+ 52 - 2
static/app/views/performance/newTraceDetails/traceDrawer/details/error.tsx

@@ -10,6 +10,7 @@ import {
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
 import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import type {EventError} from 'sentry/types/event';
 import {useApiQuery} from 'sentry/utils/queryClient';
 
@@ -19,11 +20,61 @@ import {TraceTree} from '../../traceModels/traceTree';
 import type {TraceTreeNode} from '../../traceModels/traceTreeNode';
 import {makeTraceNodeBarColor} from '../../traceRow/traceBar';
 import {getTraceTabTitle} from '../../traceState/traceTabs';
+import {useHasTraceNewUi} from '../../useHasTraceNewUi';
 
 import {IssueList} from './issues/issues';
 import {type SectionCardKeyValueList, TraceDrawerComponents} from './styles';
 
-export function ErrorNodeDetails({
+export function ErrorNodeDetails(
+  props: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.TraceError>>
+) {
+  const hasTraceNewUi = useHasTraceNewUi();
+  const {node, organization, onTabScrollToNode} = props;
+  const issues = useMemo(() => {
+    return [...node.errors];
+  }, [node.errors]);
+
+  if (!hasTraceNewUi) {
+    return <LegacyErrorNodeDetails {...props} />;
+  }
+
+  return (
+    <TraceDrawerComponents.DetailContainer hasNewTraceUi={hasTraceNewUi}>
+      <TraceDrawerComponents.HeaderContainer>
+        <TraceDrawerComponents.Title>
+          <TraceDrawerComponents.LegacyTitleText>
+            <TraceDrawerComponents.TitleText>
+              {t('Error')}
+            </TraceDrawerComponents.TitleText>
+            <TraceDrawerComponents.SubtitleWithCopyButton
+              text={`ID: ${props.node.value.event_id}`}
+            />
+          </TraceDrawerComponents.LegacyTitleText>
+        </TraceDrawerComponents.Title>
+        <TraceDrawerComponents.NodeActions
+          node={node}
+          organization={organization}
+          onTabScrollToNode={onTabScrollToNode}
+        />
+      </TraceDrawerComponents.HeaderContainer>
+      <Description>
+        {t(
+          'This error is related to an ongoing issue. For details about how many users this affects and more, go to the issue below.'
+        )}
+      </Description>
+      <IssueList issues={issues} node={node} organization={organization} />
+    </TraceDrawerComponents.DetailContainer>
+  );
+}
+
+const Description = styled('div')`
+  margin-bottom: ${space(2)};
+  font-size: ${p => p.theme.fontSizeLarge};
+  line-height: 1.5;
+  text-align: left;
+`;
+
+function LegacyErrorNodeDetails({
   node,
   organization,
   onTabScrollToNode,
@@ -133,7 +184,6 @@ export function ErrorNodeDetails({
     </TraceDrawerComponents.DetailContainer>
   ) : null;
 }
-
 const StackTraceWrapper = styled('div')`
   .traceback {
     margin-bottom: 0;

+ 3 - 1
static/app/views/performance/newTraceDetails/traceDrawer/details/issues/issues.tsx

@@ -645,6 +645,7 @@ const StyledPanelHeader = styled(PanelHeader)<{hasNewLayout: boolean}>`
 
 const StyledLoadingIndicatorWrapper = styled('div')`
   display: flex;
+  align-items: center;
   justify-content: center;
   width: 100%;
   padding: ${space(2)} 0;
@@ -732,7 +733,8 @@ const StyledPanelItem = styled(StyledLegacyPanelItem)`
   justify-content: left;
   align-items: center;
   gap: ${space(1.5)};
-  padding: 0 ${space(2)};
+  height: fit-content;
+  padding: ${space(1)} ${space(2)};
 `;
 
 const StyledIssueStreamHeaderLabel = styled(IssueStreamHeaderLabel)`

+ 89 - 2
static/app/views/performance/newTraceDetails/traceDrawer/details/missingInstrumentation.tsx

@@ -1,23 +1,110 @@
 import {useTheme} from '@emotion/react';
+import styled from '@emotion/styled';
 
+import ExternalLink from 'sentry/components/links/externalLink';
 import {IconSpan} from 'sentry/icons';
-import {t} from 'sentry/locale';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import getDuration from 'sentry/utils/duration/getDuration';
 import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
 import useProjects from 'sentry/utils/useProjects';
 import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
 import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
 
+import {getCustomInstrumentationLink} from '../../traceConfigurations';
 import {ProfilePreview} from '../../traceDrawer/details/profiling/profilePreview';
 import type {TraceTreeNodeDetailsProps} from '../../traceDrawer/tabs/traceTreeNodeDetails';
 import type {MissingInstrumentationNode} from '../../traceModels/missingInstrumentationNode';
 import {TraceTree} from '../../traceModels/traceTree';
 import {makeTraceNodeBarColor} from '../../traceRow/traceBar';
 import {getTraceTabTitle} from '../../traceState/traceTabs';
+import {useHasTraceNewUi} from '../../useHasTraceNewUi';
 
 import {type SectionCardKeyValueList, TraceDrawerComponents} from './styles';
 
-export function MissingInstrumentationNodeDetails({
+export function MissingInstrumentationNodeDetails(
+  props: TraceTreeNodeDetailsProps<MissingInstrumentationNode>
+) {
+  const {projects} = useProjects();
+  const hasTraceNewUi = useHasTraceNewUi();
+
+  if (!hasTraceNewUi) {
+    return <LegacyMissingInstrumentationNodeDetails {...props} />;
+  }
+
+  const {node, organization, onTabScrollToNode} = props;
+  const event = node.previous.event ?? node.next.event ?? null;
+  const project = projects.find(proj => proj.slug === event?.projectSlug);
+  const profileId = event?.contexts?.profile?.profile_id ?? null;
+
+  return (
+    <TraceDrawerComponents.DetailContainer hasNewTraceUi={hasTraceNewUi}>
+      <TraceDrawerComponents.HeaderContainer>
+        <TraceDrawerComponents.Title>
+          <TraceDrawerComponents.LegacyTitleText>
+            <TraceDrawerComponents.TitleText>
+              {t('No Instrumentation')}
+            </TraceDrawerComponents.TitleText>
+            <TraceDrawerComponents.SubtitleWithCopyButton
+              hideCopyButton
+              text={t('How Awkward')}
+            />
+          </TraceDrawerComponents.LegacyTitleText>
+        </TraceDrawerComponents.Title>
+        <TraceDrawerComponents.NodeActions
+          node={node}
+          organization={organization}
+          onTabScrollToNode={onTabScrollToNode}
+        />
+      </TraceDrawerComponents.HeaderContainer>
+
+      <TextBlock>
+        {tct(
+          'It looks like there’s more than 100ms unaccounted for. This might be a missing service or just idle time. If you know there’s something going on, you can [customInstrumentationLink: add more spans using custom instrumentation].',
+          {
+            customInstrumentationLink: (
+              <ExternalLink href={getCustomInstrumentationLink(project)} />
+            ),
+          }
+        )}
+      </TextBlock>
+
+      {event?.projectSlug ? (
+        <ProfilesProvider
+          orgSlug={organization.slug}
+          projectSlug={node.event?.projectSlug ?? ''}
+          profileId={profileId || ''}
+        >
+          <ProfileContext.Consumer>
+            {profiles => (
+              <ProfileGroupProvider
+                type="flamechart"
+                input={profiles?.type === 'resolved' ? profiles.data : null}
+                traceID={profileId || ''}
+              >
+                <ProfilePreview event={event!} node={node} />
+              </ProfileGroupProvider>
+            )}
+          </ProfileContext.Consumer>
+        </ProfilesProvider>
+      ) : null}
+
+      <TextBlock>
+        {t(
+          "You can turn off the 'No Instrumentation' feature using the settings dropdown above."
+        )}
+      </TextBlock>
+    </TraceDrawerComponents.DetailContainer>
+  );
+}
+
+const TextBlock = styled('div')`
+  font-size: ${p => p.theme.fontSizeLarge};
+  line-height: 1.5;
+  margin-bottom: ${space(2)};
+`;
+
+function LegacyMissingInstrumentationNodeDetails({
   node,
   onParentClick,
   onTabScrollToNode,

+ 209 - 6
static/app/views/performance/newTraceDetails/traceDrawer/details/profiling/profilePreview.tsx

@@ -1,4 +1,4 @@
-import {useMemo, useState} from 'react';
+import {Fragment, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import emptyStateImg from 'sentry-images/spot/profiling-empty-state.svg';
@@ -8,6 +8,8 @@ import {SectionHeading} from 'sentry/components/charts/styles';
 import InlineDocs from 'sentry/components/events/interfaces/spans/inlineDocs';
 import ExternalLink from 'sentry/components/links/externalLink';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
+import Panel from 'sentry/components/panels/panel';
+import PanelBody from 'sentry/components/panels/panelBody';
 import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {t, tct} from 'sentry/locale';
@@ -26,12 +28,15 @@ import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/ro
 import {Rect} from 'sentry/utils/profiling/speedscope';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
+import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection';
+import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
 import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
 import {useProfiles} from 'sentry/views/profiling/profilesProvider';
 
 import type {MissingInstrumentationNode} from '../../../traceModels/missingInstrumentationNode';
 import {TraceTree} from '../../../traceModels/traceTree';
 import type {TraceTreeNode} from '../../../traceModels/traceTreeNode';
+import {useHasTraceNewUi} from '../../../useHasTraceNewUi';
 
 interface SpanProfileProps {
   event: Readonly<EventTransaction>;
@@ -39,6 +44,157 @@ interface SpanProfileProps {
 }
 
 export function ProfilePreview({event, node}: SpanProfileProps) {
+  const {projects} = useProjects();
+  const hasNewTraceUi = useHasTraceNewUi();
+  const profiles = useProfiles();
+  const profileGroup = useProfileGroup();
+  const project = useMemo(
+    () => projects.find(p => p.id === event.projectID),
+    [projects, event]
+  );
+
+  const organization = useOrganization();
+  const [canvasView, setCanvasView] = useState<CanvasView<FlamegraphModel> | null>(null);
+
+  const profile = useMemo(() => {
+    const threadId = profileGroup.profiles[profileGroup.activeProfileIndex]?.threadId;
+    if (!defined(threadId)) {
+      return null;
+    }
+    return profileGroup.profiles.find(p => p.threadId === threadId) ?? null;
+  }, [profileGroup.profiles, profileGroup.activeProfileIndex]);
+
+  const transactionHasProfile = useMemo(() => {
+    return (TraceTree.ParentTransaction(node)?.profiles?.length ?? 0) > 0;
+  }, [node]);
+
+  const flamegraph = useMemo(() => {
+    if (!transactionHasProfile || !profile) {
+      return FlamegraphModel.Example();
+    }
+
+    return new FlamegraphModel(profile, {});
+  }, [transactionHasProfile, profile]);
+
+  if (!hasNewTraceUi) {
+    return (
+      <LegacyProfilePreview event={event} node={node as MissingInstrumentationNode} />
+    );
+  }
+
+  // The most recent profile formats should contain a timestamp indicating
+  // the beginning of the profile. This timestamp can be after the start
+  // timestamp on the transaction, so we need to account for the gap and
+  // make sure the relative start timestamps we compute for the span is
+  // relative to the start of the profile.
+  //
+  // If the profile does not contain a timestamp, we fall back to using the
+  // start timestamp on the transaction. This won't be as accurate but it's
+  // the next best thing.
+  const startTimestamp = profile?.timestamp ?? event.startTimestamp;
+  const relativeStartTimestamp = transactionHasProfile
+    ? node.value.start_timestamp - startTimestamp
+    : 0;
+  const relativeStopTimestamp = transactionHasProfile
+    ? node.value.timestamp - startTimestamp
+    : flamegraph.configSpace.width;
+
+  const profileId = event.contexts.profile?.profile_id || '';
+  // we want to try to go straight to the same config view as the preview
+  const query = canvasView?.configView
+    ? {
+        // TODO: this assumes that profile start timestamp == transaction timestamp
+        fov: Rect.encode(canvasView.configView),
+        // the flamechart persists some preferences to local storage,
+        // force these settings so the view is the same as the preview
+        view: 'top down',
+        type: 'flamechart',
+      }
+    : undefined;
+
+  const target = generateProfileFlamechartRouteWithQuery({
+    orgSlug: organization.slug,
+    projectSlug: event?.projectSlug ?? '',
+    profileId,
+    query,
+  });
+
+  function handleGoToProfile() {
+    trackAnalytics('profiling_views.go_to_flamegraph', {
+      organization,
+      source: 'performance.missing_instrumentation',
+    });
+  }
+
+  const message = (
+    <TextBlock>{t('Or, see if profiling can provide more context on this:')}</TextBlock>
+  );
+
+  if (transactionHasProfile) {
+    return (
+      <FlamegraphThemeProvider>
+        {message}
+        <SectionDivider />
+        <InterimSection
+          title={t('Related Profile')}
+          type="no_instrumentation_profile"
+          initialCollapse={false}
+          actions={
+            <LinkButton size="xs" onClick={handleGoToProfile} to={target}>
+              {t('Open in Profiling')}
+            </LinkButton>
+          }
+        >
+          {/* If you remove this div, padding between elements will break */}
+          <div>
+            <ProfilePreviewLegend />
+            <FlamegraphContainer>
+              {profiles.type === 'loading' ? (
+                <LoadingIndicator />
+              ) : (
+                <FlamegraphPreview
+                  flamegraph={flamegraph}
+                  updateFlamegraphView={setCanvasView}
+                  relativeStartTimestamp={relativeStartTimestamp}
+                  relativeStopTimestamp={relativeStopTimestamp}
+                />
+              )}
+            </FlamegraphContainer>
+          </div>
+        </InterimSection>
+      </FlamegraphThemeProvider>
+    );
+  }
+
+  // The event's platform is more accurate than the project
+  // so use that first and fall back to the project's platform
+  const docsLink =
+    getProfilingDocsForPlatform(event.platform) ??
+    (project && getProfilingDocsForPlatform(project.platform));
+
+  // This project has received a profile before so they've already
+  // set up profiling. No point showing the profiling setup again.
+  if (!docsLink || project?.hasProfiles) {
+    return null;
+  }
+
+  // At this point we must have a project on a supported
+  // platform that has not setup profiling yet
+  return (
+    <Fragment>
+      {message}
+      <SetupProfiling link={docsLink} />
+    </Fragment>
+  );
+}
+
+const TextBlock = styled('div')`
+  font-size: ${p => p.theme.fontSizeLarge};
+  line-height: 1.5;
+  margin-bottom: ${space(2)};
+`;
+
+function LegacyProfilePreview({event, node}: SpanProfileProps) {
   const {projects} = useProjects();
   const profiles = useProfiles();
   const profileGroup = useProfileGroup();
@@ -130,7 +286,7 @@ export function ProfilePreview({event, node}: SpanProfileProps) {
 
   // At this point we must have a project on a supported
   // platform that has not setup profiling yet
-  return <SetupProfiling link={docsLink} />;
+  return <LegacySetupProfiling link={docsLink} />;
 }
 
 interface ProfilePreviewProps {
@@ -210,9 +366,56 @@ function ProfilePreviewLegend() {
 }
 
 function SetupProfiling({link}: {link: string}) {
+  return (
+    <Panel>
+      <StyledPanelBody>
+        <span>
+          <h5>{t('Profiling for a Better Picture')}</h5>
+          <TextBlock>
+            {t(
+              'Profiles can also give you additional context on which functions are getting sampled at the time of these spans.'
+            )}
+          </TextBlock>
+          <LinkButton size="sm" priority="primary" href={link} external>
+            {t('Get Started')}
+          </LinkButton>
+        </span>
+        <ImageContainer>
+          <Image src={emptyStateImg} />
+        </ImageContainer>
+      </StyledPanelBody>
+    </Panel>
+  );
+}
+
+const Image = styled('img')`
+  user-select: none;
+  width: 250px;
+  align-self: center;
+`;
+
+const StyledPanelBody = styled(PanelBody)`
+  display: flex;
+  gap: ${space(2)};
+  justify-content: space-between;
+  padding: ${space(2)};
+  container-type: inline-size;
+`;
+
+const ImageContainer = styled('div')`
+  display: flex;
+  min-width: 200px;
+  justify-content: center;
+
+  @container (max-width: 600px) {
+    display: none;
+  }
+`;
+
+function LegacySetupProfiling({link}: {link: string}) {
   return (
     <Container>
-      <Image src={emptyStateImg} />
+      <LegacyImage src={emptyStateImg} />
       <InstructionsContainer>
         <h5>{t('With Profiling, we could paint a better picture')}</h5>
         <p>
@@ -270,9 +473,9 @@ const InstructionsContainer = styled('div')`
   align-items: start;
 `;
 
-const Image = styled('img')`
+const LegacyImage = styled('img')`
   user-select: none;
-  width: 420px;
+  width: 200px;
   align-self: center;
 
   @media (min-width: ${p => p.theme.breakpoints.medium}) {
@@ -289,7 +492,7 @@ const Image = styled('img')`
 `;
 
 const FlamegraphContainer = styled('div')`
-  height: 310px;
+  height: 200px;
   margin-top: ${space(1)};
   margin-bottom: ${space(1)};
   position: relative;

+ 102 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/spanSummaryLink.tsx

@@ -0,0 +1,102 @@
+import styled from '@emotion/styled';
+
+import type {SpanType} from 'sentry/components/events/interfaces/spans/types';
+import Link from 'sentry/components/links/link';
+import {IconStats} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {EventTransaction} from 'sentry/types/event';
+import type {Organization} from 'sentry/types/organization';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {useLocation} from 'sentry/utils/useLocation';
+import {DATA_TYPE} from 'sentry/views/insights/browser/resources/settings';
+import {resolveSpanModule} from 'sentry/views/insights/common/utils/resolveSpanModule';
+import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
+import {ModuleName} from 'sentry/views/insights/types';
+import {
+  querySummaryRouteWithQuery,
+  resourceSummaryRouteWithQuery,
+} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
+
+interface Props {
+  event: Readonly<EventTransaction>;
+  organization: Organization;
+  span: SpanType;
+}
+
+function SpanSummaryLink(props: Props) {
+  const location = useLocation();
+  const resourceBaseUrl = useModuleURL(ModuleName.RESOURCE);
+  const queryBaseUrl = useModuleURL(ModuleName.DB);
+
+  const {event, organization, span} = props;
+
+  const sentryTags = span.sentry_tags;
+  if (!sentryTags || !sentryTags.group) {
+    return null;
+  }
+
+  const resolvedModule = resolveSpanModule(sentryTags.op, sentryTags.category);
+
+  if (
+    organization.features.includes('insights-initial-modules') &&
+    resolvedModule === ModuleName.DB
+  ) {
+    return (
+      <Link
+        to={querySummaryRouteWithQuery({
+          base: queryBaseUrl,
+          query: location.query,
+          group: sentryTags.group,
+          projectID: event.projectID,
+        })}
+        onClick={() => {
+          trackAnalytics('trace.trace_layout.view_in_insight_module', {
+            organization,
+            module: ModuleName.DB,
+          });
+        }}
+      >
+        <StyledIconStats size="xs" />
+        {t('View Query Summary')}
+      </Link>
+    );
+  }
+
+  if (
+    organization.features.includes('insights-initial-modules') &&
+    resolvedModule === ModuleName.RESOURCE &&
+    resourceSummaryAvailable(sentryTags.op)
+  ) {
+    return (
+      <Link
+        to={resourceSummaryRouteWithQuery({
+          baseUrl: resourceBaseUrl,
+          query: location.query,
+          group: sentryTags.group,
+          projectID: event.projectID,
+        })}
+        onClick={() => {
+          trackAnalytics('trace.trace_layout.view_in_insight_module', {
+            organization,
+            module: ModuleName.RESOURCE,
+          });
+        }}
+      >
+        <StyledIconStats size="xs" />
+        {tct('View [dataType] Summary', {dataType: DATA_TYPE})}
+      </Link>
+    );
+  }
+
+  return null;
+}
+
+const StyledIconStats = styled(IconStats)`
+  margin-right: ${space(0.5)};
+`;
+
+const resourceSummaryAvailable = (op: string = '') =>
+  ['resource.script', 'resource.css'].includes(op);
+
+export default SpanSummaryLink;

Some files were not shown because too many files changed in this diff