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
6 changes: 6 additions & 0 deletions .changeset/public-trace-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@voltagent/core": patch
"@voltagent/sdk": patch
---

Add a VoltOps observability trace list API for loading persisted traces with project keys.
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export {
trace,
context,
} from "./observability";
export type {
VoltOpsObservabilityApi,
VoltOpsObservabilityTrace,
VoltOpsTraceListOptions,
VoltOpsTraceListResponse,
VoltOpsTraceSortOrder,
} from "./voltops";
export { TRIGGER_CONTEXT_KEY } from "./observability/context-keys";
export { SERVERLESS_ENV_CONTEXT_KEY } from "./context-keys";
export { createTriggers } from "./triggers/dsl";
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/voltops/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,46 @@ describe("VoltOpsClient", () => {
});
});

describe("observability client", () => {
it("lists traces with API keys, filters, and pagination", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
{ trace_id: "trace-1", project_id: "project-1", start_time: "2026-04-27T00:00:00Z" },
],
total: 1,
pageCount: 1,
}),
});
const observabilityClient = new VoltOpsClient({
...mockOptions,
fetch: fetchMock as unknown as typeof fetch,
});

const result = await observabilityClient.observability.traces.list({
limit: 10,
offset: 20,
search: "checkout",
environments: ["dev", "prod"],
sortOrder: "desc",
});

expect(fetchMock).toHaveBeenCalledWith(
"https://api.voltops.com/api/public/otel/v1/traces?limit=10&offset=20&search=checkout&environments=dev%2Cprod&sortOrder=desc",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
"X-Public-Key": "pk_test_key",
"X-Secret-Key": "sk_test_key",
}),
body: undefined,
}),
);
expect(result.total).toBe(1);
});
});

describe("actions client", () => {
const originalFetch = globalThis.fetch;
let fetchMock: ReturnType<typeof vi.fn>;
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/voltops/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ import type {
VoltOpsFeedbackCreateInput,
VoltOpsFeedbackToken,
VoltOpsFeedbackTokenCreateInput,
VoltOpsObservabilityApi,
VoltOpsPromptManager,
VoltOpsScorerSummary,
VoltOpsTraceListOptions,
VoltOpsTraceListResponse,
} from "./types";

/**
Expand All @@ -76,6 +79,7 @@ export class VoltOpsClient implements IVoltOpsClient {
public readonly managedMemory: ManagedMemoryVoltOpsClient;
public readonly actions: VoltOpsActionsClient;
public readonly evals: VoltOpsEvalsApi;
public readonly observability: VoltOpsObservabilityApi;
private readonly logger: Logger;

private get fetchImpl(): typeof fetch {
Expand Down Expand Up @@ -115,6 +119,11 @@ export class VoltOpsClient implements IVoltOpsClient {
create: this.createEvalScorer.bind(this),
},
};
this.observability = {
traces: {
list: this.listObservabilityTraces.bind(this),
},
};
Comment on lines +122 to +126
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

isObservabilityEnabled() semantics now conflict with the new observability client surface.

The client now exposes observability.traces.list, but isObservabilityEnabled() still always returns false. This can mislead callers that gate behavior on that method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/voltops/client.ts` around lines 122 - 126,
isObservabilityEnabled() currently always returns false while the client exposes
observability.traces.list (bound to listObservabilityTraces), causing
inconsistent semantics; update the isObservabilityEnabled() implementation to
return true when the observability surface is actually present (e.g., check that
this.observability?.traces?.list is a function) so callers gating behavior on
isObservabilityEnabled() reflect the new observability client; ensure the check
references the existing listObservabilityTraces binding
(observability.traces.list) rather than hardcoding false.


// Check if keys are valid (not empty and have correct prefixes)
const hasValidKeys =
Expand Down Expand Up @@ -381,6 +390,16 @@ export class VoltOpsClient implements IVoltOpsClient {
return this.normalizeScorerSummary(response);
}

private async listObservabilityTraces(
options: VoltOpsTraceListOptions = {},
): Promise<VoltOpsTraceListResponse> {
const query = this.buildQueryString(options as Record<string, unknown>);
return await this.request<VoltOpsTraceListResponse>(
"GET",
`/api/public/otel/v1/traces${query}`,
);
}

private async request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
const url = `${this.options.baseUrl.replace(/\/$/, "")}${endpoint}`;
const headers: Record<string, string> = {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/voltops/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export type {
VoltOpsFeedbackExpiresIn,
VoltOpsFeedbackToken,
VoltOpsFeedbackTokenCreateInput,
VoltOpsObservabilityApi,
VoltOpsObservabilityTrace,
VoltOpsTraceListOptions,
VoltOpsTraceListResponse,
VoltOpsTraceSortOrder,
VoltOpsActionExecutionResult,
VoltOpsAirtableCreateRecordParams,
VoltOpsAirtableUpdateRecordParams,
Expand Down
84 changes: 84 additions & 0 deletions packages/core/src/voltops/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,87 @@ export interface VoltOpsScorerSummary {
updatedAt: string;
}

export type VoltOpsTraceSortOrder = "asc" | "desc";

export interface VoltOpsTraceListOptions {
agentId?: string | string[];
model?: string | string[];
traceId?: string;
limit?: number;
offset?: number;
sortBy?: string;
sortOrder?: VoltOpsTraceSortOrder;
environments?: string | string[];
status?: string | string[];
level?: string | string[];
tags?: string | string[];
userId?: string;
startDate?: string | Date;
endDate?: string | Date;
minTokens?: number;
maxTokens?: number;
minCost?: number;
maxCost?: number;
minDuration?: number;
maxDuration?: number;
search?: string;
conversationId?: string;
entityType?: string | string[];
promptId?: string;
promptVersion?: string;
feedbackKey?: string | string[];
feedbackScore?: number;
minFeedbackScore?: number;
maxFeedbackScore?: number;
feedbackValue?: string;
feedbackSourceType?: string | string[];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export interface VoltOpsObservabilityTrace {
trace_id: string;
project_id: string;
agent_id?: string;
entity_type?: string;
user_id?: string | null;
conversation_id?: string | null;
start_time: string;
end_time?: string | null;
status?: string | null;
input?: unknown;
output?: unknown;
usage?: unknown;
metadata?: Record<string, unknown> | null;
tags?: string[] | null;
model?: string | null;
level?: string | null;
status_message?: string | null;
service_name?: string;
service_version?: string | null;
span_count?: number;
error_count?: number;
duration_ms?: number | null;
resource_attributes?: Record<string, unknown> | null;
created_at?: string;
latest_feedback_score?: number | null;
latest_feedback_key?: string | null;
latest_feedback_comment?: string | null;
latest_feedback_at?: string | null;
[key: string]: unknown;
}

export interface VoltOpsTraceListResponse {
data: VoltOpsObservabilityTrace[];
total: number;
pageCount: number;
subscription?: unknown;
}

export interface VoltOpsObservabilityApi {
traces: {
list(options?: VoltOpsTraceListOptions): Promise<VoltOpsTraceListResponse>;
};
}

/**
* Main VoltOps client interface
*/
Expand All @@ -972,6 +1053,9 @@ export interface VoltOpsClient {
/** Evaluations API surface */
evals: VoltOpsEvalsApi;

/** Observability read API surface */
observability: VoltOpsObservabilityApi;

Comment on lines +1056 to +1058
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Observability comments are now contradictory to the interface surface.

VoltOpsClient now exposes observability, but nearby comments still state observability is removed. Please align the comments to avoid misleading integrators.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/voltops/types.ts` around lines 1056 - 1058, Update the
misleading comment near the VoltOpsClient observability declaration so it
reflects the actual API surface: remove or rewrite the text that says
observability was removed and instead document that VoltOpsClient exposes an
observability property of type VoltOpsObservabilityApi (observability) and
briefly indicate its purpose (observability read API surface). Locate the
comment adjacent to the observability: VoltOpsObservabilityApi; declaration and
make the comment consistent with the current interface.

/** Create a feedback token for the given trace */
createFeedbackToken(input: VoltOpsFeedbackTokenCreateInput): Promise<VoltOpsFeedbackToken>;

Expand Down
37 changes: 37 additions & 0 deletions packages/sdk/src/client/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,41 @@ describe("VoltAgentCoreAPI", () => {
expect(response).toEqual(apiResponse);
});
});

describe("observability traces", () => {
it("requests traces with search and pagination filters", async () => {
const apiResponse = {
data: [
{
trace_id: "trace-1",
project_id: "project-1",
start_time: "2026-04-27T00:00:00Z",
},
],
total: 1,
pageCount: 1,
};

fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/json" }),
json: async () => apiResponse,
});

const api = new VoltAgentCoreAPI(defaultOptions);
const response = await api.observability.traces.list({
limit: 25,
offset: 50,
search: "workflow",
environments: ["dev", "prod"],
});

expect(fetchMock).toHaveBeenCalledWith(
"https://api.voltagent.dev/api/public/otel/v1/traces?limit=25&offset=50&search=workflow&environments=dev%2Cprod",
expect.objectContaining({ method: "GET" }),
);
expect(response).toEqual(apiResponse);
});
});
});
40 changes: 40 additions & 0 deletions packages/sdk/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import type {
ListEvalDatasetItemsOptions,
ListEvalExperimentsOptions,
VoltAgentClientOptions,
VoltOpsObservabilityApi,
VoltOpsTraceListOptions,
VoltOpsTraceListResponse,
} from "../types";

const DEFAULT_TIMEOUT_MS = 30_000;
Expand Down Expand Up @@ -80,6 +83,7 @@ export class VoltAgentCoreAPI {
private readonly timeout: number;
public readonly actions: VoltOpsActionsClient;
public readonly evals: VoltAgentEvalsAPI;
public readonly observability: VoltOpsObservabilityApi;

constructor(options: VoltAgentClientOptions) {
const baseUrl = (options.baseUrl ?? DEFAULT_API_BASE_URL).trim();
Expand Down Expand Up @@ -120,6 +124,11 @@ export class VoltAgentCoreAPI {
create: this.createEvalExperiment.bind(this),
},
};
this.observability = {
traces: {
list: this.listObservabilityTraces.bind(this),
},
};
}

private async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
Expand Down Expand Up @@ -177,6 +186,30 @@ export class VoltAgentCoreAPI {
return data as T;
}

private buildQueryString(params: Record<string, unknown>): string {
const searchParams = new URLSearchParams();

for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) {
continue;
}

if (Array.isArray(value)) {
if (value.length === 0) {
continue;
}
searchParams.set(key, value.join(","));
} else if (value instanceof Date) {
searchParams.set(key, value.toISOString());
} else {
searchParams.set(key, String(value));
}
}

const query = searchParams.toString();
return query ? `?${query}` : "";
}

public async sendRequest(path: string, init?: RequestInit): Promise<Response> {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const url = `${this.baseUrl}${normalizedPath}`;
Expand All @@ -199,6 +232,13 @@ export class VoltAgentCoreAPI {
});
}

private async listObservabilityTraces(
options: VoltOpsTraceListOptions = {},
): Promise<VoltOpsTraceListResponse> {
const query = this.buildQueryString(options as Record<string, unknown>);
return await this.request<VoltOpsTraceListResponse>(`/api/public/otel/v1/traces${query}`);
}

private async appendEvalResults(
runId: string,
payload: AppendEvalRunResultsRequest,
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export type {
RagSearchKnowledgeBaseRequest,
RagSearchKnowledgeBaseResponse,
RagSearchKnowledgeBaseResult,
VoltOpsObservabilityApi,
VoltOpsObservabilityTrace,
VoltOpsTraceListOptions,
VoltOpsTraceListResponse,
VoltOpsTraceSortOrder,
} from "./types";
export type {
VoltOpsActionExecutionResult,
Expand Down
Loading
Loading