1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042 |
- import 'intersection-observer'; // this is a polyfill
- import {Component, createRef, Fragment} from 'react';
- import styled from '@emotion/styled';
- import {withProfiler} from '@sentry/react';
- import type {Location} from 'history';
- import Count from 'sentry/components/count';
- import {
- FREQUENCY_BOX_WIDTH,
- SpanFrequencyBox,
- } from 'sentry/components/events/interfaces/spans/spanFrequencyBox';
- import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants';
- import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
- import {
- Row,
- RowCell,
- RowCellContainer,
- } from 'sentry/components/performance/waterfall/row';
- import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
- import {
- DividerContainer,
- DividerLine,
- DividerLineGhostContainer,
- ErrorBadge,
- MetricsBadge,
- ProfileBadge,
- } from 'sentry/components/performance/waterfall/rowDivider';
- import {
- RowTitle,
- RowTitleContainer,
- RowTitleContent,
- } from 'sentry/components/performance/waterfall/rowTitle';
- import {
- ConnectorBar,
- TOGGLE_BORDER_BOX,
- TreeConnector,
- TreeToggle,
- TreeToggleContainer,
- TreeToggleIcon,
- } from 'sentry/components/performance/waterfall/treeConnector';
- import {
- getDurationDisplay,
- getHumanDuration,
- lightenBarColor,
- } from 'sentry/components/performance/waterfall/utils';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {EventTransaction} from 'sentry/types/event';
- import {EventOrGroupType} from 'sentry/types/event';
- import {defined} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import {generateEventSlug} from 'sentry/utils/discover/urls';
- import {hasMetricsExperimentalFeature} from 'sentry/utils/metrics/features';
- import toPercent from 'sentry/utils/number/toPercent';
- import type {QuickTraceContextChildrenProps} from 'sentry/utils/performance/quickTrace/quickTraceContext';
- import type {
- QuickTraceEvent,
- TraceErrorOrIssue,
- TraceFull,
- } from 'sentry/utils/performance/quickTrace/types';
- import {isTraceTransaction} from 'sentry/utils/performance/quickTrace/utils';
- import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
- import {decodeScalar} from 'sentry/utils/queryString';
- import {StyledZoomIcon} from 'sentry/views/performance/traceDetails/newTraceDetailsTransactionBar';
- import {ProfileContext} from 'sentry/views/profiling/profilesProvider';
- import * as DividerHandlerManager from './dividerHandlerManager';
- import type {SpanDetailProps} from './newTraceDetailsSpanDetails';
- import {withScrollbarManager} from './scrollbarManager';
- import type {SpanBarProps} from './spanBar';
- import SpanBarCursorGuide from './spanBarCursorGuide';
- import {MeasurementMarker} from './styles';
- import type {AggregateSpanType, GapSpanType, ProcessedSpanType} from './types';
- import {GroupType} from './types';
- import type {SpanGeneratedBoundsType, SpanViewBoundsType, VerticalMark} from './utils';
- import {
- durationlessBrowserOps,
- formatSpanTreeLabel,
- getMeasurementBounds,
- getMeasurements,
- getSpanID,
- getSpanOperation,
- getSpanSubTimings,
- isEventFromBrowserJavaScriptSDK,
- isGapSpan,
- isOrphanSpan,
- isOrphanTreeDepth,
- parseTraceDetailsURLHash,
- shouldLimitAffectedToTiming,
- spanTargetHash,
- transactionTargetHash,
- unwrapTreeDepth,
- } from './utils';
- export const MARGIN_LEFT = 0;
- const SPAN_BAR_HEIGHT = 24;
- export type NewTraceDetailsSpanBarProps = SpanBarProps & {
- location: Location;
- quickTrace: QuickTraceContextChildrenProps;
- measurements?: Map<number, VerticalMark>;
- onRowClick?: (detailKey: SpanDetailProps | undefined) => void;
- };
- type State = {
- isIntersecting: boolean;
- };
- export class NewTraceDetailsSpanBar extends Component<
- NewTraceDetailsSpanBarProps,
- State
- > {
- state = {
- isIntersecting: false,
- };
- componentDidMount() {
- const {didAnchoredSpanMount, markAnchoredSpanIsMounted} = this.props;
- this._mounted = true;
- this.updateHighlightedState();
- this.connectObservers();
- // If span is anchored scroll to span bar and open its detail panel
- if (this.isHighlighted && this.props.onRowClick) {
- this.props.onRowClick(undefined);
- // Needs a little delay after bar is rendered, to achieve
- // scrollto bar functionality for spans that exist much further down the
- // react virtualized list.
- if (!didAnchoredSpanMount()) {
- setTimeout(() => {
- this.scrollIntoView();
- }, 100);
- }
- markAnchoredSpanIsMounted?.();
- }
- if (this.spanRowDOMRef.current) {
- this.props.storeSpanBar(this);
- }
- if (this.spanTitleRef.current) {
- this.spanTitleRef.current.addEventListener('wheel', this.handleWheel, {
- passive: false,
- });
- }
- // On mount, it is necessary to set the left styling of the content here due to the span tree being virtualized.
- // If we rely on the scrollBarManager to set the styling, it happens too late and awkwardly applies an animation.
- if (this.spanContentRef) {
- this.props.addContentSpanBarRef(this.spanContentRef);
- const left = -this.props.getCurrentLeftPos();
- this.spanContentRef.style.transform = `translateX(${left}px)`;
- this.spanContentRef.style.transformOrigin = 'left';
- }
- const {span} = this.props;
- if (isGapSpan(span)) {
- return;
- }
- }
- componentDidUpdate(prevProps: Readonly<NewTraceDetailsSpanBarProps>): void {
- if (this.props.location.query !== prevProps.location.query) {
- this.updateHighlightedState();
- }
- if (this.props.quickTrace !== prevProps.quickTrace) {
- const relatedErrors = this.getRelatedErrors(this.props.quickTrace);
- if (
- this.isHighlighted &&
- this.props.onRowClick &&
- relatedErrors &&
- relatedErrors.length > 0
- ) {
- this.props.onRowClick(undefined);
- }
- }
- }
- componentWillUnmount() {
- this._mounted = false;
- this.disconnectObservers();
- if (this.spanTitleRef.current) {
- this.spanTitleRef.current.removeEventListener('wheel', this.handleWheel);
- }
- const {span} = this.props;
- if (isGapSpan(span)) {
- return;
- }
- this.props.removeContentSpanBarRef(this.spanContentRef);
- }
- spanRowDOMRef = createRef<HTMLDivElement>();
- spanTitleRef = createRef<HTMLDivElement>();
- spanContentRef: HTMLDivElement | null = null;
- intersectionObserver?: IntersectionObserver = void 0;
- zoomLevel: number = 1; // assume initial zoomLevel is 100%
- _mounted: boolean = false;
- hashSpanId: string | undefined = undefined;
- isHighlighted: boolean = false;
- updateHighlightedState = () => {
- const hashValues = parseTraceDetailsURLHash(this.props.location.hash);
- this.hashSpanId = hashValues?.spanId;
- this.isHighlighted = !!(
- !isGapSpan(this.props.span) &&
- this.hashSpanId &&
- this.hashSpanId === this.props.span.span_id
- );
- // TODO Abdullah Khan: Converting the component to a functional component will help us get rid
- // of the forcedUpdate.
- this.forceUpdate();
- };
- handleWheel = (event: WheelEvent) => {
- // https://stackoverflow.com/q/57358640
- // https://github.com/facebook/react/issues/14856
- if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
- return;
- }
- const {onWheel} = this.props;
- onWheel(event.deltaX);
- };
- scrollIntoView = () => {
- const element = this.spanRowDOMRef.current;
- if (!element) {
- return;
- }
- const boundingRect = element.getBoundingClientRect();
- const offset = boundingRect.top + window.scrollY - 40;
- window.scrollTo(0, offset);
- };
- getBounds(bounds?: SpanGeneratedBoundsType): SpanViewBoundsType {
- const {event, span, generateBounds} = this.props;
- bounds ??= generateBounds({
- startTimestamp: span.start_timestamp,
- endTimestamp: span.timestamp,
- });
- const shouldHideSpanWarnings = isEventFromBrowserJavaScriptSDK(event);
- switch (bounds.type) {
- case 'TRACE_TIMESTAMPS_EQUAL': {
- return {
- warning: t('Trace times are equal'),
- left: void 0,
- width: void 0,
- isSpanVisibleInView: bounds.isSpanVisibleInView,
- };
- }
- case 'INVALID_VIEW_WINDOW': {
- return {
- warning: t('Invalid view window'),
- left: void 0,
- width: void 0,
- isSpanVisibleInView: bounds.isSpanVisibleInView,
- };
- }
- case 'TIMESTAMPS_EQUAL': {
- const warning =
- shouldHideSpanWarnings &&
- 'op' in span &&
- span.op &&
- durationlessBrowserOps.includes(span.op)
- ? void 0
- : t('Equal start and end times');
- return {
- warning,
- left: bounds.start,
- width: 0.00001,
- isSpanVisibleInView: bounds.isSpanVisibleInView,
- };
- }
- case 'TIMESTAMPS_REVERSED': {
- return {
- warning: t('Reversed start and end times'),
- left: bounds.start,
- width: bounds.end - bounds.start,
- isSpanVisibleInView: bounds.isSpanVisibleInView,
- };
- }
- case 'TIMESTAMPS_STABLE': {
- return {
- warning: void 0,
- left: bounds.start,
- width: bounds.end - bounds.start,
- isSpanVisibleInView: bounds.isSpanVisibleInView,
- };
- }
- default: {
- const _exhaustiveCheck: never = bounds;
- return _exhaustiveCheck;
- }
- }
- }
- renderMeasurements() {
- const {event, generateBounds, measurements} = this.props;
- if (this.isHighlighted) {
- return null;
- }
- const spanMeasurements = measurements ?? getMeasurements(event, generateBounds);
- return (
- <Fragment>
- {Array.from(spanMeasurements.values()).map(verticalMark => {
- const mark = Object.values(verticalMark.marks)[0];
- const {timestamp} = mark;
- const bounds = getMeasurementBounds(timestamp, generateBounds);
- const shouldDisplay = defined(bounds.left) && defined(bounds.width);
- if (!shouldDisplay || !bounds.isSpanVisibleInView) {
- return null;
- }
- return (
- <MeasurementMarker
- key={String(timestamp)}
- style={{
- left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
- }}
- failedThreshold={verticalMark.failedThreshold}
- />
- );
- })}
- </Fragment>
- );
- }
- renderSpanTreeConnector({hasToggler}: {hasToggler: boolean}) {
- const {
- isLast,
- isRoot,
- treeDepth: spanTreeDepth,
- continuingTreeDepths,
- span,
- showSpanTree,
- } = this.props;
- const spanID = getSpanID(span);
- if (isRoot) {
- if (hasToggler) {
- return (
- <ConnectorBar
- style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
- key={`${spanID}-last`}
- orphanBranch={false}
- />
- );
- }
- return null;
- }
- const connectorBars: Array<React.ReactNode> = continuingTreeDepths.map(treeDepth => {
- const depth: number = unwrapTreeDepth(treeDepth);
- if (depth === 0) {
- // do not render a connector bar at depth 0,
- // if we did render a connector bar, this bar would be placed at depth -1
- // which does not exist.
- return null;
- }
- const left = ((spanTreeDepth - depth) * (TOGGLE_BORDER_BOX / 2) + 2) * -1;
- return (
- <ConnectorBar
- style={{left}}
- key={`${spanID}-${depth}`}
- orphanBranch={isOrphanTreeDepth(treeDepth)}
- />
- );
- });
- if (hasToggler && showSpanTree) {
- // if there is a toggle button, we add a connector bar to create an attachment
- // between the toggle button and any connector bars below the toggle button
- connectorBars.push(
- <ConnectorBar
- style={{
- right: '15px',
- height: `${ROW_HEIGHT / 2}px`,
- bottom: isLast ? `-${ROW_HEIGHT / 2 + 2}px` : '0',
- top: 'auto',
- }}
- key={`${spanID}-last-bottom`}
- orphanBranch={false}
- />
- );
- }
- return (
- <TreeConnector
- isLast={isLast}
- hasToggler={hasToggler}
- orphanBranch={isOrphanSpan(span)}
- >
- {connectorBars}
- </TreeConnector>
- );
- }
- renderSpanTreeToggler({left, errored}: {errored: boolean; left: number}) {
- const {numOfSpanChildren, isRoot, showSpanTree} = this.props;
- const chevron = <TreeToggleIcon direction={showSpanTree ? 'up' : 'down'} />;
- if (numOfSpanChildren <= 0) {
- return (
- <TreeToggleContainer style={{left: `${left}px`}}>
- {this.renderSpanTreeConnector({hasToggler: false})}
- </TreeToggleContainer>
- );
- }
- const chevronElement = !isRoot ? <div>{chevron}</div> : null;
- return (
- <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
- {this.renderSpanTreeConnector({hasToggler: true})}
- <TreeToggle
- disabled={!!isRoot}
- isExpanded={showSpanTree}
- errored={errored}
- onClick={event => {
- event.stopPropagation();
- if (isRoot) {
- return;
- }
- PerformanceInteraction.startInteraction('SpanTreeToggle', 1000 * 10);
- this.props.toggleSpanTree();
- // TODO Abdullah Khan: A little bit hacky, but ensures that the toggled tree aligns
- // with the rest of the span tree, in the trace view.
- if (this.props.fromTraceView) {
- this.props.updateHorizontalScrollState(0.5);
- }
- }}
- >
- <Count value={numOfSpanChildren} />
- {chevronElement}
- </TreeToggle>
- </TreeToggleContainer>
- );
- }
- renderTitle(errors: TraceErrorOrIssue[] | null) {
- const {
- span,
- spanBarType,
- treeDepth,
- groupOccurrence,
- toggleSpanGroup,
- toggleSiblingSpanGroup,
- addContentSpanBarRef,
- removeContentSpanBarRef,
- groupType,
- event,
- } = this.props;
- let titleFragments: React.ReactNode[] = [];
- if (
- typeof toggleSpanGroup === 'function' ||
- typeof toggleSiblingSpanGroup === 'function'
- ) {
- titleFragments.push(
- <Regroup
- key={`regroup-${span.timestamp}`}
- onClick={e => {
- e.stopPropagation();
- e.preventDefault();
- if (groupType === GroupType.SIBLINGS && 'op' in span) {
- toggleSiblingSpanGroup?.(span, groupOccurrence ?? 0);
- } else {
- toggleSpanGroup?.();
- }
- }}
- >
- <a
- href="#regroup"
- onClick={e => {
- e.preventDefault();
- }}
- >
- {t('Regroup')}
- </a>
- </Regroup>
- );
- }
- const spanOperationName = getSpanOperation(span);
- if (spanOperationName) {
- titleFragments.push(spanOperationName);
- }
- titleFragments = titleFragments.flatMap(current => [current, ' \u2014 ']);
- const isAggregateEvent = event.type === EventOrGroupType.AGGREGATE_TRANSACTION;
- const left =
- treeDepth * (TOGGLE_BORDER_BOX / 2) +
- MARGIN_LEFT +
- (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0);
- const errored = Boolean(errors && errors.length > 0);
- return (
- <Fragment>
- {isAggregateEvent && (
- <SpanFrequencyBox span={span as AggregateSpanType | GapSpanType} />
- )}
- <RowTitleContainer
- data-debug-id="SpanBarTitleContainer"
- ref={ref => {
- if (!ref) {
- removeContentSpanBarRef(this.spanContentRef);
- return;
- }
- addContentSpanBarRef(ref);
- this.spanContentRef = ref;
- }}
- >
- {this.renderSpanTreeToggler({left, errored})}
- <RowTitle
- style={{
- left: `${left}px`,
- width: '100%',
- }}
- >
- <RowTitleContent
- errored={errored}
- data-test-id={`row-title-content${spanBarType ? `-${spanBarType}` : ''}`}
- >
- <strong>{titleFragments}</strong>
- {formatSpanTreeLabel(span)}
- </RowTitleContent>
- </RowTitle>
- </RowTitleContainer>
- </Fragment>
- );
- }
- connectObservers() {
- const observer = new IntersectionObserver(([entry]) =>
- this.setState({isIntersecting: entry.isIntersecting}, () => {
- // Scrolls the next(invisible) bar from the virtualized list,
- // by its height. Allows us to look for anchored span bars occuring
- // at the bottom of the span tree.
- if (
- this.hashSpanId &&
- !this.props.didAnchoredSpanMount() &&
- !this.state.isIntersecting
- ) {
- window.scrollBy(0, SPAN_BAR_HEIGHT);
- }
- })
- );
- this.intersectionObserver = observer;
- if (this.spanRowDOMRef.current) {
- observer.observe(this.spanRowDOMRef.current);
- }
- }
- disconnectObservers() {
- if (this.intersectionObserver) {
- this.intersectionObserver.disconnect();
- }
- }
- renderDivider(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) {
- if (this.isHighlighted) {
- // Mock component to preserve layout spacing
- return (
- <DividerLine
- showDetail
- style={{
- position: 'absolute',
- }}
- />
- );
- }
- const {addDividerLineRef} = dividerHandlerChildrenProps;
- return (
- <DividerLine
- ref={addDividerLineRef()}
- style={{
- position: 'absolute',
- }}
- onMouseEnter={() => {
- dividerHandlerChildrenProps.setHover(true);
- }}
- onMouseLeave={() => {
- dividerHandlerChildrenProps.setHover(false);
- }}
- onMouseOver={() => {
- dividerHandlerChildrenProps.setHover(true);
- }}
- onMouseDown={dividerHandlerChildrenProps.onDragStart}
- onClick={event => {
- // we prevent the propagation of the clicks from this component to prevent
- // the span detail from being opened.
- event.stopPropagation();
- }}
- />
- );
- }
- getRelatedErrors(
- quickTrace: QuickTraceContextChildrenProps
- ): TraceErrorOrIssue[] | null {
- if (!quickTrace) {
- return null;
- }
- const {span, isSpanInEmbeddedTree} = this.props;
- const {currentEvent} = quickTrace;
- if (
- isGapSpan(span) ||
- !currentEvent ||
- !isTraceTransaction<TraceFull>(currentEvent)
- ) {
- return null;
- }
- const performanceIssues = currentEvent.performance_issues.filter(
- issue =>
- issue.span.some(id => id === span.span_id) ||
- issue.suspect_spans.some(suspectSpanId => suspectSpanId === span.span_id)
- );
- return [
- ...currentEvent.errors.filter(error => error.span === span.span_id),
- ...(isSpanInEmbeddedTree ? [] : performanceIssues), // Spans can be shown when embedded in performance issues
- ];
- }
- getChildTransactions(
- quickTrace: QuickTraceContextChildrenProps
- ): QuickTraceEvent[] | null {
- if (!quickTrace) {
- return null;
- }
- const {span} = this.props;
- const {trace} = quickTrace;
- if (isGapSpan(span) || !trace) {
- return null;
- }
- return trace.filter(({parent_span_id}) => parent_span_id === span.span_id);
- }
- renderErrorBadge(errors: TraceErrorOrIssue[] | null): React.ReactNode {
- return errors?.length ? <ErrorBadge /> : null;
- }
- renderMetricsBadge(span: NewTraceDetailsSpanBarProps['span']): React.ReactNode {
- const hasMetrics =
- '_metrics_summary' in span && Object.keys(span._metrics_summary ?? {}).length > 0;
- return hasMetrics && hasMetricsExperimentalFeature(this.props.organization) ? (
- <MetricsBadge />
- ) : null;
- }
- renderEmbeddedTransactionsBadge(
- transactions: QuickTraceEvent[] | null
- ): React.ReactNode {
- const {toggleEmbeddedChildren, organization, showEmbeddedChildren} = this.props;
- if (transactions && transactions.length >= 1) {
- return (
- <Tooltip
- title={
- <span>
- {showEmbeddedChildren
- ? t('This span is showing a direct child. Remove transaction to hide')
- : t('This span has a direct child. Add transaction to view')}
- </span>
- }
- position="top"
- containerDisplayMode="block"
- >
- <StyledZoomIcon
- isZoomIn={!showEmbeddedChildren}
- onClick={() => {
- if (toggleEmbeddedChildren) {
- const eventKey = showEmbeddedChildren
- ? 'span_view.embedded_child.hide'
- : 'span_view.embedded_child.show';
- trackAnalytics(eventKey, {organization});
- const eventSlugs = transactions.map(transaction =>
- generateEventSlug({
- id: transaction.event_id,
- project: transaction.project_slug,
- })
- );
- toggleEmbeddedChildren(organization.slug, eventSlugs);
- }
- }}
- />
- </Tooltip>
- );
- }
- return null;
- }
- renderMissingInstrumentationProfileBadge(): React.ReactNode {
- const {organization, span} = this.props;
- if (!organization.features.includes('profiling')) {
- return null;
- }
- if (!isGapSpan(span)) {
- return null;
- }
- return (
- <ProfileContext.Consumer>
- {profiles => {
- if (profiles?.type !== 'resolved') {
- return null;
- }
- return <ProfileBadge />;
- }}
- </ProfileContext.Consumer>
- );
- }
- renderWarningText() {
- let warningText = this.getBounds().warning;
- if (this.props.isEmbeddedTransactionTimeAdjusted) {
- const embeddedWarningText = t(
- 'All child span timestamps have been adjusted to account for mismatched client and server clocks.'
- );
- warningText = warningText
- ? `${warningText}. ${embeddedWarningText}`
- : embeddedWarningText;
- }
- if (!warningText) {
- return null;
- }
- return (
- <Tooltip containerDisplayMode="flex" title={warningText}>
- <StyledIconWarning size="xs" />
- </Tooltip>
- );
- }
- getSpanDetailsProps() {
- const {
- span,
- organization,
- event,
- isRoot,
- trace,
- resetCellMeasureCache,
- quickTrace,
- location,
- } = this.props;
- const openPanel = decodeScalar(location.query.openPanel);
- const errors = this.getRelatedErrors(quickTrace);
- const transactions = this.getChildTransactions(quickTrace);
- return {
- span: span as ProcessedSpanType,
- organization,
- event: event as EventTransaction,
- isRoot: !!isRoot,
- openPanel,
- trace,
- childTransactions: transactions,
- relatedErrors: errors,
- scrollToHash: this.scrollIntoView,
- resetCellMeasureCache,
- };
- }
- handleRowClick() {
- const {span, event, location, markAnchoredSpanIsMounted} = this.props;
- const spanDetailProps = this.getSpanDetailsProps();
- if (this.props.onRowClick && !isGapSpan(span)) {
- markAnchoredSpanIsMounted?.();
- const isTransactionEvent = event.type === EventOrGroupType.TRANSACTION;
- if (isTransactionEvent) {
- browserHistory.push({
- ...location,
- hash: `${transactionTargetHash(event.eventID)}${spanTargetHash(span.span_id)}`,
- query: {
- ...location.query,
- openPanel: 'open',
- },
- });
- spanDetailProps.openPanel = 'open';
- }
- this.props.onRowClick(undefined);
- }
- }
- renderHeader({
- dividerHandlerChildrenProps,
- }: {
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
- }) {
- const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
- const {quickTrace} = this.props;
- const errors = this.getRelatedErrors(quickTrace);
- const transactions = this.getChildTransactions(quickTrace);
- return (
- <RowCellContainer showDetail={this.isHighlighted}>
- <RowCell
- data-type="span-row-cell"
- showDetail={this.isHighlighted}
- style={{
- width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
- paddingTop: 0,
- }}
- onClick={() => this.handleRowClick()}
- ref={this.spanTitleRef}
- >
- {this.renderTitle(errors)}
- </RowCell>
- <DividerContainer>
- {this.renderDivider(dividerHandlerChildrenProps)}
- {this.renderMetricsBadge(this.props.span)}
- {this.renderErrorBadge(errors)}
- {this.renderEmbeddedTransactionsBadge(transactions)}
- {this.renderMissingInstrumentationProfileBadge()}
- </DividerContainer>
- <RowCell
- data-type="span-row-cell"
- showDetail={this.isHighlighted}
- showStriping={this.props.spanNumber % 2 !== 0}
- style={{
- width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
- }}
- onClick={() => this.handleRowClick()}
- >
- {this.renderSpanBarRectangles()}
- {this.renderMeasurements()}
- <SpanBarCursorGuide />
- </RowCell>
- {!this.isHighlighted && (
- <DividerLineGhostContainer
- style={{
- width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
- display: 'none',
- }}
- >
- <DividerLine
- ref={addGhostDividerLineRef()}
- style={{
- right: 0,
- }}
- className="hovering"
- onClick={event => {
- // the ghost divider line should not be interactive.
- // we prevent the propagation of the clicks from this component to prevent
- // the span detail from being opened.
- event.stopPropagation();
- }}
- />
- </DividerLineGhostContainer>
- )}
- </RowCellContainer>
- );
- }
- renderSpanBarRectangles() {
- const {span, spanBarColor, spanBarType, generateBounds} = this.props;
- const startTimestamp: number = span.start_timestamp;
- const endTimestamp: number = span.timestamp;
- const duration = Math.abs(endTimestamp - startTimestamp);
- const durationString = getHumanDuration(duration);
- const bounds = this.getBounds();
- const displaySpanBar = defined(bounds.left) && defined(bounds.width);
- if (!displaySpanBar) {
- return null;
- }
- const subTimings = getSpanSubTimings(span);
- const hasSubTimings = !!subTimings;
- const subSpans = hasSubTimings
- ? subTimings.map(timing => {
- const timingGeneratedBounds = generateBounds(timing);
- const timingBounds = this.getBounds(timingGeneratedBounds);
- const isAffectedSubTiming = shouldLimitAffectedToTiming(timing);
- return (
- <RowRectangle
- key={timing.name}
- spanBarType={isAffectedSubTiming ? spanBarType : undefined}
- style={{
- backgroundColor: lightenBarColor(
- getSpanOperation(span),
- timing.colorLighten
- ),
- left: `min(${toPercent(timingBounds.left || 0)}, calc(100% - 1px))`,
- width: toPercent(timingBounds.width || 0),
- }}
- />
- );
- })
- : null;
- const durationDisplay = getDurationDisplay(bounds);
- return (
- <Fragment>
- {subSpans}
- <RowRectangle
- spanBarType={spanBarType}
- style={{
- backgroundColor: hasSubTimings ? 'rgba(0,0,0,0.0)' : spanBarColor,
- left: `min(${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
- width: toPercent(bounds.width || 0),
- }}
- isHidden={hasSubTimings}
- >
- <DurationPill
- durationDisplay={durationDisplay}
- showDetail={this.isHighlighted}
- spanBarType={spanBarType}
- >
- {durationString}
- {this.renderWarningText()}
- </DurationPill>
- </RowRectangle>
- </Fragment>
- );
- }
- renderEmbeddedChildrenState() {
- const {fetchEmbeddedChildrenState} = this.props;
- switch (fetchEmbeddedChildrenState) {
- case 'loading_embedded_transactions': {
- return (
- <MessageRow>
- <span>{t('Loading embedded transaction')}</span>
- </MessageRow>
- );
- }
- case 'error_fetching_embedded_transactions': {
- return (
- <MessageRow>
- <span>{t('Error loading embedded transaction')}</span>
- </MessageRow>
- );
- }
- default:
- return null;
- }
- }
- render() {
- const {spanNumber} = this.props;
- const bounds = this.getBounds();
- const {isSpanVisibleInView} = bounds;
- return (
- <Fragment>
- <Row
- ref={this.spanRowDOMRef}
- visible={isSpanVisibleInView}
- showBorder={this.isHighlighted}
- data-test-id={`span-row-${spanNumber}`}
- >
- <Fragment>
- <DividerHandlerManager.Consumer>
- {(
- dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
- ) =>
- this.renderHeader({
- dividerHandlerChildrenProps,
- })
- }
- </DividerHandlerManager.Consumer>
- </Fragment>
- </Row>
- {this.renderEmbeddedChildrenState()}
- </Fragment>
- );
- }
- }
- const StyledIconWarning = styled(IconWarning)`
- margin-left: ${space(0.25)};
- margin-bottom: ${space(0.25)};
- `;
- const Regroup = styled('span')``;
- export const NewTraceDetailsProfiledSpanBar = withProfiler(
- withScrollbarManager(NewTraceDetailsSpanBar)
- );
|