Skip to content

Commit 356f5fc

Browse files
authored
[OPIK-3582] [FE] Add navigation shortcut from trace tree to spans table (#4541)
1 parent 810bfb5 commit 356f5fc

File tree

6 files changed

+128
-6
lines changed

6 files changed

+128
-6
lines changed

apps/opik-backend/src/main/java/com/comet/opik/api/filter/SpanField.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public enum SpanField implements Field {
2626
DURATION(DURATION_QUERY_PARAM, FieldType.NUMBER),
2727
ERROR_INFO(ERROR_INFO_QUERY_PARAM, FieldType.ERROR_CONTAINER),
2828
TYPE(TYPE_QUERY_PARAM, FieldType.ENUM),
29+
TRACE_ID(TRACE_ID_QUERY_PARAM, FieldType.STRING),
2930
CUSTOM(CUSTOM_QUERY_PARAM, FieldType.CUSTOM),
3031
;
3132

apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ public class FilterQueryBuilder {
330330
.put(SpanField.DURATION, DURATION_ANALYTICS_DB)
331331
.put(SpanField.ERROR_INFO, ERROR_INFO_DB)
332332
.put(SpanField.TYPE, TYPE_ANALYTICS_DB)
333+
.put(SpanField.TRACE_ID, TRACE_ID_DB)
333334
.build());
334335

335336
private static final Map<ExperimentField, String> EXPERIMENT_FIELDS_MAP = new EnumMap<>(
@@ -518,7 +519,8 @@ private static Map<FilterStrategy, Set<? extends Field>> createFilterStrategyMap
518519
SpanField.USAGE_TOTAL_TOKENS,
519520
SpanField.DURATION,
520521
SpanField.ERROR_INFO,
521-
SpanField.TYPE));
522+
SpanField.TYPE,
523+
SpanField.TRACE_ID));
522524

523525
map.put(FilterStrategy.FEEDBACK_SCORES, Set.of(
524526
TraceField.FEEDBACK_SCORES,

apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FindSpansResourceTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import java.util.ArrayList;
8585
import java.util.Arrays;
8686
import java.util.Comparator;
87+
import java.util.HashMap;
8788
import java.util.List;
8889
import java.util.Map;
8990
import java.util.Objects;
@@ -1456,6 +1457,86 @@ void whenFilterNameLessThan__thenReturnSpansFiltered(String endpoint, SpanPageTe
14561457
values.all(), filters, Map.of());
14571458
}
14581459

1460+
@ParameterizedTest
1461+
@MethodSource("getFilterTestArguments")
1462+
void whenFilterTraceIdEqual__thenReturnSpansFiltered(String endpoint, SpanPageTestAssertion testAssertion) {
1463+
String workspaceName = UUID.randomUUID().toString();
1464+
String workspaceId = UUID.randomUUID().toString();
1465+
String apiKey = UUID.randomUUID().toString();
1466+
1467+
mockTargetWorkspace(apiKey, workspaceName, workspaceId);
1468+
1469+
var projectName = generator.generate().toString();
1470+
var traceId = UUID.randomUUID();
1471+
1472+
// Create 5 spans with the same trace_id (these should be returned by the filter)
1473+
var expectedSpanCount = 5;
1474+
var expectedSpans = IntStream.range(0, expectedSpanCount)
1475+
.mapToObj(i -> podamFactory.manufacturePojo(Span.class).toBuilder()
1476+
.projectId(null)
1477+
.projectName(projectName)
1478+
.traceId(traceId)
1479+
.name("span-" + i)
1480+
.feedbackScores(null)
1481+
.totalEstimatedCost(null)
1482+
.build())
1483+
.toList();
1484+
1485+
spanResourceClient.batchCreateSpans(expectedSpans, apiKey, workspaceName);
1486+
1487+
// Create 3 spans with different trace_ids (these should NOT be returned)
1488+
var unexpectedSpans = IntStream.range(0, 3)
1489+
.mapToObj(i -> podamFactory.manufacturePojo(Span.class).toBuilder()
1490+
.projectId(null)
1491+
.projectName(projectName)
1492+
.traceId(UUID.randomUUID())
1493+
.name("other-span-" + i)
1494+
.feedbackScores(null)
1495+
.totalEstimatedCost(null)
1496+
.build())
1497+
.toList();
1498+
1499+
spanResourceClient.batchCreateSpans(unexpectedSpans, apiKey, workspaceName);
1500+
1501+
// Apply trace_id filter
1502+
var filters = List.of(SpanFilter.builder()
1503+
.field(SpanField.TRACE_ID)
1504+
.operator(Operator.EQUAL)
1505+
.value(traceId.toString())
1506+
.build());
1507+
1508+
Map<String, String> queryParams = new HashMap<>();
1509+
queryParams.put("project_name", projectName);
1510+
queryParams.put("filters", toURLEncodedQueryParam(filters));
1511+
1512+
// Execute the request and verify results
1513+
try (var actualResponse = spanResourceClient.callGetSpansWithQueryParams(apiKey, workspaceName,
1514+
queryParams)) {
1515+
var actualPage = actualResponse.readEntity(Span.SpanPage.class);
1516+
var actualSpans = actualPage.content();
1517+
1518+
assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_OK);
1519+
1520+
// Verify we got the correct number of spans
1521+
assertThat(actualSpans).hasSize(expectedSpanCount);
1522+
assertThat(actualPage.total()).isEqualTo(expectedSpanCount);
1523+
1524+
// Verify all returned spans have the correct trace_id
1525+
assertThat(actualSpans)
1526+
.allMatch(span -> span.traceId().equals(traceId),
1527+
"All returned spans should have traceId: " + traceId);
1528+
1529+
// Verify no spans with different trace_ids are returned
1530+
var unexpectedTraceIds = unexpectedSpans.stream()
1531+
.map(Span::traceId)
1532+
.collect(Collectors.toSet());
1533+
1534+
assertThat(actualSpans)
1535+
.noneMatch(span -> unexpectedTraceIds.contains(span.traceId()),
1536+
"No spans with different trace_ids should be returned");
1537+
}
1538+
}
1539+
14591540
@ParameterizedTest
14601541
@MethodSource("getFilterTestArguments")
14611542
void whenFilterNameGreaterThan__thenReturnSpansFiltered(String endpoint, SpanPageTestAssertion testAssertion) {

apps/opik-frontend/src/components/pages-shared/traces/TraceDetailsPanel/TraceDetailsPanel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ const TraceDetailsPanel: React.FunctionComponent<TraceDetailsPanelProps> = ({
185185
<ResizablePanelGroup direction="horizontal" autoSaveId="trace-sidebar">
186186
<ResizablePanel id="tree-viewer" defaultSize={40} minSize={20}>
187187
<TraceTreeViewer
188+
projectId={projectId}
188189
trace={trace}
189190
spans={spansData?.content}
190191
rowId={spanId || traceId}

apps/opik-frontend/src/components/pages-shared/traces/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { useCallback, useEffect, useMemo, useRef } from "react";
22
import useLocalStorageState from "use-local-storage-state";
33
import { FoldVertical, UnfoldVertical } from "lucide-react";
4+
import { Link } from "@tanstack/react-router";
45

56
import {
67
addAllParentIds,
78
constructDataMapAndSearchIds,
89
filterFunction,
910
} from "./helpers";
10-
import { OnChangeFn } from "@/types/shared";
11+
import { COLUMN_TYPE, OnChangeFn } from "@/types/shared";
1112
import { Span, Trace } from "@/types/traces";
1213
import { Filters } from "@/types/filters";
1314
import { SPANS_COLORS_MAP, TRACE_TYPE_FOR_TREE } from "@/constants/traces";
@@ -23,6 +24,8 @@ import useTreeDetailsStore, {
2324
TreeNodeConfig,
2425
} from "@/components/pages-shared/traces/TraceDetailsPanel/TreeDetailsStore";
2526
import SpanDetailsButton from "@/components/pages-shared/traces/TraceDetailsPanel/TraceTreeViewer/SpanDetailsButton";
27+
import useAppStore from "@/store/AppStore";
28+
import { createFilter } from "@/lib/filters";
2629

2730
const SELECTED_TREE_DATABLOCKS_KEY = "tree-datablocks-config";
2831
const SELECTED_TREE_DATABLOCKS_DEFAULT_VALUE: TreeNodeConfig = {
@@ -39,6 +42,7 @@ const SELECTED_TREE_DATABLOCKS_DEFAULT_VALUE: TreeNodeConfig = {
3942
};
4043

4144
type TraceTreeViewerProps = {
45+
projectId: string;
4246
trace: Trace;
4347
spans?: Span[];
4448
rowId: string;
@@ -50,6 +54,7 @@ type TraceTreeViewerProps = {
5054
};
5155

5256
const TraceTreeViewer: React.FunctionComponent<TraceTreeViewerProps> = ({
57+
projectId,
5358
trace,
5459
spans,
5560
rowId,
@@ -59,6 +64,7 @@ const TraceTreeViewer: React.FunctionComponent<TraceTreeViewerProps> = ({
5964
filters,
6065
setFilters,
6166
}) => {
67+
const workspaceName = useAppStore((state) => state.activeWorkspaceName);
6268
const traceSpans = useMemo(() => spans ?? [], [spans]);
6369
const scrollRef = useRef<HTMLDivElement>(null);
6470
const [config, setConfig] = useLocalStorageState(
@@ -73,6 +79,18 @@ const TraceTreeViewer: React.FunctionComponent<TraceTreeViewerProps> = ({
7379
const hasSearchOrFilter = hasSearch || hasFilter;
7480
const title = !hasSearchOrFilter ? "Trace" : "Results";
7581

82+
const spansFilterForTrace = useMemo(
83+
() => [
84+
createFilter({
85+
field: "trace_id",
86+
type: COLUMN_TYPE.string,
87+
operator: "=",
88+
value: trace.id,
89+
}),
90+
],
91+
[trace.id],
92+
);
93+
7694
const predicate = useCallback(
7795
(data: Span | Trace) =>
7896
!hasSearch && !hasFilter ? true : filterFunction(data, filters, search),
@@ -192,10 +210,24 @@ const TraceTreeViewer: React.FunctionComponent<TraceTreeViewerProps> = ({
192210
<div className="min-w-[400px] max-w-full">
193211
<div className="sticky top-0 z-10 flex flex-row items-center justify-between gap-2 bg-background pb-2 pl-6 pr-4 pt-4">
194212
<div className="flex h-8 items-center gap-1">
195-
<div className="comet-title-xs">{title}</div>
196-
<div className="comet-body-s text-muted-slate">
197-
{!hasSearchOrFilter ? traceSpans.length : searchIds.size} items
198-
</div>
213+
<div className="comet-title-xs">{title} -</div>
214+
<TooltipWrapper content="View all spans of this trace in table view">
215+
<Link
216+
to={`/$workspaceName/projects/$projectId/traces`}
217+
params={{ workspaceName, projectId }}
218+
search={{
219+
type: "spans",
220+
spans_filters: spansFilterForTrace,
221+
}}
222+
>
223+
<Button variant="link" className="comet-body-s px-0" asChild>
224+
<span>
225+
{!hasSearchOrFilter ? traceSpans.length : searchIds.size}{" "}
226+
spans
227+
</span>
228+
</Button>
229+
</Link>
230+
</TooltipWrapper>
199231
<ExplainerIcon
200232
{...EXPLAINERS_MAP[
201233
EXPLAINER_ID.what_are_these_elements_in_the_tree

apps/opik-frontend/src/components/pages/TracesPage/TracesSpansTab/TracesSpansTab.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,11 @@ export const TracesSpansTab: React.FC<TracesSpansTabProps> = ({
838838
label: "Type",
839839
type: COLUMN_TYPE.category,
840840
},
841+
{
842+
id: "trace_id",
843+
label: "Trace ID",
844+
type: COLUMN_TYPE.string,
845+
},
841846
]
842847
: []),
843848
{

0 commit comments

Comments
 (0)