mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
import {createRef, Fragment, useEffect} from 'react';
|
|
import type {RouteComponentProps} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import * as Sentry from '@sentry/react';
|
|
|
|
import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
|
|
import MeasurementsPanel from 'sentry/components/events/interfaces/spans/measurementsPanel';
|
|
import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
|
|
import {
|
|
boundsGenerator,
|
|
getMeasurements,
|
|
} from 'sentry/components/events/interfaces/spans/utils';
|
|
import Panel from 'sentry/components/panels/panel';
|
|
import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
|
|
import {
|
|
DividerSpacer,
|
|
ScrollbarContainer,
|
|
VirtualScrollbar,
|
|
VirtualScrollbarGrip,
|
|
} from 'sentry/components/performance/waterfall/miniHeader';
|
|
import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
|
|
import {tct} from 'sentry/locale';
|
|
import type {Organization} from 'sentry/types';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import type EventView from 'sentry/utils/discover/eventView';
|
|
import toPercent from 'sentry/utils/number/toPercent';
|
|
import type {
|
|
TraceError,
|
|
TraceFullDetailed,
|
|
TraceMeta,
|
|
} from 'sentry/utils/performance/quickTrace/types';
|
|
import {
|
|
TraceDetailBody,
|
|
TraceViewContainer,
|
|
TraceViewHeaderContainer,
|
|
} from 'sentry/views/performance/traceDetails/styles';
|
|
import TransactionGroup from 'sentry/views/performance/traceDetails/transactionGroup';
|
|
import type {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types';
|
|
import {
|
|
getTraceInfo,
|
|
hasTraceData,
|
|
isRootTransaction,
|
|
} from 'sentry/views/performance/traceDetails/utils';
|
|
|
|
import LimitExceededMessage from './limitExceededMessage';
|
|
import TraceNotFound from './traceNotFound';
|
|
|
|
type AccType = {
|
|
lastIndex: number;
|
|
numberOfHiddenTransactionsAbove: number;
|
|
renderedChildren: React.ReactNode[];
|
|
};
|
|
|
|
type Props = Pick<RouteComponentProps<{}, {}>, 'location'> & {
|
|
meta: TraceMeta | null;
|
|
organization: Organization;
|
|
traceEventView: EventView;
|
|
traceSlug: string;
|
|
traces: TraceFullDetailed[];
|
|
filteredEventIds?: Set<string>;
|
|
handleLimitChange?: (newLimit: number) => void;
|
|
orphanErrors?: TraceError[];
|
|
traceInfo?: TraceInfo;
|
|
};
|
|
|
|
function TraceHiddenMessage({
|
|
isVisible,
|
|
numberOfHiddenTransactionsAbove,
|
|
numberOfHiddenErrorsAbove,
|
|
}: {
|
|
isVisible: boolean;
|
|
numberOfHiddenErrorsAbove: number;
|
|
numberOfHiddenTransactionsAbove: number;
|
|
}) {
|
|
if (
|
|
!isVisible ||
|
|
(numberOfHiddenTransactionsAbove < 1 && numberOfHiddenErrorsAbove < 1)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const numOfTransaction = <strong>{numberOfHiddenTransactionsAbove}</strong>;
|
|
const numOfErrors = <strong>{numberOfHiddenErrorsAbove}</strong>;
|
|
|
|
const hiddenTransactionsMessage =
|
|
numberOfHiddenTransactionsAbove < 1
|
|
? ''
|
|
: numberOfHiddenTransactionsAbove === 1
|
|
? tct('[numOfTransaction] hidden transaction', {
|
|
numOfTransaction,
|
|
})
|
|
: tct('[numOfTransaction] hidden transactions', {
|
|
numOfTransaction,
|
|
});
|
|
|
|
const hiddenErrorsMessage =
|
|
numberOfHiddenErrorsAbove < 1
|
|
? ''
|
|
: numberOfHiddenErrorsAbove === 1
|
|
? tct('[numOfErrors] hidden error', {
|
|
numOfErrors,
|
|
})
|
|
: tct('[numOfErrors] hidden errors', {
|
|
numOfErrors,
|
|
});
|
|
|
|
return (
|
|
<MessageRow>
|
|
<span key="trace-info-message">
|
|
{hiddenTransactionsMessage}
|
|
{hiddenErrorsMessage && hiddenTransactionsMessage && ', '}
|
|
{hiddenErrorsMessage}
|
|
</span>
|
|
</MessageRow>
|
|
);
|
|
}
|
|
|
|
function isRowVisible(
|
|
row: TraceFullDetailed | TraceError,
|
|
filteredEventIds?: Set<string>
|
|
): boolean {
|
|
return filteredEventIds ? filteredEventIds.has(row.event_id) : true;
|
|
}
|
|
|
|
function generateBounds(traceInfo: TraceInfo) {
|
|
return boundsGenerator({
|
|
traceStartTimestamp: traceInfo.startTimestamp,
|
|
traceEndTimestamp: traceInfo.endTimestamp,
|
|
viewStart: 0,
|
|
viewEnd: 1,
|
|
});
|
|
}
|
|
|
|
export default function TraceView({
|
|
location,
|
|
meta,
|
|
organization,
|
|
traces,
|
|
traceSlug,
|
|
traceEventView,
|
|
filteredEventIds,
|
|
orphanErrors,
|
|
handleLimitChange,
|
|
...props
|
|
}: Props) {
|
|
const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
|
|
const sentrySpan = sentryTransaction?.startChild({
|
|
op: 'trace.render',
|
|
description: 'trace-view-content',
|
|
});
|
|
const hasOrphanErrors = orphanErrors && orphanErrors.length > 0;
|
|
const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0);
|
|
|
|
useEffect(() => {
|
|
trackAnalytics('performance_views.trace_view.view', {
|
|
organization,
|
|
});
|
|
}, [organization]);
|
|
|
|
function renderTransaction(
|
|
transaction: TraceFullDetailed,
|
|
{
|
|
continuingDepths,
|
|
isOrphan,
|
|
isLast,
|
|
index,
|
|
numberOfHiddenTransactionsAbove,
|
|
traceInfo,
|
|
hasGuideAnchor,
|
|
}: {
|
|
continuingDepths: TreeDepth[];
|
|
hasGuideAnchor: boolean;
|
|
index: number;
|
|
isLast: boolean;
|
|
isOrphan: boolean;
|
|
numberOfHiddenTransactionsAbove: number;
|
|
traceInfo: TraceInfo;
|
|
}
|
|
) {
|
|
const {children, event_id: eventId} = transaction;
|
|
// Add 1 to the generation to make room for the "root trace"
|
|
const generation = transaction.generation + 1;
|
|
|
|
const isVisible = isRowVisible(transaction, filteredEventIds);
|
|
|
|
const accumulated: AccType = children.reduce(
|
|
(acc: AccType, child: TraceFullDetailed, idx: number) => {
|
|
const isLastChild = idx === children.length - 1;
|
|
const hasChildren = child.children.length > 0;
|
|
|
|
const result = renderTransaction(child, {
|
|
continuingDepths:
|
|
!isLastChild && hasChildren
|
|
? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
|
|
: continuingDepths,
|
|
isOrphan,
|
|
isLast: isLastChild,
|
|
index: acc.lastIndex + 1,
|
|
numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
|
|
traceInfo,
|
|
hasGuideAnchor: false,
|
|
});
|
|
|
|
acc.lastIndex = result.lastIndex;
|
|
acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
|
|
acc.renderedChildren.push(result.transactionGroup);
|
|
|
|
return acc;
|
|
},
|
|
{
|
|
renderedChildren: [],
|
|
lastIndex: index,
|
|
numberOfHiddenTransactionsAbove: isVisible
|
|
? 0
|
|
: numberOfHiddenTransactionsAbove + 1,
|
|
}
|
|
);
|
|
|
|
return {
|
|
transactionGroup: (
|
|
<Fragment key={eventId}>
|
|
<TraceHiddenMessage
|
|
isVisible={isVisible}
|
|
numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
|
|
numberOfHiddenErrorsAbove={0}
|
|
/>
|
|
<TransactionGroup
|
|
location={location}
|
|
traceViewRef={traceViewRef}
|
|
organization={organization}
|
|
traceInfo={traceInfo}
|
|
transaction={{
|
|
...transaction,
|
|
generation,
|
|
}}
|
|
measurements={
|
|
traces && traces.length > 0
|
|
? getMeasurements(traces[0], generateBounds(traceInfo))
|
|
: undefined
|
|
}
|
|
generateBounds={generateBounds(traceInfo)}
|
|
continuingDepths={continuingDepths}
|
|
isOrphan={isOrphan}
|
|
isLast={isLast}
|
|
index={index}
|
|
isVisible={isVisible}
|
|
hasGuideAnchor={hasGuideAnchor}
|
|
renderedChildren={accumulated.renderedChildren}
|
|
barColor={pickBarColor(transaction['transaction.op'])}
|
|
/>
|
|
</Fragment>
|
|
),
|
|
lastIndex: accumulated.lastIndex,
|
|
numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
|
|
};
|
|
}
|
|
|
|
const traceViewRef = createRef<HTMLDivElement>();
|
|
const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
|
|
|
|
if (!hasTraceData(traces, orphanErrors)) {
|
|
return (
|
|
<TraceNotFound
|
|
meta={meta}
|
|
traceEventView={traceEventView}
|
|
traceSlug={traceSlug}
|
|
location={location}
|
|
organization={organization}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const traceInfo = props.traceInfo || getTraceInfo(traces);
|
|
|
|
const accumulator: {
|
|
index: number;
|
|
numberOfHiddenTransactionsAbove: number;
|
|
traceInfo: TraceInfo;
|
|
transactionGroups: React.ReactNode[];
|
|
} = {
|
|
index: 1,
|
|
numberOfHiddenTransactionsAbove: 0,
|
|
traceInfo,
|
|
transactionGroups: [],
|
|
};
|
|
|
|
let lastIndex: number = 0;
|
|
const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
|
|
(acc, trace, index) => {
|
|
const isLastTransaction = index === traces.length - 1;
|
|
const hasChildren = trace.children.length > 0;
|
|
const isNextChildOrphaned =
|
|
!isLastTransaction && traces[index + 1].parent_span_id !== null;
|
|
|
|
const result = renderTransaction(trace, {
|
|
...acc,
|
|
// if the root of a subtrace has a parent_span_id, then it must be an orphan
|
|
isOrphan: !isRootTransaction(trace),
|
|
isLast: isLastTransaction && !hasOrphanErrors,
|
|
continuingDepths:
|
|
(!isLastTransaction && hasChildren) || hasOrphanErrors
|
|
? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
|
|
: [],
|
|
hasGuideAnchor: index === 0,
|
|
});
|
|
|
|
acc.index = result.lastIndex + 1;
|
|
lastIndex = Math.max(lastIndex, result.lastIndex);
|
|
acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
|
|
acc.transactionGroups.push(result.transactionGroup);
|
|
return acc;
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Build transaction groups for orphan errors
|
|
let numOfHiddenErrorsAbove = 0;
|
|
let totalNumOfHiddenErrors = 0;
|
|
if (hasOrphanErrors) {
|
|
orphanErrors.forEach((error, index) => {
|
|
const isLastError = index === orphanErrors.length - 1;
|
|
const isVisible = isRowVisible(error, filteredEventIds);
|
|
const currentHiddenCount = numOfHiddenErrorsAbove;
|
|
|
|
if (!isVisible) {
|
|
numOfHiddenErrorsAbove += 1;
|
|
totalNumOfHiddenErrors += 1;
|
|
} else {
|
|
numOfHiddenErrorsAbove = 0;
|
|
}
|
|
|
|
transactionGroups.push(
|
|
<Fragment key={error.event_id}>
|
|
<TraceHiddenMessage
|
|
isVisible={isVisible}
|
|
numberOfHiddenTransactionsAbove={
|
|
index === 0 ? numberOfHiddenTransactionsAbove : 0
|
|
}
|
|
numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
|
|
/>
|
|
<TransactionGroup
|
|
location={location}
|
|
organization={organization}
|
|
traceViewRef={traceViewRef}
|
|
traceInfo={traceInfo}
|
|
transaction={{
|
|
...error,
|
|
generation: 1,
|
|
}}
|
|
generateBounds={generateBounds(traceInfo)}
|
|
measurements={
|
|
traces && traces.length > 0
|
|
? getMeasurements(traces[0], generateBounds(traceInfo))
|
|
: undefined
|
|
}
|
|
continuingDepths={[]}
|
|
isOrphan
|
|
isLast={isLastError}
|
|
index={lastIndex + index + 1}
|
|
isVisible={isVisible}
|
|
hasGuideAnchor={index === 0 && transactionGroups.length === 0}
|
|
renderedChildren={[]}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
});
|
|
}
|
|
|
|
const bounds = generateBounds(traceInfo);
|
|
const measurements =
|
|
traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
|
|
? getMeasurements(traces[0], bounds)
|
|
: undefined;
|
|
|
|
const traceView = (
|
|
<TraceDetailBody>
|
|
<DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
|
|
<DividerHandlerManager.Consumer>
|
|
{({dividerPosition}) => (
|
|
<ScrollbarManager.Provider
|
|
dividerPosition={dividerPosition}
|
|
interactiveLayerRef={virtualScrollbarContainerRef}
|
|
isEmbedded
|
|
>
|
|
<StyledTracePanel>
|
|
<TraceViewHeaderContainer>
|
|
<ScrollbarManager.Consumer>
|
|
{({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
|
|
return (
|
|
<ScrollbarContainer
|
|
ref={virtualScrollbarContainerRef}
|
|
style={{
|
|
// the width of this component is shrunk to compensate for half of the width of the divider line
|
|
width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
|
|
}}
|
|
onScroll={onScroll}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 0,
|
|
height: '1px',
|
|
}}
|
|
ref={scrollBarAreaRef}
|
|
/>
|
|
<VirtualScrollbar
|
|
data-type="virtual-scrollbar"
|
|
ref={virtualScrollbarRef}
|
|
onMouseDown={onDragStart}
|
|
>
|
|
<VirtualScrollbarGrip />
|
|
</VirtualScrollbar>
|
|
</ScrollbarContainer>
|
|
);
|
|
}}
|
|
</ScrollbarManager.Consumer>
|
|
<DividerSpacer />
|
|
{measurements ? (
|
|
<MeasurementsPanel
|
|
measurements={measurements}
|
|
generateBounds={bounds}
|
|
dividerPosition={dividerPosition}
|
|
/>
|
|
) : null}
|
|
</TraceViewHeaderContainer>
|
|
<TraceViewContainer ref={traceViewRef}>
|
|
<TransactionGroup
|
|
location={location}
|
|
organization={organization}
|
|
traceInfo={traceInfo}
|
|
transaction={{
|
|
traceSlug,
|
|
generation: 0,
|
|
'transaction.duration':
|
|
traceInfo.endTimestamp - traceInfo.startTimestamp,
|
|
children: traces,
|
|
start_timestamp: traceInfo.startTimestamp,
|
|
timestamp: traceInfo.endTimestamp,
|
|
}}
|
|
measurements={measurements}
|
|
generateBounds={bounds}
|
|
continuingDepths={[]}
|
|
isOrphan={false}
|
|
isLast={false}
|
|
index={0}
|
|
isVisible
|
|
hasGuideAnchor={false}
|
|
renderedChildren={transactionGroups}
|
|
barColor={pickBarColor('')}
|
|
onlyOrphanErrors={onlyOrphanErrors}
|
|
traceViewRef={traceViewRef}
|
|
numOfOrphanErrors={orphanErrors?.length}
|
|
/>
|
|
<TraceHiddenMessage
|
|
isVisible
|
|
numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
|
|
numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
|
|
/>
|
|
<LimitExceededMessage
|
|
traceInfo={traceInfo}
|
|
organization={organization}
|
|
traceEventView={traceEventView}
|
|
meta={meta}
|
|
handleLimitChange={handleLimitChange}
|
|
/>
|
|
</TraceViewContainer>
|
|
</StyledTracePanel>
|
|
</ScrollbarManager.Provider>
|
|
)}
|
|
</DividerHandlerManager.Consumer>
|
|
</DividerHandlerManager.Provider>
|
|
</TraceDetailBody>
|
|
);
|
|
|
|
sentrySpan?.finish();
|
|
|
|
return traceView;
|
|
}
|
|
|
|
export const StyledTracePanel = styled(Panel)`
|
|
height: 100%;
|
|
overflow-x: visible;
|
|
|
|
${TraceViewContainer} {
|
|
overflow-x: visible;
|
|
}
|
|
`;
|