Browse Source

feat(new-trace): Creating sample trace on view sample transaction click. (#69247)

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdkhan14 10 months ago
parent
commit
1a23c80c79

+ 1 - 1
static/app/utils/discover/urls.tsx

@@ -75,7 +75,7 @@ export function generateLinkToEventInTraceView({
       organization,
       String(traceSlug),
       dateSelection,
-      {},
+      location.query,
       normalizedTimestamp,
       eventId,
       spanId

+ 105 - 7
static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx

@@ -4,7 +4,7 @@ import * as qs from 'query-string';
 
 import type {Client} from 'sentry/api';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
-import type {PageFilters} from 'sentry/types/core';
+import type {EventTransaction, PageFilters} from 'sentry/types';
 import type {
   TraceFullDetailed,
   TraceSplitResults,
@@ -40,6 +40,7 @@ export function getTraceQueryParams(
   limit: number;
   timestamp: string | undefined;
   useSpans: number;
+  demo?: string | undefined;
   pageEnd?: string | undefined;
   pageStart?: string | undefined;
   statsPeriod?: string | undefined;
@@ -48,6 +49,7 @@ export function getTraceQueryParams(
     allowAbsolutePageDatetime: true,
   });
   const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
+  const demo = decodeScalar(normalizedParams.demo);
   const timestamp = decodeScalar(normalizedParams.timestamp);
   let decodedLimit: string | number | undefined =
     options.limit ?? decodeScalar(normalizedParams.limit);
@@ -78,7 +80,14 @@ export function getTraceQueryParams(
     delete otherParams.statsPeriod;
   }
 
-  const queryParams = {...otherParams, limit, timestamp, eventId, useSpans: 1};
+  const queryParams = {
+    ...otherParams,
+    demo,
+    limit,
+    timestamp,
+    eventId,
+    useSpans: 1,
+  };
   for (const key in queryParams) {
     if (
       queryParams[key] === '' ||
@@ -92,6 +101,93 @@ export function getTraceQueryParams(
   return queryParams;
 }
 
+function parseDemoEventSlug(
+  demoEventSlug: string | undefined
+): {event_id: string; project_slug: string} | null {
+  if (!demoEventSlug) {
+    return null;
+  }
+
+  const [project_slug, event_id] = demoEventSlug.split(':');
+  return {project_slug, event_id};
+}
+
+function makeTraceFromTransaction(
+  event: EventTransaction | undefined
+): TraceSplitResults<TraceFullDetailed> | undefined {
+  if (!event) {
+    return undefined;
+  }
+
+  const traceContext = event.contexts?.trace;
+
+  const transaction = {
+    event_id: event.eventID,
+    generation: 0,
+    parent_event_id: '',
+    parent_span_id: traceContext?.parent_span_id ?? '',
+    performance_issues: [],
+    project_id: Number(event.projectID),
+    project_slug: event.projectSlug ?? '',
+    span_id: traceContext?.span_id ?? '',
+    timestamp: event.endTimestamp,
+    transaction: event.title,
+    'transaction.duration': (event.endTimestamp - event.startTimestamp) * 1000,
+    errors: [],
+    children: [],
+    start_timestamp: event.startTimestamp,
+    'transaction.op': traceContext?.op ?? '',
+    'transaction.status': traceContext?.status ?? '',
+    measurements: event.measurements ?? {},
+    tags: [],
+  };
+
+  return {transactions: [transaction], orphan_errors: []};
+}
+
+function useDemoTrace(
+  demo: string | undefined,
+  organization: {slug: string}
+): UseApiQueryResult<TraceSplitResults<TraceFullDetailed> | undefined, any> {
+  const demoEventSlug = parseDemoEventSlug(demo);
+
+  // When projects don't have performance set up, we allow them to view a demo transaction.
+  // The backend creates the demo transaction, however the trace is created async, so when the
+  // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
+  // When this happens, we assemble a fake trace response to only include the transaction that had already been
+  // created and stored already so that the users can visualize in the context of a trace.
+  const demoEventQuery = useApiQuery<EventTransaction>(
+    [
+      `/organizations/${organization.slug}/events/${demoEventSlug?.project_slug}:${demoEventSlug?.event_id}/`,
+      {
+        query: {
+          referrer: 'trace-view',
+        },
+      },
+    ],
+    {
+      staleTime: Infinity,
+      enabled: !!demoEventSlug,
+    }
+  );
+
+  // Without the useMemo, the demoTraceQueryResults will be re-created on every render,
+  // causing the trace view to re-render as we interact with it.
+  const demoTraceQueryResults = useMemo(() => {
+    return {
+      ...demoEventQuery,
+      data: makeTraceFromTransaction(demoEventQuery.data),
+    };
+  }, [demoEventQuery]);
+
+  // Casting here since the 'select' option is not available in the useApiQuery hook to transform the data
+  // from EventTransaction to TraceSplitResults<TraceFullDetailed>
+  return demoTraceQueryResults as UseApiQueryResult<
+    TraceSplitResults<TraceFullDetailed> | undefined,
+    any
+  >;
+}
+
 type UseTraceParams = {
   limit?: number;
 };
@@ -99,25 +195,27 @@ type UseTraceParams = {
 const DEFAULT_OPTIONS = {};
 export function useTrace(
   options: Partial<UseTraceParams> = DEFAULT_OPTIONS
-): UseApiQueryResult<TraceSplitResults<TraceFullDetailed>, any> {
+): UseApiQueryResult<TraceSplitResults<TraceFullDetailed> | undefined, any> {
   const filters = usePageFilters();
   const organization = useOrganization();
   const params = useParams<{traceSlug?: string}>();
-
   const queryParams = useMemo(() => {
     const query = qs.parse(location.search);
     return getTraceQueryParams(query, filters.selection, options);
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [options]);
-
-  return useApiQuery(
+  const mode = queryParams.demo ? 'demo' : undefined;
+  const demoTrace = useDemoTrace(queryParams.demo, organization);
+  const traceQuery = useApiQuery<TraceSplitResults<TraceFullDetailed>>(
     [
       `/organizations/${organization.slug}/events-trace/${params.traceSlug ?? ''}/`,
       {query: queryParams},
     ],
     {
       staleTime: Infinity,
-      enabled: !!params.traceSlug && !!organization.slug,
+      enabled: !!params.traceSlug && !!organization.slug && mode !== 'demo',
     }
   );
+
+  return mode === 'demo' ? demoTrace : traceQuery;
 }

+ 52 - 3
static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx

@@ -17,9 +17,13 @@ function getMetaQueryParams(
   filters: Partial<PageFilters> = {}
 ):
   | {
+      // demo has the format ${projectSlug}:${eventId}
+      // used to query a demo transaction event from the backend.
+      demo: string | undefined;
       statsPeriod: string;
     }
   | {
+      demo: string | undefined;
       timestamp: string;
     } {
   const normalizedParams = normalizeDateTimeParams(query, {
@@ -30,11 +34,12 @@ function getMetaQueryParams(
   const timestamp = decodeScalar(normalizedParams.timestamp);
 
   if (timestamp) {
-    return {timestamp};
+    return {timestamp, demo: decodeScalar(normalizedParams.demo)};
   }
 
   return {
     statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
+    demo: decodeScalar(normalizedParams.demo),
   };
 }
 
@@ -51,16 +56,60 @@ export function useTraceMeta(
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
+  const mode = queryParams.demo ? 'demo' : undefined;
   const trace = traceSlug ?? params.traceSlug;
 
-  return useApiQuery(
+  const traceMetaQueryResults = useApiQuery<TraceMeta>(
     [
       `/organizations/${organization.slug}/events-trace-meta/${trace ?? ''}/`,
       {query: queryParams},
     ],
     {
       staleTime: Infinity,
-      enabled: !!trace && !!organization.slug,
+      enabled: !!trace && !!organization.slug && mode !== 'demo',
     }
   );
+
+  // When projects don't have performance set up, we allow them to view a sample transaction.
+  // The backend creates the sample transaction, however the trace is created async, so when the
+  // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
+  // When this happens, we assemble a fake trace response to only include the transaction that had already been
+  // created and stored already so that the users can visualize in the context of a trace.
+  // The trace meta query has to reflect this by returning a single transaction and project.
+  if (mode === 'demo') {
+    return {
+      data: {
+        errors: 0,
+        performance_issues: 0,
+        projects: 1,
+        transactions: 1,
+      },
+      failureCount: 0,
+      errorUpdateCount: 0,
+      failureReason: null,
+      error: null,
+      isError: false,
+      isFetched: true,
+      isFetchedAfterMount: true,
+      isFetching: false,
+      isLoading: false,
+      isLoadingError: false,
+      isInitialLoading: false,
+      isPaused: false,
+      isPlaceholderData: false,
+      isPreviousData: false,
+      isRefetchError: false,
+      isRefetching: false,
+      isStale: false,
+      isSuccess: true,
+      status: 'success',
+      fetchStatus: 'idle',
+      dataUpdatedAt: Date.now(),
+      errorUpdatedAt: Date.now(),
+      refetch: traceMetaQueryResults.refetch,
+      remove: traceMetaQueryResults.remove,
+    };
+  }
+
+  return traceMetaQueryResults;
 }

+ 17 - 4
static/app/views/performance/onboarding.tsx

@@ -30,10 +30,10 @@ import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
 import type {Organization} from 'sentry/types/organization';
 import type {Project} from 'sentry/types/project';
 import {trackAnalytics} from 'sentry/utils/analytics';
+import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
 import useApi from 'sentry/utils/useApi';
 import {useLocation} from 'sentry/utils/useLocation';
 import useProjects from 'sentry/utils/useProjects';
-import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 
 const performanceSetupUrl =
   'https://docs.sentry.io/performance-monitoring/getting-started/';
@@ -189,10 +189,23 @@ function Onboarding({organization, project}: Props) {
             const url = `/projects/${organization.slug}/${project.slug}/create-sample-transaction/`;
             try {
               const eventData = await api.requestPromise(url, {method: 'POST'});
+              const traceSlug = eventData.contexts?.trace?.trace_id ?? '';
+
               browserHistory.push(
-                normalizeUrl(
-                  `/organizations/${organization.slug}/performance/${project.slug}:${eventData.eventID}/`
-                )
+                generateLinkToEventInTraceView({
+                  eventId: eventData.eventID,
+                  projectSlug: project.slug,
+                  organization,
+                  location: {
+                    ...location,
+                    query: {
+                      ...location.query,
+                      demo: `${project.slug}:${eventData.eventID}`,
+                    },
+                  },
+                  timestamp: eventData.endTimestamp,
+                  traceSlug,
+                })
               );
               clearIndicators();
             } catch (error) {