Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(traceview): Add previous trace link #85627

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
12 changes: 12 additions & 0 deletions static/app/components/events/interfaces/spans/types.tsx
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ export type RawSpanType = {
description?: string;
exclusive_time?: number;
hash?: string;
links?: SpanLink[];
op?: string;
origin?: string;
parent_span_id?: string;
@@ -199,6 +200,16 @@ export enum TickAlignment {
CENTER = 2,
}

type AttributeValue = string | number | boolean | string[] | number[] | boolean[];

export type SpanLink = {
span_id: string;
trace_id: string;
attributes?: Record<string, AttributeValue> & {'sentry.link.type'?: AttributeValue};
parent_span_id?: string;
sampled?: boolean;
};

export type TraceContextType = {
client_sample_rate?: number;
count?: number;
@@ -207,6 +218,7 @@ export type TraceContextType = {
exclusive_time?: number;
frequency?: number;
hash?: string;
links?: SpanLink[];
op?: string;
parent_span_id?: string;
span_id?: string;
Original file line number Diff line number Diff line change
@@ -8,9 +8,11 @@ import {space} from 'sentry/styles/space';
import type {EventTransaction} from 'sentry/types/event';
import type {UseApiQueryResult} from 'sentry/utils/queryClient';
import type RequestError from 'sentry/utils/requestError/requestError';
import useOrganization from 'sentry/utils/useOrganization';
import type {SectionKey} from 'sentry/views/issueDetails/streamline/context';
import {FoldSection} from 'sentry/views/issueDetails/streamline/foldSection';
import {TraceContextVitals} from 'sentry/views/performance/newTraceDetails/traceContextVitals';
import {TraceLinkNavigationButton} from 'sentry/views/performance/newTraceDetails/traceLinksNavigation/traceLinkNavigationButton';
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
import {TraceViewLogsSection} from 'sentry/views/performance/newTraceDetails/traceOurlogs';

@@ -35,8 +37,25 @@ export function TraceContextPanel({tree, rootEvent}: Props) {
);
}, [rootEvent.data]);

const organization = useOrganization();
const showLinkedTraces = organization?.features.includes('trace-view-linked-traces');

return (
<Container>
{showLinkedTraces && (
<TraceLinksNavigationContainer>
<TraceLinkNavigationButton
direction={'previous'}
isLoading={rootEvent.isLoading}
traceContext={rootEvent.data?.contexts.trace}
currentTraceTimestamps={{
start: rootEvent.data?.startTimestamp,
end: rootEvent.data?.endTimestamp,
}}
/>
</TraceLinksNavigationContainer>
)}

<VitalMetersContainer>
<TraceContextVitals tree={tree} />
</VitalMetersContainer>
@@ -77,3 +96,10 @@ const TraceTagsContainer = styled('div')`
border-radius: ${p => p.theme.borderRadius};
padding: ${space(1)};
`;

const TraceLinksNavigationContainer = styled('div')`
display: flex;
justify-content: space-between;
flex-direction: row;
margin: ${space(1)} 0;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {useMemo} from 'react';
import styled from '@emotion/styled';

import type {
SpanLink,
TraceContextType,
} from 'sentry/components/events/interfaces/spans/types';
import Link from 'sentry/components/links/link';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {Tooltip} from 'sentry/components/tooltip';
import {IconChevron} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
import {isEmptyTrace} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';

// Currently, we only support previous but component can be used for 'next trace' in the future
type ConnectedTraceConnection = 'previous'; // | 'next';

const LINKED_TRACE_MAX_DURATION = 3600; // 1h in seconds

function useIsTraceAvailable(
traceLink?: SpanLink,
previousTraceTimestamp?: number
): {
isAvailable: boolean;
isLoading: boolean;
} {
const trace = useTrace({
traceSlug: traceLink?.trace_id,
timestamp: previousTraceTimestamp,
});
Copy link
Member

@mjq mjq Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trace fetching performance looks okay on our current system, but the EAP equivalent is currently too expensive for this purpose IMO. It's still in development (only internal users) so not currently a blocker and hopefully this is resolved before release, but if it doesn't then we'll probably want to replace this with an endpoint specifically for this check (to avoid loading a full trace) as you mentioned on Slack.

Copy link
Contributor

@Abdkhan14 Abdkhan14 Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today making use of an explore query as such should work right @mjq? It should be future proof

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, great call. We could make an explore query for count(span.duration) where trace:<trace_id>, and if the result is > 0, show the trace link?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see a way to do this in the explore interface, but if we could instead search for min(timestamp) we'd be able to both confirm trace existence and also get the timestamp needed for the trace details URL in a single call, which would be slick.

Copy link
Contributor

@Abdkhan14 Abdkhan14 Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjq we don't use the events endpoint for that query we use a /traces/ endpoint that comes with numOfspans and the timestamp linked to the trace.

The only concern I have now is that, since the tracelink doesn't come with a timestamp, how do we set the date range for that explore query? Either of, 'querying by the current trace's timestamp with a buffer' or
'quering by max date range at all times' doesn't seem to be right 🤔

Copy link
Member

@mjq mjq Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point I think I'd suggest putting this behind a feature flag but otherwise merging it as is so we can start gathering real data from a subset of customers. We can loop back on optimizing the call with one of the options in this thread (or something else) after that. None of the options are perfect so I imagine it'll take some iterating, but with the feature flag we don't have to block the work so far on that iteration. How do you feel @s1gr1d @Lms24 ? (I guess this addresses "does this need a feature flag guard?" from the PR description 😄 )

Copy link
Member

@s1gr1d s1gr1d Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this thread, I am not 100% sure what should be the path forward now :D
Should we either:

  1. Leave as-is and (don't?) put it behind a feature flag
  2. Use another API request and (don't?) put it behind a feature flag

And if we use another request here: Which one do you mean exactly here? I am not too familiar with the codebase so maybe you could tell me which hooks are already available for that.

As for the timestamp: The attached "previous trace" data only provides the spanId and traceId so I cannot know a timestamp at the point of fetching for this trace but we can define a time range. We know that the end timestamp of the previous trace would be anytime before the start timestamp of the trace that is currently shown in the trace view. And if we say that the maximum duration between linked traces is 1h, we can define the start time as "end time minus 1h". So then we would have this for the previous trace query:

  • start timestamp: start time of current trace minus 1h
  • end timestamp: start time of current trace

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start timestamp: end time of current trace minus 1h

shouldn't this be start time of current trace minus 1h?


const isAvailable = useMemo(() => {
if (!traceLink) {
return false;
}

return Boolean(trace.data && !isEmptyTrace(trace.data));
}, [traceLink, trace]);

return {
isAvailable,
isLoading: trace.isLoading,
};
}

type TraceLinkNavigationButtonProps = {
currentTraceTimestamps: {end?: number; start?: number};
direction: ConnectedTraceConnection;
isLoading?: boolean;
traceContext?: TraceContextType;
};

export function TraceLinkNavigationButton({
direction,
traceContext,
isLoading,
currentTraceTimestamps,
}: TraceLinkNavigationButtonProps) {
const organization = useOrganization();
const location = useLocation();

const traceLink = traceContext?.links?.find(
link => link.attributes?.['sentry.link.type'] === `${direction}_trace`
);

// We connect traces over a 1h period - As we don't have timestamps of the linked trace, it is calculated based on this timeframe
const linkedTraceTimestamp =
direction === 'previous' && currentTraceTimestamps.start
? currentTraceTimestamps.start - LINKED_TRACE_MAX_DURATION // Earliest start times of previous trace
: // : direction === 'next' && currentTraceTimestamps.end
// ? currentTraceTimestamps.end + LINKED_TRACE_MAX_DURATION
undefined;

const dateSelection = useMemo(
() => normalizeDateTimeParams(location.query),
[location.query]
);

const {isAvailable: isLinkedTraceAvailable} = useIsTraceAvailable(
traceLink,
linkedTraceTimestamp
);

if (isLoading) {
// We don't show a placeholder/skeleton here as it would cause layout shifts most of the time.
// Most traces don't have a next/previous trace and the hard to avoid layout shift should only occur if the actual button can be shown.
return null;
}

if (traceLink && isLinkedTraceAvailable) {
return (
<TraceLink
color="gray500"
to={getTraceDetailsUrl({
traceSlug: traceLink.trace_id,
spanId: traceLink.span_id,
dateSelection,
timestamp: linkedTraceTimestamp,
location,
organization,
})}
>
<IconChevron direction="left" />
<TraceLinkText>{t('Go to Previous Trace')}</TraceLinkText>
</TraceLink>
);
}

if (traceLink?.sampled === false) {
return (
<StyledTooltip
position="right"
title={t(
'Trace contains a link to unsampled trace. Increase traces sample rate in SDK settings to see more connected traces'
)}
>
<TraceLinkText>{t('Previous trace not available')}</TraceLinkText>
</StyledTooltip>
);
}

// If there is no linked trace or an undefined sampling decision
return null;
}

const StyledTooltip = styled(Tooltip)`
padding: ${space(0.5)} ${space(1)};
text-decoration: underline dotted
${p => (p.disabled ? p.theme.gray300 : p.theme.gray300)};
`;

const TraceLink = styled(Link)`
font-weight: ${p => p.theme.fontWeightNormal};
color: ${p => p.theme.subText};
padding: ${space(0.25)} ${space(0.5)};
display: flex;
align-items: center;
`;

const TraceLinkText = styled('span')`
line-height: normal;
`;