diff --git a/frontend/src/container/SpanList/SpanList.tsx b/frontend/src/container/SpanList/SpanList.tsx
index f6a771e75a72..a8d0cd5aee6e 100644
--- a/frontend/src/container/SpanList/SpanList.tsx
+++ b/frontend/src/container/SpanList/SpanList.tsx
@@ -1,11 +1,14 @@
import './SpanList.styles.scss';
+import { ENTITY_VERSION_V5 } from 'constants/app';
+import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
+import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useMemo } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
-import { mockEntrySpanData } from './mockData';
import SearchFilters from './SearchFilters';
import SpanTable from './SpanTable';
+import { SpanDataRow } from './types';
import { transformEntrySpansToHierarchy } from './utils';
interface SpanListProps {
@@ -14,9 +17,90 @@ interface SpanListProps {
}
function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
+ const payload = initialQueriesMap.traces;
+
+ const { data, isLoading, isFetching } = useGetQueryRange(
+ {
+ graphType: PANEL_TYPES.LIST,
+ selectedTime: 'GLOBAL_TIME',
+ query: {
+ ...payload,
+ builder: {
+ ...payload.builder,
+ queryData: [
+ {
+ ...payload.builder.queryData[0],
+ ...{
+ name: 'A',
+ signal: 'traces',
+ stepInterval: null,
+ disabled: false,
+ filter: {
+ expression: `trace_id = '${traceId}' isEntryPoint = 'true'`,
+ },
+ limit: 10,
+ offset: 0,
+ order: [
+ {
+ key: {
+ name: 'timestamp',
+ },
+ direction: 'desc',
+ },
+ ],
+ having: {
+ expression: '',
+ },
+ selectFields: [
+ {
+ name: 'service.name',
+ fieldDataType: 'string',
+ signal: 'traces',
+ fieldContext: 'resource',
+ },
+ {
+ name: 'name',
+ fieldDataType: 'string',
+ signal: 'traces',
+ },
+ {
+ name: 'duration_nano',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ {
+ name: 'http_method',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ {
+ name: 'response_status_code',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ // version,
+ ENTITY_VERSION_V5,
+ );
+
const hierarchicalData = useMemo(
- () => transformEntrySpansToHierarchy(mockEntrySpanData),
- [],
+ () =>
+ // TODO(shaheer): properly fix the type
+ transformEntrySpansToHierarchy(
+ (data?.payload.data.newResult.data.result[0].list as unknown) as
+ | SpanDataRow[]
+ | undefined,
+ ),
+ [data?.payload.data.newResult.data.result],
);
return (
@@ -27,6 +111,7 @@ function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
diff --git a/frontend/src/container/SpanList/SpanTable.tsx b/frontend/src/container/SpanList/SpanTable.tsx
index 98f0dde3a43d..d05491b8e57a 100644
--- a/frontend/src/container/SpanList/SpanTable.tsx
+++ b/frontend/src/container/SpanList/SpanTable.tsx
@@ -1,11 +1,12 @@
import { Button } from '@signozhq/button';
import { ColumnDef, DataTable, Row } from '@signozhq/table';
+import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types';
-import { fetchServiceSpans, formatDuration } from './utils';
+import { fetchServiceSpans } from './utils';
// Constants
const SPAN_TYPE_ENTRY = 'entry-span';
@@ -15,6 +16,7 @@ interface SpanTableProps {
data: HierarchicalSpanData;
traceId?: string;
setSelectedSpan?: (span: Span) => void;
+ isLoading?: boolean;
}
interface TableRowData {
@@ -36,6 +38,7 @@ function SpanTable({
data,
traceId,
setSelectedSpan,
+ isLoading,
}: SpanTableProps): JSX.Element {
const [expandedEntrySpans, setExpandedEntrySpans] = useState<
Record
@@ -156,17 +159,6 @@ function SpanTable({
[],
);
- const renderSpanCountCell = useCallback(
- ({ row }: { row: Row }): JSX.Element => {
- const { original } = row;
- if (original.type === SPAN_TYPE_ENTRY && original.spanCount !== undefined) {
- return {original.spanCount};
- }
- return -;
- },
- [],
- );
-
const renderDurationCell = useCallback(
({ row }: { row: Row }): JSX.Element => {
const { original } = row;
@@ -190,7 +182,15 @@ function SpanTable({
const renderStatusCodeCell = useCallback(
({ row }: { row: Row }): JSX.Element => {
const { original } = row;
- return {original.statusCode};
+ return {original.statusCode || '-'};
+ },
+ [],
+ );
+
+ const renderSpanIdCell = useCallback(
+ ({ row }: { row: Row }): JSX.Element => {
+ const { original } = row;
+ return {original.spanId};
},
[],
);
@@ -218,6 +218,13 @@ function SpanTable({
size: 180,
cell: renderTimestampCell,
},
+ {
+ id: 'spanId',
+ header: 'Span ID',
+ accessorKey: 'spanId',
+ size: 150,
+ cell: renderSpanIdCell,
+ },
{
id: 'httpMethod',
header: 'Method',
@@ -239,13 +246,6 @@ function SpanTable({
size: 120,
cell: renderServiceCell,
},
- {
- id: 'spans',
- header: 'Spans',
- accessorKey: 'spanCount',
- size: 80,
- cell: renderSpanCountCell,
- },
{
id: 'duration',
header: 'Duration',
@@ -272,7 +272,10 @@ function SpanTable({
spanName: entrySpan.spanData.data.name,
serviceName: entrySpan.serviceName,
spanCount: spanCount > 0 ? spanCount : undefined,
- duration: formatDuration(entrySpan.spanData.data.duration_nano),
+ duration: getYAxisFormattedValue(
+ entrySpan.spanData.data.duration_nano.toString(),
+ 'ns',
+ ),
timestamp: entrySpan.spanData.timestamp,
statusCode: entrySpan.spanData.data.response_status_code,
httpMethod: entrySpan.spanData.data.http_method,
@@ -288,7 +291,10 @@ function SpanTable({
type: SPAN_TYPE_SERVICE,
spanName: serviceSpan.data.name,
serviceName: serviceSpan.data['service.name'],
- duration: formatDuration(serviceSpan.data.duration_nano),
+ duration: getYAxisFormattedValue(
+ serviceSpan.data.duration_nano.toString(),
+ 'ns',
+ ),
timestamp: serviceSpan.timestamp,
statusCode: serviceSpan.data.response_status_code,
httpMethod: serviceSpan.data.http_method,
@@ -354,6 +360,7 @@ function SpanTable({
fixedHeight={args.fixedHeight}
data={flattenedData}
onRowClick={handleRowClick}
+ isLoading={isLoading}
/>
);
@@ -362,6 +369,7 @@ function SpanTable({
SpanTable.defaultProps = {
traceId: undefined,
setSelectedSpan: undefined,
+ isLoading: false,
};
export default SpanTable;
diff --git a/frontend/src/container/SpanList/mockData.ts b/frontend/src/container/SpanList/mockData.ts
deleted file mode 100644
index 4fb4dbf96ef0..000000000000
--- a/frontend/src/container/SpanList/mockData.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { SpanDataRow } from './types';
-
-export const mockEntrySpanData: SpanDataRow[] = [
- {
- data: {
- duration_nano: 3878813,
- http_method: '',
- // eslint-disable-next-line sonarjs/no-duplicate-string
- name: 'CurrencyService/Convert',
- response_status_code: '0',
- 'service.name': 'currencyservice',
- span_id: '8439d7461ab457a2',
- timestamp: '2025-09-03T11:33:46.060209146Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:46.060209146Z',
- },
- {
- data: {
- duration_nano: 12485759,
- http_method: '',
- name: 'CurrencyService/Convert',
- response_status_code: '0',
- 'service.name': 'currencyservice',
- span_id: '7dbf7811106b1049',
- timestamp: '2025-09-03T11:33:46.058715482Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:46.058715482Z',
- },
- {
- data: {
- duration_nano: 6950595,
- http_method: '',
- name: 'CurrencyService/Convert',
- response_status_code: '0',
- 'service.name': 'currencyservice',
- span_id: '9adcdf1baa56d23a',
- timestamp: '2025-09-03T11:33:46.058319328Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:46.058319328Z',
- },
- {
- data: {
- duration_nano: 5323696,
- http_method: '',
- name: 'CurrencyService/Convert',
- response_status_code: '0',
- 'service.name': 'currencyservice',
- span_id: '1a1b01a750dfe4d6',
- timestamp: '2025-09-03T11:33:46.057323233Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:46.057323233Z',
- },
- {
- data: {
- duration_nano: 133659,
- http_method: '',
- name: 'oteldemo.ProductCatalogService/GetProduct',
- response_status_code: '0',
- 'service.name': 'productcatalogservice',
- span_id: '485a987e9dbf8ddd',
- timestamp: '2025-09-03T11:33:45.963838319Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:45.963838319Z',
- },
- {
- data: {
- duration_nano: 245000000,
- http_method: 'GET',
- name: '/api/checkout',
- response_status_code: '200',
- 'service.name': 'checkoutservice',
- span_id: 'abc123def456',
- timestamp: '2025-09-03T11:33:45.800000000Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:45.800000000Z',
- },
- {
- data: {
- duration_nano: 150000000,
- http_method: 'POST',
- name: '/api/payment',
- response_status_code: '200',
- 'service.name': 'paymentservice',
- span_id: 'def456ghi789',
- timestamp: '2025-09-03T11:33:45.750000000Z',
- trace_id: '5ad9c01671f0e38582efe03bbf81360a',
- },
- timestamp: '2025-09-03T11:33:45.750000000Z',
- },
-];
diff --git a/frontend/src/container/SpanList/utils.ts b/frontend/src/container/SpanList/utils.ts
index 402c6f2975d8..95398884fe61 100644
--- a/frontend/src/container/SpanList/utils.ts
+++ b/frontend/src/container/SpanList/utils.ts
@@ -1,30 +1,28 @@
+import { ENTITY_VERSION_V5 } from 'constants/app';
+import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
+import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
+import { uniqBy } from 'lodash-es';
+
import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types';
-export function formatDuration(durationNano: number): string {
- if (durationNano < 1000) {
- return `${durationNano}ns`;
- }
- if (durationNano < 1000000) {
- return `${(durationNano / 1000).toFixed(2)}μs`;
- }
- if (durationNano < 1000000000) {
- return `${(durationNano / 1000000).toFixed(2)}ms`;
- }
- return `${(durationNano / 1000000000).toFixed(2)}s`;
-}
-
export function transformEntrySpansToHierarchy(
- entrySpans: SpanDataRow[],
+ entrySpans?: SpanDataRow[],
): HierarchicalSpanData {
let totalTraceTime = 0;
+ if (!entrySpans) {
+ return { entrySpans: [], totalTraceTime: 0 };
+ }
+
+ const uniqueEntrySpans = uniqBy(entrySpans, 'data.span_id');
+
// Calculate total trace time from all entry spans
- entrySpans.forEach((span) => {
+ uniqueEntrySpans.forEach((span) => {
totalTraceTime += span.data.duration_nano;
});
// Transform entry spans to ServiceEntrySpan structure
- const entrySpansList: ServiceEntrySpan[] = entrySpans.map((span) => ({
+ const entrySpansList: ServiceEntrySpan[] = uniqueEntrySpans.map((span) => ({
spanData: span,
serviceName: span.data['service.name'],
isExpanded: false,
@@ -32,58 +30,101 @@ export function transformEntrySpansToHierarchy(
isLoadingServiceSpans: false,
}));
- // Sort by timestamp (most recent first)
- entrySpansList.sort(
- (a, b) =>
- new Date(b.spanData.timestamp).getTime() -
- new Date(a.spanData.timestamp).getTime(),
- );
-
return {
entrySpans: entrySpansList,
totalTraceTime,
};
}
-// Mock function to simulate fetching service spans
-export function fetchServiceSpans(
+export async function fetchServiceSpans(
traceId: string,
serviceName: string,
): Promise {
- // This would normally make an API call to get spans for the specific service
- // For now, return mock data filtered by service name
- return new Promise((resolve) => {
- setTimeout(() => {
- // Mock response - in real implementation, this would call the API
- const mockServiceSpans: SpanDataRow[] = [
- {
- data: {
- duration_nano: 1500000,
- http_method: 'GET',
- name: `${serviceName}/internal-call-1`,
- response_status_code: '200',
- 'service.name': serviceName,
- span_id: `${serviceName}-span-1`,
- timestamp: '2025-09-03T11:33:46.100000000Z',
- trace_id: traceId,
+ // Use the same payload structure as in SpanList component but with service-specific filter
+ const payload = initialQueriesMap.traces;
+
+ try {
+ const response = await GetMetricQueryRange(
+ {
+ graphType: PANEL_TYPES.LIST,
+ selectedTime: 'GLOBAL_TIME',
+ query: {
+ ...payload,
+ builder: {
+ ...payload.builder,
+ queryData: [
+ {
+ ...payload.builder.queryData[0],
+ ...{
+ name: 'A',
+ signal: 'traces',
+ stepInterval: null,
+ disabled: false,
+ filter: {
+ expression: `trace_id = '${traceId}' and service.name = '${serviceName}'`,
+ },
+ limit: 10,
+ offset: 0,
+ order: [
+ {
+ key: {
+ name: 'timestamp',
+ },
+ direction: 'desc',
+ },
+ ],
+ having: {
+ expression: '',
+ },
+ selectFields: [
+ {
+ name: 'service.name',
+ fieldDataType: 'string',
+ signal: 'traces',
+ fieldContext: 'resource',
+ },
+ {
+ name: 'name',
+ fieldDataType: 'string',
+ signal: 'traces',
+ },
+ {
+ name: 'duration_nano',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ {
+ name: 'http_method',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ {
+ name: 'response_status_code',
+ fieldDataType: '',
+ signal: 'traces',
+ fieldContext: 'span',
+ },
+ ],
+ },
+ },
+ ],
},
- timestamp: '2025-09-03T11:33:46.100000000Z',
},
- {
- data: {
- duration_nano: 2500000,
- http_method: 'POST',
- name: `${serviceName}/internal-call-2`,
- response_status_code: '200',
- 'service.name': serviceName,
- span_id: `${serviceName}-span-2`,
- timestamp: '2025-09-03T11:33:46.200000000Z',
- trace_id: traceId,
- },
- timestamp: '2025-09-03T11:33:46.200000000Z',
- },
- ];
- resolve(mockServiceSpans);
- }, 500); // Simulate network delay
- });
+ },
+ ENTITY_VERSION_V5,
+ );
+
+ // Extract spans from the API response using the same path as SpanList component
+ const spans =
+ response?.payload?.data?.newResult?.data?.result?.[0]?.list || [];
+
+ // Transform the API response to SpanDataRow format if needed
+ // The API should return the correct format for traces, but we'll handle any potential transformation
+ return (spans as unknown) as SpanDataRow[];
+ } catch (error) {
+ console.error('Failed to fetch service spans:', error);
+ return [];
+ }
}