Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/reverse-span-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

feat: show spans referencing the current span in the trace span detail panel ("Referenced By" section)
75 changes: 64 additions & 11 deletions packages/app/src/components/DBRowOverviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,11 @@ import { ExceptionSubpanel } from './ExceptionSubpanel';
import { NetworkPropertySubpanel } from './NetworkPropertyPanel';
import { SpanEventsSubpanel } from './SpanEventsSubpanel';
import { getValidSpanLinks, SpanLinksSubpanel } from './SpanLinksSubpanel';
import { SpansReverseLinksSubpanel } from './SpansReverseLinksSubpanel';

const EMPTY_OBJ = {};
export function RowOverviewPanel({
source,
rowId,
aliasWith,
dateRange,
hideHeader = false,
flush = false,
'data-testid': dataTestId,
}: {

interface DBRowOverviewPanelProps {
source: TSource;
rowId: string | undefined | null;
aliasWith?: WithClause[];
Expand All @@ -41,7 +35,22 @@ export function RowOverviewPanel({
// surrounding chrome (e.g. the tab bar in the trace span detail panel).
flush?: boolean;
'data-testid'?: string;
}) {
// All spans belonging to the current trace, used to find spans that
// reference the currently selected span (reverse span links). This must
// be the full trace's span list, NOT just the single selected row.
allTraceRows?: Array<Record<string, any>>;
}

export function RowOverviewPanel({
source,
rowId,
aliasWith,
dateRange,
hideHeader = false,
flush = false,
'data-testid': dataTestId,
allTraceRows,
}: DBRowOverviewPanelProps) {
const contentPx = flush ? 0 : 'md';
const { data } = useRowData({ source, rowId, aliasWith, dateRange });
const { onPropertyAddClick, generateSearchUrl, onOpenLinkedTrace } =
Expand All @@ -50,7 +59,7 @@ export function RowOverviewPanel({
const highlightedAttributeValues = useMemo(() => {
const attributeExpressions =
source.kind === SourceKind.Trace || source.kind === SourceKind.Log
? (source.highlightedRowAttributeExpressions ?? [])
? source.highlightedRowAttributeExpressions ?? []
: [];

return data
Expand Down Expand Up @@ -195,6 +204,31 @@ export function RowOverviewPanel({
return getValidSpanLinks(firstRow?.__hdx_span_links).length > 0;
}, [firstRow?.__hdx_span_links]);

// P1 fix: scan the FULL trace's spans (allTraceRows), not data.data,
// which only ever contains the single selected row from useRowData.
const hasReverseSpanLinks = useMemo(() => {
if (!firstRow?.SpanId || !Array.isArray(allTraceRows)) {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return false;
}
const currentSpanId = String(firstRow.SpanId);
for (const row of allTraceRows) {
if (!row || typeof row !== 'object') continue;
const sl = row.__hdx_span_links;
if (!Array.isArray(sl)) continue;
for (const link of sl) {
if (
link &&
typeof link === 'object' &&
'SpanId' in link &&
link.SpanId === currentSpanId
) {
return true;
}
}
Comment on lines +216 to +227

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Visibility accepts invalid span links

When a trace row has the selected SpanId but lacks a string TraceId or an Attributes value, hasReverseSpanLinks displays the Referenced By accordion even though getValidSpanLinks rejects that entry, causing the expanded panel to report that no spans reference the selected span.

Suggested change
const sl = row.__hdx_span_links;
if (!Array.isArray(sl)) continue;
for (const link of sl) {
if (
link &&
typeof link === 'object' &&
'SpanId' in link &&
link.SpanId === currentSpanId
) {
return true;
}
}
const validLinks = getValidSpanLinks(row.__hdx_span_links);
if (validLinks.some(link => link.SpanId === currentSpanId)) {
return true;
}

Knowledge Base Used: App Components and Charts

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

}
return false;
}, [allTraceRows, firstRow?.SpanId]);

const mainContentColumn = getEventBody(source);
const mainContent = isString(firstRow?.['__hdx_body'])
? firstRow['__hdx_body']
Expand Down Expand Up @@ -226,6 +260,7 @@ export function RowOverviewPanel({
'exception',
'spanEvents',
'spanLinks',
'reverseSpanLinks',
'network',
'resourceAttributes',
'eventAttributes',
Expand Down Expand Up @@ -342,6 +377,24 @@ export function RowOverviewPanel({
</Accordion.Panel>
</Accordion.Item>
)}
{hasReverseSpanLinks && (
<Accordion.Item value="reverseSpanLinks">
<Accordion.Control>
<Text size="sm" ps="md">
Referenced By
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<SpansReverseLinksSubpanel
rows={allTraceRows}
currentSpanId={firstRow?.SpanId as string | undefined}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
onOpenTrace={onOpenLinkedTrace}
/>
</Box>
Comment on lines 377 to +394

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 User-facing changeset is missing

This adds the user-visible Referenced By panel to the published application package without a corresponding .changeset/ entry, so the release metadata omits this behavior change.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

</Accordion.Panel>
</Accordion.Item>
)}

{Object.keys(resourceAttributes).length > 0 && (
<Accordion.Item value="resourceAttributes">
Expand Down
43 changes: 31 additions & 12 deletions packages/app/src/components/DBTracePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ import {
IconLayoutSidebarRight,
IconX,
} from '@tabler/icons-react';

import { DBTraceWaterfallChartContainer } from '@/components/DBTraceWaterfallChart';
import {
DBTraceWaterfallChartContainer,
useFilteredEventsAroundFocus,
} from '@/components/DBTraceWaterfallChart';
import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor';
import useResizable from '@/hooks/useResizable';
import { WithClause } from '@/hooks/useRowWhere';
import { useSource, useUpdateSource } from '@/source';
import TabBar from '@/TabBar';
import { parseAsJsonEncoded } from '@/utils/queryParsers';

import DBInfraPanel from './DBInfraPanel';
import { RowDataPanel, rowHasK8sContext, useRowData } from './DBRowDataPanel';
import { RowOverviewPanel } from './DBRowOverviewPanel';
Expand All @@ -54,7 +55,6 @@ import SourceSchemaPreview, {
isSourceSchemaPreviewEnabled,
} from './SourceSchemaPreview';
import { SourceSelectControlled } from './SourceSelect';

import resizeStyles from '@/../styles/ResizablePanel.module.scss';

type EventRowWhere = {
Expand All @@ -75,6 +75,7 @@ const eventRowWhereSchema = z.object({
aliasWith: z.array(WithClauseSchema),
traceId: z.string().optional(),
});

const eventRowWhereParser = parseAsJsonEncoded<EventRowWhere>(
eventRowWhereSchema.parse,
);
Expand Down Expand Up @@ -105,21 +106,25 @@ function SpanDetailPanel({
onClose,
isSideLayout,
onToggleLayout,
allTraceRows,
}: {
source: TSource;
rowId: string;
aliasWith?: WithClause[];
onClose: () => void;
isSideLayout: boolean;
onToggleLayout: () => void;
// All spans belonging to the current trace (not just the selected span),
// used by RowOverviewPanel to render the "Referenced By" reverse-span-link
// section. Passed down from DBTracePanel, where the trace's full span list
// is already fetched for the waterfall chart.
allTraceRows?: Array<Record<string, any>>;
}) {
const [displayedTab, setDisplayedTab] = useState<SpanDetailTab>(
SpanDetailTab.Overview,
);

const { data: rowData } = useRowData({ source, rowId, aliasWith });
const normalizedRow = rowData?.data?.[0];

// The selected event may come from a different source than the search this
// panel was opened from (e.g. a log event on a trace opened in the Traces
// view). Rebind search-url generation to the event's own source and drop
Expand All @@ -129,12 +134,10 @@ function SpanDetailPanel({
() => deriveRowSidePanelContextForSource(parentContext, source),
[parentContext, source],
);

const hasK8sContext = useMemo(
() => rowHasK8sContext(source, normalizedRow),
[source, normalizedRow],
);

// If the selected span loses k8s context (e.g. switching spans) while the
// Infrastructure tab is active, fall back to Overview so we don't show a
// blank panel. Derived rather than synced via an effect.
Expand Down Expand Up @@ -217,6 +220,7 @@ function SpanDetailPanel({
source={source}
rowId={rowId}
aliasWith={aliasWith}
allTraceRows={allTraceRows}
flush
/>
)}
Expand Down Expand Up @@ -289,6 +293,7 @@ export default function DBTracePanel({
: childSourceData?.kind === SourceKind.Log
? childSourceData
: null;

const traceSourceData =
parentSourceData?.kind === SourceKind.Trace
? parentSourceData
Expand Down Expand Up @@ -335,6 +340,7 @@ export default function DBTracePanel({
'',
},
});

useEffect(() => {
if (
parentSourceData &&
Expand Down Expand Up @@ -363,10 +369,8 @@ export default function DBTracePanel({

const { size: rightPanelSize, startResize: startHorizontalResize } =
useResizable(35, 'right');

const { size: bottomPanelSize, startResize: startVerticalResize } =
useResizable(40, 'top');

const detailPanelSize = isSideLayout ? rightPanelSize : bottomPanelSize;

const handleCloseSpanDetails = useCallback(() => {
Expand All @@ -381,6 +385,22 @@ export default function DBTracePanel({
return traceSourceData;
}, [selectedSpan, logSourceData, traceSourceData]);

// Fetch the full set of spans for this trace so the span detail panel can
// determine which spans reference the currently selected span ("Referenced
// By" / reverse span links). This reuses the same hook the waterfall chart
// uses for its trace-side rows, so we're not issuing a materially different
// query. Only enabled once we have a real trace source + traceId; the
// `tableSource` fallback below only exists to satisfy the hook's non-null
// parameter type while disabled and is never actually queried.
const isTraceReady = traceSourceData?.kind === SourceKind.Trace && !!traceId;
const { rows: allTraceRows } = useFilteredEventsAroundFocus({
tableSource: traceSourceData ?? logSourceData ?? ({} as any),
focusDate,
dateRange,
traceId: traceId ?? '',
enabled: isTraceReady,
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

return (
<div
data-testid={dataTestId}
Expand Down Expand Up @@ -488,7 +508,6 @@ export default function DBTracePanel({
/>
)}
</div>

{selectedSpan != null && (
<Box
className={
Expand All @@ -501,7 +520,6 @@ export default function DBTracePanel({
}
/>
)}

{traceSourceData != null &&
selectedSpan != null &&
selectedSpanSource != null && (
Expand Down Expand Up @@ -531,6 +549,7 @@ export default function DBTracePanel({
onToggleLayout={() =>
setDetailLayout(isSideLayout ? 'bottom' : 'side')
}
allTraceRows={allTraceRows}
/>
</div>
)}
Expand Down
24 changes: 10 additions & 14 deletions packages/app/src/components/DBTraceWaterfallChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,17 @@ function getConfig(
SpanId: source.spanIdExpression ?? '',
ParentSpanId:
source.kind === SourceKind.Trace
? (source.parentSpanIdExpression ?? '')
? source.parentSpanIdExpression ?? ''
: '',
StatusCode:
source.kind === SourceKind.Trace
? (source.statusCodeExpression ?? '')
: '',
source.kind === SourceKind.Trace ? source.statusCodeExpression ?? '' : '',
ServiceName: source.serviceNameExpression ?? '',
SeverityText:
source.kind === SourceKind.Log
? (source.severityTextExpression ?? '')
: '',
source.kind === SourceKind.Log ? source.severityTextExpression ?? '' : '',
SpanAttributes: source.eventAttributesExpression ?? '',
SpanEvents:
source.kind === SourceKind.Trace
? (source.spanEventsValueExpression ?? '')
? source.spanEventsValueExpression ?? ''
: '',
};

Expand Down Expand Up @@ -424,7 +420,7 @@ export function useEventsAroundFocus({
};
}

function useFilteredEventsAroundFocus(
export function useFilteredEventsAroundFocus(
args: Parameters<typeof useEventsAroundFocus>[0],
) {
const filtered = useEventsAroundFocus(args);
Expand Down Expand Up @@ -625,11 +621,11 @@ export function DBTraceWaterfallChartContainer({
traceWhereLanguage:
traceWhereLanguage === 'sql' || traceWhereLanguage === 'lucene'
? traceWhereLanguage
: (getStoredLanguage() ?? 'lucene'),
: getStoredLanguage() ?? 'lucene',
logWhereLanguage:
logWhereLanguage === 'sql' || logWhereLanguage === 'lucene'
? logWhereLanguage
: (getStoredLanguage() ?? 'lucene'),
: getStoredLanguage() ?? 'lucene',
},
});

Expand Down Expand Up @@ -1093,7 +1089,7 @@ export function DBTraceWaterfallChartContainer({
type === SourceKind.Log
? getChartColorSuccess()
: serviceName
? (serviceColorMap.get(serviceName) ?? '#6A7077')
? serviceColorMap.get(serviceName) ?? '#6A7077'
: '#6A7077';

return {
Expand Down Expand Up @@ -1515,9 +1511,9 @@ export function DBTraceWaterfallChartContainer({
flattenedNodes.length > 0 ? (
<div className="my-3">All items are hidden by filters</div>
) : (
(emptyState ?? (
emptyState ?? (
<div className="my-3">No matching spans or logs found</div>
))
)
)
) : (
<TimelineChart
Expand Down
Loading
Loading