From 66f054b048d9922d69c84856b561f25cc067fac4 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Fri, 19 Jul 2019 12:22:52 -0400 Subject: [PATCH 01/25] SEN-846, SEN-803, SEN-808: trace view + minimap --- .../events/interfaces/spans/dragManager.tsx | 214 ++++++ .../events/interfaces/spans/index.tsx | 51 ++ .../events/interfaces/spans/minimap.tsx | 346 +++++++++ .../events/interfaces/spans/spanDetail.tsx | 131 ++++ .../events/interfaces/spans/span_tree.tsx | 665 ++++++++++++++++++ .../interfaces/spans/transactionView.tsx | 71 ++ .../events/interfaces/spans/types.tsx | 20 + .../events/interfaces/spans/utils.tsx | 108 +++ 8 files changed, 1606 insertions(+) create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx new file mode 100644 index 00000000000000..4c870b0f7c11b1 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx @@ -0,0 +1,214 @@ +import React from 'react'; + +import {rectOfContent, clamp} from './utils'; + +// we establish the minimum window size so that the window size of 0% is not possible +const MINIMUM_WINDOW_SIZE = 0.5 / 100; // 0.5% window size + +enum ViewHandleType { + Left, + Right, +} + +export type DragManagerChildrenProps = { + isDragging: boolean; + + // left-side handle + + onLeftHandleDragStart: (event: React.MouseEvent) => void; + leftHandlePosition: number; // between 0 to 1 + viewWindowStart: number; // between 0 to 1 + + // right-side handle + + onRightHandleDragStart: (event: React.MouseEvent) => void; + rightHandlePosition: number; // between 0 to 1 + viewWindowEnd: number; // between 0 to 1 +}; + +type DragManagerProps = { + children: (props: DragManagerChildrenProps) => JSX.Element; + interactiveLayerRef: React.RefObject; +}; + +type DragManagerState = { + isDragging: boolean; + currentDraggingHandle: ViewHandleType | undefined; + leftHandlePosition: number; + rightHandlePosition: number; + + viewWindowStart: number; + viewWindowEnd: number; +}; + +class DragManager extends React.Component { + state: DragManagerState = { + isDragging: false, + currentDraggingHandle: void 0, + leftHandlePosition: 0, // positioned on the left-most side at 0% + rightHandlePosition: 1, // positioned on the right-most side at 100% + + viewWindowStart: 0, + viewWindowEnd: 1, + }; + + previousUserSelect: string | null = null; + + hasInteractiveLayer = (): boolean => { + return !!this.props.interactiveLayerRef.current; + }; + + onDragStart = (viewHandle: ViewHandleType) => ( + event: React.MouseEvent + ) => { + if ( + this.state.isDragging || + event.type !== 'mousedown' || + !this.hasInteractiveLayer() + ) { + return; + } + + // prevent the user from selecting things outside the minimap when dragging + // the mouse cursor outside the minimap + + this.previousUserSelect = document.body.style.userSelect; + document.body.style.userSelect = 'none'; + + // attach event listeners so that the mouse cursor can drag outside of the + // minimap + window.addEventListener('mousemove', this.onDragMove); + window.addEventListener('mouseup', this.onDragEnd); + + // indicate drag has begun + + this.setState({ + isDragging: true, + currentDraggingHandle: viewHandle, + }); + }; + + onLeftHandleDragStart = (event: React.MouseEvent) => { + this.onDragStart(ViewHandleType.Left)(event); + }; + + onRightHandleDragStart = (event: React.MouseEvent) => { + this.onDragStart(ViewHandleType.Right)(event); + }; + + onDragMove = (event: MouseEvent) => { + if ( + !this.state.isDragging || + event.type !== 'mousemove' || + !this.hasInteractiveLayer() + ) { + return; + } + + const rect = rectOfContent(this.props.interactiveLayerRef.current!); + + // mouse x-coordinate relative to the interactive layer's left side + const rawMouseX = (event.pageX - rect.x) / rect.width; + + switch (this.state.currentDraggingHandle) { + case ViewHandleType.Left: { + const min = 0; + const max = this.state.rightHandlePosition - MINIMUM_WINDOW_SIZE; + + this.setState({ + // clamp rawMouseX to be within [0, rightHandlePosition - MINIMUM_WINDOW_SIZE] + leftHandlePosition: clamp(rawMouseX, min, max), + }); + break; + } + case ViewHandleType.Right: { + const min = this.state.leftHandlePosition + MINIMUM_WINDOW_SIZE; + const max = 1; + + this.setState({ + // clamp rawMouseX to be within [leftHandlePosition + MINIMUM_WINDOW_SIZE, 1] + rightHandlePosition: clamp(rawMouseX, min, max), + }); + break; + } + default: { + // this should never occur + } + } + }; + + onDragEnd = (event: MouseEvent) => { + if ( + !this.state.isDragging || + event.type !== 'mouseup' || + !this.hasInteractiveLayer() + ) { + return; + } + + // remove listeners that were attached in onDragStart + + window.removeEventListener('mousemove', this.onDragMove); + window.removeEventListener('mouseup', this.onDragEnd); + + // restore body styles + + document.body.style.userSelect = this.previousUserSelect; + this.previousUserSelect = null; + + // indicate drag has ended + + switch (this.state.currentDraggingHandle) { + case ViewHandleType.Left: { + this.setState(state => { + return { + isDragging: false, + currentDraggingHandle: void 0, + + // commit leftHandlePosition to be viewWindowStart + viewWindowStart: state.leftHandlePosition, + }; + }); + break; + } + case ViewHandleType.Right: { + this.setState(state => { + return { + isDragging: false, + currentDraggingHandle: void 0, + + // commit rightHandlePosition to be viewWindowEnd + viewWindowEnd: state.rightHandlePosition, + }; + }); + break; + } + default: { + // unreachable + } + } + + this.setState({ + isDragging: false, + currentDraggingHandle: void 0, + }); + }; + + render() { + const childrenProps = { + isDragging: this.state.isDragging, + + onLeftHandleDragStart: this.onLeftHandleDragStart, + leftHandlePosition: this.state.leftHandlePosition, + viewWindowStart: this.state.viewWindowStart, + + onRightHandleDragStart: this.onRightHandleDragStart, + rightHandlePosition: this.state.rightHandlePosition, + viewWindowEnd: this.state.viewWindowEnd, + }; + + return this.props.children(childrenProps); + } +} + +export default DragManager; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx new file mode 100644 index 00000000000000..9b82671f628765 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +// TODO: remove +// import PropTypes from 'prop-types'; + +import {t} from 'app/locale'; +import SentryTypes from 'app/sentryTypes'; + +import {Panel, PanelHeader, PanelBody} from 'app/components/panels'; + +import {SpanEntry, SentryEvent} from './types'; +import TransactionView from './transactionView'; + +type SpansInterfacePropTypes = { + event: SentryEvent; +} & SpanEntry; + +class SpansInterface extends React.Component { + static propTypes = { + event: SentryTypes.Event.isRequired, + // TODO: necessary? + // type: PropTypes.oneOf(['spans']).isRequired, + // data: PropTypes.arrayOf( + // PropTypes.shape({ + // trace_id: PropTypes.string.isRequired, + // parent_span_id: PropTypes.string, + // span_id: PropTypes.string.isRequired, + // start_timestamp: PropTypes.number.isRequired, + // timestamp: PropTypes.number.isRequired, // same as end_timestamp + // same_process_as_parent: PropTypes.bool.isRequired, + // op: PropTypes.string.isRequired, + // data: PropTypes.object.isRequired, + // }) + // ).isRequired, + }; + render() { + const {event} = this.props; + + return ( + + + {t('Trace View')} + + + + + + ); + } +} + +export default SpansInterface; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx new file mode 100644 index 00000000000000..4b4e5afed855cd --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -0,0 +1,346 @@ +import React from 'react'; +import styled from 'react-emotion'; + +import {rectOfContent, clamp, rectRelativeTo, rectOfElement, toPercent} from './utils'; +import {DragManagerChildrenProps} from './dragManager'; + +const MINIMAP_HEIGHT = 75; + +type MinimapProps = { + traceViewRef: React.RefObject; + minimapInteractiveRef: React.RefObject; + dragProps: DragManagerChildrenProps; +}; + +type MinimapState = { + showCursorGuide: boolean; + mousePageX: number | undefined; + startViewHandleX: number; +}; + +class Minimap extends React.Component { + state: MinimapState = { + showCursorGuide: false, + mousePageX: void 0, + startViewHandleX: 100, + }; + + minimapRef = React.createRef(); + + componentDidMount() { + this.drawMinimap(); + } + + drawMinimap = () => { + const canvas = this.minimapRef.current; + const traceViewDOM = this.props.traceViewRef.current; + + if (!canvas || !traceViewDOM) { + return; + } + + const canvasContext = canvas.getContext('2d'); + + if (!canvasContext) { + return; + } + + const root_rect = rectOfContent(traceViewDOM); + + const scaleX = canvas.clientWidth / root_rect.width; + const scaleY = canvas.clientHeight / root_rect.height; + + // https://www.html5rocks.com/en/tutorials/canvas/hidpi/ + // we consider the devicePixelRatio (dpr) factor so that the canvas looks decent on hidpi screens + // such as retina macbooks + const devicePixelRatio = window.devicePixelRatio || 1; + + const resize_canvas = (width: number, height: number) => { + // scale the canvas up by the dpr factor + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + + // scale the canvas down by the dpr factor thru CSS + canvas.style.width = '100%'; + canvas.style.height = `${height}px`; + }; + + resize_canvas(root_rect.width * scaleX, root_rect.height * scaleY); + + canvasContext.setTransform(1, 0, 0, 1, 0, 0); + canvasContext.clearRect(0, 0, canvas.width, canvas.height); + canvasContext.scale(scaleX, scaleY); + + // scale canvas operations by the dpr factor + canvasContext.scale(devicePixelRatio, devicePixelRatio); + + const black = (pc: number) => `rgba(0,0,0,${pc / 100})`; + const back = black(0); + + const drawRect = ( + rect: {x: number; y: number; width: number; height: number}, + colour: string + ) => { + if (colour) { + canvasContext.beginPath(); + canvasContext.rect(rect.x, rect.y, rect.width, rect.height); + canvasContext.fillStyle = colour; + canvasContext.fill(); + } + }; + + // draw background + + drawRect(rectRelativeTo(root_rect, root_rect), back); + + // draw the spans + + Array.from(traceViewDOM.querySelectorAll('[data-span="true"]')).forEach( + el => { + const backgroundColor = window.getComputedStyle(el).backgroundColor || black(10); + drawRect(rectRelativeTo(rectOfElement(el), root_rect), backgroundColor); + } + ); + }; + + renderCursorGuide = () => { + if (!this.state.showCursorGuide || !this.state.mousePageX) { + return null; + } + + const minimapCanvas = this.props.minimapInteractiveRef.current; + + if (!minimapCanvas) { + return null; + } + + const rect = rectOfContent(minimapCanvas); + + // clamp mouseLeft to be within [0, 100] + const mouseLeft = clamp( + ((this.state.mousePageX - rect.x) / rect.width) * 100, + 0, + 100 + ); + + return ( + + + + ); + }; + + renderViewHandles = ({ + isDragging, + onLeftHandleDragStart, + leftHandlePosition, + viewWindowStart, + onRightHandleDragStart, + rightHandlePosition, + viewWindowEnd, + }: DragManagerChildrenProps) => { + const leftHandleGhost = isDragging ? ( + + + + + ) : null; + + const leftHandle = ( + + + + + ); + + const rightHandle = ( + + + + + ); + + const rightHandleGhost = isDragging ? ( + + + + + ) : null; + + return ( + + {leftHandleGhost} + {rightHandleGhost} + {leftHandle} + {rightHandle} + + ); + }; + + renderFog = (dragProps: DragManagerChildrenProps) => { + return ( + + + + + ); + }; + + render() { + return ( + + +
{ + this.setState({ + showCursorGuide: true, + mousePageX: event.pageX, + }); + }} + onMouseLeave={() => { + this.setState({showCursorGuide: false, mousePageX: void 0}); + }} + onMouseMove={event => { + this.setState({ + showCursorGuide: true, + mousePageX: event.pageX, + }); + }} + > + + {this.renderFog(this.props.dragProps)} + {this.renderCursorGuide()} + {this.renderViewHandles(this.props.dragProps)} + +
+
+ ); + } +} + +const Container = styled('div')` + width: 100%; + position: relative; + left: 0; + border-bottom: 1px solid #d1cad8; +`; + +const MinimapBackground = styled('canvas')` + height: ${MINIMAP_HEIGHT}px; + width: 100%; + position: absolute; + top: 0; + left: 0; +`; + +const InteractiveLayer = styled('svg')` + height: ${MINIMAP_HEIGHT}px; + width: 100%; + position: relative; + left: 0; +`; + +const ViewHandle = styled('rect')` + fill: #6c5fc7; + + cursor: col-resize; + + height: 20px; + + ${({isDragging}: {isDragging: boolean}) => { + if (isDragging) { + return ` + width: 5px; + transform: translate(-2.5px, ${MINIMAP_HEIGHT - 20}px); + `; + } + + return ` + width: 3px; + transform: translate(-1.5px, ${MINIMAP_HEIGHT - 20}px); + `; + }}; + + &:hover { + width: 5px; + transform: translate(-2.5px, ${MINIMAP_HEIGHT - 20}px); + } +`; + +const Fog = styled('rect')` + fill: rgba(241, 245, 251, 0.5); +`; + +export default Minimap; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx new file mode 100644 index 00000000000000..b12d5d9ca7e180 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import styled from 'react-emotion'; +import _ from 'lodash'; + +import DateTime from 'app/components/dateTime'; +import Pills from 'app/components/pills'; +import Pill from 'app/components/pill'; +import space from 'app/styles/space'; + +import {SpanType} from './types'; + +type PropTypes = { + span: Readonly; +}; + +const SpanDetail = (props: PropTypes) => { + const {span} = props; + + const start_timestamp: number = span.start_timestamp; + const end_timestamp: number = span.timestamp; + + const duration = (end_timestamp - start_timestamp) * 1000; + const durationString = `${duration.toFixed(3)} ms`; + + return ( + { + // prevent toggling the span detail + event.stopPropagation(); + }} + > + + + {span.span_id} + {span.trace_id} + {span.parent_span_id || ''} + {_.get(span, 'description', '')} + + + + {` (${start_timestamp})`} + + + + + + {` (${end_timestamp})`} + + + {durationString} + {span.op || ''} + + {String(!!span.same_process_as_parent)} + + + {_.map(_.get(span, 'data', {}), (value, key) => { + return ( + + {JSON.stringify(value, null, 4) || ''} + + ); + })} + {JSON.stringify(span, null, 4)} + +
+
+ ); +}; + +const SpanDetailContainer = styled('div')` + border-bottom: 1px solid #d1cad8; + padding: ${space(2)}; + background-color: #fff; + + cursor: auto; +`; + +const Row = ({ + title, + keep, + children, +}: { + title: string; + keep?: boolean; + children: JSX.Element | string; +}) => { + if (!keep && !children) { + return null; + } + + return ( + + {title} + +
+          {children}
+        
+ + + ); +}; + +const Tags = ({span}: {span: SpanType}) => { + const tags: {[tag_name: string]: string} | undefined = _.get(span, 'tags'); + + if (!tags) { + return null; + } + + const keys = Object.keys(tags); + + if (keys.length <= 0) { + return null; + } + + return ( + + Tags + + + {keys.map((key, index) => { + return ; + })} + + + + ); +}; + +export default SpanDetail; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx new file mode 100644 index 00000000000000..dbbfb1c4c1aa77 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx @@ -0,0 +1,665 @@ +import React from 'react'; +import styled from 'react-emotion'; +import _ from 'lodash'; + +import space from 'app/styles/space'; +import Count from 'app/components/count'; + +import {SpanType, SpanEntry, SentryEvent} from './types'; +import { + isValidSpanID, + toPercent, + boundsGenerator, + SpanBoundsType, + SpanGeneratedBoundsType, +} from './utils'; +import {DragManagerChildrenProps} from './dragManager'; +import SpanDetail from './spanDetail'; + +type TraceType = { + type: 'trace'; + span_id: string; + trace_id: string; +}; + +type LookupType = {[span_id: string]: SpanType[]}; + +type RenderedSpanTree = { + spanTree: JSX.Element; + numOfHiddenSpansAbove: number; +}; + +type SpanTreeProps = { + traceViewRef: React.RefObject; + event: SentryEvent; + dragProps: DragManagerChildrenProps; +}; + +class SpanTree extends React.Component { + renderSpan = ({ + treeDepth, + numberOfChildrenOfParent, + numOfHiddenSpansAbove, + spanID, + traceID, + lookup, + span, + generateBounds, + pickSpanBarColour, + }: { + treeDepth: number; + numberOfChildrenOfParent: Array; + numOfHiddenSpansAbove: number; + spanID: string; + traceID: string; + span: Readonly; + lookup: Readonly; + generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; + pickSpanBarColour: () => string; + }): RenderedSpanTree => { + const spanBarColour: string = pickSpanBarColour(); + + const spanChildren: SpanType[] = _.get(lookup, spanID, []); + + const numOfSpanChildren = spanChildren.length; + + const start_timestamp: number = span.start_timestamp; + const end_timestamp: number = span.timestamp; + + const bounds = generateBounds({ + startTimestamp: start_timestamp, + endTimestamp: end_timestamp, + }); + + const isCurrentSpanHidden = bounds.end <= 0; + + type AccType = { + renderedSpanChildren: Array; + numOfHiddenSpansAbove: number; + }; + + const reduced: AccType = spanChildren.reduce( + (acc: AccType, spanChild) => { + const key = `${traceID}${spanChild.span_id}`; + + const results = this.renderSpan({ + treeDepth: treeDepth + 1, + numberOfChildrenOfParent: [...numberOfChildrenOfParent, numOfSpanChildren], + numOfHiddenSpansAbove: acc.numOfHiddenSpansAbove, + span: spanChild, + spanID: spanChild.span_id, + traceID, + lookup, + generateBounds, + pickSpanBarColour, + }); + + acc.renderedSpanChildren.push( + {results.spanTree} + ); + + acc.numOfHiddenSpansAbove = results.numOfHiddenSpansAbove; + + return acc; + }, + { + renderedSpanChildren: [], + numOfHiddenSpansAbove: isCurrentSpanHidden ? numOfHiddenSpansAbove + 1 : 0, + } + ); + + const showHiddenSpansMessage = !isCurrentSpanHidden && numOfHiddenSpansAbove > 0; + + const hiddenSpansMessage = showHiddenSpansMessage ? ( + + Number of hidden spans: {numOfHiddenSpansAbove} + + ) : null; + + return { + numOfHiddenSpansAbove: reduced.numOfHiddenSpansAbove, + spanTree: ( + + {hiddenSpansMessage} + + + ), + }; + }; + + renderRootSpan = (): JSX.Element | null => { + const {event, dragProps} = this.props; + + const trace: TraceType | undefined = _.get(event, 'contexts.trace'); + + if (!trace) { + return null; + } + + const parsedTrace = this.parseTrace(); + + // TODO: ideally this should be provided + const rootSpan: SpanType = { + trace_id: trace.trace_id, + parent_span_id: void 0, + span_id: trace.span_id, + start_timestamp: parsedTrace.traceStartTimestamp, + timestamp: parsedTrace.traceEndTimestamp, + same_process_as_parent: true, + op: 'transaction', + data: {}, + }; + + const COLORS = ['#e9e7f7', '#fcefde', '#fffbee', '#f1f5fb']; + let current_index = 0; + + const pickSpanBarColour = () => { + const next_colour = COLORS[current_index]; + + current_index++; + current_index = current_index % COLORS.length; + + return next_colour; + }; + + // TODO: remove later + // const traceEndTimestamp = _.isNumber(parsedTrace.traceEndTimestamp) + // ? parsedTrace.traceStartTimestamp == parsedTrace.traceEndTimestamp + // ? parsedTrace.traceStartTimestamp + 0.05 + // : parsedTrace.traceEndTimestamp + // : parsedTrace.traceStartTimestamp + 0.05; + + const generateBounds = boundsGenerator({ + traceStartTimestamp: parsedTrace.traceStartTimestamp, + traceEndTimestamp: parsedTrace.traceEndTimestamp, + viewStart: dragProps.viewWindowStart, + viewEnd: dragProps.viewWindowEnd, + }); + + return this.renderSpan({ + treeDepth: 0, + numberOfChildrenOfParent: [], + numOfHiddenSpansAbove: 0, + span: rootSpan, + spanID: trace.span_id, + traceID: trace.trace_id, + lookup: parsedTrace.lookup, + generateBounds, + pickSpanBarColour, + }).spanTree; + }; + + parseTrace = () => { + const {event} = this.props; + + const spanEntry: SpanEntry | undefined = event.entries.find( + (entry: {type: string}) => entry.type === 'spans' + ); + + const spans: SpanType[] = _.get(spanEntry, 'data', []); + + if (!spanEntry || spans.length <= 0) { + return { + lookup: {}, + traceStartTimestamp: 0, + traceEndTimestamp: 0, + }; + } + + // we reduce spans to become an object mapping span ids to their children + + type ReducedType = { + lookup: LookupType; + traceStartTimestamp: number; + traceEndTimestamp: number; + }; + + const init: ReducedType = { + lookup: {}, + traceStartTimestamp: spans[0].start_timestamp, + traceEndTimestamp: 0, + }; + + const reduced: ReducedType = spans.reduce((acc, span) => { + if (!isValidSpanID(span.parent_span_id)) { + return acc; + } + + const spanChildren: SpanType[] = _.get(acc.lookup, span.parent_span_id!, []); + + spanChildren.push(span); + + _.set(acc.lookup, span.parent_span_id!, spanChildren); + + if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) { + acc.traceStartTimestamp = span.start_timestamp; + } + + // establish trace end timestamp + + const hasEndTimestamp = _.isNumber(span.timestamp); + + if (!acc.traceEndTimestamp) { + if (hasEndTimestamp) { + acc.traceEndTimestamp = span.timestamp; + return acc; + } + + acc.traceEndTimestamp = span.start_timestamp; + return acc; + } + + if (hasEndTimestamp && span.timestamp! > acc.traceEndTimestamp) { + acc.traceEndTimestamp = span.timestamp; + return acc; + } + + if (span.start_timestamp > acc.traceEndTimestamp) { + acc.traceEndTimestamp = span.start_timestamp; + } + + return acc; + }, init); + + // sort span children by their start timestamps in ascending order + + _.forEach(reduced.lookup, spanChildren => { + spanChildren.sort((firstSpan, secondSpan) => { + if (firstSpan.start_timestamp < secondSpan.start_timestamp) { + return -1; + } + + if (firstSpan.start_timestamp === secondSpan.start_timestamp) { + return 0; + } + + return 1; + }); + }); + + return reduced; + }; + + render() { + return ( + + {this.renderRootSpan()} + + ); + } +} + +type SpanPropTypes = { + span: Readonly; + generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; + treeDepth: number; + numOfSpanChildren: number; + renderedSpanChildren: Array; + spanBarColour: string; + numberOfChildrenOfParent: Array; +}; + +type SpanState = { + displayDetail: boolean; + showSpanTree: boolean; +}; + +class Span extends React.Component { + state: SpanState = { + displayDetail: false, + showSpanTree: true, + }; + + toggleSpanTree = () => { + this.setState(state => { + return { + showSpanTree: !state.showSpanTree, + }; + }); + }; + + toggleDisplayDetail = () => { + this.setState(state => { + return { + displayDetail: !state.displayDetail, + }; + }); + }; + + renderDetail = ({isVisible}: {isVisible: boolean}) => { + if (!this.state.displayDetail || !isVisible) { + return null; + } + + const {span} = this.props; + + return ; + }; + + getBounds = () => { + const {span, generateBounds} = this.props; + + const start_timestamp: number = span.start_timestamp; + const end_timestamp: number = span.timestamp; + + return generateBounds({ + startTimestamp: start_timestamp, + endTimestamp: end_timestamp, + }); + }; + + renderSpanTreeToggler = ({left}: {left: number}) => { + const {numOfSpanChildren, numberOfChildrenOfParent} = this.props; + + const chevron = this.state.showSpanTree ? : ; + + const hiddenTreasure = numberOfChildrenOfParent.map((num, index) => { + return ( + + + + +
{chevron}
+
+ ); + }); + + if (numOfSpanChildren <= 0) { + if (hiddenTreasure.length > 0) { + return hiddenTreasure; + } + return null; + } + + return ( + + {hiddenTreasure} + { + event.stopPropagation(); + + this.toggleSpanTree(); + }} + > + + + +
+ {chevron} +
+
+
+ ); + }; + + renderTitle = () => { + const {span, treeDepth} = this.props; + + const op = span.op ? {`${span.op} \u2014 `} : ''; + const description = _.get(span, 'description', span.span_id); + + const MARGIN_LEFT = 8; + const left = treeDepth * (8 * 0) + MARGIN_LEFT; + + return ( + + {this.renderSpanTreeToggler({left})} + + + {op} + {description} + + + + ); + }; + + renderSpanChildren = () => { + if (!this.state.showSpanTree) { + return null; + } + + return this.props.renderedSpanChildren; + }; + + render() { + const {span, spanBarColour} = this.props; + + const start_timestamp: number = span.start_timestamp; + const end_timestamp: number = span.timestamp; + + const duration = (end_timestamp - start_timestamp) * 1000; + const durationString = `${duration.toFixed(3)} ms`; + + const bounds = this.getBounds(); + + const isVisible = bounds.end > 0 && bounds.start < 1; + + return ( + + { + this.toggleDisplayDetail(); + }} + > + + {this.renderTitle()} + {durationString} + {this.renderDetail({isVisible})} + + {this.renderSpanChildren()} + + ); + } +} + +const TraceViewContainer = styled('div')` + overflow-x: hidden; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +`; + +const SPAN_ROW_HEIGHT = 25; + +const SpanRow = styled('div')` + position: relative; + overflow: hidden; + + cursor: pointer; + transition: background-color 0.15s ease-in-out; + + &:last-child { + & > [data-component='span-detail'] { + border-bottom: none !important; + } + } + + & > [data-span='true'] { + transition: border-color 0.15s ease-in-out; + border: 1px solid rgba(0, 0, 0, 0); + } + + &:hover { + background-color: rgba(189, 180, 199, 0.1); + + & > [data-span='true'] { + transition: border-color 0.15s ease-in-out; + border: 1px solid rgba(0, 0, 0, 0.1); + } + } +`; + +const SpanRowMessage = styled(SpanRow)` + cursor: auto; + + color: #4a3e56; + font-size: 12px; + line-height: ${SPAN_ROW_HEIGHT}px; + + padding-left: ${space(1)}; + padding-right: ${space(1)}; + + background-color: #f1f5fb !important; + + outline: 1px solid #c9d4ea; + + z-index: 99999; +`; + +const SpanBarTitleContainer = styled('div')` + display: flex; + align-items: center; + + height: ${SPAN_ROW_HEIGHT}px; + position: absolute; + left: 0; + top: 0; + width: 100%; +`; + +const SpanBarTitle = styled('div')` + position: relative; + top: 0; + + height: ${SPAN_ROW_HEIGHT}px; + line-height: ${SPAN_ROW_HEIGHT}px; + + color: #4a3e56; + font-size: 12px; + + user-select: none; + + white-space: nowrap; +`; + +const SpanTreeToggler = styled('div')` + position: relative; + + white-space: nowrap; + + height: 15px; + min-width: 25px; + + padding-left: 4px; + padding-right: 4px; + + margin-right: 8px; + + z-index: 999999; + + user-select: none; + + display: flex; + flex-wrap: nowrap; + align-items: center; + align-content: center; + justify-content: center; + + > span { + flex-grow: 999; + } + + border-radius: 99px; + border: 1px solid #6e5f7d; + + background: #fbfaf9; + transition: all 0.15s ease-in-out; + + font-size: 9px; + line-height: 0; + color: #6e5f7d; + + &:hover { + background: #6e5f7d; + border: 1px solid #452650; + color: #ffffff; + + & svg path { + stroke: #fff; + } + } +`; + +const Duration = styled('div')` + position: absolute; + right: 0; + top: 0; + height: ${SPAN_ROW_HEIGHT}px; + line-height: ${SPAN_ROW_HEIGHT}px; + + color: #9585a3; + font-size: 12px; + padding-right: ${space(1)}; + + user-select: none; +`; + +const SpanBar = styled('div')` + position: relative; + min-height: ${SPAN_ROW_HEIGHT - 4}px; + height: ${SPAN_ROW_HEIGHT - 4}px; + max-height: ${SPAN_ROW_HEIGHT - 4}px; + + margin-top: 2px; + margin-bottom: 2px; + border-radius: 3px; + + overflow: hidden; + + user-select: none; + + padding: 4px; +`; + +const ChevronOpen = props => ( + + + +); + +const ChevronClosed = props => ( + + + +); + +export default SpanTree; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx new file mode 100644 index 00000000000000..0b339acb9daa43 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import DragManager, {DragManagerChildrenProps} from './dragManager'; +import SpanTree from './span_tree'; +import {SentryEvent} from './types'; + +import TraceViewMinimap from './minimap'; + +type TransactionViewProps = { + event: SentryEvent; +}; + +type TransactionViewState = { + renderMinimap: boolean; +}; + +class TransactionView extends React.Component< + TransactionViewProps, + TransactionViewState +> { + minimapInteractiveRef = React.createRef(); + traceViewRef = React.createRef(); + + state: TransactionViewState = { + renderMinimap: false, + }; + + componentDidMount() { + if (this.traceViewRef.current) { + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ + renderMinimap: true, + }); + } + } + + renderMinimap = (dragProps: DragManagerChildrenProps) => { + if (!this.state.renderMinimap) { + return null; + } + + return ( + + ); + }; + + render() { + return ( + + {(dragProps: DragManagerChildrenProps) => { + return ( + + {this.renderMinimap(dragProps)} + + + ); + }} + + ); + } +} + +export default TransactionView; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx new file mode 100644 index 00000000000000..6ea5d8bb9f23d7 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx @@ -0,0 +1,20 @@ +export type SpanType = { + trace_id: string; + parent_span_id?: string; + span_id: string; + start_timestamp: number; + timestamp: number; // this is essentially end_timestamp + same_process_as_parent: boolean; + op?: string; + description?: string; + data: Object; +}; + +export type SpanEntry = { + type: 'spans'; + data: SpanType[]; +}; + +export type SentryEvent = { + entries: SpanEntry[]; +}; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx new file mode 100644 index 00000000000000..236c51beb20565 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx @@ -0,0 +1,108 @@ +import {isString, isNumber} from 'lodash'; + +const Rect = (x: number, y: number, width: number, height: number) => { + // x and y are left/top coords respectively + return {x, y, width, height}; +}; + +// get position of element relative to top/left of document +const getOffsetOfElement = (element: HTMLElement) => { + // left and top are relative to viewport + const {left, top} = element.getBoundingClientRect(); + + // get values that the document is currently scrolled by + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + return {x: left + scrollLeft, y: top + scrollTop}; +}; + +export const rectOfContent = (element: HTMLElement) => { + const {x, y} = getOffsetOfElement(element); + + // offsets for the border and any scrollbars (clientLeft and clientTop), + // and if the element was scrolled (scrollLeft and scrollTop) + // + // NOTE: clientLeft and clientTop does not account for any margins nor padding + const contentOffsetLeft = element.clientLeft - element.scrollLeft; + const contentOffsetTop = element.clientTop - element.scrollTop; + + return Rect( + x + contentOffsetLeft, + y + contentOffsetTop, + element.scrollWidth, + element.scrollHeight + ); +}; + +export const rectRelativeTo = ( + rect: {x: number; y: number; width: number; height: number}, + pos = {x: 0, y: 0} +) => { + return Rect(rect.x - pos.x, rect.y - pos.y, rect.width, rect.height); +}; + +export const rectOfElement = (element: HTMLElement) => { + const {x, y} = getOffsetOfElement(element); + return Rect(x, y, element.offsetWidth, element.offsetHeight); +}; + +export const clamp = (value: number, min: number, max: number): number => { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +}; + +export const isValidSpanID = (maybeSpanID: any) => { + return isString(maybeSpanID) && maybeSpanID.length > 0; +}; + +export const toPercent = (value: number) => { + return `${(value * 100).toFixed(3)}%`; +}; + +export type SpanBoundsType = {startTimestamp: number; endTimestamp: number}; +export type SpanGeneratedBoundsType = {start: number; end: number}; + +export const boundsGenerator = (bounds: { + traceStartTimestamp: number; + traceEndTimestamp: number; + viewStart: number; // in [0, 1] + viewEnd: number; // in [0, 1] +}) => { + const {traceEndTimestamp, traceStartTimestamp, viewStart, viewEnd} = bounds; + + // viewStart and viewEnd are percentage values (%) of the view window relative to the left + // side of the trace view minimap + + // invariant: viewStart <= viewEnd + + // duration of the entire trace in seconds + const duration = traceEndTimestamp - traceStartTimestamp; + + const viewStartTimestamp = traceStartTimestamp + viewStart * duration; + const viewEndTimestamp = traceEndTimestamp - (1 - viewEnd) * duration; + const viewDuration = viewEndTimestamp - viewStartTimestamp; + + return (spanBounds: SpanBoundsType): SpanGeneratedBoundsType => { + const {startTimestamp, endTimestamp} = spanBounds; + + const start = (startTimestamp - viewStartTimestamp) / viewDuration; + + if (!isNumber(endTimestamp)) { + return { + start, + end: 1, + }; + } + + return { + start, + end: (endTimestamp - viewStartTimestamp) / viewDuration, + }; + }; +}; From 9e733327044b474a4930c5016c48c7d8d2e8fc1b Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 13:37:12 -0400 Subject: [PATCH 02/25] add span interface --- src/sentry/static/sentry/app/components/events/eventEntries.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/static/sentry/app/components/events/eventEntries.jsx b/src/sentry/static/sentry/app/components/events/eventEntries.jsx index 38a6f108ee3db6..d2b9bf0637c49f 100644 --- a/src/sentry/static/sentry/app/components/events/eventEntries.jsx +++ b/src/sentry/static/sentry/app/components/events/eventEntries.jsx @@ -32,6 +32,7 @@ import SentryTypes from 'app/sentryTypes'; import StacktraceInterface from 'app/components/events/interfaces/stacktrace'; import TemplateInterface from 'app/components/events/interfaces/template'; import ThreadsInterface from 'app/components/events/interfaces/threads'; +import SpansInterface from 'app/components/events/interfaces/spans'; import withApi from 'app/utils/withApi'; import withOrganization from 'app/utils/withOrganization'; @@ -48,6 +49,7 @@ export const INTERFACES = { breadcrumbs: BreadcrumbsInterface, threads: ThreadsInterface, debugmeta: DebugMetaInterface, + spans: SpansInterface, }; class EventEntries extends React.Component { From 7c3a9d6474dca18ef52f8a098b3ba52ab2fd2505 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 15:16:45 -0400 Subject: [PATCH 03/25] clean up --- .../components/events/interfaces/spans/index.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx index 9b82671f628765..63fbb6fe7ff462 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx @@ -1,6 +1,4 @@ import React from 'react'; -// TODO: remove -// import PropTypes from 'prop-types'; import {t} from 'app/locale'; import SentryTypes from 'app/sentryTypes'; @@ -17,20 +15,6 @@ type SpansInterfacePropTypes = { class SpansInterface extends React.Component { static propTypes = { event: SentryTypes.Event.isRequired, - // TODO: necessary? - // type: PropTypes.oneOf(['spans']).isRequired, - // data: PropTypes.arrayOf( - // PropTypes.shape({ - // trace_id: PropTypes.string.isRequired, - // parent_span_id: PropTypes.string, - // span_id: PropTypes.string.isRequired, - // start_timestamp: PropTypes.number.isRequired, - // timestamp: PropTypes.number.isRequired, // same as end_timestamp - // same_process_as_parent: PropTypes.bool.isRequired, - // op: PropTypes.string.isRequired, - // data: PropTypes.object.isRequired, - // }) - // ).isRequired, }; render() { const {event} = this.props; From 2b8d698211201beaabf62801c8d2cc1ecf80917f Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 17:47:25 -0400 Subject: [PATCH 04/25] polish toggle button --- .../events/interfaces/spans/span_tree.tsx | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx index dbbfb1c4c1aa77..72aa522e8515ca 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx @@ -38,7 +38,6 @@ type SpanTreeProps = { class SpanTree extends React.Component { renderSpan = ({ treeDepth, - numberOfChildrenOfParent, numOfHiddenSpansAbove, spanID, traceID, @@ -48,7 +47,6 @@ class SpanTree extends React.Component { pickSpanBarColour, }: { treeDepth: number; - numberOfChildrenOfParent: Array; numOfHiddenSpansAbove: number; spanID: string; traceID: string; @@ -61,8 +59,6 @@ class SpanTree extends React.Component { const spanChildren: SpanType[] = _.get(lookup, spanID, []); - const numOfSpanChildren = spanChildren.length; - const start_timestamp: number = span.start_timestamp; const end_timestamp: number = span.timestamp; @@ -84,7 +80,6 @@ class SpanTree extends React.Component { const results = this.renderSpan({ treeDepth: treeDepth + 1, - numberOfChildrenOfParent: [...numberOfChildrenOfParent, numOfSpanChildren], numOfHiddenSpansAbove: acc.numOfHiddenSpansAbove, span: spanChild, spanID: spanChild.span_id, @@ -128,7 +123,6 @@ class SpanTree extends React.Component { numOfSpanChildren={spanChildren.length} renderedSpanChildren={reduced.renderedSpanChildren} spanBarColour={spanBarColour} - numberOfChildrenOfParent={numberOfChildrenOfParent} /> ), @@ -186,7 +180,6 @@ class SpanTree extends React.Component { return this.renderSpan({ treeDepth: 0, - numberOfChildrenOfParent: [], numOfHiddenSpansAbove: 0, span: rootSpan, spanID: trace.span_id, @@ -304,7 +297,6 @@ type SpanPropTypes = { numOfSpanChildren: number; renderedSpanChildren: Array; spanBarColour: string; - numberOfChildrenOfParent: Array; }; type SpanState = { @@ -357,33 +349,17 @@ class Span extends React.Component { }; renderSpanTreeToggler = ({left}: {left: number}) => { - const {numOfSpanChildren, numberOfChildrenOfParent} = this.props; + const {numOfSpanChildren} = this.props; const chevron = this.state.showSpanTree ? : ; - const hiddenTreasure = numberOfChildrenOfParent.map((num, index) => { - return ( - - - - -
{chevron}
-
- ); - }); - if (numOfSpanChildren <= 0) { - if (hiddenTreasure.length > 0) { - return hiddenTreasure; - } return null; } return ( - - {hiddenTreasure} + { event.stopPropagation(); @@ -397,7 +373,7 @@ class Span extends React.Component { {chevron} - + ); }; @@ -408,7 +384,11 @@ class Span extends React.Component { const description = _.get(span, 'description', span.span_id); const MARGIN_LEFT = 8; - const left = treeDepth * (8 * 0) + MARGIN_LEFT; + const TOGGLE_BUTTON_MARGIN_RIGHT = 8; + const TOGGLE_BUTTON_MAX_WIDTH = 40; + + const left = + treeDepth * (TOGGLE_BUTTON_MAX_WIDTH + TOGGLE_BUTTON_MARGIN_RIGHT) + MARGIN_LEFT; return ( @@ -559,16 +539,15 @@ const SpanBarTitle = styled('div')` white-space: nowrap; `; -const SpanTreeToggler = styled('div')` +const SpanTreeTogglerContainer = styled('div')` position: relative; - - white-space: nowrap; + top: 0; height: 15px; - min-width: 25px; - padding-left: 4px; - padding-right: 4px; + max-width: 40px; + width: 40px; + min-width: 40px; margin-right: 8px; @@ -576,6 +555,21 @@ const SpanTreeToggler = styled('div')` user-select: none; + display: flex; + justify-content: flex-end; +`; + +const SpanTreeToggler = styled('div')` + position: relative; + + white-space: nowrap; + + height: 15px; + min-width: 25px; + + padding-left: 4px; + padding-right: 4px; + display: flex; flex-wrap: nowrap; align-items: center; From b8794514b4f9ef7ee7e24d1328076bc67be90d51 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:03:00 -0400 Subject: [PATCH 05/25] TraceContextType --- .../app/components/events/interfaces/spans/span_tree.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx index 72aa522e8515ca..97cf45cf228906 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx @@ -16,7 +16,7 @@ import { import {DragManagerChildrenProps} from './dragManager'; import SpanDetail from './spanDetail'; -type TraceType = { +type TraceContextType = { type: 'trace'; span_id: string; trace_id: string; @@ -132,7 +132,7 @@ class SpanTree extends React.Component { renderRootSpan = (): JSX.Element | null => { const {event, dragProps} = this.props; - const trace: TraceType | undefined = _.get(event, 'contexts.trace'); + const trace: TraceContextType | undefined = _.get(event, 'contexts.trace'); if (!trace) { return null; From b36da9ce590d5e00af640054d6317fdb09f7114b Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:07:26 -0400 Subject: [PATCH 06/25] Rect type --- .../events/interfaces/spans/utils.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx index 236c51beb20565..e1aaafb261395d 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/utils.tsx @@ -1,8 +1,11 @@ import {isString, isNumber} from 'lodash'; -const Rect = (x: number, y: number, width: number, height: number) => { +type Rect = { // x and y are left/top coords respectively - return {x, y, width, height}; + x: number; + y: number; + width: number; + height: number; }; // get position of element relative to top/left of document @@ -17,7 +20,7 @@ const getOffsetOfElement = (element: HTMLElement) => { return {x: left + scrollLeft, y: top + scrollTop}; }; -export const rectOfContent = (element: HTMLElement) => { +export const rectOfContent = (element: HTMLElement): Rect => { const {x, y} = getOffsetOfElement(element); // offsets for the border and any scrollbars (clientLeft and clientTop), @@ -27,24 +30,31 @@ export const rectOfContent = (element: HTMLElement) => { const contentOffsetLeft = element.clientLeft - element.scrollLeft; const contentOffsetTop = element.clientTop - element.scrollTop; - return Rect( - x + contentOffsetLeft, - y + contentOffsetTop, - element.scrollWidth, - element.scrollHeight - ); + return { + x: x + contentOffsetLeft, + y: y + contentOffsetTop, + width: element.scrollWidth, + height: element.scrollHeight, + }; }; -export const rectRelativeTo = ( - rect: {x: number; y: number; width: number; height: number}, - pos = {x: 0, y: 0} -) => { - return Rect(rect.x - pos.x, rect.y - pos.y, rect.width, rect.height); +export const rectRelativeTo = (rect: Rect, pos = {x: 0, y: 0}): Rect => { + return { + x: rect.x - pos.x, + y: rect.y - pos.y, + width: rect.width, + height: rect.height, + }; }; -export const rectOfElement = (element: HTMLElement) => { +export const rectOfElement = (element: HTMLElement): Rect => { const {x, y} = getOffsetOfElement(element); - return Rect(x, y, element.offsetWidth, element.offsetHeight); + return { + x, + y, + width: element.offsetWidth, + height: element.offsetHeight, + }; }; export const clamp = (value: number, min: number, max: number): number => { From bb662a92b2f78823c0cb5eddb2cce618ed11dcdb Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:10:51 -0400 Subject: [PATCH 07/25] throw errors on unexpected ViewHandleType type --- .../app/components/events/interfaces/spans/dragManager.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx index 4c870b0f7c11b1..41c30cbfb35999 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/dragManager.tsx @@ -132,7 +132,7 @@ class DragManager extends React.Component { break; } default: { - // this should never occur + throw Error('this.state.currentDraggingHandle is undefined'); } } }; @@ -184,7 +184,7 @@ class DragManager extends React.Component { break; } default: { - // unreachable + throw Error('this.state.currentDraggingHandle is undefined'); } } From 31f894193715368007815ca25450978afb362fe4 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:11:52 -0400 Subject: [PATCH 08/25] root_rect => rootRect --- .../components/events/interfaces/spans/minimap.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index 4b4e5afed855cd..c1394a77018f67 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -45,10 +45,10 @@ class Minimap extends React.Component { return; } - const root_rect = rectOfContent(traceViewDOM); + const rootRect = rectOfContent(traceViewDOM); - const scaleX = canvas.clientWidth / root_rect.width; - const scaleY = canvas.clientHeight / root_rect.height; + const scaleX = canvas.clientWidth / rootRect.width; + const scaleY = canvas.clientHeight / rootRect.height; // https://www.html5rocks.com/en/tutorials/canvas/hidpi/ // we consider the devicePixelRatio (dpr) factor so that the canvas looks decent on hidpi screens @@ -65,7 +65,7 @@ class Minimap extends React.Component { canvas.style.height = `${height}px`; }; - resize_canvas(root_rect.width * scaleX, root_rect.height * scaleY); + resize_canvas(rootRect.width * scaleX, rootRect.height * scaleY); canvasContext.setTransform(1, 0, 0, 1, 0, 0); canvasContext.clearRect(0, 0, canvas.width, canvas.height); @@ -91,14 +91,14 @@ class Minimap extends React.Component { // draw background - drawRect(rectRelativeTo(root_rect, root_rect), back); + drawRect(rectRelativeTo(rootRect, rootRect), back); // draw the spans Array.from(traceViewDOM.querySelectorAll('[data-span="true"]')).forEach( el => { const backgroundColor = window.getComputedStyle(el).backgroundColor || black(10); - drawRect(rectRelativeTo(rectOfElement(el), root_rect), backgroundColor); + drawRect(rectRelativeTo(rectOfElement(el), rootRect), backgroundColor); } ); }; From fff591364622e4e8d7fe034124a972d1de130070 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:13:02 -0400 Subject: [PATCH 09/25] useless use of React.Fragment. kinda like useless use of cat. --- .../events/interfaces/spans/minimap.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index c1394a77018f67..3d049df6a73300 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -124,17 +124,15 @@ class Minimap extends React.Component { ); return ( - - - + ); }; From 86abd80a77fb63460d85d91b00c85d14d68887f8 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:14:40 -0400 Subject: [PATCH 10/25] resize_canvas => resizeCanvas --- .../sentry/app/components/events/interfaces/spans/minimap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index 3d049df6a73300..ba19c63ed7511e 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -55,7 +55,7 @@ class Minimap extends React.Component { // such as retina macbooks const devicePixelRatio = window.devicePixelRatio || 1; - const resize_canvas = (width: number, height: number) => { + const resizeCanvas = (width: number, height: number) => { // scale the canvas up by the dpr factor canvas.width = width * devicePixelRatio; canvas.height = height * devicePixelRatio; @@ -65,7 +65,7 @@ class Minimap extends React.Component { canvas.style.height = `${height}px`; }; - resize_canvas(rootRect.width * scaleX, rootRect.height * scaleY); + resizeCanvas(rootRect.width * scaleX, rootRect.height * scaleY); canvasContext.setTransform(1, 0, 0, 1, 0, 0); canvasContext.clearRect(0, 0, canvas.width, canvas.height); From 36e2976830d28a90ca60c87cafe0ca068f5413da Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:20:05 -0400 Subject: [PATCH 11/25] camels. --- .../events/interfaces/spans/spanDetail.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx index b12d5d9ca7e180..4e857fa3e7e832 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx @@ -16,10 +16,10 @@ type PropTypes = { const SpanDetail = (props: PropTypes) => { const {span} = props; - const start_timestamp: number = span.start_timestamp; - const end_timestamp: number = span.timestamp; + const startTimestamp: number = span.start_timestamp; + const endTimestamp: number = span.timestamp; - const duration = (end_timestamp - start_timestamp) * 1000; + const duration = (endTimestamp - startTimestamp) * 1000; const durationString = `${duration.toFixed(3)} ms`; return ( @@ -38,14 +38,14 @@ const SpanDetail = (props: PropTypes) => { {_.get(span, 'description', '')} - - {` (${start_timestamp})`} + + {` (${startTimestamp})`} - - {` (${end_timestamp})`} + + {` (${endTimestamp})`} {durationString} From 21379b4d07858faa9f461858576b8dc2f64b0ecd Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:22:09 -0400 Subject: [PATCH 12/25] rename --- .../events/interfaces/spans/{span_tree.tsx => spanTree.tsx} | 0 .../app/components/events/interfaces/spans/transactionView.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/sentry/static/sentry/app/components/events/interfaces/spans/{span_tree.tsx => spanTree.tsx} (100%) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx similarity index 100% rename from src/sentry/static/sentry/app/components/events/interfaces/spans/span_tree.tsx rename to src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx index 0b339acb9daa43..715b03f6cb6891 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import DragManager, {DragManagerChildrenProps} from './dragManager'; -import SpanTree from './span_tree'; +import SpanTree from './spanTree'; import {SentryEvent} from './types'; import TraceViewMinimap from './minimap'; From aa97eca59b48357b9719331cd7fa889a99354422 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:36:10 -0400 Subject: [PATCH 13/25] move styles --- .../app/components/events/interfaces/spans/spanTree.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index 97cf45cf228906..386825b5d19124 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -481,11 +481,6 @@ const SpanRow = styled('div')` } } - & > [data-span='true'] { - transition: border-color 0.15s ease-in-out; - border: 1px solid rgba(0, 0, 0, 0); - } - &:hover { background-color: rgba(189, 180, 199, 0.1); @@ -630,6 +625,9 @@ const SpanBar = styled('div')` user-select: none; padding: 4px; + + transition: border-color 0.15s ease-in-out; + border: 1px solid rgba(0, 0, 0, 0); `; const ChevronOpen = props => ( From 80289e5ddc7c6db15919956b4de916fa0f16b196 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:38:54 -0400 Subject: [PATCH 14/25] decrease lodash import scope --- .../events/interfaces/spans/spanDetail.tsx | 8 ++++---- .../events/interfaces/spans/spanTree.tsx | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx index 4e857fa3e7e832..1f44cc61cd688a 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'react-emotion'; -import _ from 'lodash'; +import {get, map} from 'lodash'; import DateTime from 'app/components/dateTime'; import Pills from 'app/components/pills'; @@ -35,7 +35,7 @@ const SpanDetail = (props: PropTypes) => { {span.span_id} {span.trace_id} {span.parent_span_id || ''} - {_.get(span, 'description', '')} + {get(span, 'description', '')} @@ -54,7 +54,7 @@ const SpanDetail = (props: PropTypes) => { {String(!!span.same_process_as_parent)} - {_.map(_.get(span, 'data', {}), (value, key) => { + {map(get(span, 'data', {}), (value, key) => { return ( {JSON.stringify(value, null, 4) || ''} @@ -102,7 +102,7 @@ const Row = ({ }; const Tags = ({span}: {span: SpanType}) => { - const tags: {[tag_name: string]: string} | undefined = _.get(span, 'tags'); + const tags: {[tag_name: string]: string} | undefined = get(span, 'tags'); if (!tags) { return null; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index 386825b5d19124..5b7d4d245c2fee 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'react-emotion'; -import _ from 'lodash'; +import {get, set, isNumber, forEach} from 'lodash'; import space from 'app/styles/space'; import Count from 'app/components/count'; @@ -57,7 +57,7 @@ class SpanTree extends React.Component { }): RenderedSpanTree => { const spanBarColour: string = pickSpanBarColour(); - const spanChildren: SpanType[] = _.get(lookup, spanID, []); + const spanChildren: SpanType[] = get(lookup, spanID, []); const start_timestamp: number = span.start_timestamp; const end_timestamp: number = span.timestamp; @@ -132,7 +132,7 @@ class SpanTree extends React.Component { renderRootSpan = (): JSX.Element | null => { const {event, dragProps} = this.props; - const trace: TraceContextType | undefined = _.get(event, 'contexts.trace'); + const trace: TraceContextType | undefined = get(event, 'contexts.trace'); if (!trace) { return null; @@ -197,7 +197,7 @@ class SpanTree extends React.Component { (entry: {type: string}) => entry.type === 'spans' ); - const spans: SpanType[] = _.get(spanEntry, 'data', []); + const spans: SpanType[] = get(spanEntry, 'data', []); if (!spanEntry || spans.length <= 0) { return { @@ -226,11 +226,11 @@ class SpanTree extends React.Component { return acc; } - const spanChildren: SpanType[] = _.get(acc.lookup, span.parent_span_id!, []); + const spanChildren: SpanType[] = get(acc.lookup, span.parent_span_id!, []); spanChildren.push(span); - _.set(acc.lookup, span.parent_span_id!, spanChildren); + set(acc.lookup, span.parent_span_id!, spanChildren); if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) { acc.traceStartTimestamp = span.start_timestamp; @@ -238,7 +238,7 @@ class SpanTree extends React.Component { // establish trace end timestamp - const hasEndTimestamp = _.isNumber(span.timestamp); + const hasEndTimestamp = isNumber(span.timestamp); if (!acc.traceEndTimestamp) { if (hasEndTimestamp) { @@ -264,7 +264,7 @@ class SpanTree extends React.Component { // sort span children by their start timestamps in ascending order - _.forEach(reduced.lookup, spanChildren => { + forEach(reduced.lookup, spanChildren => { spanChildren.sort((firstSpan, secondSpan) => { if (firstSpan.start_timestamp < secondSpan.start_timestamp) { return -1; @@ -381,7 +381,7 @@ class Span extends React.Component { const {span, treeDepth} = this.props; const op = span.op ? {`${span.op} \u2014 `} : ''; - const description = _.get(span, 'description', span.span_id); + const description = get(span, 'description', span.span_id); const MARGIN_LEFT = 8; const TOGGLE_BUTTON_MARGIN_RIGHT = 8; From c78ac7abfdca23e21f6171a068d087f6c5d1f996 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 20:47:22 -0400 Subject: [PATCH 15/25] camels --- .../events/interfaces/spans/spanTree.tsx | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index 5b7d4d245c2fee..f79ddc3a0a0f1f 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -59,12 +59,9 @@ class SpanTree extends React.Component { const spanChildren: SpanType[] = get(lookup, spanID, []); - const start_timestamp: number = span.start_timestamp; - const end_timestamp: number = span.timestamp; - const bounds = generateBounds({ - startTimestamp: start_timestamp, - endTimestamp: end_timestamp, + startTimestamp: span.start_timestamp, + endTimestamp: span.timestamp, }); const isCurrentSpanHidden = bounds.end <= 0; @@ -164,13 +161,6 @@ class SpanTree extends React.Component { return next_colour; }; - // TODO: remove later - // const traceEndTimestamp = _.isNumber(parsedTrace.traceEndTimestamp) - // ? parsedTrace.traceStartTimestamp == parsedTrace.traceEndTimestamp - // ? parsedTrace.traceStartTimestamp + 0.05 - // : parsedTrace.traceEndTimestamp - // : parsedTrace.traceStartTimestamp + 0.05; - const generateBounds = boundsGenerator({ traceStartTimestamp: parsedTrace.traceStartTimestamp, traceEndTimestamp: parsedTrace.traceEndTimestamp, @@ -339,12 +329,9 @@ class Span extends React.Component { getBounds = () => { const {span, generateBounds} = this.props; - const start_timestamp: number = span.start_timestamp; - const end_timestamp: number = span.timestamp; - return generateBounds({ - startTimestamp: start_timestamp, - endTimestamp: end_timestamp, + startTimestamp: span.start_timestamp, + endTimestamp: span.timestamp, }); }; @@ -420,10 +407,10 @@ class Span extends React.Component { render() { const {span, spanBarColour} = this.props; - const start_timestamp: number = span.start_timestamp; - const end_timestamp: number = span.timestamp; + const startTimestamp: number = span.start_timestamp; + const endTimestamp: number = span.timestamp; - const duration = (end_timestamp - start_timestamp) * 1000; + const duration = (endTimestamp - startTimestamp) * 1000; const durationString = `${duration.toFixed(3)} ms`; const bounds = this.getBounds(); From a70298daa57f4e6c3a3f369dfaa279e400814e9d Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Tue, 23 Jul 2019 21:02:52 -0400 Subject: [PATCH 16/25] display # of hidden spans below --- .../events/interfaces/spans/spanTree.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index f79ddc3a0a0f1f..5e092b7fdcc46b 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -25,7 +25,7 @@ type TraceContextType = { type LookupType = {[span_id: string]: SpanType[]}; type RenderedSpanTree = { - spanTree: JSX.Element; + spanTree: JSX.Element | null; numOfHiddenSpansAbove: number; }; @@ -64,7 +64,7 @@ class SpanTree extends React.Component { endTimestamp: span.timestamp, }); - const isCurrentSpanHidden = bounds.end <= 0; + const isCurrentSpanHidden = bounds.end <= 0 || bounds.start >= 1; type AccType = { renderedSpanChildren: Array; @@ -126,13 +126,16 @@ class SpanTree extends React.Component { }; }; - renderRootSpan = (): JSX.Element | null => { + renderRootSpan = (): RenderedSpanTree => { const {event, dragProps} = this.props; const trace: TraceContextType | undefined = get(event, 'contexts.trace'); if (!trace) { - return null; + return { + spanTree: null, + numOfHiddenSpansAbove: 0, + }; } const parsedTrace = this.parseTrace(); @@ -177,7 +180,7 @@ class SpanTree extends React.Component { lookup: parsedTrace.lookup, generateBounds, pickSpanBarColour, - }).spanTree; + }); }; parseTrace = () => { @@ -272,9 +275,19 @@ class SpanTree extends React.Component { }; render() { + const {spanTree, numOfHiddenSpansAbove} = this.renderRootSpan(); + + const hiddenSpansMessage = + numOfHiddenSpansAbove > 0 ? ( + + Number of hidden spans: {numOfHiddenSpansAbove} + + ) : null; + return ( - {this.renderRootSpan()} + {spanTree} + {hiddenSpansMessage} ); } From 2e44967cb257d042a33ed88124c98bfaf3cea5d9 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 02:39:01 -0400 Subject: [PATCH 17/25] init time axis --- .../events/interfaces/spans/minimap.tsx | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index ba19c63ed7511e..52a646b53e169f 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -5,6 +5,7 @@ import {rectOfContent, clamp, rectRelativeTo, rectOfElement, toPercent} from './ import {DragManagerChildrenProps} from './dragManager'; const MINIMAP_HEIGHT = 75; +const TIME_AXIS_HEIGHT = 30; type MinimapProps = { traceViewRef: React.RefObject; @@ -251,48 +252,66 @@ class Minimap extends React.Component { render() { return ( - - -
{ - this.setState({ - showCursorGuide: true, - mousePageX: event.pageX, - }); - }} - onMouseLeave={() => { - this.setState({showCursorGuide: false, mousePageX: void 0}); - }} - onMouseMove={event => { - this.setState({ - showCursorGuide: true, - mousePageX: event.pageX, - }); - }} - > - - {this.renderFog(this.props.dragProps)} - {this.renderCursorGuide()} - {this.renderViewHandles(this.props.dragProps)} - -
-
+ + + +
{ + this.setState({ + showCursorGuide: true, + mousePageX: event.pageX, + }); + }} + onMouseLeave={() => { + this.setState({showCursorGuide: false, mousePageX: void 0}); + }} + onMouseMove={event => { + this.setState({ + showCursorGuide: true, + mousePageX: event.pageX, + }); + }} + > + + {this.renderFog(this.props.dragProps)} + {this.renderCursorGuide()} + {this.renderViewHandles(this.props.dragProps)} + + hello +
+
+
); } } -const Container = styled('div')` +const TimeAxis = styled('div')` + width: 100%; + position: absolute; + left: 0; + top: ${MINIMAP_HEIGHT}px; + + border-top: 1px solid #d1cad8; + + height: ${TIME_AXIS_HEIGHT}px; + background-color: #faf9fb; +`; + +const MinimapContainer = styled('div')` width: 100%; position: relative; left: 0; border-bottom: 1px solid #d1cad8; + + height: ${MINIMAP_HEIGHT + TIME_AXIS_HEIGHT + 1}px; `; const MinimapBackground = styled('canvas')` From 8d5ee442b0d659a084f8d0130c1aefe5344b2e99 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 03:02:59 -0400 Subject: [PATCH 18/25] parse trace earlier --- .../events/interfaces/spans/spanTree.tsx | 139 ++---------------- .../interfaces/spans/transactionView.tsx | 129 +++++++++++++++- .../events/interfaces/spans/types.tsx | 14 +- 3 files changed, 152 insertions(+), 130 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index 5e092b7fdcc46b..bbc97096fc0112 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -1,13 +1,12 @@ import React from 'react'; import styled from 'react-emotion'; -import {get, set, isNumber, forEach} from 'lodash'; +import {get} from 'lodash'; import space from 'app/styles/space'; import Count from 'app/components/count'; -import {SpanType, SpanEntry, SentryEvent} from './types'; +import {SpanType, SpanChildrenLookupType, ParsedTraceType} from './types'; import { - isValidSpanID, toPercent, boundsGenerator, SpanBoundsType, @@ -16,14 +15,6 @@ import { import {DragManagerChildrenProps} from './dragManager'; import SpanDetail from './spanDetail'; -type TraceContextType = { - type: 'trace'; - span_id: string; - trace_id: string; -}; - -type LookupType = {[span_id: string]: SpanType[]}; - type RenderedSpanTree = { spanTree: JSX.Element | null; numOfHiddenSpansAbove: number; @@ -31,7 +22,7 @@ type RenderedSpanTree = { type SpanTreeProps = { traceViewRef: React.RefObject; - event: SentryEvent; + trace: ParsedTraceType; dragProps: DragManagerChildrenProps; }; @@ -51,7 +42,7 @@ class SpanTree extends React.Component { spanID: string; traceID: string; span: Readonly; - lookup: Readonly; + lookup: Readonly; generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; pickSpanBarColour: () => string; }): RenderedSpanTree => { @@ -127,26 +118,15 @@ class SpanTree extends React.Component { }; renderRootSpan = (): RenderedSpanTree => { - const {event, dragProps} = this.props; - - const trace: TraceContextType | undefined = get(event, 'contexts.trace'); - - if (!trace) { - return { - spanTree: null, - numOfHiddenSpansAbove: 0, - }; - } - - const parsedTrace = this.parseTrace(); + const {dragProps, trace} = this.props; // TODO: ideally this should be provided const rootSpan: SpanType = { - trace_id: trace.trace_id, + trace_id: trace.traceID, parent_span_id: void 0, - span_id: trace.span_id, - start_timestamp: parsedTrace.traceStartTimestamp, - timestamp: parsedTrace.traceEndTimestamp, + span_id: trace.rootSpanID, + start_timestamp: trace.traceStartTimestamp, + timestamp: trace.traceEndTimestamp, same_process_as_parent: true, op: 'transaction', data: {}, @@ -165,8 +145,8 @@ class SpanTree extends React.Component { }; const generateBounds = boundsGenerator({ - traceStartTimestamp: parsedTrace.traceStartTimestamp, - traceEndTimestamp: parsedTrace.traceEndTimestamp, + traceStartTimestamp: trace.traceStartTimestamp, + traceEndTimestamp: trace.traceEndTimestamp, viewStart: dragProps.viewWindowStart, viewEnd: dragProps.viewWindowEnd, }); @@ -175,105 +155,14 @@ class SpanTree extends React.Component { treeDepth: 0, numOfHiddenSpansAbove: 0, span: rootSpan, - spanID: trace.span_id, - traceID: trace.trace_id, - lookup: parsedTrace.lookup, + spanID: rootSpan.span_id, + traceID: rootSpan.trace_id, + lookup: trace.lookup, generateBounds, pickSpanBarColour, }); }; - parseTrace = () => { - const {event} = this.props; - - const spanEntry: SpanEntry | undefined = event.entries.find( - (entry: {type: string}) => entry.type === 'spans' - ); - - const spans: SpanType[] = get(spanEntry, 'data', []); - - if (!spanEntry || spans.length <= 0) { - return { - lookup: {}, - traceStartTimestamp: 0, - traceEndTimestamp: 0, - }; - } - - // we reduce spans to become an object mapping span ids to their children - - type ReducedType = { - lookup: LookupType; - traceStartTimestamp: number; - traceEndTimestamp: number; - }; - - const init: ReducedType = { - lookup: {}, - traceStartTimestamp: spans[0].start_timestamp, - traceEndTimestamp: 0, - }; - - const reduced: ReducedType = spans.reduce((acc, span) => { - if (!isValidSpanID(span.parent_span_id)) { - return acc; - } - - const spanChildren: SpanType[] = get(acc.lookup, span.parent_span_id!, []); - - spanChildren.push(span); - - set(acc.lookup, span.parent_span_id!, spanChildren); - - if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) { - acc.traceStartTimestamp = span.start_timestamp; - } - - // establish trace end timestamp - - const hasEndTimestamp = isNumber(span.timestamp); - - if (!acc.traceEndTimestamp) { - if (hasEndTimestamp) { - acc.traceEndTimestamp = span.timestamp; - return acc; - } - - acc.traceEndTimestamp = span.start_timestamp; - return acc; - } - - if (hasEndTimestamp && span.timestamp! > acc.traceEndTimestamp) { - acc.traceEndTimestamp = span.timestamp; - return acc; - } - - if (span.start_timestamp > acc.traceEndTimestamp) { - acc.traceEndTimestamp = span.start_timestamp; - } - - return acc; - }, init); - - // sort span children by their start timestamps in ascending order - - forEach(reduced.lookup, spanChildren => { - spanChildren.sort((firstSpan, secondSpan) => { - if (firstSpan.start_timestamp < secondSpan.start_timestamp) { - return -1; - } - - if (firstSpan.start_timestamp === secondSpan.start_timestamp) { - return 0; - } - - return 1; - }); - }); - - return reduced; - }; - render() { const {spanTree, numOfHiddenSpansAbove} = this.renderRootSpan(); diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx index 715b03f6cb6891..3fadb07a3cd891 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx @@ -1,11 +1,21 @@ import React from 'react'; +import {get, set, isNumber, forEach} from 'lodash'; + +import {t} from 'app/locale'; +import EmptyStateWarning from 'app/components/emptyStateWarning'; import DragManager, {DragManagerChildrenProps} from './dragManager'; import SpanTree from './spanTree'; -import {SentryEvent} from './types'; - +import {SpanType, SpanEntry, SentryEvent, ParsedTraceType} from './types'; +import {isValidSpanID} from './utils'; import TraceViewMinimap from './minimap'; +type TraceContextType = { + type: 'trace'; + span_id: string; + trace_id: string; +}; + type TransactionViewProps = { event: SentryEvent; }; @@ -48,7 +58,120 @@ class TransactionView extends React.Component< ); }; + getTraceContext = () => { + const {event} = this.props; + + const traceContext: TraceContextType | undefined = get(event, 'contexts.trace'); + + return traceContext; + }; + + parseTrace = (): ParsedTraceType => { + const {event} = this.props; + + console.log('event', event); + + const spanEntry: SpanEntry | undefined = event.entries.find( + (entry: {type: string}) => entry.type === 'spans' + ); + + const spans: Array = get(spanEntry, 'data', []); + + const traceContext = this.getTraceContext(); + const traceID = (traceContext && traceContext.trace_id) || ''; + const rootSpanID = (traceContext && traceContext.span_id) || ''; + + if (!spanEntry || spans.length <= 0) { + return { + lookup: {}, + traceStartTimestamp: 0, + traceEndTimestamp: 0, + traceID, + rootSpanID, + }; + } + + // we reduce spans to become an object mapping span ids to their children + + const init: ParsedTraceType = { + lookup: {}, + traceStartTimestamp: spans[0].start_timestamp, + traceEndTimestamp: 0, + traceID, + rootSpanID, + }; + + const reduced: ParsedTraceType = spans.reduce((acc, span) => { + if (!isValidSpanID(span.parent_span_id)) { + return acc; + } + + const spanChildren: Array = get(acc.lookup, span.parent_span_id!, []); + + spanChildren.push(span); + + set(acc.lookup, span.parent_span_id!, spanChildren); + + if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) { + acc.traceStartTimestamp = span.start_timestamp; + } + + // establish trace end timestamp + + const hasEndTimestamp = isNumber(span.timestamp); + + if (!acc.traceEndTimestamp) { + if (hasEndTimestamp) { + acc.traceEndTimestamp = span.timestamp; + return acc; + } + + acc.traceEndTimestamp = span.start_timestamp; + return acc; + } + + if (hasEndTimestamp && span.timestamp! > acc.traceEndTimestamp) { + acc.traceEndTimestamp = span.timestamp; + return acc; + } + + if (span.start_timestamp > acc.traceEndTimestamp) { + acc.traceEndTimestamp = span.start_timestamp; + } + + return acc; + }, init); + + // sort span children by their start timestamps in ascending order + + forEach(reduced.lookup, spanChildren => { + spanChildren.sort((firstSpan, secondSpan) => { + if (firstSpan.start_timestamp < secondSpan.start_timestamp) { + return -1; + } + + if (firstSpan.start_timestamp === secondSpan.start_timestamp) { + return 0; + } + + return 1; + }); + }); + + return reduced; + }; + render() { + if (!this.getTraceContext()) { + return ( + +

{t('There is no trace for this transaction')}

+
+ ); + } + + const parsedTrace = this.parseTrace(); + return ( {(dragProps: DragManagerChildrenProps) => { @@ -57,7 +180,7 @@ class TransactionView extends React.Component< {this.renderMinimap(dragProps)} diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx index 6ea5d8bb9f23d7..7a5eb3e828a86d 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx @@ -12,9 +12,19 @@ export type SpanType = { export type SpanEntry = { type: 'spans'; - data: SpanType[]; + data: Array; }; export type SentryEvent = { - entries: SpanEntry[]; + entries: Array; +}; + +export type SpanChildrenLookupType = {[span_id: string]: Array}; + +export type ParsedTraceType = { + lookup: SpanChildrenLookupType; + traceID: string; + rootSpanID: string; + traceStartTimestamp: number; + traceEndTimestamp: number; }; From 4b56b11c953109e35abd8d5d536315955fdb64a5 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 03:25:05 -0400 Subject: [PATCH 19/25] things --- .../events/interfaces/spans/minimap.tsx | 21 +++++++++++++++++-- .../events/interfaces/spans/spanTree.tsx | 5 +++-- .../interfaces/spans/transactionView.tsx | 7 +++---- .../events/interfaces/spans/utils.tsx | 7 +++++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index 52a646b53e169f..c1baf488055546 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -1,8 +1,16 @@ import React from 'react'; import styled from 'react-emotion'; -import {rectOfContent, clamp, rectRelativeTo, rectOfElement, toPercent} from './utils'; +import { + rectOfContent, + clamp, + rectRelativeTo, + rectOfElement, + toPercent, + getHumanDuration, +} from './utils'; import {DragManagerChildrenProps} from './dragManager'; +import {ParsedTraceType} from './types'; const MINIMAP_HEIGHT = 75; const TIME_AXIS_HEIGHT = 30; @@ -11,6 +19,7 @@ type MinimapProps = { traceViewRef: React.RefObject; minimapInteractiveRef: React.RefObject; dragProps: DragManagerChildrenProps; + trace: ParsedTraceType; }; type MinimapState = { @@ -250,6 +259,14 @@ class Minimap extends React.Component { ); }; + renderTimeAxis = () => { + const {trace} = this.props; + + const duration = Math.abs(trace.traceEndTimestamp - trace.traceStartTimestamp); + + return hello: {getHumanDuration(duration)}; + }; + render() { return ( @@ -285,7 +302,7 @@ class Minimap extends React.Component { {this.renderCursorGuide()} {this.renderViewHandles(this.props.dragProps)} - hello + {this.renderTimeAxis()} diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index bbc97096fc0112..bdbecba4beed30 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -11,6 +11,7 @@ import { boundsGenerator, SpanBoundsType, SpanGeneratedBoundsType, + getHumanDuration, } from './utils'; import {DragManagerChildrenProps} from './dragManager'; import SpanDetail from './spanDetail'; @@ -312,8 +313,8 @@ class Span extends React.Component { const startTimestamp: number = span.start_timestamp; const endTimestamp: number = span.timestamp; - const duration = (endTimestamp - startTimestamp) * 1000; - const durationString = `${duration.toFixed(3)} ms`; + const duration = Math.abs(endTimestamp - startTimestamp); + const durationString = getHumanDuration(duration); const bounds = this.getBounds(); diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx index 3fadb07a3cd891..c81fb20d63aee4 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx @@ -44,7 +44,7 @@ class TransactionView extends React.Component< } } - renderMinimap = (dragProps: DragManagerChildrenProps) => { + renderMinimap = (dragProps: DragManagerChildrenProps, parsedTrace: ParsedTraceType) => { if (!this.state.renderMinimap) { return null; } @@ -54,6 +54,7 @@ class TransactionView extends React.Component< traceViewRef={this.traceViewRef} minimapInteractiveRef={this.minimapInteractiveRef} dragProps={dragProps} + trace={parsedTrace} /> ); }; @@ -69,8 +70,6 @@ class TransactionView extends React.Component< parseTrace = (): ParsedTraceType => { const {event} = this.props; - console.log('event', event); - const spanEntry: SpanEntry | undefined = event.entries.find( (entry: {type: string}) => entry.type === 'spans' ); @@ -177,7 +176,7 @@ class TransactionView extends React.Component< {(dragProps: DragManagerChildrenProps) => { return ( - {this.renderMinimap(dragProps)} + {this.renderMinimap(dragProps, parsedTrace)} { + // note: duration is assumed to be in seconds + + const durationMS = duration * 1000; + return `${durationMS.toFixed(3)} ms`; +}; From de3f7c406f7cc0a04ea6884fc3d034f4c8da777d Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 04:19:10 -0400 Subject: [PATCH 20/25] more timeaxis --- .../events/interfaces/spans/minimap.tsx | 177 +++++++++++++++++- 1 file changed, 174 insertions(+), 3 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index c1baf488055546..bd1421bd7f9ac3 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -1,6 +1,8 @@ import React from 'react'; import styled from 'react-emotion'; +import space from 'app/styles/space'; + import { rectOfContent, clamp, @@ -113,7 +115,7 @@ class Minimap extends React.Component { ); }; - renderCursorGuide = () => { + renderMinimapCursorGuide = () => { if (!this.state.showCursorGuide || !this.state.mousePageX) { return null; } @@ -264,7 +266,108 @@ class Minimap extends React.Component { const duration = Math.abs(trace.traceEndTimestamp - trace.traceStartTimestamp); - return hello: {getHumanDuration(duration)}; + const firstTick = ( + + ); + + const secondTick = ( + + ); + + const thirdTick = ( + + ); + + const fourthTick = ( + + ); + + const lastTick = ( + + ); + + return ( + + {firstTick} + {secondTick} + {thirdTick} + {fourthTick} + {lastTick} + + {this.renderTimeAxisCursorGuide()} + + + ); + }; + + renderTimeAxisCursorGuide = () => { + if (!this.state.showCursorGuide || !this.state.mousePageX) { + return null; + } + + const minimapCanvas = this.props.minimapInteractiveRef.current; + + if (!minimapCanvas) { + return null; + } + + const rect = rectOfContent(minimapCanvas); + + // clamp mouseLeft to be within [0, 100] + const mouseLeft = clamp( + ((this.state.mousePageX - rect.x) / rect.width) * 100, + 0, + 100 + ); + + return ( + + ); }; render() { @@ -299,7 +402,7 @@ class Minimap extends React.Component { > {this.renderFog(this.props.dragProps)} - {this.renderCursorGuide()} + {this.renderMinimapCursorGuide()} {this.renderViewHandles(this.props.dragProps)} {this.renderTimeAxis()} @@ -320,8 +423,76 @@ const TimeAxis = styled('div')` height: ${TIME_AXIS_HEIGHT}px; background-color: #faf9fb; + + color: #9585a3; + font-size: 10px; + font-weight: 500; +`; + +const TickLabelContainer = styled('div')` + height: ${TIME_AXIS_HEIGHT}px; + + position: absolute; + top: 0; + + user-select: none; `; +const TickText = styled('span')` + ${({align}: {align: TickAlignment}) => { + switch (align) { + case TickAlignment.Center: { + return 'transform: translateX(-50%)'; + } + case TickAlignment.Left: { + return null; + } + + case TickAlignment.Right: { + return 'transform: translateX(-100%)'; + } + } + }}; +`; + +enum TickAlignment { + Left, + Right, + Center, +} + +const TickMarker = styled('div')` + width: 1px; + height: 5px; + + background-color: #d1cad8; + + position: absolute; + top: 0; + left: 0; +`; + +const TickLabel = (props: { + style: React.CSSProperties; + hideTickMarker?: boolean; + align?: TickAlignment; + duration: number; +}) => { + const {style, duration, hideTickMarker = false, align = TickAlignment.Center} = props; + + return ( + + {hideTickMarker ? null : } + + {getHumanDuration(duration)} + + + ); +}; + const MinimapContainer = styled('div')` width: 100%; position: relative; From 6f84d95d066ba4274ce2fc8b2d67996dcf95304e Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 09:23:19 -0400 Subject: [PATCH 21/25] improve ticks --- .../events/interfaces/spans/minimap.tsx | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index bd1421bd7f9ac3..2a5037b3f41406 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -261,6 +261,38 @@ class Minimap extends React.Component { ); }; + renderDurationGuide = () => { + if (!this.state.showCursorGuide || !this.state.mousePageX) { + return null; + } + + const minimapCanvas = this.props.minimapInteractiveRef.current; + + if (!minimapCanvas) { + return null; + } + + const rect = rectOfContent(minimapCanvas); + + // clamp mouseLeft to be within [0, 1] + const mouseLeft = clamp((this.state.mousePageX - rect.x) / rect.width, 0, 1); + + const {trace} = this.props; + + const duration = + mouseLeft * Math.abs(trace.traceEndTimestamp - trace.traceStartTimestamp); + + const style = {top: 0, left: `calc(${mouseLeft * 100}% + 4px)`}; + + const alignLeft = (1 - mouseLeft) * rect.width <= 100; + + return ( + + {getHumanDuration(duration)} + + ); + }; + renderTimeAxis = () => { const {trace} = this.props; @@ -333,6 +365,7 @@ class Minimap extends React.Component { > {this.renderTimeAxisCursorGuide()} + {this.renderDurationGuide()} ); }; @@ -439,6 +472,12 @@ const TickLabelContainer = styled('div')` `; const TickText = styled('span')` + line-height: 1; + + position: absolute; + bottom: 8px; + white-space: nowrap; + ${({align}: {align: TickAlignment}) => { switch (align) { case TickAlignment.Center: { @@ -470,6 +509,8 @@ const TickMarker = styled('div')` position: absolute; top: 0; left: 0; + + transform: translateX(-50%); `; const TickLabel = (props: { @@ -483,16 +524,38 @@ const TickLabel = (props: { return ( {hideTickMarker ? null : } - - {getHumanDuration(duration)} - + {getHumanDuration(duration)} ); }; +const DurationGuideBox = styled('div')` + position: absolute; + + background-color: rgba(255, 255, 255, 1); + padding: 4px; + + border-radius: 3px; + border: 1px solid rgba(0, 0, 0, 0.1); + + height: 16px; + + line-height: 1; + vertical-align: middle; + + transform: translateY(50%); + + white-space: nowrap; + + ${({alignLeft}: {alignLeft: boolean}) => { + if (!alignLeft) { + return null; + } + + return `transform: translateY(50%) translateX(-100%) translateX(-8px);`; + }}; +`; + const MinimapContainer = styled('div')` width: 100%; position: relative; From f24a66242f885854fa233852e34370edf6617654 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 10:23:10 -0400 Subject: [PATCH 22/25] nit --- .../sentry/app/components/events/interfaces/spans/spanTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx index bdbecba4beed30..25a1b51391d23a 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanTree.tsx @@ -49,7 +49,7 @@ class SpanTree extends React.Component { }): RenderedSpanTree => { const spanBarColour: string = pickSpanBarColour(); - const spanChildren: SpanType[] = get(lookup, spanID, []); + const spanChildren: Array = get(lookup, spanID, []); const bounds = generateBounds({ startTimestamp: span.start_timestamp, From ce5b6ee28e6d5d0f469603b225c3eb03ddfbeab6 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 10:33:17 -0400 Subject: [PATCH 23/25] use startTimestamp and endTimestamp --- .../events/interfaces/spans/transactionView.tsx | 10 +++++----- .../app/components/events/interfaces/spans/types.tsx | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx index c81fb20d63aee4..5437ab5901e287 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/transactionView.tsx @@ -17,7 +17,7 @@ type TraceContextType = { }; type TransactionViewProps = { - event: SentryEvent; + event: Readonly; }; type TransactionViewState = { @@ -83,8 +83,8 @@ class TransactionView extends React.Component< if (!spanEntry || spans.length <= 0) { return { lookup: {}, - traceStartTimestamp: 0, - traceEndTimestamp: 0, + traceStartTimestamp: event.startTimestamp, + traceEndTimestamp: event.endTimestamp, traceID, rootSpanID, }; @@ -94,8 +94,8 @@ class TransactionView extends React.Component< const init: ParsedTraceType = { lookup: {}, - traceStartTimestamp: spans[0].start_timestamp, - traceEndTimestamp: 0, + traceStartTimestamp: event.startTimestamp, + traceEndTimestamp: event.endTimestamp, traceID, rootSpanID, }; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx index 7a5eb3e828a86d..4a2619f0ec3bce 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx @@ -17,6 +17,8 @@ export type SpanEntry = { export type SentryEvent = { entries: Array; + startTimestamp: number; + endTimestamp: number; }; export type SpanChildrenLookupType = {[span_id: string]: Array}; From 9fa70c0eae4b0b28b474b3bd3a57e9d5ab5b8170 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 11:35:27 -0400 Subject: [PATCH 24/25] overflow visible --- .../sentry/app/components/events/interfaces/spans/minimap.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index 2a5037b3f41406..fbb9807b23274a 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -361,6 +361,7 @@ class Minimap extends React.Component { top: 0, width: '100%', height: `${TIME_AXIS_HEIGHT}px`, + overflow: 'visible', }} > {this.renderTimeAxisCursorGuide()} From a0e6f5c9935cfa280b5a7ed3938dd7c10839de51 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 11:44:57 -0400 Subject: [PATCH 25/25] lint fix --- .../components/events/interfaces/spans/minimap.tsx | 14 ++++++-------- .../components/events/interfaces/spans/types.tsx | 6 ++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx index fbb9807b23274a..bf62f7a6799f90 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/minimap.tsx @@ -12,7 +12,7 @@ import { getHumanDuration, } from './utils'; import {DragManagerChildrenProps} from './dragManager'; -import {ParsedTraceType} from './types'; +import {ParsedTraceType, TickAlignment} from './types'; const MINIMAP_HEIGHT = 75; const TIME_AXIS_HEIGHT = 30; @@ -491,16 +491,14 @@ const TickText = styled('span')` case TickAlignment.Right: { return 'transform: translateX(-100%)'; } + + default: { + throw Error(`Invalid tick alignment: ${align}`); + } } }}; `; -enum TickAlignment { - Left, - Right, - Center, -} - const TickMarker = styled('div')` width: 1px; height: 5px; @@ -553,7 +551,7 @@ const DurationGuideBox = styled('div')` return null; } - return `transform: translateY(50%) translateX(-100%) translateX(-8px);`; + return 'transform: translateY(50%) translateX(-100%) translateX(-8px);'; }}; `; diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx index 4a2619f0ec3bce..a2dc2308903339 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/types.tsx @@ -30,3 +30,9 @@ export type ParsedTraceType = { traceStartTimestamp: number; traceEndTimestamp: number; }; + +export enum TickAlignment { + Left, + Right, + Center, +}