Skip to content
Closed
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
17 changes: 11 additions & 6 deletions web/oss/src/services/tracing/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const fetchAllPreviewTraces = async (
const projectId = ensureProjectId()
const applicationId = ensureAppId(appId)

// New query endpoint expects POST with JSON body
const url = new URL(`${base}/tracing/spans/query`)
// POST /spans/query — always returns flat spans (focus param removed)
const url = new URL(`${base}/spans/query`)
if (projectId) url.searchParams.set("project_id", projectId)
if (applicationId) url.searchParams.set("application_id", applicationId)

Expand All @@ -41,6 +41,9 @@ export const fetchAllPreviewTraces = async (
} catch {
payload.filter = value
}
} else if (key === "focus") {
// `focus` is no longer accepted by POST /spans/query — skip it.
return
} else {
payload[key] = value
}
Expand Down Expand Up @@ -94,7 +97,7 @@ export const fetchAllPreviewTracesWithMeta = async (
const projectId = ensureProjectId()
const applicationId = ensureAppId(appId)

const url = new URL(`${base}/tracing/spans/query`)
const url = new URL(`${base}/spans/query`)
if (projectId) url.searchParams.set("project_id", projectId)
if (applicationId) url.searchParams.set("application_id", applicationId)

Expand All @@ -109,6 +112,8 @@ export const fetchAllPreviewTracesWithMeta = async (
} catch {
payload.filter = value
}
} else if (key === "focus") {
return
} else {
payload[key] = value
}
Expand All @@ -134,7 +139,7 @@ export const fetchPreviewTrace = async (traceId: string) => {
const base = getBaseUrl()
const projectId = ensureProjectId()

const url = new URL(`${base}/tracing/traces/${traceId}`)
const url = new URL(`${base}/traces/${traceId}`)
if (projectId) url.searchParams.set("project_id", projectId)

return fetchJson(url)
Expand All @@ -144,7 +149,7 @@ export const deletePreviewTrace = async (traceId: string) => {
const base = getBaseUrl()
const projectId = ensureProjectId()

const url = new URL(`${base}/tracing/traces/${traceId}`)
const url = new URL(`${base}/traces/${traceId}`)
if (projectId) url.searchParams.set("project_id", projectId)

return fetchJson(url, {method: "DELETE"})
Expand All @@ -167,7 +172,7 @@ export const fetchSessions = async (params: {
const projectId = ensureProjectId()
const applicationId = params.appId ? ensureAppId(params.appId) : undefined

const url = new URL(`${base}/tracing/sessions/query`)
const url = new URL(`${base}/spans/sessions/query`)
if (projectId) url.searchParams.set("project_id", projectId)
if (applicationId) url.searchParams.set("application_id", applicationId)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,15 @@ async function resolveTraceLinkSpanId({

try {
const traceResponse = await fetchPreviewTrace(traceId, projectId)
const traceKey = traceId.replace(/-/g, "")
const traceEntry = traceResponse?.traces?.[traceKey] ?? traceResponse?.traces?.[traceId]
// New API returns {count, trace: {trace_id, spans}} — extract spans
// from the single trace object. Fallback to legacy traces record shape
// for backward compatibility during rollout.
const traceEntry =
traceResponse?.trace ?? (() => {
const traceKey = traceId.replace(/-/g, "")
return (traceResponse as any)?.traces?.[traceKey] ??
(traceResponse as any)?.traces?.[traceId]
})()
Comment on lines +641 to +646
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Type the legacy fallback path instead of casting to any

This keeps the rollout fallback while preserving strict typing in package code.

Suggested fix
+        type LegacyTraceResponse = {
+            traces?: Record<string, {spans?: Record<string, TraceSpan>}>
+        }
         const traceEntry =
             traceResponse?.trace ?? (() => {
                 const traceKey = traceId.replace(/-/g, "")
-                return (traceResponse as any)?.traces?.[traceKey] ??
-                    (traceResponse as any)?.traces?.[traceId]
+                const legacy = traceResponse as LegacyTraceResponse | null
+                return legacy?.traces?.[traceKey] ?? legacy?.traces?.[traceId]
             })()

As per coding guidelines: web/packages/**/*.{ts,tsx} requires “Do not use any types in package code; follow strict typing rules.”

Source: Coding guidelines

const rawSpans = traceEntry?.spans ? Object.values(traceEntry.spans) : []
const spans = rawSpans.filter(
(span): span is TraceSpan =>
Expand Down
85 changes: 48 additions & 37 deletions web/packages/agenta-entities/src/trace/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
* Uses the shared axios instance which should be configured with auth interceptors
* by the app at startup.
*
* Migrated from deprecated `/tracing/*` endpoints to canonical `/traces/*` and
* `/spans/*` endpoints (see #4492).
*
* @example
* ```typescript
* import { fetchAllPreviewTraces, fetchPreviewTrace } from '@agenta/entities/trace'
*
* const spans = await fetchAllPreviewTraces({ size: 100, focus: 'span' }, appId)
* const spans = await fetchAllPreviewTraces({ size: 100 }, appId, projectId)
* const trace = await fetchPreviewTrace(traceId, projectId)
* ```
*/
Expand All @@ -19,18 +22,24 @@ import {axios, getAgentaApiUrl} from "@agenta/shared/api"
// See testcase/api/api.ts for rationale — the shared barrel pulls in CSS deps.
import {safeParseWithLogging} from "../../shared/utils/zodSchema"
import {
sessionIdsResponseSchema,
spansResponseSchema,
tracesResponseSchema,
traceIdResponseSchema,
traceResponseSchema,
type SessionIdsResponse,
type SpansResponse,
type TracesResponse,
type TraceIdResponse,
type TraceResponse,
} from "../core"

/**
* Query parameters for fetching traces/spans
* Query parameters for fetching spans.
*
* Note: `focus` is no longer accepted — `POST /spans/query` always returns
* flat spans. For trace-tree views, use `fetchPreviewTrace` instead.
*/
export interface TraceQueryParams {
size?: number
focus?: "trace" | "span" | "chat"
format?: string
filter?: string | Record<string, unknown>
oldest?: string
Expand All @@ -40,26 +49,27 @@ export interface TraceQueryParams {
}

/**
* Fetch preview traces/spans from the API.
* Fetch spans from the API (flat list).
*
* Calls `POST /spans/query` which always returns a flat `SpansResponse`.
* For trace-tree views, use `fetchPreviewTrace` instead.
*
* @param params - Query parameters for filtering
* @param appId - Application ID (optional)
* @param projectId - Project ID (required)
* @returns API response with spans (validated)
* @returns Validated SpansResponse
*/
export async function fetchAllPreviewTraces(
params: TraceQueryParams = {},
appId: string,
projectId: string,
): Promise<SpansResponse | TracesResponse | null> {
): Promise<SpansResponse | null> {
const baseUrl = getAgentaApiUrl()

// Build query parameters
const queryParams = new URLSearchParams()
if (projectId) queryParams.set("project_id", projectId)
if (appId) queryParams.set("application_id", appId)

// Build request payload
const payload: Record<string, unknown> = {}
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) return
Expand All @@ -71,68 +81,71 @@ export async function fetchAllPreviewTraces(
} catch {
payload.filter = value
}
} else if (key === "focus") {
// `focus` is no longer accepted by POST /spans/query — skip it.
return
} else {
payload[key] = value
}
})

const response = await axios.post(
`${baseUrl}/tracing/spans/query?${queryParams.toString()}`,
`${baseUrl}/spans/query?${queryParams.toString()}`,
payload,
)

// Try parsing as SpansResponse first (spans array format)
const spansResult = spansResponseSchema.safeParse(response.data)
if (spansResult.success) {
return spansResult.data
}

// Fall back to TracesResponse (traces record format)
return safeParseWithLogging(tracesResponseSchema, response.data, "[fetchAllPreviewTraces]")
return safeParseWithLogging(spansResponseSchema, response.data, "[fetchAllPreviewTraces]")
}

/**
* Fetch a single trace by ID.
* Fetch a single trace by ID (with trace-tree structure).
*
* Calls `GET /traces/{id}` which returns a `TraceResponse` with a single
* `trace` object containing `trace_id` and a `spans` record.
*
* @param traceId - Trace ID to fetch
* @param projectId - Project ID
* @returns Trace span data (validated)
* @returns Validated TraceResponse
*/
export async function fetchPreviewTrace(
traceId: string,
projectId: string,
): Promise<TracesResponse | null> {
): Promise<TraceResponse | null> {
const baseUrl = getAgentaApiUrl()

const queryParams = new URLSearchParams()
if (projectId) queryParams.set("project_id", projectId)

const response = await axios.get(
`${baseUrl}/tracing/traces/${traceId}?${queryParams.toString()}`,
`${baseUrl}/traces/${traceId}?${queryParams.toString()}`,
)

// API returns TracesResponse format with count and traces record
return safeParseWithLogging(tracesResponseSchema, response.data, "[fetchPreviewTrace]")
return safeParseWithLogging(traceResponseSchema, response.data, "[fetchPreviewTrace]")
}

/**
* Delete a trace by ID.
*
* Calls `DELETE /traces/{id}`.
*
* @param traceId - Trace ID to delete
* @param projectId - Project ID
* @returns Delete response
* @returns Validated TraceIdResponse
*/
export async function deletePreviewTrace(traceId: string, projectId: string): Promise<unknown> {
export async function deletePreviewTrace(
traceId: string,
projectId: string,
): Promise<TraceIdResponse | null> {
const baseUrl = getAgentaApiUrl()

const queryParams = new URLSearchParams()
if (projectId) queryParams.set("project_id", projectId)

const response = await axios.delete(
`${baseUrl}/tracing/traces/${traceId}?${queryParams.toString()}`,
`${baseUrl}/traces/${traceId}?${queryParams.toString()}`,
)

return response.data
return safeParseWithLogging(traceIdResponseSchema, response.data, "[deletePreviewTrace]")
}

/**
Expand All @@ -155,14 +168,16 @@ export interface SessionQueryParams {
/**
* Fetch sessions with filtering and pagination.
*
* Calls `POST /spans/sessions/query`.
*
* @param params - Session query parameters
* @param projectId - Project ID
* @returns Session list response
* @returns Validated SessionIdsResponse
*/
export async function fetchSessions(
params: SessionQueryParams,
projectId: string,
): Promise<unknown> {
): Promise<SessionIdsResponse | null> {
const baseUrl = getAgentaApiUrl()

const queryParams = new URLSearchParams()
Expand All @@ -171,11 +186,8 @@ export async function fetchSessions(

const payload: Record<string, unknown> = {}

// Initialize windowing if it doesn't exist but we have a cursor
if (params.windowing || params.cursor) {
payload.windowing = {...(params.windowing || {})}

// If cursor is provided, it goes into windowing.next
if (params.cursor) {
;(payload.windowing as Record<string, unknown>).next = params.cursor
}
Expand All @@ -185,15 +197,14 @@ export async function fetchSessions(
payload.filter = params.filter
}

// Add realtime parameter (true = latest/unstable, false/undefined = all/stable)
if (params.realtime !== undefined) {
payload.realtime = params.realtime
}

const response = await axios.post(
`${baseUrl}/tracing/sessions/query?${queryParams.toString()}`,
`${baseUrl}/spans/sessions/query?${queryParams.toString()}`,
payload,
)

return response.data
return safeParseWithLogging(sessionIdsResponseSchema, response.data, "[fetchSessions]")
}
Loading