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
75 changes: 68 additions & 7 deletions devex-ui/src/components/TraceViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { searchTraces, fetchTracesByCorrelation, fetchTraceById } from "@/data";
import { searchTraces, fetchTracesByCorrelation } from "@/data";

interface TraceNode {
id: string;
Expand Down Expand Up @@ -45,6 +45,26 @@ interface SearchResult {
display: string;
}

// Helper function to identify nodes related to the selected node
const getRelatedNodeIds = (selected: TraceNode | null, all: TraceNode[]): Set<string> => {
if (!selected) return new Set();

const relatedIds = new Set<string>();

if (selected.type === 'Command') {
// highlight events caused by this command
all.forEach(node => {
if (node.causationId === selected.id) relatedIds.add(node.id);
});
} else if (selected.type === 'Event' && selected.causationId) {
// highlight the command that caused this event
const cmd = all.find(n => n.id === selected.causationId && n.type === 'Command');
if (cmd) relatedIds.add(cmd.id);
}

return relatedIds;
};

export const TraceViewer = () => {
const [params, setParams] = useSearchParams();
const [searchQuery, setSearchQuery] = useState(() => params.get('q') || "");
Expand Down Expand Up @@ -123,6 +143,26 @@ export const TraceViewer = () => {
try {
// Load traces for the selected correlation ID
const correlationId = selectedResult.correlationId;

// If correlationId is undefined, perform fallback search instead
if (!correlationId) {
console.warn('CorrelationId is undefined, performing fallback search instead');
const searchResults = await searchTraces(resultId);
if (searchResults.length > 0) {
// Find the exact match if possible
const exactMatch = searchResults.find(t => t.id === resultId);
const traceToShow = exactMatch || searchResults[0];

// Set the trace and edges
setTraces([traceToShow]);
setEdges([]);
setSelectedNode(traceToShow);
} else {
console.warn('No traces found for ID:', resultId);
}
return;
}

const { traces: filteredTraces, edges: newEdges } = await fetchTracesByCorrelation(correlationId);

setTraces(filteredTraces);
Expand Down Expand Up @@ -168,6 +208,9 @@ export const TraceViewer = () => {
return selectedTraceId === nodeId;
};

// Calculate related node IDs based on selected node
const relatedNodeIds = getRelatedNodeIds(selectedNode, traces);

return (
<div className="space-y-6">
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -254,17 +297,28 @@ export const TraceViewer = () => {
<div className="flex items-center gap-6 flex-wrap">
{traces.filter(t => t.level === level).map((node, index) => (
<div key={node.id} className="flex items-center gap-2">
{/* Selection state logic:
1. If node.id === selectedNode.id: orange border (primary selection)
2. If node is related to selected node: yellow border (related)
3. Otherwise: default slate border
4. Background color based on node type (Command, Event, Snapshot)
*/}
<div
className={`relative p-3 rounded-lg border-2 cursor-pointer transition-all ${
selectedNode?.id === node.id
? 'border-orange-400 shadow-lg'
className={`relative p-3 rounded-lg border-2 cursor-pointer transition-all
${selectedNode?.id === node.id
? 'border-orange-400 shadow-lg'
: relatedNodeIds.has(node.id)
? 'border-yellow-400 shadow shadow-yellow-500/30'
: 'border-slate-600 hover:border-slate-500'
} ${
isNodeSelected(node.id)
? 'bg-yellow-500 bg-opacity-30 border-yellow-400'
: `${getNodeColor(node.type)} bg-opacity-20`
}`}
onClick={() => setSelectedNode(node)}
onClick={() => {
setSelectedNode(node);
setSelectedTraceId(node.id); // for URL sync and selection logic
}}
>
<div className="flex items-center gap-2">
{getNodeIcon(node.type)}
Expand All @@ -278,10 +332,13 @@ export const TraceViewer = () => {
</div>
</div>
</div>
{/* Arrow to next level if there's a causation relationship */}
{edges.some(e => e.from === node.id) && (
{/* Arrow to next level based on edge type */}
{edges.some(e => e.from === node.id && e.type === 'causation') && (
<ArrowDownCircle className="h-4 w-4 text-slate-500" />
)}
{edges.some(e => e.from === node.id && e.type === 'snapshot') && (
<ArrowDown01 className="h-4 w-4 text-purple-500" />
)}
</div>
))}
</div>
Expand Down Expand Up @@ -388,6 +445,10 @@ export const TraceViewer = () => {
<ArrowRight className="h-3 w-3 text-slate-500" />
<span className="text-white">Causation Flow</span>
</div>
<div className="flex items-center gap-2 text-sm">
<ArrowDown01 className="h-3 w-3 text-purple-500" />
<span className="text-white">Snapshot Flow</span>
</div>
</CardContent>
</Card>
</div>
Expand Down
36 changes: 31 additions & 5 deletions devex-ui/src/data/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import { apiClient, API_CONFIG } from './api';
import type { Event, Command, CommandResult, CommandSchema } from './types';
import type { LogLine } from './mockLogs';
import { isMock } from '@/config/apiMode';
import { findTracesByCorrelationId, searchTracesFullText, traceStore } from '@/mocks/stores/trace.store';
import { generateEdges } from '@/graph/edgeUtils';

// Events API
export const fetchEvents = async (tenantId: string, limit = 50): Promise<Event[]> => {
Expand Down Expand Up @@ -58,17 +61,40 @@ export const fetchRecentCommands = async (limit = 10) => {
};

// Traces API
// Helper function to normalize trace data from API
function normalizeTrace(raw: any) {
return {
id: raw.id,
type: raw.type || 'Event',
subtype: raw.subtype || raw.type,
timestamp: raw.timestamp || raw.created_at,
correlationId: raw.correlationId || raw.correlation_id,
causationId: raw.causationId || raw.causation_id,
aggregateId: raw.aggregateId || raw.aggregate_id,
tenantId: raw.tenantId || raw.tenant_id,
level: raw.level ?? 0
};
}

export const searchTraces = async (query: string) => {
return apiClient.get(`${API_CONFIG.endpoints.traces}/search`, { query });
if (isMock) return searchTracesFullText(query);
const raw = await apiClient.get(`${API_CONFIG.endpoints.traces}/search`, { query });
return raw.map(normalizeTrace);
};

export const fetchTracesByCorrelation = async (correlationId: string) => {
return apiClient.get(`${API_CONFIG.endpoints.traces}/correlation/${correlationId}`);
if (isMock) {
const traces = findTracesByCorrelationId(correlationId);
const edges = generateEdges(traces);
return { traces, edges };
}
const raw = await apiClient.get(`${API_CONFIG.endpoints.traces}/correlation/${correlationId}`);
return {
traces: raw.traces.map(normalizeTrace),
edges: raw.edges
};
};

export const fetchTraceById = async (traceId: string) => {
return apiClient.get(`${API_CONFIG.endpoints.traces}/${traceId}`);
};

export const fetchLogs = (tenant: string, limit=50) =>
apiClient.get<LogLine[]>(API_CONFIG.endpoints.logs, { tenant_id: tenant, limit: limit+'' });
Expand Down
4 changes: 2 additions & 2 deletions devex-ui/src/graph/edgeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//devex-ui/src/graph/edgeUtils.ts
export interface Edge { from:string; to:string; type:'causation' }
export interface Edge { from:string; to:string; type:'causation' | 'snapshot' }

export function generateEdges(
traces:{ id:string; causationId?:string }[]
Expand All @@ -12,4 +12,4 @@ export function generateEdges(
seen.add(k);
return [{ from:t.causationId, to:t.id, type:'causation' }];
});
}
}
4 changes: 4 additions & 0 deletions src/api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import metricsRoutes from './metrics';
import commandsRoutes from './commands';
import eventsRoutes from './events';
import logsRoutes from './logs';
import traceRoutes from './traces';
import accessLogMiddleware from '../middlewares/accessLog';

/**
Expand Down Expand Up @@ -32,6 +33,9 @@ export const registerRoutes = (app: Express): void => {

// Register logs routes
app.use(logsRoutes);

// Register trace routes
app.use(traceRoutes);
};

export default registerRoutes;
Loading