diff --git a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/assets/DeployVariantModalContent/index.tsx b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/assets/DeployVariantModalContent/index.tsx index bf8f3830da..141c3b2d0b 100644 --- a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/assets/DeployVariantModalContent/index.tsx +++ b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/assets/DeployVariantModalContent/index.tsx @@ -51,7 +51,8 @@ const DeployVariantModalContent = ({variantName, revision, isLoading}: any) => { return (
- Select an environment to deploy {variantName}{" "} + Select environments to deploy{" "} + {variantName}{" "} {typeof revision !== "undefined" && ( )} @@ -59,7 +60,7 @@ const DeployVariantModalContent = ({variantName, revision, isLoading}: any) => { { @@ -76,7 +77,11 @@ const DeployVariantModalContent = ({variantName, revision, isLoading}: any) => { onRow={(env) => ({ className: "cursor-pointer", onClick: () => { - setSelectedEnvName([env.name]) + setSelectedEnvName((prev) => + prev.includes(env.name) + ? prev.filter((n) => n !== env.name) + : [...prev, env.name], + ) }, })} /> diff --git a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/index.tsx b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/index.tsx index d646bbeb16..b786ff21b7 100644 --- a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/index.tsx +++ b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/index.tsx @@ -84,11 +84,15 @@ const DeployVariantModal = ({ if (result?.error) message.error(result.error) return } - const env = result.env as string + const envs = result.envs as string[] onClose() - message.success(`Published ${variantName} to ${env}`) - posthog?.capture?.("app_deployed", {app_id: appId, environment: env}) + message.success(`Published ${variantName} to ${envs.join(", ")}`) + posthog?.capture?.("app_deployed", {app_id: appId, environment: envs.join(",")}) recordWidgetEvent("variant_deployed") + if (result.error) { + // Partial success: some environments failed + message.warning(result.error) + } }, [submitDeploy, onClose, variantName, appId, posthog, recordWidgetEvent]) return ( diff --git a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/store/deployVariantModalStore.ts b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/store/deployVariantModalStore.ts index 2ac723f207..98afda9c39 100644 --- a/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/store/deployVariantModalStore.ts +++ b/web/oss/src/components/Playground/Components/Modals/DeployVariantModal/store/deployVariantModalStore.ts @@ -63,7 +63,8 @@ export const deployResetAtom = atom(null, (get, set) => { }) // Async submit action. Optional overrides allow providing ids directly. -// Returns { ok: boolean, env?: string, error?: string } +// Deploys to all selected environments. +// Returns { ok: boolean, envs?: string[], error?: string } export const deploySubmitAtom = atom( null, async (get, set, overrides?: {parentVariantId?: string | null; revisionId?: string | null}) => { @@ -73,7 +74,7 @@ export const deploySubmitAtom = atom( parentVariantId: overrides?.parentVariantId ?? baseState.parentVariantId ?? null, revisionId: overrides?.revisionId ?? baseState.revisionId ?? null, } - const selectedEnvName = get(deploySelectedEnvAtom) + const selectedEnvNames = get(deploySelectedEnvAtom) const note = get(deployNoteAtom) const appId = get(routerAppIdAtom) const {mutateAsync: publish} = get(publishMutationAtom) @@ -82,12 +83,11 @@ export const deploySubmitAtom = atom( console.debug("[DeployModal] submit:start", { state, overrides, - selectedEnvName, + selectedEnvNames, note, }) - const env = selectedEnvName[0] - if (!env) { + if (!selectedEnvNames.length) { console.debug("[DeployModal] submit:fail", {reason: "no_env_selected"}) return {ok: false, error: "No environment selected"} } @@ -116,36 +116,49 @@ export const deploySubmitAtom = atom( const workflowEntity = workflows.find((w) => w.id === workflowId) const applicationSlug = workflowEntity?.slug || undefined - try { - console.debug("[DeployModal] submit:publish", { - revisionId, - env, - workflowData: workflowData - ? { - workflow_id: workflowData.workflow_id, - workflow_variant_id: workflowData.workflow_variant_id, - slug: workflowData.slug, - resolvedVariantSlug, - applicationSlug, - } - : null, - }) - await publish({ - revisionId, - environmentSlug: env, - applicationId: workflowId || appId || "", - workflowVariantId: workflowData?.workflow_variant_id ?? undefined, - variantSlug: resolvedVariantSlug ?? undefined, - applicationSlug, - revisionVersion: workflowData?.version ?? undefined, - note, - }) - console.debug("[DeployModal] submit:success", {env}) - return {ok: true, env} - } catch (e: unknown) { - const errorMessage = e instanceof Error ? e.message : "Failed to deploy" - console.debug("[DeployModal] submit:error", {error: e}) - return {ok: false, error: errorMessage} + const succeeded: string[] = [] + const errors: string[] = [] + + for (const env of selectedEnvNames) { + try { + console.debug("[DeployModal] submit:publish", { + revisionId, + env, + workflowData: workflowData + ? { + workflow_id: workflowData.workflow_id, + workflow_variant_id: workflowData.workflow_variant_id, + slug: workflowData.slug, + resolvedVariantSlug, + applicationSlug, + } + : null, + }) + await publish({ + revisionId, + environmentSlug: env, + applicationId: workflowId || appId || "", + workflowVariantId: workflowData?.workflow_variant_id ?? undefined, + variantSlug: resolvedVariantSlug ?? undefined, + applicationSlug, + revisionVersion: workflowData?.version ?? undefined, + note, + }) + console.debug("[DeployModal] submit:success", {env}) + succeeded.push(env) + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : "Failed to deploy" + console.debug("[DeployModal] submit:error", {env, error: e}) + errors.push(`${env}: ${errorMessage}`) + } + } + + if (succeeded.length && !errors.length) { + return {ok: true, envs: succeeded} + } else if (succeeded.length && errors.length) { + return {ok: true, envs: succeeded, error: `Failed for: ${errors.join("; ")}`} + } else { + return {ok: false, error: errors.join("; ")} } }, ) diff --git a/web/oss/src/services/tracing/api/index.ts b/web/oss/src/services/tracing/api/index.ts index 2cfb68924c..3ff885a4c3 100644 --- a/web/oss/src/services/tracing/api/index.ts +++ b/web/oss/src/services/tracing/api/index.ts @@ -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) @@ -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 } @@ -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) @@ -109,6 +112,8 @@ export const fetchAllPreviewTracesWithMeta = async ( } catch { payload.filter = value } + } else if (key === "focus") { + return } else { payload[key] = value } @@ -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) @@ -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"}) @@ -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) diff --git a/web/packages/agenta-annotation/src/state/controllers/annotationFormController.ts b/web/packages/agenta-annotation/src/state/controllers/annotationFormController.ts index 993117198e..d14345c36a 100644 --- a/web/packages/agenta-annotation/src/state/controllers/annotationFormController.ts +++ b/web/packages/agenta-annotation/src/state/controllers/annotationFormController.ts @@ -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] + })() const rawSpans = traceEntry?.spans ? Object.values(traceEntry.spans) : [] const spans = rawSpans.filter( (span): span is TraceSpan => diff --git a/web/packages/agenta-entities/src/trace/api/api.ts b/web/packages/agenta-entities/src/trace/api/api.ts index 0028971021..a566b80da1 100644 --- a/web/packages/agenta-entities/src/trace/api/api.ts +++ b/web/packages/agenta-entities/src/trace/api/api.ts @@ -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) * ``` */ @@ -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 oldest?: string @@ -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 { +): Promise { 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 = {} Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null) return @@ -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 { +): Promise { 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 { +export async function deletePreviewTrace( + traceId: string, + projectId: string, +): Promise { 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]") } /** @@ -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 { +): Promise { const baseUrl = getAgentaApiUrl() const queryParams = new URLSearchParams() @@ -171,11 +186,8 @@ export async function fetchSessions( const payload: Record = {} - // 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).next = params.cursor } @@ -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]") } diff --git a/web/packages/agenta-entities/src/trace/api/helpers.ts b/web/packages/agenta-entities/src/trace/api/helpers.ts index 17d0756fca..c4c12d24d4 100644 --- a/web/packages/agenta-entities/src/trace/api/helpers.ts +++ b/web/packages/agenta-entities/src/trace/api/helpers.ts @@ -15,20 +15,33 @@ * ``` */ -import type {SpansResponse, TracesResponse, TraceSpanNode, TraceSpan} from "../core" +import type { + SpansResponse, + TraceResponse, + TracesResponse, + TraceSpanNode, + TraceSpan, +} from "../core" /** - * Type guard for TracesResponse (response with traces object) + * Type guard for TracesResponse (legacy response with traces record object). */ export const isTracesResponse = (data: unknown): data is TracesResponse => { return typeof data === "object" && data !== null && "traces" in data } /** - * Type guard for SpansResponse (response with spans array) + * Type guard for SpansResponse (response with flat spans array). */ export const isSpansResponse = (data: unknown): data is SpansResponse => { - return typeof data === "object" && data !== null && "spans" in data + return typeof data === "object" && data !== null && "spans" in data && Array.isArray((data as any).spans) +} + +/** + * Type guard for TraceResponse (new single-trace response from GET /traces/{id}). + */ +export const isTraceResponse = (data: unknown): data is TraceResponse => { + return typeof data === "object" && data !== null && "trace" in data } /** @@ -76,43 +89,59 @@ export const sortSpansByStartTime = < } /** - * Transform a TracesResponse into a tree of TraceSpanNodes. + * Build a tree of TraceSpanNodes from a spans record. * - * @param data - TracesResponse from the API - * @returns Array of TraceSpanNode trees + * Shared by both the legacy `TracesResponse` path and the new `TraceResponse` + * path — the inner span structure is identical. */ -export const transformTracesResponseToTree = (data: TracesResponse): TraceSpanNode[] => { - const buildTree = (spans: Record | unknown[]): TraceSpanNode[] => { - if (!spans) { - return [] - } +const buildSpanTree = (spans: Record | unknown[]): TraceSpanNode[] => { + if (!spans) { + return [] + } - const spanArray = Object.values(spans).flatMap((span: unknown) => { - if (Array.isArray(span)) { - return buildTree(span) - } + const spanArray = Object.values(spans).flatMap((span: unknown) => { + if (Array.isArray(span)) { + return buildSpanTree(span) + } - const spanObj = span as TraceSpan & {spans?: Record} - const node: TraceSpanNode = { - ...spanObj, - } + const spanObj = span as TraceSpan & {spans?: Record} + const node: TraceSpanNode = { + ...spanObj, + } - if (spanObj?.spans && Object.keys(spanObj.spans).length > 0) { - node.children = buildTree(spanObj.spans) - } + if (spanObj?.spans && Object.keys(spanObj.spans).length > 0) { + node.children = buildSpanTree(spanObj.spans) + } - return node - }) + return node + }) - // Sort spans at this hierarchy level by start_time - return sortSpansByStartTime(spanArray) - } + return sortSpansByStartTime(spanArray) +} +/** + * Transform a TracesResponse (legacy, with `traces` record) into a tree. + * + * @param data - TracesResponse from the API + * @returns Array of TraceSpanNode trees + */ +export const transformTracesResponseToTree = (data: TracesResponse): TraceSpanNode[] => { return Object.values(data.traces).flatMap((trace: {spans?: Record}) => - buildTree(trace.spans || {}), + buildSpanTree(trace.spans || {}), ) } +/** + * Transform a TraceResponse (new single-trace from GET /traces/{id}) into a tree. + * + * @param data - TraceResponse from the API + * @returns Array of TraceSpanNode trees (single trace) + */ +export const transformTraceResponseToTree = (data: TraceResponse): TraceSpanNode[] => { + if (!data.trace?.spans) return [] + return buildSpanTree(data.trace.spans) +} + /** * Enhance trace span nodes with key and invocationIds for tree rendering. * diff --git a/web/packages/agenta-entities/src/trace/api/index.ts b/web/packages/agenta-entities/src/trace/api/index.ts index 6ca65af063..6247740f20 100644 --- a/web/packages/agenta-entities/src/trace/api/index.ts +++ b/web/packages/agenta-entities/src/trace/api/index.ts @@ -18,7 +18,9 @@ export { export { isTracesResponse, isSpansResponse, + isTraceResponse, sortSpansByStartTime, transformTracesResponseToTree, + transformTraceResponseToTree, transformTracingResponse, } from "./helpers" diff --git a/web/packages/agenta-entities/src/trace/core/index.ts b/web/packages/agenta-entities/src/trace/core/index.ts index 93bdbb0a66..6be69880b4 100644 --- a/web/packages/agenta-entities/src/trace/core/index.ts +++ b/web/packages/agenta-entities/src/trace/core/index.ts @@ -37,6 +37,12 @@ export { type TracesResponse, spansResponseSchema, type SpansResponse, + traceResponseSchema, + type TraceResponse, + traceIdResponseSchema, + type TraceIdResponse, + sessionIdsResponseSchema, + type SessionIdsResponse, type TraceListResponse, // Parsing utilities parseTraceSpan, diff --git a/web/packages/agenta-entities/src/trace/core/schema.ts b/web/packages/agenta-entities/src/trace/core/schema.ts index 520d190e72..5c2a4438b6 100644 --- a/web/packages/agenta-entities/src/trace/core/schema.ts +++ b/web/packages/agenta-entities/src/trace/core/schema.ts @@ -233,6 +233,52 @@ export const spansResponseSchema = z.object({ }) export type SpansResponse = z.infer +/** + * Response schema for `GET /traces/{id}`. + * + * The new endpoint returns a single `trace` object (not a `traces` record). + * `spans` inside the trace is a flat record keyed by span_id. + */ +export const traceResponseSchema = z.object({ + count: z.number().optional().default(0), + trace: z + .object({ + trace_id: z.string().optional().nullable(), + spans: z.record(z.string(), traceSpanSchema).optional().nullable(), + }) + .optional() + .nullable(), +}) +export type TraceResponse = z.infer + +/** + * Response schema for `DELETE /traces/{id}`. + */ +export const traceIdResponseSchema = z.object({ + count: z.number(), + trace_id: z.string().optional().nullable(), +}) +export type TraceIdResponse = z.infer + +/** + * Response schema for `POST /spans/sessions/query`. + */ +export const sessionIdsResponseSchema = z.object({ + count: z.number(), + session_ids: z.array(z.string()), + windowing: z + .object({ + next: z.string().optional().nullable(), + oldest: z.string().optional().nullable(), + newest: z.string().optional().nullable(), + limit: z.number().optional().nullable(), + order: z.enum(["ascending", "descending"]).optional().nullable(), + }) + .optional() + .nullable(), +}) +export type SessionIdsResponse = z.infer + // Combined response type for list queries export interface TraceListResponse { traces: TraceSpanNode[] diff --git a/web/packages/agenta-entities/src/trace/index.ts b/web/packages/agenta-entities/src/trace/index.ts index 6be72564cd..43e9083315 100644 --- a/web/packages/agenta-entities/src/trace/index.ts +++ b/web/packages/agenta-entities/src/trace/index.ts @@ -84,6 +84,12 @@ export { type TracesResponse, spansResponseSchema, type SpansResponse, + traceResponseSchema, + type TraceResponse, + traceIdResponseSchema, + type TraceIdResponse, + sessionIdsResponseSchema, + type SessionIdsResponse, type TraceListResponse, } from "./core" @@ -114,8 +120,10 @@ export { export { isTracesResponse, isSpansResponse, + isTraceResponse, sortSpansByStartTime, transformTracesResponseToTree, + transformTraceResponseToTree, transformTracingResponse, } from "./api"