Browse Source

feat(profiling): Support both profile formats in transaction samples tab (#75165)

Similar to #75099. This will render a profiling icon to link to the
transaction/continuous profile automatically based on the data ingested.
Tony Xiao 7 months ago
parent
commit
91b183ffab

+ 71 - 43
static/app/views/performance/transactionSummary/transactionEvents/content.tsx

@@ -1,3 +1,4 @@
+import {useMemo} from 'react';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 import omit from 'lodash/omit';
@@ -72,57 +73,84 @@ function EventsContent(props: Props) {
     projects,
   } = props;
   const routes = useRoutes();
-  const eventView = originalEventView.clone();
-  const transactionsListTitles = TRANSACTIONS_LIST_TITLES.slice();
-  const project = projects.find(p => p.id === projectId);
 
-  const fields = [...eventView.fields];
+  const {eventView, titles} = useMemo(() => {
+    const eventViewClone = originalEventView.clone();
+    const transactionsListTitles = TRANSACTIONS_LIST_TITLES.slice();
+    const project = projects.find(p => p.id === projectId);
 
-  if (webVital) {
-    transactionsListTitles.splice(3, 0, webVital);
-  }
+    const fields = [...eventViewClone.fields];
 
-  const spanOperationBreakdownConditions = filterToSearchConditions(
-    spanOperationBreakdownFilter,
-    location
-  );
+    if (webVital) {
+      transactionsListTitles.splice(3, 0, webVital);
+    }
 
-  if (spanOperationBreakdownConditions) {
-    eventView.query = `${eventView.query} ${spanOperationBreakdownConditions}`.trim();
-    transactionsListTitles.splice(2, 1, t('%s duration', spanOperationBreakdownFilter));
-  }
+    const spanOperationBreakdownConditions = filterToSearchConditions(
+      spanOperationBreakdownFilter,
+      location
+    );
 
-  const platform = platformToPerformanceType(projects, eventView.project);
-  if (platform === ProjectPerformanceType.BACKEND) {
-    const userIndex = transactionsListTitles.indexOf('user');
-    if (userIndex > 0) {
-      transactionsListTitles.splice(userIndex + 1, 0, 'http.method');
-      fields.splice(userIndex + 1, 0, {field: 'http.method'});
+    if (spanOperationBreakdownConditions) {
+      eventViewClone.query =
+        `${eventViewClone.query} ${spanOperationBreakdownConditions}`.trim();
+      transactionsListTitles.splice(2, 1, t('%s duration', spanOperationBreakdownFilter));
     }
-  }
 
-  if (
-    organization.features.includes('profiling') &&
-    project &&
-    // only show for projects that already sent a profile
-    // once we have a more compact design we will show this for
-    // projects that support profiling as well
-    project.hasProfiles
-  ) {
-    transactionsListTitles.push(t('profile'));
-    fields.push({field: 'profile.id'});
-  }
+    const platform = platformToPerformanceType(projects, eventViewClone.project);
+    if (platform === ProjectPerformanceType.BACKEND) {
+      const userIndex = transactionsListTitles.indexOf('user');
+      if (userIndex > 0) {
+        transactionsListTitles.splice(userIndex + 1, 0, 'http.method');
+        fields.splice(userIndex + 1, 0, {field: 'http.method'});
+      }
+    }
 
-  if (
-    organization.features.includes('session-replay') &&
-    project &&
-    projectSupportsReplay(project)
-  ) {
-    transactionsListTitles.push(t('replay'));
-    fields.push({field: 'replayId'});
-  }
+    if (
+      // only show for projects that already sent a profile
+      // once we have a more compact design we will show this for
+      // projects that support profiling as well
+      project?.hasProfiles &&
+      (organization.features.includes('profiling') ||
+        organization.features.includes('continuous-profiling'))
+    ) {
+      transactionsListTitles.push(t('profile'));
 
-  eventView.fields = fields;
+      if (organization.features.includes('profiling')) {
+        fields.push({field: 'profile.id'});
+      }
+
+      if (organization.features.includes('continuous-profiling')) {
+        fields.push({field: 'profiler.id'});
+        fields.push({field: 'thread.id'});
+        fields.push({field: 'precise.start_ts'});
+        fields.push({field: 'precise.finish_ts'});
+      }
+    }
+
+    if (
+      organization.features.includes('session-replay') &&
+      project &&
+      projectSupportsReplay(project)
+    ) {
+      transactionsListTitles.push(t('replay'));
+      fields.push({field: 'replayId'});
+    }
+
+    eventViewClone.fields = fields;
+
+    return {
+      eventView: eventViewClone,
+      titles: transactionsListTitles,
+    };
+  }, [
+    originalEventView,
+    location,
+    organization,
+    projects,
+    projectId,
+    spanOperationBreakdownFilter,
+    webVital,
+  ]);
 
   return (
     <Layout.Main fullWidth>
@@ -133,7 +161,7 @@ function EventsContent(props: Props) {
         routes={routes}
         location={location}
         setError={setError}
-        columnTitles={transactionsListTitles}
+        columnTitles={titles}
         transactionName={transactionName}
       />
     </Layout.Main>

+ 32 - 5
static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx

@@ -5,6 +5,7 @@ import type {Location, LocationDescriptor, LocationDescriptorObject} from 'histo
 import groupBy from 'lodash/groupBy';
 
 import {Client} from 'sentry/api';
+import {LinkButton} from 'sentry/components/button';
 import type {GridColumn} from 'sentry/components/gridEditable';
 import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
 import SortLink from 'sentry/components/gridEditable/sortLink';
@@ -12,6 +13,7 @@ import Link from 'sentry/components/links/link';
 import Pagination from 'sentry/components/pagination';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {Tooltip} from 'sentry/components/tooltip';
+import {IconProfiling} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import type {IssueAttachment, Organization} from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
@@ -29,6 +31,7 @@ import {
 } from 'sentry/utils/discover/fields';
 import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
 import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
+import {isEmptyObject} from 'sentry/utils/object/isEmptyObject';
 import parseLinkHeader from 'sentry/utils/parseLinkHeader';
 import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
 import CellAction, {Actions, updateQuery} from 'sentry/views/discover/table/cellAction';
@@ -47,6 +50,23 @@ import {
 import type {TitleProps} from './operationSort';
 import OperationSort from './operationSort';
 
+function shouldRenderColumn(containsSpanOpsBreakdown: boolean, col: string): boolean {
+  if (containsSpanOpsBreakdown && isSpanOperationBreakdownField(col)) {
+    return false;
+  }
+
+  if (
+    col === 'profiler.id' ||
+    col === 'thread.id' ||
+    col === 'precise.start_ts' ||
+    col === 'precise.finish_ts'
+  ) {
+    return false;
+  }
+
+  return true;
+}
+
 function OperationTitle({onClick}: TitleProps) {
   return (
     <div onClick={onClick}>
@@ -242,7 +262,15 @@ class EventsTable extends Component<Props, State> {
             handleCellAction={this.handleCellAction(column)}
             allowActions={allowActions}
           >
-            {target ? <Link to={target}>{rendered}</Link> : rendered}
+            <div>
+              <LinkButton
+                disabled={!target || isEmptyObject(target)}
+                to={target || {}}
+                size="xs"
+              >
+                <IconProfiling size="xs" />
+              </LinkButton>
+            </div>
           </CellAction>
         </Tooltip>
       );
@@ -379,7 +407,7 @@ class EventsTable extends Component<Props, State> {
     totalEventsView.fields = [{field: 'count()', width: -1}];
 
     const {widths} = this.state;
-    const containsSpanOpsBreakdown = eventView
+    const containsSpanOpsBreakdown = !!eventView
       .getColumns()
       .find(
         (col: TableColumn<React.ReactText>) =>
@@ -388,9 +416,8 @@ class EventsTable extends Component<Props, State> {
 
     const columnOrder = eventView
       .getColumns()
-      .filter(
-        (col: TableColumn<React.ReactText>) =>
-          !containsSpanOpsBreakdown || !isSpanOperationBreakdownField(col.name)
+      .filter((col: TableColumn<React.ReactText>) =>
+        shouldRenderColumn(containsSpanOpsBreakdown, col.name)
       )
       .map((col: TableColumn<React.ReactText>, i: number) => {
         if (typeof widths[i] === 'number') {