Browse Source

feat(insights): update headers from module to trace view (#79924)

Work for #77572 

This Pr makes the following changes.
1. When you click into the trace view from a module (typically from the
span samples panel) and user is within a performance domain view
(frontend, backend, ai, mobile), the headers are correctly reflected.
(There is likely some outstanding urls, but I can go through this after
this PR)
<img width="1037" alt="image"
src="https://github.com/user-attachments/assets/71eaef61-2930-4a2f-9811-4f6098c4e5be">

2. Updates the trace view header to build urls from `ModuleURLBuilder`
instead of hardcoding them.

3. Adds LLM monitoring module, and some general cleanup items.
Dominik Buszowiecki 4 months ago
parent
commit
6ffb191da1

+ 4 - 0
static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx

@@ -21,6 +21,7 @@ import {
   TextAlignRight,
 } from 'sentry/views/insights/common/components/textAlign';
 import type {SpanSample} from 'sentry/views/insights/common/queries/useSpanSamples';
+import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
 import {type ModuleName, SpanMetricsField} from 'sentry/views/insights/types';
 
 const {HTTP_RESPONSE_CONTENT_LENGTH, SPAN_DESCRIPTION} = SpanMetricsField;
@@ -93,6 +94,7 @@ export function SpanSamplesTable({
 }: Props) {
   const location = useLocation();
   const organization = useOrganization();
+  const {view} = useDomainViewFilters();
 
   function renderHeadCell(column: GridColumnHeader): React.ReactNode {
     if (
@@ -130,6 +132,7 @@ export function SpanSamplesTable({
             },
             spanId: row.span_id,
             source,
+            view,
           })}
         >
           {row['transaction.id'].slice(0, 8)}
@@ -161,6 +164,7 @@ export function SpanSamplesTable({
             },
             spanId: row.span_id,
             source,
+            view,
           })}
         >
           {row.span_id}

+ 4 - 1
static/app/views/insights/common/utils/useModuleURL.tsx

@@ -49,7 +49,10 @@ export const useModuleURL = (
   return builder(moduleName, view);
 };
 
-type URLBuilder = (moduleName: RoutableModuleNames, domainView?: DomainView) => string;
+export type URLBuilder = (
+  moduleName: RoutableModuleNames,
+  domainView?: DomainView
+) => string;
 
 /**
  *  This hook returns a function to build URLs for the module summary pages.

+ 13 - 3
static/app/views/insights/llmMonitoring/components/tables/pipelineSpansTable.tsx

@@ -23,6 +23,7 @@ import {
 } from 'sentry/views/insights/common/queries/useDiscover';
 import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
 import {SpanIndexedField} from 'sentry/views/insights/types';
+import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
 
 type Column = GridColumnHeader<
   | SpanIndexedField.ID
@@ -74,6 +75,7 @@ export function isAValidSort(sort: Sort): sort is ValidSort {
 interface Props {
   groupId: string;
   useEAP: boolean;
+  referrer?: string;
 }
 export function PipelineSpansTable({groupId, useEAP}: Props) {
   const location = useLocation();
@@ -162,7 +164,7 @@ export function PipelineSpansTable({groupId, useEAP}: Props) {
               sortParameterName: QueryParameterNames.SPANS_SORT,
             }),
           renderBodyCell: (column, row) =>
-            renderBodyCell(column, row, meta, location, organization),
+            renderBodyCell(column, row, meta, location, organization, groupId),
         }}
       />
     </VisuallyCompleteWithData>
@@ -174,7 +176,8 @@ function renderBodyCell(
   row: any,
   meta: EventsMetaType | undefined,
   location: Location,
-  organization: Organization
+  organization: Organization,
+  groupId: string
 ) {
   if (column.key === SpanIndexedField.ID) {
     if (!row[SpanIndexedField.ID]) {
@@ -191,9 +194,16 @@ function renderBodyCell(
           projectSlug: row[SpanIndexedField.PROJECT],
           traceSlug: row[SpanIndexedField.TRACE],
           timestamp: row[SpanIndexedField.TIMESTAMP],
-          location,
+          location: {
+            ...location,
+            query: {
+              ...location.query,
+              groupId,
+            },
+          },
           eventView: EventView.fromLocation(location),
           spanId: row[SpanIndexedField.ID],
+          source: TraceViewSources.LLM_MODULE,
         })}
       >
         {row[SpanIndexedField.ID]}

+ 1 - 0
static/app/views/insights/pages/settings.ts

@@ -8,6 +8,7 @@ import type {ModuleName} from 'sentry/views/insights/types';
 
 export const OVERVIEW_PAGE_TITLE = t('Overview');
 export const DOMAIN_VIEW_BASE_URL = 'performance';
+export const DOMAIN_VIEW_BASE_TITLE = t('Performance');
 
 export const DOMAIN_VIEW_MODULES: Record<DomainView, ModuleName[]> = {
   frontend: FRONTEND_MODULES,

+ 12 - 0
static/app/views/insights/pages/types.ts

@@ -0,0 +1,12 @@
+import {AI_LANDING_TITLE} from 'sentry/views/insights/pages/ai/settings';
+import {BACKEND_LANDING_TITLE} from 'sentry/views/insights/pages/backend/settings';
+import {FRONTEND_LANDING_TITLE} from 'sentry/views/insights/pages/frontend/settings';
+import {MOBILE_LANDING_TITLE} from 'sentry/views/insights/pages/mobile/settings';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
+
+export const DOMAIN_VIEW_TITLES: Record<DomainView, string> = {
+  ai: AI_LANDING_TITLE,
+  backend: BACKEND_LANDING_TITLE,
+  frontend: FRONTEND_LANDING_TITLE,
+  mobile: MOBILE_LANDING_TITLE,
+};

+ 110 - 77
static/app/views/performance/newTraceDetails/traceHeader/breadcrumbs.tsx

@@ -5,6 +5,18 @@ import type {Crumb} from 'sentry/components/breadcrumbs';
 import {t} from 'sentry/locale';
 import type {Organization} from 'sentry/types/organization';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import type {
+  RoutableModuleNames,
+  URLBuilder,
+} from 'sentry/views/insights/common/utils/useModuleURL';
+import {
+  DOMAIN_VIEW_BASE_TITLE,
+  DOMAIN_VIEW_BASE_URL,
+} from 'sentry/views/insights/pages/settings';
+import {DOMAIN_VIEW_TITLES} from 'sentry/views/insights/pages/types';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
+import {MODULE_TITLES} from 'sentry/views/insights/settings';
+import {ModuleName} from 'sentry/views/insights/types';
 
 import Tab from '../../transactionSummary/tabs';
 
@@ -20,11 +32,31 @@ export const enum TraceViewSources {
   WEB_VITALS_MODULE = 'web_vitals_module',
   CACHES_MODULE = 'caches_module',
   QUEUES_MODULE = 'queues_module',
+  LLM_MODULE = 'llm_module',
+  SCREEN_LOAD_MODULE = 'screen_load_module',
+  MOBILE_SCREENS_MODULE = 'mobile_screens_module',
+  SCREEN_RENDERING_MODULE = 'screen_rendering_module',
   PERFORMANCE_TRANSACTION_SUMMARY = 'performance_transaction_summary',
   PERFORMANCE_TRANSACTION_SUMMARY_PROFILES = 'performance_transaction_summary_profiles',
   ISSUE_DETAILS = 'issue_details',
 }
 
+// Ideally every new entry to ModuleName, would require a new source to be added here so we don't miss any.
+const TRACE_SOURCE_TO_MODULE: Partial<Record<TraceViewSources, ModuleName>> = {
+  app_starts_module: ModuleName.APP_START,
+  assets_module: ModuleName.RESOURCE,
+  caches_module: ModuleName.CACHE,
+  llm_module: ModuleName.AI,
+  queries_module: ModuleName.DB,
+  requests_module: ModuleName.HTTP,
+  screen_loads_module: ModuleName.SCREEN_LOAD,
+  web_vitals_module: ModuleName.VITAL,
+  queues_module: ModuleName.QUEUE,
+  screen_load_module: ModuleName.SCREEN_LOAD,
+  screen_rendering_module: ModuleName.SCREEN_RENDERING,
+  mobile_screens_module: ModuleName.MOBILE_SCREENS,
+};
+
 function getBreadCrumbTarget(
   path: string,
   query: Location['query'],
@@ -135,36 +167,63 @@ function getIssuesBreadCrumbs(organization: Organization, location: Location) {
   return crumbs;
 }
 
-function getInsightsModuleBreadcrumbs(location: Location, organization: Organization) {
+function getInsightsModuleBreadcrumbs(
+  location: Location,
+  organization: Organization,
+  moduleURLBuilder: URLBuilder,
+  view?: DomainView
+) {
   const crumbs: Crumb[] = [];
 
-  crumbs.push({
-    label: t('Insights'),
-  });
+  if (view && DOMAIN_VIEW_TITLES[view]) {
+    crumbs.push({
+      label: DOMAIN_VIEW_BASE_TITLE,
+      to: undefined,
+    });
+    crumbs.push({
+      label: DOMAIN_VIEW_TITLES[view],
+      to: getBreadCrumbTarget(
+        `${DOMAIN_VIEW_BASE_URL}/${view}/`,
+        location.query,
+        organization
+      ),
+    });
+  } else {
+    crumbs.push({
+      label: t('Insights'),
+    });
+  }
 
-  switch (location.query.source) {
-    case TraceViewSources.REQUESTS_MODULE:
-      crumbs.push({
-        label: t('Requests'),
-        to: getBreadCrumbTarget(`insights/http/`, location.query, organization),
-      });
+  let moduleName: RoutableModuleNames | undefined = undefined;
 
+  if (
+    typeof location.query.source === 'string' &&
+    TRACE_SOURCE_TO_MODULE[location.query.source]
+  ) {
+    moduleName = TRACE_SOURCE_TO_MODULE[location.query.source] as RoutableModuleNames;
+    crumbs.push({
+      label: MODULE_TITLES[moduleName],
+      to: moduleURLBuilder(moduleName),
+    });
+  }
+
+  switch (moduleName) {
+    case ModuleName.HTTP:
       crumbs.push({
         label: t('Domain Summary'),
-        to: getBreadCrumbTarget(`insights/http/domains/`, location.query, organization),
+        to: getBreadCrumbTarget(
+          `${moduleURLBuilder(moduleName, view)}/domains`,
+          location.query,
+          organization
+        ),
       });
       break;
-    case TraceViewSources.QUERIES_MODULE:
-      crumbs.push({
-        label: t('Queries'),
-        to: getBreadCrumbTarget(`insights/database`, location.query, organization),
-      });
-
+    case ModuleName.DB:
       if (location.query.groupId) {
         crumbs.push({
           label: t('Query Summary'),
           to: getBreadCrumbTarget(
-            `insights/database/spans/span/${location.query.groupId}`,
+            `${moduleURLBuilder(moduleName, view)}/spans/span/${location.query.groupId}`,
             location.query,
             organization
           ),
@@ -175,17 +234,12 @@ function getInsightsModuleBreadcrumbs(location: Location, organization: Organiza
         });
       }
       break;
-    case TraceViewSources.ASSETS_MODULE:
-      crumbs.push({
-        label: t('Assets'),
-        to: getBreadCrumbTarget(`insights/browser/assets`, location.query, organization),
-      });
-
+    case ModuleName.RESOURCE:
       if (location.query.groupId) {
         crumbs.push({
           label: t('Asset Summary'),
           to: getBreadCrumbTarget(
-            `insights/browser/assets/spans/span/${location.query.groupId}`,
+            `${moduleURLBuilder(moduleName)}/spans/span/${location.query.groupId}`,
             location.query,
             organization
           ),
@@ -196,80 +250,59 @@ function getInsightsModuleBreadcrumbs(location: Location, organization: Organiza
         });
       }
       break;
-    case TraceViewSources.APP_STARTS_MODULE:
-      crumbs.push({
-        label: t('App Starts'),
-        to: getBreadCrumbTarget(
-          `insights/mobile/app-startup`,
-          location.query,
-          organization
-        ),
-      });
-
+    case ModuleName.APP_START:
       crumbs.push({
         label: t('Screen Summary'),
         to: getBreadCrumbTarget(
-          `mobile/app-startup/spans/`,
+          `${moduleURLBuilder(moduleName, view)}/spans`,
           location.query,
           organization
         ),
       });
       break;
-    case TraceViewSources.SCREEN_LOADS_MODULE:
-      crumbs.push({
-        label: t('Screen Loads'),
-        to: getBreadCrumbTarget(`insights/mobile/screens`, location.query, organization),
-      });
-
+    case ModuleName.SCREEN_LOAD:
       crumbs.push({
         label: t('Screen Summary'),
         to: getBreadCrumbTarget(
-          `insights/mobile/screens/spans`,
+          `${moduleURLBuilder(moduleName, view)}/spans`,
           location.query,
           organization
         ),
       });
       break;
-    case TraceViewSources.WEB_VITALS_MODULE:
-      crumbs.push({
-        label: t('Web Vitals'),
-        to: getBreadCrumbTarget(
-          `insights/browser/pageloads`,
-          location.query,
-          organization
-        ),
-      });
-
+    case ModuleName.VITAL:
       crumbs.push({
         label: t('Page Overview'),
         to: getBreadCrumbTarget(
-          `insights/browser/pageloads/overview`,
+          `${moduleURLBuilder(moduleName, view)}/overview`,
           location.query,
           organization
         ),
       });
       break;
-    case TraceViewSources.CACHES_MODULE:
-      crumbs.push({
-        label: t('Caches'),
-        to: getBreadCrumbTarget(`insights/caches`, location.query, organization),
-      });
-      break;
-    case TraceViewSources.QUEUES_MODULE:
-      crumbs.push({
-        label: t('Queues'),
-        to: getBreadCrumbTarget(`insights/queues`, location.query, organization),
-      });
-
+    case ModuleName.QUEUE:
       crumbs.push({
         label: t('Destination Summary'),
         to: getBreadCrumbTarget(
-          `insights/queues/destination`,
+          `${moduleURLBuilder(moduleName, view)}/destination`,
           location.query,
           organization
         ),
       });
       break;
+    case ModuleName.AI:
+      if (location.query.groupId) {
+        crumbs.push({
+          label: t('Pipeline Summary'),
+          to: getBreadCrumbTarget(
+            `${moduleURLBuilder(moduleName, view)}/pipeline-type/${location.query.groupId}`,
+            location.query,
+            organization
+          ),
+        });
+      }
+      break;
+    case ModuleName.CACHE:
     default:
       break;
   }
@@ -283,8 +316,17 @@ function getInsightsModuleBreadcrumbs(location: Location, organization: Organiza
 
 export function getTraceViewBreadcrumbs(
   organization: Organization,
-  location: Location
+  location: Location,
+  moduleUrlBuilder: URLBuilder,
+  view?: DomainView
 ): Crumb[] {
+  if (
+    typeof location.query.source === 'string' &&
+    TRACE_SOURCE_TO_MODULE[location.query.source]
+  ) {
+    return getInsightsModuleBreadcrumbs(location, organization, moduleUrlBuilder, view);
+  }
+
   switch (location.query.source) {
     case TraceViewSources.TRACES:
       return [
@@ -320,15 +362,6 @@ export function getTraceViewBreadcrumbs(
       return getIssuesBreadCrumbs(organization, location);
     case TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY:
       return getPerformanceBreadCrumbs(organization, location);
-    case TraceViewSources.REQUESTS_MODULE:
-    case TraceViewSources.QUERIES_MODULE:
-    case TraceViewSources.ASSETS_MODULE:
-    case TraceViewSources.APP_STARTS_MODULE:
-    case TraceViewSources.SCREEN_LOADS_MODULE:
-    case TraceViewSources.WEB_VITALS_MODULE:
-    case TraceViewSources.CACHES_MODULE:
-    case TraceViewSources.QUEUES_MODULE:
-      return getInsightsModuleBreadcrumbs(location, organization);
     default:
       return [{label: t('Trace View')}];
   }

+ 32 - 3
static/app/views/performance/newTraceDetails/traceHeader/index.tsx

@@ -20,6 +20,8 @@ import type RequestError from 'sentry/utils/requestError/requestError';
 import {useLocation} from 'sentry/utils/useLocation';
 import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
 import {ProjectsRenderer} from 'sentry/views/explore/tables/tracesTable/fieldRenderers';
+import {useModuleURLBuilder} from 'sentry/views/insights/common/utils/useModuleURL';
+import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
 
 import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta';
 import TraceConfigurations from '../traceConfigurations';
@@ -40,13 +42,22 @@ interface TraceMetadataHeaderProps {
 }
 
 function PlaceHolder({organization}: {organization: Organization}) {
+  const {view} = useDomainViewFilters();
+  const moduleURLBuilder = useModuleURLBuilder(true);
   const location = useLocation();
 
   return (
     <Layout.Header>
       <HeaderContent>
         <HeaderRow>
-          <Breadcrumbs crumbs={getTraceViewBreadcrumbs(organization, location)} />
+          <Breadcrumbs
+            crumbs={getTraceViewBreadcrumbs(
+              organization,
+              location,
+              moduleURLBuilder,
+              view
+            )}
+          />
         </HeaderRow>
         <HeaderRow>
           <PlaceHolderTitleWrapper>
@@ -92,6 +103,8 @@ const StyledPlaceholder = styled(Placeholder)<{_height: number; _width: number}>
 
 function LegacyTraceMetadataHeader(props: TraceMetadataHeaderProps) {
   const location = useLocation();
+  const {view} = useDomainViewFilters();
+  const moduleURLBuilder = useModuleURLBuilder(true);
 
   const trackOpenInDiscover = useCallback(() => {
     trackAnalytics('performance_views.trace_view.open_in_discover', {
@@ -102,7 +115,14 @@ function LegacyTraceMetadataHeader(props: TraceMetadataHeaderProps) {
   return (
     <Layout.Header>
       <Layout.HeaderContent>
-        <Breadcrumbs crumbs={getTraceViewBreadcrumbs(props.organization, location)} />
+        <Breadcrumbs
+          crumbs={getTraceViewBreadcrumbs(
+            props.organization,
+            location,
+            moduleURLBuilder,
+            view
+          )}
+        />
       </Layout.HeaderContent>
       <Layout.HeaderActions>
         <ButtonBar gap={1}>
@@ -131,6 +151,8 @@ function LegacyTraceMetadataHeader(props: TraceMetadataHeaderProps) {
 export function TraceMetaDataHeader(props: TraceMetadataHeaderProps) {
   const location = useLocation();
   const hasNewTraceViewUi = useHasTraceNewUi();
+  const {view} = useDomainViewFilters();
+  const moduleURLBuilder = useModuleURLBuilder(true);
 
   if (!hasNewTraceViewUi) {
     return <LegacyTraceMetadataHeader {...props} />;
@@ -149,7 +171,14 @@ export function TraceMetaDataHeader(props: TraceMetadataHeaderProps) {
     <Layout.Header>
       <HeaderContent>
         <HeaderRow>
-          <Breadcrumbs crumbs={getTraceViewBreadcrumbs(props.organization, location)} />
+          <Breadcrumbs
+            crumbs={getTraceViewBreadcrumbs(
+              props.organization,
+              location,
+              moduleURLBuilder,
+              view
+            )}
+          />
         </HeaderRow>
         <HeaderRow>
           <Title traceSlug={props.traceSlug} tree={props.tree} />