Browse Source

feat(analytics): augement analytics for trace explorer/viewer (#72790)

Adds requested analytic parameters to trace explorer/viewer, including:
- [x] num_traces and project_platforms for
`trace_explorer.search_success`
- [x] additional trace metadata for (`trace.metadata`, previously
`trace.shape`)
- [x] view similar spans analytic event
- [x] span clicks metadata in trace viewer
Kevin Liu 8 months ago
parent
commit
109c1dc811

+ 20 - 2
static/app/utils/analytics/tracingEventMap.tsx

@@ -1,6 +1,8 @@
 export type TracingEventParameters = {
-  'trace.shape': {
+  'trace.metadata': {
+    num_root_children: number;
     shape: string;
+    trace_duration_seconds: number;
   };
   'trace.trace_layout.change': {
     layout: string;
@@ -14,6 +16,10 @@ export type TracingEventParameters = {
     interaction: string;
   };
   'trace.trace_layout.show_in_view': {};
+  'trace.trace_layout.span_row_click': {
+    num_children: number;
+    project_platform: string;
+  };
   'trace.trace_layout.tab_pin': {};
   'trace.trace_layout.tab_view': {
     tab: string;
@@ -23,6 +29,13 @@ export type TracingEventParameters = {
     module: string;
   };
   'trace.trace_layout.view_shortcuts': {};
+  'trace.trace_layout.view_similar_spans': {
+    module: string;
+    source: string;
+  };
+  'trace.trace_layout.view_span_summary': {
+    module: string;
+  };
   'trace.trace_layout.zoom_to_fill': {};
   'trace.trace_warning_type': {
     type: string;
@@ -41,6 +54,8 @@ export type TracingEventParameters = {
   };
   'trace_explorer.search_success': {
     has_data: boolean;
+    num_traces: number;
+    project_platforms: string[];
     queries: string[];
   };
   'trace_explorer.toggle_trace_details': {
@@ -51,7 +66,7 @@ export type TracingEventParameters = {
 export type TracingEventKey = keyof TracingEventParameters;
 
 export const tracingEventMap: Record<TracingEventKey, string | null> = {
-  'trace.shape': 'Trace Shape',
+  'trace.metadata': 'Trace Load Metadata',
   'trace.trace_layout.change': 'Changed Trace Layout',
   'trace.trace_layout.drawer_minimize': 'Minimized Trace Drawer',
   'trace.trace_layout.show_in_view': 'Clicked Show in View Action',
@@ -66,6 +81,9 @@ export const tracingEventMap: Record<TracingEventKey, string | null> = {
   'trace.trace_layout.search_clear': 'Clear Trace Search',
   'trace.trace_layout.view_in_insight_module': 'View Trace Span in Insight Module',
   'trace.trace_layout.search_match_navigate': 'Navigate Trace Search Matches',
+  'trace.trace_layout.view_similar_spans': 'View Similar Spans in Trace',
+  'trace.trace_layout.view_span_summary': 'View Span Summary in Trace',
+  'trace.trace_layout.span_row_click': 'Clicked Span Row in Trace',
   'trace_explorer.add_span_condition': 'Trace Explorer: Add Span Condition',
   'trace_explorer.open_in_issues': 'Trace Explorer: Open Trace in Issues',
   'trace_explorer.open_trace': 'Trace Explorer: Open Trace in Trace Viewer',

+ 13 - 6
static/app/views/performance/newTraceDetails/index.tsx

@@ -91,8 +91,8 @@ function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null {
   return null;
 }
 
-function logTraceType(type: TraceType, organization: Organization) {
-  switch (type) {
+function logTraceMetadata(tree: TraceTree, organization: Organization) {
+  switch (tree.shape) {
     case TraceType.BROKEN_SUBTRACES:
     case TraceType.EMPTY_TRACE:
     case TraceType.MULTIPLE_ROOTS:
@@ -100,7 +100,7 @@ function logTraceType(type: TraceType, organization: Organization) {
     case TraceType.NO_ROOT:
     case TraceType.ONLY_ERRORS:
     case TraceType.BROWSER_MULTIPLE_ROOTS:
-      traceAnalytics.trackTraceShape(type, organization);
+      traceAnalytics.trackTraceMetadata(tree, organization);
       break;
     default: {
       Sentry.captureMessage('Unknown trace type');
@@ -521,6 +521,13 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       event: React.MouseEvent<HTMLElement>,
       index: number
     ) => {
+      trackAnalytics('trace.trace_layout.span_row_click', {
+        organization,
+        num_children: node.children.length,
+        project_platform:
+          projects.find(p => p.slug === node.metadata.project_slug)?.platform || 'other',
+      });
+
       if (traceStateRef.current.preferences.drawer.minimized) {
         traceDispatch({type: 'minimize drawer', payload: false});
       }
@@ -544,7 +551,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
         node,
       });
     },
-    [setRowAsFocused, traceDispatch]
+    [setRowAsFocused, traceDispatch, organization, projects]
   );
 
   const scrollRowIntoView = useCallback(
@@ -815,8 +822,8 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       return;
     }
 
-    logTraceType(shape, props.organization);
-  }, [tree, shape, props.organization]);
+    logTraceMetadata(tree, props.organization);
+  }, [tree, props.organization]);
 
   useLayoutEffect(() => {
     if (!tree.root?.space || tree.type !== 'trace') {

+ 13 - 8
static/app/views/performance/newTraceDetails/traceAnalytics.tsx

@@ -2,13 +2,18 @@ import * as Sentry from '@sentry/react';
 
 import type {Organization} from 'sentry/types/organization';
 import {trackAnalytics} from 'sentry/utils/analytics';
-
-import type {TraceType} from './traceType';
-
-const trackTraceShape = (shape: TraceType, organization: Organization) => {
-  Sentry.metrics.increment(`trace.trace_shape.${shape}`);
-  trackAnalytics('trace.shape', {
-    shape,
+import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import type {TraceType} from 'sentry/views/performance/newTraceDetails/traceType';
+
+const trackTraceMetadata = (tree: TraceTree, organization: Organization) => {
+  Sentry.metrics.increment(`trace.trace_shape.${tree.shape}`);
+  // space[1] represents the node duration (in milliseconds)
+  const trace_duration_seconds = (tree.root.space?.[1] ?? 0) / 1000;
+  trackAnalytics('trace.metadata', {
+    shape: tree.shape,
+    // round trace_duration_seconds to nearest two decimal places
+    trace_duration_seconds: Math.round(trace_duration_seconds * 100) / 100,
+    num_root_children: tree.root.children.length,
     organization,
   });
 };
@@ -73,7 +78,7 @@ const trackTraceWarningType = (type: TraceType, organization: Organization) =>
 
 const traceAnalytics = {
   // Trace shape
-  trackTraceShape,
+  trackTraceMetadata,
   trackEmptyTraceState,
   trackFailedToFetchTraceState,
   // Drawer actions

+ 13 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx

@@ -8,6 +8,7 @@ import SpanSummaryButton from 'sentry/components/events/interfaces/spans/spanSum
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import type {
   TraceTree,
   TraceTreeNode,
@@ -76,6 +77,18 @@ export function SpanDescription({
             spanSlug: {op: span.op, group: groupHash},
             projectID: event.projectID,
           })}
+          onClick={() => {
+            hasNewSpansUIFlag
+              ? trackAnalytics('trace.trace_layout.view_span_summary', {
+                  organization,
+                  module: resolvedModule,
+                })
+              : trackAnalytics('trace.trace_layout.view_similar_spans', {
+                  organization,
+                  module: resolvedModule,
+                  source: 'span_description',
+                });
+          }}
         >
           {hasNewSpansUIFlag ? t('View Span Summary') : t('View Similar Spans')}
         </Button>

+ 8 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/generalInfo.tsx

@@ -3,6 +3,7 @@ import type {Location} from 'history';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {t} from 'sentry/locale';
 import type {Organization} from 'sentry/types/organization';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import type {
   TraceTree,
   TraceTreeNode,
@@ -90,6 +91,13 @@ export function GeneralInfo(props: GeneralnfoProps) {
               projectID: event.projectID,
             })}
             linkText={t('View Similar Spans')}
+            onClick={() =>
+              trackAnalytics('trace.trace_layout.view_similar_spans', {
+                organization: props.organization,
+                module: resolvedModule,
+                source: 'general_info',
+              })
+            }
           />
         ) : (
           <TraceDrawerComponents.CopyableCardValueWithLink value={span.description} />

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

@@ -551,10 +551,12 @@ function CopyableCardValueWithLink({
   value,
   linkTarget,
   linkText,
+  onClick,
 }: {
   value: React.ReactNode;
   linkTarget?: LocationDescriptor;
   linkText?: string;
+  onClick?: () => void;
 }) {
   return (
     <CardValueContainer>
@@ -569,7 +571,11 @@ function CopyableCardValueWithLink({
           />
         ) : null}
       </CardValueText>
-      {linkTarget && linkTarget ? <Link to={linkTarget}>{linkText}</Link> : null}
+      {linkTarget && linkTarget ? (
+        <Link to={linkTarget} onClick={onClick}>
+          {linkText}
+        </Link>
+      ) : null}
     </CardValueContainer>
   );
 }

+ 13 - 0
static/app/views/traces/content.tsx

@@ -34,6 +34,7 @@ import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString'
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjects from 'sentry/utils/useProjects';
 import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
 
 import {type Field, FIELDS, SORTS} from './data';
@@ -558,6 +559,7 @@ function useTraces<F extends string>({
   sort,
 }: UseTracesOptions<F>) {
   const organization = useOrganization();
+  const {projects} = useProjects();
   const {selection} = usePageFilters();
 
   const path = `/organizations/${organization.slug}/traces/`;
@@ -608,10 +610,21 @@ function useTraces<F extends string>({
 
   useEffect(() => {
     if (result.status === 'success') {
+      const project_slugs = new Set(
+        result.data.data.flatMap(trace =>
+          trace.spans.map((span: SpanResult<string>) => span.project)
+        )
+      );
+      const project_platforms = [...project_slugs]
+        .map(slug => projects.find(p => p.slug === slug))
+        .map(project => project?.platform || 'other');
+
       trackAnalytics('trace_explorer.search_success', {
         organization,
         queries,
+        project_platforms,
         has_data: result.data.data.length > 0,
+        num_traces: result.data.data.length,
       });
     } else if (result.status === 'error') {
       const response = result.error.responseJSON;