mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
990 lines
30 KiB
TypeScript
990 lines
30 KiB
TypeScript
import {createRef, Fragment, useCallback, useEffect, useMemo, useState} from 'react';
|
|
import {browserHistory} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import type {Location} from 'history';
|
|
import {Observer} from 'mobx-react';
|
|
|
|
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
|
|
import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
|
|
import type {SpanDetailProps} from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails';
|
|
import NewTraceDetailsSpanTree from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanTree';
|
|
import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
|
|
import * as SpanContext from 'sentry/components/events/interfaces/spans/spanContext';
|
|
import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles';
|
|
import type {
|
|
SpanBoundsType,
|
|
SpanGeneratedBoundsType,
|
|
VerticalMark,
|
|
} from 'sentry/components/events/interfaces/spans/utils';
|
|
import {
|
|
getMeasurementBounds,
|
|
parseTraceDetailsURLHash,
|
|
transactionTargetHash,
|
|
} from 'sentry/components/events/interfaces/spans/utils';
|
|
import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
|
|
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
|
|
import Link from 'sentry/components/links/link';
|
|
import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
|
|
import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
|
|
import {
|
|
Row,
|
|
RowCell,
|
|
RowCellContainer,
|
|
RowReplayTimeIndicators,
|
|
} from 'sentry/components/performance/waterfall/row';
|
|
import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
|
|
import {
|
|
DividerContainer,
|
|
DividerLine,
|
|
DividerLineGhostContainer,
|
|
ErrorBadge,
|
|
MetricsBadge,
|
|
} 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,
|
|
} from 'sentry/components/performance/waterfall/utils';
|
|
import {TransactionProfileIdProvider} from 'sentry/components/profiling/transactionProfileIdProvider';
|
|
import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
|
|
import {Tooltip} from 'sentry/components/tooltip';
|
|
import {IconZoom} from 'sentry/icons/iconZoom';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {EventTransaction, Organization} from 'sentry/types';
|
|
import {defined} from 'sentry/utils';
|
|
import {hasDDMExperimentalFeature} from 'sentry/utils/metrics/features';
|
|
import toPercent from 'sentry/utils/number/toPercent';
|
|
import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
|
|
import type {
|
|
TraceError,
|
|
TraceFullDetailed,
|
|
} from 'sentry/utils/performance/quickTrace/types';
|
|
import {
|
|
isTraceError,
|
|
isTraceRoot,
|
|
isTraceTransaction,
|
|
} from 'sentry/utils/performance/quickTrace/utils';
|
|
import Projects from 'sentry/utils/projects';
|
|
import {useApiQuery} from 'sentry/utils/queryClient';
|
|
import {decodeScalar} from 'sentry/utils/queryString';
|
|
import useRouter from 'sentry/utils/useRouter';
|
|
import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
|
|
import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
|
|
|
|
import type {EventDetail} from './newTraceDetailsContent';
|
|
import {ProjectBadgeContainer} from './styles';
|
|
import type {TraceInfo, TraceRoot, TreeDepth} from './types';
|
|
import {shortenErrorTitle} from './utils';
|
|
|
|
const MARGIN_LEFT = 0;
|
|
const TRANSACTION_BAR_HEIGHT = 24;
|
|
|
|
type Props = {
|
|
addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
|
|
continuingDepths: TreeDepth[];
|
|
generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
|
|
hasGuideAnchor: boolean;
|
|
index: number;
|
|
isBarScrolledTo: boolean;
|
|
isExpanded: boolean;
|
|
isLast: boolean;
|
|
isOrphan: boolean;
|
|
isVisible: boolean;
|
|
location: Location;
|
|
onBarScrolledTo: () => void;
|
|
onWheel: (deltaX: number) => void;
|
|
organization: Organization;
|
|
removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
|
|
toggleExpandedState: () => void;
|
|
traceInfo: TraceInfo;
|
|
traceViewRef: React.RefObject<HTMLDivElement>;
|
|
transaction: TraceRoot | TraceFullDetailed | TraceError;
|
|
barColor?: string;
|
|
isOrphanError?: boolean;
|
|
measurements?: Map<number, VerticalMark>;
|
|
numOfOrphanErrors?: number;
|
|
onRowClick?: (detailKey: EventDetail | SpanDetailProps | undefined) => void;
|
|
onlyOrphanErrors?: boolean;
|
|
};
|
|
|
|
function NewTraceDetailsTransactionBar(props: Props) {
|
|
const hashValues = parseTraceDetailsURLHash(props.location.hash);
|
|
const openPanel = decodeScalar(props.location.query.openPanel);
|
|
const eventIDInQueryParam = !!(
|
|
isTraceTransaction(props.transaction) &&
|
|
hashValues?.eventId &&
|
|
hashValues.eventId === props.transaction.event_id
|
|
);
|
|
const isHighlighted = !!(!hashValues?.spanId && eventIDInQueryParam);
|
|
const highlightEmbeddedSpan = !!(hashValues?.spanId && eventIDInQueryParam);
|
|
const [showEmbeddedChildren, setShowEmbeddedChildren] = useState(
|
|
isHighlighted || highlightEmbeddedSpan
|
|
);
|
|
const [isIntersecting, setIntersecting] = useState(false);
|
|
const transactionRowDOMRef = createRef<HTMLDivElement>();
|
|
const transactionTitleRef = createRef<HTMLDivElement>();
|
|
let spanContentRef: HTMLDivElement | null = null;
|
|
const router = useRouter();
|
|
|
|
const handleWheel = useCallback(
|
|
(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} = props;
|
|
onWheel(event.deltaX);
|
|
},
|
|
[props]
|
|
);
|
|
|
|
const scrollIntoView = useCallback(() => {
|
|
const element = transactionRowDOMRef.current;
|
|
if (!element) {
|
|
return;
|
|
}
|
|
const boundingRect = element.getBoundingClientRect();
|
|
const offset = boundingRect.top + window.scrollY - TRANSACTION_BAR_HEIGHT;
|
|
window.scrollTo(0, offset);
|
|
props.onBarScrolledTo();
|
|
}, [transactionRowDOMRef, props]);
|
|
|
|
useEffect(() => {
|
|
const {transaction, isBarScrolledTo} = props;
|
|
const observer = new IntersectionObserver(([entry]) =>
|
|
setIntersecting(entry.isIntersecting)
|
|
);
|
|
|
|
if (transactionRowDOMRef.current) {
|
|
observer.observe(transactionRowDOMRef.current);
|
|
}
|
|
|
|
if (
|
|
'event_id' in transaction &&
|
|
hashValues?.eventId === transaction.event_id &&
|
|
!isIntersecting &&
|
|
!isBarScrolledTo
|
|
) {
|
|
scrollIntoView();
|
|
}
|
|
|
|
if (isIntersecting) {
|
|
props.onBarScrolledTo();
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, [
|
|
setIntersecting,
|
|
hashValues?.eventId,
|
|
hashValues?.spanId,
|
|
props,
|
|
scrollIntoView,
|
|
isIntersecting,
|
|
transactionRowDOMRef,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const transactionTitleRefCurrentCopy = transactionTitleRef.current;
|
|
|
|
if (transactionTitleRefCurrentCopy) {
|
|
transactionTitleRefCurrentCopy.addEventListener('wheel', handleWheel, {
|
|
passive: false,
|
|
});
|
|
}
|
|
|
|
return () => {
|
|
if (transactionTitleRefCurrentCopy) {
|
|
transactionTitleRefCurrentCopy.removeEventListener('wheel', handleWheel);
|
|
}
|
|
};
|
|
}, [handleWheel, props, transactionTitleRef]);
|
|
|
|
const transactionEvent =
|
|
isTraceTransaction<TraceFullDetailed>(props.transaction) ||
|
|
isTraceError(props.transaction)
|
|
? props.transaction
|
|
: undefined;
|
|
const {
|
|
data: embeddedChildren,
|
|
isLoading: isEmbeddedChildrenLoading,
|
|
error: embeddedChildrenError,
|
|
} = useApiQuery<EventTransaction>(
|
|
[
|
|
`/organizations/${props.organization.slug}/events/${transactionEvent?.project_slug}:${transactionEvent?.event_id}/`,
|
|
],
|
|
{
|
|
staleTime: 2 * 60 * 1000,
|
|
enabled: showEmbeddedChildren || isHighlighted,
|
|
}
|
|
);
|
|
|
|
const waterfallModel = useMemo(() => {
|
|
return embeddedChildren
|
|
? new WaterfallModel(
|
|
embeddedChildren,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
props.traceInfo
|
|
)
|
|
: null;
|
|
}, [embeddedChildren, props.traceInfo]);
|
|
|
|
useEffect(() => {
|
|
if (isTraceTransaction(props.transaction) && !isTraceError(props.transaction)) {
|
|
if (isHighlighted && props.onRowClick) {
|
|
props.onRowClick({
|
|
traceFullDetailedEvent: props.transaction,
|
|
event: embeddedChildren,
|
|
openPanel,
|
|
});
|
|
}
|
|
}
|
|
}, [isHighlighted, embeddedChildren, props, props.transaction, openPanel]);
|
|
|
|
const renderEmbeddedChildrenState = () => {
|
|
if (showEmbeddedChildren) {
|
|
if (isEmbeddedChildrenLoading) {
|
|
return (
|
|
<MessageRow>
|
|
<span>{t('Loading embedded transaction')}</span>
|
|
</MessageRow>
|
|
);
|
|
}
|
|
|
|
if (embeddedChildrenError) {
|
|
return (
|
|
<MessageRow>
|
|
<span>{t('Error loading embedded transaction')}</span>
|
|
</MessageRow>
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const handleRowCellClick = () => {
|
|
const {transaction, organization, location} = props;
|
|
|
|
if (isTraceError(transaction)) {
|
|
browserHistory.push(generateIssueEventTarget(transaction, organization));
|
|
return;
|
|
}
|
|
|
|
if (isTraceTransaction<TraceFullDetailed>(transaction)) {
|
|
router.replace({
|
|
...location,
|
|
hash: transactionTargetHash(transaction.event_id),
|
|
query: {
|
|
...location.query,
|
|
openPanel: 'open',
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
const getCurrentOffset = () => {
|
|
const {transaction} = props;
|
|
const {generation} = transaction;
|
|
|
|
return getOffset(generation);
|
|
};
|
|
|
|
const renderMeasurements = () => {
|
|
const {measurements, generateBounds} = props;
|
|
if (!measurements) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
{Array.from(measurements.values()).map(verticalMark => {
|
|
const mark = Object.values(verticalMark.marks)[0];
|
|
const {timestamp} = mark;
|
|
const bounds = getMeasurementBounds(timestamp, generateBounds);
|
|
|
|
const shouldDisplay = defined(bounds.left) && defined(bounds.width);
|
|
|
|
if (!shouldDisplay || !bounds.isSpanVisibleInView) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<MeasurementMarker
|
|
key={String(timestamp)}
|
|
style={{
|
|
left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
|
|
}}
|
|
failedThreshold={verticalMark.failedThreshold}
|
|
/>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
const renderConnector = (hasToggle: boolean) => {
|
|
const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props;
|
|
|
|
const {generation = 0} = transaction;
|
|
const eventId =
|
|
isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
|
|
? transaction.event_id
|
|
: transaction.traceSlug;
|
|
|
|
if (generation === 0) {
|
|
if (hasToggle) {
|
|
return (
|
|
<ConnectorBar
|
|
style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
|
|
orphanBranch={false}
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const connectorBars: Array<React.ReactNode> = continuingDepths.map(
|
|
({depth, isOrphanDepth}) => {
|
|
if (generation - depth <= 1) {
|
|
// If the difference is less than or equal to 1, then it means that the continued
|
|
// bar is from its direct parent. In this case, do not render a connector bar
|
|
// because the tree connector below will suffice.
|
|
return null;
|
|
}
|
|
|
|
const left = -1 * getOffset(generation - depth - 1) - 2;
|
|
|
|
return (
|
|
<ConnectorBar
|
|
style={{left}}
|
|
key={`${eventId}-${depth}`}
|
|
orphanBranch={isOrphanDepth}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
const embeddedChildrenLength =
|
|
(embeddedChildren && waterfallModel && waterfallModel.rootSpan.children.length) ??
|
|
0;
|
|
if (
|
|
hasToggle &&
|
|
(isExpanded || (showEmbeddedChildren && embeddedChildrenLength > 0))
|
|
) {
|
|
connectorBars.push(
|
|
<ConnectorBar
|
|
style={{
|
|
right: '15px',
|
|
height: '10px',
|
|
bottom: isLast ? `-${ROW_HEIGHT / 2 + 1}px` : '0',
|
|
top: 'auto',
|
|
}}
|
|
key={`${eventId}-last`}
|
|
orphanBranch={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TreeConnector isLast={isLast} hasToggler={hasToggle} orphanBranch={isOrphan}>
|
|
{connectorBars}
|
|
</TreeConnector>
|
|
);
|
|
};
|
|
|
|
const renderEmbeddedTransactionsBadge = (): React.ReactNode => {
|
|
return (
|
|
<Tooltip
|
|
title={
|
|
<span>
|
|
{showEmbeddedChildren
|
|
? t(
|
|
'This transaction is showing a direct child. Remove transaction to hide'
|
|
)
|
|
: t('This transaction has a direct child. Add transaction to view')}
|
|
</span>
|
|
}
|
|
position="top"
|
|
containerDisplayMode="block"
|
|
delay={400}
|
|
>
|
|
<StyledZoomIcon
|
|
isZoomIn={!showEmbeddedChildren}
|
|
onClick={() => {
|
|
setShowEmbeddedChildren(prev => !prev);
|
|
|
|
if (
|
|
(props.isExpanded && !showEmbeddedChildren) ||
|
|
(!props.isExpanded && showEmbeddedChildren)
|
|
) {
|
|
props.toggleExpandedState();
|
|
}
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
};
|
|
|
|
const renderEmbeddedChildren = () => {
|
|
if (!embeddedChildren || !showEmbeddedChildren || !waterfallModel) {
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
organization,
|
|
traceViewRef,
|
|
location,
|
|
isLast,
|
|
traceInfo,
|
|
isExpanded,
|
|
toggleExpandedState,
|
|
} = props;
|
|
const profileId = embeddedChildren.contexts?.profile?.profile_id ?? null;
|
|
|
|
if (isExpanded) {
|
|
toggleExpandedState();
|
|
}
|
|
|
|
return (
|
|
<Fragment>
|
|
<QuickTraceQuery
|
|
event={embeddedChildren}
|
|
location={location}
|
|
orgSlug={organization.slug}
|
|
>
|
|
{results => (
|
|
<ProfilesProvider
|
|
orgSlug={organization.slug}
|
|
projectSlug={embeddedChildren.projectSlug ?? ''}
|
|
profileId={profileId || ''}
|
|
>
|
|
<ProfileContext.Consumer>
|
|
{profiles => (
|
|
<ProfileGroupProvider
|
|
type="flamechart"
|
|
input={profiles?.type === 'resolved' ? profiles.data : null}
|
|
traceID={profileId || ''}
|
|
>
|
|
<TransactionProfileIdProvider
|
|
projectId={embeddedChildren.projectID}
|
|
timestamp={embeddedChildren.dateReceived}
|
|
transactionId={embeddedChildren.id}
|
|
>
|
|
<SpanContext.Provider>
|
|
<SpanContext.Consumer>
|
|
{spanContextProps => (
|
|
<Observer>
|
|
{() => (
|
|
<NewTraceDetailsSpanTree
|
|
measurements={props.measurements}
|
|
quickTrace={results}
|
|
location={props.location}
|
|
onRowClick={props.onRowClick}
|
|
traceInfo={traceInfo}
|
|
traceViewHeaderRef={traceViewRef}
|
|
traceViewRef={traceViewRef}
|
|
parentContinuingDepths={props.continuingDepths}
|
|
traceHasMultipleRoots={props.continuingDepths.some(
|
|
c => c.depth === 0 && c.isOrphanDepth
|
|
)}
|
|
parentIsOrphan={props.isOrphan}
|
|
parentIsLast={isLast}
|
|
parentGeneration={transaction.generation ?? 0}
|
|
organization={organization}
|
|
waterfallModel={waterfallModel}
|
|
filterSpans={waterfallModel.filterSpans}
|
|
spans={waterfallModel
|
|
.getWaterfall({
|
|
viewStart: 0,
|
|
viewEnd: 1,
|
|
})
|
|
.slice(1)}
|
|
focusedSpanIds={waterfallModel.focusedSpanIds}
|
|
spanContextProps={spanContextProps}
|
|
operationNameFilters={
|
|
waterfallModel.operationNameFilters
|
|
}
|
|
/>
|
|
)}
|
|
</Observer>
|
|
)}
|
|
</SpanContext.Consumer>
|
|
</SpanContext.Provider>
|
|
</TransactionProfileIdProvider>
|
|
</ProfileGroupProvider>
|
|
)}
|
|
</ProfileContext.Consumer>
|
|
</ProfilesProvider>
|
|
)}
|
|
</QuickTraceQuery>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
const renderToggle = (errored: boolean) => {
|
|
const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props;
|
|
const left = getCurrentOffset();
|
|
|
|
const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
|
|
let childrenLength: number | string =
|
|
(!isTraceError(transaction) && transaction.children?.length) || 0;
|
|
const generation = transaction.generation || 0;
|
|
if (childrenLength <= 0 && !hasOrphanErrors && !showEmbeddedChildren) {
|
|
return (
|
|
<TreeToggleContainer style={{left: `${left}px`}}>
|
|
{renderConnector(false)}
|
|
</TreeToggleContainer>
|
|
);
|
|
}
|
|
|
|
if (showEmbeddedChildren) {
|
|
childrenLength =
|
|
embeddedChildren && waterfallModel
|
|
? waterfallModel.rootSpan.children.length
|
|
: '?';
|
|
} else {
|
|
childrenLength = childrenLength + (numOfOrphanErrors ?? 0);
|
|
}
|
|
|
|
const isRoot = generation === 0;
|
|
|
|
return (
|
|
<TreeToggleContainer style={{left: `${left}px`}} hasToggler>
|
|
{renderConnector(true)}
|
|
<TreeToggle
|
|
disabled={isRoot}
|
|
isExpanded={isExpanded}
|
|
errored={errored}
|
|
onClick={event => {
|
|
event.stopPropagation();
|
|
|
|
if (isRoot || showEmbeddedChildren) {
|
|
return;
|
|
}
|
|
|
|
toggleExpandedState();
|
|
setShowEmbeddedChildren(false);
|
|
}}
|
|
>
|
|
<span>{childrenLength}</span>
|
|
{!isRoot && !showEmbeddedChildren && (
|
|
<div>
|
|
<TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
|
|
</div>
|
|
)}
|
|
</TreeToggle>
|
|
</TreeToggleContainer>
|
|
);
|
|
};
|
|
|
|
const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => {
|
|
const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
|
|
props;
|
|
const left = getCurrentOffset();
|
|
const errored = isTraceTransaction<TraceFullDetailed>(transaction)
|
|
? transaction.errors &&
|
|
transaction.errors.length + transaction.performance_issues.length > 0
|
|
: false;
|
|
|
|
const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
|
|
isTraceError(transaction)) && (
|
|
<Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
|
|
{({projects}) => {
|
|
const project = projects.find(p => p.slug === transaction.project_slug);
|
|
return (
|
|
<ProjectBadgeContainer>
|
|
<Tooltip title={transaction.project_slug}>
|
|
<ProjectBadge
|
|
project={project ? project : {slug: transaction.project_slug}}
|
|
avatarSize={16}
|
|
hideName
|
|
/>
|
|
</Tooltip>
|
|
</ProjectBadgeContainer>
|
|
);
|
|
}}
|
|
</Projects>
|
|
);
|
|
|
|
const content = isTraceError(transaction) ? (
|
|
<Fragment>
|
|
{projectBadge}
|
|
<RowTitleContent errored>
|
|
<ErrorLink to={generateIssueEventTarget(transaction, organization)}>
|
|
<strong>{'Unknown \u2014 '}</strong>
|
|
{shortenErrorTitle(transaction.title)}
|
|
</ErrorLink>
|
|
</RowTitleContent>
|
|
</Fragment>
|
|
) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
|
|
<Fragment>
|
|
{projectBadge}
|
|
<RowTitleContent errored={errored}>
|
|
<strong>
|
|
{transaction['transaction.op']}
|
|
{' \u2014 '}
|
|
</strong>
|
|
{transaction.transaction}
|
|
</RowTitleContent>
|
|
</Fragment>
|
|
) : (
|
|
<RowTitleContent errored={false}>
|
|
<strong>{'Trace \u2014 '}</strong>
|
|
{transaction.traceSlug}
|
|
</RowTitleContent>
|
|
);
|
|
|
|
return (
|
|
<RowTitleContainer
|
|
ref={ref => {
|
|
if (!ref) {
|
|
removeContentSpanBarRef(spanContentRef);
|
|
return;
|
|
}
|
|
|
|
addContentSpanBarRef(ref);
|
|
spanContentRef = ref;
|
|
}}
|
|
>
|
|
{renderToggle(errored)}
|
|
<RowTitle
|
|
style={{
|
|
left: `${left}px`,
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{content}
|
|
</RowTitle>
|
|
</RowTitleContainer>
|
|
);
|
|
};
|
|
|
|
const renderDivider = (
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
|
|
) => {
|
|
if (isHighlighted) {
|
|
// Mock component to preserve layout spacing
|
|
return (
|
|
<DividerLine
|
|
showDetail={isHighlighted}
|
|
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={e => {
|
|
dividerHandlerChildrenProps.onDragStart(e);
|
|
}}
|
|
onClick={event => {
|
|
// we prevent the propagation of the clicks from this component to prevent
|
|
// the span detail from being opened.
|
|
event.stopPropagation();
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const renderGhostDivider = (
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
|
|
) => {
|
|
const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
|
|
|
|
return (
|
|
<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>
|
|
);
|
|
};
|
|
|
|
const renderErrorBadge = () => {
|
|
const {transaction} = props;
|
|
|
|
if (
|
|
isTraceRoot(transaction) ||
|
|
isTraceError(transaction) ||
|
|
!(transaction.errors.length + transaction.performance_issues.length)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return <ErrorBadge />;
|
|
};
|
|
|
|
const renderMetricsBadge = () => {
|
|
const {organization} = props;
|
|
const hasMetrics = Object.keys(embeddedChildren?._metrics_summary ?? {}).length > 0;
|
|
|
|
if (
|
|
!hasDDMExperimentalFeature(organization) ||
|
|
isTraceRoot(transaction) ||
|
|
isTraceError(transaction) ||
|
|
!hasMetrics
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return <MetricsBadge />;
|
|
};
|
|
|
|
const renderRectangle = () => {
|
|
const {transaction, traceInfo, barColor} = props;
|
|
|
|
// Use 1 as the difference in the case that startTimestamp === endTimestamp
|
|
const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
|
|
const start_timestamp = isTraceError(transaction)
|
|
? transaction.timestamp
|
|
: transaction.start_timestamp;
|
|
|
|
if (!(start_timestamp && transaction.timestamp)) {
|
|
return null;
|
|
}
|
|
|
|
const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
|
|
const startPercentage = startPosition / delta;
|
|
const duration = Math.abs(transaction.timestamp - start_timestamp);
|
|
const widthPercentage = duration / delta;
|
|
|
|
return (
|
|
<StyledRowRectangle
|
|
style={{
|
|
backgroundColor: barColor,
|
|
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
|
|
width: toPercent(widthPercentage || 0),
|
|
}}
|
|
>
|
|
{renderPerformanceIssues()}
|
|
{isTraceError(transaction) ? (
|
|
<ErrorBadge />
|
|
) : (
|
|
<Fragment>
|
|
{renderMetricsBadge()}
|
|
{renderErrorBadge()}
|
|
<DurationPill
|
|
durationDisplay={getDurationDisplay({
|
|
left: startPercentage,
|
|
width: widthPercentage,
|
|
})}
|
|
showDetail={isHighlighted}
|
|
>
|
|
{getHumanDuration(duration)}
|
|
</DurationPill>
|
|
</Fragment>
|
|
)}
|
|
</StyledRowRectangle>
|
|
);
|
|
};
|
|
|
|
const renderPerformanceIssues = () => {
|
|
const {transaction, barColor} = props;
|
|
if (isTraceError(transaction) || isTraceRoot(transaction)) {
|
|
return null;
|
|
}
|
|
|
|
const rows: React.ReactElement[] = [];
|
|
// Use 1 as the difference in the case that startTimestamp === endTimestamp
|
|
const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1;
|
|
for (let i = 0; i < transaction.performance_issues.length; i++) {
|
|
const issue = transaction.performance_issues[i];
|
|
const startPosition = Math.abs(issue.start - transaction.start_timestamp);
|
|
const startPercentage = startPosition / delta;
|
|
const duration = Math.abs(issue.end - issue.start);
|
|
const widthPercentage = duration / delta;
|
|
rows.push(
|
|
<RowRectangle
|
|
style={{
|
|
backgroundColor: barColor,
|
|
left: `min(${toPercent(startPercentage || 0)}, calc(100% - 1px))`,
|
|
width: toPercent(widthPercentage || 0),
|
|
}}
|
|
spanBarType={SpanBarType.AFFECTED}
|
|
/>
|
|
);
|
|
}
|
|
return rows;
|
|
};
|
|
|
|
const renderHeader = ({
|
|
dividerHandlerChildrenProps,
|
|
scrollbarManagerChildrenProps,
|
|
}: {
|
|
dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
|
|
scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
|
|
}) => {
|
|
const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props;
|
|
const {dividerPosition} = dividerHandlerChildrenProps;
|
|
const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors;
|
|
|
|
return (
|
|
<RowCellContainer showDetail={isHighlighted}>
|
|
<RowCell
|
|
data-test-id="transaction-row-title"
|
|
data-type="span-row-cell"
|
|
style={{
|
|
width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
|
|
paddingTop: 0,
|
|
}}
|
|
showDetail={isHighlighted}
|
|
onClick={handleRowCellClick}
|
|
ref={transactionTitleRef}
|
|
>
|
|
<GuideAnchor target="trace_view_guide_row" disabled={!hasGuideAnchor}>
|
|
{renderTitle(scrollbarManagerChildrenProps)}
|
|
</GuideAnchor>
|
|
</RowCell>
|
|
<DividerContainer>
|
|
{renderDivider(dividerHandlerChildrenProps)}
|
|
{!isTraceRoot(transaction) &&
|
|
!isTraceError(transaction) &&
|
|
renderEmbeddedTransactionsBadge()}
|
|
</DividerContainer>
|
|
<RowCell
|
|
data-test-id="transaction-row-duration"
|
|
data-type="span-row-cell"
|
|
showStriping={index % 2 !== 0}
|
|
style={{
|
|
width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
|
|
paddingTop: 0,
|
|
overflow: 'visible',
|
|
}}
|
|
showDetail={isHighlighted}
|
|
onClick={handleRowCellClick}
|
|
>
|
|
<RowReplayTimeIndicators />
|
|
<GuideAnchor target="trace_view_guide_row_details" disabled={!hasGuideAnchor}>
|
|
{!hideDurationRectangle && renderRectangle()}
|
|
{renderMeasurements()}
|
|
</GuideAnchor>
|
|
</RowCell>
|
|
{!isHighlighted && renderGhostDivider(dividerHandlerChildrenProps)}
|
|
</RowCellContainer>
|
|
);
|
|
};
|
|
|
|
const {isVisible, transaction} = props;
|
|
|
|
return (
|
|
<div>
|
|
<StyledRow
|
|
ref={transactionRowDOMRef}
|
|
visible={isVisible}
|
|
showBorder={isHighlighted}
|
|
cursor={
|
|
isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'
|
|
}
|
|
>
|
|
<ScrollbarManager.Consumer>
|
|
{scrollbarManagerChildrenProps => (
|
|
<DividerHandlerManager.Consumer>
|
|
{dividerHandlerChildrenProps =>
|
|
renderHeader({
|
|
dividerHandlerChildrenProps,
|
|
scrollbarManagerChildrenProps,
|
|
})
|
|
}
|
|
</DividerHandlerManager.Consumer>
|
|
)}
|
|
</ScrollbarManager.Consumer>
|
|
</StyledRow>
|
|
{renderEmbeddedChildrenState()}
|
|
{renderEmbeddedChildren()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getOffset(generation) {
|
|
return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
|
|
}
|
|
|
|
export default NewTraceDetailsTransactionBar;
|
|
|
|
const StyledRow = styled(Row)`
|
|
&,
|
|
${RowCellContainer} {
|
|
overflow: visible;
|
|
}
|
|
`;
|
|
|
|
const ErrorLink = styled(Link)`
|
|
color: ${p => p.theme.error};
|
|
`;
|
|
|
|
const StyledRowRectangle = styled(RowRectangle)`
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
export const StyledZoomIcon = styled(IconZoom)`
|
|
position: absolute;
|
|
left: -20px;
|
|
top: 4px;
|
|
height: 16px;
|
|
width: 18px;
|
|
z-index: 1000;
|
|
background: ${p => p.theme.background};
|
|
padding: 1px;
|
|
border: 1px solid ${p => p.theme.border};
|
|
border-radius: ${space(0.5)};
|
|
`;
|