Browse Source

feat(trace) add continuous profile link (#73948)

Add a temporary link to the UI so that we can link to continuous
profiles. This is not the final place for the button, but it's placed in
a convenient top level so that we can quickly move back and forth

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Jonas 8 months ago
parent
commit
a8661b991f

+ 1 - 2
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/ancestry.tsx

@@ -22,7 +22,6 @@ import {assert} from 'sentry/types/utils';
 import {defined} from 'sentry/utils';
 import EventView from 'sentry/utils/discover/eventView';
 import {generateEventSlug} from 'sentry/utils/discover/urls';
-import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
 import type {
   TraceTree,
   TraceTreeNode,
@@ -44,7 +43,7 @@ function SpanChild({
   organization,
   location,
 }: {
-  childTransaction: TraceTreeNode<TraceFullDetailed>;
+  childTransaction: TraceTreeNode<TraceTree.Transaction>;
   location: Location;
   organization: Organization;
 }) {

+ 23 - 1
static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx

@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
 import type {LocationDescriptor} from 'history';
 import * as qs from 'query-string';
 
-import {Button} from 'sentry/components/button';
+import {Button, LinkButton} from 'sentry/components/button';
 import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
 import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
 import Tags from 'sentry/components/events/eventTagsAndScreenshot/tags';
@@ -41,6 +41,7 @@ import {
   isTransactionNode,
 } from 'sentry/views/performance/newTraceDetails/guards';
 import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
+import {makeTraceContinuousProfilingLink} from 'sentry/views/performance/newTraceDetails/traceDrawer/traceProfilingLink';
 import type {
   MissingInstrumentationNode,
   NoDataNode,
@@ -375,6 +376,7 @@ function NodeActions(props: {
   organization: Organization;
   eventSize?: number | undefined;
 }) {
+  const organization = useOrganization();
   const items = useMemo(() => {
     const showInView: MenuItemProps = {
       key: 'show-in-view',
@@ -431,9 +433,29 @@ function NodeActions(props: {
     return [showInView];
   }, [props]);
 
+  const profilerId = useMemo(() => {
+    if (isTransactionNode(props.node)) {
+      return props.node.value.profiler_id;
+    }
+    if (isSpanNode(props.node)) {
+      return props.node.value.sentry_tags?.profiler_id ?? '';
+    }
+    return '';
+  }, [props]);
+
+  const profileLink = makeTraceContinuousProfilingLink(props.node, profilerId, {
+    orgSlug: props.organization.slug,
+    projectSlug: props.node.value.project_slug,
+  });
+
   return (
     <ActionsContainer>
       <Actions className="Actions">
+        {organization.features.includes('continuous-profiling-ui') && !!profileLink ? (
+          <LinkButton size="xs" to={profileLink}>
+            {t('Continuous Profile')}
+          </LinkButton>
+        ) : null}
         <Button
           size="xs"
           onClick={_e => {

+ 8 - 22
static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.spec.tsx

@@ -1,5 +1,4 @@
 import type {LocationDescriptor} from 'history';
-import {TransactionEventFixture} from 'sentry-fixture/event';
 
 import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 import {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
@@ -17,6 +16,7 @@ function makeTransaction(
     transaction: 'transaction',
     'transaction.op': '',
     'transaction.status': '',
+    profiler_id: '',
     performance_issues: [],
     errors: [],
     ...overrides,
@@ -31,33 +31,27 @@ describe('traceProfilingLink', () => {
     });
 
     it('requires projectSlug', () => {
-      const event = TransactionEventFixture();
+      const event = makeTransaction();
       expect(
-        makeTraceContinuousProfilingLink(node, event, {
+        makeTraceContinuousProfilingLink(node, event.profiler_id, {
           projectSlug: 'project',
           orgSlug: '',
         })
       ).toBeNull();
     });
     it('requires orgSlug', () => {
-      const event = TransactionEventFixture();
+      const event = makeTransaction();
       expect(
-        makeTraceContinuousProfilingLink(node, event, {
+        makeTraceContinuousProfilingLink(node, event.profiler_id, {
           projectSlug: '',
           orgSlug: 'sentry',
         })
       ).toBeNull();
     });
     it('requires profilerId', () => {
-      const event = TransactionEventFixture({
-        contexts: {
-          profile: {
-            profiler_id: undefined,
-          },
-        },
-      });
       expect(
-        makeTraceContinuousProfilingLink(node, event, {
+        // @ts-expect-error missing profiler_id
+        makeTraceContinuousProfilingLink(node, undefined, {
           projectSlug: 'project',
           orgSlug: 'sentry',
         })
@@ -66,14 +60,6 @@ describe('traceProfilingLink', () => {
   });
 
   it('creates a window of time around end timestamp', () => {
-    const event = TransactionEventFixture({
-      contexts: {
-        profile: {
-          profiler_id: 'profiler',
-        },
-      },
-    });
-
     const timestamp = new Date().getTime();
 
     const node = new TraceTreeNode(
@@ -87,7 +73,7 @@ describe('traceProfilingLink', () => {
 
     const link: LocationDescriptor | null = makeTraceContinuousProfilingLink(
       node,
-      event,
+      'profiler',
       {
         projectSlug: 'project',
         orgSlug: 'sentry',

+ 2 - 4
static/app/views/performance/newTraceDetails/traceDrawer/traceProfilingLink.ts

@@ -1,6 +1,5 @@
 import type {Location, LocationDescriptor} from 'history';
 
-import type {EventTransaction} from 'sentry/types';
 import {generateContinuousProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
 import type {
   TraceTree,
@@ -26,7 +25,7 @@ function toDate(value: unknown): Date | null {
  */
 export function makeTraceContinuousProfilingLink(
   value: TraceTreeNode<TraceTree.NodeValue>,
-  transaction: EventTransaction,
+  profilerId: string,
   options: {
     orgSlug: string;
     projectSlug: string;
@@ -39,10 +38,9 @@ export function makeTraceContinuousProfilingLink(
 
   let start: Date | null = toDate(value.space[0]);
   let end: Date | null = toDate(value.space[0] + value.space[1]);
-  const profilerId: string | null = transaction.contexts?.profile?.profiler_id ?? null;
 
   // End timestamp is required to generate a link
-  if (end === null || profilerId === null) {
+  if (end === null || typeof profilerId !== 'string' || profilerId === '') {
     return null;
   }
 

+ 3 - 1
static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx

@@ -120,6 +120,7 @@ type ArgumentTypes<F> = F extends (...args: infer A) => any ? A : never;
 
 export declare namespace TraceTree {
   interface Transaction extends TraceFullDetailed {
+    profiler_id: string;
     sdk_name: string;
   }
   interface Span extends RawSpanType {
@@ -430,7 +431,7 @@ function fetchSingleTrace(
     query: string;
     traceId: string;
   }
-): Promise<TraceSplitResults<TraceFullDetailed>> {
+): Promise<TraceSplitResults<TraceTree.Transaction>> {
   return api.requestPromise(
     `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
   );
@@ -2630,6 +2631,7 @@ function partialTransaction(
     parent_event_id: '',
     project_id: 0,
     sdk_name: '',
+    profiler_id: '',
     'transaction.duration': 0,
     'transaction.op': 'loading-transaction',
     'transaction.status': 'loading-status',

+ 2 - 1
static/app/views/performance/traceDetails/content.tsx

@@ -40,6 +40,7 @@ import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
 import useDismissAlert from 'sentry/utils/useDismissAlert';
 import useProjects from 'sentry/utils/useProjects';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 import {MetaData} from 'sentry/views/performance/transactionDetails/styles';
 
 import {TraceDetailHeader, TraceSearchBar, TraceSearchContainer} from './styles';
@@ -61,7 +62,7 @@ type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'loca
   organization: Organization;
   traceEventView: EventView;
   traceSlug: string;
-  traces: TraceFullDetailed[] | null;
+  traces: TraceTree.Transaction[] | null;
   handleLimitChange?: (newLimit: number) => void;
   orphanErrors?: TraceError[];
 };

+ 5 - 5
static/app/views/performance/traceDetails/index.tsx

@@ -14,13 +14,13 @@ import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
 import {TraceFullDetailedQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import TraceMetaQuery from 'sentry/utils/performance/quickTrace/traceMetaQuery';
 import type {
-  TraceFullDetailed,
   TraceMeta,
   TraceSplitResults,
 } from 'sentry/utils/performance/quickTrace/types';
 import {decodeScalar} from 'sentry/utils/queryString';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 
 import {TraceView as TraceViewV1} from './../newTraceDetails';
 import TraceDetailsContent from './content';
@@ -108,9 +108,9 @@ class TraceSummary extends Component<Props> {
       error: QueryError | null;
       isLoading: boolean;
       meta: TraceMeta | null;
-      traces: (TraceFullDetailed[] | TraceSplitResults<TraceFullDetailed>) | null;
+      traces: (TraceTree.Transaction[] | TraceSplitResults<TraceTree.Transaction>) | null;
     }) => {
-      const {transactions, orphanErrors} = getTraceSplitResults<TraceFullDetailed>(
+      const {transactions, orphanErrors} = getTraceSplitResults<TraceTree.Transaction>(
         traces ?? [],
         organization
       );
@@ -125,7 +125,7 @@ class TraceSummary extends Component<Props> {
         isLoading,
         error,
         orphanErrors,
-        traces: transactions ?? (traces as TraceFullDetailed[]),
+        traces: transactions ?? (traces as TraceTree.Transaction[]),
         meta,
         handleLimitChange: this.handleLimitChange,
       };
@@ -170,7 +170,7 @@ class TraceSummary extends Component<Props> {
               content({
                 isLoading: traceResults.isLoading || metaResults.isLoading,
                 error: traceResults.error || metaResults.error,
-                traces: traceResults.traces,
+                traces: traceResults.traces as unknown as TraceTree.Transaction[],
                 meta: metaResults.meta,
               })
             }

+ 2 - 1
static/app/views/performance/traceDetails/newTraceDetailsContent.tsx

@@ -38,6 +38,7 @@ import useRouter from 'sentry/utils/useRouter';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import Tags from 'sentry/views/discover/tags';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 import {MetaData} from 'sentry/views/performance/transactionDetails/styles';
 
 import {BrowserDisplay} from '../transactionDetails/eventMetas';
@@ -55,7 +56,7 @@ type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'loca
   organization: Organization;
   traceEventView: EventView;
   traceSlug: string;
-  traces: TraceFullDetailed[] | null;
+  traces: TraceTree.Transaction[] | null;
   handleLimitChange?: (newLimit: number) => void;
   orphanErrors?: TraceError[];
 };

+ 4 - 3
static/app/views/performance/traceDetails/traceView.tsx

@@ -29,6 +29,7 @@ import type {
   TraceFullDetailed,
   TraceMeta,
 } from 'sentry/utils/performance/quickTrace/types';
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 import {
   TraceDetailBody,
   TraceViewContainer,
@@ -51,12 +52,12 @@ type AccType = {
   renderedChildren: React.ReactNode[];
 };
 
-type Props = Pick<RouteComponentProps<{}, {}>, 'location'> & {
+type TraceViewProps = Pick<RouteComponentProps<{}, {}>, 'location'> & {
   meta: TraceMeta | null;
   organization: Organization;
   traceEventView: EventView;
   traceSlug: string;
-  traces: TraceFullDetailed[];
+  traces: TraceTree.Transaction[];
   filteredEventIds?: Set<string>;
   handleLimitChange?: (newLimit: number) => void;
   orphanErrors?: TraceError[];
@@ -142,7 +143,7 @@ export default function TraceView({
   orphanErrors,
   handleLimitChange,
   ...props
-}: Props) {
+}: TraceViewProps) {
   const sentrySpan = Sentry.startInactiveSpan({
     op: 'trace.render',
     name: 'trace-view-content',

+ 4 - 6
static/app/views/replays/detail/trace/trace.tsx

@@ -7,10 +7,7 @@ import {IconSad} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import type {Organization} from 'sentry/types/organization';
 import type EventView from 'sentry/utils/discover/eventView';
-import type {
-  TraceError,
-  TraceFullDetailed,
-} from 'sentry/utils/performance/quickTrace/types';
+import type {TraceError} from 'sentry/utils/performance/quickTrace/types';
 import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -19,6 +16,7 @@ import {TraceViewWaterfall} from 'sentry/views/performance/newTraceDetails';
 import {useReplayTraceMeta} from 'sentry/views/performance/newTraceDetails/traceApi/useReplayTraceMeta';
 import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
 import {useTraceRootEvent} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent';
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
 import type {TracePreferencesState} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
 import {loadTraceViewPreferences} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
 import {TraceStateProvider} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
@@ -59,7 +57,7 @@ function TraceFound({
   eventView: EventView | null;
   organization: Organization;
   performanceActive: boolean;
-  traces: TraceFullDetailed[] | null;
+  traces: TraceTree.Transaction[] | null;
   orphanErrors?: TraceError[];
 }) {
   const location = useLocation();
@@ -143,7 +141,7 @@ function Trace({replayRecord}: {replayRecord: undefined | ReplayRecord}) {
       performanceActive={performanceActive}
       organization={organization}
       eventView={eventView}
-      traces={traces ?? []}
+      traces={(traces as TraceTree.Transaction[]) ?? []}
       orphanErrors={orphanErrors}
     />
   );