Browse Source

feat(profiling): Transaction to profile button (#36087)

This introduces a simple profiling + performance integration to link a
transaction to the corresponding profile. This will only be shown when the
feature is enabled and for transactions where we can find a profile.
Tony Xiao 2 years ago
parent
commit
8b55fa55b4

+ 8 - 10
static/app/utils/analytics/profilingAnalyticsEvents.tsx

@@ -1,19 +1,17 @@
-type ProfilingPrefix = 'profiling';
-type ProfilingViews =
-  | 'landing'
-  | 'profile_summary'
-  | 'profile_details'
-  | 'profile_flamegraph';
-
-type EventKey = `${ProfilingPrefix}_views.${ProfilingViews}`;
-
 export type ProfilingEventParameters = {
-  [K in EventKey]: {};
+  'profiling_views.go_to_flamegraph': {source: string};
+  'profiling_views.landing': {};
+  'profiling_views.profile_details': {};
+  'profiling_views.profile_flamegraph': {};
+  'profiling_views.profile_summary': {};
 };
 
+type EventKey = keyof ProfilingEventParameters;
+
 export const profilingEventMap: Record<EventKey, string> = {
   'profiling_views.landing': 'Profiling Views: Landing',
   'profiling_views.profile_flamegraph': 'Profiling Views: Flamegraph',
   'profiling_views.profile_summary': 'Profiling Views: Profile Summary',
   'profiling_views.profile_details': 'Profiling Views: Profile Details',
+  'profiling_views.go_to_flamegraph': 'Profiling Views: Go to Flamegraph',
 };

+ 10 - 0
static/app/views/performance/transactionDetails/content.tsx

@@ -35,6 +35,7 @@ import {getSelectedProjectPlatforms} from '../utils';
 
 import EventMetas from './eventMetas';
 import FinishSetupAlert from './finishSetupAlert';
+import {TransactionToProfileButton} from './transactionToProfileButton';
 
 type Props = Pick<
   RouteComponentProps<{eventSlug: string}, {}>,
@@ -140,6 +141,8 @@ class EventDetailsContent extends AsyncComponent<Props, State> {
     const traceId = event.contexts?.trace?.trace_id ?? '';
     const {start, end} = getTraceTimeRangeFromEvent(event);
 
+    const hasProfilingFeature = organization.features.includes('profiling');
+
     return (
       <TraceMetaQuery
         location={location}
@@ -175,6 +178,13 @@ class EventDetailsContent extends AsyncComponent<Props, State> {
                           {t('JSON')} (<FileSize bytes={event.size} />)
                         </Button>
                       )}
+                      {hasProfilingFeature && (
+                        <TransactionToProfileButton
+                          orgId={organization.slug}
+                          projectId={this.projectId}
+                          transactionId={event.eventID}
+                        />
+                      )}
                     </ButtonBar>
                   </Layout.HeaderActions>
                 </Layout.Header>

+ 74 - 0
static/app/views/performance/transactionDetails/transactionToProfileButton.tsx

@@ -0,0 +1,74 @@
+import {useEffect, useState} from 'react';
+
+import {Client} from 'sentry/api';
+import Button from 'sentry/components/button';
+import {t} from 'sentry/locale';
+import {RequestState} from 'sentry/types/core';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {generateProfileFlamegraphRoute} from 'sentry/utils/profiling/routes';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+
+interface Props {
+  orgId: string;
+  projectId: string;
+  transactionId: string;
+}
+
+function TransactionToProfileButton({transactionId, orgId, projectId}: Props) {
+  const api = useApi();
+  const organization = useOrganization();
+
+  const [profileIdState, setProfileIdState] = useState<RequestState<string>>({
+    type: 'initial',
+  });
+
+  useEffect(() => {
+    fetchProfileId(api, transactionId, orgId, projectId).then((profileId: ProfileId) => {
+      setProfileIdState({type: 'resolved', data: profileId.profile_id});
+    });
+  }, [api, transactionId, orgId, projectId]);
+
+  if (profileIdState.type !== 'resolved') {
+    return null;
+  }
+
+  function handleGoToProfile() {
+    trackAdvancedAnalyticsEvent('profiling_views.go_to_flamegraph', {
+      organization,
+      source: 'transaction_details',
+    });
+  }
+
+  const target = generateProfileFlamegraphRoute({
+    orgSlug: orgId,
+    projectSlug: projectId,
+    profileId: profileIdState.data,
+  });
+
+  return (
+    <Button onClick={handleGoToProfile} to={target}>
+      {t('Go to Profile')}
+    </Button>
+  );
+}
+
+type ProfileId = {
+  profile_id: string;
+};
+
+function fetchProfileId(
+  api: Client,
+  transactionId: string,
+  orgId: string,
+  projectId: string
+): Promise<ProfileId> {
+  return api.requestPromise(
+    `/projects/${orgId}/${projectId}/profiling/transactions/${transactionId}/`,
+    {
+      method: 'GET',
+    }
+  );
+}
+
+export {TransactionToProfileButton};