import {Fragment, type PropsWithChildren, useMemo} from 'react';
import styled from '@emotion/styled';
import type {Location} from 'history';
import * as qs from 'query-string';
import {Button as CommonButton, LinkButton} from 'sentry/components/button';
import {DataSection} from 'sentry/components/events/styles';
import type {LazyRenderProps} from 'sentry/components/lazyRender';
import Link from 'sentry/components/links/link';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {TransactionToProfileButton} from 'sentry/components/profiling/transactionToProfileButton';
import QuestionTooltip from 'sentry/components/questionTooltip';
import {Tooltip} from 'sentry/components/tooltip';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {EventTransaction, Project} from 'sentry/types';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getDuration} from 'sentry/utils/formatters';
import {decodeScalar} from 'sentry/utils/queryString';
import type {ColorOrAlias} from 'sentry/utils/theme';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {
isAutogroupedNode,
isMissingInstrumentationNode,
isSpanNode,
isTraceErrorNode,
isTransactionNode,
} from 'sentry/views/performance/newTraceDetails/guards';
import type {
TraceTree,
TraceTreeNode,
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
const DetailContainer = styled('div')`
display: flex;
flex-direction: column;
gap: ${space(2)};
padding: ${space(1)};
${DataSection} {
padding: 0;
}
`;
const FlexBox = styled('div')`
display: flex;
align-items: center;
`;
const Actions = styled(FlexBox)`
gap: ${space(0.5)};
flex-wrap: wrap;
justify-content: end;
`;
const Title = styled(FlexBox)`
gap: ${space(1)};
flex: none;
width: 50%;
> span {
min-width: 30px;
}
`;
const TitleText = styled('div')`
${p => p.theme.overflowEllipsis}
`;
function TitleWithTestId(props: PropsWithChildren<{}>) {
return
{props.children};
}
const Type = styled('div')`
font-size: ${p => p.theme.fontSizeSmall};
`;
const TitleOp = styled('div')`
font-size: 15px;
font-weight: bold;
${p => p.theme.overflowEllipsis}
`;
const Table = styled('table')`
margin-bottom: 0 !important;
td {
overflow: hidden;
}
`;
const IconTitleWrapper = styled(FlexBox)`
gap: ${space(1)};
min-width: 30px;
`;
const IconBorder = styled('div')<{backgroundColor: string; errored?: boolean}>`
background-color: ${p => p.backgroundColor};
border-radius: ${p => p.theme.borderRadius};
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
svg {
fill: ${p => p.theme.white};
width: 14px;
height: 14px;
}
`;
const Button = styled(CommonButton)`
position: absolute;
top: ${space(0.75)};
right: ${space(0.5)};
`;
const HeaderContainer = styled(Title)`
justify-content: space-between;
overflow: hidden;
width: 100%;
`;
interface EventDetailsLinkProps {
node: TraceTreeNode;
organization: Organization;
}
function EventDetailsLink(props: EventDetailsLinkProps) {
const params = useMemo((): {
eventId: string | undefined;
projectSlug: string | undefined;
} => {
const eventId = props.node.metadata.event_id;
const projectSlug = props.node.metadata.project_slug;
if (eventId && projectSlug) {
return {eventId, projectSlug};
}
if (isSpanNode(props.node) || isAutogroupedNode(props.node)) {
const parent = props.node.parent_transaction;
if (parent?.metadata.event_id && parent?.metadata.project_slug) {
return {
eventId: parent.metadata.event_id,
projectSlug: parent.metadata.project_slug,
};
}
}
return {eventId: undefined, projectSlug: undefined};
}, [props.node]);
const locationDescriptor = useMemo(() => {
const query = {...qs.parse(location.search), legacy: 1};
return {
query: query,
pathname: `/performance/${params.projectSlug}:${params.eventId}/`,
hash: isSpanNode(props.node) ? `#span-${props.node.value.span_id}` : undefined,
};
}, [params.eventId, params.projectSlug, props.node]);
return (
{
trackAnalytics('performance_views.trace_details.view_event_details', {
organization: props.organization,
});
}}
>
{t('View Event Details')}
);
}
const DURATION_COMPARISON_STATUS_COLORS: {
equal: {light: ColorOrAlias; normal: ColorOrAlias};
faster: {light: ColorOrAlias; normal: ColorOrAlias};
slower: {light: ColorOrAlias; normal: ColorOrAlias};
} = {
faster: {
light: 'green100',
normal: 'green300',
},
slower: {
light: 'red100',
normal: 'red300',
},
equal: {
light: 'gray100',
normal: 'gray300',
},
};
const MIN_PCT_DURATION_DIFFERENCE = 10;
type DurationProps = {
baseline: number | undefined;
duration: number;
baseDescription?: string;
ratio?: number;
};
function Duration(props: DurationProps) {
if (typeof props.duration !== 'number' || Number.isNaN(props.duration)) {
return {t('unknown')};
}
if (props.baseline === undefined || props.baseline === 0) {
return {getDuration(props.duration, 2, true)};
}
const delta = props.duration - props.baseline;
const deltaPct = Math.round(Math.abs((delta / props.baseline) * 100));
const status = delta > 0 ? 'slower' : delta < 0 ? 'faster' : 'equal';
const formattedBaseDuration = (
{getDuration(props.baseline, 2, true)}
);
const deltaText =
status === 'equal'
? tct(`equal to the avg of [formattedBaseDuration]`, {
formattedBaseDuration,
})
: status === 'faster'
? tct(`[deltaPct] faster than the avg of [formattedBaseDuration]`, {
formattedBaseDuration,
deltaPct: `${deltaPct}%`,
})
: tct(`[deltaPct] slower than the avg of [formattedBaseDuration]`, {
formattedBaseDuration,
deltaPct: `${deltaPct}%`,
});
return (
{getDuration(props.duration, 2, true)}{' '}
{props.ratio ? `(${(props.ratio * 100).toFixed()}%)` : null}
{deltaPct >= MIN_PCT_DURATION_DIFFERENCE ? (
{deltaText}
) : null}
);
}
function TableRow({
title,
keep,
children,
prefix,
extra = null,
toolTipText,
}: {
children: React.ReactNode;
title: JSX.Element | string | null;
extra?: React.ReactNode;
keep?: boolean;
prefix?: JSX.Element;
toolTipText?: string;
}) {
if (!keep && !children) {
return null;
}
return (
{prefix}
{title}
{toolTipText ? : null}
|
{children}
{extra}
);
}
function getSearchParamFromNode(node: TraceTreeNode) {
if (isTransactionNode(node) || isTraceErrorNode(node)) {
return `id:${node.value.event_id}`;
}
// Issues associated to a span or autogrouped node are not queryable, so we query by
// the parent transaction's id
const parentTransaction = node.parent_transaction;
if ((isSpanNode(node) || isAutogroupedNode(node)) && parentTransaction) {
return `id:${parentTransaction.value.event_id}`;
}
if (isMissingInstrumentationNode(node)) {
throw new Error('Missing instrumentation nodes do not have associated issues');
}
return '';
}
function IssuesLink({
node,
children,
}: {
children: React.ReactNode;
node?: TraceTreeNode;
}) {
const organization = useOrganization();
const params = useParams<{traceSlug?: string}>();
const traceSlug = params.traceSlug?.trim() ?? '';
const dateSelection = useMemo(() => {
const normalizedParams = normalizeDateTimeParams(qs.parse(window.location.search), {
allowAbsolutePageDatetime: true,
});
const start = decodeScalar(normalizedParams.start);
const end = decodeScalar(normalizedParams.end);
const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
return {start, end, statsPeriod};
}, []);
return (
{children}
);
}
const LAZY_RENDER_PROPS: Partial = {
observerOptions: {rootMargin: '50px'},
};
const DurationContainer = styled('span')`
font-weight: bold;
margin-right: ${space(1)};
`;
const Comparison = styled('span')<{status: 'faster' | 'slower' | 'equal'}>`
color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
`;
const Flex = styled('div')`
display: flex;
align-items: center;
`;
const TableValueRow = styled('div')`
display: grid;
grid-template-columns: auto min-content;
gap: ${space(1)};
border-radius: 4px;
background-color: ${p => p.theme.surface200};
margin: 2px;
`;
const StyledQuestionTooltip = styled(QuestionTooltip)`
margin-left: ${space(0.5)};
`;
const StyledPre = styled('pre')`
margin: 0 !important;
background-color: transparent !important;
`;
const TableRowButtonContainer = styled('div')`
padding: 8px 10px;
`;
const ValueTd = styled('td')`
position: relative;
`;
function ProfileLink({
event,
project,
query,
}: {
event: EventTransaction;
project: Project | undefined;
query?: Location['query'];
}) {
const profileId = event.contexts.profile?.profile_id || '';
if (!profileId) {
return null;
}
return profileId && project?.slug ? (
{t('View Profile')}
}
>
{profileId}
) : null;
}
const TraceDrawerComponents = {
DetailContainer,
FlexBox,
Title: TitleWithTestId,
Type,
TitleOp,
HeaderContainer,
Actions,
Table,
IconTitleWrapper,
IconBorder,
EventDetailsLink,
Button,
TitleText,
Duration,
TableRow,
LAZY_RENDER_PROPS,
TableRowButtonContainer,
TableValueRow,
ProfileLink,
IssuesLink,
};
export {TraceDrawerComponents};