Skip to content

Commit 6dc40e5

Browse files
committed
feat: migrate tracing web client from deprecated to new API endpoints
Migrate 4 of 5 deprecated /tracing/* endpoints to their canonical replacements. The analytics endpoint (/tracing/spans/analytics) is deferred per issue author's recommendation. Endpoint migrations: - POST /tracing/spans/query → POST /spans/query - GET /tracing/traces/{id} → GET /traces/{id} - DELETE /tracing/traces/{id} → DELETE /traces/{id} - POST /tracing/sessions/query → POST /spans/sessions/query Changes: - Add traceResponseSchema, traceIdResponseSchema, sessionIdsResponseSchema Zod schemas for the new response shapes - Update entities API (api.ts) to hit new endpoints with new response parsing. POST /spans/query now always returns flat SpansResponse (focus param removed). GET /traces/{id} returns single TraceResponse. - Update OSS API (index.ts) to hit new URLs, strip focus param - Update annotationFormController to handle new trace response shape ({trace: {trace_id, spans}} instead of {traces: {id: {spans}}}) - Add isTraceResponse type guard and transformTraceResponseToTree helper - Existing consumer branching logic (isTracesResponse/isSpansResponse) continues to work — POST /spans/query always returns SpansResponse so isTracesResponse branch becomes dead code for list queries Fixes #4492
1 parent 98b8a9d commit 6dc40e5

8 files changed

Lines changed: 188 additions & 74 deletions

File tree

web/oss/src/services/tracing/api/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const fetchAllPreviewTraces = async (
2525
const projectId = ensureProjectId()
2626
const applicationId = ensureAppId(appId)
2727

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

@@ -41,6 +41,9 @@ export const fetchAllPreviewTraces = async (
4141
} catch {
4242
payload.filter = value
4343
}
44+
} else if (key === "focus") {
45+
// `focus` is no longer accepted by POST /spans/query — skip it.
46+
return
4447
} else {
4548
payload[key] = value
4649
}
@@ -94,7 +97,7 @@ export const fetchAllPreviewTracesWithMeta = async (
9497
const projectId = ensureProjectId()
9598
const applicationId = ensureAppId(appId)
9699

97-
const url = new URL(`${base}/tracing/spans/query`)
100+
const url = new URL(`${base}/spans/query`)
98101
if (projectId) url.searchParams.set("project_id", projectId)
99102
if (applicationId) url.searchParams.set("application_id", applicationId)
100103

@@ -109,6 +112,8 @@ export const fetchAllPreviewTracesWithMeta = async (
109112
} catch {
110113
payload.filter = value
111114
}
115+
} else if (key === "focus") {
116+
return
112117
} else {
113118
payload[key] = value
114119
}
@@ -134,7 +139,7 @@ export const fetchPreviewTrace = async (traceId: string) => {
134139
const base = getBaseUrl()
135140
const projectId = ensureProjectId()
136141

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

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

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

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

170-
const url = new URL(`${base}/tracing/sessions/query`)
175+
const url = new URL(`${base}/spans/sessions/query`)
171176
if (projectId) url.searchParams.set("project_id", projectId)
172177
if (applicationId) url.searchParams.set("application_id", applicationId)
173178

web/packages/agenta-annotation/src/state/controllers/annotationFormController.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,8 +635,15 @@ async function resolveTraceLinkSpanId({
635635

636636
try {
637637
const traceResponse = await fetchPreviewTrace(traceId, projectId)
638-
const traceKey = traceId.replace(/-/g, "")
639-
const traceEntry = traceResponse?.traces?.[traceKey] ?? traceResponse?.traces?.[traceId]
638+
// New API returns {count, trace: {trace_id, spans}} — extract spans
639+
// from the single trace object. Fallback to legacy traces record shape
640+
// for backward compatibility during rollout.
641+
const traceEntry =
642+
traceResponse?.trace ?? (() => {
643+
const traceKey = traceId.replace(/-/g, "")
644+
return (traceResponse as any)?.traces?.[traceKey] ??
645+
(traceResponse as any)?.traces?.[traceId]
646+
})()
640647
const rawSpans = traceEntry?.spans ? Object.values(traceEntry.spans) : []
641648
const spans = rawSpans.filter(
642649
(span): span is TraceSpan =>

web/packages/agenta-entities/src/trace/api/api.ts

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
* Uses the shared axios instance which should be configured with auth interceptors
66
* by the app at startup.
77
*
8+
* Migrated from deprecated `/tracing/*` endpoints to canonical `/traces/*` and
9+
* `/spans/*` endpoints (see #4492).
10+
*
811
* @example
912
* ```typescript
1013
* import { fetchAllPreviewTraces, fetchPreviewTrace } from '@agenta/entities/trace'
1114
*
12-
* const spans = await fetchAllPreviewTraces({ size: 100, focus: 'span' }, appId)
15+
* const spans = await fetchAllPreviewTraces({ size: 100 }, appId, projectId)
1316
* const trace = await fetchPreviewTrace(traceId, projectId)
1417
* ```
1518
*/
@@ -19,18 +22,24 @@ import {axios, getAgentaApiUrl} from "@agenta/shared/api"
1922
// See testcase/api/api.ts for rationale — the shared barrel pulls in CSS deps.
2023
import {safeParseWithLogging} from "../../shared/utils/zodSchema"
2124
import {
25+
sessionIdsResponseSchema,
2226
spansResponseSchema,
23-
tracesResponseSchema,
27+
traceIdResponseSchema,
28+
traceResponseSchema,
29+
type SessionIdsResponse,
2430
type SpansResponse,
25-
type TracesResponse,
31+
type TraceIdResponse,
32+
type TraceResponse,
2633
} from "../core"
2734

2835
/**
29-
* Query parameters for fetching traces/spans
36+
* Query parameters for fetching spans.
37+
*
38+
* Note: `focus` is no longer accepted — `POST /spans/query` always returns
39+
* flat spans. For trace-tree views, use `fetchPreviewTrace` instead.
3040
*/
3141
export interface TraceQueryParams {
3242
size?: number
33-
focus?: "trace" | "span" | "chat"
3443
format?: string
3544
filter?: string | Record<string, unknown>
3645
oldest?: string
@@ -40,26 +49,27 @@ export interface TraceQueryParams {
4049
}
4150

4251
/**
43-
* Fetch preview traces/spans from the API.
52+
* Fetch spans from the API (flat list).
53+
*
54+
* Calls `POST /spans/query` which always returns a flat `SpansResponse`.
55+
* For trace-tree views, use `fetchPreviewTrace` instead.
4456
*
4557
* @param params - Query parameters for filtering
4658
* @param appId - Application ID (optional)
4759
* @param projectId - Project ID (required)
48-
* @returns API response with spans (validated)
60+
* @returns Validated SpansResponse
4961
*/
5062
export async function fetchAllPreviewTraces(
5163
params: TraceQueryParams = {},
5264
appId: string,
5365
projectId: string,
54-
): Promise<SpansResponse | TracesResponse | null> {
66+
): Promise<SpansResponse | null> {
5567
const baseUrl = getAgentaApiUrl()
5668

57-
// Build query parameters
5869
const queryParams = new URLSearchParams()
5970
if (projectId) queryParams.set("project_id", projectId)
6071
if (appId) queryParams.set("application_id", appId)
6172

62-
// Build request payload
6373
const payload: Record<string, unknown> = {}
6474
Object.entries(params).forEach(([key, value]) => {
6575
if (value === undefined || value === null) return
@@ -71,68 +81,71 @@ export async function fetchAllPreviewTraces(
7181
} catch {
7282
payload.filter = value
7383
}
84+
} else if (key === "focus") {
85+
// `focus` is no longer accepted by POST /spans/query — skip it.
86+
return
7487
} else {
7588
payload[key] = value
7689
}
7790
})
7891

7992
const response = await axios.post(
80-
`${baseUrl}/tracing/spans/query?${queryParams.toString()}`,
93+
`${baseUrl}/spans/query?${queryParams.toString()}`,
8194
payload,
8295
)
8396

84-
// Try parsing as SpansResponse first (spans array format)
85-
const spansResult = spansResponseSchema.safeParse(response.data)
86-
if (spansResult.success) {
87-
return spansResult.data
88-
}
89-
90-
// Fall back to TracesResponse (traces record format)
91-
return safeParseWithLogging(tracesResponseSchema, response.data, "[fetchAllPreviewTraces]")
97+
return safeParseWithLogging(spansResponseSchema, response.data, "[fetchAllPreviewTraces]")
9298
}
9399

94100
/**
95-
* Fetch a single trace by ID.
101+
* Fetch a single trace by ID (with trace-tree structure).
102+
*
103+
* Calls `GET /traces/{id}` which returns a `TraceResponse` with a single
104+
* `trace` object containing `trace_id` and a `spans` record.
96105
*
97106
* @param traceId - Trace ID to fetch
98107
* @param projectId - Project ID
99-
* @returns Trace span data (validated)
108+
* @returns Validated TraceResponse
100109
*/
101110
export async function fetchPreviewTrace(
102111
traceId: string,
103112
projectId: string,
104-
): Promise<TracesResponse | null> {
113+
): Promise<TraceResponse | null> {
105114
const baseUrl = getAgentaApiUrl()
106115

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

110119
const response = await axios.get(
111-
`${baseUrl}/tracing/traces/${traceId}?${queryParams.toString()}`,
120+
`${baseUrl}/traces/${traceId}?${queryParams.toString()}`,
112121
)
113122

114-
// API returns TracesResponse format with count and traces record
115-
return safeParseWithLogging(tracesResponseSchema, response.data, "[fetchPreviewTrace]")
123+
return safeParseWithLogging(traceResponseSchema, response.data, "[fetchPreviewTrace]")
116124
}
117125

118126
/**
119127
* Delete a trace by ID.
120128
*
129+
* Calls `DELETE /traces/{id}`.
130+
*
121131
* @param traceId - Trace ID to delete
122132
* @param projectId - Project ID
123-
* @returns Delete response
133+
* @returns Validated TraceIdResponse
124134
*/
125-
export async function deletePreviewTrace(traceId: string, projectId: string): Promise<unknown> {
135+
export async function deletePreviewTrace(
136+
traceId: string,
137+
projectId: string,
138+
): Promise<TraceIdResponse | null> {
126139
const baseUrl = getAgentaApiUrl()
127140

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

131144
const response = await axios.delete(
132-
`${baseUrl}/tracing/traces/${traceId}?${queryParams.toString()}`,
145+
`${baseUrl}/traces/${traceId}?${queryParams.toString()}`,
133146
)
134147

135-
return response.data
148+
return safeParseWithLogging(traceIdResponseSchema, response.data, "[deletePreviewTrace]")
136149
}
137150

138151
/**
@@ -155,14 +168,16 @@ export interface SessionQueryParams {
155168
/**
156169
* Fetch sessions with filtering and pagination.
157170
*
171+
* Calls `POST /spans/sessions/query`.
172+
*
158173
* @param params - Session query parameters
159174
* @param projectId - Project ID
160-
* @returns Session list response
175+
* @returns Validated SessionIdsResponse
161176
*/
162177
export async function fetchSessions(
163178
params: SessionQueryParams,
164179
projectId: string,
165-
): Promise<unknown> {
180+
): Promise<SessionIdsResponse | null> {
166181
const baseUrl = getAgentaApiUrl()
167182

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

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

174-
// Initialize windowing if it doesn't exist but we have a cursor
175189
if (params.windowing || params.cursor) {
176190
payload.windowing = {...(params.windowing || {})}
177-
178-
// If cursor is provided, it goes into windowing.next
179191
if (params.cursor) {
180192
;(payload.windowing as Record<string, unknown>).next = params.cursor
181193
}
@@ -185,15 +197,14 @@ export async function fetchSessions(
185197
payload.filter = params.filter
186198
}
187199

188-
// Add realtime parameter (true = latest/unstable, false/undefined = all/stable)
189200
if (params.realtime !== undefined) {
190201
payload.realtime = params.realtime
191202
}
192203

193204
const response = await axios.post(
194-
`${baseUrl}/tracing/sessions/query?${queryParams.toString()}`,
205+
`${baseUrl}/spans/sessions/query?${queryParams.toString()}`,
195206
payload,
196207
)
197208

198-
return response.data
209+
return safeParseWithLogging(sessionIdsResponseSchema, response.data, "[fetchSessions]")
199210
}

0 commit comments

Comments
 (0)