Browse Source

feat(profiling): Support both profile formats in transaction summary … (#75099)

…overview

Without changing the end user experience, we're changing the profile
column to render an profile button that links to the underlying profile
regardless of if it's a transaction profile or a continuous profile.
Tony Xiao 7 months ago
parent
commit
7f458d2fff

+ 15 - 11
static/app/components/discover/transactionsTable.tsx

@@ -2,11 +2,13 @@ import {Fragment, PureComponent} from 'react';
 import styled from '@emotion/styled';
 import type {Location, LocationDescriptor} from 'history';
 
+import {LinkButton} from 'sentry/components/button';
 import SortLink from 'sentry/components/gridEditable/sortLink';
 import Link from 'sentry/components/links/link';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {PanelTable} from 'sentry/components/panels/panelTable';
 import QuestionTooltip from 'sentry/components/questionTooltip';
+import {IconProfiling} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
@@ -145,23 +147,25 @@ class TransactionsTable extends PureComponent<Props> {
 
       const target = generateLink?.[field]?.(organization, row, location);
 
-      if (target && !isEmptyObject(target)) {
+      if (fields[index] === 'profile.id') {
+        rendered = (
+          <LinkButton
+            data-test-id={`view-${fields[index]}`}
+            disabled={!target || isEmptyObject(target)}
+            to={target || {}}
+            onClick={getProfileAnalyticsHandler(organization, referrer)}
+            size="xs"
+          >
+            <IconProfiling size="xs" />
+          </LinkButton>
+        );
+      } else if (target && !isEmptyObject(target)) {
         if (fields[index] === 'replayId') {
           rendered = (
             <ViewReplayLink replayId={row.replayId} to={target}>
               {rendered}
             </ViewReplayLink>
           );
-        } else if (fields[index] === 'profile.id') {
-          rendered = (
-            <Link
-              data-test-id={`view-${fields[index]}`}
-              to={target}
-              onClick={getProfileAnalyticsHandler(organization, referrer)}
-            >
-              {rendered}
-            </Link>
-          );
         } else {
           rendered = (
             <Link data-test-id={`view-${fields[index]}`} to={target}>

+ 17 - 0
static/app/utils/dates.tsx

@@ -304,3 +304,20 @@ export function getDateWithTimezoneInUtc(date?: Date, utc?: boolean | null) {
     .utc()
     .toDate();
 }
+
+/**
+ * Converts a string or timestamp in milliseconds to a Date
+ */
+export function getDateFromTimestamp(value: unknown): Date | null {
+  if (typeof value !== 'string' && typeof value !== 'number') {
+    return null;
+  }
+
+  const dateObj = new Date(value);
+
+  if (isNaN(dateObj.getTime())) {
+    return null;
+  }
+
+  return dateObj;
+}

+ 5 - 16
static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts

@@ -1,5 +1,6 @@
 import type {Location, LocationDescriptor} from 'history';
 
+import {getDateFromTimestamp} from 'sentry/utils/dates';
 import {generateContinuousProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
 import {
   isSpanNode,
@@ -10,20 +11,6 @@ import type {
   TraceTreeNode,
 } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 
-function toDate(value: unknown): Date | null {
-  if (typeof value !== 'string' && typeof value !== 'number') {
-    return null;
-  }
-
-  const dateObj = new Date(value);
-
-  if (isNaN(dateObj.getTime())) {
-    return null;
-  }
-
-  return dateObj;
-}
-
 function getNodeId(node: TraceTreeNode<TraceTree.NodeValue>): string | undefined {
   if (isTransactionNode(node)) {
     return node.value.event_id;
@@ -66,8 +53,10 @@ export function makeTraceContinuousProfilingLink(
   if (!transaction) {
     return null;
   }
-  let start: Date | null = toDate(transaction.space[0]);
-  let end: Date | null = toDate(transaction.space[0] + transaction.space[1]);
+  let start: Date | null = getDateFromTimestamp(transaction.space[0]);
+  let end: Date | null = getDateFromTimestamp(
+    transaction.space[0] + transaction.space[1]
+  );
 
   // End timestamp is required to generate a link
   if (end === null || typeof profilerId !== 'string' || profilerId === '') {

+ 14 - 4
static/app/views/performance/transactionSummary/transactionOverview/content.tsx

@@ -263,15 +263,25 @@ function SummaryContent({
   }
 
   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
+    project?.hasProfiles &&
+    (organization.features.includes('profiling') ||
+      organization.features.includes('continuous-profiling'))
   ) {
     transactionsListTitles.push(t('profile'));
-    fields.push({field: 'profile.id'});
+
+    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'});
+    }
   }
 
   // update search conditions

+ 42 - 8
static/app/views/performance/transactionSummary/utils.tsx

@@ -4,10 +4,14 @@ import type {Location, LocationDescriptor, Query} from 'history';
 
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
+import {getDateFromTimestamp} from 'sentry/utils/dates';
 import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
-import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
+import {
+  generateContinuousProfileFlamechartRouteWithQuery,
+  generateProfileFlamechartRoute,
+} from 'sentry/utils/profiling/routes';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
@@ -171,15 +175,45 @@ export function generateProfileLink() {
     tableRow: TableDataRow,
     _location: Location | undefined
   ) => {
+    const projectSlug = tableRow['project.name'];
+
     const profileId = tableRow['profile.id'];
-    if (!profileId) {
-      return {};
+    if (projectSlug && profileId) {
+      return generateProfileFlamechartRoute({
+        orgSlug: organization.slug,
+        projectSlug: String(tableRow['project.name']),
+        profileId: String(profileId),
+      });
     }
-    return generateProfileFlamechartRoute({
-      orgSlug: organization.slug,
-      projectSlug: String(tableRow['project.name']),
-      profileId: String(profileId),
-    });
+
+    const profilerId = tableRow['profiler.id'];
+    const threadId = tableRow['thread.id'];
+    const start =
+      typeof tableRow['precise.start_ts'] === 'number'
+        ? getDateFromTimestamp(tableRow['precise.start_ts'] * 1000)
+        : null;
+    const finish =
+      typeof tableRow['precise.finish_ts'] === 'number'
+        ? getDateFromTimestamp(tableRow['precise.finish_ts'] * 1000)
+        : null;
+    if (projectSlug && profilerId && threadId && start && finish) {
+      const query: Record<string, string> = {tid: String(threadId)};
+      if (tableRow.id && tableRow.trace) {
+        query.eventId = String(tableRow.id);
+        query.traceId = String(tableRow.trace);
+      }
+
+      return generateContinuousProfileFlamechartRouteWithQuery(
+        organization.slug,
+        String(projectSlug),
+        String(profilerId),
+        start.toISOString(),
+        finish.toISOString(),
+        query
+      );
+    }
+
+    return {};
   };
 }