Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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': minor
---

Show reverse span links and resolved span-link details in the span detail Overview panel.
59 changes: 55 additions & 4 deletions packages/app/src/components/DBRowOverviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Accordion, Box, Flex, Text } from '@mantine/core';
import { WithClause } from '@/hooks/useRowWhere';
import { getEventBody } from '@/source';
import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes';
import { resolveRowTimestampAnchor } from '@/utils/rowTimestamps';

import {
getJSONColumnNames,
Expand All @@ -18,8 +19,10 @@ import { RowSidePanelContext } from './DBRowSidePanel';
import DBRowSidePanelHeader from './DBRowSidePanelHeader';
import EventTag from './EventTag';
import { ExceptionSubpanel } from './ExceptionSubpanel';
import { useLinkedSpanDetails, useReverseSpanLinks } from './linkedSpans';
import { NetworkPropertySubpanel } from './NetworkPropertyPanel';
import { SpanEventsSubpanel } from './SpanEventsSubpanel';
import { SpanLinkedFromSubpanel } from './SpanLinkedFromSubpanel';
import { getValidSpanLinks, SpanLinksSubpanel } from './SpanLinksSubpanel';

const EMPTY_OBJ = {};
Expand Down Expand Up @@ -191,9 +194,37 @@ export function RowOverviewPanel({
);
}, [firstRow?.__hdx_span_events]);

const hasSpanLinks = useMemo(() => {
return getValidSpanLinks(firstRow?.__hdx_span_links).length > 0;
const validSpanLinks = useMemo(() => {
return getValidSpanLinks(firstRow?.__hdx_span_links);
}, [firstRow?.__hdx_span_links]);
const hasSpanLinks = validSpanLinks.length > 0;

const rowMeta = data?.meta;
const linkAnchorDate = useMemo(
() =>
resolveRowTimestampAnchor({
timestampValueExpression: source.timestampValueExpression,
row: firstRow,
meta: rowMeta,
}),
[source.timestampValueExpression, firstRow, rowMeta],
);

const rowTraceId = firstRow?.__hdx_trace_id;
const rowSpanId = firstRow?.__hdx_span_id;

const { links: reverseSpanLinks } = useReverseSpanLinks({
source,
traceId: typeof rowTraceId === 'string' ? rowTraceId : undefined,
spanId: typeof rowSpanId === 'string' ? rowSpanId : undefined,
anchorDate: linkAnchorDate,
});

const { details: linkedSpanDetails } = useLinkedSpanDetails({
source,
links: validSpanLinks,
anchorDate: linkAnchorDate,
});

const mainContentColumn = getEventBody(source);
const mainContent = isString(firstRow?.['__hdx_body'])
Expand Down Expand Up @@ -226,6 +257,7 @@ export function RowOverviewPanel({
'exception',
'spanEvents',
'spanLinks',
'linkedFrom',
'network',
'resourceAttributes',
'eventAttributes',
Expand Down Expand Up @@ -328,14 +360,33 @@ export function RowOverviewPanel({
{hasSpanLinks && (
<Accordion.Item value="spanLinks">
<Accordion.Control>
<Text size="sm" ps="md">
<Text size="sm" ps={contentPx}>
Span Links
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<Box ps={contentPx}>
<SpanLinksSubpanel
spanLinks={firstRow?.__hdx_span_links}
linkedSpanDetails={linkedSpanDetails}
onOpenTrace={onOpenLinkedTrace}
/>
</Box>
</Accordion.Panel>
</Accordion.Item>
)}

{reverseSpanLinks.length > 0 && (
<Accordion.Item value="linkedFrom">
<Accordion.Control>
<Text size="sm" ps={contentPx}>
Linked from
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box ps={contentPx}>
<SpanLinkedFromSubpanel
links={reverseSpanLinks}
onOpenTrace={onOpenLinkedTrace}
/>
</Box>
Expand Down
22 changes: 22 additions & 0 deletions packages/app/src/components/DBRowSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,20 @@ export const DBRowSidePanelInner = ({
]),
SqlString.format('?=?', [SqlString.raw(spanIdExpression), link.SpanId]),
].join(' AND ');
// A link often points right back at the row one level up (forward and
// reverse span links come in pairs). Pop back to that breadcrumb instead
// of pushing an endless A <-> B trail. Only when no nav drilldown sits
// on top: popOne would pop the nav entry, not the source frame.
Comment thread
karl-power marked this conversation as resolved.
Resolved
const topFrame =
sourceStack.length > 0 ? sourceStack[sourceStack.length - 1] : null;
if (
navStack.length === 0 &&
topFrame?.originRowId != null &&
topFrame.originRowId === rowId
) {
popOne();
return;
}
// Mark this frame so its breadcrumb switches from the `Trace <id>`
// fallback below to the landed span name once the row loads.
spanLinkFrameRowIdsRef.current.add(rowId);
Expand All @@ -634,13 +648,21 @@ export const DBRowSidePanelInner = ({
label: `Trace ${link.TraceId.slice(0, 8)}`,
sourceKind: traceSourceData.kind as SourceKind,
aliasWith: [],
// The current row's canonical trace/span row id: the exact string a
// link hop targeting this row would compute, so the pop-back check
// above can compare by equality.
originRowId: traceSpanRowId,
});
},
[
traceSourceData,
traceIdExpression,
spanIdExpression,
handleSourceStackPush,
sourceStack,
navStack,
popOne,
traceSpanRowId,
],
);

Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/components/DBRowSidePanel.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const SourceFrameSchema = z.object({
* trace) leave it unset.
*/
focusTimestamp: z.string().optional(),
/**
* Row id of the row displayed when this frame was pushed, in the same
* canonical form a navigation back to that row would use. Rows one hop
* apart somtimes reference each other (e.g. span links).
*/
originRowId: z.string().optional(),
});

export type SourceFrame = z.infer<typeof SourceFrameSchema>;
Expand Down
130 changes: 130 additions & 0 deletions packages/app/src/components/SpanLinkedFromSubpanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ErrorBoundary } from 'react-error-boundary';
import { Anchor, Button, Stack, Tooltip } from '@mantine/core';
import {
IconArrowUpRight,
IconChevronDown,
IconChevronUp,
} from '@tabler/icons-react';

import { SectionWrapper, useShowMoreRows } from './ExceptionSubpanel';
import {
LinkedSpanDetails,
linkedSpanKey,
LinkedSpanMetaLine,
} from './linkedSpans';
import { SpanLinkData } from './SpanLinksSubpanel';

function LinkedSpanRow({
link,
onOpenTrace,
}: {
link: LinkedSpanDetails;
onOpenTrace?: (link: SpanLinkData) => void;
}) {
return (
<Stack gap={4}>
<Tooltip
withArrow
position="top"
maw={420}
multiline
label={
<div className="font-monospace" style={{ fontSize: 11 }}>
<div style={{ wordBreak: 'break-all' }}>Trace: {link.TraceId}</div>
<div style={{ wordBreak: 'break-all' }}>Span: {link.SpanId}</div>
</div>
}
>
<Anchor
component="button"
type="button"
data-testid="linked-from-open-span"
onClick={() =>
onOpenTrace?.({
TraceId: link.TraceId,
SpanId: link.SpanId,
TraceState: '',
Attributes: {},
})
}
size="sm"
fw={500}
className="d-inline-flex align-items-center"
>
<IconArrowUpRight size={14} className="me-1" />
{link.spanName || 'Open span'}
</Anchor>
</Tooltip>
<LinkedSpanMetaLine details={link} />
</Stack>
);
}

export const SpanLinkedFromSubpanel = ({
links,
onOpenTrace,
}: {
links: LinkedSpanDetails[];
onOpenTrace?: (link: SpanLinkData) => void;
}) => {
const { handleToggleMoreRows, hiddenRowsCount, visibleRows, isExpanded } =
useShowMoreRows({
rows: links,
maxRows: 5,
});

if (links.length === 0) {
return null;
}

return (
<div>
<SectionWrapper>
<ErrorBoundary
onError={err => {
console.error(err);
}}
fallbackRender={() => (
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering linked spans
</div>
)}
>
<Stack gap="sm" px="xs" py="xs">
{visibleRows.map((link, index) => (
<div
key={linkedSpanKey(link.TraceId, link.SpanId)}
data-testid="linked-from-row"
className={
index > 0 ? 'pt-2 border-top border-dark' : undefined
}
>
<LinkedSpanRow link={link} onOpenTrace={onOpenTrace} />
</div>
))}
</Stack>
</ErrorBoundary>

{hiddenRowsCount ? (
<Button
variant="secondary"
size="xs"
my="sm"
onClick={handleToggleMoreRows}
>
{isExpanded ? (
<>
<IconChevronUp size={14} className="me-2" /> Hide spans
</>
) : (
<>
<IconChevronDown size={14} className="me-2" />
Show {hiddenRowsCount} more spans
</>
)}
</Button>
) : null}
</SectionWrapper>
</div>
);
};
24 changes: 22 additions & 2 deletions packages/app/src/components/SpanLinksSubpanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {

import EventTag from './EventTag';
import { SectionWrapper, useShowMoreRows } from './ExceptionSubpanel';
import {
LinkedSpanDetails,
linkedSpanKey,
LinkedSpanMetaLine,
} from './linkedSpans';

// Make sure SpanLinkData implements Record<string, unknown>
export interface SpanLinkData extends Record<string, unknown> {
Expand Down Expand Up @@ -37,9 +42,11 @@ export function getValidSpanLinks(

function SpanLinkRow({
link,
details,
onOpenTrace,
}: {
link: SpanLinkData;
details?: LinkedSpanDetails;
onOpenTrace?: (link: SpanLinkData) => void;
}) {
const attributeEntries = Object.entries(link.Attributes ?? {});
Expand Down Expand Up @@ -71,10 +78,12 @@ function SpanLinkRow({
className="d-inline-flex align-items-center"
>
<IconArrowUpRight size={14} className="me-1" />
Open trace
{details?.spanName || 'Open trace'}
</Anchor>
</Tooltip>

{details ? <LinkedSpanMetaLine details={details} /> : null}

{hasChips ? (
<Flex wrap="wrap" gap="2px" align="baseline">
{hasTraceState ? (
Expand Down Expand Up @@ -104,9 +113,14 @@ function SpanLinkRow({

export const SpanLinksSubpanel = ({
spanLinks,
linkedSpanDetails,
onOpenTrace,
}: {
spanLinks?: Record<string, unknown>[] | null;
// Resolved details for the spans the links point to, keyed by
// `linkedSpanKey`. Links without an entry render with the bare
// "Open trace" fallback.
linkedSpanDetails?: Map<string, LinkedSpanDetails>;
onOpenTrace?: (link: SpanLinkData) => void;
}) => {
const links = useMemo(() => getValidSpanLinks(spanLinks), [spanLinks]);
Expand Down Expand Up @@ -148,7 +162,13 @@ export const SpanLinksSubpanel = ({
index > 0 ? 'pt-2 border-top border-dark' : undefined
}
>
<SpanLinkRow link={link} onOpenTrace={onOpenTrace} />
<SpanLinkRow
link={link}
details={linkedSpanDetails?.get(
linkedSpanKey(link.TraceId, link.SpanId),
)}
onOpenTrace={onOpenTrace}
/>
</div>
))}
</Stack>
Expand Down
Loading
Loading