Browse Source

feat(profiling): Add profile id to transaction summary page (#44378)

This adds the profile id to the transaction summary page to easily
navigate from a transaction to a profile.

Closes getsentry/team-profiling#122
Tony Xiao 2 years ago
parent
commit
9a28d10d92

+ 3 - 0
static/app/components/discover/transactionsList.tsx

@@ -112,6 +112,7 @@ type Props = {
    * The callback for when Open in Discover is clicked.
    */
   handleOpenInDiscoverClick?: (e: React.MouseEvent<Element>) => void;
+  referrer?: string;
   showTransactions?: TransactionFilterOptions;
   /**
    * A list of preferred table headers to use over the field names.
@@ -228,6 +229,7 @@ class _TransactionsList extends Component<Props> {
       titles,
       generateLink,
       forceLoading,
+      referrer,
     } = this.props;
 
     const eventView = this.getEventView();
@@ -256,6 +258,7 @@ class _TransactionsList extends Component<Props> {
             generateLink={generateLink}
             handleCellAction={handleCellAction}
             useAggregateAlias={false}
+            referrer={referrer}
           />
         </GuideAnchor>
       </Fragment>

+ 28 - 0
static/app/components/discover/transactionsTable.tsx

@@ -12,6 +12,7 @@ import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
 import {objectIsEmpty} from 'sentry/utils';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import EventView, {MetaType} from 'sentry/utils/discover/eventView';
 import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
@@ -46,6 +47,7 @@ type Props = {
   handleCellAction?: (
     c: TableColumn<React.ReactText>
   ) => (a: Actions, v: React.ReactText) => void;
+  referrer?: string;
   titles?: string[];
 };
 
@@ -125,6 +127,7 @@ class TransactionsTable extends PureComponent<Props> {
       handleCellAction,
       titles,
       useAggregateAlias,
+      referrer,
     } = this.props;
     const fields = eventView.getFields();
 
@@ -151,6 +154,16 @@ class TransactionsTable extends PureComponent<Props> {
               {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}>
@@ -237,6 +250,21 @@ class TransactionsTable extends PureComponent<Props> {
   }
 }
 
+function getProfileAnalyticsHandler(organization: Organization, referrer?: string) {
+  return () => {
+    let source;
+    if (referrer === 'performance.transactions_summary') {
+      source = 'performance.transactions_summary.overview';
+    } else {
+      source = 'discover.transactions_table';
+    }
+    trackAdvancedAnalyticsEvent('profiling_views.go_to_flamegraph', {
+      organization,
+      source,
+    });
+  };
+}
+
 const HeadCellContainer = styled('div')`
   padding: ${space(2)};
 `;

+ 6 - 13
static/app/components/events/interfaces/spans/gapSpanDetails.tsx

@@ -4,8 +4,6 @@ import styled from '@emotion/styled';
 import {Button} from 'sentry/components/button';
 import ExternalLink from 'sentry/components/links/externalLink';
 import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview';
-import {getDocsPlatformSDKForPlatform} from 'sentry/components/profiling/ProfilingOnboarding/util';
-import {PlatformKey} from 'sentry/data/platformCategories';
 import {IconProfiling} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {Organization} from 'sentry/types';
@@ -15,6 +13,7 @@ import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAna
 import {CanvasView} from 'sentry/utils/profiling/canvasView';
 import {Flamegraph as FlamegraphModel} from 'sentry/utils/profiling/flamegraph';
 import {Rect} from 'sentry/utils/profiling/gl/utils';
+import {getProfilingDocsForPlatform} from 'sentry/utils/profiling/platforms';
 import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
@@ -71,11 +70,11 @@ export function GapSpanDetails({
     ? span.timestamp - event.startTimestamp
     : flamegraph.configSpace.width;
 
-  const docsPlatform = getDocsPlatformSDKForPlatform(event.platform);
+  const docsLink = getProfilingDocsForPlatform(event.platform || '');
 
   if (
     // profiling isn't supported for this platform
-    !docsPlatform ||
+    !docsLink ||
     // the project already sent a profile but this transaction doesnt have a profile
     (projectHasProfile && !hasProfile)
   ) {
@@ -91,7 +90,7 @@ export function GapSpanDetails({
 
   return (
     <Container>
-      {!projectHasProfile && <SetupProfilingInstructions docsPlatform={docsPlatform} />}
+      {!projectHasProfile && <SetupProfilingInstructions docsLink={docsLink} />}
       {projectHasProfile && hasProfile && (
         <ProfilePreview
           canvasView={canvasView}
@@ -113,16 +112,10 @@ export function GapSpanDetails({
 }
 
 interface SetupProfilingInstructionsProps {
-  docsPlatform: PlatformKey;
+  docsLink: string;
 }
 
-function SetupProfilingInstructions({docsPlatform}: SetupProfilingInstructionsProps) {
-  // ios is the only supported apple platform right now
-  const docsLink =
-    docsPlatform === 'apple-ios'
-      ? 'https://docs.sentry.io/platforms/apple/guides/ios/profiling/'
-      : `https://docs.sentry.io/platforms/${docsPlatform}/profiling/`;
-
+function SetupProfilingInstructions({docsLink}: SetupProfilingInstructionsProps) {
   return (
     <InstructionsContainer>
       <Heading>{t('Requires Manual Instrumentation')}</Heading>

+ 5 - 48
static/app/components/profiling/ProfilingOnboarding/util.ts

@@ -1,54 +1,11 @@
 import partition from 'lodash/partition';
 
-import {PlatformKey, profiling} from 'sentry/data/platformCategories';
+import {PlatformKey} from 'sentry/data/platformCategories';
 import {Project} from 'sentry/types/project';
-
-export const supportedProfilingPlatforms = profiling;
-export const supportedProfilingPlatformSDKs = [
-  'android',
-  'apple-ios',
-  'node',
-  'python',
-  'rust',
-] as const;
-export type SupportedProfilingPlatform = (typeof supportedProfilingPlatforms)[number];
-export type SupportedProfilingPlatformSDK =
-  (typeof supportedProfilingPlatformSDKs)[number];
-
-export function getDocsPlatformSDKForPlatform(
-  platform
-): SupportedProfilingPlatform | null {
-  if (platform === 'android') {
-    return 'android';
-  }
-
-  if (platform === 'apple-ios') {
-    return 'apple-ios';
-  }
-
-  if (platform.startsWith('node')) {
-    return 'node';
-  }
-
-  if (platform.startsWith('python')) {
-    return 'python';
-  }
-
-  if (platform === 'rust') {
-    return 'rust';
-  }
-
-  return null;
-}
-
-export function isProfilingSupportedOrProjectHasProfiles(project: Project): boolean {
-  return !!(
-    (project.platform && getDocsPlatformSDKForPlatform(project.platform)) ||
-    // If this project somehow managed to send profiles, then profiling is supported for this project.
-    // Sometimes and for whatever reason, platform can also not be set on a project so the above check alone would fail
-    project.hasProfiles
-  );
-}
+import {
+  getDocsPlatformSDKForPlatform,
+  SupportedProfilingPlatformSDK,
+} from 'sentry/utils/profiling/platforms';
 
 export const profilingOnboardingDocKeys = [
   '0-alert',

+ 2 - 0
static/app/utils/analytics/profilingAnalyticsEvents.tsx

@@ -1,8 +1,10 @@
 import {PlatformKey} from 'sentry/data/platformCategories';
 
 type ProfilingEventSource =
+  | 'discover.transactions_table'
   | 'performance.missing_instrumentation'
   | 'performance.trace_view'
+  | 'performance.transactions_summary.overview'
   | 'slowest_transaction_panel'
   | 'transaction_details'
   | 'transaction_hovercard.trigger'

+ 70 - 0
static/app/utils/profiling/platforms.spec.tsx

@@ -0,0 +1,70 @@
+import {
+  getDocsPlatformSDKForPlatform,
+  getProfilingDocsForPlatform,
+} from 'sentry/utils/profiling/platforms';
+
+describe('getDocsPlatformSDKForPlatform', function () {
+  it.each([
+    ['android', 'android'],
+    ['apple-macos', null],
+    ['apple-ios', 'apple-ios'],
+    ['python', 'python'],
+    ['python-django', 'python'],
+    ['python-flask', 'python'],
+    ['python-fastapi', 'python'],
+    ['python-starlette', 'python'],
+    ['python-sanic', 'python'],
+    ['python-celery', 'python'],
+    ['python-bottle', 'python'],
+    ['python-pylons', 'python'],
+    ['python-pyramid', 'python'],
+    ['python-tornado', 'python'],
+    ['python-rq', 'python'],
+    ['python-awslambda', 'python'],
+    ['python-azurefunctions', 'python'],
+    ['python-gcpfunctions', 'python'],
+    ['node', 'node'],
+    ['node-express', 'node'],
+    ['node-koa', 'node'],
+    ['node-connect', 'node'],
+    ['node-awslambda', 'node'],
+    ['node-azurefunctions', 'node'],
+    ['node-gcpfunctions', 'node'],
+    ['rust', 'rust'],
+  ])('gets docs platform for %s', function (platform, docsPlatform) {
+    expect(getDocsPlatformSDKForPlatform(platform)).toEqual(docsPlatform);
+  });
+});
+
+describe('getProfilingDocsForPlatform', function () {
+  it.each([
+    ['android', 'https://docs.sentry.io/platforms/android/profiling/'],
+    ['apple-macos', null],
+    ['apple-ios', 'https://docs.sentry.io/platforms/apple/guides/ios/profiling/'],
+    ['python', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-django', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-flask', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-fastapi', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-starlette', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-sanic', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-celery', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-bottle', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-pylons', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-pyramid', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-tornado', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-rq', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-awslambda', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-azurefunctions', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['python-gcpfunctions', 'https://docs.sentry.io/platforms/python/profiling/'],
+    ['node', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-express', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-koa', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-connect', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-awslambda', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-azurefunctions', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['node-gcpfunctions', 'https://docs.sentry.io/platforms/node/profiling/'],
+    ['rust', 'https://docs.sentry.io/platforms/rust/profiling/'],
+  ])('gets profiling docs for %s', function (platform, docs) {
+    expect(getProfilingDocsForPlatform(platform)).toEqual(docs);
+  });
+});

+ 59 - 0
static/app/utils/profiling/platforms.tsx

@@ -0,0 +1,59 @@
+import {profiling} from 'sentry/data/platformCategories';
+import {Project} from 'sentry/types/project';
+
+export const supportedProfilingPlatforms = profiling;
+export const supportedProfilingPlatformSDKs = [
+  'android',
+  'apple-ios',
+  'node',
+  'python',
+  'rust',
+] as const;
+export type SupportedProfilingPlatform = (typeof supportedProfilingPlatforms)[number];
+export type SupportedProfilingPlatformSDK =
+  (typeof supportedProfilingPlatformSDKs)[number];
+
+export function getDocsPlatformSDKForPlatform(
+  platform
+): SupportedProfilingPlatform | null {
+  if (platform === 'android') {
+    return 'android';
+  }
+
+  if (platform === 'apple-ios') {
+    return 'apple-ios';
+  }
+
+  if (platform.startsWith('node')) {
+    return 'node';
+  }
+
+  if (platform.startsWith('python')) {
+    return 'python';
+  }
+
+  if (platform === 'rust') {
+    return 'rust';
+  }
+
+  return null;
+}
+
+export function isProfilingSupportedOrProjectHasProfiles(project: Project): boolean {
+  return !!(
+    (project.platform && getDocsPlatformSDKForPlatform(project.platform)) ||
+    // If this project somehow managed to send profiles, then profiling is supported for this project.
+    // Sometimes and for whatever reason, platform can also not be set on a project so the above check alone would fail
+    project.hasProfiles
+  );
+}
+
+export function getProfilingDocsForPlatform(platform: string): string | null {
+  const docsPlatform = getDocsPlatformSDKForPlatform(platform);
+  if (!docsPlatform) {
+    return null;
+  }
+  return docsPlatform === 'apple-ios'
+    ? 'https://docs.sentry.io/platforms/apple/guides/ios/profiling/'
+    : `https://docs.sentry.io/platforms/${docsPlatform}/profiling/`;
+}

+ 1 - 1
static/app/views/performance/transactionSummary/header.tsx

@@ -8,7 +8,6 @@ import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton';
 import FeatureBadge from 'sentry/components/featureBadge';
 import IdBadge from 'sentry/components/idBadge';
 import * as Layout from 'sentry/components/layouts/thirds';
-import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/components/profiling/ProfilingOnboarding/util';
 import ReplayCountBadge from 'sentry/components/replays/replayCountBadge';
 import ReplaysFeatureBadge from 'sentry/components/replays/replaysFeatureBadge';
 import useReplaysCount from 'sentry/components/replays/useReplaysCount';
@@ -19,6 +18,7 @@ import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAna
 import EventView from 'sentry/utils/discover/eventView';
 import {MetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
 import HasMeasurementsQuery from 'sentry/utils/performance/vitals/hasMeasurementsQuery';
+import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/utils/profiling/platforms';
 import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
 

+ 16 - 3
static/app/views/performance/transactionSummary/transactionOverview/content.tsx

@@ -29,6 +29,7 @@ import {
   SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
 } from 'sentry/utils/discover/fields';
 import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
+import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/utils/profiling/platforms';
 import {decodeScalar} from 'sentry/utils/queryString';
 import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
 import {useRoutes} from 'sentry/utils/useRoutes';
@@ -53,6 +54,7 @@ import Filter, {
   SpanOperationBreakdownFilter,
 } from '../filter';
 import {
+  generateProfileLink,
   generateReplayLink,
   generateTraceLink,
   generateTransactionLink,
@@ -244,14 +246,25 @@ function SummaryContent({
 
   const project = projects.find(p => p.id === projectId);
 
+  let transactionsListEventView = eventView.clone();
+  const fields = [...transactionsListEventView.fields];
+
   if (
     organization.features.includes('session-replay-ui') &&
     projectSupportsReplay(project)
   ) {
     transactionsListTitles.push(t('replay'));
+    fields.push({field: 'replayId'});
   }
 
-  let transactionsListEventView = eventView.clone();
+  if (
+    organization.features.includes('profiling') &&
+    project &&
+    isProfilingSupportedOrProjectHasProfiles(project)
+  ) {
+    transactionsListTitles.push(t('profile'));
+    fields.push({field: 'profile.id'});
+  }
 
   // update search conditions
 
@@ -284,8 +297,6 @@ function SummaryContent({
     durationField = filterToField(spanOperationBreakdownFilter)!;
   }
 
-  const fields = [...transactionsListEventView.fields];
-
   // add ops breakdown duration column as the 3rd column
   fields.splice(2, 0, {field: durationField});
 
@@ -362,6 +373,7 @@ function SummaryContent({
             id: generateTransactionLink(transactionName),
             trace: generateTraceLink(eventView.normalizeDateSelection(location)),
             replayId: generateReplayLink(routes),
+            'profile.id': generateProfileLink(),
           }}
           handleCellAction={handleCellAction}
           {...getTransactionsListSort(location, {
@@ -369,6 +381,7 @@ function SummaryContent({
             spanOperationBreakdownFilter,
           })}
           forceLoading={isLoading}
+          referrer="performance.transactions_summary"
         />
         <SuspectSpans
           location={location}

+ 0 - 5
static/app/views/performance/transactionSummary/transactionOverview/index.tsx

@@ -191,7 +191,6 @@ function getDocumentTitle(transactionName: string): string {
 
 function generateEventView({
   location,
-  organization,
   transactionName,
 }: {
   location: Location;
@@ -214,10 +213,6 @@ function generateEventView({
 
   const fields = ['id', 'user.display', 'transaction.duration', 'trace', 'timestamp'];
 
-  if (organization.features.includes('session-replay-ui')) {
-    fields.push('replayId');
-  }
-
   return EventView.fromNewQueryWithLocation(
     {
       id: undefined,

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