Browse Source

feat(trace): Add measurements to the trace view (#45389)

- This updates the trace view to also include the measurement lines so
that transactions can be seen in the context of vitals
-
![image](https://user-images.githubusercontent.com/4205004/222820765-d2e54441-31a6-44a3-8cd3-68e7178c37b4.png)
- Closes [#45314](https://github.com/getsentry/sentry/issues/45314)
William Mak 2 years ago
parent
commit
4205efca10

+ 2 - 1
static/app/components/events/interfaces/spans/header.tsx

@@ -40,6 +40,7 @@ import {
 } from './types';
 import {
   boundsGenerator,
+  getMeasurements,
   getSpanOperation,
   SpanBoundsType,
   SpanGeneratedBoundsType,
@@ -399,7 +400,7 @@ class TraceViewHeader extends Component<PropType, State> {
               <DividerSpacer />
               {hasMeasurements ? (
                 <MeasurementsPanel
-                  event={event}
+                  measurements={getMeasurements(event, this.generateBounds())}
                   generateBounds={this.generateBounds()}
                   dividerPosition={dividerPosition}
                 />

+ 3 - 5
static/app/components/events/interfaces/spans/measurementsPanel.tsx

@@ -4,22 +4,21 @@ import styled from '@emotion/styled';
 import {toPercent} from 'sentry/components/performance/waterfall/utils';
 import {Tooltip} from 'sentry/components/tooltip';
 import {space} from 'sentry/styles/space';
-import {EventTransaction} from 'sentry/types/event';
 import {defined} from 'sentry/utils';
 import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
 import {Vital} from 'sentry/utils/performance/vitals/types';
 
 import {
   getMeasurementBounds,
-  getMeasurements,
   SpanBoundsType,
   SpanGeneratedBoundsType,
+  VerticalMark,
 } from './utils';
 
 type Props = {
   dividerPosition: number;
-  event: EventTransaction;
   generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
+  measurements: Map<number, VerticalMark>;
 };
 
 type VitalLabel = {
@@ -28,8 +27,7 @@ type VitalLabel = {
 };
 
 function MeasurementsPanel(props: Props) {
-  const {event, generateBounds, dividerPosition} = props;
-  const measurements = getMeasurements(event, generateBounds);
+  const {measurements, generateBounds, dividerPosition} = props;
 
   return (
     <Container

+ 7 - 6
static/app/components/events/interfaces/spans/utils.tsx

@@ -12,7 +12,7 @@ import {EntrySpans, EntryType, EventTransaction} from 'sentry/types/event';
 import {assert} from 'sentry/types/utils';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {WebVital} from 'sentry/utils/fields';
-import {TraceError} from 'sentry/utils/performance/quickTrace/types';
+import {TraceError, TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
 import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
 import {getPerformanceTransaction} from 'sentry/utils/performanceForSentry';
 
@@ -515,7 +515,7 @@ type Measurements = {
   };
 };
 
-type VerticalMark = {
+export type VerticalMark = {
   failedThreshold: boolean;
   marks: Measurements;
 };
@@ -536,15 +536,16 @@ function hasFailedThreshold(marks: Measurements): boolean {
 }
 
 export function getMeasurements(
-  event: EventTransaction,
+  event: EventTransaction | TraceFullDetailed,
   generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
 ): Map<number, VerticalMark> {
-  if (!event.measurements || !event.startTimestamp) {
+  const startTimestamp =
+    (event as EventTransaction).startTimestamp ||
+    (event as TraceFullDetailed).start_timestamp;
+  if (!event.measurements || !startTimestamp) {
     return new Map();
   }
 
-  const {startTimestamp} = event;
-
   // Note: CLS and INP should not be included here, since they are not timeline-based measurements.
   const allowedVitals = new Set<string>([
     WebVital.FCP,

+ 35 - 0
static/app/views/performance/traceDetails/traceView.tsx

@@ -3,7 +3,12 @@ import {RouteComponentProps} from 'react-router';
 import * as Sentry from '@sentry/react';
 
 import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
+import MeasurementsPanel from 'sentry/components/events/interfaces/spans/measurementsPanel';
 import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
+import {
+  boundsGenerator,
+  getMeasurements,
+} from 'sentry/components/events/interfaces/spans/utils';
 import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
 import {
   DividerSpacer,
@@ -82,6 +87,15 @@ function isTransactionVisible(
   return filteredTransactionIds ? filteredTransactionIds.has(transaction.event_id) : true;
 }
 
+function generateBounds(traceInfo: TraceInfo) {
+  return boundsGenerator({
+    traceStartTimestamp: traceInfo.startTimestamp,
+    traceEndTimestamp: traceInfo.endTimestamp,
+    viewStart: 0,
+    viewEnd: 1,
+  });
+}
+
 export default function TraceView({
   location,
   meta,
@@ -177,6 +191,12 @@ export default function TraceView({
               ...transaction,
               generation,
             }}
+            measurements={
+              traces && traces.length > 0
+                ? getMeasurements(traces[0], generateBounds(traceInfo))
+                : undefined
+            }
+            generateBounds={generateBounds(traceInfo)}
             continuingDepths={continuingDepths}
             isOrphan={isOrphan}
             isLast={isLast}
@@ -249,6 +269,12 @@ export default function TraceView({
     accumulator
   );
 
+  const bounds = generateBounds(traceInfo);
+  const measurements =
+    Object.keys(traces[0].measurements ?? {}).length > 0
+      ? getMeasurements(traces[0], bounds)
+      : undefined;
+
   const traceView = (
     <TraceDetailBody>
       <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
@@ -290,6 +316,13 @@ export default function TraceView({
                     }}
                   </ScrollbarManager.Consumer>
                   <DividerSpacer />
+                  {measurements ? (
+                    <MeasurementsPanel
+                      measurements={measurements}
+                      generateBounds={bounds}
+                      dividerPosition={dividerPosition}
+                    />
+                  ) : null}
                 </TraceViewHeaderContainer>
                 <TraceViewContainer ref={traceViewRef}>
                   <TransactionGroup
@@ -305,6 +338,8 @@ export default function TraceView({
                       start_timestamp: traceInfo.startTimestamp,
                       timestamp: traceInfo.endTimestamp,
                     }}
+                    measurements={measurements}
+                    generateBounds={bounds}
                     continuingDepths={[]}
                     isOrphan={false}
                     isLast={false}

+ 45 - 1
static/app/views/performance/traceDetails/transactionBar.tsx

@@ -5,7 +5,14 @@ import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import Count from 'sentry/components/count';
 import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
 import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
-import {transactionTargetHash} from 'sentry/components/events/interfaces/spans/utils';
+import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles';
+import {
+  getMeasurementBounds,
+  SpanBoundsType,
+  SpanGeneratedBoundsType,
+  transactionTargetHash,
+  VerticalMark,
+} from 'sentry/components/events/interfaces/spans/utils';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants';
 import {
@@ -40,6 +47,7 @@ import {
 } from 'sentry/components/performance/waterfall/utils';
 import {Tooltip} from 'sentry/components/tooltip';
 import {Organization} from 'sentry/types';
+import {defined} from 'sentry/utils';
 import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
 import {isTraceFullDetailed} from 'sentry/utils/performance/quickTrace/utils';
 import Projects from 'sentry/utils/projects';
@@ -53,6 +61,7 @@ const MARGIN_LEFT = 0;
 type Props = {
   addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
   continuingDepths: TreeDepth[];
+  generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
   hasGuideAnchor: boolean;
   index: number;
   isExpanded: boolean;
@@ -67,6 +76,7 @@ type Props = {
   traceInfo: TraceInfo;
   transaction: TraceRoot | TraceFullDetailed;
   barColor?: string;
+  measurements?: Map<number, VerticalMark>;
 };
 
 type State = {
@@ -139,6 +149,39 @@ class TransactionBar extends Component<Props, State> {
     onWheel(event.deltaX);
   };
 
+  renderMeasurements() {
+    const {measurements, generateBounds} = this.props;
+    if (!measurements) {
+      return null;
+    }
+
+    return (
+      <Fragment>
+        {Array.from(measurements.values()).map(verticalMark => {
+          const mark = Object.values(verticalMark.marks)[0];
+          const {timestamp} = mark;
+          const bounds = getMeasurementBounds(timestamp, generateBounds);
+
+          const shouldDisplay = defined(bounds.left) && defined(bounds.width);
+
+          if (!shouldDisplay || !bounds.isSpanVisibleInView) {
+            return null;
+          }
+
+          return (
+            <MeasurementMarker
+              key={String(timestamp)}
+              style={{
+                left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
+              }}
+              failedThreshold={verticalMark.failedThreshold}
+            />
+          );
+        })}
+      </Fragment>
+    );
+  }
+
   renderConnector(hasToggle: boolean) {
     const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = this.props;
 
@@ -472,6 +515,7 @@ class TransactionBar extends Component<Props, State> {
         >
           <GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
             {this.renderRectangle()}
+            {this.renderMeasurements()}
           </GuideAnchor>
         </RowCell>
         {!showDetail && this.renderGhostDivider(dividerHandlerChildrenProps)}

+ 11 - 0
static/app/views/performance/traceDetails/transactionGroup.tsx

@@ -5,6 +5,11 @@ import {
   ScrollbarManagerChildrenProps,
   withScrollbarManager,
 } from 'sentry/components/events/interfaces/spans/scrollbarManager';
+import {
+  SpanBoundsType,
+  SpanGeneratedBoundsType,
+  VerticalMark,
+} from 'sentry/components/events/interfaces/spans/utils';
 import {Organization} from 'sentry/types';
 import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
 
@@ -13,6 +18,7 @@ import {TraceInfo, TraceRoot, TreeDepth} from './types';
 
 type Props = ScrollbarManagerChildrenProps & {
   continuingDepths: TreeDepth[];
+  generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
   hasGuideAnchor: boolean;
   index: number;
   isLast: boolean;
@@ -24,6 +30,7 @@ type Props = ScrollbarManagerChildrenProps & {
   traceInfo: TraceInfo;
   transaction: TraceRoot | TraceFullDetailed;
   barColor?: string;
+  measurements?: Map<number, VerticalMark>;
 };
 
 type State = {
@@ -62,6 +69,8 @@ class TransactionGroup extends Component<Props, State> {
       addContentSpanBarRef,
       removeContentSpanBarRef,
       onWheel,
+      measurements,
+      generateBounds,
     } = this.props;
     const {isExpanded} = this.state;
 
@@ -70,6 +79,8 @@ class TransactionGroup extends Component<Props, State> {
         <TransactionBar
           location={location}
           organization={organization}
+          measurements={measurements}
+          generateBounds={generateBounds}
           index={index}
           transaction={transaction}
           traceInfo={traceInfo}